Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions e2e/lib/playwright/mock-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,27 @@ export async function applyStandardMocks(page: Page): Promise<void> {
await applyStorageConfig(page);
await routeMockAgents(page);
await routeAppConfig(page);
await routeSignedOutVelaStatus(page);
await suppressWhatsNew(page);
}

/**
* Keep local-agent scenarios independent from a developer machine's Vela
* session. Workspace-aware project creation treats a signed-in account as
* requiring an exact Workspace authority, so leaking the host login here can
* reject a mocked local-agent create before POST /api/projects is dispatched.
* AMR scenarios register their own status route after this standard fallback.
*/
export async function routeSignedOutVelaStatus(page: Page): Promise<void> {
await page.route('**/api/integrations/vela/status*', async (route) => {
if (route.request().method() !== 'GET') {
await route.fallback();
return;
}
await route.fulfill({ json: { loggedIn: false } });
});
}

/** Keep unrelated release announcements from covering the surface under test. */
export async function suppressWhatsNew(page: Page): Promise<void> {
await page.route('**/api/whats-new', async (route) => {
Expand Down
7 changes: 7 additions & 0 deletions e2e/lib/playwright/suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const uiP0Groups = {
"ui/workspace-team-interactions.test.ts",
],
},
"entry-automations": {
grep: String.raw`\[P0\]`,
workers: 1,
files: ["ui/automations-page.test.ts"],
},
"project-workspace": {
grep: String.raw`\[P0\]`,
workers: 1,
Expand Down Expand Up @@ -77,6 +82,7 @@ export type UiP0GroupName = keyof typeof uiP0Groups;

export const uiP0CiMatrix = [
{ name: "entry-settings", shard: "entry-settings" },
{ name: "entry-automations", shard: "entry-automations" },
{ name: "project-workspace", shard: "project-workspace" },
{ name: "project-collab", shard: "project-collab" },
{ name: "project-runtime", shard: "project-runtime" },
Expand All @@ -97,6 +103,7 @@ const uiP0CoverageFiles = [
"ui/app-manual-edit.test.ts",
"ui/app-restoration.test.ts",
"ui/app.test.ts",
"ui/automations-page.test.ts",
"ui/critical-smoke.test.ts",
"ui/entry-chrome-flows.test.ts",
"ui/entry-configuration-flows.test.ts",
Expand Down
1 change: 1 addition & 0 deletions e2e/tests/packaged-smoke-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ process.stdin.on("end", () => {
expect(uiP0).toContain("include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}");
expect(uiP0CiMatrix.map((entry) => entry.name)).toEqual([
"entry-settings",
"entry-automations",
"project-workspace",
"project-collab",
"project-runtime",
Expand Down
9 changes: 8 additions & 1 deletion e2e/tests/scripts/scopes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,14 @@ test("runtime-definition shadow fails closed for mixed, unknown, empty, and unre
assert.equal(decision.mode, "full-fallback", files.join(", "));
assert.deepEqual(
decision.matrix.map((entry) => entry.name),
["entry-settings", "project-workspace", "project-collab", "project-runtime", "workspace-restoration"],
[
"entry-settings",
"entry-automations",
"project-workspace",
"project-collab",
"project-runtime",
"workspace-restoration",
],
);
}
assert.equal(evaluateUiP0Shadow([], false).reason, "files-unresolved");
Expand Down
7 changes: 6 additions & 1 deletion e2e/ui/api-empty-response.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test } from '@/playwright/suite';
import { fulfillAgentsRoute, routeSuccessfulRuns } from '@/playwright/mock-factory';
import {
fulfillAgentsRoute,
routeSignedOutVelaStatus,
routeSuccessfulRuns,
} from '@/playwright/mock-factory';
import { openNewProjectModal as openNewProjectModalFromProjects } from '@/playwright/rail';
import type { Page } from '@playwright/test';
import { T } from '@/timeouts';
Expand All @@ -9,6 +13,7 @@ const STORAGE_KEY = 'open-design:config';
test.describe.configure({ timeout: T.xlong });

test.beforeEach(async ({ page }) => {
await routeSignedOutVelaStatus(page);
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
Expand Down
41 changes: 2 additions & 39 deletions e2e/ui/app-design-files.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from '@/playwright/suite';
import { openNewProjectModal as openNewProjectModalFromProjects } from '@/playwright/rail';
import { routeAgents } from '@/playwright/mock-factory';
import { applyStandardMocks, routeAgents } from '@/playwright/mock-factory';
import { expectAllProjectFilesActive, openAllProjectFiles } from '@/playwright/workspace';
import type { Locator, Page, Request } from '@playwright/test';
import { automatedUiScenarios } from '@/playwright/resources';
Expand All @@ -14,44 +14,7 @@ const TINY_PNG_B64 =
test.describe.configure({ timeout: T.xlong });

test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
}),
);
}, STORAGE_KEY);

await page.route('**/api/app-config', async (route) => {
if (route.request().method() !== 'GET') {
await route.continue();
return;
}
await route.fulfill({
json: {
config: {
onboardingCompleted: true,
agentId: 'mock',
skillId: null,
designSystemId: null,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
},
},
});
});
await applyStandardMocks(page);
});

const designFileFlows = new Set([
Expand Down
41 changes: 2 additions & 39 deletions e2e/ui/app-manual-edit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from '@/playwright/suite';
import { expectStableCount } from '@/playwright/assertions';
import { routeAgents, routeSuccessfulRuns } from '@/playwright/mock-factory';
import { applyStandardMocks, routeAgents, routeSuccessfulRuns } from '@/playwright/mock-factory';
import { clickDeckNextSlide, openAllProjectFiles } from '@/playwright/workspace';
import type { Page } from '@playwright/test';
import { T } from '@/timeouts';
Expand All @@ -19,44 +19,7 @@ function artifactPreviewFrame(page: Page) {
}

test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
}),
);
}, STORAGE_KEY);

