Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pids
# Coverage directory used by tools like istanbul
coverage/

# Playwright artifacts
playwright-report/
test-results/

# Dependency directories
.npm
.pnpm
Expand Down
8 changes: 7 additions & 1 deletion docs/E2E-TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ npx playwright install chromium
## Running Tests

```bash
# Run with Playwright-managed dev server (recommended)
npm run test:e2e

# If you want to reuse an already-running dev server, start it on 5177:
# npm run dev -- --port 5177 --strictPort --host 127.0.0.1
# then run:
# Run all E2E tests
npm run test:e2e

Expand Down Expand Up @@ -103,7 +109,7 @@ These logs are visible in:

**Fixes Applied**:
- Restored missing `DiffResultsRepo.ts` file
- Fixed port configuration (5173 everywhere)
- Fixed port configuration (dedicated 5177 for E2E)
- Fixed database name ('lexicon-forge' instead of 'LexiconForge')
- Updated test assertions to match actual log messages

Expand Down
85 changes: 0 additions & 85 deletions playwright-report/index.html

This file was deleted.

19 changes: 10 additions & 9 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ export default defineConfig({

// Reporter to use
reporter: [
['html'],
['html', { open: 'never' }],
['list']
],

use: {
// Base URL to use in actions like `await page.goto('/')`
baseURL: 'http://localhost:5173',
// Use a dedicated port to avoid collisions with other local dev servers.
baseURL: 'http://127.0.0.1:5177',

// Collect trace when retrying the failed test
trace: 'on-first-retry',
Expand All @@ -52,11 +53,11 @@ export default defineConfig({
},
],

// Use existing dev server instead of starting a new one
// webServer: {
// command: 'npm run dev',
// url: 'http://localhost:5177',
// reuseExistingServer: true,
// timeout: 120 * 1000,
// },
// Start a dedicated dev server for E2E to avoid “wrong app on same port” issues.
webServer: {
command: 'npm run dev -- --port 5177 --strictPort --host 127.0.0.1',
url: 'http://127.0.0.1:5177',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
157 changes: 157 additions & 0 deletions tests/e2e/chapterview-large.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { test, expect } from '@playwright/test';
import { importSessionFromFile, prepareFreshApp } from './helpers/sessionHarness';

const buildLargeTranslationHtml = (paragraphCount: number, fillerRepeat: number): string => {
const filler = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(fillerRepeat).trim();
const paragraphs: string[] = [];
for (let i = 0; i < paragraphCount; i += 1) {
paragraphs.push(`Paragraph ${i + 1}: ${filler}`);
}
return paragraphs.join('<br><br>');
};

test.describe('ChapterView (E2E) — Large Translation + Diff Navigation', () => {
// This spec does a fresh app boot + session import; give the beforeEach enough headroom
// when running alongside other parallel E2E tests.
test.describe.configure({ timeout: 60_000 });

test.beforeEach(async ({ page }) => {
await prepareFreshApp(page, {
// Enable diff heatmap to validate marker layout in real browser geometry.
appSettings: { showDiffHeatmap: true },
});
});

test('imports a large session and renders within a reasonable time', async ({ page }) => {
const chapterId = 'fixture-large-chapter';
const paragraphCount = 120;
const translation = buildLargeTranslationHtml(paragraphCount, 12);

const sessionData = {
metadata: { format: 'lexiconforge-full-1', exportedAt: new Date().toISOString() },
settings: null,
urlMappings: [],
novels: [],
promptTemplates: [],
amendmentLogs: [],
chapters: [
{
stableId: chapterId,
url: 'https://example.com/large',
canonicalUrl: 'https://example.com/large',
title: 'Large Fixture Chapter',
content: 'Raw content (fixture)',
nextUrl: null,
prevUrl: null,
chapterNumber: 1,
fanTranslation: null,
feedback: [],
translations: [
{
id: 'translation-1',
version: 1,
translatedTitle: 'Large Fixture Chapter',
translation,
proposal: null,
footnotes: [],
suggestedIllustrations: [],
usageMetrics: {
totalTokens: 100,
promptTokens: 60,
completionTokens: 40,
estimatedCost: 0.0,
requestTime: 0,
provider: 'Gemini',
model: 'gemini-2.5-flash',
},
provider: 'Gemini',
model: 'gemini-2.5-flash',
temperature: 0.7,
systemPrompt: 'fixture',
promptId: null,
promptName: null,
isActive: true,
createdAt: new Date().toISOString(),
},
],
},
],
diffResults: [
{
chapterId,
aiVersionId: 'ai-v1',
fanVersionId: null,
rawVersionId: 'raw-v1',
algoVersion: '1.0.0',
analyzedAt: Date.now(),
costUsd: 0,
model: 'gpt-4o-mini',
markers: [
{
chunkId: 'fixture-marker-0',
colors: ['blue'],
reasons: ['fan-divergence'],
explanations: ['Fixture marker'],
aiRange: { start: 0, end: 10 },
position: 0,
},
{
chunkId: 'fixture-marker-mid',
colors: ['orange'],
reasons: ['raw-divergence'],
explanations: ['Fixture marker'],
aiRange: { start: 0, end: 10 },
position: 60,
},
{
chunkId: 'fixture-marker-last',
colors: ['grey'],
reasons: ['stylistic'],
explanations: ['Fixture marker'],
aiRange: { start: 0, end: 10 },
position: 119,
},
],
},
],
};

const t0 = Date.now();
await importSessionFromFile(page, sessionData);

await page
.locator('span[data-lf-type="text"][data-lf-chunk]')
.first()
.waitFor({ state: 'visible' });

const ms = Date.now() - t0;
console.log(`[E2E] Large session import+render took ${ms}ms`);
expect(ms).toBeLessThan(15_000);

const paragraphs = page.locator('[data-testid^="diff-paragraph-"]');
await expect(paragraphs).toHaveCount(paragraphCount);

const pips = page.locator('[data-testid^="diff-pip-"]');
await expect(pips).toHaveCount(3);

// Clicking a later marker should scroll the corresponding paragraph into view.
const lastPip = pips.nth(2);
const pos = await lastPip.getAttribute('data-diff-position');
expect(pos).toBe('119');

const lastPara = page.locator(`[data-testid^="diff-paragraph-"][data-diff-position=\"${pos}\"]`);
await lastPip.locator('button').first().click();

// Verify the paragraph intersects viewport (scroll happened).
await expect
.poll(async () => {
return await lastPara.evaluate((el) => {
const r = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
return r.bottom > 0 && r.top < vh;
});
}, { timeout: 5_000 })
.toBe(true);
});
});

