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
361 changes: 5 additions & 356 deletions tests/integration/ChapterView.critical-flows.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
/**
* ChapterView Critical User Flows
*
* TEST-QUALITY: 7.5/10 (Target: High, user-facing)
* This file intentionally contains ONLY Flow #1 (diff markers).
*
* Construct: "Users can view translations with diff markers, edit inline,
* handle large chapters, and use media without layout collapse."
*
* Addresses audit gaps:
* - ChapterView at 6.93% coverage (CRITICAL)
* - Tests actual user interactions, not just rendering
* - Decision-useful: blocks UI regressions users will notice
*
* These 4 flows cover 80% of daily user interactions.
* Other flows were split out to keep files small and to place layout/perf
* assertions into Playwright where the browser can provide real geometry:
* - Flow #2 (inline edit): `tests/integration/ChapterView.inline-edit.test.tsx`
* - Flow #3/#4 (perf + media/layout): `tests/e2e/chapterview.*.spec.ts`
*/

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
Expand Down Expand Up @@ -231,349 +226,3 @@ describe('ChapterView: Critical Flow #1 - Diff Markers Visible & Positioned', ()
scrollSpy.mockRestore();
});
});

describe('ChapterView: Critical Flow #2 - Inline Edit Preserves Markers', () => {
it.skip('[Flow 2] editing text updates markers or shows stale indicator', async () => {
const { useAppStore } = await import('../../store');
const mockUpdateFn = vi.fn();

vi.mocked(useAppStore).mockReturnValue({
settings: {
fontSize: 16,
fontStyle: 'sans',
lineHeight: 1.6,
showDiffHeatmap: true,
} as any,
updateTranslationInline: mockUpdateFn,
showNotification: vi.fn(),
} as any);

const translation = createMockTranslation(2);

const { container } = render(
<ChapterView
originalTitle="Test"
originalContent="Test"
translation={translation}
translationResult={{
translatedTitle: 'Test',
translation,
proposal: null,
footnotes: [],
suggestedIllustrations: [],
usageMetrics: {
totalTokens: 100,
promptTokens: 60,
completionTokens: 40,
estimatedCost: 0.001,
requestTime: 2,
provider: 'Gemini',
model: 'gemini-2.5-flash',
}
}}
/>
);

// Enable inline editing (double-click or button click)
const firstParagraph = container.querySelector('[data-lf-type="text"]') as HTMLElement;
fireEvent.doubleClick(firstParagraph);

await waitFor(() => {
// Should show edit mode (textarea or contentEditable)
const editField = container.querySelector('textarea') ||
container.querySelector('[contenteditable="true"]');
expect(editField).toBeTruthy();
});

// Type new text
const editField = container.querySelector('textarea') ||
container.querySelector('[contenteditable="true"]') as HTMLElement;

fireEvent.input(editField, { target: { value: 'Edited text content' } });

// Save (press Ctrl+Enter or click save button)
fireEvent.keyDown(editField, { key: 'Enter', ctrlKey: true });

await waitFor(() => {
// Should call update function
expect(mockUpdateFn).toHaveBeenCalled();

// Should show "stale" indicator or update markers
const staleIndicator = container.querySelector('[data-diff-stale]') ||
container.querySelector('.diff-stale-warning');

// Either markers update OR stale warning appears
const hasStaleWarning = staleIndicator !== null;
const markersUpdated = mockUpdateFn.mock.calls.length > 0;

expect(hasStaleWarning || markersUpdated).toBe(true);
});
});
});

describe('ChapterView: Critical Flow #3 - Large Chapter Performance', () => {
it.skip('[Flow 3] renders 50KB chapter without catastrophic slowdown', async () => {
// Generate large translation (~50KB)
const largeParagraph = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(100);
const largeTranslation = (largeParagraph + '<br><br>').repeat(100); // ~50KB

const startTime = Date.now();

render(
<ChapterView
originalTitle="Large Chapter"
originalContent="Large raw content"
translation={largeTranslation}
translationResult={{
translatedTitle: 'Large Chapter',
translation: largeTranslation,
proposal: null,
footnotes: [],
suggestedIllustrations: [],
usageMetrics: {
totalTokens: 50000,
promptTokens: 30000,
completionTokens: 20000,
estimatedCost: 0.5,
requestTime: 15,
provider: 'Gemini',
model: 'gemini-2.5-pro',
}
}}
/>
);

const renderTime = Date.now() - startTime;

// Performance guard: should render in under 3 seconds
// (This catches O(n²) algorithms and React key issues)
expect(renderTime).toBeLessThan(3000);

// Should render content
await waitFor(() => {
const content = document.querySelector('[data-chapter-content]') ||
document.querySelector('.chapter-view');
expect(content).toBeTruthy();
expect(content!.textContent!.length).toBeGreaterThan(1000);
});

// Layout should be valid (no NaN, no collapsed container)
const container = document.querySelector('[data-chapter-content]') as HTMLElement;
const height = container?.offsetHeight || 0;
expect(height).toBeGreaterThan(0);
expect(Number.isNaN(height)).toBe(false);
});

it.skip('[Flow 3] paragraph count matches expected for large chapter', async () => {
const paragraphCount = 200;
const translation = createMockTranslation(paragraphCount);

render(
<ChapterView
originalTitle="Test"
originalContent="Test"
translation={translation}
translationResult={{
translatedTitle: 'Test',
translation,
proposal: null,
footnotes: [],
suggestedIllustrations: [],
usageMetrics: {
totalTokens: 10000,
promptTokens: 6000,
completionTokens: 4000,
estimatedCost: 0.1,
requestTime: 10,
provider: 'Gemini',
model: 'gemini-2.5-flash',
}
}}
/>
);

await waitFor(() => {
// Count rendered paragraph nodes
const paragraphs = document.querySelectorAll('[data-lf-type="text"]');

// Should render approximately the right number
// (within 10% tolerance for chunking algorithm)
expect(paragraphs.length).toBeGreaterThan(paragraphCount * 0.9);
expect(paragraphs.length).toBeLessThan(paragraphCount * 1.1);
});
});
});