await page.route('**/api/app-config', async (route) => {
if (route.request().method() !== 'GET') {
await route.continue();
return;
}
await route.fulfill({
json: {
config: {
onboardingCompleted: true,
agentId: 'mock',
skillId: null,
designSystemId: null,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
},
},
});
});
await applyStandardMocks(page);
});

test('[P0] manual edit inspector previews and persists page and selected element styles', async ({ page }) => {
Expand Down
40 changes: 2 additions & 38 deletions e2e/ui/app-restoration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { UiScenario } from '@/playwright/resources';
import { T } from '@/timeouts';
import { expectStableCount } from '../lib/playwright/assertions.js';
import {
applyStandardMocks,
failedRunEventBody,
routeMockAgents,
routeRunSequence,
Expand Down Expand Up @@ -50,44 +51,7 @@ function isDesignFileUploadResponse(response: Response): boolean {
}

test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
}),
);
}, STORAGE_KEY);

await page.route('**/api/app-config', async (route) => {
if (route.request().method() !== 'GET') {
await route.continue();
return;
}
await route.fulfill({
json: {
config: {
onboardingCompleted: true,
agentId: 'mock',
skillId: null,
designSystemId: null,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
},
},
});
});
await applyStandardMocks(page);
});

async function routeSimpleSuccessfulRun(page: Page, runIdPrefix: string): Promise<void> {
Expand Down
46 changes: 7 additions & 39 deletions e2e/ui/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expect, test } from '@/playwright/suite';
import { openNewProjectModal as openNewProjectModalFromProjects } from '@/playwright/rail';
import { routeAgents, routeSuccessfulRuns, successfulRunEventBody } from '@/playwright/mock-factory';
import {
applyStandardMocks,
routeAgents,
routeSuccessfulRuns,
successfulRunEventBody,
} from '@/playwright/mock-factory';
import { clickDeckNextSlide, clickDeckPreviousSlide, openAllProjectFiles } from '@/playwright/workspace';
import type { Dialog, Locator, Page, Request, Response } from '@playwright/test';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
Expand Down Expand Up @@ -57,44 +62,7 @@
}

test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
}),
);
}, STORAGE_KEY);

await page.route('**/api/app-config', async (route) => {
if (route.request().method() !== 'GET') {
await route.continue();
return;
}
await route.fulfill({
json: {
config: {
onboardingCompleted: true,
agentId: 'mock',
skillId: null,
designSystemId: null,
agentModels: {},
privacyDecisionAt: 1,
telemetry: { metrics: false, content: false, artifactManifest: false },
},
},
});
});
await applyStandardMocks(page);
});

