oxedyne/daimond/dev/verify_sweep_seen.mjs
20.4 KiB, 1 run
created by r2519314175:711, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // verify_sweep_seen.mjs — is it on the screen, in the corner, and pressable? |
| 2 | // |
| 3 | // The owner exported eighteen defects from an ordinary day of using Daimond and |
| 4 | // at least half of them were visible to anyone who opened the app and looked. |
| 5 | // The suite is 290-odd verifier files and it was green throughout, because |
| 6 | // almost every check in it asserts that something is TRUE rather than that a |
| 7 | // person can SEE, REACH or PRESS it. A grep proves code exists; a passing |
| 8 | // assertion proves a value; neither proves anything about a screen. |
| 9 | // |
| 10 | // So this drives the real app at four shapes, opens every panel in every zone, |
| 11 | // and measures four families of property. What each one is here for is a defect |
| 12 | // that got past everything else: |
| 13 | // |
| 14 | // SEEN Social was not a flex column, so `.imp-notes { overflow-y: auto }` |
| 15 | // had no overflow to engage on. Twenty notes kept, 2606px of list in |
| 16 | // a 434px panel, NOT ONE note on the screen and nothing to scroll. |
| 17 | // Measured here at 20 notes, and the check is general: no list may be |
| 18 | // left with no item inside its panel and no way to scroll to one. |
| 19 | // ANCHOR the Web panel's closer went to 229px in and 65px down whenever the |
| 20 | // page title was long, and Social's sat 248px in at every width. Both |
| 21 | // are asked against the panel's OWN corner. And the second half of |
| 22 | // the rule, which one snapshot cannot see: THE POSITIONS ARE TAKEN |
| 23 | // AGAIN in a changed state -- a long title, a revealed chip, a |
| 24 | // narrower window -- and every control that stayed must not have |
| 25 | // moved. The owner's own sentence for it is the one encoded: a |
| 26 | // transient element must never sit in the flow of clickable targets. |
| 27 | // PRESS `elementFromPoint` at each control's centre. Every click in this |
| 28 | // suite is forced, so a button under a stale overlay passes them all. |
| 29 | // SINGULAR four "Daimond Optimiser" tiles where two belong. Nothing counted. |
| 30 | // |
| 31 | // The state pairs are driven through the app's own paths where there is one -- |
| 32 | // `DaimondWeb.open` for the title, `DaimondPanels.show` for the dock -- and |
| 33 | // through the element's own declaration where there is not: `#sync-chip` is |
| 34 | // built lazily by sync.js on the first status it has to report, so a run with no |
| 35 | // gateway holds no chip to reveal. See LIMITS at the foot of this header. |
| 36 | // |
| 37 | // node dev/verify_sweep_seen.mjs # the sweep |
| 38 | // node dev/verify_sweep_seen.mjs --quick # desktop and iphone only |
| 39 | // node dev/verify_sweep_seen.mjs --headed # under a real display, with shots |
| 40 | // node dev/verify_sweep_seen.mjs --selftest # prove each family can go red |
| 41 | // |
| 42 | // Needs dev/serve.mjs (DAIMOND_PORT, default 8777) and dev/mockllm.mjs |
| 43 | // (DAIMOND_MOCK_PORT, default 9099). No gateway. |
| 44 | // |
| 45 | // ── LIMITS, WHICH ARE THE POINT OF WRITING THEM DOWN ──────────────────────── |
| 46 | // |
| 47 | // A sweep that overclaims is worse than a small one that knows its edges, and |
| 48 | // the failure this file exists to correct is a suite that was green while the |
| 49 | // app was visibly broken. So: |
| 50 | // |
| 51 | // * A CHROMIUM AT 390x844 IS NOT AN iPHONE. It is not iOS Safari, it has no |
| 52 | // home-indicator inset, its scrollers are not rubber-banded and its fonts |
| 53 | // are this machine's. Geometry that is wrong here is wrong there; geometry |
| 54 | // that is right here may still be wrong there. |
| 55 | // * IT MEASURES WHAT IS DRAWN, NEVER WHAT IT MEANS. An icon nobody recognises, |
| 56 | // a word in the wrong register, a control that is exactly where it belongs |
| 57 | // and is the wrong control -- all of them pass every family above. The |
| 58 | // owner's "the system-prompt button became an unlabelled glyph and I |
| 59 | // concluded the feature had been deleted" is not reachable from here, and |
| 60 | // nothing in this file should be read as covering it. |
| 61 | // * A REVEALED TRANSIENT IS THE ELEMENT'S OWN DECLARATION, not its module's |
| 62 | // decision to show it. What is proved is that the row cannot hold it without |
| 63 | // moving; what is not proved is when the app puts it there. |
| 64 | // * ONE PALETTE, ONE SPACING. dev/verify_sweep_desktop.mjs walks eleven |
| 65 | // palettes against two spacings for ink; this walks four shapes for |
| 66 | // geometry, and the two are deliberately not merged. |
| 67 | |
| 68 | import fs from 'node:fs'; |
| 69 | import path from 'node:path'; |
| 70 | import { fileURLToPath } from 'node:url'; |
| 71 | import { open, shot, errors } from './harness.mjs'; |
| 72 | import { audit, positions, drifted, transients, reveal, restore, |
| 73 | SHAPES, CORNER, seedNotes, showPanels, openPanels } from './sweepkit.mjs'; |
| 74 | |
| 75 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 76 | const SHOTS = path.join(HERE, 'shots', 'nsweep'); |
| 77 | fs.mkdirSync(SHOTS, { recursive: true }); |
| 78 | |
| 79 | const QUICK = process.argv.includes('--quick'); |
| 80 | const HEADED = process.argv.includes('--headed'); |
| 81 | const SELFTEST = process.argv.includes('--selftest'); |
| 82 | const shapes = QUICK ? SHAPES.filter((s) => s.name === 'desktop' || s.name === 'iphone') : SHAPES; |
| 83 | |
| 84 | const ok = [], bad = []; |
| 85 | const check = (name, pass, detail) => { |
| 86 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 87 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 88 | }; |
| 89 | /// Findings printed under a heading, so a red says what a person would see. |
| 90 | const say = (fs_) => { for (const f of fs_) console.log(` ${f.family} ${f.what} @ ${f.where} — ${f.detail}`); }; |
| 91 | |
| 92 | // Every panel there is, so nothing is swept only in the arrangement that |
| 93 | // happens to be saved. The dock takes two at a time, which is the arrangement |
| 94 | // the Social defect was reported in. |
| 95 | const STAGE = ['ai', 'web', 'doc', 'tools', 'graph']; |
| 96 | const DOCK = ['social', 'trash', 'agents', 'work', 'spend', 'pending']; |
| 97 | |
| 98 | const s = await open({ name: 'nsweep', headed: HEADED }); |
| 99 | const p = s.page; |
| 100 | |
| 101 | // The world the panels are asked about: twenty notes kept, which is the state |
| 102 | // the Social panel was reported in, and which no fixture in this suite had. |
| 103 | await seedNotes(p, 20); |
| 104 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 105 | const { signInAs } = await import('./harness.mjs'); |
| 106 | await signInAs(s, s.name); |
| 107 | await p.waitForTimeout(800); |
| 108 | |
| 109 | const findings = []; |
| 110 | |
| 111 | for (const shape of shapes) { |
| 112 | console.log(`\n── ${shape.name} ${shape.width}x${shape.height} ──────────────────────\n`); |
| 113 | await p.setViewportSize({ width: shape.width, height: shape.height }); |
| 114 | await p.waitForTimeout(400); |
| 115 | |
| 116 | // A panel at a time in the stage, and PAIRS in the dock — a dock panel |
| 117 | // sharing its column is the case the Social list was measured in, and the |
| 118 | // one no other verifier reaches. |
| 119 | const rounds = []; |
| 120 | for (const id of STAGE) rounds.push({ label: id, show: ['ai', id] }); |
| 121 | for (let i = 0; i < DOCK.length; i += 2) rounds.push({ label: DOCK.slice(i, i + 2).join('+'), show: DOCK.slice(i, i + 2) }); |
| 122 | |
| 123 | for (const r of rounds) { |
| 124 | await showPanels(p, r.show); |
| 125 | const live = await openPanels(p); |
| 126 | // A round whose panels did not open would produce a full set of clean |
| 127 | // measurements of whatever WAS open, under this round's name. That is the |
| 128 | // silent-skip this file's own header argues against, so it is a failure. |
| 129 | const missing = r.show.filter((id) => !live.includes(id)); |
| 130 | if (missing.length) { |
| 131 | check(`${shape.name}/${r.label}: the panels opened`, false, `never opened: ${missing.join(', ')}`); |
| 132 | continue; |
| 133 | } |
| 134 | const all = (await audit(p)).map((f) => ({ ...f, shape: shape.name, round: r.label })); |
| 135 | // BEHIND is a count for a person, not a verdict. See the PRESS note in |
| 136 | // sweepkit.mjs: a sheet over a panel and a stale overlay over the app are |
| 137 | // the same geometry, and this file does not pretend to tell them apart. |
| 138 | const found = all.filter((f) => f.family !== 'BEHIND'); |
| 139 | const behind = all.filter((f) => f.family === 'BEHIND'); |
| 140 | findings.push(...all); |
| 141 | check(`${shape.name}/${r.label}: nothing hidden, stranded, covered or doubled`, |
| 142 | found.length === 0, found.length ? `${found.length} finding(s)` : ''); |
| 143 | if (found.length) say(found); |
| 144 | if (behind.length) console.log(` note ${behind.length} control(s) behind another surface`); |
| 145 | if (HEADED || shape.name === 'iphone' || r.label === 'social+trash') { |
| 146 | await p.screenshot({ path: path.join(SHOTS, `${shape.name}-${r.label}.png`) }).catch(() => {}); |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | // ── ANCHOR, the half a snapshot cannot see ──────────────────────────────── |
| 152 | console.log('\n── does it stay there? ───────────────────────────────────\n'); |
| 153 | |
| 154 | await p.setViewportSize({ width: 1280, height: 800 }); |
| 155 | await showPanels(p, ['ai', 'web']); |
| 156 | await p.waitForTimeout(500); |
| 157 | |
| 158 | // 1. A LONG TITLE. The Web panel's head holds a title and a row of buttons, and |
| 159 | // an ordinary URL is long. This is the exact state the owner reported. |
| 160 | { |
| 161 | // SCOPED TO THE PANEL WHOSE STATE CHANGED. Opening a page correctly takes the |
| 162 | // Web chip out of the header's row, so every chip after it moves and a |
| 163 | // document-wide reading would call that drift. The defect was in the head. |
| 164 | const before = await positions(p, '#panel-web'); |
| 165 | const url = await p.evaluate(async () => { |
| 166 | const u = location.origin + '/guide/index.html?' |
| 167 | + 'a-query-string-of-the-kind-an-ordinary-page-carries=' + 'x'.repeat(120); |
| 168 | try { await window.DaimondWeb.open(u); } catch (e) { return ''; } |
| 169 | return u; |
| 170 | }); |
| 171 | await p.waitForTimeout(1200); |
| 172 | check('the Web panel took a long page title', !!url, url ? '' : 'DaimondWeb.open refused'); |
| 173 | const after = await positions(p, '#panel-web'); |
| 174 | const moved = drifted(before, after); |
| 175 | check('a long page title moves nothing in the head', |
| 176 | moved.length === 0, |
| 177 | moved.slice(0, 4).map((m) => `${m.name} ${m.dx > 0 ? '+' : ''}${m.dx},${m.dy > 0 ? '+' : ''}${m.dy}`).join(' ')); |
| 178 | const corner = (await audit(p)).filter((f) => f.family === 'ANCHOR'); |
| 179 | check('every closer is still in its own corner under a long title', corner.length === 0); |
| 180 | if (corner.length) { say(corner); findings.push(...corner.map((f) => ({ ...f, shape: 'laptop', round: 'long title' }))); } |
| 181 | await p.screenshot({ path: path.join(SHOTS, 'longtitle.png') }).catch(() => {}); |
| 182 | } |
| 183 | |
| 184 | // 2. A TRANSIENT IN THE FLOW. The owner's rule, and the one this file states as |
| 185 | // a rule rather than as a measurement: an element the app shows and hides |
| 186 | // must not displace the controls a person is aiming at. |
| 187 | { |
| 188 | // A ROW OF PERSISTENT CONTROLS, WHEREVER IT IS. This asked `.top-actions` for |
| 189 | // its own transients and reported "none in the DOM — nothing was proved here" |
| 190 | // when it found none — so a top bar that had been EMPTIED of transients, which |
| 191 | // is the strongest form of the fix and the one lane n-chrome took, read as a |
| 192 | // failure to look. The sync chip and the held-sweep notice moved into the |
| 193 | // rail's status strip, which is a row of buttons too, so both rows are asked. |
| 194 | // |
| 195 | // AND THE DRIFT IS MEASURED DOCUMENT-WIDE, not in the row that changed. That |
| 196 | // is the opposite of the scoping the Web panel needed above, and deliberately: |
| 197 | // a long page title is a state change the head is ABOUT, so its own controls |
| 198 | // may reflow; a transient appearing is about nothing, and is entitled to move |
| 199 | // nothing anywhere. |
| 200 | const ROWS = ['.top-actions', '#admin-status']; |
| 201 | const ts = []; |
| 202 | for (const sel of ROWS) { |
| 203 | for (const t of await transients(p, sel)) ts.push({ ...t, row: sel }); |
| 204 | } |
| 205 | check('the app holds transients to test', ts.length > 0, |
| 206 | ts.length ? ts.map((t) => `${t.name} (${t.row})`).join(', ') |
| 207 | : 'none in either row — nothing was proved here'); |
| 208 | for (const t of ts) { |
| 209 | const before = await positions(p); |
| 210 | await reveal(p, t.key, 'Syncing…'); |
| 211 | await p.waitForTimeout(250); |
| 212 | const after = await positions(p); |
| 213 | const moved = drifted(before, after); |
| 214 | await restore(p, t.key); |
| 215 | await p.waitForTimeout(200); |
| 216 | // BOTH AXES, because a row of controls in a column moves DOWN. Said as |
| 217 | // `dx` alone, the first three findings here read "new-session-btn 0px" — |
| 218 | // a defect reported as no movement at all, which is how a real one gets |
| 219 | // waved past. |
| 220 | const said = (m) => `${m.name} ${m.dx}×${m.dy}px`; |
| 221 | const far = Math.max(...moved.map((m) => Math.max(Math.abs(m.dx), Math.abs(m.dy))), 0); |
| 222 | check(`showing ${t.name} moves no persistent control`, |
| 223 | moved.length === 0, |
| 224 | moved.length ? `${moved.length} moved, e.g. ${moved.slice(0, 3).map(said).join(', ')}` : ''); |
| 225 | if (moved.length) { |
| 226 | findings.push({ family: 'ANCHOR', what: 'a transient that displaces persistent controls', |
| 227 | where: t.name, detail: `${moved.length} control(s) moved, up to ${far}px`, |
| 228 | shape: 'laptop', round: 'transient' }); |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | // 3. A NARROWER WINDOW. Everything is entitled to reflow; nothing is entitled to |
| 234 | // leave its corner while it does. |
| 235 | { |
| 236 | await p.setViewportSize({ width: 900, height: 800 }); |
| 237 | await p.waitForTimeout(700); |
| 238 | const corner = (await audit(p)).filter((f) => f.family === 'ANCHOR' || f.family === 'PRESS'); |
| 239 | check('narrowing to 900px leaves every closer in its corner and every control pressable', |
| 240 | corner.length === 0, corner.length ? `${corner.length} finding(s)` : ''); |
| 241 | if (corner.length) { say(corner); findings.push(...corner.map((f) => ({ ...f, shape: 'narrow', round: 'reflow' }))); } |
| 242 | } |
| 243 | |
| 244 | // ── The same geometry in the engine an iPhone actually runs ─────────────── |
| 245 | // |
| 246 | // A Chromium at 390x844 is a rectangle, not a phone, and the paragraph in this |
| 247 | // file's header says so. WebKit is the engine iOS Safari is built on, so the |
| 248 | // same audit through it is the closest this machine gets to the device the |
| 249 | // owner reported these faults from -- and the distance that remains is stated |
| 250 | // rather than closed: this is WebKit on Linux under Playwright, with this box's |
| 251 | // fonts, no home-indicator inset and no iOS compositor. It narrows the claim; it |
| 252 | // does not make it a claim about iOS. |
| 253 | // |
| 254 | // It SKIPS LOUDLY where WebKit will not launch, and a skip is never counted as a |
| 255 | // pass -- dev/run_all.sh's header spends a paragraph on what that costs. |
| 256 | { |
| 257 | console.log('\n── the same audit, in WebKit at iPhone size ───────────────\n'); |
| 258 | process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = '1'; |
| 259 | const { pathToFileURL } = await import('node:url'); |
| 260 | const os = await import('node:os'); |
| 261 | const PWPATH = process.env.DAIMOND_PW |
| 262 | || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 263 | const { webkit } = await import(pathToFileURL(PWPATH).href); |
| 264 | // A FORWARDED DISPLAY STALLS THE LAUNCH, which is the house note about |
| 265 | // WebKit and the reason these two lines are here rather than in the harness: |
| 266 | // nothing in this block goes through `open()`. |
| 267 | const env = { ...process.env }; |
| 268 | delete env.DISPLAY; |
| 269 | delete env.WAYLAND_DISPLAY; |
| 270 | const { scratch, APP } = await import('./harness.mjs'); |
| 271 | let ctx = null; |
| 272 | try { |
| 273 | ctx = await webkit.launchPersistentContext(scratch(`wk-nsweep-${process.pid}`), { |
| 274 | viewport: { width: 390, height: 844 }, deviceScaleFactor: 3, |
| 275 | hasTouch: true, isMobile: true, env, timeout: 45000, |
| 276 | }); |
| 277 | } catch (e) { |
| 278 | check('WebKit at iPhone size: the audit ran', false, |
| 279 | 'SKIPPED — WebKit would not launch: ' + String(e).split('\n')[0]); |
| 280 | } |
| 281 | if (ctx) { |
| 282 | const wp = ctx.pages()[0] || await ctx.newPage(); |
| 283 | await wp.goto(APP, { waitUntil: 'domcontentloaded' }); |
| 284 | await signInAs({ page: wp, name: 'nsweepwk' }, 'nsweepwk'); |
| 285 | await wp.waitForTimeout(2500); |
| 286 | await seedNotes(wp, 20); |
| 287 | await wp.reload({ waitUntil: 'domcontentloaded' }); |
| 288 | await signInAs({ page: wp, name: 'nsweepwk' }, 'nsweepwk'); |
| 289 | await wp.waitForTimeout(2500); |
| 290 | for (const round of [['ai'], ['social', 'trash'], ['web']]) { |
| 291 | await showPanels(wp, round); |
| 292 | const all = (await audit(wp)).map((f) => ({ ...f, shape: 'webkit-iphone', round: round.join('+') })); |
| 293 | const found = all.filter((f) => f.family !== 'BEHIND'); |
| 294 | findings.push(...all); |
| 295 | check(`webkit-iphone/${round.join('+')}: nothing hidden, stranded, covered or doubled`, |
| 296 | found.length === 0, found.length ? `${found.length} finding(s)` : ''); |
| 297 | if (found.length) say(found); |
| 298 | await wp.screenshot({ path: path.join(SHOTS, `webkit-iphone-${round.join('+')}.png`) }).catch(() => {}); |
| 299 | } |
| 300 | await ctx.close(); |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | // ── The instrument proves it can go red ─────────────────────────────────── |
| 305 | // |
| 306 | // A check never seen fail is not evidence. Under --selftest each family is |
| 307 | // given the defect it was written for -- the real ones, reverted in the page -- |
| 308 | // and must find it. |
| 309 | if (SELFTEST) { |
| 310 | console.log('\n── selftest: each family, against the defect it was written for ──\n'); |
| 311 | await p.setViewportSize({ width: 1280, height: 800 }); |
| 312 | await showPanels(p, ['social', 'trash']); |
| 313 | |
| 314 | // SEEN: the Social panel as it was — a block, so the list's `overflow-y: |
| 315 | // auto` has nothing to engage on. This is the reverted fix, in one line. |
| 316 | await p.addStyleTag({ content: '.social { display: block !important; overflow: visible !important; }' }); |
| 317 | await p.waitForTimeout(500); |
| 318 | const seen = (await audit(p)).filter((f) => f.family === 'SEEN'); |
| 319 | check('selftest SEEN: a block Social panel is found', seen.length > 0, `${seen.length} finding(s)`); |
| 320 | say(seen.slice(0, 3)); |
| 321 | |
| 322 | // ANCHOR: the head's controls back inside the wrapping chip group. |
| 323 | await p.addStyleTag({ content: '#panel-social .railhead-acts { order: 3 !important; }' }); |
| 324 | await p.waitForTimeout(400); |
| 325 | const anch = (await audit(p)).filter((f) => f.family === 'ANCHOR'); |
| 326 | check('selftest ANCHOR: a closer out of its corner is found', anch.length > 0, `${anch.length} finding(s)`); |
| 327 | say(anch.slice(0, 3)); |
| 328 | |
| 329 | // PRESS: a sheet of glass over the app, of the kind a dialog leaves behind. |
| 330 | await p.evaluate(() => { |
| 331 | const g = document.createElement('div'); |
| 332 | g.id = 'nsweep-glass'; |
| 333 | g.style.cssText = 'position:fixed;inset:0;z-index:99999;background:transparent'; |
| 334 | document.body.appendChild(g); |
| 335 | }); |
| 336 | await p.waitForTimeout(200); |
| 337 | // A sheet of glass is a SURFACE, so its findings land in BEHIND -- which is |
| 338 | // the honest answer, and what the count is there for. What is proved here is |
| 339 | // that the instrument sees it at all: without the hit test nothing in this |
| 340 | // suite would, because every click in it is forced. |
| 341 | const press = (await audit(p)).filter((f) => f.family === 'PRESS' || f.family === 'BEHIND'); |
| 342 | check('selftest PRESS: a stale overlay is found', press.length > 10, `${press.length} control(s) under it`); |
| 343 | await p.evaluate(() => document.getElementById('nsweep-glass').remove()); |
| 344 | |
| 345 | // SINGULAR: a second tile of a name that is already in the rail. |
| 346 | const dup = await p.evaluate(() => { |
| 347 | const list = document.getElementById('diamond-list'); |
| 348 | if (!list || !list.firstElementChild) return false; |
| 349 | list.appendChild(list.firstElementChild.cloneNode(true)); |
| 350 | return true; |
| 351 | }); |
| 352 | await p.waitForTimeout(200); |
| 353 | const sing = (await audit(p)).filter((f) => f.family === 'SINGULAR'); |
| 354 | check('selftest SINGULAR: a doubled rail tile is found', dup && sing.length > 0, `${sing.length} finding(s)`); |
| 355 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 356 | await signInAs(s, s.name); |
| 357 | } |
| 358 | |
| 359 | // ── The report ──────────────────────────────────────────────────────────── |
| 360 | const REPORT = path.join(HERE, 'sweep_seen_report.md'); |
| 361 | const byFamily = {}; |
| 362 | for (const f of findings) (byFamily[f.family] = byFamily[f.family] || []).push(f); |
| 363 | let md = '# What a person would see\n\n' |
| 364 | + `Written by \`dev/verify_sweep_seen.mjs\`, ${new Date().toISOString().slice(0, 10)}, ` |
| 365 | + `${shapes.map((x) => x.name).join(', ')}.\n\n`; |
| 366 | for (const fam of Object.keys(byFamily)) { |
| 367 | md += `## ${fam}\n\n| where | what | detail | shape | round |\n|---|---|---|---|---|\n`; |
| 368 | for (const f of byFamily[fam]) md += `| \`${f.where}\` | ${f.what} | ${f.detail} | ${f.shape || ''} | ${f.round || ''} |\n`; |
| 369 | md += '\n'; |
| 370 | } |
| 371 | if (!findings.length) md += 'Nothing found.\n'; |
| 372 | fs.writeFileSync(REPORT, md); |
| 373 | console.log(`\nreport: ${REPORT}`); |
| 374 | |
| 375 | // A WORLD WITH NO GATEWAY REFUSES `/api` ON PURPOSE, and says so with a 502 -- |
| 376 | // see dev/world.sh, which made that refusal honest rather than letting a |
| 377 | // stranger's gateway answer. This file needs no gateway, so those 502s are the |
| 378 | // world working, and counting them would make it red for a reason that has |
| 379 | // nothing to do with what it measures. Nothing else is filtered. |
| 380 | const errs = errors(s).filter((e) => !(/status of 502/.test(e) && /\/api\//.test(e))); |
| 381 | check('nothing threw in the page while it swept', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 382 | await shot(s, 'nsweep-seen-last'); |
| 383 | await s.close(); |
| 384 | |
| 385 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 386 | if (bad.length) { for (const b of bad) console.log(' FAIL ' + b); process.exit(1); } |