@afokapu/atdd-bun 0.4.0 → 0.6.0
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/README.md +85 -3
- package/conventions/coder.bun/coder.bun.complexity-cyclomatic.convention.yaml +1 -3
- package/conventions/coder.bun/coder.bun.complexity-length.convention.yaml +1 -2
- package/conventions/coder.bun/coder.bun.complexity-nesting.convention.yaml +1 -2
- package/conventions/coder.bun/coder.bun.dead-code-reachability.convention.yaml +1 -2
- package/conventions/coder.bun/coder.bun.design-token-color.convention.yaml +1 -1
- package/conventions/coder.bun/coder.bun.design-token-hardcoded.convention.yaml +1 -1
- package/conventions/coder.bun/coder.bun.dto-mapper.convention.yaml +2 -1
- package/conventions/coder.bun/coder.bun.dto-placement.convention.yaml +2 -1
- package/conventions/coder.bun/coder.bun.dto-purity.convention.yaml +2 -1
- package/conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml +1 -2
- package/conventions/coder.bun/coder.bun.green-header-order.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-header-purpose.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-header-runtime.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-header-tested-by.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-urn-layer.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-urn-marker.convention.yaml +2 -12
- package/conventions/coder.bun/coder.bun.green-urn-name-matches.convention.yaml +3 -12
- package/conventions/coder.bun/coder.bun.green-urn-pattern.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-urn-side.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.green-urn-wagon-feature.convention.yaml +2 -11
- package/conventions/coder.bun/coder.bun.layer-naming.convention.yaml +2 -1
- package/conventions/coder.bun/coder.bun.logging-structured.convention.yaml +2 -4
- package/conventions/coder.bun/coder.bun.quality-comments.convention.yaml +1 -2
- package/conventions/coder.bun/coder.bun.quality-mi.convention.yaml +1 -2
- package/conventions/coder.bun/coder.bun.responsive-breakpoints-declared.convention.yaml +31 -0
- package/conventions/coder.bun/coder.bun.responsive-no-fixed-width.convention.yaml +30 -0
- package/conventions/coder.bun/coder.bun.responsive-viewport-meta.convention.yaml +31 -0
- package/conventions/coder.bun/coder.bun.runtime-executes-the-declaration.convention.yaml +2 -18
- package/conventions/coder.bun/coder.bun.runtime-typecheck-enforced.convention.yaml +2 -15
- package/conventions/coder.htmx/coder.htmx.swap-no-inline-handler.convention.yaml +3 -3
- package/conventions/coder.htmx/coder.htmx.verb-endpoint-is-routed.convention.yaml +2 -2
- package/conventions/coder.htmx/coder.htmx.verb-mutation-signals-progress.convention.yaml +3 -2
- package/conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.area-index-required.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml +2 -13
- package/conventions/planner.docs/planner.docs.doc-id-unique.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.graph-target-resolves.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.identity-required.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.reference-integrity.convention.yaml +2 -12
- package/conventions/planner.docs/planner.docs.undeclared-change.convention.yaml +2 -12
- package/conventions/tester.bun/tester.bun.acceptance-binding-declared.convention.yaml +2 -6
- package/conventions/tester.bun/tester.bun.acceptance-covers-tag-well-formed.convention.yaml +1 -5
- package/conventions/tester.bun/tester.bun.interlocking-train-sequence-is-exercised.convention.yaml +2 -14
- package/conventions/tester.bun/tester.bun.red-behavioral-assertion.convention.yaml +1 -5
- package/conventions/tester.bun/tester.bun.routing-runtime-family.convention.yaml +2 -1
- package/conventions/tester.bun/tester.bun.smoke-no-collaborator-substitution.convention.yaml +2 -7
- package/conventions/tester.bun/tester.bun.smoke-observable-assertion.convention.yaml +2 -7
- package/conventions/tester.bun/tester.bun.telemetry-emit.convention.yaml +2 -1
- package/conventions/tester.bun/tester.bun.test-carries-urn-identity.convention.yaml +2 -6
- package/conventions/tester.bun/tester.bun.test-isolation-no-live-state.convention.yaml +1 -5
- package/conventions/tester.bun/tester.bun.test-no-self-skip.convention.yaml +1 -5
- package/conventions/tester.bun/tester.bun.test-phase-declared.convention.yaml +2 -6
- package/conventions/tester.htmx/tester.htmx.a11y-harness.convention.yaml +32 -0
- package/conventions/tester.htmx/tester.htmx.covered-train-is-routed.convention.yaml +30 -0
- package/conventions/tester.htmx/tester.htmx.e2e-binds-declared-subject.convention.yaml +31 -0
- package/conventions/tester.htmx/tester.htmx.e2e-spec-naming.convention.yaml +32 -0
- package/conventions/tester.htmx/tester.htmx.exposed-journey-e2e-coverage.convention.yaml +31 -0
- package/conventions/tester.htmx/tester.htmx.fragment-asserts-returned-markup.convention.yaml +2 -6
- package/conventions/tester.htmx/tester.htmx.journey-binding-header.convention.yaml +32 -0
- package/conventions/tester.htmx/tester.htmx.journey-layer-assembly.convention.yaml +29 -0
- package/conventions/tester.htmx/tester.htmx.journey-no-acceptance-marker.convention.yaml +29 -0
- package/conventions/tester.htmx/tester.htmx.journey-urn-format.convention.yaml +32 -0
- package/conventions/tester.htmx/tester.htmx.presentation-smoke-coverage.convention.yaml +31 -0
- package/conventions/tester.htmx/tester.htmx.responsive-harness.convention.yaml +33 -0
- package/conventions/tester.htmx/tester.htmx.responsive-journey-coverage.convention.yaml +30 -0
- package/conventions/tester.htmx/tester.htmx.swap-oob-asserts-destination-id.convention.yaml +2 -1
- package/conventions/tester.htmx/tester.htmx.train-e2e-coverage.convention.yaml +32 -0
- package/conventions/tester.htmx/tester.htmx.verb-endpoint-coverage.convention.yaml +2 -1
- package/conventions/tester.htmx/tester.htmx.visual-harness.convention.yaml +28 -0
- package/conventions/traceability/traceability.plan.executable-acceptance-has-test.convention.yaml +5 -0
- package/conventions/traceability/traceability.source.tested-by-present.convention.yaml +5 -0
- package/conventions/traceability/traceability.source.tested-by-resolves.convention.yaml +5 -0
- package/conventions/traceability/traceability.test.binding-resolves.convention.yaml +5 -0
- package/detectors/atdd_topology/atdd.implementation.yaml +13 -0
- package/detectors/atdd_topology/detect.mjs +171 -0
- package/detectors/atdd_topology/fixtures/clean/atdd-bun.yaml +2 -0
- package/detectors/atdd_topology/fixtures/clean/e2e/interlockings/checkout/nominal.routes.test.ts +4 -0
- package/detectors/atdd_topology/fixtures/clean/e2e/journeys/checkout.journey.test.ts +4 -0
- package/detectors/atdd_topology/fixtures/clean/implementation/orders/features/place-order/domain/order.ts +4 -0
- package/detectors/atdd_topology/fixtures/clean/plan/_journeys/checkout.yaml +4 -0
- package/detectors/atdd_topology/fixtures/clean/plan/_trains/_interlockings/checkout.yaml +4 -0
- package/detectors/atdd_topology/fixtures/clean/plan/_trains/checkout.yaml +1 -0
- package/detectors/atdd_topology/fixtures/clean/plan/orders/E001.yaml +6 -0
- package/detectors/atdd_topology/fixtures/clean/plan/orders/_orders.yaml +5 -0
- package/detectors/atdd_topology/fixtures/clean/plan/orders/place-order.yaml +3 -0
- package/detectors/atdd_topology/fixtures/clean/tests/wagons/orders/features/place-order/unit/order.test.ts +4 -0
- package/detectors/atdd_topology/fixtures/dirty/plan/_journeys/checkout.yaml +3 -0
- package/detectors/atdd_topology/fixtures/dirty/plan/orders/E001.yaml +4 -0
- package/detectors/atdd_topology/fixtures/dirty/plan/orders/_orders.yaml +4 -0
- package/detectors/atdd_topology/fixtures/dirty/plan/orders/features/place-order.yaml +3 -0
- package/detectors/atdd_topology/fixtures/dirty/src/wagons/orders/features/place-order/domain/forged.ts +2 -0
- package/detectors/atdd_topology/fixtures/dirty/src/wagons/orders/features/place-order/domain/order.ts +2 -0
- package/detectors/atdd_topology/fixtures/dirty/tests/wagons/orders/features/place-order/unit/order.test.ts +4 -0
- package/detectors/atdd_traceability_closure/detect.mjs +5 -2
- package/detectors/bun_green_traceability_detector/urn_header.mjs +2 -1
- package/detectors/bun_interlocking_binding/checks/interlocking_runtime_executes.mjs +3 -2
- package/detectors/bun_interlocking_binding/detect.mjs +6 -5
- package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +4 -3
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +2 -2
- package/detectors/bun_interlocking_infrastructure/_shared/interlocking.mjs +2 -1
- package/detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs +3 -2
- package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +2 -1
- package/detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs +3 -2
- package/detectors/bun_responsive_detector/atdd.implementation.yaml +18 -0
- package/detectors/bun_responsive_detector/checks/_map.json +5 -0
- package/detectors/bun_responsive_detector/checks/_responsive.mjs +20 -0
- package/detectors/bun_responsive_detector/checks/responsive_breakpoints_declared.mjs +21 -0
- package/detectors/bun_responsive_detector/checks/responsive_no_fixed_width.mjs +13 -0
- package/detectors/bun_responsive_detector/checks/responsive_viewport_meta.mjs +20 -0
- package/detectors/bun_responsive_detector/detect.mjs +46 -0
- package/detectors/bun_responsive_detector/fixtures/clean/public/index.html +9 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/app.css +13 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/document.ts +7 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/fragment.html +1 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/head.ts +1 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/layout.tsx +8 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/range.css +3 -0
- package/detectors/bun_responsive_detector/fixtures/clean/src/viewport-expression.tsx +5 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/public/fixed-viewport.html +5 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/public/no-meta.html +5 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/public/no-zoom.html +5 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/public/scale.html +5 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/src/app.css +4 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/src/render.ts +2 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/src/units.css +3 -0
- package/detectors/bun_responsive_detector/fixtures/dirty/src/wide.tsx +3 -0
- package/detectors/bun_tester_discipline_detector/checks/t_acceptance_resolves.mjs +7 -6
- package/detectors/htmx_e2e_detector/atdd.implementation.yaml +40 -0
- package/detectors/htmx_e2e_detector/checks/_e2e.mjs +98 -0
- package/detectors/htmx_e2e_detector/checks/_map.json +16 -0
- package/detectors/htmx_e2e_detector/checks/a11y_harness.mjs +21 -0
- package/detectors/htmx_e2e_detector/checks/covered_train_is_routed.mjs +6 -0
- package/detectors/htmx_e2e_detector/checks/e2e_binds_declared_subject.mjs +11 -0
- package/detectors/htmx_e2e_detector/checks/e2e_spec_naming.mjs +6 -0
- package/detectors/htmx_e2e_detector/checks/exposed_journey_e2e_coverage.mjs +7 -0
- package/detectors/htmx_e2e_detector/checks/journey_binding_header.mjs +13 -0
- package/detectors/htmx_e2e_detector/checks/journey_layer_assembly.mjs +11 -0
- package/detectors/htmx_e2e_detector/checks/journey_no_acceptance_marker.mjs +6 -0
- package/detectors/htmx_e2e_detector/checks/journey_urn_format.mjs +18 -0
- package/detectors/htmx_e2e_detector/checks/presentation_smoke_coverage.mjs +15 -0
- package/detectors/htmx_e2e_detector/checks/responsive_harness.mjs +30 -0
- package/detectors/htmx_e2e_detector/checks/responsive_journey_coverage.mjs +7 -0
- package/detectors/htmx_e2e_detector/checks/train_e2e_coverage.mjs +7 -0
- package/detectors/htmx_e2e_detector/checks/visual_harness.mjs +7 -0
- package/detectors/htmx_e2e_detector/detect.mjs +46 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/buy-alias.a11y.e2e.ts +12 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/buy-inline.responsive.e2e.ts +11 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/buy.a11y.e2e.ts +12 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/buy.responsive.e2e.ts +14 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/buy.visual.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/checkout-buy.smoke.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/crlf.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/place-order.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/reject-empty-cart.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/support/fixtures.ts +3 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/e2e/take-payment.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_journeys/buy.yaml +19 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_journeys/settle.yaml +14 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/_interlockings/checkout.yaml +21 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/_interlockings/payment.yaml +16 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/_interlockings/settlement.yaml +16 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/orders/place-order.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/orders/reject-empty-cart.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/orders/settle-ledger.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/plan/_trains/orders/take-payment.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/playwright.config.ts +3 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/src/wagons/checkout/presentation/cart.tsx +3 -0
- package/detectors/htmx_e2e_detector/fixtures/clean/tests/urn-parser.test.ts +7 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/acceptance.e2e.ts +6 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/bad-id.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/bad-urn.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/both.e2e.ts +6 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/buy.a11y.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/buy.responsive.e2e.ts +11 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/buy.visual.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/covered.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/first-only.responsive.e2e.ts +13 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/ghost.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/misnamed.spec.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/no-binding.e2e.ts +4 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/no-layer.e2e.ts +4 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/no-urn.e2e.ts +4 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/tautology.a11y.e2e.ts +11 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/two-trains.e2e.ts +6 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/unrouted.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/wrong-layer.e2e.ts +5 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/e2e/wrong-urn.e2e.ts +10 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/plan/_journeys/browse.yaml +16 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/plan/_journeys/buy.yaml +16 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/plan/_trains/_interlockings/checkout.yaml +21 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/plan/_trains/orders/covered.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/plan/_trains/orders/uncovered.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/plan/_trains/orders/unrouted.yaml +11 -0
- package/detectors/htmx_e2e_detector/fixtures/dirty/src/wagons/billing/presentation/invoice.tsx +3 -0
- package/integrity.json +208 -79
- package/lib/frontend.mjs +17 -0
- package/lib/scan.mjs +6 -2
- package/package.json +5 -1
- package/planner-nodes/ENFORCEMENT_SCOPE.yaml +18 -1
- package/relationships.yaml +4782 -4506
- package/src/enforce.ts +10 -7
- package/src/hooks.ts +2 -1
- package/src/index.ts +2 -0
- package/src/planner-kernel.ts +2 -1
- package/src/planner-schema-validator.ts +12 -10
- package/src/planner-validators.ts +16 -15
- package/src/topology.ts +40 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// coder.bun.responsive-viewport-meta: every full HTML document declares a device-width viewport and never blocks zoom.
|
|
3
|
+
import { runCheck } from "./_responsive.mjs";
|
|
4
|
+
const RULE = "coder.bun.responsive-viewport-meta";
|
|
5
|
+
runCheck("responsive-viewport-meta", (root, files, report) => {
|
|
6
|
+
for (const { file, text } of files) {
|
|
7
|
+
const html = text.search(/<html[\s>]/i);
|
|
8
|
+
if (file.endsWith(".css") || html === -1) continue;
|
|
9
|
+
const meta = [...text.matchAll(/<meta\b[^>]*>/gi)].find((m) => /name\s*=\s*["']viewport["']/i.test(m[0]));
|
|
10
|
+
// A head assembled from a partial (`<head>${head}</head>`, `<head>{head}</head>`) is judged where the partial is written.
|
|
11
|
+
const head = text.slice(html).match(/<head\b[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
|
|
12
|
+
if (!meta && /\$\{|\{/.test(head)) continue;
|
|
13
|
+
if (!meta) { report(RULE, file, text, html, 'document has no <meta name="viewport" content="width=device-width, initial-scale=1">; phones render it as a zoomed-out desktop page'); continue; }
|
|
14
|
+
if (/content\s*=\s*\{/.test(meta[0])) continue; // a JSX expression is not statically known; its presence is enough
|
|
15
|
+
const content = meta[0].match(/content\s*=\s*["']([^"']*)["']/i)?.[1] ?? "";
|
|
16
|
+
if (!/width\s*=\s*device-width/i.test(content)) report(RULE, file, text, meta.index, `viewport content "${content}" does not set width=device-width`);
|
|
17
|
+
const scale = content.match(/maximum-scale\s*=\s*([\d.]+)/i);
|
|
18
|
+
if (/user-scalable\s*=\s*(no|0)\b/i.test(content) || (scale && Number(scale[1]) < 2)) report(RULE, file, text, meta.index, `viewport content "${content}" blocks zoom (user-scalable=no or maximum-scale below 2), which fails users who need to enlarge text`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// FAMILY validator: bun_responsive_detector
|
|
3
|
+
// Runs each member check (checks/*.mjs) VERBATIM as a subprocess and merges their
|
|
4
|
+
// RAW v1.1 reports into one — ONE implementation realizing a family of rule_ids
|
|
5
|
+
// (the Core pattern; see frontend.workspace.runtime's families).
|
|
6
|
+
//
|
|
7
|
+
// Members are spawned with `process.execPath`, which under this provider IS the
|
|
8
|
+
// bun binary, so a member may be authored as .mjs OR .ts with no build step.
|
|
9
|
+
// Files whose name begins with `_` are skipped: they are shared helpers, not
|
|
10
|
+
// checks. (The node-runtime families have no such convention because each of
|
|
11
|
+
// their checks re-implements its own walker; this provider factors the walk into
|
|
12
|
+
// lib/scan.mjs instead.)
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { readFileSync, writeFileSync, mkdtempSync, readdirSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const reportPath = process.env.ATDD_VIOLATIONS_REPORT;
|
|
21
|
+
if (!reportPath) {
|
|
22
|
+
process.stderr.write("family: ATDD_VIOLATIONS_REPORT not set\n");
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
const checks = readdirSync(join(here, "checks"))
|
|
26
|
+
.filter((f) => (f.endsWith(".mjs") || f.endsWith(".ts")) && !f.startsWith("_"))
|
|
27
|
+
.sort();
|
|
28
|
+
const td = mkdtempSync(join(tmpdir(), "atdd-bun-fam-"));
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const c of checks) {
|
|
31
|
+
const rep = join(td, c + ".json");
|
|
32
|
+
try {
|
|
33
|
+
execFileSync(process.execPath, [join(here, "checks", c)], {
|
|
34
|
+
env: { ...process.env, ATDD_VIOLATIONS_REPORT: rep },
|
|
35
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
/* a member may exit non-zero; still try to read its report */
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
out.push(...JSON.parse(readFileSync(rep, "utf8")).violations);
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(reportPath, JSON.stringify({ violations: out }, null, 2), "utf8");
|
|
45
|
+
process.stderr.write("family bun_responsive_detector: " + out.length + " violation(s)\n");
|
|
46
|
+
process.exit(0);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--page: 1200px;
|
|
3
|
+
}
|
|
4
|
+
.page {
|
|
5
|
+
width: 100%;
|
|
6
|
+
max-width: 1200px;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
border: 1px solid currentColor;
|
|
9
|
+
}
|
|
10
|
+
.sidebar { width: 280px; }
|
|
11
|
+
@media (min-width: 768px) { .page { padding: 2rem; } }
|
|
12
|
+
@media (max-width: 767.98px) { .sidebar { width: 100%; } }
|
|
13
|
+
@media (min-width: 64em) { .page { margin: 0 auto; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<section class="card"><p>No document here, so no viewport meta is required.</p></section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const head = `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function Layout(props: { children: unknown }) {
|
|
2
|
+
return (
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5" /></head>
|
|
5
|
+
<body style={{ maxWidth: 1200, width: "100%" }}>{props.children}</body>
|
|
6
|
+
</html>
|
|
7
|
+
);
|
|
8
|
+
}
|
|
@@ -48,9 +48,9 @@ function planFiles(dir, excludes) {
|
|
|
48
48
|
// and WMBT documents, and a reference is resolvable if the plan states that URN
|
|
49
49
|
// anywhere. Over-accepting here is safe — the failure this rule exists to catch is a
|
|
50
50
|
// reference to something the plan never mentions at all.
|
|
51
|
-
function declaredUrns(croot, excludes) {
|
|
51
|
+
function declaredUrns(croot, excludes, planRoot) {
|
|
52
52
|
const urns = new Set();
|
|
53
|
-
for (const f of planFiles(join(croot,
|
|
53
|
+
for (const f of planFiles(join(croot, planRoot), excludes)) {
|
|
54
54
|
for (const m of read(f).matchAll(/\b((?:acc|wmbt):[a-z0-9][\w.-]*(?::[\w.-]+)?)/gi)) {
|
|
55
55
|
urns.add(m[1]);
|
|
56
56
|
}
|
|
@@ -58,13 +58,13 @@ function declaredUrns(croot, excludes) {
|
|
|
58
58
|
return urns;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function consumerRoots(root, excludes) {
|
|
61
|
+
function consumerRoots(root, excludes, planRoot) {
|
|
62
62
|
const roots = [];
|
|
63
63
|
(function rec(d) {
|
|
64
64
|
let st;
|
|
65
65
|
try { st = statSync(d); } catch { return; }
|
|
66
66
|
if (!st.isDirectory()) return;
|
|
67
|
-
try { if (statSync(join(d,
|
|
67
|
+
try { if (statSync(join(d, planRoot)).isDirectory()) { roots.push(d); return; } } catch {}
|
|
68
68
|
let entries;
|
|
69
69
|
try { entries = readdirSync(d).sort(); } catch { return; }
|
|
70
70
|
for (const n of entries) if (!excludes.includes(n)) rec(join(d, n));
|
|
@@ -79,10 +79,11 @@ if (!reportPath) {
|
|
|
79
79
|
}
|
|
80
80
|
const excludes = [...DEFAULT_EXCLUDES, ...parseJsonEnv("ATDD_SCAN_EXCLUDES", [])];
|
|
81
81
|
const violations = [];
|
|
82
|
+
const planRoot = process.env.ATDD_PLAN_ROOT || "plan";
|
|
82
83
|
|
|
83
84
|
for (const root of parseJsonEnv("ATDD_SCAN_ROOTS", [])) {
|
|
84
|
-
for (const croot of consumerRoots(root, excludes)) {
|
|
85
|
-
const declared = declaredUrns(croot, excludes);
|
|
85
|
+
for (const croot of consumerRoots(root, excludes, planRoot)) {
|
|
86
|
+
const declared = declaredUrns(croot, excludes, planRoot);
|
|
86
87
|
if (!declared.size) continue; // nothing declared: nothing to resolve against
|
|
87
88
|
for (const file of walkTests(croot, excludes)) {
|
|
88
89
|
const H = parseHeader(read(file));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
kind: implementation
|
|
3
|
+
subtype: validator
|
|
4
|
+
implementation_id: htmx_e2e_detector
|
|
5
|
+
targets_workspace: atdd.workspace.bun
|
|
6
|
+
contract_version: 1.1.0
|
|
7
|
+
emits_rule_ids:
|
|
8
|
+
- tester.htmx.e2e-spec-naming
|
|
9
|
+
- tester.htmx.journey-binding-header
|
|
10
|
+
- tester.htmx.journey-urn-format
|
|
11
|
+
- tester.htmx.journey-layer-assembly
|
|
12
|
+
- tester.htmx.journey-no-acceptance-marker
|
|
13
|
+
- tester.htmx.e2e-binds-declared-subject
|
|
14
|
+
- tester.htmx.covered-train-is-routed
|
|
15
|
+
- tester.htmx.train-e2e-coverage
|
|
16
|
+
- tester.htmx.exposed-journey-e2e-coverage
|
|
17
|
+
- tester.htmx.presentation-smoke-coverage
|
|
18
|
+
- tester.htmx.a11y-harness
|
|
19
|
+
- tester.htmx.visual-harness
|
|
20
|
+
- tester.htmx.responsive-harness
|
|
21
|
+
- tester.htmx.responsive-journey-coverage
|
|
22
|
+
entrypoint: detect.mjs
|
|
23
|
+
report: detect.mjs
|
|
24
|
+
realizes_convention:
|
|
25
|
+
# The Bun/htmx browser tester: Playwright specs (*.e2e.ts), their bindings to plan/, and the
|
|
26
|
+
# harnesses they must use. Ported from frontend.extension.tester.vite, strict throughout.
|
|
27
|
+
- tester.htmx.e2e-spec-naming
|
|
28
|
+
- tester.htmx.journey-binding-header
|
|
29
|
+
- tester.htmx.journey-urn-format
|
|
30
|
+
- tester.htmx.journey-layer-assembly
|
|
31
|
+
- tester.htmx.journey-no-acceptance-marker
|
|
32
|
+
- tester.htmx.e2e-binds-declared-subject
|
|
33
|
+
- tester.htmx.covered-train-is-routed
|
|
34
|
+
- tester.htmx.train-e2e-coverage
|
|
35
|
+
- tester.htmx.exposed-journey-e2e-coverage
|
|
36
|
+
- tester.htmx.presentation-smoke-coverage
|
|
37
|
+
- tester.htmx.a11y-harness
|
|
38
|
+
- tester.htmx.visual-harness
|
|
39
|
+
- tester.htmx.responsive-harness
|
|
40
|
+
- tester.htmx.responsive-journey-coverage
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Shared model for the htmx_e2e_detector family: browser (Playwright) specs and the plan they cover.
|
|
2
|
+
//
|
|
3
|
+
// A BROWSER SPEC is a `*.e2e.ts` file. It is named that way, not `*.spec.ts`, because `bun test`
|
|
4
|
+
// collects every `*.spec.*` and `*.test.*` file and would try to run a Playwright spec with the
|
|
5
|
+
// wrong runner. A JOURNEY SPEC is a browser spec, or any test file whose header comments carry a
|
|
6
|
+
// journey marker (`// Train:`, `// Journey:`, `// URN: test:train|journey:`), so a misnamed one is still judged.
|
|
7
|
+
//
|
|
8
|
+
// A journey spec binds to ONE plan subject through its header, and every test URN it carries
|
|
9
|
+
// must agree with that binding:
|
|
10
|
+
//
|
|
11
|
+
// // Train: train:orders:place-order or // Journey: journey:buy
|
|
12
|
+
// // Layer: assembly
|
|
13
|
+
// test:train:orders:place-order:E2E-001-places-an-order (harness E2E | SMOKE | A11Y | VIS | RESP)
|
|
14
|
+
import { basename, join, resolve } from "node:path";
|
|
15
|
+
import { walk, readText, maskLiteralsAndComments } from "../../../lib/scan.mjs";
|
|
16
|
+
import { loadPlan } from "../../../src/planner-kernel.ts";
|
|
17
|
+
|
|
18
|
+
export const TRAIN_ID = /^train:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$/;
|
|
19
|
+
export const JOURNEY_ID = /^journey:[a-z][a-z0-9-]*$/;
|
|
20
|
+
export const HARNESSES = ["E2E", "SMOKE", "A11Y", "VIS", "RESP"];
|
|
21
|
+
const URN_STRICT = /^test:(train:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*|journey:[a-z][a-z0-9-]*):(E2E|SMOKE|A11Y|VIS|RESP)-\d{3}-[a-z0-9][a-z0-9-]*$/;
|
|
22
|
+
const URN_CANDIDATE = /test:(?:train|journey):[A-Za-z0-9:_-]+/g;
|
|
23
|
+
const SPEC_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"]);
|
|
24
|
+
const TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
25
|
+
export const E2E_RE = /\.e2e\.[cm]?[jt]sx?$/;
|
|
26
|
+
|
|
27
|
+
const header = (text, name) => { const m = text.match(new RegExp(`^[ \\t]*//[ \\t]*${name}:[ \\t]*(\\S+)[ \\t]*$`, "m")); return m ? { value: m[1], line: lineOf(text, m.index) } : null; };
|
|
28
|
+
export const lineOf = (text, index) => text.slice(0, index).split("\n").length;
|
|
29
|
+
export const firstLine = (text) => (text.split(/\r?\n/)[0] || "").trim();
|
|
30
|
+
|
|
31
|
+
/** Everything a check needs to know about one test file. */
|
|
32
|
+
export function parseSpec(file, text) {
|
|
33
|
+
const train = header(text, "Train"), journey = header(text, "Journey");
|
|
34
|
+
const urns = [...text.matchAll(URN_CANDIDATE)].map((m) => ({ urn: m[0], index: m.index, valid: URN_STRICT.test(m[0]) }));
|
|
35
|
+
const harnesses = new Set(urns.filter((u) => u.valid).map((u) => u.urn.split(":").at(-1).split("-")[0]));
|
|
36
|
+
const name = basename(file).toLowerCase();
|
|
37
|
+
if (/(^|[.-])a11y([.-]|$)/.test(name)) harnesses.add("A11Y");
|
|
38
|
+
if (/(^|[.-])(visual|vis)([.-]|$)/.test(name)) harnesses.add("VIS");
|
|
39
|
+
if (/(^|[.-])(responsive|resp)([.-]|$)/.test(name)) harnesses.add("RESP");
|
|
40
|
+
if (/(^|[.-])smoke([.-]|$)/.test(name)) harnesses.add("SMOKE");
|
|
41
|
+
const binding = train ? { kind: "train", id: train.value, line: train.line } : journey ? { kind: "journey", id: journey.value, line: journey.line } : null;
|
|
42
|
+
const bindingCount = [...text.matchAll(/^[ \t]*\/\/[ \t]*(Train|Journey):/gm)].length;
|
|
43
|
+
return {
|
|
44
|
+
file, text, binding, bindingCount, urns, harnesses,
|
|
45
|
+
layer: header(text, "Layer"),
|
|
46
|
+
acceptance: text.match(/^[ \t]*\/\/[ \t]*(Acceptance|WMBT):[ \t]*\S/m),
|
|
47
|
+
playwright: /from\s+["']@playwright\/test["']/.test(text),
|
|
48
|
+
// A spec declares tests; a config (`defineConfig`) or a fixture module (`base.extend`) only imports the runner.
|
|
49
|
+
declaresTests: !/(^|[\\/])playwright\.config\.[cm]?[jt]s$/.test(file) && /(^|[^.\w])test(\.(describe|only|skip|fixme|fail|slow|step))?\s*\(/m.test(maskLiteralsAndComments(text)),
|
|
50
|
+
e2eNamed: E2E_RE.test(file),
|
|
51
|
+
// Outside *.e2e.ts only HEADER comments mark a journey spec; a unit test that merely mentions a
|
|
52
|
+
// journey URN in a string is not one.
|
|
53
|
+
journeyMarked: Boolean(train || journey) || /^[ \t]*\/\/[ \t]*URN:[ \t]*test:(train|journey):/m.test(text),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Every file a check should look at: browser specs, test files, and anything importing Playwright. */
|
|
58
|
+
export function collectSpecs(roots, excludes) {
|
|
59
|
+
const out = new Map();
|
|
60
|
+
for (const root of roots) for (const file of walk(root, excludes, SPEC_EXT, true)) {
|
|
61
|
+
if (out.has(resolve(file))) continue;
|
|
62
|
+
const text = readText(file); if (text === null) continue;
|
|
63
|
+
if (E2E_RE.test(file) || TEST_RE.test(file) || /@playwright\/test/.test(text)) out.set(resolve(file), parseSpec(file, text));
|
|
64
|
+
}
|
|
65
|
+
return [...out.values()];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const isJourneySpec = (spec) => spec.e2eNamed || spec.journeyMarked;
|
|
69
|
+
|
|
70
|
+
/** The plan subjects a spec can bind to, read with the package's own loader. */
|
|
71
|
+
export async function planOf(root) {
|
|
72
|
+
const graph = await loadPlan(root);
|
|
73
|
+
const trainArtifacts = graph.artifacts.filter((a) => a.kind === "train"), trains = new Set(trainArtifacts.map((a) => a.id));
|
|
74
|
+
const trainFiles = new Map(trainArtifacts.map((a) => [a.id, join(root, a.file)]));
|
|
75
|
+
// A train is human-facing when a person takes part in it; only those have a screen a browser spec can drive.
|
|
76
|
+
const refs = (a) => [...(Array.isArray(a.data.participants) ? a.data.participants : []), ...(Array.isArray(a.data.sequence) ? a.data.sequence.flatMap((s) => [s?.from, s?.to]) : [])].map(String);
|
|
77
|
+
const humanTrains = new Set(trainArtifacts.filter((a) => refs(a).some((r) => r.startsWith("user:"))).map((a) => a.id));
|
|
78
|
+
const interlockings = graph.artifacts.filter((a) => a.kind === "interlocking");
|
|
79
|
+
const routed = new Set(interlockings.flatMap((il) => (Array.isArray(il.data.routes) ? il.data.routes : []).map((r) => String(r?.train_id ?? ""))).filter(Boolean));
|
|
80
|
+
// A journey has a browser surface unless its entrypoint declares surfaces that exclude `frontend`.
|
|
81
|
+
const frontend = (surfaces) => !Array.isArray(surfaces) || surfaces.map(String).includes("frontend");
|
|
82
|
+
const journeys = graph.artifacts.filter((a) => a.kind === "journey").map((a) => ({ id: a.id, file: a.file, exposed: a.data?.entrypoint?.exposed === true, frontend: frontend(a.data?.entrypoint?.surfaces) }));
|
|
83
|
+
return { trains, trainFiles, humanTrains, routed, journeys: journeys.map((j) => ({ ...j, path: join(root, j.file) })), journeyIds: new Set(journeys.map((j) => j.id)), hasPlan: trains.size > 0 || journeys.length > 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { frontendConfig, DEFAULT_VIEWPORTS, DEFAULT_BREAKPOINTS } from "../../../lib/frontend.mjs";
|
|
87
|
+
|
|
88
|
+
/** Run one check over every scan root: `judge(root, specs, plan, report)`. */
|
|
89
|
+
export async function runCheck(tag, judge) {
|
|
90
|
+
const { readRoots, readExcludes, emit } = await import("../../../lib/scan.mjs");
|
|
91
|
+
const violations = [];
|
|
92
|
+
for (const root of readRoots()) {
|
|
93
|
+
const report = (rule_id, file, line, evidence, source_line = "") => violations.push({ rule_id, file, line, col: 1, evidence, source_line });
|
|
94
|
+
await judge(resolve(root), collectSpecs([root], readExcludes()), await planOf(resolve(root)), report);
|
|
95
|
+
}
|
|
96
|
+
process.stderr.write(`bun-detector[${tag}]: ${violations.length} violation(s)\n`);
|
|
97
|
+
emit(violations);
|
|
98
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"e2e_spec_naming": "tester.htmx.e2e-spec-naming",
|
|
3
|
+
"journey_binding_header": "tester.htmx.journey-binding-header",
|
|
4
|
+
"journey_urn_format": "tester.htmx.journey-urn-format",
|
|
5
|
+
"journey_layer_assembly": "tester.htmx.journey-layer-assembly",
|
|
6
|
+
"journey_no_acceptance_marker": "tester.htmx.journey-no-acceptance-marker",
|
|
7
|
+
"e2e_binds_declared_subject": "tester.htmx.e2e-binds-declared-subject",
|
|
8
|
+
"covered_train_is_routed": "tester.htmx.covered-train-is-routed",
|
|
9
|
+
"train_e2e_coverage": "tester.htmx.train-e2e-coverage",
|
|
10
|
+
"exposed_journey_e2e_coverage": "tester.htmx.exposed-journey-e2e-coverage",
|
|
11
|
+
"presentation_smoke_coverage": "tester.htmx.presentation-smoke-coverage",
|
|
12
|
+
"a11y_harness": "tester.htmx.a11y-harness",
|
|
13
|
+
"visual_harness": "tester.htmx.visual-harness",
|
|
14
|
+
"responsive_harness": "tester.htmx.responsive-harness",
|
|
15
|
+
"responsive_journey_coverage": "tester.htmx.responsive-journey-coverage"
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.a11y-harness: an A11Y spec runs axe and asserts, in a way that can fail, on its violations.
|
|
3
|
+
import { maskLiteralsAndComments } from "../../../lib/scan.mjs";
|
|
4
|
+
import { runCheck, isJourneySpec } from "./_e2e.mjs";
|
|
5
|
+
|
|
6
|
+
// The builder may be imported under any name: `import Axe from ...` or `import { AxeBuilder as Axe } from ...`.
|
|
7
|
+
const IMPORT = /import\s+(?:(\w+)|\{\s*AxeBuilder(?:\s+as\s+(\w+))?\s*\})\s+from\s+["']@axe-core\/playwright["']/;
|
|
8
|
+
// The assertion must be able to fail: the violations compared with none, or with a bounded count. A tautology
|
|
9
|
+
// such as `toBeGreaterThanOrEqual(0)` or `toBeDefined()` asserts nothing about the page.
|
|
10
|
+
const ASSERTS = /expect\s*\([^;]*?violations[^;]*?\)\s*\.\s*(toEqual\s*\(\s*\[\s*\]\s*\)|toStrictEqual\s*\(\s*\[\s*\]\s*\)|toHaveLength\s*\(\s*0\s*\)|toBe\s*\(\s*0\s*\)|toBeLessThanOrEqual\s*\(\s*\d+\s*\)|toBeLessThan\s*\(\s*\d+\s*\))/;
|
|
11
|
+
|
|
12
|
+
await runCheck("a11y-harness", (root, specs, plan, report) => {
|
|
13
|
+
for (const s of specs) {
|
|
14
|
+
if (!isJourneySpec(s) || !s.harnesses.has("A11Y")) continue;
|
|
15
|
+
const code = maskLiteralsAndComments(s.text), imported = s.text.match(IMPORT), builder = imported ? imported[1] ?? imported[2] ?? "AxeBuilder" : null, missing = [];
|
|
16
|
+
if (!builder) missing.push("import AxeBuilder from @axe-core/playwright");
|
|
17
|
+
else if (!new RegExp(`new\\s+${builder}\\s*\\(`).test(code) || !/\.analyze\s*\(/.test(code)) missing.push(`construct ${builder} and call .analyze()`);
|
|
18
|
+
if (!ASSERTS.test(code)) missing.push("assert that the violations are none (toEqual([]), toHaveLength(0)) or bounded (toBeLessThanOrEqual(n))");
|
|
19
|
+
if (missing.length) report("tester.htmx.a11y-harness", s.file, 1, `A11Y spec does not ${missing.join("; ")}; an accessibility check that never runs axe, or cannot fail, passes whatever the page's state`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.covered-train-is-routed: a train a spec covers is selected by some interlocking route.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("covered-train-is-routed", (root, specs, plan, report) => {
|
|
5
|
+
for (const s of specs) if (isJourneySpec(s) && s.binding?.kind === "train" && plan.trains.has(s.binding.id) && !plan.routed.has(s.binding.id)) report("tester.htmx.covered-train-is-routed", s.file, s.binding.line, `${s.binding.id} is declared but no interlocking route selects it, so nothing in the application can reach what this spec exercises`);
|
|
6
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.e2e-binds-declared-subject: the train or journey a spec binds to is declared in plan/.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("e2e-binds-declared-subject", (root, specs, plan, report) => {
|
|
5
|
+
if (!plan.hasPlan) return;
|
|
6
|
+
for (const s of specs) {
|
|
7
|
+
if (!isJourneySpec(s) || !s.binding) continue;
|
|
8
|
+
const declared = s.binding.kind === "train" ? plan.trains.has(s.binding.id) : plan.journeyIds.has(s.binding.id);
|
|
9
|
+
if (!declared && (s.binding.kind === "train" ? TRAIN_ID : JOURNEY_ID).test(s.binding.id)) report("tester.htmx.e2e-binds-declared-subject", s.file, s.binding.line, `bound to ${s.binding.id}, which plan/ does not declare; the spec is orphaned from the plan`);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.e2e-spec-naming: a Playwright spec is named *.e2e.ts, so `bun test` never runs it.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("e2e-spec-naming", (root, specs, plan, report) => {
|
|
5
|
+
for (const s of specs) if (s.playwright && s.declaresTests && !s.e2eNamed) report("tester.htmx.e2e-spec-naming", s.file, 1, "imports @playwright/test but is not named *.e2e.ts; bun test collects *.test.* and *.spec.* files and would run it with the wrong runner", firstLine(s.text));
|
|
6
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.exposed-journey-e2e-coverage: every exposed journey has an E2E or SMOKE browser spec bound to it.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("exposed-journey-e2e-coverage", (root, specs, plan, report) => {
|
|
5
|
+
const covered = new Set(specs.filter((s) => isJourneySpec(s) && s.binding?.kind === "journey" && (s.harnesses.has("E2E") || s.harnesses.has("SMOKE"))).map((s) => s.binding.id));
|
|
6
|
+
for (const j of plan.journeys) if (j.exposed && j.frontend && !covered.has(j.id)) report("tester.htmx.exposed-journey-e2e-coverage", j.path, 1, `exposed journey ${j.id} has no E2E or SMOKE browser spec bound by \`// Journey: ${j.id}\``);
|
|
7
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.journey-binding-header: a journey spec binds to exactly one train or journey by a well-formed header.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
const RULE = "tester.htmx.journey-binding-header";
|
|
5
|
+
await runCheck("journey-binding-header", (root, specs, plan, report) => {
|
|
6
|
+
for (const s of specs) {
|
|
7
|
+
if (!isJourneySpec(s)) continue;
|
|
8
|
+
if (!s.binding) { report(RULE, s.file, 1, "journey spec has no `// Train: train:<subject>:<slug>` or `// Journey: journey:<id>` header", firstLine(s.text)); continue; }
|
|
9
|
+
if (s.bindingCount > 1) report(RULE, s.file, s.binding.line, `journey spec declares ${s.bindingCount} \`// Train:\`/\`// Journey:\` headers; bind it to exactly one plan subject`);
|
|
10
|
+
const valid = s.binding.kind === "train" ? TRAIN_ID : JOURNEY_ID;
|
|
11
|
+
if (!valid.test(s.binding.id)) report(RULE, s.file, s.binding.line, `\`${s.binding.id}\` is not a valid ${s.binding.kind} id (${valid.source})`);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.journey-layer-assembly: a journey spec declares `// Layer: assembly`.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
const RULE = "tester.htmx.journey-layer-assembly";
|
|
5
|
+
await runCheck("journey-layer-assembly", (root, specs, plan, report) => {
|
|
6
|
+
for (const s of specs) {
|
|
7
|
+
if (!isJourneySpec(s)) continue;
|
|
8
|
+
if (!s.layer) report(RULE, s.file, 1, "journey spec has no `// Layer:` header; it MUST be `// Layer: assembly`", firstLine(s.text));
|
|
9
|
+
else if (s.layer.value !== "assembly") report(RULE, s.file, s.layer.line, `\`// Layer: ${s.layer.value}\`: a journey spec exercises the assembled application and MUST declare Layer: assembly`);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.journey-no-acceptance-marker: journey and acceptance identities are exclusive.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("journey-no-acceptance-marker", (root, specs, plan, report) => {
|
|
5
|
+
for (const s of specs) if (isJourneySpec(s) && s.acceptance) report("tester.htmx.journey-no-acceptance-marker", s.file, lineOf(s.text, s.acceptance.index + s.acceptance[0].search(/\S/)), `journey spec carries \`// ${s.acceptance[1]}:\`; a journey test binds to a train or journey, an acceptance test to an Acceptance/WMBT pair, never both`);
|
|
6
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.journey-urn-format: a journey spec carries test URNs of the canonical grammar, all for its bound subject.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
const RULE = "tester.htmx.journey-urn-format";
|
|
5
|
+
await runCheck("journey-urn-format", (root, specs, plan, report) => {
|
|
6
|
+
for (const s of specs) {
|
|
7
|
+
if (!isJourneySpec(s)) continue;
|
|
8
|
+
if (!s.urns.length) { report(RULE, s.file, 1, "journey spec carries no test URN test:{train|journey}:<id>:{E2E|SMOKE|A11Y|VIS|RESP}-NNN-<slug>", firstLine(s.text)); continue; }
|
|
9
|
+
const seen = new Set(), bound = s.binding && (s.binding.kind === "train" ? TRAIN_ID : JOURNEY_ID).test(s.binding.id) ? s.binding.id : null;
|
|
10
|
+
for (const u of s.urns) {
|
|
11
|
+
if (seen.has(u.urn)) continue;
|
|
12
|
+
seen.add(u.urn);
|
|
13
|
+
if (!u.valid) { report(RULE, s.file, lineOf(s.text, u.index), `test URN \`${u.urn}\` is malformed; want test:{train|journey}:<id>:{E2E|SMOKE|A11Y|VIS|RESP}-NNN-<slug>`); continue; }
|
|
14
|
+
const subject = u.urn.slice("test:".length, u.urn.lastIndexOf(":"));
|
|
15
|
+
if (bound && subject !== bound) report(RULE, s.file, lineOf(s.text, u.index), `test URN \`${u.urn}\` names ${subject}, but the spec is bound to ${s.binding.id}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.presentation-smoke-coverage: every presentation component is covered by a SMOKE browser spec naming its wagon.
|
|
3
|
+
import { relative, sep } from "node:path";
|
|
4
|
+
import { walk } from "../../../lib/scan.mjs";
|
|
5
|
+
import { runCheck, isJourneySpec } from "./_e2e.mjs";
|
|
6
|
+
await runCheck("presentation-smoke-coverage", async (root, specs, plan, report) => {
|
|
7
|
+
const { readExcludes } = await import("../../../lib/scan.mjs");
|
|
8
|
+
const smoke = specs.filter((s) => isJourneySpec(s) && s.harnesses.has("SMOKE")).map((s) => relative(root, s.file));
|
|
9
|
+
for (const file of walk(root, readExcludes(), new Set([".tsx", ".jsx"]))) {
|
|
10
|
+
const parts = relative(root, file).split(sep), at = parts.lastIndexOf("presentation");
|
|
11
|
+
if (at < 1 || at === parts.length - 1) continue;
|
|
12
|
+
const wagon = parts[at - 1], token = new RegExp(`(^|[^a-z0-9])${wagon.replace(/[^a-z0-9-]/gi, "")}([^a-z0-9]|$)`, "i");
|
|
13
|
+
if (!smoke.some((path) => token.test(path))) report("tester.htmx.presentation-smoke-coverage", file, 1, `presentation component of wagon "${wagon}" has no SMOKE browser spec whose path names the wagon`);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.responsive-harness: a RESP spec renders at every declared viewport and asserts no horizontal overflow.
|
|
3
|
+
import { maskLiteralsAndComments } from "../../../lib/scan.mjs";
|
|
4
|
+
import { runCheck, isJourneySpec, frontendConfig } from "./_e2e.mjs";
|
|
5
|
+
|
|
6
|
+
/** The widths a spec actually renders at: literal viewport widths, or a list of widths it iterates. */
|
|
7
|
+
function renderedWidths(code) {
|
|
8
|
+
const widths = new Set([...code.matchAll(/(?:setViewportSize\s*\(\s*\{|viewport\s*:\s*\{)[^}]*?width\s*:\s*(\d+)/g)].map((m) => Number(m[1])));
|
|
9
|
+
const add = (list) => { for (const n of list.split(",")) if (n.trim()) widths.add(Number(n)); };
|
|
10
|
+
for (const m of code.matchAll(/for\s*\(\s*(?:const|let|var)\s+\w+\s+of\s+\[([\d\s,]+)\]/g)) add(m[1]);
|
|
11
|
+
// A named list counts only when it is iterated; picking `VIEWPORTS[0]` renders one width, not all of them.
|
|
12
|
+
for (const m of code.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*\[([\d\s,]+)\]/g)) if (new RegExp(`(of\\s+${m[1]}\\b|\\b${m[1]}\\s*\\.\\s*(forEach|map)\\s*\\()`).test(code)) add(m[2]);
|
|
13
|
+
return widths;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Whether some expect() takes scrollWidth, directly or through a variable measured from it. */
|
|
17
|
+
function assertsOverflow(code) {
|
|
18
|
+
const measured = [...code.matchAll(/(?:const|let|var)\s+(\w+)\s*=[^;]*scrollWidth/g)].map((m) => m[1]);
|
|
19
|
+
return [...code.matchAll(/expect\s*\(([^;]*?)\)\s*\./g)].some((m) => /scrollWidth/.test(m[1]) || measured.some((name) => new RegExp(`\\b${name}\\b`).test(m[1])));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await runCheck("responsive-harness", (root, specs, plan, report) => {
|
|
23
|
+
const { viewports } = frontendConfig(root);
|
|
24
|
+
for (const s of specs) {
|
|
25
|
+
if (!isJourneySpec(s) || !s.harnesses.has("RESP")) continue;
|
|
26
|
+
const code = maskLiteralsAndComments(s.text), widths = renderedWidths(code), missing = viewports.filter((w) => !widths.has(w));
|
|
27
|
+
if (missing.length) report("tester.htmx.responsive-harness", s.file, 1, `RESP spec never renders at viewport width(s) ${missing.join(", ")} of the declared ${viewports.join("/")}`);
|
|
28
|
+
if (!assertsOverflow(code)) report("tester.htmx.responsive-harness", s.file, 1, "RESP spec never asserts on scrollWidth; it must fail when the page overflows the viewport horizontally");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.responsive-journey-coverage: every exposed journey has a RESP browser spec bound to it.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("responsive-journey-coverage", (root, specs, plan, report) => {
|
|
5
|
+
const covered = new Set(specs.filter((s) => isJourneySpec(s) && s.binding?.kind === "journey" && s.harnesses.has("RESP")).map((s) => s.binding.id));
|
|
6
|
+
for (const j of plan.journeys) if (j.exposed && j.frontend && !covered.has(j.id)) report("tester.htmx.responsive-journey-coverage", j.path, 1, `exposed journey ${j.id} has no RESP browser spec bound by \`// Journey: ${j.id}\`; nothing proves its screens work from mobile to desktop`);
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.train-e2e-coverage: every human-facing train (a `user:` takes part) has at least one browser spec bound to it.
|
|
3
|
+
import { runCheck, isJourneySpec, TRAIN_ID, JOURNEY_ID, firstLine, lineOf, frontendConfig } from "./_e2e.mjs";
|
|
4
|
+
await runCheck("train-e2e-coverage", (root, specs, plan, report) => {
|
|
5
|
+
const covered = new Set(specs.filter((s) => isJourneySpec(s) && s.binding?.kind === "train").map((s) => s.binding.id));
|
|
6
|
+
for (const train of [...plan.humanTrains].sort()) if (!covered.has(train)) report("tester.htmx.train-e2e-coverage", plan.trainFiles.get(train), 1, `${train} has no browser spec bound by \`// Train: ${train}\`; the journey it declares can regress with nothing turning red`);
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// tester.htmx.visual-harness: a VIS spec compares a screenshot.
|
|
3
|
+
import { maskLiteralsAndComments } from "../../../lib/scan.mjs";
|
|
4
|
+
import { runCheck, isJourneySpec } from "./_e2e.mjs";
|
|
5
|
+
await runCheck("visual-harness", (root, specs, plan, report) => {
|
|
6
|
+
for (const s of specs) if (isJourneySpec(s) && s.harnesses.has("VIS") && !/\.(toHaveScreenshot|toMatchSnapshot)\s*\(/.test(maskLiteralsAndComments(s.text))) report("tester.htmx.visual-harness", s.file, 1, "VIS spec never calls toHaveScreenshot() or toMatchSnapshot(); a visual spec that compares no screenshot performs no visual regression");
|
|
7
|
+
});
|