Playwright + launchPersistentContext + unpacked extension.
Same harness as existing scripts/video-demo.mjs.
expect() on DOMsw.evaluate() on SW singletonsscripts/dbx-upload.shVisuals are not a replacement for assertions — they catch regressions in motion that DOM state alone misses.
| Area | Files (LOC) | Coverage |
|---|---|---|
| App shell | App, main, hooks (588) | ~75% |
| Header | Header.tsx (438) | ~75% |
| Grid | TabGrid, TabCard (494) | ~80% |
| Stack | StackView.tsx (764) | ~70% |
| Zoom entrance | ZoomEntrance, pre-overlay (417) | ~65% |
| Welcome | WelcomeOverlay.tsx (254) | ~80% |
| Refresh popup | RefreshPopup.tsx (151) | ~80% |
| Content script | zoom-out.ts (67) | ~75% |
| Commands & icon | index, tab-ready (219) | ~70% |
| Capture/storage | capture, storage (1260) | ~55% |
| Messages | messages.ts (424) | ~65% |
| Backup | backup.ts (271) | ~50% |
| Clustering | clustering.ts (500) | ~30% |
| Analytics | analytics.ts (159) | ~30% |
Weighted total: ~60–65% LOC · ~50% branch
A Bootstrap (4)
B Header controls (7)
C Grid layout (8)
D Stack layout (12)
E Zoom entrance/exit (3)
F Refresh screenshots (4)
G Content-script pinch (1)
H Keyboard commands (2)
I Capture + storage (4)
J Debug + backup (3)
K Analytics (1)
tab.html/, Cmd+F, Esc)Cmd+Shift+X — expose modeCmd+Shift+A — expose allgetDebugSettings round-tripOnNewTabPageLoaded, First Time Usage| Kind | Count | Examples |
|---|---|---|
| Functional only | 16 | close window, commands, persistence, debug settings |
| Functional + PNG | 10 | layouts, menus, clusters, search empty state |
| Functional + MP4 | 19 | zoom, pinch, DnD, welcome flow, refresh progress |
| Visual only | — | every visual test also asserts |
// Item 5 — Search filters tabs
await page.fill('input[placeholder^="Search"]', 'alpha')
await expect(page.locator('[data-tab-id]')).toHaveCount(1)
// Item 15 — Close button removes tab
const before = await sw.evaluate(() => chrome.tabs.query({}).then(t => t.length))
await page.locator('[data-tab-id] button.close').first().click()
const after = await sw.evaluate(() => chrome.tabs.query({}).then(t => t.length))
expect(after).toBe(before - 1)
// Item 42 — Thumbnail captured on activation
await page.goto('http://127.0.0.1:17001/')
await waitForCaptures(sw, ['http://127.0.0.1:17001/'], 10_000)
const rows = await sw.evaluate(() => self.__tabbyStorage.getAllRaw())
expect(rows.find(r => r.url === 'http://127.0.0.1:17001/').dataUrl.length)
.toBeGreaterThan(1000)
showDirectoryPicker, permission re-auth promptsvisualViewport.scale in content scriptExplicitly out of scope so expectations stay clear.
test/e2e/
setup.mjs # shared helpers
functional/
bootstrap.test.mjs # items 1–4
header.test.mjs # items 5–11
grid.test.mjs # items 12–19
stack.test.mjs # items 20–31
zoom.test.mjs # items 32–34
refresh.test.mjs # items 35–38
commands.test.mjs # items 39–41
capture.test.mjs # items 42–45
backup.test.mjs # items 46–48
visual/ # PNGs → docs/e2e-screens/
video/ # MP4s → docs/videos/ (Xvfb wrapper)
"test:e2e": "node test/e2e/run-functional.mjs",
"test:e2e:visual": "node test/e2e/run-visual.mjs",
"test:e2e:video": "bash test/e2e/run-video.sh"
| Phase | Scope | Coverage | Runtime |
|---|---|---|---|
| 1. Smoke | Bootstrap, layouts, search, click, close, DnD, capture, persistence | ~30% | <2 min |
| 2. Breadth | Menus, clusters, commands, backup, all remaining functional | ~55% | ~5 min |
| 3. Visual | PNGs + MP4s under Xvfb + Dropbox upload | ~65% | ~10 min |
Each phase is independently shippable.
test/e2e/ tree, or extend scripts/?Pick any, then I start writing code.
docs/e2e-test-plan.mddocs/e2e-test-plan-slides.html