for (const entry of automatedUiScenarios().filter(
Expand Down Expand Up @@ -1468,7 +1436,7 @@
await expect(page.getByTestId('chat-composer')).toBeVisible();
await expect(page.getByText('reference.txt', { exact: true })).toBeVisible();

await page.getByTestId('chat-composer-input').click();

Check failure on line 1439 in e2e/ui/app.test.ts

View workflow job for this annotation

GitHub Actions / UI P0 (project-runtime)

[chromium] › ui/app.test.ts:71:3 › [P1] @critical @merge-extra file-mention: Uploaded files can be mentioned and sent back to the agent

1) [chromium] › ui/app.test.ts:71:3 › [P1] @critical @merge-extra file-mention: Uploaded files can be mentioned and sent back to the agent Error: locator.click: Test timeout of 45000ms exceeded. Call log: - waiting for getByTestId('chat-composer-input') - locator resolved to <div role="combobox" spellcheck="true" aria-readonly="true" aria-expanded="false" contenteditable="false" aria-autocomplete="none" data-lexical-editor="true" aria-controls="mention-listbox" data-testid="chat-composer-input" class="ph-no-capture composer-editable" title="This is a shared project — you can view and comment, but not change artifacts through Chat or the editing tools." aria-placeholder="This is a shared project — you can view and comment, but not change artifacts through Chat or the ed…>…</div> - attempting click action 2 × waiting for element to be visible, enabled and stable - element is not enabled - retrying click action - waiting 20ms 2 × waiting for element to be visible, enabled and stable - element is not enabled - retrying click action - waiting 100ms 60 × waiting for element to be visible, enabled and stable - element is not enabled - retrying click action - waiting 500ms 1437 | await expect(page.getByText('reference.txt', { exact: true })).toBeVisible(); 1438 | > 1439 | await page.getByTestId('chat-composer-input').click(); | ^ 1440 | await page.getByTestId('chat-composer-input').pressSequentially('Review @ref'); 1441 | await expect(page.getByTestId('mention-popover')).toBeVisible(); 1442 | await page.getByTestId('mention-popover').getByRole('option', { name: /reference\.txt/i }).click(); at runFileMentionFlow (/home/runner/_work/open-design/open-design/e2e/ui/app.test.ts:1439:49) at /home/runner/_work/open-design/open-design/e2e/ui/app.test.ts:279:7
await page.getByTestId('chat-composer-input').pressSequentially('Review @ref');
await expect(page.getByTestId('mention-popover')).toBeVisible();
await page.getByTestId('mention-popover').getByRole('option', { name: /reference\.txt/i }).click();
Expand Down Expand Up @@ -1532,7 +1500,7 @@
await expect((await uploadResponse).ok()).toBeTruthy();

await expect(stagedAttachmentName(page, 'reference.txt')).toBeVisible();
await expect(page.getByText('reference.txt', { exact: true })).toBeVisible();

Check failure on line 1503 in e2e/ui/app.test.ts

View workflow job for this annotation

GitHub Actions / UI P0 (project-workspace)

[chromium] › ui/app.test.ts:71:3 › [P0] @critical file-upload-send: Composer file picker uploads a file and sends it with the prompt

1) [chromium] › ui/app.test.ts:71:3 › [P0] @critical file-upload-send: Composer file picker uploads a file and sends it with the prompt Error: expect(locator).toBeVisible() failed Locator: getByText('reference.txt', { exact: true }) Expected: visible Error: strict mode violation: getByText('reference.txt', { exact: true }) resolved to 2 elements: 1) <span class="df-row-name" title="reference.txt">reference.txt</span> aka getByRole('button', { name: 'reference.txt Text' }) 2) <span class="staged-name" title="reference.txt">reference.txt</span> aka getByTestId('staged-contexts').getByText('reference.txt') Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('reference.txt', { exact: true }) 1501 | 1502 | await expect(stagedAttachmentName(page, 'reference.txt')).toBeVisible(); > 1503 | await expect(page.getByText('reference.txt', { exact: true })).toBeVisible(); | ^ 1504 | 1505 | await sendPrompt(page, entry.prompt); 1506 | await expect(page.locator('.msg.user').getByText(entry.prompt, { exact: true })).toBeVisible(); at runFileUploadSendFlow (/home/runner/_work/open-design/open-design/e2e/ui/app.test.ts:1503:66) at /home/runner/_work/open-design/open-design/e2e/ui/app.test.ts:287:7

Check failure on line 1503 in e2e/ui/app.test.ts

View workflow job for this annotation

GitHub Actions / UI P0 (project-workspace)

[chromium] › ui/app.test.ts:71:3 › [P0] @critical file-upload-send: Composer file picker uploads a file and sends it with the prompt