93 changes: 93 additions & 0 deletions tests/e2e/chapterview-media.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { test, expect } from '@playwright/test';
import { importSessionFromFile, prepareFreshApp } from './helpers/sessionHarness';

const ONE_BY_ONE_PNG =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=';

test.describe('ChapterView (E2E) — Media Rendering', () => {
test.beforeEach(async ({ page }) => {
await prepareFreshApp(page);
});

test('renders inline illustrations from suggestedIllustrations', async ({ page }) => {
const chapterId = 'fixture-media-chapter';
const sessionData = {
metadata: { format: 'lexiconforge-full-1', exportedAt: new Date().toISOString() },
settings: null,
urlMappings: [],
novels: [],
promptTemplates: [],
chapters: [
{
stableId: chapterId,
url: 'https://example.com/media',
canonicalUrl: 'https://example.com/media',
title: 'Media Fixture Chapter',
content: 'Raw content (fixture)',
nextUrl: null,
prevUrl: null,
chapterNumber: 1,
fanTranslation: null,
feedback: [],
translations: [
{
id: 'translation-1',
version: 1,
translatedTitle: 'Media Fixture Chapter',
translation: 'Some text.<br><br>[ILLUSTRATION-1]<br><br>More text.',
proposal: null,
footnotes: [],
suggestedIllustrations: [
{
placementMarker: '[ILLUSTRATION-1]',
imagePrompt: 'A tiny red square.',
generatedImage: {
imageData: ONE_BY_ONE_PNG,
requestTime: 0,
cost: 0,
metadata: {
version: 1,
prompt: 'A tiny red square.',
generatedAt: new Date().toISOString(),
},
},
},
],
usageMetrics: {
totalTokens: 42,
promptTokens: 20,
completionTokens: 22,
estimatedCost: 0,
requestTime: 0,
provider: 'Gemini',
model: 'gemini-2.5-flash',
},
provider: 'Gemini',
model: 'gemini-2.5-flash',
temperature: 0.7,
systemPrompt: 'fixture',
promptId: null,
promptName: null,
isActive: true,
createdAt: new Date().toISOString(),
},
],
},
],
diffResults: [],
amendmentLogs: [],
};

await importSessionFromFile(page, sessionData);

// Illustration should render as an <img> from the persisted base64 payload.
const illustration = page.getByAltText('A tiny red square.').first();
await expect(illustration).toBeVisible();
await expect(illustration).toHaveAttribute('src', ONE_BY_ONE_PNG);

// Audio player should render and expose the generate button (enabled by default task type).
const audioGenerateButton = page.locator('button[title^=\"Generate background music\"]').first();
await expect(audioGenerateButton).toBeVisible();
await expect(audioGenerateButton).toBeEnabled();
});
});
8 changes: 5 additions & 3 deletions tests/e2e/debug-console.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { test, expect } from '@playwright/test';

test('capture console logs on page load', async ({ page }) => {
const testDiagnostics = process.env.LF_E2E_DIAGNOSTICS === '1' ? test : test.skip;

testDiagnostics('capture console logs on page load', async ({ page }) => {
const logs: string[] = [];

page.on('console', msg => {
Expand All @@ -15,10 +17,10 @@ test('capture console logs on page load', async ({ page }) => {
console.log(text);
});

console.log('Navigating to http://localhost:5173/...');
console.log('Navigating to baseURL / ...');

try {
await page.goto('http://localhost:5173/', {
await page.goto('/', {
waitUntil: 'domcontentloaded',
timeout: 10000
});
Expand Down
Loading
Loading