@3fn/core 13.0.0 → 14.0.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/.kiro/agents/ada-prompt.md +80 -132
- package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
- package/.kiro/agents/ada.json +44 -59
- package/.kiro/agents/ada.json.attribution.json +13 -0
- package/.kiro/agents/data-prompt.md +83 -74
- package/.kiro/agents/data-prompt.md.attribution.json +53 -0
- package/.kiro/agents/data.json +31 -38
- package/.kiro/agents/data.json.attribution.json +13 -0
- package/.kiro/agents/kenya-prompt.md +83 -72
- package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
- package/.kiro/agents/kenya.json +27 -35
- package/.kiro/agents/kenya.json.attribution.json +13 -0
- package/.kiro/agents/leonardo-prompt.md +176 -234
- package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
- package/.kiro/agents/leonardo.json +28 -36
- package/.kiro/agents/leonardo.json.attribution.json +13 -0
- package/.kiro/agents/lina-prompt.md +110 -151
- package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
- package/.kiro/agents/lina.json +46 -59
- package/.kiro/agents/lina.json.attribution.json +13 -0
- package/.kiro/agents/sparky-prompt.md +89 -72
- package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
- package/.kiro/agents/sparky.json +37 -37
- package/.kiro/agents/sparky.json.attribution.json +13 -0
- package/.kiro/agents/stacy-prompt.md +74 -48
- package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
- package/.kiro/agents/stacy.json +28 -30
- package/.kiro/agents/stacy.json.attribution.json +13 -0
- package/.kiro/agents/thurgood-prompt.md +94 -138
- package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
- package/.kiro/agents/thurgood.json +31 -35
- package/.kiro/agents/thurgood.json.attribution.json +13 -0
- package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
- package/.kiro/steering/Civitas-System-Overview.md +5 -5
- package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
- package/.kiro/steering/Task-Completion-Protocol.md +98 -17
- package/.kiro/steering/core-goals.md +3 -3
- package/.kiro/steering/personal-note.md +1 -1
- package/.kiro/steering/start-up-tasks.md +14 -3
- package/application-mcp-server/src/index.ts +26 -0
- package/dist/ComponentTokens.android.kt +1 -1
- package/dist/ComponentTokens.ios.swift +1 -1
- package/dist/ComponentTokens.web.css +1 -1
- package/dist/DesignTokens.android.kt +1 -1
- package/dist/DesignTokens.dtcg.json +8 -5
- package/dist/DesignTokens.figma.json +2 -2
- package/dist/DesignTokens.ios.swift +1 -1
- package/dist/DesignTokens.web.css +1 -1
- package/dist/android/DesignTokens.android.kt +1 -1
- package/dist/blend/OklchBlendCalculator.js +1 -0
- package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
- package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
- package/dist/browser/designerpunk.esm.js +22 -81
- package/dist/browser/designerpunk.esm.min.js +29 -32
- package/dist/browser/designerpunk.umd.js +22 -81
- package/dist/browser/designerpunk.umd.min.js +42 -45
- package/dist/browser/tokens.css +1 -1
- package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
- package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
- package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
- package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
- package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
- package/dist/components/core/Button-CTA/types.d.ts +0 -24
- package/dist/components/core/Button-CTA/types.js +6 -0
- package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
- package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
- package/dist/generators/DTCGFormatGenerator.js +8 -0
- package/dist/integration/BuildErrorHandler.js +2 -2
- package/dist/ios/DesignTokens.ios.swift +1 -1
- package/dist/mcp/application-mcp.js +24 -0
- package/dist/mcp/docs-mcp.js +130 -15
- package/dist/mcp/product-mcp.js +25 -0
- package/dist/tokens/OpacityTokens.js +1 -1
- package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
- package/dist/tokens/semantic/BlendTokens.js +17 -5
- package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
- package/dist/tokens/semantic/OpacityTokens.js +4 -4
- package/dist/types/ComponentTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.js +1 -1
- package/dist/validators/StemmaTokenUsageValidator.js +3 -2
- package/dist/web/DesignTokens.web.css +1 -1
- package/governance/Component-Development-Guide.md +22 -12
- package/governance/Component-Development-Standards.md +2 -2
- package/governance/Component-Family-Avatar.md +0 -1
- package/governance/Component-Family-Badge.md +0 -1
- package/governance/Component-Family-Button.md +4 -17
- package/governance/Component-Family-Chip.md +0 -1
- package/governance/Component-Family-Container.md +0 -1
- package/governance/Component-Family-Data-Display.md +1 -2
- package/governance/Component-Family-Divider.md +1 -2
- package/governance/Component-Family-Form-Inputs.md +5 -5
- package/governance/Component-Family-Icon.md +1 -2
- package/governance/Component-Family-Loading.md +1 -2
- package/governance/Component-Family-Modal.md +1 -2
- package/governance/Component-Family-Navigation.md +0 -1
- package/governance/Component-Family-Progress.md +0 -1
- package/governance/Component-Inheritance-Structures.md +195 -76
- package/governance/Component-MCP-Document-Template.md +6 -5
- package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
- package/governance/Component-Quick-Reference.md +33 -33
- package/governance/Component-Readiness-Status.md +54 -38
- package/governance/Component-Templates.md +32 -36
- package/governance/Contract-System-Reference.md +6 -6
- package/governance/MCP-Integration-Guide.md +1 -1
- package/governance/Process-Cross-Reference-Standards.md +31 -13
- package/governance/Process-Development-Workflow.md +49 -59
- package/governance/Process-File-Organization.md +24 -24
- package/governance/Process-Hook-Operations.md +19 -7
- package/governance/Process-Orchestration-Model-Selection.md +92 -0
- package/governance/Process-Spec-Planning.md +91 -39
- package/governance/Process-Task-Type-Definitions.md +80 -4
- package/governance/Product-Handoff-Protocol.md +2 -0
- package/governance/Rosetta-System-Architecture.md +6 -6
- package/governance/Test-Behavioral-Contract-Validation.md +38 -31
- package/governance/Test-Failure-Audit-Methodology.md +1 -1
- package/governance/Token-Family-Accessibility.md +1 -2
- package/governance/Token-Family-Blend.md +18 -16
- package/governance/Token-Family-Blur.md +0 -1
- package/governance/Token-Family-Border.md +1 -2
- package/governance/Token-Family-Color.md +0 -1
- package/governance/Token-Family-Glow.md +1 -2
- package/governance/Token-Family-Layering.md +0 -1
- package/governance/Token-Family-Motion.md +1 -2
- package/governance/Token-Family-Opacity.md +0 -1
- package/governance/Token-Family-Radius.md +1 -2
- package/governance/Token-Family-Responsive.md +1 -2
- package/governance/Token-Family-Shadow.md +1 -2
- package/governance/Token-Family-Sizing.md +0 -1
- package/governance/Token-Family-Spacing.md +1 -2
- package/governance/Token-Family-Typography.md +1 -2
- package/governance/Token-Governance.md +8 -8
- package/governance/Token-Quick-Reference.md +21 -21
- package/governance/Token-Resolution-Patterns.md +1 -1
- package/governance/Token-Semantic-Structure.md +1 -1
- package/governance/Web-Authoring-Standards.md +5 -5
- package/governance/browser-distribution-guide.md +1 -4
- package/governance/classification-map.md +368 -0
- package/governance/completion-documentation-guide.md +11 -8
- package/governance/component-meta-authoring-guide.md +1 -1
- package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
- package/governance/platform-implementation-guidelines.md +1 -1
- package/governance/release-management-system.md +2 -2
- package/governance/rosetta-system-principles.md +8 -6
- package/governance/stemma-system-principles.md +18 -17
- package/mcp-server/src/index.ts +24 -6
- package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
- package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
- package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
- package/mcp-server/src/indexer/index-health.ts +27 -2
- package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
- package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
- package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
- package/mcp-server/src/tools/list-cross-references.ts +2 -2
- package/package.json +23 -21
- package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
- package/src/__tests__/console-allowlist.json +14 -0
- package/src/__tests__/console-fail-setup.ts +169 -0
- package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
- package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
- package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
- package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
- package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
- package/src/blend/OklchBlendCalculator.ts +3 -0
- package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
- package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
- package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
- package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
- package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
- package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
- package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
- package/src/cli/__tests__/init.test.ts +45 -11
- package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
- package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
- package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
- package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
- package/src/components/core/Avatar-Base/contracts.yaml +11 -1
- package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
- package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
- package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
- package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
- package/src/components/core/Button-CTA/README.md +3 -6
- package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
- package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
- package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
- package/src/components/core/Button-CTA/contracts.yaml +6 -29
- package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
- package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
- package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
- package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
- package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
- package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
- package/src/components/core/Button-CTA/types.ts +10 -28
- package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
- package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
- package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
- package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
- package/src/components/core/Input-Text-Base/README.md +25 -2
- package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
- package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
- package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
- package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
- package/src/components/core/Input-Text-Base/types.ts +13 -1
- package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
- package/src/components/core/Input-Text-Email/README.md +8 -7
- package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
- package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
- package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
- package/src/components/core/Input-Text-Password/README.md +9 -8
- package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
- package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
- package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
- package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
- package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
- package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
- package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
- package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
- package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
- package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
- package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
- package/src/generators/DTCGFormatGenerator.ts +6 -0
- package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
- package/src/integration/BuildErrorHandler.ts +2 -2
- package/src/tokens/OpacityTokens.ts +1 -1
- package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
- package/src/tokens/semantic/BlendTokens.ts +26 -5
- package/src/tokens/semantic/OpacityTokens.ts +4 -4
- package/src/tools/release/__tests__/ReleasePipeline.test.ts +1 -1
- package/src/types/ComponentTypes.ts +1 -1
- package/src/types/generated/TokenTypes.ts +1 -1
- package/src/validators/StemmaTokenUsageValidator.ts +3 -2
- package/token-index/semantics.yaml +1 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console-fail hook (125-B, Requirement 11 / Design C8).
|
|
3
|
+
*
|
|
4
|
+
* Captures console.error/console.warn per test and FAILS the test when
|
|
5
|
+
* unallowlisted output is produced. Checked against the checked-in
|
|
6
|
+
* allowlist (`src/__tests__/console-allowlist.json`, a (suite × message-pattern)
|
|
7
|
+
* grain — see that file for entries + reasons, and
|
|
8
|
+
* governance/classification-map.md § "console-fail-root-lanes" for the rule
|
|
9
|
+
* this hook enforces).
|
|
10
|
+
*
|
|
11
|
+
* SCOPE — root functional lanes ONLY (wired via the root jest.config.js's
|
|
12
|
+
* `setupFilesAfterEnv`). Sub-package suites (mcp-server, application-mcp-server
|
|
13
|
+
* — own jest 29 configs outside this repo's root `roots`) are NOT covered by
|
|
14
|
+
* this hook; see governance/classification-map.md
|
|
15
|
+
* § "console-fail-subpackage-deferred" for the recorded deferral.
|
|
16
|
+
* `product-mcp-server/src/**` IS covered — it lives inside root `roots`.
|
|
17
|
+
*
|
|
18
|
+
* VERSION-AGNOSTIC BY DESIGN (125-B C8 constraint, recorded for any future
|
|
19
|
+
* sub-package replication): plain `jest.spyOn` / method-swap capture only —
|
|
20
|
+
* no jest-30-only API. Stable on jest 29 and jest 30.
|
|
21
|
+
*
|
|
22
|
+
* Known limitation (recorded, not solved here): suites that install their
|
|
23
|
+
* OWN console.error/warn spy on top of this hook's (26 files predate this
|
|
24
|
+
* hook — "house style", nested `describe`-level `beforeEach`, which runs
|
|
25
|
+
* AFTER this hook's root-level `beforeEach`) shadow this hook for the
|
|
26
|
+
* duration of their own mock. Output fully swallowed by a local mock's
|
|
27
|
+
* `mockImplementation(() => {})` never reaches this hook (nothing to check
|
|
28
|
+
* — correct no-op). Output a local mock passes through (e.g. Chip's
|
|
29
|
+
* "anything else passes through" pattern) DOES reach this hook via the
|
|
30
|
+
* bound reference the local suite captured. Because this hook is a plain
|
|
31
|
+
* property swap rather than a `jest.spyOn` mock (see below), a local
|
|
32
|
+
* suite's own `jest.restoreAllMocks()` restores only ITS OWN jest-tracked
|
|
33
|
+
* spy — never this hook's swap, which isn't in Jest's mock registry at all.
|
|
34
|
+
* The residual edge case is narrower than a spyOn-based hook would have:
|
|
35
|
+
* async/deferred output (e.g. an rAF-queued warn) that fires after this
|
|
36
|
+
* test's `afterEach` has already restored `console.error`/`console.warn`
|
|
37
|
+
* back to whatever was there before this test's `beforeEach` — a normal,
|
|
38
|
+
* expected end-of-test boundary, not a cross-hook interaction — can land on
|
|
39
|
+
* the real console unrecorded and unchecked. It does not produce false
|
|
40
|
+
* failures.
|
|
41
|
+
*
|
|
42
|
+
* Chain-through, not shadow (fixed during 4.4's own gate-bite run): the
|
|
43
|
+
* "before" console.error/warn reference is captured FRESH inside
|
|
44
|
+
* `beforeEach`, immediately before this hook installs itself — not once at
|
|
45
|
+
* module load. A handful of CLI suites (figma-extract.test.ts,
|
|
46
|
+
* figma-push.test.ts) install a PERSISTENT module-top-level console mock of
|
|
47
|
+
* their own (no per-test beforeEach/afterEach — restored once in `afterAll`,
|
|
48
|
+
* if at all) and assert against its `.mock.calls`. Capturing "before" once
|
|
49
|
+
* at this module's own load time would have bound to the pristine native
|
|
50
|
+
* console — permanently bypassing those suites' own mocks for every test
|
|
51
|
+
* and silently breaking their `consoleOutput()`-style assertions.
|
|
52
|
+
*
|
|
53
|
+
* PLAIN PROPERTY SWAP, NOT `jest.spyOn` (also fixed during 4.4's gate-bite
|
|
54
|
+
* run — the reason this hook does NOT use jest.spyOn even though the header
|
|
55
|
+
* above still calls the general technique "spyOn/method swap"): `jest.spyOn`
|
|
56
|
+
* has a documented reuse rule — spying on a property that is ALREADY a Jest
|
|
57
|
+
* mock function returns and mutates THAT SAME mock object rather than
|
|
58
|
+
* wrapping a new one. Against the figma-extract/figma-push suites'
|
|
59
|
+
* persistent console.error mock, that reuse rule made `jest.spyOn(console,
|
|
60
|
+
* 'error').mockImplementation(fn)` overwrite THEIR mock's implementation
|
|
61
|
+
* with `fn` — and this hook's own "before" reference, bound to that same
|
|
62
|
+
* mutable mock object, then called back into `fn` on every invocation:
|
|
63
|
+
* infinite recursion, observed as a stack overflow on the very first
|
|
64
|
+
* `console.error` call in a chained suite. A plain property assignment
|
|
65
|
+
* (`console.error = wrapper; ...; console.error = original;`) sidesteps
|
|
66
|
+
* Jest's mock-identity bookkeeping entirely — the same technique the header
|
|
67
|
+
* above documents as version-agnostic ("plain jest.spyOn / method swap"),
|
|
68
|
+
* using the method-swap half specifically because it composes safely with
|
|
69
|
+
* another suite's pre-existing mock, which spyOn's reuse rule does not.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
import allowlistData from './console-allowlist.json';
|
|
73
|
+
import * as path from 'path';
|
|
74
|
+
|
|
75
|
+
interface AllowlistEntry {
|
|
76
|
+
suite: string;
|
|
77
|
+
pattern: string;
|
|
78
|
+
reason: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const allowlist = allowlistData as AllowlistEntry[];
|
|
82
|
+
|
|
83
|
+
function isAllowed(suite: string, message: string): boolean {
|
|
84
|
+
return allowlist.some((entry) => {
|
|
85
|
+
if (entry.suite !== suite) return false;
|
|
86
|
+
try {
|
|
87
|
+
return new RegExp(entry.pattern).test(message);
|
|
88
|
+
} catch {
|
|
89
|
+
// A malformed pattern never silently passes output through.
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stringifyArg(value: unknown): string {
|
|
96
|
+
if (value instanceof Error) return value.stack || value.message;
|
|
97
|
+
if (typeof value === 'string') return value;
|
|
98
|
+
try {
|
|
99
|
+
return JSON.stringify(value);
|
|
100
|
+
} catch {
|
|
101
|
+
return String(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatArgs(args: unknown[]): string {
|
|
106
|
+
return args.map(stringifyArg).join(' ');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function currentSuitePath(): string {
|
|
110
|
+
// expect.getState().testPath is the absolute path to the currently
|
|
111
|
+
// running test file. Repo root is resolved relative to THIS file
|
|
112
|
+
// (src/__tests__/), so it's correct regardless of process cwd.
|
|
113
|
+
const testPath = expect.getState().testPath;
|
|
114
|
+
if (!testPath) return '';
|
|
115
|
+
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
116
|
+
return path.relative(repoRoot, testPath).split(path.sep).join('/');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface CapturedOutput {
|
|
120
|
+
method: 'error' | 'warn';
|
|
121
|
+
message: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let captured: CapturedOutput[] = [];
|
|
125
|
+
let originalError: typeof console.error | undefined;
|
|
126
|
+
let originalWarn: typeof console.warn | undefined;
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
captured = [];
|
|
130
|
+
|
|
131
|
+
// Capture whatever is CURRENTLY installed (native console, or another
|
|
132
|
+
// suite's own persistent mock) immediately before swapping — see the
|
|
133
|
+
// "Chain-through, not shadow" / "Plain property swap" header notes.
|
|
134
|
+
originalError = console.error;
|
|
135
|
+
originalWarn = console.warn;
|
|
136
|
+
|
|
137
|
+
console.error = (...args: unknown[]) => {
|
|
138
|
+
captured.push({ method: 'error', message: formatArgs(args) });
|
|
139
|
+
originalError!.apply(console, args);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
console.warn = (...args: unknown[]) => {
|
|
143
|
+
captured.push({ method: 'warn', message: formatArgs(args) });
|
|
144
|
+
originalWarn!.apply(console, args);
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
const suite = currentSuitePath();
|
|
150
|
+
|
|
151
|
+
// Targeted restore only — put back exactly what was there before this
|
|
152
|
+
// test's beforeEach ran (native console, or another suite's own mock).
|
|
153
|
+
if (originalError) console.error = originalError;
|
|
154
|
+
if (originalWarn) console.warn = originalWarn;
|
|
155
|
+
|
|
156
|
+
const unallowed = captured.filter((entry) => !isAllowed(suite, entry.message));
|
|
157
|
+
captured = [];
|
|
158
|
+
|
|
159
|
+
if (unallowed.length > 0) {
|
|
160
|
+
const details = unallowed
|
|
161
|
+
.map((entry) => ` [console.${entry.method}] ${entry.message}`)
|
|
162
|
+
.join('\n');
|
|
163
|
+
throw new Error(
|
|
164
|
+
`console-fail: unallowlisted console output in ${suite}:\n${details}\n\n` +
|
|
165
|
+
'Legitimate expected noise: add a { suite, pattern, reason } entry to ' +
|
|
166
|
+
'src/__tests__/console-allowlist.json. Genuine defect: fix the source.'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
@@ -16,6 +16,22 @@ const PHILOSOPHY_PATH = path.resolve(__dirname, '../../../design-language/design
|
|
|
16
16
|
const PRODUCT_FIXTURES = path.resolve(__dirname, '../../../product-mcp-server/src/__tests__/fixtures');
|
|
17
17
|
|
|
18
18
|
describe('Spec 107 Integration: Design Language Context', () => {
|
|
19
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
// ProductIndexer logs indexing summaries via console.error (informational, not a failure).
|
|
23
|
+
// Installed in beforeAll (not beforeEach) so it's active before nested beforeAll hooks
|
|
24
|
+
// that construct/index a ProductIndexer run.
|
|
25
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
consoleErrorSpy.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
consoleErrorSpy.mockClear();
|
|
34
|
+
});
|
|
19
35
|
|
|
20
36
|
describe('Application MCP — Design Philosophy', () => {
|
|
21
37
|
let indexer: any;
|
|
@@ -323,33 +323,83 @@ describe('Behavioral Contract Validation Suite', () => {
|
|
|
323
323
|
});
|
|
324
324
|
|
|
325
325
|
describe('WCAG Reference Validation', () => {
|
|
326
|
+
/**
|
|
327
|
+
* NORMATIVE WCAG-REQUIRED MATCHER — Spec 125-B U2, Task 4.2 (audit) → 4.3 (arm).
|
|
328
|
+
*
|
|
329
|
+
* Copied VERBATIM from the Task 4.2 audit matcher:
|
|
330
|
+
* .kiro/specs/125-B-classification-map/completion/u2/wcag-required-matcher.ts
|
|
331
|
+
*
|
|
332
|
+
* CONTINUITY CONTRACT (Req 12.3 / LINA tasks-R1): Task 4.2's pre-arm WCAG audit
|
|
333
|
+
* enumerated the corpus THROUGH this exact function (and the same COMPONENTS
|
|
334
|
+
* loader below), so "audit-clean ⇒ arm-green by construction" holds ONLY while
|
|
335
|
+
* this predicate stays unmodified. ANY change to it RE-OPENS the 4.2 audit —
|
|
336
|
+
* do not "improve" it here.
|
|
337
|
+
*
|
|
338
|
+
* Replaces the legacy DORMANT six-name trigger (armed-but-aimed-at-6-retired
|
|
339
|
+
* legacy contract names — Spec 063 renamed them to canonical
|
|
340
|
+
* `{category}_{concept}` form; this check never matched the renamed names).
|
|
341
|
+
*/
|
|
342
|
+
const WCAG_REQUIRED_EXACT: ReadonlySet<string> = new Set([
|
|
343
|
+
'interaction_focusable',
|
|
344
|
+
'interaction_focus_ring',
|
|
345
|
+
'state_disabled',
|
|
346
|
+
'state_error',
|
|
347
|
+
]);
|
|
348
|
+
|
|
349
|
+
function isWcagRequiredContract(contractName: string): boolean {
|
|
350
|
+
if (WCAG_REQUIRED_EXACT.has(contractName)) return true;
|
|
351
|
+
if (contractName.startsWith('accessibility_')) return true;
|
|
352
|
+
if (contractName.startsWith('content_') && contractName.endsWith('_label')) return true;
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
326
356
|
it('accessibility-related contracts should have WCAG references', () => {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
357
|
+
// Match-count floor (Req 12.4 / DD3): the DORMANT lesson made structural —
|
|
358
|
+
// an empty (or stale) selection fails the check itself.
|
|
359
|
+
let totalSelected = 0;
|
|
360
|
+
const perLiteralCounts: Record<string, number> = {
|
|
361
|
+
interaction_focusable: 0,
|
|
362
|
+
interaction_focus_ring: 0,
|
|
363
|
+
state_error: 0,
|
|
364
|
+
};
|
|
365
|
+
|
|
336
366
|
for (const component of COMPONENTS) {
|
|
337
367
|
const contracts = loadComponentContracts(component);
|
|
338
|
-
|
|
368
|
+
|
|
339
369
|
if (!contracts) continue;
|
|
340
|
-
|
|
370
|
+
|
|
341
371
|
for (const [contractName, contract] of Object.entries(contracts)) {
|
|
342
|
-
if (
|
|
372
|
+
if (isWcagRequiredContract(contractName)) {
|
|
373
|
+
totalSelected++;
|
|
374
|
+
if (contractName in perLiteralCounts) {
|
|
375
|
+
perLiteralCounts[contractName]++;
|
|
376
|
+
}
|
|
377
|
+
|
|
343
378
|
const hasWcag = contract.wcag && contract.wcag.length > 0;
|
|
344
|
-
|
|
379
|
+
|
|
345
380
|
if (!hasWcag) {
|
|
346
381
|
console.log(`${component}.${contractName}: Missing WCAG reference`);
|
|
347
382
|
}
|
|
348
|
-
|
|
383
|
+
|
|
349
384
|
expect(hasWcag).toBe(true);
|
|
350
385
|
}
|
|
351
386
|
}
|
|
352
387
|
}
|
|
388
|
+
|
|
389
|
+
// Aggregate floor (DD3): non-empty selection (audited at 69 on the current
|
|
390
|
+
// corpus — Task 4.2 adjudication table).
|
|
391
|
+
expect(totalSelected).toBeGreaterThan(0);
|
|
392
|
+
|
|
393
|
+
// Per-literal presence floor (DD3), THREE literals per Peter's 2026-07-14
|
|
394
|
+
// amendment — NOT the four DD3 originally recorded. `state_disabled` is
|
|
395
|
+
// EXCLUDED from this floor pending the Button-CTA disabled-state
|
|
396
|
+
// adjudication (the matcher itself is unchanged: state_disabled contracts
|
|
397
|
+
// are still selected above and still must carry a valid wcag ref; only the
|
|
398
|
+
// per-literal PRESENCE floor omits it). See:
|
|
399
|
+
// .kiro/specs/125-B-classification-map/completion/u2/stemma-pre-arm-adjudication.md § 7
|
|
400
|
+
expect(perLiteralCounts.interaction_focusable).toBeGreaterThan(0);
|
|
401
|
+
expect(perLiteralCounts.interaction_focus_ring).toBeGreaterThan(0);
|
|
402
|
+
expect(perLiteralCounts.state_error).toBeGreaterThan(0);
|
|
353
403
|
});
|
|
354
404
|
|
|
355
405
|
it('WCAG references should follow standard format', () => {
|
|
@@ -430,9 +480,12 @@ describe('Behavioral Contract Validation Suite', () => {
|
|
|
430
480
|
console.log(`\nValidation Criteria Summary:`);
|
|
431
481
|
console.log(` With validation: ${contractsWithValidation}`);
|
|
432
482
|
console.log(` Without validation: ${contractsWithoutValidation}`);
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
483
|
+
|
|
484
|
+
// Promoted (Req 12.6 / DD4, audit-first — Task 4.2's inventory found the
|
|
485
|
+
// corpus already clean: 234 non-inherited contracts, 0 without validation;
|
|
486
|
+
// zero fixes, zero escalations needed). Inherited-contract skip preserved
|
|
487
|
+
// above. DD4: no exemption mechanism — fix-all, escalate-if-candidate.
|
|
488
|
+
expect(contractsWithoutValidation).toBe(0);
|
|
436
489
|
});
|
|
437
490
|
|
|
438
491
|
it('validation criteria should include testable assertions', () => {
|
|
@@ -26,6 +26,19 @@ const CATALOG_PATH = path.join(process.cwd(), 'governance/Contract-System-Refere
|
|
|
26
26
|
const BASELINE_CONCEPT_COUNT = 116;
|
|
27
27
|
const EXPECTED_CATEGORY_COUNT = 10;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Contracts authored ahead of Concept Catalog ratification (ballot pending).
|
|
31
|
+
*
|
|
32
|
+
* Each entry MUST reference the ruling that authorized authoring the contract
|
|
33
|
+
* before the concept ballot ratified. This allowlist is SELF-CLEANING: once
|
|
34
|
+
* the concept lands in the catalog (ballot RATIFIED), the validation below
|
|
35
|
+
* fails with a removal instruction — a stale entry cannot linger.
|
|
36
|
+
*/
|
|
37
|
+
const PENDING_BALLOT_CONCEPTS: Record<string, string> = {
|
|
38
|
+
// (empty — state_readonly ratified and landed in the catalog, 2026-07-15;
|
|
39
|
+
// add entries here only for contracts whose concept ballot is still pending)
|
|
40
|
+
};
|
|
41
|
+
|
|
29
42
|
/**
|
|
30
43
|
* Parse the Concept Catalog from Contract-System-Reference.md.
|
|
31
44
|
*
|
|
@@ -145,6 +158,21 @@ describe('Contract Catalog Name Validation', () => {
|
|
|
145
158
|
);
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
// Pending-ballot carve-out (self-cleaning): a contract authored ahead
|
|
162
|
+
// of catalog ratification passes only while the concept is absent
|
|
163
|
+
// from the catalog. The moment the ballot ratifies and the concept
|
|
164
|
+
// lands in the catalog, the stale allowlist entry fails the test.
|
|
165
|
+
if (contractKey in PENDING_BALLOT_CONCEPTS) {
|
|
166
|
+
if (catalogCategory.has(concept)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Contract '${contractKey}' is now in the Concept Catalog — its ballot has ` +
|
|
169
|
+
`ratified. Remove it from PENDING_BALLOT_CONCEPTS. ` +
|
|
170
|
+
`(Entry: ${PENDING_BALLOT_CONCEPTS[contractKey]})`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return; // valid category + documented pending ballot
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
if (!catalogCategory.has(concept)) {
|
|
149
177
|
throw new Error(
|
|
150
178
|
`Component ${componentName}: contract '${contractKey}' has unrecognized concept '${concept}' in category '${category}'`
|
|
@@ -93,22 +93,8 @@ const CONTRACT_PATTERNS: Record<string, Record<string, RegExp[]>> = {
|
|
|
93
93
|
/errorMessage/g,
|
|
94
94
|
],
|
|
95
95
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/disabled/gi,
|
|
99
|
-
/aria-disabled/g,
|
|
100
|
-
/cursor:\s*not-allowed/g,
|
|
101
|
-
],
|
|
102
|
-
ios: [
|
|
103
|
-
/disabled/gi,
|
|
104
|
-
/isDisabled/g,
|
|
105
|
-
],
|
|
106
|
-
android: [
|
|
107
|
-
/enabled/gi,
|
|
108
|
-
/isEnabled/g,
|
|
109
|
-
/disabled/gi,
|
|
110
|
-
],
|
|
111
|
-
},
|
|
96
|
+
// NOTE: disabled_state is a standardized EXCLUSION (no-disabled-states philosophy).
|
|
97
|
+
// Implementation-level guard patterns live in DISABLED_EXCLUSION_GUARD_PATTERNS below.
|
|
112
98
|
focus_ring: {
|
|
113
99
|
web: [
|
|
114
100
|
/:focus-visible/g,
|
|
@@ -141,6 +127,22 @@ const CONTRACT_PATTERNS: Record<string, Record<string, RegExp[]>> = {
|
|
|
141
127
|
},
|
|
142
128
|
};
|
|
143
129
|
|
|
130
|
+
// Disabled-state exclusion guard (no-disabled-states philosophy, Spec 066 / Button-CTA pattern).
|
|
131
|
+
// These patterns must NOT appear in any Form Inputs platform implementation.
|
|
132
|
+
// The iOS `.disabled(` pattern was added per the iOS readOnly adjudication
|
|
133
|
+
// (.kiro/issues/input-text-base-ios-readonly-adjudication.md — RULED B-prime,
|
|
134
|
+
// Peter 2026-07-15, condition 5): iOS previously implemented readOnly via
|
|
135
|
+
// SwiftUI .disabled(readOnly), which the original patterns missed — that is
|
|
136
|
+
// how a "dimmed" readOnly field survived two disabled-cleanup passes. There
|
|
137
|
+
// are zero legitimate `.disabled(` uses in Form Inputs platform sources.
|
|
138
|
+
const DISABLED_EXCLUSION_GUARD_PATTERNS: RegExp[] = [
|
|
139
|
+
/isDisabled/,
|
|
140
|
+
/disabledBlend/,
|
|
141
|
+
/aria-disabled/,
|
|
142
|
+
/cursor:\s*not-allowed/,
|
|
143
|
+
/\.disabled\(/, // SwiftUI disabled modifier — disabled semantics regardless of intent
|
|
144
|
+
];
|
|
145
|
+
|
|
144
146
|
/**
|
|
145
147
|
* Load component schema
|
|
146
148
|
*/
|
|
@@ -371,6 +373,211 @@ describe('Form Inputs Family Behavioral Contracts', () => {
|
|
|
371
373
|
expect(content).toContain('state_disabled');
|
|
372
374
|
expect(content).toContain('excludes');
|
|
373
375
|
});
|
|
376
|
+
|
|
377
|
+
it('no platform implementation should contain disabled-state handling', () => {
|
|
378
|
+
// Exclusion guard (Spec 066 / Button-CTA pattern): the contracts.yaml
|
|
379
|
+
// exclusion must hold in code. readOnly is the documented alternative
|
|
380
|
+
// and must never be implemented via disabled semantics — iOS renders
|
|
381
|
+
// readOnly as selectable Text in the field chrome (B-prime ruling,
|
|
382
|
+
// 2026-07-15), never via .disabled(readOnly).
|
|
383
|
+
for (const formComponent of FORM_INPUT_COMPONENTS) {
|
|
384
|
+
for (const platform of ['web', 'ios', 'android']) {
|
|
385
|
+
const content = loadPlatformImpl(formComponent, platform);
|
|
386
|
+
if (!content) continue;
|
|
387
|
+
|
|
388
|
+
// Raw-source matching (strict): banned patterns may not appear
|
|
389
|
+
// anywhere in a platform implementation, comments included —
|
|
390
|
+
// reference the mechanism descriptively in docs, never literally.
|
|
391
|
+
const violations = DISABLED_EXCLUSION_GUARD_PATTERNS
|
|
392
|
+
.filter(pattern => pattern.test(content))
|
|
393
|
+
.map(pattern => pattern.source);
|
|
394
|
+
|
|
395
|
+
if (violations.length > 0) {
|
|
396
|
+
console.log(`${formComponent} (${platform}) disabled-state handling found: ${violations.join(', ')}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
expect(violations).toEqual([]);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('state_readonly contract (iOS readOnly adjudication, RULED B-prime 2026-07-15)', () => {
|
|
406
|
+
// @see .kiro/issues/input-text-base-ios-readonly-adjudication.md
|
|
407
|
+
// These are the first executable readOnly assertions anywhere in the
|
|
408
|
+
// component (pre-ruling coverage was zero on all platforms).
|
|
409
|
+
|
|
410
|
+
it('Input-Text-Base contracts.yaml declares state_readonly', () => {
|
|
411
|
+
const contracts = loadContracts('Input-Text-Base');
|
|
412
|
+
expect(contracts).not.toBeNull();
|
|
413
|
+
expect(contracts!.state_readonly).toBeDefined();
|
|
414
|
+
expect(contracts!.state_readonly.category).toBe('state');
|
|
415
|
+
expect(contracts!.state_readonly.required).toBe(true);
|
|
416
|
+
expect(contracts!.state_readonly.platforms).toEqual(['web', 'ios', 'android']);
|
|
417
|
+
// The contract must pin "never disabled semantics" — the invariant
|
|
418
|
+
// the original implementation violated.
|
|
419
|
+
expect(contracts!.state_readonly.behavior).toMatch(/disabled/i);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('Input-Text-Base interaction_focusable declares the iOS readOnly carve-out', () => {
|
|
423
|
+
const contractsPath = path.join(COMPONENTS_DIR, 'Input-Text-Base', 'contracts.yaml');
|
|
424
|
+
const content = fs.readFileSync(contractsPath, 'utf-8');
|
|
425
|
+
const parsed = yaml.load(content) as Record<string, any>;
|
|
426
|
+
const focusable = parsed.contracts.interaction_focusable;
|
|
427
|
+
|
|
428
|
+
// Declared, never silent: iOS carve-out + affirmative web/Android
|
|
429
|
+
// focus-order participation (adjudication condition 1).
|
|
430
|
+
expect(focusable.behavior).toMatch(/iOS readOnly carve-out/i);
|
|
431
|
+
expect(focusable.behavior).toMatch(/mitigable declared exception/i);
|
|
432
|
+
expect(focusable.behavior).toMatch(/REMAIN in\s+focus order/);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('Input-Text-Password contracts.yaml excludes state_readonly (security fork deleted)', () => {
|
|
436
|
+
const contractsPath = path.join(COMPONENTS_DIR, 'Input-Text-Password', 'contracts.yaml');
|
|
437
|
+
const content = fs.readFileSync(contractsPath, 'utf-8');
|
|
438
|
+
const parsed = yaml.load(content) as Record<string, any>;
|
|
439
|
+
|
|
440
|
+
expect(parsed.excludes).toBeDefined();
|
|
441
|
+
expect(parsed.excludes.state_readonly).toBeDefined();
|
|
442
|
+
expect(parsed.excludes.state_readonly.reason).toMatch(/security/i);
|
|
443
|
+
expect(parsed.excludes.state_readonly.reference).toMatch(
|
|
444
|
+
/input-text-base-ios-readonly-adjudication/
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('no Input-Text iOS implementation contains .disabled( anywhere', () => {
|
|
449
|
+
for (const formComponent of FORM_INPUT_COMPONENTS) {
|
|
450
|
+
const content = loadPlatformImpl(formComponent, 'ios');
|
|
451
|
+
if (!content) continue;
|
|
452
|
+
|
|
453
|
+
// Raw-source assertion, comments included: this is how
|
|
454
|
+
// .disabled(readOnly) survived two disabled-cleanup passes.
|
|
455
|
+
expect(content).not.toMatch(/\.disabled\(/);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('iOS readOnly path renders selectable Text inside the shared field chrome', () => {
|
|
460
|
+
const content = loadPlatformImpl('Input-Text-Base', 'ios');
|
|
461
|
+
expect(content).not.toBeNull();
|
|
462
|
+
|
|
463
|
+
// B-prime: Text(value).textSelection(.enabled) wrapped by the
|
|
464
|
+
// extracted chrome modifier, shared with the editable path.
|
|
465
|
+
expect(content).toMatch(/\.textSelection\(\.enabled\)/);
|
|
466
|
+
expect(content).toMatch(/InputTextBaseFieldChrome/);
|
|
467
|
+
// The chrome must reserve min-height from the typographyInput
|
|
468
|
+
// line-height token unconditionally on both paths (Kenya hardening).
|
|
469
|
+
expect(content).toMatch(/typographyInput\.lineHeight/);
|
|
470
|
+
expect(content).toMatch(/minHeight/);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('iOS readOnly Text path is unreachable for secure fields (SECURITY gate)', () => {
|
|
474
|
+
const content = loadPlatformImpl('Input-Text-Base', 'ios');
|
|
475
|
+
expect(content).not.toBeNull();
|
|
476
|
+
|
|
477
|
+
// The gate: the read-only display path must exclude .password so a
|
|
478
|
+
// secure value can never render as selectable plaintext Text.
|
|
479
|
+
expect(content).toMatch(/readOnly\s*&&\s*type\s*!=\s*\.password/);
|
|
480
|
+
// And the rendering branch must key off the gated property, not the
|
|
481
|
+
// raw readOnly prop.
|
|
482
|
+
expect(content).toMatch(/if\s+isReadOnlyDisplay/);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('iOS Input-Text-Password never forwards readOnly to the base', () => {
|
|
486
|
+
const content = loadPlatformImpl('Input-Text-Password', 'ios');
|
|
487
|
+
expect(content).not.toBeNull();
|
|
488
|
+
|
|
489
|
+
// Both branches (masked SecureField and revealed .text) must strip
|
|
490
|
+
// readOnly — the revealed branch would otherwise route a secret to
|
|
491
|
+
// the base's plaintext Text path.
|
|
492
|
+
const forwarded = content!.match(/readOnly:\s*readOnly/g);
|
|
493
|
+
expect(forwarded).toBeNull();
|
|
494
|
+
|
|
495
|
+
const stripped = content!.match(/readOnly:\s*false/g);
|
|
496
|
+
expect(stripped).not.toBeNull();
|
|
497
|
+
expect(stripped!.length).toBeGreaterThanOrEqual(2);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('iOS composes a read-only accessibility indication (never a dimmed trait)', () => {
|
|
501
|
+
const content = loadPlatformImpl('Input-Text-Base', 'ios');
|
|
502
|
+
expect(content).not.toBeNull();
|
|
503
|
+
|
|
504
|
+
// Composed label + value + read-only hint (Kenya R1: iOS has no
|
|
505
|
+
// read-only trait; the hint is the announcement route).
|
|
506
|
+
expect(content).toMatch(/Read only/);
|
|
507
|
+
expect(content).toMatch(/accessibilityHint/);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('web implements readOnly via the native readonly attribute', () => {
|
|
511
|
+
const content = loadPlatformImpl('Input-Text-Base', 'web');
|
|
512
|
+
expect(content).not.toBeNull();
|
|
513
|
+
|
|
514
|
+
// Native readonly attribute = native semantics (implicit
|
|
515
|
+
// aria-readonly), focus-order participation, selectable content.
|
|
516
|
+
expect(content).toMatch(/readOnly\s*\?\s*'readonly'/);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('Android passes readOnly through to the text field without disabled semantics', () => {
|
|
520
|
+
const content = loadPlatformImpl('Input-Text-Base', 'android');
|
|
521
|
+
expect(content).not.toBeNull();
|
|
522
|
+
|
|
523
|
+
// Compose readOnly keeps the field focusable/selectable/copyable.
|
|
524
|
+
expect(content).toMatch(/readOnly\s*=\s*readOnly/);
|
|
525
|
+
// Never enabled=false — the only Compose mechanism that would remove
|
|
526
|
+
// a readOnly field from focus order is banned disabled semantics.
|
|
527
|
+
expect(content).not.toMatch(/enabled\s*=\s*false/);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('Android sets TalkBack stateDescription("read only") conditionally on readOnly', () => {
|
|
531
|
+
const content = loadPlatformImpl('Input-Text-Base', 'android');
|
|
532
|
+
expect(content).not.toBeNull();
|
|
533
|
+
|
|
534
|
+
// Announcement parity (adjudication condition 3): explicit
|
|
535
|
+
// stateDescription in the semantics block, gated on readOnly —
|
|
536
|
+
// never announced on editable fields, never a disabled/dimmed state.
|
|
537
|
+
expect(content).toMatch(
|
|
538
|
+
/if\s*\(readOnly\)\s*\{[^}]*stateDescription\s*=\s*"read only"/
|
|
539
|
+
);
|
|
540
|
+
// And the semantics property must actually be wired up (import), not
|
|
541
|
+
// just mentioned in a comment.
|
|
542
|
+
expect(content).toMatch(/import\s+androidx\.compose\.ui\.semantics\.stateDescription/);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('Android Input-Text-Password never forwards readOnly to the base', () => {
|
|
546
|
+
const content = loadPlatformImpl('Input-Text-Password', 'android');
|
|
547
|
+
expect(content).not.toBeNull();
|
|
548
|
+
|
|
549
|
+
// readOnly is contracted out of the Password variant (adjudication
|
|
550
|
+
// condition 4). On Android this is contract conformance, not a
|
|
551
|
+
// security fix — the masked PasswordVisualTransformation already
|
|
552
|
+
// suppresses copy/cut ([DATA R2] §3) — but the exclusion must hold
|
|
553
|
+
// uniformly: the prop is accepted for API stability and stripped.
|
|
554
|
+
const forwarded = content!.match(/readOnly\s*=\s*readOnly/g);
|
|
555
|
+
expect(forwarded).toBeNull();
|
|
556
|
+
|
|
557
|
+
const stripped = content!.match(/readOnly\s*=\s*false/g);
|
|
558
|
+
expect(stripped).not.toBeNull();
|
|
559
|
+
expect(stripped!.length).toBeGreaterThanOrEqual(1);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('web Input-Text-Password never forwards read-only to the base', () => {
|
|
563
|
+
const content = loadPlatformImpl('Input-Text-Password', 'web');
|
|
564
|
+
expect(content).not.toBeNull();
|
|
565
|
+
|
|
566
|
+
// readOnly is contracted out of the Password variant (adjudication
|
|
567
|
+
// condition 4). On web this is contract conformance, not a security
|
|
568
|
+
// fix — a readonly password input stays masked and browsers block
|
|
569
|
+
// copy from type=password — but the exclusion holds uniformly on all
|
|
570
|
+
// three platforms.
|
|
571
|
+
// The render template must not interpolate read-only onto the inner
|
|
572
|
+
// input-text-base, and no code path may read the attribute.
|
|
573
|
+
expect(content).not.toMatch(/\$\{readOnly/);
|
|
574
|
+
expect(content).not.toMatch(/hasAttribute\(\s*['"]read-only['"]\s*\)/);
|
|
575
|
+
|
|
576
|
+
// The attribute stays accepted (observedAttributes) for API
|
|
577
|
+
// stability, documented as ignored with the adjudication reference.
|
|
578
|
+
expect(content).toMatch(/'read-only',/);
|
|
579
|
+
expect(content).toMatch(/input-text-base-ios-readonly-adjudication/);
|
|
580
|
+
});
|
|
374
581
|
});
|
|
375
582
|
});
|
|
376
583
|
|