Skip to content

Commit c4755cc

Browse files
committed
test(e2e): make the Mosaic UserButton suite pass against a real instance
Six failures the first real run surfaced: - The page used SignedIn/SignedOut, which @clerk/nextjs does not export. Show with when='signed-in' is the current API. - The popup selector matched on substring, so 'Account' also matched clerk-js's 'Account panel'. Exact now. - The only organization is already active on sign-in, so its row is the current item rather than a button. Personal is selected first, then the organization. - Manage account lives behind the account row's actions menu, not on the popup. - The inflated 90s/180s timeouts were compensating for a stale local registry, not real slowness. Back to the repo default.
1 parent 4a67a94 commit c4755cc

2 files changed

Lines changed: 39 additions & 29 deletions

File tree

integration/tests/mosaic-user-button.test.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { createTestUtils } from '../testUtils';
2424
// The template's layout, plus the one line this suite adds. Its appearance options are what the
2525
// shared sign-in page object's selectors depend on, so they are carried over verbatim.
2626
const layout = () => `import './globals.css';
27-
import './mosaic.css';
27+
import '@clerk/nextjs/experimental/mosaic/styles.css';
2828
import { Inter } from 'next/font/google';
2929
import { ClerkProvider } from '@clerk/nextjs';
3030
@@ -52,20 +52,16 @@ export default function RootLayout({ children }: { children: React.ReactNode })
5252
);
5353
}`;
5454

55-
// The layered form is what `@clerk/react`'s mosaic entry documents. If Next cannot resolve a bare
56-
// specifier through `@import`, the fallback is a plain `import` of the same file from the layout.
57-
const mosaicCss = () => `@import '@clerk/nextjs/experimental/mosaic/styles.css' layer(clerk);\n`;
58-
5955
// Both UserButtons mount together while signed in, so every case below runs with the legacy Emotion
6056
// tree and the Mosaic tree live at once rather than proving coexistence in one isolated smoke test.
6157
const mosaicPage = () => `'use client';
6258
import { UserButton } from '@clerk/nextjs/experimental/mosaic';
63-
import { SignIn, SignedIn, SignedOut, UserButton as LegacyUserButton } from '@clerk/nextjs';
59+
import { SignIn, Show, UserButton as LegacyUserButton } from '@clerk/nextjs';
6460
6561
export default function Page() {
6662
return (
6763
<main>
68-
<SignedIn>
64+
<Show when='signed-in'>
6965
<UserButton
7066
userProfileProps={{
7167
customPages: [
@@ -78,10 +74,10 @@ export default function Page() {
7874
}}
7975
/>
8076
<LegacyUserButton />
81-
</SignedIn>
82-
<SignedOut>
77+
</Show>
78+
<Show when='signed-out'>
8379
<SignIn routing='hash' />
84-
</SignedOut>
80+
</Show>
8581
</main>
8682
);
8783
}`;
@@ -107,18 +103,23 @@ test.describe('Mosaic UserButton @nextjs', () => {
107103
let fakeUser: FakeUserWithEmail;
108104
let otherUser: FakeUserWithEmail;
109105
let fakeOrganization: FakeOrganization;
106+
// Filled as each resource is created, so a setup that fails halfway still tears down what it made.
107+
const cleanup: (() => Promise<unknown>)[] = [];
110108

111109
test.beforeAll(async () => {
110+
// Installing and booting a fresh app, before a single test runs.
111+
test.setTimeout(90_000);
112+
112113
app = await appConfigs.next.appRouter
113114
.clone()
114115
// Deliberately no `.addDependency('@clerk/ui', PKGLAB)`, unlike composed-components.test.ts.
115116
// The Mosaic entry is bundled into @clerk/react at build time, so @clerk/nextjs alone has to
116117
// be enough. The absence of that line is what the last test in this file asserts.
117-
.addFile('src/app/mosaic.css', mosaicCss)
118118
.addFile('src/app/layout.tsx', layout)
119119
.addFile('src/app/mosaic/page.tsx', mosaicPage)
120120
.addFile('src/app/api/me/route.ts', meRoute)
121121
.commit();
122+
cleanup.push(() => app.teardown());
122123
await app.setup();
123124
await app.withEnv(appConfigs.envs.withEmailCodes);
124125
await app.dev();
@@ -136,17 +137,17 @@ test.describe('Mosaic UserButton @nextjs', () => {
136137
fakeUser = createUser();
137138
otherUser = createUser();
138139
const user = await m.services.users.createBapiUser(fakeUser);
140+
cleanup.unshift(() => fakeUser.deleteIfExists());
139141
await m.services.users.createBapiUser(otherUser);
142+
cleanup.unshift(() => otherUser.deleteIfExists());
140143
fakeOrganization = await m.services.users.createFakeOrganization(user.id);
144+
cleanup.unshift(() => fakeOrganization.delete());
141145
});
142146

143147
test.afterAll(async () => {
144-
try {
145-
await fakeOrganization.delete();
146-
await otherUser.deleteIfExists();
147-
await fakeUser.deleteIfExists();
148-
} finally {
149-
await app.teardown();
148+
test.setTimeout(90_000);
149+
for (const teardown of cleanup) {
150+
await teardown();
150151
}
151152
});
152153

@@ -194,7 +195,7 @@ test.describe('Mosaic UserButton @nextjs', () => {
194195
await expect(u.po.mosaicUserButton.popup()).toHaveCount(0);
195196
});
196197

197-
test('selecting an organization, then the personal workspace, reaches the server', async ({ page, context }) => {
198+
test('selecting the personal workspace, then the organization, reaches the server', async ({ page, context }) => {
198199
const u = createTestUtils({ app, page, context });
199200
await context.clearCookies();
200201

@@ -205,26 +206,30 @@ test.describe('Mosaic UserButton @nextjs', () => {
205206
await u.page.goToRelative('/mosaic');
206207
await u.po.mosaicUserButton.waitForMounted();
207208

209+
// The only organization is already active on sign-in, so its row is the current item rather than
210+
// a button. Personal is the reachable move first; the organization becomes selectable after it.
211+
await u.po.mosaicUserButton.expectTriggerLabel(fakeOrganization.name);
212+
208213
await u.po.mosaicUserButton.toggleTrigger();
209214
await u.po.mosaicUserButton.waitForPopover();
210-
await u.po.mosaicUserButton.selectWorkspace(fakeOrganization.name);
215+
await u.po.mosaicUserButton.selectPersonalWorkspace();
211216
await u.po.mosaicUserButton.waitForPopoverClosed();
212-
await u.po.mosaicUserButton.expectTriggerLabel(fakeOrganization.name);
213217

214218
await expect(async () => {
215-
const auth = await readAuthState(page);
216-
expect(auth.orgId).toBe(fakeOrganization.organization.id);
217-
expect(auth.orgRole).toBe('org:admin');
218-
expect(auth.orgSlug).toBeTruthy();
219+
expect((await readAuthState(page)).orgId).toBeNull();
219220
}).toPass();
220221

221222
await u.po.mosaicUserButton.toggleTrigger();
222223
await u.po.mosaicUserButton.waitForPopover();
223-
await u.po.mosaicUserButton.selectPersonalWorkspace();
224+
await u.po.mosaicUserButton.selectWorkspace(fakeOrganization.name);
224225
await u.po.mosaicUserButton.waitForPopoverClosed();
226+
await u.po.mosaicUserButton.expectTriggerLabel(fakeOrganization.name);
225227

226228
await expect(async () => {
227-
expect((await readAuthState(page)).orgId).toBeNull();
229+
const auth = await readAuthState(page);
230+
expect(auth.orgId).toBe(fakeOrganization.organization.id);
231+
expect(auth.orgRole).toBe('org:admin');
232+
expect(auth.orgSlug).toBeTruthy();
228233
}).toPass();
229234
});
230235

@@ -298,7 +303,7 @@ test.describe('Mosaic UserButton @nextjs', () => {
298303

299304
await u.po.mosaicUserButton.toggleTrigger();
300305
await u.po.mosaicUserButton.waitForPopover();
301-
await u.po.mosaicUserButton.triggerManageAccount();
306+
await u.po.mosaicUserButton.triggerManageAccount(fakeUser.email);
302307

303308
// The modal is rendered by clerk-js, so the legacy page object is the right tool. Reaching it
304309
// from a Mosaic surface is the hand-off under test.

packages/testing/src/playwright/unstable/page-objects/mosaicUserButton.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export const createMosaicUserButtonPageObject = (testArgs: { page: EnhancedPage
1111
const { page } = testArgs;
1212

1313
const trigger = () => page.getByRole('button', { name: /^Open account menu for / });
14-
const popup = () => page.getByRole('dialog', { name: 'Account' });
14+
// Exact, because clerk-js labels the legacy popover "Account panel" and role names match on substring.
15+
const popup = () => page.getByRole('dialog', { name: 'Account', exact: true });
1516

1617
const self = {
1718
trigger,
@@ -39,7 +40,11 @@ export const createMosaicUserButtonPageObject = (testArgs: { page: EnhancedPage
3940
clickMenuItem: (name: string) => page.getByRole('menuitem', { name, exact: true }).click(),
4041
/** Header and foot actions alike; each name appears once in the popup. */
4142
clickAction: (name: string) => popup().getByRole('button', { name, exact: true }).click(),
42-
triggerManageAccount: () => self.clickAction('Manage account'),
43+
/** Manage account is only reachable through the account row's actions menu. */
44+
triggerManageAccount: async (identifier: string) => {
45+
await self.openAccountActions(identifier);
46+
await self.clickMenuItem('Manage account');
47+
},
4348
triggerManageOrganization: () => self.clickAction('Manage organization'),
4449
triggerSignOutAll: () => self.clickAction('Sign out of all accounts'),
4550
};

0 commit comments

Comments
 (0)