Skip to content

Commit cff187d

Browse files
tthallosclaude
andcommitted
chore(playground): add visual-test harness
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7e81d44 commit cff187d

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

playground/src/visual-test.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Visual regression test harness.
3+
*
4+
* Renders individual slides via SlideView without animations or transitions,
5+
* so Playwright can screenshot each slide for pixel-level comparison.
6+
*
7+
* URL params:
8+
* ?page=N — show slide N (1-based), default 1
9+
*
10+
* Exposes DOM attributes for Playwright:
11+
* #visual-test-root[data-status] — 'idle' | 'parsing' | 'loading-fonts' | 'ready' | 'error'
12+
* #visual-test-root[data-slide-count] — total slide count
13+
* #visual-test-root[data-current-page]— current page (1-based)
14+
* #slide-container — the div wrapping the SVG, screenshot target
15+
*/
16+
17+
import React, { useState, useCallback, useRef, useEffect } from 'react'
18+
import { createRoot } from 'react-dom/client'
19+
import { SlideView, usePresentation, useFonts } from '@pagus-kit/react'
20+
21+
function getPageFromUrl(): number {
22+
const params = new URLSearchParams(window.location.search)
23+
return Math.max(1, parseInt(params.get('page') ?? '1', 10))
24+
}
25+
26+
function VisualTestHarness() {
27+
const [file, setFile] = useState<File | null>(null)
28+
const [page, setPage] = useState(getPageFromUrl)
29+
const fileInputRef = useRef<HTMLInputElement>(null)
30+
31+
const { status: parseStatus, presentation, error } = usePresentation(file)
32+
const { status: fontStatus, fontSubstitutes } = useFonts(presentation?.fonts ?? undefined, {
33+
useGoogleFonts: true,
34+
useEmbeddedFonts: true,
35+
timeout: 15000,
36+
})
37+
38+
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
39+
const f = e.target.files?.[0]
40+
if (f) setFile(f)
41+
}, [])
42+
43+
// Listen for URL changes (Playwright navigates by changing ?page=N)
44+
useEffect(() => {
45+
const onPopState = () => setPage(getPageFromUrl())
46+
window.addEventListener('popstate', onPopState)
47+
return () => window.removeEventListener('popstate', onPopState)
48+
}, [])
49+
50+
// Expose goToPage globally for Playwright
51+
useEffect(() => {
52+
;(window as any).__visualTestGoToPage = (p: number) => {
53+
setPage(p)
54+
window.history.replaceState({}, '', `?page=${p}`)
55+
}
56+
}, [])
57+
58+
let status: string
59+
if (error) status = 'error'
60+
else if (!file) status = 'idle'
61+
else if (parseStatus === 'loading') status = 'parsing'
62+
else if (fontStatus === 'loading') status = 'loading-fonts'
63+
else if (parseStatus === 'ready' && (fontStatus === 'ready' || fontStatus === 'idle')) status = 'ready'
64+
else status = 'idle'
65+
66+
const slideCount = presentation?.slides.length ?? 0
67+
const slideIndex = Math.min(page - 1, slideCount - 1)
68+
const slide = presentation?.slides[slideIndex]
69+
70+
return (
71+
<div
72+
id="visual-test-root"
73+
data-status={status}
74+
data-slide-count={slideCount}
75+
data-current-page={slideIndex + 1}
76+
style={{ margin: 0, padding: 0, background: '#f0f0f0' }}
77+
>
78+
{/* File upload — hidden once file is loaded */}
79+
{!file && (
80+
<div style={{ padding: 20, textAlign: 'center' }}>
81+
<input
82+
ref={fileInputRef}
83+
type="file"
84+
accept=".pptx"
85+
onChange={handleFileChange}
86+
data-testid="file-input"
87+
/>
88+
</div>
89+
)}
90+
91+
{/* Status display */}
92+
{status === 'parsing' && <div style={{ padding: 20, textAlign: 'center', color: '#999' }}>Parsing...</div>}
93+
{status === 'loading-fonts' && <div style={{ padding: 20, textAlign: 'center', color: '#999' }}>Loading fonts...</div>}
94+
{status === 'error' && <div style={{ padding: 20, textAlign: 'center', color: 'red' }}>Error: {String(error)}</div>}
95+
96+
{/* Slide rendering — no animations, no transitions */}
97+
{status === 'ready' && slide && presentation && (
98+
<div
99+
id="slide-container"
100+
style={{
101+
display: 'inline-block',
102+
background: '#fff',
103+
}}
104+
>
105+
<SlideView
106+
slide={slide}
107+
slideSize={presentation.slideSize}
108+
scale={1}
109+
fontSubstitutes={fontSubstitutes}
110+
/* No hiddenShapeIds — all shapes visible */
111+
/>
112+
</div>
113+
)}
114+
</div>
115+
)
116+
}
117+
118+
const root = createRoot(document.getElementById('app')!)
119+
root.render(<VisualTestHarness />)

playground/visual-test.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Pagus Visual Regression Test</title>
7+
<style>
8+
* { margin: 0; padding: 0; box-sizing: border-box; }
9+
body { background: #f0f0f0; }
10+
</style>
11+
</head>
12+
<body>
13+
<div id="app"></div>
14+
<script type="module" src="/src/visual-test.tsx"></script>
15+
</body>
16+
</html>

0 commit comments

Comments
 (0)