@multiplatform.one/theme 7.4.1 → 7.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 +2 -1
- package/package.json +5 -5
- package/src/audit/index.ts +19 -0
- package/src/audit/themeMatrix.spec.ts +274 -0
- package/src/audit/themeMatrix.ts +630 -0
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +37 -25
- package/src/index.ts +12 -0
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +14 -31
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +306 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +270 -3
- package/src/theme/cornerSmoothing.ts +147 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +24 -0
- package/src/theme/defaults/fonts.spec.ts +163 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +139 -0
- package/src/theme/defaults/fonts.ts +214 -50
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +119 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryMetrics.ts +57 -0
- package/src/theme/fontCategoryStacks.ts +37 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +155 -13
- package/src/theme/resolveKnobs.ts +193 -27
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/audit/index.d.ts +2 -0
- package/types/audit/index.d.ts.map +1 -1
- package/types/audit/themeMatrix.d.ts +172 -0
- package/types/audit/themeMatrix.d.ts.map +1 -0
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +8 -12
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +23 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +140 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryMetrics.d.ts +36 -0
- package/types/theme/fontCategoryMetrics.d.ts.map +1 -0
- package/types/theme/fontCategoryStacks.d.ts +24 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +4 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ Requires `react` and `tamagui` as peers.
|
|
|
22
22
|
including intent + component overrides
|
|
23
23
|
- **Recipes** — control/text/elevation state recipes (hover, press, focus-visible)
|
|
24
24
|
- **Optics** — the `cornerSmoothing` knob (`round` | `smooth` squircle corners:
|
|
25
|
-
web progressive enhancement via CSS `corner-shape`,
|
|
25
|
+
web progressive enhancement via CSS `corner-shape: squircle`, computed
|
|
26
|
+
`superellipse(2)` on Chromium 152; native stays round),
|
|
26
27
|
`hairline` separator treatments (0.5 device-pixel rules with a 1px low-DPI
|
|
27
28
|
fallback), and tracking-by-size in the default font faces
|
|
28
29
|
- **Chart palette** — `useChartPalette()`: identity-led series colors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@tamagui/toast": "2.7.6",
|
|
54
54
|
"@tamagui/web": "2.7.6",
|
|
55
55
|
"react-cookie": "^8.1.2",
|
|
56
|
-
"@multiplatform.one/platform": "7.
|
|
57
|
-
"@multiplatform.one/store": "7.
|
|
56
|
+
"@multiplatform.one/platform": "7.6.0",
|
|
57
|
+
"@multiplatform.one/store": "7.6.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@tamagui/animations-css": "2.7.6",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"tamagui": "2.7.6",
|
|
70
70
|
"typescript": "~5.9.3",
|
|
71
71
|
"vitest": "^4.1.5",
|
|
72
|
-
"@multiplatform.one/config": "7.
|
|
73
|
-
"@multiplatform.one/test-utils": "7.
|
|
72
|
+
"@multiplatform.one/config": "7.6.0",
|
|
73
|
+
"@multiplatform.one/test-utils": "7.6.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@tamagui/animations-css": "^2.0.0-rc",
|
package/src/audit/index.ts
CHANGED
|
@@ -8,3 +8,22 @@ export type {
|
|
|
8
8
|
export { runConstraintAudit } from "./constraintAudit";
|
|
9
9
|
export type { AuditDiff, ElementDiff, PropertyDiff } from "./diffReport";
|
|
10
10
|
export { diffAuditReports, formatAuditReport } from "./diffReport";
|
|
11
|
+
export type {
|
|
12
|
+
GeometryDiff,
|
|
13
|
+
GeometryMove,
|
|
14
|
+
MatrixElementGeometry,
|
|
15
|
+
MatrixRect,
|
|
16
|
+
MatrixSnapshot,
|
|
17
|
+
MatrixTextSample,
|
|
18
|
+
NoBreakageInput,
|
|
19
|
+
NoBreakageResult,
|
|
20
|
+
TextContrastResult,
|
|
21
|
+
TextContrastViolation,
|
|
22
|
+
} from "./themeMatrix";
|
|
23
|
+
export {
|
|
24
|
+
auditSignatures,
|
|
25
|
+
captureMatrixSnapshot,
|
|
26
|
+
diffGeometry,
|
|
27
|
+
evaluateNoBreakage,
|
|
28
|
+
evaluateTextContrast,
|
|
29
|
+
} from "./themeMatrix";
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateSizeRecipes } from "../theme/recipeInputs";
|
|
3
|
+
import type { SizeRecipeInputs } from "../theme/recipeInputs";
|
|
4
|
+
import {
|
|
5
|
+
captureMatrixSnapshot,
|
|
6
|
+
diffGeometry,
|
|
7
|
+
evaluateNoBreakage,
|
|
8
|
+
evaluateTextContrast,
|
|
9
|
+
type MatrixSnapshot,
|
|
10
|
+
type MatrixTextSample,
|
|
11
|
+
} from "./themeMatrix";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders a size-recipe table into the geometry shape the matrix runner
|
|
15
|
+
* captures from the DOM, so the invariance diff can be held against the
|
|
16
|
+
* ONE banned coupling (LC-80): recipe inputs consuming a theme option.
|
|
17
|
+
* A control's height/padding/font per size token is exactly what the
|
|
18
|
+
* generated recipes pin, so any theme-dependence of the inputs shows up
|
|
19
|
+
* here the same way it would on screen.
|
|
20
|
+
*/
|
|
21
|
+
function snapshotFromRecipeInputs(inputs: Partial<SizeRecipeInputs>): MatrixSnapshot {
|
|
22
|
+
const generated = generateSizeRecipes(inputs);
|
|
23
|
+
const geometry = Object.entries(generated.recipes).map(([token, recipe]) => ({
|
|
24
|
+
key: `button:${token}`,
|
|
25
|
+
tag: "button",
|
|
26
|
+
rect: { x: 0, y: 0, w: recipe.paddingHorizontal * 2 + 80, h: recipe.height },
|
|
27
|
+
props: {
|
|
28
|
+
paddingLeft: `${recipe.paddingHorizontal}px`,
|
|
29
|
+
paddingRight: `${recipe.paddingHorizontal}px`,
|
|
30
|
+
fontSize: `${recipe.fontSize}px`,
|
|
31
|
+
rowGap: `${recipe.gap}px`,
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
return { storyRendered: true, geometry, textSamples: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("theme-axis geometry invariance (LC-80 tripwire)", () => {
|
|
38
|
+
it("identical recipe inputs across theme colours keep the diff clean", () => {
|
|
39
|
+
const base = snapshotFromRecipeInputs({});
|
|
40
|
+
const blue = snapshotFromRecipeInputs({});
|
|
41
|
+
const diff = diffGeometry(base, blue);
|
|
42
|
+
expect(diff.identical).toBe(true);
|
|
43
|
+
expect(diff.moves).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("deliberately feeding a theme option into recipeInputs FAILS the invariance diff", () => {
|
|
47
|
+
// The banned coupling: a size recipe input derived from the active
|
|
48
|
+
// theme colour. Geometry becomes a function of theme and the theme
|
|
49
|
+
// axis is no longer paint-only.
|
|
50
|
+
const inputsForTheme = (theme: string): Partial<SizeRecipeInputs> =>
|
|
51
|
+
theme === "blue" ? { baseHeight: 48 } : {};
|
|
52
|
+
|
|
53
|
+
const defaultCell = snapshotFromRecipeInputs(inputsForTheme(""));
|
|
54
|
+
const blueCell = snapshotFromRecipeInputs(inputsForTheme("blue"));
|
|
55
|
+
|
|
56
|
+
const diff = diffGeometry(defaultCell, blueCell);
|
|
57
|
+
expect(diff.identical).toBe(false);
|
|
58
|
+
expect(diff.moves.length).toBeGreaterThan(0);
|
|
59
|
+
// The moved properties are the generated recipe outputs, named per element.
|
|
60
|
+
const movedProps = new Set(diff.moves.map((m) => m.property));
|
|
61
|
+
expect(movedProps.has("rect")).toBe(true);
|
|
62
|
+
expect(movedProps.has("fontSize")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("reports structural drift (added/removed elements) as non-identical", () => {
|
|
66
|
+
const base = snapshotFromRecipeInputs({});
|
|
67
|
+
const mutated: MatrixSnapshot = {
|
|
68
|
+
...base,
|
|
69
|
+
geometry: base.geometry.slice(1),
|
|
70
|
+
};
|
|
71
|
+
const diff = diffGeometry(base, mutated);
|
|
72
|
+
expect(diff.identical).toBe(false);
|
|
73
|
+
expect(diff.removedKeys).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("absorbs sub-pixel rect jitter only within the explicit epsilon", () => {
|
|
77
|
+
const base = snapshotFromRecipeInputs({});
|
|
78
|
+
const jittered: MatrixSnapshot = {
|
|
79
|
+
...base,
|
|
80
|
+
geometry: base.geometry.map((g) => ({
|
|
81
|
+
...g,
|
|
82
|
+
rect: { ...g.rect, x: g.rect.x + 0.25 },
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
expect(diffGeometry(base, jittered, { epsilonPx: 0.25 }).identical).toBe(true);
|
|
86
|
+
expect(diffGeometry(base, jittered, { epsilonPx: 0 }).identical).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
91
|
+
const cleanContrast = { measured: 10, unmeasurable: 0, violations: [] };
|
|
92
|
+
|
|
93
|
+
it("does NOT flag moved geometry — boldPreset is EXPECTED to move pixels", () => {
|
|
94
|
+
// A preset cell whose geometry differs wildly from the default cell
|
|
95
|
+
// still passes no-breakage as long as nothing broke. Geometry equality
|
|
96
|
+
// belongs exclusively to the theme axis.
|
|
97
|
+
const defaultCell = snapshotFromRecipeInputs({});
|
|
98
|
+
const boldCell = snapshotFromRecipeInputs({ baseHeight: 52, radiusPx: 50 });
|
|
99
|
+
expect(diffGeometry(defaultCell, boldCell).identical).toBe(false);
|
|
100
|
+
|
|
101
|
+
const result = evaluateNoBreakage({
|
|
102
|
+
storyRendered: true,
|
|
103
|
+
offScaleViolations: 0,
|
|
104
|
+
baselineOffScaleViolations: 0,
|
|
105
|
+
contrast: cleanContrast,
|
|
106
|
+
baselineContrastViolations: 0,
|
|
107
|
+
});
|
|
108
|
+
expect(result.ok).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("FAILS when a preset cell falls off the knob scales beyond the default cell", () => {
|
|
112
|
+
const result = evaluateNoBreakage({
|
|
113
|
+
storyRendered: true,
|
|
114
|
+
offScaleViolations: 3,
|
|
115
|
+
baselineOffScaleViolations: 1,
|
|
116
|
+
contrast: cleanContrast,
|
|
117
|
+
baselineContrastViolations: 0,
|
|
118
|
+
});
|
|
119
|
+
expect(result.ok).toBe(false);
|
|
120
|
+
expect(result.failures[0]).toContain("off-scale");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("FAILS when a preset cell loses contrast the default cell had", () => {
|
|
124
|
+
const result = evaluateNoBreakage({
|
|
125
|
+
storyRendered: true,
|
|
126
|
+
offScaleViolations: 0,
|
|
127
|
+
baselineOffScaleViolations: 0,
|
|
128
|
+
contrast: {
|
|
129
|
+
measured: 10,
|
|
130
|
+
unmeasurable: 0,
|
|
131
|
+
violations: [
|
|
132
|
+
{
|
|
133
|
+
key: "button:$4",
|
|
134
|
+
text: "Save",
|
|
135
|
+
color: "#aaaaaa",
|
|
136
|
+
background: "#ffffff",
|
|
137
|
+
ratio: 2.32,
|
|
138
|
+
required: 4.5,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
baselineContrastViolations: 0,
|
|
143
|
+
});
|
|
144
|
+
expect(result.ok).toBe(false);
|
|
145
|
+
expect(result.failures[0]).toContain("contrast");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("FAILS when the story does not render at all", () => {
|
|
149
|
+
const result = evaluateNoBreakage({
|
|
150
|
+
storyRendered: false,
|
|
151
|
+
offScaleViolations: 0,
|
|
152
|
+
baselineOffScaleViolations: 0,
|
|
153
|
+
contrast: cleanContrast,
|
|
154
|
+
baselineContrastViolations: 0,
|
|
155
|
+
});
|
|
156
|
+
expect(result.ok).toBe(false);
|
|
157
|
+
expect(result.failures[0]).toContain("render");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("evaluateTextContrast (the assertion downstream repos call)", () => {
|
|
162
|
+
const sample = (overrides: Partial<MatrixTextSample>): MatrixTextSample => ({
|
|
163
|
+
key: "p:0",
|
|
164
|
+
text: "body text",
|
|
165
|
+
color: "#000000",
|
|
166
|
+
background: "#ffffff",
|
|
167
|
+
backgroundResolved: true,
|
|
168
|
+
overImage: false,
|
|
169
|
+
fontSizePx: 14,
|
|
170
|
+
fontWeightNum: 400,
|
|
171
|
+
rect: { x: 0, y: 0, w: 100, h: 20 },
|
|
172
|
+
...overrides,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("passes black-on-white body text", () => {
|
|
176
|
+
const result = evaluateTextContrast([sample({})]);
|
|
177
|
+
expect(result.measured).toBe(1);
|
|
178
|
+
expect(result.violations).toHaveLength(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("fails low-contrast body text against the 4.5:1 AA floor", () => {
|
|
182
|
+
const result = evaluateTextContrast([sample({ color: "#999999" })]);
|
|
183
|
+
expect(result.violations).toHaveLength(1);
|
|
184
|
+
expect(result.violations[0].required).toBe(4.5);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("holds large text (>=24px, or >=18.66px bold) to the 3:1 floor instead", () => {
|
|
188
|
+
// #949494 on white is ~3.5:1 — fails body text, clears large text.
|
|
189
|
+
const failing = evaluateTextContrast([sample({ color: "#949494" })]);
|
|
190
|
+
expect(failing.violations).toHaveLength(1);
|
|
191
|
+
const heading = evaluateTextContrast([sample({ color: "#949494", fontSizePx: 24 })]);
|
|
192
|
+
expect(heading.violations).toHaveLength(0);
|
|
193
|
+
const boldLead = evaluateTextContrast([
|
|
194
|
+
sample({ color: "#949494", fontSizePx: 19, fontWeightNum: 700 }),
|
|
195
|
+
]);
|
|
196
|
+
expect(boldLead.violations).toHaveLength(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("counts unresolved backdrops instead of silently dropping them", () => {
|
|
200
|
+
const result = evaluateTextContrast([sample({ backgroundResolved: false })]);
|
|
201
|
+
expect(result.measured).toBe(0);
|
|
202
|
+
expect(result.unmeasurable).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("fails the old storefront accent-on-white pair (1.17:1) that lived in shc contrast.spec.ts", () => {
|
|
206
|
+
// rgb(238,236,249) on white — the pair the SHC spec measured at 1.17:1
|
|
207
|
+
// from accent.ts's then-lightPalette[0]. Re-introducing that wash as
|
|
208
|
+
// body ink must fail the theme assertion, not a consumer-local spec.
|
|
209
|
+
const result = evaluateTextContrast([sample({ color: "#eeecf9", background: "#ffffff" })]);
|
|
210
|
+
expect(result.violations).toHaveLength(1);
|
|
211
|
+
expect(result.violations[0].ratio).toBeLessThan(2);
|
|
212
|
+
expect(result.violations[0].required).toBe(4.5);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("counts unparseable colours as unmeasurable (measureContrast throws, never skips silently)", () => {
|
|
216
|
+
const result = evaluateTextContrast([sample({ color: "potato" })]);
|
|
217
|
+
expect(result.measured).toBe(0);
|
|
218
|
+
expect(result.unmeasurable).toBe(1);
|
|
219
|
+
expect(result.violations).toHaveLength(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("captureMatrixSnapshot keys survive the theme axis", () => {
|
|
224
|
+
/**
|
|
225
|
+
* The measured artefact this locks down. Tamagui's `<Theme>` emits a
|
|
226
|
+
* boxless `display: contents` passthrough when the requested theme is
|
|
227
|
+
* already the active one and a REAL theme host when it differs, so the
|
|
228
|
+
* wrapper count is a function of the colour global. Keying on it made
|
|
229
|
+
* every coloured cell of dogfood--settings report `+N/-M elements` with
|
|
230
|
+
* ZERO moves: 158 nodes at the default colour, 151 with `color:blue`, 142
|
|
231
|
+
* with `color:gray`, against 130 in every cell once the boxless wrappers
|
|
232
|
+
* are hoisted through.
|
|
233
|
+
*/
|
|
234
|
+
function mount(html: string): void {
|
|
235
|
+
document.body.innerHTML = `<div id="mpo-matrix-root">${html}</div>`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const card = '<div class="card"><span class="label">Name</span></div>';
|
|
239
|
+
|
|
240
|
+
it("hoists through a display:contents wrapper so the key set is identical", () => {
|
|
241
|
+
mount(card);
|
|
242
|
+
const bare = captureMatrixSnapshot();
|
|
243
|
+
mount(`<span style="display: contents">${card}</span>`);
|
|
244
|
+
const wrapped = captureMatrixSnapshot();
|
|
245
|
+
|
|
246
|
+
expect(bare.geometry.map((g) => g.key)).toEqual(wrapped.geometry.map((g) => g.key));
|
|
247
|
+
const diff = diffGeometry(bare, wrapped);
|
|
248
|
+
expect(diff.addedKeys).toEqual([]);
|
|
249
|
+
expect(diff.removedKeys).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("stays stable when the wrapper count differs the way the colour axis makes it differ", () => {
|
|
253
|
+
mount(`<span style="display: contents"><span style="display: contents">${card}</span></span>`);
|
|
254
|
+
const twoWrappers = captureMatrixSnapshot();
|
|
255
|
+
mount(card);
|
|
256
|
+
const noWrapper = captureMatrixSnapshot();
|
|
257
|
+
expect(diffGeometry(noWrapper, twoWrappers).identical).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("still counts a real boxed wrapper as structural drift", () => {
|
|
261
|
+
mount(card);
|
|
262
|
+
const bare = captureMatrixSnapshot();
|
|
263
|
+
mount(`<span style="display: block">${card}</span>`);
|
|
264
|
+
const boxed = captureMatrixSnapshot();
|
|
265
|
+
const diff = diffGeometry(bare, boxed);
|
|
266
|
+
expect(diff.identical).toBe(false);
|
|
267
|
+
expect(diff.addedKeys.length + diff.removedKeys.length).toBeGreaterThan(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("reports an empty story root as not rendered", () => {
|
|
271
|
+
document.body.innerHTML = '<div id="mpo-matrix-root"></div>';
|
|
272
|
+
expect(captureMatrixSnapshot().storyRendered).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
});
|