Skip to content

Commit 0fabfa4

Browse files
chore(swingset): use realistic faker data for chaos mode (#10409)
1 parent 1913b28 commit 0fabfa4

20 files changed

Lines changed: 232 additions & 119 deletions

‎.changeset/swingset-faker-chaos.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/swingset/CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ These require reading several files together; the `README.md` covers the step-by
3737

3838
- **Every story renders inside `MosaicProvider`.** `StoryPreview` (the MDX `<Preview>`) renders a named story with the playground's knob values as props and exposes a Reset button. `StoryEmbed` (the MDX `<Story>`) renders a single static variation with default knob values and no controls.
3939
- The header's **RTL** switch (`DirectionToggle`, state in `DirectionProvider`) sets `dir` on `<html>`. It has to sit that high because popover and menu popups portal to `<body>`, so a `dir` on the preview container would never reach them; swingset's own chrome flips along with the story.
40-
- The header's **Chaos** switch (`ChaosToggle`, state in `ChaosProvider`) stress-tests layouts with worst-case copy and data. `StoryMosaicProvider` (used by both story renderers) passes `chaosLocalization` from `lib/chaos.ts` as the `localization` prop, and remounts the story on toggle so fixture state reseeds. That catalog is generated from `mosaicMessages`, so new messages need no wiring. Fixtures opt their seed data in with `useChaosFixture(data, toChaos)`, building the chaos version from the helpers in `lib/chaos.ts` (`chaosName`, `chaosEmail`, `chaosText`, `chaosRows`). When you add a fixture, wrap its seed data the same way. Stories that mount their own `MosaicProvider` (the Localization page) ignore the catalog.
40+
- The header's **Chaos** switch (`ChaosToggle`, state in `ChaosProvider`) stress-tests layouts with worst-case data that still reads as real: long names, companies, emails and places across scripts and RTL, plus long row counts. `StoryMosaicProvider` (used by both story renderers) remounts the story on toggle so fixture state reseeds. UI copy stays the built-in English. Fixtures opt their seed data in with `useChaosFixture(data, toChaos)`, building the chaos version from the helpers in `lib/chaos.ts`: seeded `@faker-js/faker` generators (`chaosName`, `chaosPerson`, `chaosUsername`, `chaosEmail`, `chaosCompany`, `chaosSlug`, `chaosPhone`, `chaosPlace`), which take the longest of several samples across locales, plus `chaosRole`, `chaosPick` for hand-written lists faker can't supply, and `chaosRows`. Never stretch copy with filler; pick a longer value someone would actually type. When you add a fixture, wrap its seed data the same way.
4141

4242
- **The prop table is the knob surface.** `PropTable` (MDX `<PropTable>`) derives rows from `meta.styles._variants`/`_defaultVariants`, then appends the `className` + `style` escape-hatch rows every Mosaic component accepts. Each variant row renders a `KnobControl` in its **Value** column, seeded with the prop's default and bound to the playground context. The escape-hatch rows and `extra` stay static.
4343

‎packages/swingset/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@clerk/mosaic": "workspace:*",
1515
"@clerk/nextjs": "workspace:*",
1616
"@clerk/shared": "workspace:*",
17+
"@faker-js/faker": "^10.0.0",
1718
"@floating-ui/react": "catalog:repo",
1819
"@stylexjs/stylex": "0.19.0",
1920
"@tailwindcss/typography": "^0.5.19",

‎packages/swingset/src/components/ChaosProvider.tsx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import { MosaicProvider } from '@clerk/mosaic/mosaic-provider';
44
import * as React from 'react';
55

6-
import { chaosLocalization } from '@/lib/chaos';
7-
86
import { useDirection } from './DirectionProvider';
97

108
interface ChaosContextValue {
@@ -36,7 +34,6 @@ export function StoryMosaicProvider({ children }: { children: React.ReactNode })
3634
<MosaicProvider
3735
key={String(chaos)}
3836
direction={direction}
39-
localization={chaos ? chaosLocalization : undefined}
4037
>
4138
{children}
4239
</MosaicProvider>

‎packages/swingset/src/lib/chaos.ts‎

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,72 @@
1-
import type { MosaicLocalization } from '@clerk/mosaic/localization';
2-
import { mosaicMessages } from '@clerk/mosaic/localization';
3-
4-
const CHAOS_SUFFIX = 'Ŵïţħ ëẍţŕą łöñğ ţêẍţ ţħąţ ẅŕąṗṡ Pneumonoultramicroscopicsilicovolcanoconiosis';
5-
6-
const CHAOS_NAMES = [
7-
'Maximiliana-Alexandrina Wolfeschlegelsteinhausenbergerdorff-Montgomery',
8-
'X',
9-
'محمد بن عبد الرحمن آل سعود',
10-
'李小龍',
11-
'Zoë Ångström-Ñúñez',
12-
"Seán O'Brien-Smith 🦄🚀",
13-
'Z̴̢a̷̛l̸̨g̵̢o̶̧ ̷̨T̵̛e̸̢x̴̨t̶̛',
14-
'ALLCAPSNAMEWITHOUTANYSPACESTHATJUSTKEEPSGOINGANDGOINGANDGOING',
15-
'<b>Not Bold</b> & "Quoted"',
16-
];
1+
import type { Faker } from '@faker-js/faker';
2+
import { fakerAR, fakerDE, fakerFI, fakerFR, fakerJA, fakerPT_BR, fakerVI, fakerZH_CN } from '@faker-js/faker';
3+
4+
const LOCALES = [fakerDE, fakerAR, fakerFI, fakerJA, fakerPT_BR, fakerVI, fakerFR, fakerZH_CN];
5+
6+
const SAMPLES = 20;
177

18-
const CHAOS_EMAILS = [
19-
'maximiliana.alexandrina.wolfeschlegelsteinhausenbergerdorff+notifications@engineering.eu-west.very-long-company-name.example.com',
20-
'x@y.co',
21-
'first.last+tag+another-tag@xn--80ak6aa92e.com',
22-
'"quoted local part"@example.com',
23-
'用户@例子.广告',
24-
'ALL.CAPS.ADDRESS@EXAMPLE.COM',
8+
const ROLES = [
9+
{
10+
label: 'Billing administrator',
11+
description: 'Can manage the subscription, payment methods and invoices, and export billing history.',
12+
},
13+
{
14+
label: 'Customer success lead (EMEA)',
15+
description: 'Can view every member and organization setting, and invite people to regional workspaces.',
16+
},
17+
{
18+
label: 'Read-only auditor',
19+
description: 'Can view members, roles, API keys and audit logs, but cannot change anything.',
20+
},
2521
];
2622

27-
export function chaosText(text: string): string;
28-
export function chaosText(text: string | undefined): string | undefined;
29-
export function chaosText(text: string | undefined) {
30-
return text === undefined ? undefined : `${text} ${CHAOS_SUFFIX}`;
23+
export function chaosName(index: number) {
24+
return longest(index, f => f.person.fullName());
3125
}
3226

33-
export function chaosName(index: number) {
34-
return CHAOS_NAMES[index % CHAOS_NAMES.length];
27+
export function chaosPerson(index: number) {
28+
return {
29+
firstName: longest(index, f => f.person.firstName()),
30+
lastName: longest(index, f => f.person.lastName()),
31+
};
32+
}
33+
34+
export function chaosUsername(index: number) {
35+
return longest(index, f => f.internet.username());
3536
}
3637

3738
export function chaosEmail(index: number) {
38-
return CHAOS_EMAILS[index % CHAOS_EMAILS.length];
39+
return longest(index, f =>
40+
f.internet.email({
41+
firstName: f.person.firstName(),
42+
lastName: f.person.lastName(),
43+
provider: f.internet.domainName(),
44+
}),
45+
);
46+
}
47+
48+
export function chaosCompany(index: number) {
49+
return longest(index, f => f.company.name());
50+
}
51+
52+
export function chaosPhone(index: number) {
53+
return longest(index, f => f.phone.number({ style: 'international' }));
54+
}
55+
56+
export function chaosSlug(index: number) {
57+
return `${chaosPick(LOCALES, index).helpers.slugify(chaosCompany(index)).toLowerCase()}-177654156132154`;
58+
}
59+
60+
export function chaosPlace(index: number) {
61+
return longest(index, f => `${f.location.city()}, ${f.location.state()}, ${f.location.country()}`);
62+
}
63+
64+
export function chaosRole(index: number) {
65+
return chaosPick(ROLES, index);
66+
}
67+
68+
export function chaosPick<T>(values: readonly T[], index: number) {
69+
return values[index % values.length];
3970
}
4071

4172
export function chaosRows<T extends { id: string }>(items: readonly T[], count = 60) {
@@ -48,14 +79,8 @@ export function chaosRows<T extends { id: string }>(items: readonly T[], count =
4879
});
4980
}
5081

51-
type Stretched<T> = { [K in keyof T]: T[K] extends string ? string : Stretched<T[K]> };
52-
53-
function stretch<T extends object>(tree: T): Stretched<T> {
54-
const entries = Object.entries(tree).map(([key, value]) => [
55-
key,
56-
typeof value === 'string' ? chaosText(value) : stretch(value),
57-
]);
58-
return Object.fromEntries(entries) as Stretched<T>;
82+
function longest(index: number, make: (faker: Faker) => string) {
83+
const faker = chaosPick(LOCALES, index);
84+
faker.seed(index);
85+
return Array.from({ length: SAMPLES }, () => make(faker)).reduce((a, b) => (b.length > a.length ? b : a));
5986
}
60-
61-
export const chaosLocalization: MosaicLocalization = { messages: stretch(mosaicMessages) };

‎packages/swingset/src/stories/fixtures/api-keys-table.tsx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useLocale, useMessages } from '@clerk/mosaic/localization';
1313
import { useEffect, useRef, useState } from 'react';
1414

1515
import { useChaosFixture } from '@/components/ChaosProvider';
16-
import { chaosRows, chaosText } from '@/lib/chaos';
16+
import { chaosPick, chaosRows } from '@/lib/chaos';
1717

1818
interface FixtureAPIKey {
1919
id: string;
@@ -25,6 +25,15 @@ interface FixtureAPIKey {
2525

2626
const exampleTime = Date.now();
2727

28+
const chaosAPIKeyNames = [
29+
'Production webhook ingestion for the EU billing reconciliation pipeline',
30+
'Nightly data warehouse export (Snowflake to BigQuery)',
31+
'Customer support dashboard, read-only access',
32+
'Legacy v1 API: do not revoke until the mobile migration ships',
33+
'Zapier',
34+
'Internal tooling for the Growth and Lifecycle Marketing team',
35+
];
36+
2837
export const exampleAPIKeys: FixtureAPIKey[] = [
2938
'Web app',
3039
'Mobile app',
@@ -67,7 +76,7 @@ export function useAPIKeysTableFixture({
6776
const locale = useLocale();
6877
const messages = resolveAPIKeysTableMessages(useMessages('apiKeysTable'), subjectKind);
6978
const seed = useChaosFixture(initialKeys, items =>
70-
chaosRows(items).map(key => ({ ...key, name: chaosText(key.name) })),
79+
chaosRows(items).map((key, index) => ({ ...key, name: chaosPick(chaosAPIKeyNames, index) })),
7180
);
7281
const [items, setItems] = useState(seed);
7382
const [searchValue, setSearchValue] = useState('');

‎packages/swingset/src/stories/fixtures/invitations-table-tab.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useLocale } from '@clerk/mosaic/localization';
88
import { useRef, useState } from 'react';
99

1010
import { useChaosFixture } from '@/components/ChaosProvider';
11-
import { chaosEmail, chaosRows, chaosText } from '@/lib/chaos';
11+
import { chaosEmail, chaosRole, chaosRows } from '@/lib/chaos';
1212

1313
const exampleInvitations = [
1414
'ada',
@@ -36,7 +36,7 @@ export function useInvitationsTableFixture({ proposed = false, empty = false } =
3636
chaosRows(items).map((invitation, index) => ({
3737
...invitation,
3838
email: chaosEmail(index),
39-
roleLabel: chaosText(invitation.roleLabel),
39+
roleLabel: chaosRole(index).label,
4040
})),
4141
);
4242
const [items, setItems] = useState(empty ? [] : invitations);

‎packages/swingset/src/stories/fixtures/members-table-tab.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useLocale } from '@clerk/mosaic/localization';
88
import { useRef, useState } from 'react';
99

1010
import { useChaosFixture } from '@/components/ChaosProvider';
11-
import { chaosEmail, chaosName, chaosRows, chaosText } from '@/lib/chaos';
11+
import { chaosEmail, chaosName, chaosRole, chaosRows } from '@/lib/chaos';
1212

1313
const exampleRoles = [
1414
{ value: 'admin', label: 'Admin' },
@@ -38,7 +38,9 @@ const exampleMembers = [
3838

3939
export function useMembersTableFixture({ proposed = false, empty = false } = {}): MembersTableTabViewProps {
4040
const locale = useLocale();
41-
const roles = useChaosFixture(exampleRoles, items => items.map(role => ({ ...role, label: chaosText(role.label) })));
41+
const roles = useChaosFixture(exampleRoles, items =>
42+
items.map((role, index) => ({ ...role, label: chaosRole(index).label })),
43+
);
4244
const seed = useChaosFixture(exampleMembers, items =>
4345
chaosRows(items).map((member, index) => ({
4446
...member,

‎packages/swingset/src/stories/fixtures/organization-profile-invite-members.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { MouseEvent } from 'react';
55
import { useRef, useState } from 'react';
66

77
import { useChaosFixture } from '@/components/ChaosProvider';
8-
import { chaosText } from '@/lib/chaos';
8+
import { chaosRole } from '@/lib/chaos';
99

1010
const exampleRoles = [
1111
{ value: 'member', label: 'Member', description: 'Role with non-privileged permissions in the organization.' },
@@ -20,9 +20,7 @@ export function useInviteMembersFixture(): {
2020
} {
2121
const m = useMessages('organizationProfileInviteMembers');
2222
const locale = useLocale();
23-
const roles = useChaosFixture(exampleRoles, items =>
24-
items.map(role => ({ ...role, label: chaosText(role.label), description: chaosText(role.description) })),
25-
);
23+
const roles = useChaosFixture(exampleRoles, items => items.map((role, index) => ({ ...role, ...chaosRole(index) })));
2624
const toast = useToastManager();
2725
const trigger = useRef<HTMLButtonElement | null>(null);
2826
const [open, setOpen] = useState(false);

‎packages/swingset/src/stories/fixtures/organization-profile.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { SaveError } from '@clerk/mosaic/utils/errors';
1010
import { useState } from 'react';
1111

1212
import { useChaosFixture } from '@/components/ChaosProvider';
13-
import { chaosName } from '@/lib/chaos';
13+
import { chaosCompany, chaosSlug } from '@/lib/chaos';
1414

1515
import { APIKeysPanelExample, useAPIKeysTableFixture } from './api-keys-table';
1616
import { useInvitationsTableFixture } from './invitations-table-tab';
@@ -44,8 +44,8 @@ export interface OrganizationProfileFixtureOptions {
4444
export function useOrganizationProfileFixture({ failWith }: OrganizationProfileFixtureOptions = {}) {
4545
const [activePage, setActivePage] = useState<OrganizationProfileViewProps['activePage']>('general');
4646
const organization = useChaosFixture(exampleOrganization, () => ({
47-
name: chaosName(0),
48-
slug: 'a-really-long-organization-slug-that-never-seems-to-end-177654156132154',
47+
name: chaosCompany(0),
48+
slug: chaosSlug(0),
4949
memberCount: 1_234_567,
5050
}));
5151
const [name, setName] = useState(organization.name);

0 commit comments

Comments
 (0)