Repository Public
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