-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsc-sweep.spec.ts
More file actions
171 lines (160 loc) · 7.86 KB
/
Copy pathsc-sweep.spec.ts
File metadata and controls
171 lines (160 loc) · 7.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/**
* T096 — Success-Criteria sweep against production.
*
* Each `test()` here verifies one SC item from spec.md. Runs against the
* live production URL by default; override with `TARGET_BASE`.
*
* Coverage in this spec (locally observable, no live data needed beyond a
* deployed site):
*
* SC-002 search latency — type into the search bar; assert the
* visible result count updates within 500 ms.
* SC-003 cell-switch timing — change the model dropdown; assert the
* UMAP cell shard swap completes < 1.5 s.
* SC-004 mobile viewport — 360 × 640 viewport has no horizontal
* scroll on the home page.
* SC-005 accepted-only — the abstracts shard contains zero
* `accepted_for === "Withdrawn"` records.
* SC-011 footer build SHA — home + about + abstract-permalink all
* render the same 7-char short SHA in
* the footer.
*
* Out of scope (need separate infra):
* SC-001 first interactive paint — measured by Lighthouse-CI workflow.
* SC-006 data-package size — measured against the local build dir.
* SC-007 link check — runs in deploy-ui.yml as a hard gate.
* SC-008 PR-preview timing — observed manually across several PRs.
* SC-009 cart reload — covered by cart_email + cart unit tests.
* SC-010 typo recall — separate `eval_typo_recall.py` script.
* SC-012 AI-attribution — separate Playwright assertion (TBD).
*/
import { test, expect, chromium, devices } from '@playwright/test';
test.setTimeout(180_000);
const BASE = (process.env.TARGET_BASE || 'https://abstractatlas.brainkb.org').replace(/\/$/, '');
test.describe('SC sweep', () => {
test('SC-002 — search latency: typing returns a filtered count in < 500 ms (warm path)', async () => {
// In a real session the semantic worker pre-warms during page load,
// so by the time the user types, it's ready. We mirror that here:
// wait for the ✨ Semantic toggle to drop its `loading` class
// (i.e., the worker reached `ready`) before measuring keystroke
// latency. Cold-start latency is bounded by the MiniLM ONNX
// download (~23 MB) and is reported separately below for context.
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto(`${BASE}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="result-card"]', { timeout: 30000 });
// Wait for the semantic worker to settle.
await page
.waitForFunction(
() => {
const btn = document.querySelector<HTMLButtonElement>('[data-testid="toggle-semantic"]');
return !!btn && !btn.classList.contains('loading') && !btn.disabled;
},
{ timeout: 30000 }
)
.catch(() => null);
await page.waitForTimeout(500); // belt-and-braces idle
const before = await page.locator('[data-testid="result-count"]').textContent();
const start = Date.now();
await page.getByTestId('search-input').fill('memory');
await expect
.poll(
async () => {
const t = (await page.locator('[data-testid="result-count"]').textContent())?.trim();
return t !== before && /^\d+$/.test(t || '');
},
{ timeout: 1500, intervals: [50, 100, 200] }
)
.toBe(true);
const elapsed = Date.now() - start;
console.log(`SC-002 search latency (warm): ${elapsed} ms`);
expect(elapsed).toBeLessThanOrEqual(500);
await browser.close();
});
test('SC-003 — cell-switch timing: model dropdown change re-renders UMAP in < 1500 ms', async () => {
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto(`${BASE}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="umap-chart-2d"]', { timeout: 30000 });
// Wait for initial UMAP render
await page.waitForTimeout(2000);
// Switch model — neuroscape → voyage.
const start = Date.now();
await page
.locator('[data-testid="model-selector-model"]')
.selectOption({ value: 'voyage' })
.catch(() => null);
await page.waitForFunction(
() => {
const el = document.querySelector('h3 code');
return el?.textContent?.startsWith('voyage_');
},
{ timeout: 5000 }
);
const elapsed = Date.now() - start;
console.log(`SC-003 cell-switch timing: ${elapsed} ms`);
expect(elapsed).toBeLessThanOrEqual(1500);
await browser.close();
});
test('SC-004 — 360×640 mobile: home page has no horizontal scroll', async () => {
const browser = await chromium.launch();
const ctx = await browser.newContext({ ...devices['Pixel 5'], viewport: { width: 360, height: 640 } });
const page = await ctx.newPage();
await page.goto(`${BASE}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="search-input"]', { timeout: 30000 });
await page.waitForTimeout(1500);
const scroll = await page.evaluate(() => ({
docW: document.documentElement.scrollWidth,
viewW: document.documentElement.clientWidth
}));
console.log(`SC-004 mobile overflow: docW=${scroll.docW} viewW=${scroll.viewW}`);
expect(scroll.docW).toBeLessThanOrEqual(scroll.viewW + 1);
await browser.close();
});
test('SC-005 — accepted-only: no Withdrawn records leak into the deployed corpus', async () => {
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto(`${BASE}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="result-card"]', { timeout: 30000 });
await page.waitForTimeout(2000);
// `+page.svelte` exposes a debug `window.__abstracts` snapshot.
const withdrawnCount = await page.evaluate(() => {
const arr = (window as unknown as { __abstracts?: { accepted_for: string }[] }).__abstracts;
if (!Array.isArray(arr)) return -1;
return arr.filter((a) => a.accepted_for === 'Withdrawn').length;
});
console.log(`SC-005 withdrawn-count: ${withdrawnCount}`);
expect(withdrawnCount).toBe(0);
await browser.close();
});
test('SC-011 — footer build_info SHA consistent across home / about / abstract permalink', async () => {
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto(`${BASE}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="build-info-short-sha"]', { timeout: 30000 });
const homeSha = (await page.locator('[data-testid="build-info-short-sha"]').first().textContent())?.trim();
expect(homeSha).toMatch(/^[0-9a-f]{7,12}$/);
await page.goto(`${BASE}/about/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="build-info-short-sha"]', { timeout: 30000 });
const aboutSha = (await page.locator('[data-testid="build-info-short-sha"]').first().textContent())?.trim();
expect(aboutSha).toBe(homeSha);
// Sample a real poster_id from the home page to test the permalink route.
await page.goto(`${BASE}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="result-card"]', { timeout: 30000 });
const posterId = await page.locator('[data-testid="result-card"]').first().getAttribute('data-poster-id');
if (posterId) {
await page.goto(`${BASE}/abstract/${encodeURIComponent(posterId)}/`, { waitUntil: 'load' });
await page.waitForSelector('[data-testid="build-info-short-sha"]', { timeout: 30000 });
const permalinkSha = (
await page.locator('[data-testid="build-info-short-sha"]').first().textContent()
)?.trim();
expect(permalinkSha).toBe(homeSha);
console.log(`SC-011 SHA consistent across routes: ${homeSha}`);
}
await browser.close();
});
});