mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* Add launch review E2E regression coverage * Harden daemon launch review regressions * Stabilize daemon runtime tests * fix(tests): restore e2e preflight typing Generated-By: looper 0.8.1 (runner=fixer, agent=codex) * fix(tests): make fake plugin runtime ESM-safe Generated-By: looper 0.8.1 (runner=fixer, agent=codex) * Stabilize e2e fake agent and regression tests * fix(tests): repair fake agent cjs runtime Generated-By: looper 0.8.1 (runner=fixer, agent=codex) * fix(review): harden plugin authoring checks Generated-By: looper 0.9.2 (runner=fixer, agent=codex) * fix(tests): bind plugin authoring run to seeded conversation Generated-By: looper 0.9.2 (runner=fixer, agent=codex)
377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import type { Locator, Page } from '@playwright/test';
|
|
|
|
const STORAGE_KEY = 'open-design:config';
|
|
const OPEN_SETTINGS_LABEL = /Open settings|打开设置|開啟設定/i;
|
|
|
|
test.describe.configure({ timeout: 30_000 });
|
|
|
|
type ConnectorFixture = {
|
|
id: string;
|
|
name: string;
|
|
provider: 'composio';
|
|
category: string;
|
|
description: string;
|
|
status: 'available' | 'connected' | 'error';
|
|
accountLabel?: string;
|
|
lastError?: string;
|
|
auth: { provider: 'composio'; configured: true };
|
|
tools: readonly unknown[];
|
|
};
|
|
|
|
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: [],
|
|
},
|
|
{
|
|
id: 'slack',
|
|
name: 'Slack',
|
|
provider: 'composio',
|
|
category: 'Communication',
|
|
description: 'Search channels and messages.',
|
|
status: 'connected',
|
|
accountLabel: 'design-team',
|
|
auth: { provider: 'composio', configured: true },
|
|
tools: [],
|
|
},
|
|
] as const satisfies readonly ConnectorFixture[];
|
|
|
|
function baseConfig(): Record<string, unknown> {
|
|
return {
|
|
mode: 'daemon',
|
|
apiKey: '',
|
|
apiProtocol: 'openai',
|
|
apiVersion: '',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4o',
|
|
apiProviderBaseUrl: 'https://api.openai.com/v1',
|
|
agentId: 'codex',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
onboardingCompleted: true,
|
|
composio: {
|
|
apiKey: '',
|
|
apiKeyConfigured: true,
|
|
apiKeyTail: '1234',
|
|
},
|
|
mediaProviders: {},
|
|
agentModels: {},
|
|
agentCliEnv: {},
|
|
};
|
|
}
|
|
|
|
function pendingAuthorizationStorage() {
|
|
return {
|
|
github: {
|
|
expiresAt: '2099-01-01T00:00:00.000Z',
|
|
},
|
|
};
|
|
}
|
|
|
|
function connectorCard(scope: Page | Locator, id: string) {
|
|
return scope.locator(`article.connector-card[data-connector-id="${id}"]`);
|
|
}
|
|
|
|
async function waitForLoadingToClear(page: Page) {
|
|
await expect(page.getByText('Loading Open Design…')).toHaveCount(0, { timeout: 15_000 });
|
|
}
|
|
|
|
async function gotoEntryHome(page: Page) {
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingToClear(page);
|
|
const privacyDialog = page.getByRole('dialog').filter({ hasText: 'Help us improve Open Design' });
|
|
if (await privacyDialog.isVisible()) {
|
|
await privacyDialog.getByRole('button', { name: /not now|don't share/i }).click();
|
|
}
|
|
await expect(page.getByTestId('home-hero')).toBeVisible();
|
|
}
|
|
|
|
async function openSettingsDialogFromEntry(page: Page) {
|
|
await waitForLoadingToClear(page);
|
|
await page.getByRole('button', { name: OPEN_SETTINGS_LABEL }).click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible();
|
|
return dialog;
|
|
}
|
|
|
|
async function openConnectorsSettings(
|
|
page: Page,
|
|
{
|
|
connectors = CONNECTORS,
|
|
onPrepare = () => ({
|
|
results: { github: { status: 'ready', authConfigId: 'cfg_123' } },
|
|
}),
|
|
onConnect = () => ({
|
|
status: 200,
|
|
body: {
|
|
connector: {
|
|
...CONNECTORS[0],
|
|
status: 'connected',
|
|
accountLabel: 'octo-user',
|
|
},
|
|
auth: { kind: 'connected' },
|
|
},
|
|
}),
|
|
onCancel = () => ({
|
|
status: 200,
|
|
body: {
|
|
connector: {
|
|
...CONNECTORS[0],
|
|
status: 'available',
|
|
},
|
|
},
|
|
}),
|
|
statusResponse = () => ({
|
|
github: pendingAuthorization
|
|
? { status: 'error', accountLabel: undefined }
|
|
: { status: CONNECTORS[0]?.status ?? 'available', accountLabel: undefined },
|
|
slack: { status: 'connected', accountLabel: 'design-team' },
|
|
}),
|
|
pendingAuthorization = null,
|
|
blockPopup = false,
|
|
}: {
|
|
connectors?: readonly ConnectorFixture[];
|
|
onPrepare?: () => Record<string, unknown>;
|
|
onConnect?: () => { status: number; body: Record<string, unknown> };
|
|
onCancel?: () => { status: number; body: Record<string, unknown> };
|
|
statusResponse?: () => Record<string, unknown>;
|
|
pendingAuthorization?: Record<string, unknown> | null;
|
|
blockPopup?: boolean;
|
|
} = {},
|
|
) {
|
|
let cancelRequestCount = 0;
|
|
await page.addInitScript(
|
|
({ key, value, pendingAuthorization, blockPopup }) => {
|
|
window.localStorage.setItem(key, JSON.stringify(value));
|
|
if (pendingAuthorization) {
|
|
window.sessionStorage.setItem(
|
|
'od-connectors-authorization-pending',
|
|
JSON.stringify(pendingAuthorization),
|
|
);
|
|
}
|
|
window.open = (blockPopup
|
|
? (() => null)
|
|
: (() => ({
|
|
document: { title: '', body: { innerHTML: '' } },
|
|
location: { replace() {} },
|
|
close() {},
|
|
}))) as unknown as typeof window.open;
|
|
},
|
|
{ key: STORAGE_KEY, value: baseConfig(), pendingAuthorization, blockPopup },
|
|
);
|
|
|
|
await page.route('**/api/health', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: '{"ok":true}',
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/agents', async (route) => {
|
|
await route.fulfill({
|
|
json: {
|
|
agents: [
|
|
{
|
|
id: 'codex',
|
|
name: 'Codex CLI',
|
|
bin: 'codex',
|
|
available: true,
|
|
version: '0.130.0',
|
|
models: [{ id: 'default', label: 'Default' }],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/app-config', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ json: { config: baseConfig() } });
|
|
return;
|
|
}
|
|
await route.fulfill({ json: { ok: true } });
|
|
});
|
|
|
|
await page.route('**/api/connectors', async (route) => {
|
|
await route.fulfill({ json: { connectors } });
|
|
});
|
|
|
|
await page.route('**/api/connectors/status', async (route) => {
|
|
await route.fulfill({ json: { statuses: statusResponse() } });
|
|
});
|
|
|
|
await page.route('**/api/connectors/discovery*', async (route) => {
|
|
await route.fulfill({
|
|
json: {
|
|
connectors,
|
|
meta: { provider: 'composio' },
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/connectors/composio/config', async (route) => {
|
|
await route.fulfill({ json: { configured: true, apiKeyTail: '1234' } });
|
|
});
|
|
|
|
await page.route('**/api/connectors/auth-configs/prepare', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(onPrepare()),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/connectors/github/connect', async (route) => {
|
|
const response = onConnect();
|
|
await route.fulfill({
|
|
status: response.status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(response.body),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/connectors/github/authorization/cancel*', async (route) => {
|
|
cancelRequestCount += 1;
|
|
const response = onCancel();
|
|
await route.fulfill({
|
|
status: response.status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(response.body),
|
|
});
|
|
});
|
|
|
|
await gotoEntryHome(page);
|
|
const dialog = await openSettingsDialogFromEntry(page);
|
|
await dialog.getByRole('button', { name: /Connectors|连接器/i }).click();
|
|
await expect(dialog.getByTestId('connector-grid-wrap')).toBeVisible();
|
|
await expect(connectorCard(dialog, 'github')).toBeVisible();
|
|
return { dialog, getCancelRequestCount: () => cancelRequestCount };
|
|
}
|
|
|
|
test.describe('Settings connectors auth recovery', () => {
|
|
test('keeps a pending authorization visible when the connector enters authorization-pending state', async ({ page }) => {
|
|
const { dialog } = await openConnectorsSettings(page, {
|
|
pendingAuthorization: pendingAuthorizationStorage(),
|
|
});
|
|
|
|
const githubCard = connectorCard(dialog, 'github');
|
|
await expect(githubCard.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
|
await expect
|
|
.poll(async () =>
|
|
page.evaluate(() => {
|
|
const raw = window.sessionStorage.getItem('od-connectors-authorization-pending');
|
|
if (!raw) return false;
|
|
const parsed = JSON.parse(raw) as Record<string, { expiresAt?: string }>;
|
|
return typeof parsed.github?.expiresAt === 'string' && parsed.github.expiresAt.length > 0;
|
|
}),
|
|
)
|
|
.toBe(true);
|
|
});
|
|
|
|
|
|
test('shows a continue-in-browser CTA for pending authorizations that include a redirect URL', async ({ page }) => {
|
|
const { dialog } = await openConnectorsSettings(page, {
|
|
pendingAuthorization: {
|
|
github: {
|
|
expiresAt: '2099-01-01T00:00:00.000Z',
|
|
redirectUrl: 'https://example.com/oauth/github',
|
|
},
|
|
},
|
|
});
|
|
|
|
const githubCard = connectorCard(dialog, 'github');
|
|
await expect(githubCard.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
|
await expect(githubCard.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
|
|
});
|
|
|
|
test('settles a pending authorization into Disconnect when status polling reports the connector as connected', async ({ page }) => {
|
|
let statusRequests = 0;
|
|
const { dialog } = await openConnectorsSettings(page, {
|
|
pendingAuthorization: pendingAuthorizationStorage(),
|
|
statusResponse: () => {
|
|
statusRequests += 1;
|
|
return {
|
|
github: statusRequests >= 2
|
|
? { status: 'connected', accountLabel: 'octo-user' }
|
|
: { status: 'error', accountLabel: undefined },
|
|
slack: { status: 'connected', accountLabel: 'design-team' },
|
|
};
|
|
},
|
|
});
|
|
|
|
const githubCard = connectorCard(dialog, 'github');
|
|
|
|
await expect
|
|
.poll(async () => statusRequests, { timeout: 5000 })
|
|
.toBeGreaterThanOrEqual(2);
|
|
await expect(githubCard.getByRole('button', { name: 'Disconnect' })).toBeVisible();
|
|
await expect(githubCard.getByRole('button', { name: 'Cancel' })).toHaveCount(0);
|
|
await expect
|
|
.poll(async () =>
|
|
page.evaluate(() => window.sessionStorage.getItem('od-connectors-authorization-pending')),
|
|
)
|
|
.toBe(null);
|
|
});
|
|
|
|
test('returns a pending authorization to Connect and clears session storage after a successful cancel', async ({ page }) => {
|
|
const { dialog } = await openConnectorsSettings(page, {
|
|
pendingAuthorization: pendingAuthorizationStorage(),
|
|
onCancel: () => ({
|
|
status: 200,
|
|
body: {
|
|
connector: {
|
|
...CONNECTORS[0],
|
|
status: 'available',
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
const githubCard = connectorCard(dialog, 'github');
|
|
const cancelButton = githubCard.getByRole('button', { name: 'Cancel' });
|
|
await expect(cancelButton).toBeVisible();
|
|
|
|
await cancelButton.click();
|
|
|
|
await expect(githubCard.getByRole('button', { name: 'Connect' })).toBeVisible();
|
|
await expect(githubCard.getByRole('button', { name: 'Cancel' })).toHaveCount(0);
|
|
await expect
|
|
.poll(async () =>
|
|
page.evaluate(() => window.sessionStorage.getItem('od-connectors-authorization-pending')),
|
|
)
|
|
.toBe(null);
|
|
});
|
|
|
|
|
|
test('surfaces a connector error state when credentials have degraded', async ({ page }) => {
|
|
const githubConnector = CONNECTORS[0];
|
|
const slackConnector = CONNECTORS[1];
|
|
if (!githubConnector || !slackConnector) throw new Error('missing connector fixtures');
|
|
const degradedConnectors: ConnectorFixture[] = [
|
|
{
|
|
...githubConnector,
|
|
status: 'error',
|
|
accountLabel: 'octo-user',
|
|
lastError: 'GitHub token expired. Reconnect to continue.',
|
|
},
|
|
slackConnector,
|
|
];
|
|
const { dialog } = await openConnectorsSettings(page, {
|
|
connectors: degradedConnectors,
|
|
});
|
|
|
|
const githubCard = connectorCard(dialog, 'github');
|
|
await expect(githubCard).toHaveClass(/status-error/);
|
|
await expect(githubCard.locator('.connector-status-pill.status-error')).toBeVisible();
|
|
await expect(githubCard.getByRole('button', { name: 'Disconnect' })).toHaveCount(0);
|
|
});
|
|
|
|
});
|