Skip to content
Merged
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
8 changes: 7 additions & 1 deletion e2e/lib/playwright/amr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,16 @@ export async function readAppConfig(page: Page) {
}

export async function seedBrowserConfig(page: Page, value: Record<string, unknown>) {
const payload = { key: STORAGE_KEY, config: value };
await page.addInitScript(
({ key, config }) => {
window.localStorage.setItem(key, JSON.stringify(config));
},
{ key: STORAGE_KEY, config: value },
payload,
);
await page.evaluate(({ key, config }) => {
window.localStorage.setItem(key, JSON.stringify(config));
}, payload).catch(() => {
// Some pre-navigation pages do not expose localStorage yet; the init script above covers the next load.
});
}
2 changes: 2 additions & 0 deletions e2e/lib/playwright/suites.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type UiPlaywrightGroup = {
files: readonly string[];
grep: string;
workers?: number;
};

export type UiP0CiMatrixEntry = {
Expand Down Expand Up @@ -46,6 +47,7 @@ export const uiP0Groups = {
},
"project-runtime": {
grep: String.raw`\[P0\]`,
workers: 1,
files: [
"ui/real-daemon-run.test.ts",
"ui/amr-run-failure-recovery.test.ts",
Expand Down
3 changes: 3 additions & 0 deletions e2e/scripts/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ async function runUiGroup(): Promise<void> {
}

const args = ['test', '-c', 'playwright.config.ts', ...group.files, '--grep', group.grep];
if (group.workers != null) {
args.push(`--workers=${group.workers}`);
}
const child = spawn('playwright', args, {
stdio: 'inherit',
shell: false,
Expand Down
7 changes: 2 additions & 5 deletions e2e/ui/amr-logout-requires-relogin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,9 @@ async function stubCatalogsEmpty(page: import('@playwright/test').Page) {
test('[P0] after local Sign out, AMR runs require re-login and Settings keeps AMR selected', async ({ page }) => {
await stubCatalogsEmpty(page);
const root = join(tmpdir(), `open-design-amr-logout-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
const successVelaBin = await writeFakeVelaBin(join(root, 'bin-success'), {
assistantText: 'Hello from the e2e fake vela.',
requireLoginConfig: false,
});
const reloginVelaBin = await writeFakeVelaBin(join(root, 'bin-relogin'), {
failAuthAtPrompt: true,
requireLoginConfig: false,
});
await mkdir(root, { recursive: true });
let loggedIn = true;
Expand Down Expand Up @@ -92,7 +89,7 @@ test('[P0] after local Sign out, AMR runs require re-login and Settings keeps AM
amr: { model: 'default', reasoning: 'default' },
},
agentCliEnv: {
amr: { VELA_BIN: successVelaBin },
amr: { VELA_BIN: reloginVelaBin },
},
};

Expand Down
230 changes: 216 additions & 14 deletions e2e/ui/amr-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ test('[P0] @critical onboarding Local CLI card lets the user pick an agent model
await expect(page.getByRole('button', { name: /^Continue$/i })).toBeVisible();
});

test('[P0] onboarding Local CLI path completes setup with the selected agent model', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: false,
initialLoggedIn: false,
codexModels: [
{ id: 'gpt-5.4-mini', label: 'gpt-5.4-mini' },
{ id: 'glm-5', label: 'GLM 5' },
],
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);

await page.getByRole('button', { name: /Local coding agent/i }).click();
const localPanel = page.locator('.onboarding-view__setup-panel');
await expect(localPanel).toBeVisible();
await selectOnboardingOption(localPanel, 'Model', 'GLM 5');
await page.getByRole('button', { name: /^Continue$/i }).click();

await expect(page.getByRole('heading', { name: /About you/i })).toBeVisible({ timeout: T.long });
await advanceFromAboutYouToBrand(page);

await expectOnboardingFinished(page);
await pollStoredConfig(page).toMatchObject({
mode: 'daemon',
agentId: 'codex',
agentModels: {
codex: { model: 'glm-5' },
},
onboardingCompleted: true,
});
});

test('[P0] onboarding Local CLI path stays gated when no local CLI is available', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: false,
Expand Down Expand Up @@ -444,20 +477,6 @@ test('[P0] onboarding gate cannot be bypassed by direct Home navigation or new-t
await expect(connectLandingHeading(page)).toBeVisible();
});

test('[P0] onboarding Connect step exposes no Skip affordance', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);

// "Skip for now" was removed — Connect is now a required step, so there is
// no way to exit onboarding from here without connecting a runtime.
await expect(page.getByRole('button', { name: /Skip for now/i })).toHaveCount(0);
});

test('[P0] onboarding visited steps become locked again when the Connect runtime becomes invalid', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
Expand Down Expand Up @@ -591,6 +610,128 @@ test('[P0] onboarding newsletter malformed email does not block finishing setup'
});
});

test('[P0] onboarding newsletter submits a valid email only when setup finishes', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});
const newsletterBodies: Array<Record<string, unknown>> = [];
await page.route('https://open-design.ai/subscribe', async (route) => {
newsletterBodies.push(route.request().postDataJSON() as Record<string, unknown>);
await route.fulfill({ json: { ok: true } });
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);
await advanceToNewsletterStep(page);

await page.getByPlaceholder('you@studio.com').fill('Designer@Example.COM ');
await page.getByRole('button', { name: /^Continue$/i }).click();
await expect(page.getByRole('heading', { name: /Create once, build everywhere/i })).toBeVisible();
await expect.poll(() => newsletterBodies.length).toBe(0);

await expectOnboardingFinished(page);
await expect.poll(() => newsletterBodies).toEqual([
{ email: 'designer@example.com', source: 'client' },
]);
await pollStoredConfig(page).toMatchObject({
onboardingCompleted: true,
});
});

test('[P0] onboarding newsletter submit failure does not block finishing setup', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});
let newsletterCalls = 0;
await page.route('https://open-design.ai/subscribe', async (route) => {
newsletterCalls += 1;
await route.fulfill({
status: 500,
json: { ok: false },
});
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);
await advanceToNewsletterStep(page);

await page.getByPlaceholder('you@studio.com').fill('designer@example.com');
await page.getByRole('button', { name: /^Continue$/i }).click();
await expect(page.getByRole('heading', { name: /Create once, build everywhere/i })).toBeVisible();

await expectOnboardingFinished(page);
await expect.poll(() => newsletterCalls).toBe(1);
await pollStoredConfig(page).toMatchObject({
onboardingCompleted: true,
});
});

test('[P0] onboarding build CTA opens design-system creation and completes onboarding', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);
await clickCloudPrimary(page);
await advanceFromAboutYouToBrand(page);

await page.getByRole('button', { name: /Build a design system/i }).click();

await expect(page).toHaveURL(/\/design-systems\/create$/);
await expect(
page.getByRole('heading', { name: /Extract from GitHub, websites, or source material/i }),
).toBeVisible();
await expect(page.getByRole('button', { name: /Continue to generation/i })).toBeDisabled();
await pollStoredConfig(page).toMatchObject({
onboardingCompleted: true,
});
});

test('[P0] onboarding build step still allows finishing directly to home', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);
await clickCloudPrimary(page);
await advanceFromAboutYouToBrand(page);

await expect(page.getByRole('heading', { name: /Create once, build everywhere/i })).toBeVisible();

await expectOnboardingFinished(page);
await pollStoredConfig(page).toMatchObject({
onboardingCompleted: true,
});
});

test('[P0] onboarding Back keeps newsletter email and brand URL draft intact', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);
await advanceToNewsletterStep(page);

await page.getByPlaceholder('you@studio.com').fill('designer@example.com');
await page.getByRole('button', { name: /^Continue$/i }).click();
await expect(page.getByRole('heading', { name: /Create once, build everywhere/i })).toBeVisible();

await page.getByRole('button', { name: /^Back$/i }).click();
await expect(page.getByRole('heading', { name: /Stay in the loop/i })).toBeVisible();
await expect(page.getByPlaceholder('you@studio.com')).toHaveValue('designer@example.com');

await page.getByRole('button', { name: /^Continue$/i }).click();
await expect(page.getByRole('heading', { name: /Create once, build everywhere/i })).toBeVisible();
});

test('[P0] @critical onboarding BYOK path can fetch models, test the provider, and complete setup', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
Expand Down Expand Up @@ -719,6 +860,67 @@ test('[P0] onboarding BYOK path cannot continue before a successful connection t
await expect(continueButton).toBeEnabled();
});

test('[P0] onboarding BYOK path supports Anthropic model selection and API key visibility before completing', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
initialLoggedIn: true,
});
let connectionBody: Record<string, unknown> | null = null;
await page.route('**/api/test/connection', async (route) => {
connectionBody = route.request().postDataJSON() as Record<string, unknown>;
await route.fulfill({
json: {
ok: true,
kind: 'success',
latencyMs: 19,
model: 'claude-custom-onboarding',
sample: 'Connected',
},
});
});

await seedOnboardingConfig(page, config);
await gotoOnboarding(page);

await page.getByRole('button', { name: /Bring your own key/i }).click();
await expect(page.getByRole('tab', { name: /^Anthropic$/i })).toHaveAttribute('aria-selected', 'true');

const apiKeyField = onboardingField(page, 'API key');
const apiKeyInput = apiKeyField.locator('input');
await expect(apiKeyInput).toHaveAttribute('type', 'password');
await fillInlineField(page, 'API key', 'anthropic-test-key');
await apiKeyField.getByRole('button', { name: /^Show$/i }).click();
await expect(apiKeyInput).toHaveAttribute('type', 'text');

await fillInlineField(page, 'Base URL', 'https://api.anthropic.com');
const byokPanel = page.locator('.onboarding-view__setup-panel').filter({ hasText: /BYOK/ });
await selectOnboardingOption(byokPanel, 'Model', 'claude-sonnet-4-5');
await page.getByRole('button', { name: /^Test$/i }).click();
await expectProviderConnectionSuccess(page);

await expect.poll(() => connectionBody).toMatchObject({
mode: 'provider',
protocol: 'anthropic',
apiKey: 'anthropic-test-key',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
});

await page.getByRole('button', { name: /^Continue$/i }).click();
await expect(page.getByRole('heading', { name: /About you/i })).toBeVisible();
await advanceFromAboutYouToBrand(page);

await expectOnboardingFinished(page);
await pollStoredConfig(page).toMatchObject({
mode: 'api',
apiProtocol: 'anthropic',
apiKey: 'anthropic-test-key',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
onboardingCompleted: true,
});
});

test('[P0] onboarding BYOK successful test is invalidated when connection settings change', async ({ page }) => {
const config = await wireOnboardingMocks(page, {
amrAvailable: true,
Expand Down
Loading
Loading