check:studio-all: open every plugin in the studio, not a sample
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
smoke:studio samples 4 tabs. On 2026-09-25 a sweep of all of them found people-groups-grid rendering 2 DOM nodes behind a page error, and 13 plugins mounting with their data 404ing on a fresh clone. The sample had passed. bun run check:studio-all -- --url <studio page> Opens every plugin row in the studio's tree and fails a plugin on the studio's error overlay, a page error or failed request while it is active (Mapbox hosts excluded), or a near-empty mount. Graded by exit code. --url is required, because hardcoded :5173 gates have measured another worktree's tree. The package.json entry carries no inline comment: a `# …` in a script string swallows the arguments bun passes. Negative-tested: re-introducing the unguarded useI18n() in FilterDropdown fails it, naming people-groups-grid, research-map-plus-grid and stack-demo. Against today's dev server: PASS, 31 of 31, in 3 of 4 runs; the fourth caught an intermittent gospel-dashboard page error, reported to its owner.
This commit is contained in:
parent
e72ac1b4a2
commit
e6f328b40b
2 changed files with 108 additions and 0 deletions
107
internal/check-studio-every-plugin.mjs
Normal file
107
internal/check-studio-every-plugin.mjs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-studio-every-plugin — open EVERY plugin in the plugin studio and fail on any that is broken.
|
||||
*
|
||||
* USAGE: bun run check:studio-all -- --url http://localhost:5299/ (dev server)
|
||||
* bun run check:studio-all -- --url http://localhost:5478/index.html (built tree, serve-strict)
|
||||
* Graded by EXIT CODE: 0 = every plugin mounted with content, 1 = at least one failed.
|
||||
*
|
||||
* ★ WHY THIS EXISTS WHEN smoke:studio ALREADY DOES. smoke:studio samples 4 tabs by index. On
|
||||
* 2026-09-25 a first-run sweep of all 35 found people-groups-grid rendering 2 DOM nodes and a page
|
||||
* error — standalone, in the studio and inside two composites — and 13 plugins mounting with
|
||||
* their data 404ing on a fresh clone. The sample had passed. A plugin the gift ships must be
|
||||
* checked, not sampled; this takes ~2 minutes.
|
||||
*
|
||||
* WHAT FAILS A PLUGIN (each measured on a real defect, none guessed):
|
||||
* - the studio's own error overlay ("Could not load …") — the element never registered
|
||||
* - a page error while that plugin was active (the i18n crash above)
|
||||
* - a failed request (4xx/5xx) while it was active — a data file that is missing is a plugin
|
||||
* that looks empty. Mapbox tile/event hosts are excluded: a URL-restricted token 403s there
|
||||
* and that is a deployment setting, not a plugin defect.
|
||||
* - a mount with almost nothing in it (< 8 elements and < 50 characters): the blank grid was 2.
|
||||
*
|
||||
* --url is REQUIRED. Several gates here hardcode :5173, and a drifted dev server made them measure
|
||||
* another worktree's tree (see AGENT-BRIEF.md) — so this one refuses to guess.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const i = process.argv.indexOf('--url')
|
||||
const URL_ = i > -1 ? process.argv[i + 1] : ''
|
||||
if (!URL_) {
|
||||
console.error('check-studio-every-plugin: pass --url <studio page>, e.g. --url http://localhost:5299/')
|
||||
process.exit(2)
|
||||
}
|
||||
const PER_PLUGIN_MS = Number(process.env.STUDIO_ALL_WAIT_MS || 16000)
|
||||
const IGNORE_HOSTS = /(^|\.)(api\.mapbox\.com|events\.mapbox\.com|tiles\.mapbox\.com)$/
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
|
||||
const page = await (await browser.newContext({ viewport: { width: 1280, height: 900 } })).newPage()
|
||||
let current = '(page load)'
|
||||
const seen = {}
|
||||
const note = (kind, v) => { const s = (seen[current] = seen[current] || { errors: [], failed: [] }); s[kind].push(v) }
|
||||
page.on('pageerror', (e) => note('errors', String(e.message || e).slice(0, 160)))
|
||||
page.on('response', (r) => {
|
||||
if (r.status() < 400) return
|
||||
let host = ''
|
||||
try { host = new URL(r.url()).hostname } catch (_) { /* keep '' */ }
|
||||
if (IGNORE_HOSTS.test(host)) return
|
||||
note('failed', r.status() + ' ' + r.url().replace(/^https?:\/\/[^/]+/, '').slice(0, 100))
|
||||
})
|
||||
|
||||
await page.goto(URL_, { waitUntil: 'networkidle' })
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const s = document.querySelector('plugin-studio')
|
||||
return !!(s && s.shadowRoot && s.shadowRoot.querySelector('.st-row[aria-level="2"]'))
|
||||
}, null, { timeout: 30000 })
|
||||
} catch (_) {
|
||||
console.error('STUDIO-ALL: FAIL — no <plugin-studio> with a plugin tree at ' + URL_)
|
||||
await browser.close(); process.exit(1)
|
||||
}
|
||||
|
||||
// Every PLUGIN row (aria-level 2 = a plugin in its capability section; deeper rows are parts that
|
||||
// open one of these same plugins, and level 1 is a section header).
|
||||
const rows = await page.evaluate(() => [...document.querySelector('plugin-studio').shadowRoot
|
||||
.querySelectorAll('.st-row[aria-level="2"]')].map((r) => r.querySelector('.st-label').textContent.trim() + ' <' + r.querySelector('.st-desc').textContent.trim() + '>'))
|
||||
|
||||
const results = []
|
||||
for (let n = 0; n < rows.length; n++) {
|
||||
current = rows[n]
|
||||
await page.evaluate((n) => document.querySelector('plugin-studio').shadowRoot.querySelectorAll('.st-row[aria-level="2"]')[n].click(), n)
|
||||
let r = null
|
||||
const deadline = Date.now() + PER_PLUGIN_MS
|
||||
// Poll until the pane has real content, errors, or time runs out — a fixed sleep would either be
|
||||
// too slow for the whole run or too short for a map.
|
||||
while (Date.now() < deadline) {
|
||||
await page.waitForTimeout(400)
|
||||
r = await page.evaluate(() => {
|
||||
const s = document.querySelector('plugin-studio').shadowRoot
|
||||
const pane = [...s.querySelectorAll('.ps-pane')].find((x) => !x.hidden)
|
||||
const hint = pane && pane.querySelector('.ps-hint')
|
||||
const host = pane && pane.querySelector('.ps-slot') && pane.querySelector('.ps-slot').children[0]
|
||||
let nodes = 0
|
||||
const walk = (root) => { for (const e of root.querySelectorAll('*')) { nodes++; if (e.shadowRoot) walk(e.shadowRoot) } }
|
||||
if (host) walk(host.shadowRoot || host)
|
||||
const text = host ? ((host.shadowRoot || host).textContent || '').replace(/\s+/g, ' ').trim().length : 0
|
||||
return { hint: hint ? hint.textContent.replace(/\s+/g, ' ').trim().slice(0, 140) : '', nodes, text }
|
||||
})
|
||||
if (r.hint.startsWith('Could not') || r.nodes > 40) break
|
||||
}
|
||||
const s = seen[current] || { errors: [], failed: [] }
|
||||
const why = []
|
||||
if (r.hint && !r.hint.startsWith('Loading')) why.push(r.hint)
|
||||
if (r.nodes < 8 && r.text < 50) why.push('mounted almost nothing (' + r.nodes + ' elements)')
|
||||
if (s.errors.length) why.push('page error: ' + [...new Set(s.errors)].join(' / '))
|
||||
if (s.failed.length) why.push('failed requests: ' + [...new Set(s.failed)].slice(0, 3).join(' , '))
|
||||
results.push({ row: current, nodes: r.nodes, why })
|
||||
}
|
||||
await browser.close()
|
||||
|
||||
const bad = results.filter((x) => x.why.length)
|
||||
for (const x of results) console.log((x.why.length ? 'FAIL ' : 'ok ') + x.row.padEnd(60) + (x.why.length ? ' ' + x.why.join(' | ') : ' ' + x.nodes + ' elements'))
|
||||
const loadErr = seen['(page load)']
|
||||
if (loadErr && (loadErr.errors.length || loadErr.failed.length)) console.log('page load: ' + [...loadErr.errors, ...loadErr.failed].slice(0, 4).join(' | '))
|
||||
console.log(bad.length
|
||||
? 'STUDIO-ALL: FAIL — ' + bad.length + ' of ' + results.length + ' plugins broken at ' + URL_
|
||||
: 'STUDIO-ALL: PASS — all ' + results.length + ' plugins mounted with content, no page errors, no failed requests, at ' + URL_)
|
||||
process.exit(bad.length ? 1 : 0)
|
||||
|
|
@ -124,6 +124,7 @@
|
|||
"smoke:aria-labels": "sh -c 'B=${BASE_URL:-http://127.0.0.1:4173}; exec node internal/check-aria-labels-localized.mjs \"$B/doxa-maps/my-upg-100-list/upg-100-list/index.html\" es,ar,zh'",
|
||||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs # every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
|
||||
"check:url-resolver": "node internal/check-url-resolver.mjs # cap-5 resolve() must not prefix a rooted URL (the dev-surface 404 class)",
|
||||
"check:studio-all": "node internal/check-studio-every-plugin.mjs",
|
||||
"smoke:studio": "node internal/smoke-studio.mjs # 5s DEV-surface check: / mounts the studio, tabs>0, 0 4xx (exit 2 = dev server down)",
|
||||
"check:manifest-covers": "node internal/check-manifest-covers-discovery.mjs # every DISCOVERED (non-gated) bundle must be in the built manifest",
|
||||
"check:csv-shape": "node internal/check-csv-export-shape.mjs # the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue