diff --git a/frontend/e2e/tests/onboarding-tests.pw.ts b/frontend/e2e/tests/onboarding-tests.pw.ts new file mode 100644 index 000000000000..d08384aa0ddb --- /dev/null +++ b/frontend/e2e/tests/onboarding-tests.pw.ts @@ -0,0 +1,109 @@ +import { test, expect } from '../test-setup'; +import { byId, createHelpers, getFlagsmith, log, visualSnapshot } from '../helpers'; +import { E2E_SIGN_UP_USER, PASSWORD } from '../config'; + +// The single-page onboarding flow (onboarding_quickstart_flow) a new user lands +// on at /getting-started. Mirror of the legacy signup test's guard: this runs +// only when the flag is on, the legacy signup test runs only when it's off. +// +// Selectors are accessibility-first (roles / labels / text), not data-test ids: +// the header inputs expose aria-labels, the copy buttons and the flag switch +// carry accessible names, and the flags table is a labelled region. +test.describe('Onboarding', () => { + test('New user connects via the single-page onboarding flow @oss', async ({ + page, + }, testInfo) => { + const { addErrorLogging, click, setText, waitForElementVisible } = + createHelpers(page); + const flagsmith = await getFlagsmith(); + + test.skip( + !flagsmith.hasFeature('onboarding_quickstart_flow'), + 'Onboarding flow is behind onboarding_quickstart_flow', + ); + + await addErrorLogging(); + + // The flow renders once bootstrap settles (it shows a loader, then an error + // heading on failure - so the welcome heading means "ready"). + const flowReady = async () => + page + .getByRole('heading', { name: /Welcome/ }) + .waitFor({ state: 'visible', timeout: 30000 }); + + // Sign up a fresh user; with the flag on, a getting-started user is routed + // to /getting-started where the flow bootstraps the org / project / flag. + log('Sign up'); + await page.goto('/'); + await click(byId('jsSignup')); + await waitForElementVisible(byId('firstName')); + await setText(byId('firstName'), 'Bullet'); + await setText(byId('lastName'), 'Train'); + await setText(byId('email'), E2E_SIGN_UP_USER); + await setText(byId('password'), PASSWORD); + await click(byId('signup-btn')); + + // Land on the flow. Navigating explicitly is robust to the exact post-signup + // redirect; the gate + idempotent bootstrap render the flow either way. + log('Land on the onboarding flow'); + await page.goto('/getting-started'); + await flowReady(); + await visualSnapshot(page, 'onboarding-flow', testInfo); + + // The verify terminal starts pre-connection: LISTENING, nothing ticked. + await expect(page.getByText('LISTENING')).toBeVisible(); + await expect(page.getByText('Copy install command')).not.toContainText('✓'); + + // Copying the install + wire snippets ticks the checklist (the visible + // [✓] prefix is the done state). + log('Copy snippets, checklist ticks'); + await page.getByRole('button', { name: 'Copy install command' }).click(); + await expect(page.getByText('Copy install command')).toContainText('✓'); + await page.getByRole('button', { name: 'Copy code snippet' }).click(); + await expect(page.getByText('Copy code snippet')).toContainText('✓'); + + // The Development toggle is real and persists (updateFeatureState). There + // are two switches on the page (theme + flag), so scope to the flags region. + log('Toggle the flag'); + const flagsTable = page.getByRole('region', { name: 'Your flags' }); + const flagSwitch = flagsTable.getByRole('switch'); + await flagSwitch.waitFor({ state: 'visible' }); + const wasChecked = (await flagSwitch.getAttribute('class'))?.includes( + 'switch-checked', + ); + await flagSwitch.click(); + await expect(flagSwitch).toHaveClass( + wasChecked ? /switch-unchecked/ : /switch-checked/, + ); + + // The Onboarding badge (attached in bootstrap) shows in the flags table. + // Exact match: the header crumb also contains the word "Onboarding". + await expect(flagsTable.getByText('Onboarding', { exact: true })).toBeVisible(); + + // Rename the flag. Names are immutable, so this delete + recreates; the + // Onboarding tag must survive (the recreate carries the old flag's tags). + log('Rename the flag'); + const flagInput = page.getByLabel('Flag name'); + await flagInput.fill('renamed_demo_flag'); + await flagInput.press('Enter'); + + // Reload to prove the rename persisted server-side and the tag came with it + // (bootstrap is idempotent and reuses the renamed flag on revisit). + await page.reload(); + await flowReady(); + await expect(page.getByLabel('Flag name')).toHaveValue('renamed_demo_flag'); + await expect( + page + .getByRole('region', { name: 'Your flags' }) + .getByText('Onboarding', { exact: true }), + ).toBeVisible(); + await visualSnapshot(page, 'onboarding-renamed', testInfo); + + // The connected state is stubbed behind ?connected pending #7767; it flips + // the badge to LIVE so the connected UI is exercised end to end. + log('Connected state'); + await page.goto('/getting-started?connected'); + await flowReady(); + await expect(page.getByText('LIVE', { exact: true })).toBeVisible(); + }); +}); diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx index 276602c213e5..709c77faa35d 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx @@ -12,12 +12,17 @@ export type CodeCardProps = { headerLeft: ReactNode // Fires when the user copies the code (drives the verify checklist). onCopy?: () => void + // Accessible name for the copy button. The visible label is the same ("Copy + // Code") on every card, so this distinguishes them for screen readers (and + // gives a11y-based tests a stable handle); defaults to the visible label. + copyLabel?: string } // Owns its own "Copied" feedback so each card is independent. Highlight escapes // the body for display; Copy uses the raw string. const CodeCard: FC = ({ code, + copyLabel, headerLeft, language, onCopy, @@ -32,6 +37,7 @@ const CodeCard: FC = ({ theme='primary' size='small' className='ms-auto' + aria-label={copyLabel} onClick={() => { copy(code) onCopy?.() diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx index 8268e0e1f3b0..b66882d24569 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx @@ -46,6 +46,7 @@ const ConnectYourCodePanel: FC = ({ code={installCode} language='bash' onCopy={onCopyInstall} + copyLabel='Copy install command' headerLeft={ sdkSnippet.installYarn ? (
@@ -81,6 +82,7 @@ const ConnectYourCodePanel: FC = ({ code={sdkSnippet.wire} language={sdkSnippet.language} onCopy={onCopyWire} + copyLabel='Copy code snippet' headerLeft={ {sdkLang.label} diff --git a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx index b10cc80f3821..ea2ab514f309 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx @@ -37,8 +37,13 @@ const OnboardingFlagsTable: FC = ({ }) => { const waiting = status === 'waiting' return ( -
-

Your flags

+
+

+ Your flags +

= ({ checked={flag.enabled} disabled={togglingFlag === flag.name} onChange={(enabled) => onToggle(flag, enabled)} + aria-label={`Toggle ${flag.name}`} />
diff --git a/frontend/web/components/pages/onboarding/hooks/useOnboardingFlagRename.ts b/frontend/web/components/pages/onboarding/hooks/useOnboardingFlagRename.ts index 62e5baa1a9e6..d93468590ab2 100644 --- a/frontend/web/components/pages/onboarding/hooks/useOnboardingFlagRename.ts +++ b/frontend/web/components/pages/onboarding/hooks/useOnboardingFlagRename.ts @@ -12,11 +12,14 @@ type UseOnboardingFlagRenameArgs = { featureName: string } -// "Rename" the onboarding flag. Feature names are immutable once created, so -// this is a delete + recreate under the hood - safe here because the flag is -// freshly bootstrapped and not yet depended on. Recreate first (names differ, -// so no unique conflict) and only then drop the old one, so a flag always -// exists even if the second call fails. Resolves true on success. +// "Rename" the onboarding flag. Feature names are immutable once created +// (UpdateFeatureSerializer marks `name` read-only), so this is a delete + +// recreate under the hood - safe here because the flag is freshly bootstrapped +// and not yet depended on. Recreate first (names differ, so no unique conflict) +// and only then drop the old one, so a flag always exists even if the second +// call fails. The recreate carries over the old flag's tags/type/description so +// the Onboarding badge (and any other tag) survives the rename. Resolves true +// on success. // // The toggle that will also act on this flag lives in #7766 (the flags table); // this hook is intentionally rename-only so the connect-panel issue doesn't pull @@ -45,9 +48,11 @@ export const useOnboardingFlagRename = ({ try { await createProjectFlag({ body: { + description: flag.description, name, project: projectId, - type: 'STANDARD', + tags: flag.tags, + type: flag.type, } as Req['createProjectFlag']['body'], project_id: projectId, }).unwrap()