mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* refactor(web): rename Execution mode and tighten settings dialog UI - Rename "Settings → Execution & model" to "Settings → Execution mode" across the web UI, i18n keys, docs, and e2e selectors. - Redesign SettingsDialog: kicker + title row in the modal head, a flatMap-driven agent grid that renders the inline test-result row beside the selected card, compact unavailable cards with right-aligned install/docs links, and an install guide that only shows when the user has no working agent picked. - Trim verbose subtitle / hint copy across chat model, CLI proxy, media providers, custom instructions, and memory sections. - Add an `info` Icon variant for the redesigned settings hints. - Update e2e selectors and docs that referenced the old menu label. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(web): polish Settings dialog — media providers, skills, MCP Media providers - Hide internal Stub fixture provider (settingsVisible: false) - Split provider list into Available (integrated, editable) and Coming Soon (collapsed <details> drawer with name/hint/Docs link only) - Drop right-side Integrated/Configured badges from every row; all rows in the main list are integrated by definition; inline grey "Saved" chip next to the provider name is the only status indicator now - "Saved" badge moves inline to the right of the provider name and uses a neutral grey treatment (was a standalone green pill below the name) - "Reload from daemon" button shows a 2s green "✓ Reloaded" flash on success instead of leaving a permanent paragraph under the header; errors remain sticky Skills - Replace three pill-row filter banks (Source, Type, Category) with a compact single-row toolbar: search + three inline <select> dropdowns side by side; active filter highlighted with a stronger border MCP server - Shorten section hint to one line - Move WHAT YOUR AGENT CAN DO capabilities above the client dropdown (motivate before asking to act) - Move "Build the daemon first" warning below the code block where it contextually explains why the command might fail, not as a top-level error before the user has done anything - Downgrade "Restart your client" left-border from accent orange to border-strong grey — it is a next step, not a warning External MCP - Shorten section hint to one line Misc CSS - Add .sr-only utility for accessible off-screen live regions - Add button.ghost.is-success-flash for transient success feedback - Add .library-filter-selects / .library-filter-select for dropdown filter rows - Add .media-provider-coming-soon-* for the roadmap drawer Co-authored-by: Cursor <cursoragent@cursor.com> * [codex] Add Cursor Agent auth diagnostics (#1538) * Add Cursor Agent auth diagnostics * Handle Cursor not logged in auth status * Address Cursor auth review feedback * Classify Cursor stdout auth failures * test: expand Memory and Routines coverage (#1521) * test: expand settings and packaged coverage * test: extend memory settings coverage * test: cover routine settings failure states * test: cover routine operation failures * test: fix daemon test typing on CI * test: decouple packaged smoke from orbit bug * test: avoid live memory LLM calls in route tests * test: fix daemon fetch typing in CI * fix: restore preview comment and inspect toggles * test: align manual edit flow with current inspector UX * test: align comment attachment flow with current preview comments UI * fix: probe resolved Codex launch path during detection * fix: remove duplicate board activation helper after rebase * test: update ghost cli detection mock * test: align FileViewer toolbar expectation * ci: move full app tests to extended lane * ci: run app tests by changed scope * ci: cover shared app inputs in test scopes * ci: avoid setup-node cache in windows packaged smoke * test: align extended settings and manual edit flows * refactor(web): rename Execution mode and tighten settings dialog UI - Rename "Settings → Execution & model" to "Settings → Execution mode" across the web UI, i18n keys, docs, and e2e selectors. - Redesign SettingsDialog: kicker + title row in the modal head, a flatMap-driven agent grid that renders the inline test-result row beside the selected card, compact unavailable cards with right-aligned install/docs links, and an install guide that only shows when the user has no working agent picked. - Trim verbose subtitle / hint copy across chat model, CLI proxy, media providers, custom instructions, and memory sections. - Add an `info` Icon variant for the redesigned settings hints. - Update e2e selectors and docs that referenced the old menu label. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(web): settings dialog UX polish — layout, dedup, and interactions - Remove duplicate section headers from all settings sections (Notifications, Appearance, Privacy, About, Design Systems, Skills, MCP server, Connectors, Media providers, Routines) - Restructure Notifications cards: title + toggle on same row, hint below - Restructure Skills toolbar: search + New skill button in row 1, filter dropdowns in row 2 with left-aligned labels - Restructure Pet section: tabs and Wake button on same row - MCP server: group capabilities and setup into separate cards, remove nested double border on client picker - Connectors: show connect errors as toast instead of inline card text, position toast inside panel, hide single-provider tab - Media providers: move Reload button to left-aligned small ghost button - Memory: info icon shows path on hover, Path copied badge inline; Extraction history and MEMORY.md as standalone collapsible cards; group header hidden when only one type visible - Pet grid cards: Adopt button hidden until hover, icon-only when adopted, description truncated to 2 lines, text fills full width via abs positioning - Agent cards: selected state uses accent border only, no background change - Add sun/moon icons to Appearance theme buttons (Light/Dark) - Shorten several hint strings for clarity Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): resolve i18n review comments from PR #1568 - Update settings.title and settings.envConfigure to localized "Execution mode" in all 17 non-English locale files - Add settings.memoryFlashPathCopied to all locales and use t() in MemorySection instead of hardcoded English "Path copied" - Add settings.agentModelHead to all locales and use t() in SettingsDialog for "Model for:" agent model row header Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): update tests to match settings dialog redesign - Add role prop to Toast (alert/status) so error toasts from ConnectorsBrowser are announced immediately by screen readers - Clear connectErrorToast on successful connector retry - Update SettingsDialog.execution tests: - Remove heading assertions for About and MCP server (headers were intentionally removed as duplicate nav labels) - Rewrite CLI env test to use codex-only fields (per-agent filtering means only selected agent's fields are shown) - Update Composio key hint text assertion to match shortened copy - Replace filter button click with select change for Type filter - Replace Configured/Unsupported/Integrated badge checks with updated assertions matching the new media provider UI - Replace disabled BFL row test with coming-soon section check - Update SettingsDialog.media test: remove Fal.ai input assertions (non-integrated providers no longer have editable fields) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): unblock CI for #1568 Three small fixes to get Playwright back to green on the settings dialog redesign: 1. `en.ts`: revert `settings.envConfigure` to "Configure execution mode". This PR collapsed both `settings.title` (header gear) and `settings.envConfigure` (entry-side foot pill) to the same string "Execution mode", so `getByRole('button', { name: 'Execution mode' })` resolved to two elements and tripped Playwright strict mode in the three Composio-flow tests (entry-configuration-flows.test.ts:174, 228, 285). Restoring the distinct label also gives screen readers a clearer hint for the pill, which doubles as a status display. Non-English locales still alias the two keys; happy to follow up on those, but they don't gate the (English-only) Playwright suite. 2. entry-configuration-flows.test.ts:167 — `Connectors` heading is now rendered at `<h2>` in the modal-head (SettingsDialog.tsx:1545), with the inner `<h3>` removed by design (see comment around line 1448). Updated the assertion from `level: 3` to `level: 2`. 3. project-management-flows.test.ts:360 — same change for the `Pets` heading. Verified locally with `pnpm --filter @open-design/web typecheck` and `pnpm --filter @open-design/e2e typecheck`. The actual Playwright specs need the dev server up; I didn't rerun them here, but the locator changes are mechanical and match the new DOM. * fix(web): use exact match for Execution mode button locator Playwright's `getByRole({ name })` defaults to substring matching, so `{ name: 'Execution mode' }` still resolved to both the header gear (aria-label "Execution mode") and the entry-side foot pill (aria-label "Configure execution mode" — substring contains "Execution mode"). Strict mode tripped in the three composio-flow tests at lines 202, 257, and 319. Adding `exact: true` makes each call resolve to just the header gear, which opens the same dialog the foot pill does — the test outcomes are unchanged. --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Caprika <56862773+alchemistklk@users.noreply.github.com> Co-authored-by: shangxinyu1 <shangxinyu@refly.ai> Co-authored-by: lefarcen <935902669@qq.com>
407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import type { Locator, Page } from '@playwright/test';
|
|
|
|
const STORAGE_KEY = 'open-design:config';
|
|
|
|
const CONNECTORS = [
|
|
{
|
|
id: 'github',
|
|
name: 'GitHub',
|
|
provider: 'composio',
|
|
category: 'Developer tools',
|
|
description: 'Read repository issues and pull requests.',
|
|
status: 'available',
|
|
auth: { provider: 'composio', configured: true },
|
|
tools: [
|
|
{
|
|
name: 'list_issues',
|
|
title: 'List issues',
|
|
description: 'List recent issues from a repository.',
|
|
safety: {
|
|
sideEffect: 'read',
|
|
approval: 'auto',
|
|
reason: 'Read-only issue lookup.',
|
|
},
|
|
refreshEligible: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'slack',
|
|
name: 'Slack',
|
|
provider: 'composio',
|
|
category: 'Communication',
|
|
description: 'Search channels and messages.',
|
|
status: 'connected',
|
|
accountLabel: 'design-team',
|
|
auth: { provider: 'composio', configured: true },
|
|
tools: [],
|
|
},
|
|
];
|
|
|
|
const IMAGE_TEMPLATE = {
|
|
id: 'editorial-poster',
|
|
surface: 'image',
|
|
title: 'Editorial Poster',
|
|
summary: 'A punchy launch poster for a product announcement.',
|
|
category: 'Marketing',
|
|
tags: ['poster', 'launch'],
|
|
model: 'gpt-image-1',
|
|
aspect: '4:5',
|
|
source: {
|
|
repo: 'open-design/test-prompts',
|
|
license: 'MIT',
|
|
author: 'Open Design QA',
|
|
},
|
|
};
|
|
|
|
async function readSavedConfig(page: Page) {
|
|
return page.evaluate((key) => {
|
|
const raw = window.localStorage.getItem(key);
|
|
return raw ? JSON.parse(raw) : null;
|
|
}, STORAGE_KEY);
|
|
}
|
|
|
|
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: {},
|
|
}),
|
|
);
|
|
}, STORAGE_KEY);
|
|
|
|
await page.route('**/api/agents', async (route) => {
|
|
await route.fulfill({
|
|
json: {
|
|
agents: [
|
|
{
|
|
id: 'mock',
|
|
name: 'Mock Agent',
|
|
bin: 'mock-agent',
|
|
available: true,
|
|
version: 'test',
|
|
models: [{ id: 'default', label: 'Default' }],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
test('prompt template retry preserves the edited body in project metadata', async ({ page }) => {
|
|
let detailRequests = 0;
|
|
await page.route('**/api/prompt-templates', async (route) => {
|
|
await route.fulfill({ json: { promptTemplates: [IMAGE_TEMPLATE] } });
|
|
});
|
|
await page.route('**/api/prompt-templates/image/editorial-poster', async (route) => {
|
|
detailRequests += 1;
|
|
if (detailRequests === 1) {
|
|
await route.fulfill({ status: 500, body: 'template unavailable' });
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
json: {
|
|
promptTemplate: {
|
|
...IMAGE_TEMPLATE,
|
|
prompt: 'Original poster prompt with dramatic type and product photography.',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
await gotoEntryHome(page);
|
|
await page.getByTestId('new-project-tab-media').click();
|
|
await page.getByTestId('new-project-media-surface-image').click();
|
|
await page.getByTestId('new-project-name').fill('Prompt template retry metadata');
|
|
|
|
await page.getByTestId('prompt-template-trigger').click();
|
|
await page.getByTestId('prompt-template-search').fill('poster');
|
|
await page.getByRole('option', { name: /Editorial Poster/i }).click();
|
|
|
|
await expect(page.getByTestId('prompt-template-error')).toBeVisible();
|
|
await page.getByTestId('prompt-template-retry').click();
|
|
await expect(page.getByTestId('prompt-template-error')).toHaveCount(0);
|
|
await expect(page.getByTestId('prompt-template-body')).toContainText('Original poster prompt');
|
|
|
|
await page.getByTestId('prompt-template-body').fill('');
|
|
await expect(page.getByTestId('prompt-template-empty-hint')).toBeVisible();
|
|
await page.getByTestId('prompt-template-body').fill(
|
|
'Edited QA prompt: bold poster, one hero product, crisp headline.',
|
|
);
|
|
await page.getByTestId('create-project').click();
|
|
|
|
const project = await fetchCurrentProject(page);
|
|
expect(project.metadata?.promptTemplate).toMatchObject({
|
|
id: 'editorial-poster',
|
|
surface: 'image',
|
|
title: 'Editorial Poster',
|
|
prompt: 'Edited QA prompt: bold poster, one hero product, crisp headline.',
|
|
});
|
|
});
|
|
|
|
test('live artifact empty connector CTA opens the gated connector setup path', async ({ page }) => {
|
|
await routeConnectors(page, []);
|
|
await routeComposioConfig(page, { configured: false, apiKeyTail: '' });
|
|
|
|
await gotoEntryHome(page);
|
|
await page.getByTestId('new-project-tab-live-artifact').click();
|
|
await expect(page.getByTestId('new-project-connectors')).toBeVisible();
|
|
|
|
// The empty CTA now opens Settings → Connectors directly. The Composio API
|
|
// key field sits at the top of the section; the catalog (and its gate)
|
|
// sits below it.
|
|
await page.getByTestId('new-project-connectors-empty').click();
|
|
const settingsDialog = page.getByRole('dialog');
|
|
await expect(settingsDialog).toBeVisible();
|
|
await expect(
|
|
settingsDialog.getByRole('heading', { level: 2, name: 'Connectors' }),
|
|
).toBeVisible();
|
|
await expect(settingsDialog.getByPlaceholder('Paste Composio API key')).toBeVisible();
|
|
await expect(settingsDialog.getByTestId('connector-gate')).toBeVisible();
|
|
await expect(settingsDialog.getByTestId('connectors-search-input')).toBeDisabled();
|
|
});
|
|
|
|
test('connectors search supports empty results and keyboard-closeable details', async ({ page }) => {
|
|
await routeConnectors(page, CONNECTORS);
|
|
await routeComposioConfig(page, { configured: true, apiKeyTail: '1234' });
|
|
await page.addInitScript((key) => {
|
|
const next = {
|
|
mode: 'daemon',
|
|
apiKey: '',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
model: 'claude-sonnet-4-5',
|
|
agentId: 'mock',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
onboardingCompleted: true,
|
|
agentModels: {},
|
|
composio: {
|
|
apiKey: '',
|
|
apiKeyConfigured: true,
|
|
apiKeyTail: '1234',
|
|
},
|
|
};
|
|
window.localStorage.setItem(key, JSON.stringify(next));
|
|
}, STORAGE_KEY);
|
|
|
|
await page.goto('/');
|
|
// Connector cards + search now live under Settings → Connectors. Open the
|
|
// settings dialog via the entry sidebar's "Execution mode" pill
|
|
// and switch to the Connectors section before exercising the
|
|
// search/empty/details flow.
|
|
await page.getByRole('button', { name: 'Execution mode', exact: true }).click();
|
|
const settingsDialog = page.getByRole('dialog');
|
|
await expect(settingsDialog).toBeVisible();
|
|
await settingsDialog.getByRole('button', { name: /^Connectors\b/ }).click();
|
|
await expect(settingsDialog.getByTestId('connector-grid-wrap')).toBeVisible();
|
|
|
|
const search = settingsDialog.getByTestId('connectors-search-input');
|
|
await search.fill('git');
|
|
await expect(connectorCard(settingsDialog, 'github')).toBeVisible();
|
|
await expect(connectorCard(settingsDialog, 'slack')).toHaveCount(0);
|
|
|
|
await search.fill('missing connector');
|
|
await expect(settingsDialog.getByTestId('connectors-empty')).toBeVisible();
|
|
await settingsDialog.getByTestId('connectors-search-clear').click();
|
|
await expect(settingsDialog.getByTestId('connectors-empty')).toHaveCount(0);
|
|
await expect(connectorCard(settingsDialog, 'github')).toBeVisible();
|
|
await expect(connectorCard(settingsDialog, 'slack')).toBeVisible();
|
|
|
|
await connectorCard(settingsDialog, 'github').focus();
|
|
await connectorCard(settingsDialog, 'github').press('Enter');
|
|
await expect(page.getByTestId('connector-drawer')).toBeVisible();
|
|
await expect(page.getByTestId('connector-drawer')).toContainText('List issues');
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.getByTestId('connector-drawer')).toHaveCount(0);
|
|
});
|
|
|
|
test('saving a Composio key from Settings unlocks the connectors gate immediately', async ({ page }) => {
|
|
const { accountLabel: _unusedAccountLabel, ...slackConnector } = CONNECTORS[1]!;
|
|
await routeConnectors(page, [
|
|
{
|
|
...CONNECTORS[0]!,
|
|
status: 'available',
|
|
auth: { provider: 'composio', configured: false },
|
|
},
|
|
{
|
|
...slackConnector,
|
|
status: 'available',
|
|
auth: { provider: 'composio', configured: false },
|
|
},
|
|
]);
|
|
|
|
let savedComposioBody: unknown = null;
|
|
await page.route('**/api/connectors/composio/config', async (route) => {
|
|
savedComposioBody = route.request().postDataJSON();
|
|
await route.fulfill({ status: 200, body: '{}' });
|
|
});
|
|
await page.route('**/api/app-config', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: { config: null } });
|
|
return;
|
|
}
|
|
await route.fulfill({ status: 200, body: '{}' });
|
|
});
|
|
|
|
await gotoEntryHome(page);
|
|
await page.getByRole('button', { name: 'Execution mode', exact: true }).click();
|
|
const settingsDialog = page.getByRole('dialog');
|
|
await expect(settingsDialog).toBeVisible();
|
|
await settingsDialog.getByRole('button', { name: /^Connectors\b/ }).click();
|
|
await expect(settingsDialog.getByTestId('connectors-search-input')).toBeDisabled();
|
|
|
|
await settingsDialog.getByPlaceholder('Paste Composio API key').fill('cmp-secret-1234');
|
|
await settingsDialog.getByRole('button', { name: 'Save key', exact: true }).click();
|
|
|
|
expect(savedComposioBody).toEqual({ apiKey: 'cmp-secret-1234' });
|
|
await expect(settingsDialog.getByTestId('connectors-search-input')).toBeEnabled();
|
|
await expect(connectorCard(settingsDialog, 'github')).toBeVisible();
|
|
|
|
await expect.poll(async () => readSavedConfig(page)).toMatchObject({
|
|
composio: {
|
|
apiKey: '',
|
|
apiKeyConfigured: true,
|
|
apiKeyTail: '1234',
|
|
},
|
|
});
|
|
const savedConfig = await readSavedConfig(page);
|
|
expect(savedConfig?.composio).toMatchObject({
|
|
apiKey: '',
|
|
apiKeyConfigured: true,
|
|
apiKeyTail: '1234',
|
|
});
|
|
});
|
|
|
|
test('typing a draft replacement Composio key does not trigger global autosave', async ({ page }) => {
|
|
await routeConnectors(page, CONNECTORS);
|
|
await routeComposioConfig(page, { configured: true, apiKeyTail: '1234' });
|
|
await page.addInitScript((key) => {
|
|
const next = {
|
|
mode: 'daemon',
|
|
apiKey: '',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
model: 'claude-sonnet-4-5',
|
|
agentId: 'mock',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
onboardingCompleted: true,
|
|
agentModels: {},
|
|
composio: {
|
|
apiKey: '',
|
|
apiKeyConfigured: true,
|
|
apiKeyTail: '1234',
|
|
},
|
|
};
|
|
window.localStorage.setItem(key, JSON.stringify(next));
|
|
}, STORAGE_KEY);
|
|
|
|
const appConfigPersistBodies: unknown[] = [];
|
|
await page.route('**/api/app-config', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: { config: null } });
|
|
return;
|
|
}
|
|
appConfigPersistBodies.push(route.request().postDataJSON());
|
|
await route.fulfill({ status: 200, body: '{}' });
|
|
});
|
|
|
|
await gotoEntryHome(page);
|
|
await page.getByRole('button', { name: 'Execution mode', exact: true }).click();
|
|
const settingsDialog = page.getByRole('dialog');
|
|
await expect(settingsDialog).toBeVisible();
|
|
await settingsDialog.getByRole('button', { name: /^Connectors\b/ }).click();
|
|
await expect(settingsDialog.getByTestId('connector-grid-wrap')).toBeVisible();
|
|
await expect(settingsDialog.getByText('Saved · ••••1234')).toBeVisible();
|
|
|
|
const appConfigPersistCountBeforeDraftEdit = appConfigPersistBodies.length;
|
|
|
|
const replacementInput = settingsDialog.getByPlaceholder('Paste a new key to replace the saved one');
|
|
await replacementInput.fill('cmp-draft-secret-9999');
|
|
await expect(settingsDialog.getByRole('button', { name: 'Save key', exact: true })).toBeEnabled();
|
|
|
|
await page.waitForTimeout(900);
|
|
expect(appConfigPersistBodies).toHaveLength(appConfigPersistCountBeforeDraftEdit);
|
|
await expect(settingsDialog.locator('.settings-autosave')).not.toContainText(/Saving|All changes saved/);
|
|
});
|
|
|
|
async function routeConnectors(page: Page, connectors: typeof CONNECTORS) {
|
|
await page.route('**/api/connectors', async (route) => {
|
|
await route.fulfill({ json: { connectors } });
|
|
});
|
|
await page.route('**/api/connectors/status', async (route) => {
|
|
const statuses = Object.fromEntries(
|
|
connectors.map((connector) => [
|
|
connector.id,
|
|
{
|
|
status: connector.status,
|
|
accountLabel: connector.accountLabel,
|
|
},
|
|
]),
|
|
);
|
|
await route.fulfill({ json: { statuses } });
|
|
});
|
|
await page.route('**/api/connectors/discovery*', async (route) => {
|
|
await route.fulfill({
|
|
json: {
|
|
connectors,
|
|
meta: { provider: 'composio' },
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async function gotoEntryHome(page: Page) {
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
await expect(page.getByTestId('new-project-panel')).toBeVisible();
|
|
}
|
|
|
|
async function routeComposioConfig(
|
|
page: Page,
|
|
config: { configured: boolean; apiKeyTail?: string },
|
|
) {
|
|
await page.route('**/api/connectors/composio/config', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ json: config });
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ json: { ok: true } });
|
|
});
|
|
}
|
|
|
|
function connectorCard(scope: Page | Locator, id: string) {
|
|
return scope.locator(`article.connector-card[data-connector-id="${id}"]`);
|
|
}
|
|
|
|
async function fetchCurrentProject(page: Page) {
|
|
await expect(page).toHaveURL(/\/projects\/[^/]+/);
|
|
const url = new URL(page.url());
|
|
const [, projectId] = url.pathname.match(/\/projects\/([^/]+)/) ?? [];
|
|
expect(projectId).toBeTruthy();
|
|
|
|
const response = await page.request.get(`/api/projects/${projectId}`);
|
|
expect(response.ok()).toBeTruthy();
|
|
const body = (await response.json()) as {
|
|
project: {
|
|
metadata?: {
|
|
promptTemplate?: {
|
|
id: string;
|
|
surface: string;
|
|
title: string;
|
|
prompt: string;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
return body.project;
|
|
}
|