Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions frontend/e2e/tests/onboarding-tests.pw.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodeCardProps> = ({
code,
copyLabel,
headerLeft,
language,
onCopy,
Expand All @@ -32,6 +37,7 @@ const CodeCard: FC<CodeCardProps> = ({
theme='primary'
size='small'
className='ms-auto'
aria-label={copyLabel}
onClick={() => {
copy(code)
onCopy?.()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const ConnectYourCodePanel: FC<ConnectYourCodePanelProps> = ({
code={installCode}
language='bash'
onCopy={onCopyInstall}
copyLabel='Copy install command'
headerLeft={
sdkSnippet.installYarn ? (
<div className='onboarding-connect__pm d-inline-flex'>
Expand Down Expand Up @@ -81,6 +82,7 @@ const ConnectYourCodePanel: FC<ConnectYourCodePanelProps> = ({
code={sdkSnippet.wire}
language={sdkSnippet.language}
onCopy={onCopyWire}
copyLabel='Copy code snippet'
headerLeft={
<span className='onboarding-connect__codecard-lang'>
{sdkLang.label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ const OnboardingFlagsTable: FC<OnboardingFlagsTableProps> = ({
}) => {
const waiting = status === 'waiting'
return (
<section className='onboarding-flags'>
<h3 className='onboarding-flags__title'>Your flags</h3>
<section
className='onboarding-flags'
aria-labelledby='onboarding-flags-title'
>
<h3 className='onboarding-flags__title' id='onboarding-flags-title'>
Your flags
</h3>
<div
className={classNames('onboarding-flags__table', {
'onboarding-flags__table--waiting': waiting,
Expand Down Expand Up @@ -70,6 +75,7 @@ const OnboardingFlagsTable: FC<OnboardingFlagsTableProps> = ({
checked={flag.enabled}
disabled={togglingFlag === flag.name}
onChange={(enabled) => onToggle(flag, enabled)}
aria-label={`Toggle ${flag.name}`}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading