@1aboveio/skills 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +13 -0
- package/.codex-plugin/resources.json +1827 -0
- package/README.md +22 -0
- package/package.json +9 -2
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/airflow/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/release-check.mjs +3 -1
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/app-debug/release-check.mjs +3 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/better-auth/release-check.mjs +3 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/release-check.mjs +3 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/data-quality/release-check.mjs +3 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/monorepo/release-check.mjs +3 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/pyspark/release-check.mjs +3 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/python-backend/release-check.mjs +3 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/shadcn/release-check.mjs +3 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/release-check.mjs +3 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-build/release-check.mjs +3 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/release-check.mjs +3 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/docker/release-check.mjs +3 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-actions/release-check.mjs +3 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/github-flow/release-check.mjs +3 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/google-cloud/release-check.mjs +3 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/mergify/release-check.mjs +3 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/cicd/podman/release-check.mjs +3 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-review/release-check.mjs +3 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/article-to-image/release-check.mjs +3 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/flowchart-svg/release-check.mjs +3 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/image-gen/release-check.mjs +3 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/mermaid/release-check.mjs +3 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/release-check.mjs +3 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-publisher/release-check.mjs +3 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/wechat-search/release-check.mjs +3 -1
- package/skills/content/xai-search/SKILL.md +88 -13
- package/skills/content/xai-search/release-check.mjs +3 -1
- package/skills/content/xai-search/scripts/xai_search.sh +125 -149
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/distill-writing/release-check.mjs +3 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/doc-to-md/release-check.mjs +3 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/editorial-review/release-check.mjs +3 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/release-check.mjs +3 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/plain-language/release-check.mjs +3 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/polish-cn/release-check.mjs +3 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/reg-parser/release-check.mjs +3 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/release-check.mjs +3 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-doc/release-check.mjs +3 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/better-review/release-check.mjs +3 -1
- package/skills/engineering/contributing/SKILL.md +1 -1
- package/skills/engineering/contributing/release-check.mjs +3 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/e2e-test/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/golden-path/release-check.mjs +3 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/references/preflight.md +7 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
- package/skills/engineering/resolve-issues/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/resolve-release/release-check.mjs +3 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-issues/release-check.mjs +3 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/rush-release/release-check.mjs +3 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/engineering/test-quality/release-check.mjs +3 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
- package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
- package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
- package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
- package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
- package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
- package/skills/content/image-gen/scripts/options.test.mjs +0 -245
- package/skills/content/xai-search/tests/smoke_test.sh +0 -36
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
- package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
- package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
- package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +0 -520
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Unit tests for the pure overflow predicate (issue #195). Run: `node --test`.
|
|
2
|
-
// The Playwright wrapper (assertNoHorizontalOverflow) needs a real browser and is
|
|
3
|
-
// exercised downstream; this proves the detection logic without one.
|
|
4
|
-
|
|
5
|
-
import { test } from 'node:test'
|
|
6
|
-
import assert from 'node:assert/strict'
|
|
7
|
-
import {
|
|
8
|
-
findHorizontalOverflow, PRESENTATION_VIEWPORT_FLOOR,
|
|
9
|
-
rectsIntersect, findOverlaps, applyOverlapAllowlist,
|
|
10
|
-
} from './presentation-checks.mjs'
|
|
11
|
-
|
|
12
|
-
// helper: build an element with a rect + interactive flag
|
|
13
|
-
const el = (selector, [left, top, right, bottom], interactive = false) =>
|
|
14
|
-
({ selector, rect: { left, top, right, bottom, width: right - left, height: bottom - top }, interactive })
|
|
15
|
-
|
|
16
|
-
const VW = 375
|
|
17
|
-
|
|
18
|
-
test('no overflow when everything fits', () => {
|
|
19
|
-
const els = [
|
|
20
|
-
{ selector: 'header', left: 0, right: 375, width: 375, height: 60 },
|
|
21
|
-
{ selector: '.card', left: 16, right: 359, width: 343, height: 120 },
|
|
22
|
-
]
|
|
23
|
-
assert.deepEqual(findHorizontalOverflow(els, VW), [])
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('flags an element wider than the viewport (right overflow)', () => {
|
|
27
|
-
const els = [{ selector: '.wide-table', left: 0, right: 520, width: 520, height: 200 }]
|
|
28
|
-
const out = findHorizontalOverflow(els, VW)
|
|
29
|
-
assert.equal(out.length, 1)
|
|
30
|
-
assert.equal(out[0].selector, '.wide-table')
|
|
31
|
-
assert.equal(out[0].overflowPx, 145) // 520 - 375
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('flags a negatively-positioned element (left overflow)', () => {
|
|
35
|
-
const els = [{ selector: '#drawer', left: -120, right: 200, width: 320, height: 400 }]
|
|
36
|
-
const out = findHorizontalOverflow(els, VW)
|
|
37
|
-
assert.equal(out.length, 1)
|
|
38
|
-
assert.equal(out[0].overflowPx, 120)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('respects the tolerance band (sub-pixel rounding does not flag)', () => {
|
|
42
|
-
const els = [{ selector: '.x', left: 0, right: 375.5, width: 375.5, height: 10 }]
|
|
43
|
-
assert.deepEqual(findHorizontalOverflow(els, VW, { tolerancePx: 1 }), [])
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('ignore list suppresses intentional overflow by selector substring', () => {
|
|
47
|
-
const els = [{ selector: '.carousel-track', left: 0, right: 900, width: 900, height: 150 }]
|
|
48
|
-
assert.deepEqual(findHorizontalOverflow(els, VW, { ignore: ['carousel'] }), [])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('zero-size (unrendered) elements are skipped', () => {
|
|
52
|
-
const els = [{ selector: '.hidden', left: 0, right: 999, width: 0, height: 0 }]
|
|
53
|
-
assert.deepEqual(findHorizontalOverflow(els, VW), [])
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('viewport floor is the documented 375 + 1280 pair', () => {
|
|
57
|
-
assert.deepEqual(PRESENTATION_VIEWPORT_FLOOR.map((v) => v.width), [375, 1280])
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// ---------- overlap detection (presentation:spatial) ----------
|
|
61
|
-
|
|
62
|
-
test('rectsIntersect: overlapping yes, separated no, touching (within tol) no', () => {
|
|
63
|
-
const a = { left: 0, top: 0, right: 100, bottom: 50 }
|
|
64
|
-
assert.equal(rectsIntersect(a, { left: 50, top: 25, right: 150, bottom: 75 }), true)
|
|
65
|
-
assert.equal(rectsIntersect(a, { left: 200, top: 0, right: 300, bottom: 50 }), false)
|
|
66
|
-
assert.equal(rectsIntersect(a, { left: 100, top: 0, right: 200, bottom: 50 }), false) // edge-touch
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('findOverlaps: flags a collision but skips pure nesting', () => {
|
|
70
|
-
const parent = el('.card', [0, 0, 200, 200])
|
|
71
|
-
const child = el('.card .title', [10, 10, 190, 40]) // nested inside parent
|
|
72
|
-
const stray = el('.badge', [180, 180, 260, 260]) // overlaps parent corner
|
|
73
|
-
const pairs = findOverlaps([parent, child, stray])
|
|
74
|
-
// parent⨯child is nesting (skipped); parent⨯stray is a real overlap
|
|
75
|
-
assert.equal(pairs.length, 1)
|
|
76
|
-
assert.equal(pairs[0].b, '.badge')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('applyOverlapAllowlist: gated suppression yields a census entry', () => {
|
|
80
|
-
const pairs = [{ a: '.carousel-track', b: '.carousel-slide', aInteractive: false, bInteractive: false }]
|
|
81
|
-
const r = applyOverlapAllowlist(pairs, [{ match: 'carousel', category: 'decorative', reason: 'z-stacked slides' }])
|
|
82
|
-
assert.equal(r.reported.length, 0)
|
|
83
|
-
assert.equal(r.suppressed.length, 1)
|
|
84
|
-
assert.equal(r.suppressed[0].reason, 'z-stacked slides')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('applyOverlapAllowlist: entry without category/reason is rejected (still fails)', () => {
|
|
88
|
-
const pairs = [{ a: '.x', b: '.y', aInteractive: false, bInteractive: false }]
|
|
89
|
-
const r = applyOverlapAllowlist(pairs, [{ match: '.x' }])
|
|
90
|
-
assert.equal(r.suppressed.length, 0)
|
|
91
|
-
assert.equal(r.rejected.length, 1)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('applyOverlapAllowlist: cannot allowlist an overlap that occludes an interactive element', () => {
|
|
95
|
-
const pairs = [{ a: '.overlay', b: 'button#save', aInteractive: false, bInteractive: true }]
|
|
96
|
-
const r = applyOverlapAllowlist(pairs, [{ match: '.overlay', category: 'decorative', reason: 'hero flourish' }])
|
|
97
|
-
assert.equal(r.suppressed.length, 0)
|
|
98
|
-
assert.equal(r.rejected.length, 1)
|
|
99
|
-
assert.match(r.rejected[0].why, /interactive/)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('applyOverlapAllowlist: unmatched overlap is reported', () => {
|
|
103
|
-
const pairs = [{ a: '.a', b: '.b', aInteractive: false, bInteractive: false }]
|
|
104
|
-
const r = applyOverlapAllowlist(pairs, [{ match: '.zzz', category: 'decorative', reason: 'n/a' }])
|
|
105
|
-
assert.equal(r.reported.length, 1)
|
|
106
|
-
})
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// Unit tests for the pure Perceivability cores (issue #196). Run: `node --test`.
|
|
2
|
-
// The Playwright wrappers (assertContrast / assertHiddenFromWrongRole /
|
|
3
|
-
// assertRemovedAfterMutation) need a real browser + @axe-core/playwright and are
|
|
4
|
-
// exercised downstream; the contrast math, axe classifier, and predicates are
|
|
5
|
-
// proven here without one.
|
|
6
|
-
|
|
7
|
-
import { test } from 'node:test'
|
|
8
|
-
import assert from 'node:assert/strict'
|
|
9
|
-
import {
|
|
10
|
-
relativeLuminance, contrastRatio, meetsContrast, classifyAxeContrast,
|
|
11
|
-
isEffectivelyHidden, isExemptFromContrast, assessElementContrast, AbstainError,
|
|
12
|
-
} from './presentation-perceivability.mjs'
|
|
13
|
-
|
|
14
|
-
const WHITE = [255, 255, 255]
|
|
15
|
-
const BLACK = [0, 0, 0]
|
|
16
|
-
|
|
17
|
-
// ---------- WCAG math ----------
|
|
18
|
-
|
|
19
|
-
test('relative luminance: white ~1, black 0', () => {
|
|
20
|
-
assert.ok(Math.abs(relativeLuminance(WHITE) - 1) < 1e-9)
|
|
21
|
-
assert.equal(relativeLuminance(BLACK), 0)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('contrast ratio: black on white is 21:1, white on white is 1:1', () => {
|
|
25
|
-
assert.ok(Math.abs(contrastRatio(BLACK, WHITE) - 21) < 1e-6)
|
|
26
|
-
assert.ok(Math.abs(contrastRatio(WHITE, WHITE) - 1) < 1e-9)
|
|
27
|
-
assert.equal(contrastRatio(BLACK, WHITE), contrastRatio(WHITE, BLACK)) // symmetric
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('meetsContrast: black text on white passes AA', () => {
|
|
31
|
-
const r = meetsContrast(BLACK, WHITE)
|
|
32
|
-
assert.equal(r.threshold, 4.5)
|
|
33
|
-
assert.equal(r.pass, true)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('meetsContrast: a faint gray fails both text and non-text', () => {
|
|
37
|
-
const faint = [170, 170, 170] // ~2.3:1 on white
|
|
38
|
-
assert.equal(meetsContrast(faint, WHITE).pass, false)
|
|
39
|
-
assert.equal(meetsContrast(faint, WHITE, { nonText: true }).pass, false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('meetsContrast: threshold flips between text (4.5) and non-text (3.0)', () => {
|
|
43
|
-
const mid = [130, 130, 130] // ~3.8:1 on white — between 3 and 4.5
|
|
44
|
-
const asText = meetsContrast(mid, WHITE)
|
|
45
|
-
const asUi = meetsContrast(mid, WHITE, { nonText: true })
|
|
46
|
-
assert.equal(asText.threshold, 4.5)
|
|
47
|
-
assert.equal(asText.pass, false, 'fails as normal text')
|
|
48
|
-
assert.equal(asUi.threshold, 3)
|
|
49
|
-
assert.equal(asUi.pass, true, 'passes as a UI component / large text')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
// ---------- axe classifier ----------
|
|
53
|
-
|
|
54
|
-
test('classifyAxeContrast: no findings → pass', () => {
|
|
55
|
-
assert.equal(classifyAxeContrast({ violations: [], incomplete: [] }).status, 'pass')
|
|
56
|
-
assert.equal(classifyAxeContrast({}).status, 'pass')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('classifyAxeContrast: a color-contrast violation → fail', () => {
|
|
60
|
-
const res = { violations: [{ id: 'color-contrast', nodes: [{ target: ['.btn'] }] }], incomplete: [] }
|
|
61
|
-
const c = classifyAxeContrast(res)
|
|
62
|
-
assert.equal(c.status, 'fail')
|
|
63
|
-
assert.equal(c.count, 1)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('classifyAxeContrast: incomplete (gradient/image) → abstain, not pass', () => {
|
|
67
|
-
const res = { violations: [], incomplete: [{ id: 'color-contrast', nodes: [{ target: ['.hero'] }] }] }
|
|
68
|
-
assert.equal(classifyAxeContrast(res).status, 'abstain')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('classifyAxeContrast: violation outranks incomplete; other rule ids ignored', () => {
|
|
72
|
-
const res = {
|
|
73
|
-
violations: [{ id: 'color-contrast', nodes: [{}] }, { id: 'label', nodes: [{}] }],
|
|
74
|
-
incomplete: [{ id: 'color-contrast', nodes: [{}] }],
|
|
75
|
-
}
|
|
76
|
-
const c = classifyAxeContrast(res)
|
|
77
|
-
assert.equal(c.status, 'fail')
|
|
78
|
-
assert.equal(c.count, 1, 'only color-contrast nodes counted')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
// ---------- visibility / exemption predicates ----------
|
|
82
|
-
|
|
83
|
-
test('isEffectivelyHidden: catches display/visibility/opacity/aria/zero-size', () => {
|
|
84
|
-
assert.equal(isEffectivelyHidden({ style: { display: 'none' } }), true)
|
|
85
|
-
assert.equal(isEffectivelyHidden({ style: { visibility: 'hidden' } }), true)
|
|
86
|
-
assert.equal(isEffectivelyHidden({ style: { opacity: '0' } }), true)
|
|
87
|
-
assert.equal(isEffectivelyHidden({ ariaHidden: true }), true)
|
|
88
|
-
assert.equal(isEffectivelyHidden({ rect: { width: 0, height: 10 } }), true)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('isEffectivelyHidden: a normal visible element is not hidden', () => {
|
|
92
|
-
assert.equal(isEffectivelyHidden({ style: { display: 'block', visibility: 'visible', opacity: '1' }, rect: { width: 100, height: 40 } }), false)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('isExemptFromContrast: disabled controls are exempt (WCAG), normal are not', () => {
|
|
96
|
-
assert.equal(isExemptFromContrast({ disabled: true }), true)
|
|
97
|
-
assert.equal(isExemptFromContrast({ ariaDisabled: true }), true)
|
|
98
|
-
assert.equal(isExemptFromContrast({}), false)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('assessElementContrast (manual/escalation path) composes exemption + threshold', () => {
|
|
102
|
-
// disabled → exempt, regardless of (bad) colors
|
|
103
|
-
assert.equal(assessElementContrast(WHITE, WHITE, { disabled: true }).status, 'exempt')
|
|
104
|
-
// enabled, good contrast → pass
|
|
105
|
-
assert.equal(assessElementContrast(BLACK, WHITE).status, 'pass')
|
|
106
|
-
// enabled, faint gray → fail, with ratio reported
|
|
107
|
-
const fail = assessElementContrast([170, 170, 170], WHITE)
|
|
108
|
-
assert.equal(fail.status, 'fail')
|
|
109
|
-
assert.ok(fail.ratio < fail.threshold)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('AbstainError is distinguishable from a hard failure', () => {
|
|
113
|
-
const e = new AbstainError('x', { count: 2 })
|
|
114
|
-
assert.equal(e.abstain, true)
|
|
115
|
-
assert.equal(e.name, 'AbstainError')
|
|
116
|
-
assert.ok(e instanceof Error)
|
|
117
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// Unit tests for the pure Reachability & Affordance cores (issue #197).
|
|
2
|
-
// Run: `node --test`. The Playwright wrappers (crawlReachableRoutes,
|
|
3
|
-
// assertNoNavOrphans, assertTargetSizes, assertFocusVisible, assertModalTrapsFocus,
|
|
4
|
-
// assertRouteNotReachable) need a real browser and are exercised downstream.
|
|
5
|
-
|
|
6
|
-
import { test } from 'node:test'
|
|
7
|
-
import assert from 'node:assert/strict'
|
|
8
|
-
import { findNavOrphans, meetsTargetSize, hasVisibleFocusIndicator } from './presentation-reachability.mjs'
|
|
9
|
-
|
|
10
|
-
// ---------- nav-orphan set diff ----------
|
|
11
|
-
|
|
12
|
-
test('findNavOrphans: routes in manifest but not reachable are orphans', () => {
|
|
13
|
-
const manifest = ['/', '/dashboard', '/settings', '/secret-report']
|
|
14
|
-
const reachable = ['/', '/dashboard', '/settings']
|
|
15
|
-
assert.deepEqual(findNavOrphans(manifest, reachable), ['/secret-report'])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('findNavOrphans: none when everything is reachable', () => {
|
|
19
|
-
assert.deepEqual(findNavOrphans(['/', '/a'], ['/', '/a', '/extra']), [])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('findNavOrphans: ignore by exact id and by prefix glob', () => {
|
|
23
|
-
const manifest = ['/', '/auth/callback', '/admin/secret', '/deep/x', '/deep/y']
|
|
24
|
-
const reachable = ['/']
|
|
25
|
-
// exact ignore for the callback, prefix ignore for everything under /deep
|
|
26
|
-
const orphans = findNavOrphans(manifest, reachable, { ignore: ['/auth/callback', '/deep/*'] })
|
|
27
|
-
assert.deepEqual(orphans, ['/admin/secret'])
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
// ---------- target size ----------
|
|
31
|
-
|
|
32
|
-
test('meetsTargetSize: 44x44 passes, smaller fails with the short dimension', () => {
|
|
33
|
-
assert.equal(meetsTargetSize({ width: 44, height: 44 }).pass, true)
|
|
34
|
-
const r = meetsTargetSize({ width: 30, height: 44 })
|
|
35
|
-
assert.equal(r.pass, false)
|
|
36
|
-
assert.deepEqual(r.short, ['width'])
|
|
37
|
-
assert.deepEqual(meetsTargetSize({ width: 10, height: 10 }).short, ['width', 'height'])
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('meetsTargetSize: custom minimum', () => {
|
|
41
|
-
assert.equal(meetsTargetSize({ width: 24, height: 24 }, 24).pass, true)
|
|
42
|
-
assert.equal(meetsTargetSize({ width: 24, height: 24 }, 44).pass, false)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// ---------- focus indicator ----------
|
|
46
|
-
|
|
47
|
-
const NO_OUTLINE = { outlineStyle: 'none', outlineWidth: '0px', outlineColor: 'rgb(0,0,0)', boxShadow: 'none' }
|
|
48
|
-
|
|
49
|
-
test('hasVisibleFocusIndicator: outline appearing on focus → visible', () => {
|
|
50
|
-
const focused = { outlineStyle: 'solid', outlineWidth: '2px', outlineColor: 'rgb(0,90,255)', boxShadow: 'none' }
|
|
51
|
-
assert.equal(hasVisibleFocusIndicator(NO_OUTLINE, focused), true)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('hasVisibleFocusIndicator: box-shadow ring appearing on focus → visible', () => {
|
|
55
|
-
const focused = { ...NO_OUTLINE, boxShadow: '0 0 0 3px rgba(0,90,255,.5)' }
|
|
56
|
-
assert.equal(hasVisibleFocusIndicator(NO_OUTLINE, focused), true)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('hasVisibleFocusIndicator: no change between states → NOT visible (the bug)', () => {
|
|
60
|
-
// outline:none in both states and identical shadow — the missing-focus-ring case
|
|
61
|
-
assert.equal(hasVisibleFocusIndicator(NO_OUTLINE, { ...NO_OUTLINE }), false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('hasVisibleFocusIndicator: zero-width color-only outline change → NOT visible', () => {
|
|
65
|
-
// `0px solid red` → `0px solid blue` paints nothing; must not false-pass
|
|
66
|
-
const blurred = { outlineStyle: 'solid', outlineWidth: '0px', outlineColor: 'rgb(255,0,0)', boxShadow: 'none' }
|
|
67
|
-
const focused = { outlineStyle: 'solid', outlineWidth: '0px', outlineColor: 'rgb(0,0,255)', boxShadow: 'none' }
|
|
68
|
-
assert.equal(hasVisibleFocusIndicator(blurred, focused), false)
|
|
69
|
-
})
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// Unit tests for the render-health spine — the pure core, no browser.
|
|
2
|
-
import { test } from 'node:test'
|
|
3
|
-
import assert from 'node:assert/strict'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
isFatalConsole,
|
|
7
|
-
isFatalResponse,
|
|
8
|
-
hasFatalMarker,
|
|
9
|
-
evaluateRenderHealth,
|
|
10
|
-
assertHealthyThrough,
|
|
11
|
-
} from './presentation-render-health.mjs'
|
|
12
|
-
|
|
13
|
-
// Minimal fake Playwright page: records listeners, lets a test emit events, and
|
|
14
|
-
// returns body text from evaluate(). Enough to exercise the watcher/compose path
|
|
15
|
-
// without a browser.
|
|
16
|
-
function fakePage(bodyText = '') {
|
|
17
|
-
const handlers = {}
|
|
18
|
-
return {
|
|
19
|
-
on(ev, fn) { (handlers[ev] ||= []).push(fn) },
|
|
20
|
-
emit(ev, arg) { (handlers[ev] || []).forEach((f) => f(arg)) },
|
|
21
|
-
async evaluate() { return bodyText },
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('console.error is fatal; warning is not', () => {
|
|
26
|
-
assert.equal(isFatalConsole('error', 'boom'), true)
|
|
27
|
-
assert.equal(isFatalConsole('warning', 'meh'), false)
|
|
28
|
-
assert.equal(isFatalConsole('log', 'hi'), false)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('ignored console substrings are not fatal', () => {
|
|
32
|
-
assert.equal(isFatalConsole('error', 'Download the React DevTools', { ignore: ['React DevTools'] }), false)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('http >= 500 is fatal; < 500 is not', () => {
|
|
36
|
-
assert.equal(isFatalResponse(500, '/api/x'), true)
|
|
37
|
-
assert.equal(isFatalResponse(503, '/api/x'), true)
|
|
38
|
-
assert.equal(isFatalResponse(404, '/api/x'), false)
|
|
39
|
-
assert.equal(isFatalResponse(200, '/api/x'), false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('cross-origin 5xx is ignored (externals are faked, not graded)', () => {
|
|
43
|
-
assert.equal(isFatalResponse(500, 'https://stripe.com/v1/x', { origin: 'http://localhost:3000' }), false)
|
|
44
|
-
assert.equal(isFatalResponse(500, 'http://localhost:3000/api/x', { origin: 'http://localhost:3000' }), true)
|
|
45
|
-
assert.equal(isFatalResponse(500, '/api/x', { origin: 'http://localhost:3000' }), true) // relative = first-party
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('error-boundary markers are detected in body text', () => {
|
|
49
|
-
assert.equal(hasFatalMarker('Application error: a client-side exception has occurred'), true)
|
|
50
|
-
assert.equal(hasFatalMarker("Cannot read properties of undefined (reading 'id')"), true)
|
|
51
|
-
assert.equal(hasFatalMarker('All good here'), false)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('the /tasks bug shape fails render-health (pageerror on mount)', () => {
|
|
55
|
-
const v = evaluateRenderHealth({
|
|
56
|
-
pageerrors: [{ message: "Cannot read properties of undefined (reading 'id')" }],
|
|
57
|
-
})
|
|
58
|
-
assert.equal(v.healthy, false)
|
|
59
|
-
assert.equal(v.offenders[0].kind, 'pageerror')
|
|
60
|
-
assert.match(v.offenders[0].detail, /reading 'id'/)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('a clean render is healthy', () => {
|
|
64
|
-
const v = evaluateRenderHealth({
|
|
65
|
-
consoles: [{ type: 'log', text: 'mounted' }, { type: 'warning', text: 'deprecated' }],
|
|
66
|
-
responses: [{ status: 200, url: '/api/tasks/list' }],
|
|
67
|
-
bodyText: 'Tasks\nNo tasks match these filters.',
|
|
68
|
-
})
|
|
69
|
-
assert.equal(v.healthy, true)
|
|
70
|
-
assert.deepEqual(v.offenders, [])
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('assertHealthyThrough fails when an error fires DURING the interaction (mutate-health)', async () => {
|
|
74
|
-
const page = fakePage()
|
|
75
|
-
await assert.rejects(
|
|
76
|
-
assertHealthyThrough(page, async () => {
|
|
77
|
-
// simulate an uncaught error thrown while a delete/submit runs
|
|
78
|
-
page.emit('pageerror', { message: "Cannot read properties of undefined (reading 'id')" })
|
|
79
|
-
}),
|
|
80
|
-
/render-health.*reading 'id'/s,
|
|
81
|
-
)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('assertHealthyThrough passes a clean interaction', async () => {
|
|
85
|
-
const page = fakePage('Tasks\nDeleted.')
|
|
86
|
-
await assertHealthyThrough(page, async () => { /* clean mutation */ })
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('aggregates multiple offenders (pageerror + console + 5xx + boundary)', () => {
|
|
90
|
-
const v = evaluateRenderHealth({
|
|
91
|
-
pageerrors: [{ message: 'x is not a function' }],
|
|
92
|
-
consoles: [{ type: 'error', text: 'render failed' }],
|
|
93
|
-
responses: [{ status: 500, url: '/api/tasks/list' }],
|
|
94
|
-
bodyText: 'Unhandled Runtime Error',
|
|
95
|
-
}, { origin: 'http://localhost:3000' })
|
|
96
|
-
assert.equal(v.healthy, false)
|
|
97
|
-
assert.deepEqual(v.offenders.map((o) => o.kind).sort(), ['console.error', 'error-boundary', 'http>=500', 'pageerror'])
|
|
98
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Unit tests for the sweep's pure core (planning + summarize) — no browser.
|
|
2
|
-
import { test } from 'node:test'
|
|
3
|
-
import assert from 'node:assert/strict'
|
|
4
|
-
|
|
5
|
-
import { planRoutes, summarize } from './presentation-sweep.mjs'
|
|
6
|
-
|
|
7
|
-
test('static routes plan to themselves; route-kind filter applies', () => {
|
|
8
|
-
const plan = planRoutes({ surfaces: [
|
|
9
|
-
{ route: '/tasks', kind: 'route' },
|
|
10
|
-
{ route: '/api/tasks/list', kind: 'api' }, // not a browser route → excluded
|
|
11
|
-
{ route: '/dashboard', kind: 'route' },
|
|
12
|
-
] })
|
|
13
|
-
assert.deepEqual(plan, [
|
|
14
|
-
{ route: '/tasks', url: '/tasks' },
|
|
15
|
-
{ route: '/dashboard', url: '/dashboard' },
|
|
16
|
-
])
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('dynamic route with a resolver becomes a concrete url', () => {
|
|
20
|
-
const plan = planRoutes(
|
|
21
|
-
{ routes: [{ route: '/tasks/[taskRunId]', kind: 'route' }] },
|
|
22
|
-
{ paramResolver: (r) => (r.includes('taskRunId') ? '/tasks/tr_seeded_1' : null) },
|
|
23
|
-
)
|
|
24
|
-
assert.deepEqual(plan, [{ route: '/tasks/[taskRunId]', url: '/tasks/tr_seeded_1' }])
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('dynamic route with NO resolver answer is blocked, not skipped or passed', () => {
|
|
28
|
-
const plan = planRoutes({ routes: [{ route: '/tasks/[id]', kind: 'route' }] })
|
|
29
|
-
assert.equal(plan.length, 1)
|
|
30
|
-
assert.equal(plan[0].url, null)
|
|
31
|
-
assert.match(plan[0].blocked, /param resolver/)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('ignore list drops routes', () => {
|
|
35
|
-
const plan = planRoutes(
|
|
36
|
-
{ routes: [{ route: '/tasks', kind: 'route' }, { route: '/_debug/x', kind: 'route' }] },
|
|
37
|
-
{ ignore: ['/_debug'] },
|
|
38
|
-
)
|
|
39
|
-
assert.deepEqual(plan.map((p) => p.route), ['/tasks'])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('accepts a bare string manifest entry', () => {
|
|
43
|
-
const plan = planRoutes({ routes: ['/a', '/b'] })
|
|
44
|
-
assert.deepEqual(plan.map((p) => p.url), ['/a', '/b'])
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('summarize: a render-health fail flips the gate to non-ok', () => {
|
|
48
|
-
const s = summarize([
|
|
49
|
-
{ route: '/dashboard', levels: [{ level: 'presentation:render-health', status: 'pass' }] },
|
|
50
|
-
{ route: '/tasks', levels: [
|
|
51
|
-
{ level: 'presentation:render-health', status: 'fail', evidence: "pageerror: reading 'id'" },
|
|
52
|
-
{ level: 'presentation:spatial', status: 'blocked', evidence: 'not assessable' },
|
|
53
|
-
] },
|
|
54
|
-
])
|
|
55
|
-
assert.equal(s.ok, false)
|
|
56
|
-
assert.equal(s.failedCount, 2) // the fail + the blocked
|
|
57
|
-
assert.deepEqual(s.viewportFloor, [375, 1280])
|
|
58
|
-
const tasksHealth = s.results.find((r) => r.route === '/tasks' && r.level === 'presentation:render-health')
|
|
59
|
-
assert.equal(tasksHealth.status, 'fail')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('summarize: all-pass is ok', () => {
|
|
63
|
-
const s = summarize([
|
|
64
|
-
{ route: '/dashboard', levels: [
|
|
65
|
-
{ level: 'presentation:render-health', status: 'pass' },
|
|
66
|
-
{ level: 'presentation:spatial', status: 'pass' },
|
|
67
|
-
] },
|
|
68
|
-
])
|
|
69
|
-
assert.equal(s.ok, true)
|
|
70
|
-
assert.equal(s.failedCount, 0)
|
|
71
|
-
})
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// Unit tests for the pure Temporal-stability cores (issue #199). Run: `node --test`.
|
|
2
|
-
// The Playwright wrappers (assertNoFirstPartyShift / assertNoThirdPartyShift /
|
|
3
|
-
// assertNoDynamicViewportShift) need a real browser and are exercised downstream.
|
|
4
|
-
|
|
5
|
-
import { test } from 'node:test'
|
|
6
|
-
import assert from 'node:assert/strict'
|
|
7
|
-
import { findLayoutShift, cumulativeShiftScore } from './presentation-temporal.mjs'
|
|
8
|
-
|
|
9
|
-
const r = (left, top, right, bottom) => ({ rect: { left, top, right, bottom } })
|
|
10
|
-
const at = (selector, left, top, right, bottom) => ({ selector, ...r(left, top, right, bottom) })
|
|
11
|
-
|
|
12
|
-
// ---------- findLayoutShift ----------
|
|
13
|
-
|
|
14
|
-
test('no shift when geometry is unchanged', () => {
|
|
15
|
-
const snap = [at('.header', 0, 0, 375, 60), at('.cta', 0, 100, 375, 140)]
|
|
16
|
-
assert.deepEqual(findLayoutShift(snap, snap), [])
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('flags an element pushed down by injected content', () => {
|
|
20
|
-
const before = [at('.cta', 0, 100, 375, 140)]
|
|
21
|
-
const after = [at('.cta', 0, 260, 375, 300)] // shoved down 160px (a banner injected above)
|
|
22
|
-
const shifted = findLayoutShift(before, after)
|
|
23
|
-
assert.equal(shifted.length, 1)
|
|
24
|
-
assert.equal(shifted[0].selector, '.cta')
|
|
25
|
-
assert.equal(shifted[0].dy, 160)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('sub-tolerance movement is not a shift', () => {
|
|
29
|
-
const before = [at('.x', 0, 100, 375, 140)]
|
|
30
|
-
const after = [at('.x', 0, 101, 375, 141)] // 1px < default 2px tolerance
|
|
31
|
-
assert.deepEqual(findLayoutShift(before, after), [])
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('a newly-injected element is not itself counted as a shift', () => {
|
|
35
|
-
const before = [at('.cta', 0, 100, 375, 140)]
|
|
36
|
-
const after = [at('.cta', 0, 100, 375, 140), at('.banner', 0, 0, 375, 80)] // banner is new, cta did not move
|
|
37
|
-
assert.deepEqual(findLayoutShift(before, after), [])
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('an element that disappears is not a shift of a stable element', () => {
|
|
41
|
-
const before = [at('.spinner', 0, 0, 50, 50), at('.cta', 0, 100, 375, 140)]
|
|
42
|
-
const after = [at('.cta', 0, 100, 375, 140)] // spinner gone, cta stable
|
|
43
|
-
assert.deepEqual(findLayoutShift(before, after), [])
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
// ---------- cumulativeShiftScore ----------
|
|
47
|
-
|
|
48
|
-
test('cumulativeShiftScore sums values without recent input', () => {
|
|
49
|
-
const entries = [
|
|
50
|
-
{ value: 0.05, hadRecentInput: false },
|
|
51
|
-
{ value: 0.10, hadRecentInput: false },
|
|
52
|
-
{ value: 0.30, hadRecentInput: true }, // user-initiated — excluded
|
|
53
|
-
]
|
|
54
|
-
assert.ok(Math.abs(cumulativeShiftScore(entries) - 0.15) < 1e-9)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('cumulativeShiftScore is 0 for no entries', () => {
|
|
58
|
-
assert.equal(cumulativeShiftScore([]), 0)
|
|
59
|
-
})
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { projectManifest, staleOverlayEntries } from './project-route-manifest.mjs'
|
|
5
|
-
|
|
6
|
-
const INVENTORY = {
|
|
7
|
-
surfaces: [
|
|
8
|
-
{ id: '/tasks', kind: 'route', class: 'read', reachability: 'reachable' },
|
|
9
|
-
{ id: '/tasks/[id]', kind: 'route', class: 'detail', reachability: 'reachable' },
|
|
10
|
-
{ id: '/settings/models', kind: 'route', class: 'mutation', reachability: 'reachable' },
|
|
11
|
-
{ id: '/legacy', kind: 'route', class: 'read', reachability: 'reachable' },
|
|
12
|
-
{ id: '/api/tasks/list', kind: 'api', class: 'read' }, // not a browser route
|
|
13
|
-
{ id: 'tasks_table', kind: 'table' },
|
|
14
|
-
],
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
test('projects only browser routes, default tier from class, sorted', () => {
|
|
18
|
-
const m = projectManifest(INVENTORY)
|
|
19
|
-
assert.equal(m.generated, true)
|
|
20
|
-
assert.deepEqual(m.routes, [
|
|
21
|
-
{ path: '/legacy', tier: 1 },
|
|
22
|
-
{ path: '/settings/models', tier: 2 },
|
|
23
|
-
{ path: '/tasks', tier: 1 },
|
|
24
|
-
{ path: '/tasks/[id]', tier: 2 },
|
|
25
|
-
])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('overlay raises tier and drops ignored routes', () => {
|
|
29
|
-
const overlay = { routes: { '/tasks': { tier: 3 }, '/legacy': { ignore: true } }, spatialIgnore: ['.sticky-header'] }
|
|
30
|
-
const m = projectManifest(INVENTORY, overlay)
|
|
31
|
-
assert.equal(m.routes.find((r) => r.path === '/tasks').tier, 3)
|
|
32
|
-
assert.ok(!m.routes.some((r) => r.path === '/legacy'), 'ignored route dropped from sweep')
|
|
33
|
-
assert.deepEqual(m.spatialIgnore, ['.sticky-header'])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('overlay specs are passed through but optional (informational only)', () => {
|
|
37
|
-
const m = projectManifest(INVENTORY, { routes: { '/tasks': { specs: ['render/tasks.spec.ts'] } } })
|
|
38
|
-
assert.deepEqual(m.routes.find((r) => r.path === '/tasks').specs, ['render/tasks.spec.ts'])
|
|
39
|
-
// a route with no overlay has no specs key
|
|
40
|
-
assert.equal('specs' in m.routes.find((r) => r.path === '/settings/models'), false)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('same inventory + overlay → identical manifest (no drift, stable diff)', () => {
|
|
44
|
-
const a = projectManifest(INVENTORY, { routes: { '/tasks': { tier: 3 } } })
|
|
45
|
-
const b = projectManifest(INVENTORY, { routes: { '/tasks': { tier: 3 } } })
|
|
46
|
-
assert.deepEqual(a, b)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('staleOverlayEntries flags curation for a route no longer in inventory', () => {
|
|
50
|
-
const stale = staleOverlayEntries(INVENTORY, { routes: { '/tasks': { tier: 3 }, '/removed': { ignore: true } } })
|
|
51
|
-
assert.deepEqual(stale, ['/removed'])
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
import { spawnSync } from 'node:child_process'
|
|
55
|
-
import { fileURLToPath } from 'node:url'
|
|
56
|
-
import fs from 'node:fs'
|
|
57
|
-
import os from 'node:os'
|
|
58
|
-
import path from 'node:path'
|
|
59
|
-
|
|
60
|
-
const CLI = fileURLToPath(new URL('./project-route-manifest.mjs', import.meta.url))
|
|
61
|
-
|
|
62
|
-
test('CLI: --help prints usage; a missing inventory is a one-line error, not a stack trace', () => {
|
|
63
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'prm-cli-'))
|
|
64
|
-
try {
|
|
65
|
-
const help = spawnSync('node', [CLI, '--help'], { cwd, encoding: 'utf8' })
|
|
66
|
-
assert.equal(help.status, 0)
|
|
67
|
-
assert.match(help.stdout, /usage: project-route-manifest\.mjs/)
|
|
68
|
-
const missing = spawnSync('node', [CLI, '--inventory', path.join(cwd, 'nope.json')], { cwd, encoding: 'utf8' })
|
|
69
|
-
assert.equal(missing.status, 1)
|
|
70
|
-
assert.match(missing.stderr, /inventory not found/)
|
|
71
|
-
assert.doesNotMatch(missing.stderr, /at Object\.|node:internal/, 'no stack trace')
|
|
72
|
-
assert.deepEqual(fs.readdirSync(cwd), [], 'nothing written')
|
|
73
|
-
} finally {
|
|
74
|
-
fs.rmSync(cwd, { recursive: true, force: true })
|
|
75
|
-
}
|
|
76
|
-
})
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const RUNTIME_DIR = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const REPO_ROOT = join(RUNTIME_DIR, '..', '..', '..');
|
|
9
|
-
const MANIFEST = JSON.parse(readFileSync(join(REPO_ROOT, 'skills', 'distribution', 'manifest.json'), 'utf8'));
|
|
10
|
-
|
|
11
|
-
// @covers package:engineering-runtime
|
|
12
|
-
// @level integration
|
|
13
|
-
test('runtime parser dependency metadata lives in the installed scripts directory', () => {
|
|
14
|
-
const scripts = join(RUNTIME_DIR, 'scripts');
|
|
15
|
-
const packageJson = JSON.parse(readFileSync(join(scripts, 'package.json'), 'utf8'));
|
|
16
|
-
const packageLock = JSON.parse(readFileSync(join(scripts, 'package-lock.json'), 'utf8'));
|
|
17
|
-
|
|
18
|
-
assert.equal(existsSync(join(RUNTIME_DIR, 'package.json')), false);
|
|
19
|
-
assert.equal(existsSync(join(RUNTIME_DIR, 'package-lock.json')), false);
|
|
20
|
-
// The lockfile must install exactly the version package.json declares.
|
|
21
|
-
assert.ok(packageJson.dependencies['js-yaml'], 'the runtime parser depends on js-yaml');
|
|
22
|
-
assert.equal(packageLock.packages['node_modules/js-yaml'].version, packageJson.dependencies['js-yaml']);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
function modules(dir) {
|
|
26
|
-
const found = [];
|
|
27
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
-
if (entry.name === 'node_modules' || entry.name.endsWith('-workspace')) continue;
|
|
29
|
-
const path = join(dir, entry.name);
|
|
30
|
-
if (entry.isDirectory()) found.push(...modules(path));
|
|
31
|
-
else if (entry.name.endsWith('.mjs') && !entry.name.endsWith('.test.mjs')) found.push(path);
|
|
32
|
-
}
|
|
33
|
-
return found;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// @covers package:engineering-runtime
|
|
37
|
-
// @level integration
|
|
38
|
-
test('every first-party Workflow consumer resolves one implementation in source and flat layouts', () => {
|
|
39
|
-
const workflow = MANIFEST.groups.find((group) => group.id === 'engineering-workflow');
|
|
40
|
-
const canonical = join(RUNTIME_DIR, 'scripts', 'main-module.mjs');
|
|
41
|
-
const flatCanonical = join('/flat-skills', 'engineering-runtime', 'scripts', 'main-module.mjs');
|
|
42
|
-
const consumers = [];
|
|
43
|
-
|
|
44
|
-
for (const member of workflow.members.filter((candidate) => candidate.role === 'skill')) {
|
|
45
|
-
const skillDir = join(REPO_ROOT, member.path);
|
|
46
|
-
for (const file of modules(skillDir)) {
|
|
47
|
-
const text = readFileSync(file, 'utf8');
|
|
48
|
-
if (!/\bisMainModule\s*\(/.test(text)) continue;
|
|
49
|
-
const staticImport = /import\s+\{\s*isMainModule\s*\}\s+from\s+['"]([^'"]+)['"]/.exec(text);
|
|
50
|
-
const dynamicImport = /await import\(['"]([^'"]+)['"]\)/.exec(text);
|
|
51
|
-
const imported = staticImport ?? dynamicImport;
|
|
52
|
-
assert.ok(imported, `${relative(REPO_ROOT, file)} calls isMainModule without resolving it from the runtime package`);
|
|
53
|
-
assert.equal(resolve(dirname(file), imported[1]), canonical, `${relative(REPO_ROOT, file)} misses the source runtime`);
|
|
54
|
-
|
|
55
|
-
const installedFile = join('/flat-skills', member.installName, relative(skillDir, file));
|
|
56
|
-
assert.equal(
|
|
57
|
-
resolve(dirname(installedFile), imported[1]),
|
|
58
|
-
flatCanonical,
|
|
59
|
-
`${relative(REPO_ROOT, file)} misses the flat-installed runtime`,
|
|
60
|
-
);
|
|
61
|
-
consumers.push(relative(REPO_ROOT, file));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Shipped members only: the archived resolve-issues/resolve-release scripts left this census when
|
|
66
|
-
// those names became aliases (they were never packaged).
|
|
67
|
-
assert.ok(consumers.length >= 17, `the Workflow consumer census found only ${consumers.length} guarded scripts`);
|
|
68
|
-
});
|