|
| 1 | +import { expect, test } from '@playwright/test' |
| 2 | + |
| 3 | +async function fixtures(page) { |
| 4 | + const errors = [] |
| 5 | + page.on('pageerror', error => errors.push(error.message)) |
| 6 | + const paper = { id: 'paper', title: 'Attention and memory', authors: ['Ada Researcher'], status: 'inbox', year: 2026, venue: 'NeurIPS', collections: [], createdAt: '2026-10-20T12:00:00Z' } |
| 7 | + const project = { id: 'project', name: 'Memory research', description: 'A study of attention and memory.', status: 'active' } |
| 8 | + const notes = [ |
| 9 | + { id: 'note', name: 'Research notes', type: 'file', parentId: null, content: '<h1>Research notes</h1><p>Evidence and ideas.</p><pre><code>const result = 42</code></pre><table><tbody><tr><th>Result</th><th>Value</th></tr><tr><td>Accuracy</td><td>95%</td></tr></tbody></table>' }, |
| 10 | + { id: 'canvas', name: 'Ideas.canvas', type: 'canvas', parentId: null, content: JSON.stringify({ nodes: [ |
| 11 | + { id: 'text', type: 'text', x: 0, y: 0, width: 300, height: 200, text: '# A connected idea\n\nSupporting evidence.' }, |
| 12 | + { id: 'file', type: 'file', x: 400, y: 0, width: 300, height: 200, file: 'Research notes', researchos: { noteId: 'note' } }, |
| 13 | + ], edges: [{ id: 'edge', fromNode: 'text', toNode: 'file', fromSide: 'right', toSide: 'left', label: 'supports' }] }) }, |
| 14 | + ] |
| 15 | + await page.route('**/api/**', route => { |
| 16 | + const path = new URL(route.request().url()).pathname |
| 17 | + let json = [] |
| 18 | + if (path === '/api/libraries') json = [{ id: 'lib', name: 'Research' }] |
| 19 | + if (path === '/api/papers') json = [paper] |
| 20 | + if (path === '/api/papers/paper') json = paper |
| 21 | + if (path === '/api/libraries/lib/notes') json = notes |
| 22 | + if (path === '/api/settings/models') json = { current: {}, defaults: {}, descriptions: {}, available_chat_models: [], available_embedding_models: [] } |
| 23 | + if (path === '/api/projects') json = [project] |
| 24 | + if (path === '/api/projects/project') json = project |
| 25 | + if (path === '/api/projects/project/notes') json = notes |
| 26 | + if (path === '/api/projects/project/task-columns') json = [{ id: 'todo', projectId: 'project', name: 'To do', color: '#93c5fd', position: 0 }] |
| 27 | + if (path === '/api/projects/project/tasks') json = [{ id: 'task', projectId: 'project', columnId: 'todo', title: 'Review evidence', position: 0, priority: 'medium', tags: [], customFields: {} }] |
| 28 | + if (path === '/api/authors') json = [{ id: 'ada', name: 'Ada Researcher', paperCount: 1 }] |
| 29 | + if (path === '/api/labs') json = { items: [], total: 0, limit: 50, offset: 0 } |
| 30 | + return route.fulfill({ json }) |
| 31 | + }) |
| 32 | + return errors |
| 33 | +} |
| 34 | + |
| 35 | +test('theme choice persists, follows system changes, and works in the collapsed sidebar', async ({ page, context }) => { |
| 36 | + await fixtures(page) |
| 37 | + await page.emulateMedia({ colorScheme: 'dark' }) |
| 38 | + await page.goto('/library') |
| 39 | + const selector = page.getByLabel('Color theme') |
| 40 | + await expect(selector).toHaveValue('system') |
| 41 | + await expect(page.locator('html')).toHaveClass('dark') |
| 42 | + await selector.selectOption('light') |
| 43 | + await page.reload() |
| 44 | + await expect(page.locator('html')).not.toHaveClass('dark') |
| 45 | + await expect(selector).toHaveValue('light') |
| 46 | + await page.getByTitle('Collapse sidebar').click() |
| 47 | + await selector.selectOption('dark') |
| 48 | + await expect(page.locator('html')).toHaveClass('dark') |
| 49 | + // A second tab shares the saved preference and receives live updates. |
| 50 | + const other = await context.newPage() |
| 51 | + await fixtures(other) |
| 52 | + await other.goto('/authors') |
| 53 | + await expect(other.locator('html')).toHaveClass('dark') |
| 54 | + await selector.selectOption('light') |
| 55 | + await expect(other.locator('html')).not.toHaveClass('dark') |
| 56 | + await other.close() |
| 57 | + await selector.selectOption('system') |
| 58 | + await page.emulateMedia({ colorScheme: 'light' }) |
| 59 | + await expect(page.locator('html')).not.toHaveClass('dark') |
| 60 | + await page.emulateMedia({ colorScheme: 'dark' }) |
| 61 | + await expect(page.locator('html')).toHaveClass('dark') |
| 62 | +}) |
| 63 | + |
| 64 | +test('dark surfaces cover the application, dialogs, and charts', async ({ page }, testInfo) => { |
| 65 | + test.setTimeout(60000) |
| 66 | + const errors = await fixtures(page) |
| 67 | + await page.addInitScript(() => localStorage.setItem('researchos.theme', 'dark')) |
| 68 | + for (const route of ['/dashboard', '/library', '/authors', '/labs', '/agents', '/projects', '/projects/project', '/projects/project/literature', '/projects/project/experiments', '/projects/project/tasks', '/projects/project/notes', '/proposals', '/library/settings', '/library/map', '/library/paper/paper']) { |
| 69 | + await page.goto(route) |
| 70 | + await expect(page.getByLabel('Color theme').first(), route).toBeVisible() |
| 71 | + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(25, 25, 25)') |
| 72 | + await expect(page.locator('main'), route).not.toBeEmpty() |
| 73 | + await expect(page.locator('.theme-sidebar')).toHaveCSS('background-color', 'rgb(24, 24, 24)') |
| 74 | + // Check actual computed values, including panels rendered through portals. |
| 75 | + for (const panel of await page.locator('.bg-white:visible').all()) { |
| 76 | + await expect(panel).toHaveCSS('background-color', 'rgb(30, 30, 30)') |
| 77 | + } |
| 78 | + if (route === '/dashboard') { |
| 79 | + await expect(page.getByRole('heading', { name: /Added Over Time/ })).toBeVisible() |
| 80 | + await page.screenshot({ path: testInfo.outputPath('dashboard-dark.png') }) |
| 81 | + } |
| 82 | + if (route === '/projects/project/tasks') { |
| 83 | + await expect(page.getByText('Review evidence')).toBeVisible() |
| 84 | + await page.screenshot({ path: testInfo.outputPath('tasks-dark.png') }) |
| 85 | + } |
| 86 | + } |
| 87 | + await page.goto('/authors') |
| 88 | + await page.getByRole('button', { name: 'Add author' }).click() |
| 89 | + await expect(page.getByRole('combobox', { name: 'Name *' })).toHaveCSS('background-color', 'rgb(30, 30, 30)') |
| 90 | + await page.screenshot({ path: testInfo.outputPath('dialog-dark.png') }) |
| 91 | + expect(errors).toEqual([]) |
| 92 | +}) |
| 93 | + |
| 94 | +test('notes, graph, and canvas stay legible when switching themes without losing content', async ({ page }, testInfo) => { |
| 95 | + const errors = await fixtures(page) |
| 96 | + await page.addInitScript(() => localStorage.setItem('researchos.theme', 'dark')) |
| 97 | + await page.goto('/library/notes') |
| 98 | + await page.getByRole('button', { name: /Research notes/ }).first().click() |
| 99 | + await expect(page.locator('.tiptap-editor h1')).toHaveCSS('color', 'rgb(237, 237, 237)') |
| 100 | + await expect(page.locator('.tiptap-editor pre')).toHaveCSS('background-color', 'rgb(25, 25, 25)') |
| 101 | + await page.screenshot({ path: testInfo.outputPath('notes-dark.png') }) |
| 102 | + await page.getByRole('button', { name: /Ideas.canvas/ }).first().click() |
| 103 | + await expect(page.locator('.canvas-card').first()).toHaveCSS('background-color', 'rgb(30, 30, 30)') |
| 104 | + await expect(page.locator('.react-flow__controls-button').first()).toHaveCSS('background-color', 'rgb(30, 30, 30)') |
| 105 | + await expect(page.locator('.react-flow__minimap')).toHaveCSS('background-color', 'rgb(30, 30, 30)') |
| 106 | + await page.screenshot({ path: testInfo.outputPath('canvas-dark.png') }) |
| 107 | + await page.getByRole('button', { name: 'File / canvas', exact: true }).click() |
| 108 | + await expect(page.getByRole('dialog')).toHaveCSS('background-color', 'rgb(30, 30, 30)') |
| 109 | + await page.getByRole('dialog').getByRole('button', { name: 'Close' }).click() |
| 110 | + await page.getByLabel('Color theme').selectOption('light') |
| 111 | + await expect(page.locator('.canvas-card').first()).toHaveCSS('background-color', 'rgb(255, 255, 255)') |
| 112 | + await expect(page.locator('.canvas-card-content').getByText('Supporting evidence.')).toBeVisible() |
| 113 | + await page.getByLabel('Color theme').selectOption('dark') |
| 114 | + await page.getByRole('button', { name: /Graph/ }).first().click() |
| 115 | + await expect(page.locator('main svg text').filter({ hasText: 'Research notes' }).first()).toHaveCSS('fill', 'rgb(212, 212, 212)') |
| 116 | + await page.screenshot({ path: testInfo.outputPath('graph-dark.png') }) |
| 117 | + expect(errors).toEqual([]) |
| 118 | +}) |
0 commit comments