SPB Git

Repository Public

codex.spec.ts 8.4 KB 147 lines typescript

1import { expect, test } from '@playwright/test';2import { watch, waitTermOpen } from './helpers';3import { setTheme, shot } from './shot-helpers';4import { CLAUDE_WEB, codexLogin, expectNoClaude, resetCodex, seedCodex, token } from './codex-helpers';56/**7 * The SAME static export served as a "Codex Remote" instance: /auth/status announces the codex8 * branding (mock: MOCK_AGENT=codex) and the static server remaps the manifest and the icons.9 */10test('codex: branding before and after sign-in, accent, manifest', async ({ page, request }) => {11  const w = watch(page);12  await resetCodex(page);1314  // first visit: nothing cached — the server answer brands the page15  await page.goto('/login/');16  await expect(page.locator('html')).toHaveAttribute('data-agent', 'codex');17  await expect(page).toHaveTitle('Codex Remote');18  await expect(page.getByText('Sign in to Codex Remote')).toBeVisible();19  await expect(page.locator('meta[name="apple-mobile-web-app-title"]')).toHaveAttribute('content', 'Codex Remote');20  expect(JSON.parse((await page.evaluate(() => localStorage.getItem('cr-branding'))) ?? 'null')).toEqual({ agent: 'codex', productName: 'Codex Remote', agentName: 'Codex' });21  await expectNoClaude(page, '/login/');2223  // second visit: the blocking /theme-init.js applies the cached branding before any API call24  await page.route('**/api/v1/auth/status', async (route) => { await new Promise((r) => setTimeout(r, 1500)); await route.continue(); });25  await page.goto('/login/', { waitUntil: 'domcontentloaded' });26  expect(await page.evaluate(() => [document.documentElement.getAttribute('data-agent'), document.title])).toEqual(['codex', 'Codex Remote']);27  await page.unroute('**/api/v1/auth/status');2829  // accent: emerald/teal, not the Claude indigo (compared with the Claude instance of the same build)30  const codexAccent = await token(page, '--color-accent');31  expect(codexAccent).toBe('#0b7268');32  const other = await page.context().newPage();33  await other.goto(`${CLAUDE_WEB}/login/`);34  await expect(other.locator('html')).toHaveAttribute('data-agent', 'claude');35  await expect(other).toHaveTitle('Claude Remote');36  expect(await token(other, '--color-accent')).toBe('#5151e8');37  const buttonBg = (p: typeof page) => p.getByTestId('login-submit').evaluate((el) => getComputedStyle(el).backgroundImage + getComputedStyle(el).backgroundColor);38  await expect(page.getByTestId('login-submit')).toBeVisible();39  expect(await buttonBg(page)).not.toBe(await buttonBg(other));40  expect(await buttonBg(page)).toContain('rgb(4, 120, 87)');41  await other.close();4243  // PWA assets of the product (same URLs, served from out/brand/codex/)44  const manifest = await (await request.get('/manifest.webmanifest')).json();45  expect(manifest.name).toBe('Codex Remote');46  expect(manifest.short_name).toBe('Codex Remote');47  expect(manifest.icons[0].src).toBe('/icons/icon-192.png');48  const [codexIcon, claudeIcon] = await Promise.all([request.get('/icons/icon.svg'), request.get(`${CLAUDE_WEB}/icons/icon.svg`)]);49  expect(await codexIcon.text()).toContain('#047857');50  expect(await claudeIcon.text()).toContain('#5B5BF0');51  expect((await (await request.get(`${CLAUDE_WEB}/manifest.webmanifest`)).json()).name).toBe('Claude Remote');5253  // signed in54  await codexLogin(page);55  await page.goto('/terminal/');56  await expect(page.getByTestId('start-view')).toBeVisible();57  await expect(page.getByTestId('wordmark')).toHaveText('Codex Remote');58  await expect(page).toHaveTitle('Codex Remote');59  await expect(page.getByTestId('start-view')).toContainText('Start a Codex or shell session');60  await expectNoClaude(page, '/terminal/ (start view)');6162  // new-session dialog: the agent kind is Codex, the resume list holds Codex session ids (UUIDs)63  await page.getByTestId('new-session').click();64  await expect(page.getByTestId('kind-claude')).toContainText('Codex');65  await expect(page.getByTestId('kind-claude')).toContainText('The Codex CLI');66  await expect(page.getByText('codex resume --last in this directory')).toBeVisible();67  await page.getByText('Resume a previous Codex conversation').click();68  await expect(page.getByTestId('conversation-list').getByRole('option')).toHaveCount(2);69  await expectNoClaude(page, 'new-session dialog');70  await page.getByTestId('conversation-list').getByRole('option').first().click();71  await page.getByTestId('create-session').click();72  await page.waitForURL(/\/terminal\/\?s=/);73  const id = new URL(page.url()).searchParams.get('s')!;74  await waitTermOpen(page, id);75  const created = (await (await request.get(`/api/v1/sessions`, { headers: { cookie: (await page.context().cookies()).map((c) => `${c.name}=${c.value}`).join('; ') } })).json()) as { id: string; title: string; kind: string; claudeConversationId: string | null }[];76  const mine = created.find((s) => s.id === id)!;77  expect(mine.kind).toBe('claude'); // wire value unchanged78  expect(mine.title).toBe('Codex · demo');79  expect(mine.claudeConversationId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/);80  // client navigations keep the product title81  await page.getByRole('link', { name: 'Sessions' }).first().click();82  await expect(page.getByTestId('sessions-list')).toBeVisible();83  await expect(page).toHaveTitle('Codex Remote');84  await expectNoClaude(page, '/sessions/');85  w.expectClean();86});8788test('codex: system, MCP and skills pages follow the agent capabilities', async ({ page }) => {89  const w = watch(page);90  await resetCodex(page);91  await codexLogin(page);9293  await page.goto('/system/');94  await expect(page.getByTestId('claude-info')).toBeVisible();95  await expect(page.getByRole('heading', { name: 'Codex', exact: true })).toBeVisible();96  await expect(page.getByTestId('agent-login-hint')).toHaveText('Run codex login in a shell session.');97  await expect(page.getByText('Health of Codex Remote itself')).toBeVisible();98  await expectNoClaude(page, '/system/');99100  await page.goto('/mcp/');101  await expect(page.getByTestId('mcp-list')).toBeVisible();102  await expect(page.getByText('configured for Codex.')).toBeVisible();103  await expect(page.getByTestId('mcp-live-state')).toHaveText('Codex does not report live server status');104  await expect(page.getByRole('button', { name: 'Check status' })).toHaveCount(0);105  for (const badge of await page.getByTestId('mcp-status').all()) await expect(badge).toHaveText('Unknown'); // no live check106  await page.getByTestId('add-mcp').click();107  await expect(page.getByTestId('mcp-name')).toBeVisible();108  const dialog = page.getByRole('dialog');109  await expect(dialog.getByText('Scope')).toHaveCount(0); // no project scope110  await dialog.getByRole('combobox').first().click();111  await expect(page.getByRole('option', { name: 'stdio (local command)' })).toBeVisible();112  await expect(page.getByRole('option', { name: 'http' })).toBeVisible();113  await expect(page.getByRole('option', { name: 'sse' })).toHaveCount(0); // no SSE transport114  await page.getByRole('option', { name: 'http' }).click();115  await expect(dialog.getByText('URL')).toBeVisible();116  await expect(dialog.getByText('Headers')).toHaveCount(0); // `codex mcp add` takes no custom headers117  await page.getByTestId('mcp-name').fill('codex-http');118  await page.getByTestId('mcp-target').fill('https://mcp.example.invalid/x');119  await page.getByTestId('mcp-submit').click();120  await expect(page.getByTestId('mcp-row')).toHaveCount(3);121  await expectNoClaude(page, '/mcp/');122123  await page.goto('/skills/');124  await expect(page.getByTestId('skills-list')).toBeVisible();125  await expect(page.getByText('Skills and sub-agents installed for Codex on the server.')).toBeVisible();126  await expect(page.getByTestId('skill-row')).toHaveCount(2); // the slash command fixture is not a Codex kind127  await expect(page.getByTestId('skills-list')).not.toContainText('Command');128  await expectNoClaude(page, '/skills/');129  w.expectClean();130});131132for (const theme of ['light'] as const) {133  test(`codex screenshots: desktop ${theme}`, async ({ page }) => {134    await setTheme(page, theme);135    const ids = await seedCodex(page);136    expect(ids.agentTitle).toBe('Codex · demo');137    await page.goto('/terminal/');138    await expect(page.getByTestId('start-view')).toBeVisible();139    await expect(page.getByTestId('session-card').first()).toBeVisible();140    await expect(page.getByTestId('host-strip')).toContainText('as of');141    await expect(page.locator('html')).toHaveAttribute('data-agent', 'codex');142    await page.waitForTimeout(700);143    await expectNoClaude(page, 'desktop start');144    await shot(page, `codex-${theme}-desktop-start`);145  });146}147
Files main
Menu

Theme