describe('ChapterView: Critical Flow #4 - Illustration + Audio Coexistence', () => {
it.skip('[Flow 4] renders illustration and audio player without layout collapse', async () => {
const translation = 'The hero arrived. [ILLUSTRATION-1] He drew his sword.';

render(
<ChapterView
originalTitle="Test"
originalContent="Test"
translation={translation}
translationResult={{
translatedTitle: 'Test',
translation,
proposal: null,
footnotes: [],
suggestedIllustrations: [
{
placementMarker: 'ILLUSTRATION-1',
imagePrompt: 'A hero drawing a sword',
generatedImage: {
imageData: 'data:image/png;base64,fakeimagedatahere',
requestTime: 5,
cost: 0.04,
}
}
],
usageMetrics: {
totalTokens: 100,
promptTokens: 60,
completionTokens: 40,
estimatedCost: 0.001,
requestTime: 2,
provider: 'Gemini',
model: 'gemini-2.5-flash',
}
}}
audioUrl="https://example.com/audio.mp3"
/>
);

await waitFor(() => {
// Should render illustration
const illustration = screen.queryByRole('img') ||
document.querySelector('[data-illustration]');
expect(illustration).toBeTruthy();

// Should render audio player
const audioPlayer = screen.queryByRole('audio') ||
document.querySelector('[data-audio-player]') ||
document.querySelector('audio');
expect(audioPlayer).toBeTruthy();
});

// Layout invariants: no collapse, no overflow
const container = document.querySelector('[data-chapter-content]') as HTMLElement;
const height = container?.offsetHeight || 0;
expect(height).toBeGreaterThan(0);
expect(Number.isNaN(height)).toBe(false);

// Containers should have valid dimensions
const illustration = document.querySelector('[data-illustration]') as HTMLElement;
if (illustration) {
const rect = illustration.getBoundingClientRect();
expect(rect.width).toBeGreaterThan(0);
expect(rect.height).toBeGreaterThan(0);
}
});

it.skip('[Flow 4] toggling audio/illustration updates UI correctly', async () => {
const translation = 'Test content [ILLUSTRATION-1]';

const { rerender } = render(
<ChapterView
originalTitle="Test"
originalContent="Test"
translation={translation}
translationResult={{
translatedTitle: 'Test',
translation,
proposal: null,
footnotes: [],
suggestedIllustrations: [
{
placementMarker: 'ILLUSTRATION-1',
imagePrompt: 'Test image',
}
],
usageMetrics: {
totalTokens: 100,
promptTokens: 60,
completionTokens: 40,
estimatedCost: 0.001,
requestTime: 2,
provider: 'Gemini',
model: 'gemini-2.5-flash',
}
}}
audioUrl={undefined}
/>
);

// Initially no audio
let audioPlayer = document.querySelector('audio');
expect(audioPlayer).toBeNull();

// Add audio
rerender(
<ChapterView
originalTitle="Test"
originalContent="Test"
translation={translation}
translationResult={{
translatedTitle: 'Test',
translation,
proposal: null,
footnotes: [],
suggestedIllustrations: [
{
placementMarker: 'ILLUSTRATION-1',
imagePrompt: 'Test image',
}
],
usageMetrics: {
totalTokens: 100,
promptTokens: 60,
completionTokens: 40,
estimatedCost: 0.001,
requestTime: 2,
provider: 'Gemini',
model: 'gemini-2.5-flash',
}
}}
audioUrl="https://example.com/audio.mp3"
/>
);

await waitFor(() => {
audioPlayer = document.querySelector('audio');
expect(audioPlayer).toBeTruthy();
});

// Layout should remain stable (no jumps)
const container = document.querySelector('[data-chapter-content]') as HTMLElement;
expect(container?.offsetHeight).toBeGreaterThan(0);
});
});

/**
* Implementation TODO (to raise score from 7.5 to 9.0):
*
* 1. Add accessibility tests:
* - Keyboard navigation through markers
* - Screen reader announcements
* - Focus management in edit mode
*
* 2. Add error state tests:
* - Illustration load failure
* - Audio playback error
* - Inline edit save failure
*
* 3. Add interaction sequences:
* - Navigate markers → edit inline → save → markers update
* - Play audio → scroll → audio continues
*
* 4. Add visual regression tests:
* - Snapshot diff gutter layout
* - Snapshot marker pip positioning
*
* Anti-Goodhart properties:
* - Tests user-facing behavior, not implementation details
* - Can't pass by mocking everything (layout checks are real)
* - Performance test catches algorithmic issues
* - Decision-useful: blocks regressions users will notice
*/
Loading
Loading