1) [chromium] › ui/app.test.ts:71:3 › [P0] @critical file-upload-send: Composer file picker uploads a file and sends it with the prompt Error: expect(locator).toBeVisible() failed Locator: getByText('reference.txt', { exact: true }) Expected: visible Error: strict mode violation: getByText('reference.txt', { exact: true }) resolved to 2 elements: 1) <span class="df-row-name" title="reference.txt">reference.txt</span> aka getByRole('button', { name: 'reference.txt Text' }) 2) <span class="staged-name" title="reference.txt">reference.txt</span> aka getByTestId('staged-contexts').getByText('reference.txt') Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('reference.txt', { exact: true }) 1501 | 1502 | await expect(stagedAttachmentName(page, 'reference.txt')).toBeVisible(); > 1503 | await expect(page.getByText('reference.txt', { exact: true })).toBeVisible(); | ^ 1504 | 1505 | await sendPrompt(page, entry.prompt); 1506 | await expect(page.locator('.msg.user').getByText(entry.prompt, { exact: true })).toBeVisible(); at runFileUploadSendFlow (/home/runner/_work/open-design/open-design/e2e/ui/app.test.ts:1503:66) at /home/runner/_work/open-design/open-design/e2e/ui/app.test.ts:287:7

await sendPrompt(page, entry.prompt);
await expect(page.locator('.msg.user').getByText(entry.prompt, { exact: true })).toBeVisible();
Expand Down
65 changes: 0 additions & 65 deletions e2e/ui/entry-chrome-flows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2108,71 +2108,6 @@ test('[P2] home starters Use with query carries the hydrated starter prompt into
expect(typeof projectBody.metadata?.kind).toBe('string');
});

test('[P0] @critical home plugin input edits are resolved and carried into project creation', async ({ page }) => {
const parameterizedPlugin = makeStarterPlugin({
id: 'parameterized-deck-plugin',
title: 'Parameterized Deck Plugin',
mode: 'deck',
featured: true,
query: 'Draft a {{topic}} deck with {{notes}}.',
inputs: [
{ name: 'topic', type: 'string', default: 'quarterly review' },
{ name: 'notes', type: 'string', default: 'speaker notes' },
],
});
const applyBodies: Array<Record<string, unknown>> = [];

await page.route('**/api/plugins', async (route) => {
await route.fulfill({ json: { plugins: [parameterizedPlugin] } });
});
await page.route('**/api/plugins/parameterized-deck-plugin/apply', async (route) => {
const body = route.request().postDataJSON() as { inputs?: Record<string, unknown> };
applyBodies.push(body);
const result = makeApplyResult(
'parameterized-deck-plugin',
'Draft a {{topic}} deck with {{notes}}.',
body.inputs ?? {},
);
await route.fulfill({
json: {
...result,
inputs: parameterizedPlugin.manifest.od.inputs,
},
});
});

await gotoEntryHome(page);
await skipWithoutHomeStarters(page);
const home = await revealHomeTemplates(page);
await home.getByTestId('plugins-home-details-parameterized-deck-plugin').click({ force: true });
await page.getByTestId('plugin-details-use-parameterized-deck-plugin').click();
await expect(page.getByTestId('home-hero-active-plugin')).toContainText('Parameterized Deck Plugin');

const input = page.getByTestId('home-hero-input');
await expect(input).toContainText('quarterly review');
await input.fill('Draft a Liquid Glasses deck with narrated speaker notes.');

const projectRequestPromise = page.waitForRequest(isCreateProjectRequest);
await expect(page.getByTestId('home-hero-submit')).toBeEnabled();
await page.getByTestId('home-hero-submit').click();

const request = await projectRequestPromise;
const body = request.postDataJSON() as {
pendingPrompt?: string;
pluginId?: string;
pluginInputs?: Record<string, unknown>;
appliedPluginSnapshotId?: string;
};
expect(body.pluginId).toBe('parameterized-deck-plugin');
expect(body.pendingPrompt).toBe('Draft a Liquid Glasses deck with narrated speaker notes.');
expect(body.pluginInputs).toMatchObject({
topic: 'Liquid Glasses',
notes: 'narrated speaker notes',
});
expect(body.appliedPluginSnapshotId).toBe('snap-parameterized-deck-plugin');
expect(applyBodies.at(-1)?.inputs).toMatchObject(body.pluginInputs ?? {});
});

test('[P2] required home plugin prompt parameters gate submit and bind the project snapshot', async ({ page }) => {
const guidedDeckPlugin = makeStarterPlugin({
id: 'guided-deck-plugin',
Expand Down
Loading
Loading