cursedbelt 5.30.0 → 5.32.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/dist/react/components/ConfirmDialog.d.ts +4 -1
- package/dist/react/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/react/components/ConfirmDialog.js +2 -2
- package/dist/react/components/ConfirmDialog.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +5 -1
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/PromptDialog.d.ts +4 -1
- package/dist/react/components/PromptDialog.d.ts.map +1 -1
- package/dist/react/components/PromptDialog.js +2 -2
- package/dist/react/components/PromptDialog.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts +26 -1
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +182 -40
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts +11 -0
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/objectLeak.d.ts +23 -0
- package/dist/testing/objectLeak.d.ts.map +1 -0
- package/dist/testing/objectLeak.js +37 -0
- package/dist/testing/objectLeak.js.map +1 -0
- package/package.json +5 -5
- package/scripts/guardrails-baseline.json +4 -1
- package/scripts/guardrailsEnforce.spec.ts +163 -6
- package/scripts/guardrailsEnforce.ts +365 -40
- package/scripts/paths.ts +1 -1
- package/src/react/components/ConfirmDialog.tsx +5 -0
- package/src/react/components/DataTable/DataTable.spec.tsx +16 -0
- package/src/react/components/DataTable/DataTable.tsx +5 -1
- package/src/react/components/PromptDialog.spec.tsx +29 -1
- package/src/react/components/PromptDialog.tsx +5 -0
- package/src/react/file-tree/FileTree.spec.tsx +205 -0
- package/src/react/file-tree/FileTree.tsx +220 -38
- package/src/testing/index.ts +3 -0
- package/src/testing/objectLeak.spec.ts +21 -0
- package/src/testing/objectLeak.ts +39 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAmB,MAAM,cAAc,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `[object Object]` on a screen, caught by the test that rendered it.
|
|
3
|
+
*
|
|
4
|
+
* DESIGN.md "Core Mandate" forbids it by name — a literal `[object Object]` leaking into a view —
|
|
5
|
+
* and no scanner can see it coming: it is a value (`${row.owner}`, `{error}`, `String(meta)`)
|
|
6
|
+
* that happens to be an object at runtime, which compiles, typechecks and renders. The only
|
|
7
|
+
* place it is visible is the rendered text, so that is what this reads (task 2381).
|
|
8
|
+
*
|
|
9
|
+
* Plain functions that throw a plain `Error`, with no test-framework import, so the same call
|
|
10
|
+
* works from a `bun:test` + happy-dom spec (`expectNoObjectLeak(container)`) and from a
|
|
11
|
+
* browser spec that reads the page's text (`expectNoObjectLeak(await page.innerText("body"))`).
|
|
12
|
+
*/
|
|
13
|
+
/** Anything a DOM node or a page can hand over as its visible text. */
|
|
14
|
+
export type TextSource = string | {
|
|
15
|
+
textContent: string | null;
|
|
16
|
+
} | {
|
|
17
|
+
innerText: string;
|
|
18
|
+
};
|
|
19
|
+
/** The first leaked value in `source` with ~40 characters either side, or `null` when clean. */
|
|
20
|
+
export declare function findObjectLeak(source: TextSource): string | null;
|
|
21
|
+
/** Throw when the rendered text shows a value that was never meant to be text. */
|
|
22
|
+
export declare function expectNoObjectLeak(source: TextSource): void;
|
|
23
|
+
//# sourceMappingURL=objectLeak.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"objectLeak.d.ts","sourceRoot":"","sources":["../../src/testing/objectLeak.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAQzF,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAOhE;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAG3D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `[object Object]` on a screen, caught by the test that rendered it.
|
|
3
|
+
*
|
|
4
|
+
* DESIGN.md "Core Mandate" forbids it by name — a literal `[object Object]` leaking into a view —
|
|
5
|
+
* and no scanner can see it coming: it is a value (`${row.owner}`, `{error}`, `String(meta)`)
|
|
6
|
+
* that happens to be an object at runtime, which compiles, typechecks and renders. The only
|
|
7
|
+
* place it is visible is the rendered text, so that is what this reads (task 2381).
|
|
8
|
+
*
|
|
9
|
+
* Plain functions that throw a plain `Error`, with no test-framework import, so the same call
|
|
10
|
+
* works from a `bun:test` + happy-dom spec (`expectNoObjectLeak(container)`) and from a
|
|
11
|
+
* browser spec that reads the page's text (`expectNoObjectLeak(await page.innerText("body"))`).
|
|
12
|
+
*/
|
|
13
|
+
const LEAK_RE = /\[object (?:Object|Promise|Map|Set|Module)\]|\bundefined(?:px|rem|%)\b|\bNaN(?:px|rem|%)\b/;
|
|
14
|
+
function textOf(source) {
|
|
15
|
+
if (typeof source === "string")
|
|
16
|
+
return source;
|
|
17
|
+
if ("innerText" in source && typeof source.innerText === "string")
|
|
18
|
+
return source.innerText;
|
|
19
|
+
return source.textContent ?? "";
|
|
20
|
+
}
|
|
21
|
+
/** The first leaked value in `source` with ~40 characters either side, or `null` when clean. */
|
|
22
|
+
export function findObjectLeak(source) {
|
|
23
|
+
const text = textOf(source);
|
|
24
|
+
const match = LEAK_RE.exec(text);
|
|
25
|
+
if (!match)
|
|
26
|
+
return null;
|
|
27
|
+
const from = Math.max(0, match.index - 40);
|
|
28
|
+
const to = Math.min(text.length, match.index + match[0].length + 40);
|
|
29
|
+
return `${from > 0 ? "…" : ""}${text.slice(from, to).replace(/\s+/g, " ")}${to < text.length ? "…" : ""}`;
|
|
30
|
+
}
|
|
31
|
+
/** Throw when the rendered text shows a value that was never meant to be text. */
|
|
32
|
+
export function expectNoObjectLeak(source) {
|
|
33
|
+
const leak = findObjectLeak(source);
|
|
34
|
+
if (leak)
|
|
35
|
+
throw new Error(`rendered text leaks a non-text value (DESIGN.md Core Mandate): ${leak}`);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=objectLeak.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"objectLeak.js","sourceRoot":"","sources":["../../src/testing/objectLeak.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,OAAO,GAAG,4FAA4F,CAAC;AAK7G,SAAS,MAAM,CAAC,MAAkB;IACjC,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC9C,IAAI,WAAW,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC;IAC3F,OAAQ,MAAyC,CAAC,WAAW,IAAI,EAAE,CAAC;AACrE,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,MAAkB;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAC3C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IACrE,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC3G,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,MAAkB;IACpD,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,kEAAkE,IAAI,EAAE,CAAC,CAAC;AACrG,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.32.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"typecheck:scripts": "tsc --noEmit --project tsconfig.scripts.json",
|
|
70
70
|
"demo:check": "tsc --noEmit --project demo/tsconfig.json",
|
|
71
71
|
"demo:paths": "bun run demo/generatePaths.ts --write",
|
|
72
|
-
"guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface --scope cursedbelt src/react src/styles.css",
|
|
72
|
+
"guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface,no-inline-style,opt-out-count,opt-out-needs-cause --scope cursedbelt src/react src/styles.css",
|
|
73
73
|
"lint": "biome lint --error-on-warnings src e2e demo scripts",
|
|
74
74
|
"styles": "bun run styles:static && bun run styles:utilities && bun run styles:areas",
|
|
75
75
|
"styles:static": "bun run scripts/generateStaticStyles.ts",
|
|
@@ -494,9 +494,9 @@
|
|
|
494
494
|
"./styles-utilities.css": "./src/styles-utilities.css",
|
|
495
495
|
"./styles.css": "./src/styles.css",
|
|
496
496
|
"./testing": {
|
|
497
|
-
"types": "./dist/testing/
|
|
498
|
-
"bun": "./src/testing/
|
|
499
|
-
"source": "./src/testing/
|
|
497
|
+
"types": "./dist/testing/index.d.ts",
|
|
498
|
+
"bun": "./src/testing/index.ts",
|
|
499
|
+
"source": "./src/testing/index.ts",
|
|
500
500
|
"default": "./dist/testing/_noBun.js"
|
|
501
501
|
},
|
|
502
502
|
"./theme.css": "./src/theme.css",
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"no-raw-lazy": 0,
|
|
18
18
|
"no-raw-popover": 0,
|
|
19
19
|
"no-hand-rolled-scroll-memory": 0,
|
|
20
|
-
"no-hand-rolled-surface": 0
|
|
20
|
+
"no-hand-rolled-surface": 0,
|
|
21
|
+
"no-inline-style": 76,
|
|
22
|
+
"opt-out-count": 267,
|
|
23
|
+
"opt-out-needs-cause": 258
|
|
21
24
|
},
|
|
22
25
|
"cursedshell": {
|
|
23
26
|
"no-raw-action-button": 0,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
APP_RULES,
|
|
12
12
|
BELT_INTERNAL_RULES,
|
|
13
13
|
beltExportedComponentNames,
|
|
14
|
+
disallowedInlineStyleKeys,
|
|
15
|
+
findOptOuts,
|
|
14
16
|
parseFileAllows,
|
|
15
17
|
positionedClassesFromCss,
|
|
16
18
|
RULES,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
setReimplementedNames,
|
|
19
21
|
setThemeCss,
|
|
20
22
|
walk,
|
|
23
|
+
weakOptOutReason,
|
|
21
24
|
} from './guardrailsEnforce';
|
|
22
25
|
|
|
23
26
|
function check(rule: keyof typeof RULES, source: string) {
|
|
@@ -56,8 +59,26 @@ describe('no-raw-action-button', () => {
|
|
|
56
59
|
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
// Task 2381: the marker was the loophole — `role="switch"` added to satisfy this rule
|
|
63
|
+
// beside the belt's `Switch`. It exempts only inside a belt group that owns the role.
|
|
64
|
+
test('🔴 a bare role marker is NOT an opt-out any more', () => {
|
|
65
|
+
expect(check('no-raw-action-button', '<button role="tab">Tab</button>')).toHaveLength(1);
|
|
66
|
+
expect(check('no-raw-action-button', '<button role="switch" aria-checked={on}>Edit</button>')).toHaveLength(1);
|
|
67
|
+
expect(check('no-raw-action-button', '<button aria-selected={on}>Row</button>')).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a role marker inside a belt choice group still passes — it is the group\'s own part', () => {
|
|
71
|
+
const source = `<SegmentedControl value={v}>
|
|
72
|
+
<button role="radio" aria-checked={v === 'a'}>A</button>
|
|
73
|
+
</SegmentedControl>`;
|
|
74
|
+
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a CLOSED or self-closing group earlier in the file exempts nothing after it', () => {
|
|
78
|
+
const closed = `<Tabs value={t}><TabsList /></Tabs>\n<button role="tab">Loose</button>`;
|
|
79
|
+
expect(check('no-raw-action-button', closed)).toHaveLength(1);
|
|
80
|
+
const selfClosed = `<RadioGroup options={o} />\n<button role="radio">Loose</button>`;
|
|
81
|
+
expect(check('no-raw-action-button', selfClosed)).toHaveLength(1);
|
|
61
82
|
});
|
|
62
83
|
});
|
|
63
84
|
|
|
@@ -77,11 +98,11 @@ describe('opening-tag extraction (comment-aware)', () => {
|
|
|
77
98
|
});
|
|
78
99
|
|
|
79
100
|
test('a prose angle bracket does not hide a role="radio" below it', () => {
|
|
80
|
-
const source = `<button
|
|
101
|
+
const source = `<RadioGroup value={v}><button
|
|
81
102
|
/* A radio-like list rather than a <select>. */
|
|
82
103
|
role="radio"
|
|
83
104
|
aria-checked={selected}
|
|
84
|
-
>Move here</button>`;
|
|
105
|
+
>Move here</button></RadioGroup>`;
|
|
85
106
|
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
86
107
|
});
|
|
87
108
|
|
|
@@ -159,12 +180,12 @@ describe('a presentational marker must be an ATTRIBUTE, not prose about one', ()
|
|
|
159
180
|
});
|
|
160
181
|
|
|
161
182
|
test('the REAL attributes still pass silently, prose and all', () => {
|
|
162
|
-
const source = `<button
|
|
183
|
+
const source = `<RadioGroup value={v}><button
|
|
163
184
|
// A radio-like list: \`role="radio"\` + \`aria-checked\` says so to a screen reader.
|
|
164
185
|
role="radio"
|
|
165
186
|
aria-checked={selected}
|
|
166
187
|
type="button"
|
|
167
|
-
>Move here</button>`;
|
|
188
|
+
>Move here</button></RadioGroup>`;
|
|
168
189
|
expect(check('no-raw-action-button', source)).toHaveLength(0);
|
|
169
190
|
});
|
|
170
191
|
|
|
@@ -1148,6 +1169,9 @@ describe('--all-app-rules (task 2219: a new belt rule reaches every app by itsel
|
|
|
1148
1169
|
const { code, report } = await runCli(dir, ['--all-app-rules', '--baseline', 'scripts/baseline.json', 'src']);
|
|
1149
1170
|
expect(code).toBe(2);
|
|
1150
1171
|
expect(report).toContain('no row for rule(s) being scanned: no-raw-lazy');
|
|
1172
|
+
// …and still scans it, printing the row to record (task 2381) — the bumping agent
|
|
1173
|
+
// copies a line instead of hunting for a count.
|
|
1174
|
+
expect(report).toContain('record it: "no-raw-lazy": 0');
|
|
1151
1175
|
});
|
|
1152
1176
|
});
|
|
1153
1177
|
});
|
|
@@ -1664,3 +1688,136 @@ describe('no-hand-rolled-surface', () => {
|
|
|
1664
1688
|
expect(check('no-hand-rolled-surface', `// was <div className="rounded-xl border bg-card">`)).toHaveLength(0);
|
|
1665
1689
|
});
|
|
1666
1690
|
});
|
|
1691
|
+
|
|
1692
|
+
// ── Task 2381: the loopholes ─────────────────────────────────────────────────────
|
|
1693
|
+
|
|
1694
|
+
describe('opt-out-needs-cause / opt-out-count (task 2381)', () => {
|
|
1695
|
+
const good = '// guardrails-ignore no-raw-inputs: canvas — the hidden input feeds the WebGL text caret\n<input />';
|
|
1696
|
+
|
|
1697
|
+
test('a reason that leads with a cause and says why in six words passes', () => {
|
|
1698
|
+
expect(check('opt-out-needs-cause', good)).toHaveLength(0);
|
|
1699
|
+
expect(check('opt-out-needs-cause', '{/* guardrails-ignore no-raw-action-button: task 2381 — the belt Switch cannot carry a tooltip slot yet */}')).toHaveLength(0);
|
|
1700
|
+
expect(check('opt-out-needs-cause', '// guardrails-ignore no-raw-overflow: #2379 — primitives.tsx is being retired onto the belt\n')).toHaveLength(0);
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
test('🔴 a free-text reason with no cause is flagged — the loophole the review counted 421 of', () => {
|
|
1704
|
+
const found = check('opt-out-needs-cause', '// guardrails-ignore no-raw-action-button: fullscreen overlay chrome — UI state only\n<button />');
|
|
1705
|
+
expect(found).toHaveLength(1);
|
|
1706
|
+
expect(found[0]?.snippet).toContain('starts with no cause');
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
test('🔴 a cause with a token reason is flagged', () => {
|
|
1710
|
+
expect(weakOptOutReason('no-raw-inputs', 'perf — hot path')).toContain('fewer than 6 words');
|
|
1711
|
+
expect(weakOptOutReason('no-raw-inputs', 'perf — this list re-renders every animation frame at sixty hertz')).toBeNull();
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
test('a directive naming no rule suppresses nothing, and says so', () => {
|
|
1715
|
+
expect(weakOptOutReason('no-raw-buttons', 'canvas — a typo in the rule name opts out of nothing')).toContain('names no rule');
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
test('the reason continues across the comment it is written in', () => {
|
|
1719
|
+
const lineComments = `// guardrails-ignore no-raw-inputs: native-media — the picker must be the platform's
|
|
1720
|
+
// own file input so iOS offers the camera roll
|
|
1721
|
+
<input type="file" />`;
|
|
1722
|
+
expect(findOptOuts(lineComments)[0]?.reason).toContain('camera roll');
|
|
1723
|
+
expect(check('opt-out-needs-cause', lineComments)).toHaveLength(0);
|
|
1724
|
+
const block = `/* guardrails-ignore no-hardcoded-colors: third-party — the embed's
|
|
1725
|
+
brand colour is dictated by its licence terms */`;
|
|
1726
|
+
expect(check('opt-out-needs-cause', block)).toHaveLength(0);
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
test('prose in a string or a quoted example is not a directive', () => {
|
|
1730
|
+
expect(findOptOuts('const help = "guardrails-ignore no-raw-inputs: x";')).toHaveLength(0);
|
|
1731
|
+
expect(findOptOuts('// take the `guardrails-ignore no-raw-inputs: <reason>` opt-out there')).toHaveLength(0);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
test('opt-out-count counts every opt-out, good reasons included, and file-wide ones', () => {
|
|
1735
|
+
expect(check('opt-out-count', good)).toHaveLength(1);
|
|
1736
|
+
const fileWide = `// guardrails-file-allow no-raw-inputs,no-hardcoded-colors: canvas-art — a WebGL studio whose controls are drawn on the canvas itself
|
|
1737
|
+
${good}`;
|
|
1738
|
+
expect(check('opt-out-count', fileWide)).toHaveLength(2);
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
test('🔴 a file-wide reason under six words is flagged', () => {
|
|
1742
|
+
const short = '// guardrails-file-allow no-raw-inputs: canvas-art — generative art surface only\n';
|
|
1743
|
+
expect(parseFileAllows(short).allows).toHaveLength(1);
|
|
1744
|
+
expect(check('opt-out-needs-cause', short)).toHaveLength(1);
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
test('🔴 neither rule can be file-allowed — an exemption from counting exemptions', () => {
|
|
1748
|
+
const { allows, warnings } = parseFileAllows(
|
|
1749
|
+
'// guardrails-file-allow opt-out-count: canvas-art — a WebGL studio with a great many exceptions\n',
|
|
1750
|
+
);
|
|
1751
|
+
expect(allows).toHaveLength(0);
|
|
1752
|
+
expect(warnings.join('')).toContain('cannot be file-allowed');
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
describe('no-inline-style (task 2381)', () => {
|
|
1757
|
+
test('🔴 a literal size or colour is flagged, naming the keys', () => {
|
|
1758
|
+
const found = check('no-inline-style', '<span style={{ fontSize: "0.75rem", color: "var(--muted-foreground)" }}>x</span>');
|
|
1759
|
+
expect(found).toHaveLength(1);
|
|
1760
|
+
expect(found[0]?.snippet).toContain('fontSize, color');
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
test('a custom property and computed geometry pass', () => {
|
|
1764
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: the fixture IS JSX source text, template literal and all
|
|
1765
|
+
expect(check('no-inline-style', '<div style={{ "--progress": pct, width: `${w}px`, transform: `translateX(${x}px)` }} />')).toHaveLength(0);
|
|
1766
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: the fixture IS JSX source text, template literal and all
|
|
1767
|
+
expect(check('no-inline-style', '<div style={{ ["--fraction" as string]: `${f}`, top: rect.top, height }} />')).toHaveLength(0);
|
|
1768
|
+
expect(check('no-inline-style', '<div style={{ ...rest, left: pos.x }} />')).toHaveLength(0);
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
test('literal geometry is a class, not a computation', () => {
|
|
1772
|
+
expect(disallowedInlineStyleKeys(' width: "1.5rem", height: 40, top: y ')).toEqual(['width', 'height']);
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
test('a computed colour is still a colour — hand it over as a custom property', () => {
|
|
1776
|
+
expect(check('no-inline-style', '<i style={{ background: tone(g) }} />')).toHaveLength(1);
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
test('a style variable, a comment and a spec are not scanned', () => {
|
|
1780
|
+
expect(check('no-inline-style', '<div style={style} />')).toHaveLength(0);
|
|
1781
|
+
expect(check('no-inline-style', '// was <div style={{ color: "red" }}>')).toHaveLength(0);
|
|
1782
|
+
expect(RULES['no-inline-style'].check('a.spec.tsx', '<div style={{ color: "red" }} />')).toHaveLength(0);
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
test('allows an opt-out comment above', () => {
|
|
1786
|
+
const src = '{/* guardrails-ignore no-inline-style: canvas — the HUD sits over pixels the theme never paints */}\n<div style={{ color: hud }} />';
|
|
1787
|
+
expect(check('no-inline-style', src)).toHaveLength(0);
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
describe('no-local-reimplementation — near names (task 2381)', () => {
|
|
1792
|
+
const belt = ['Checkbox', 'StatusPill', 'StatTile', 'Toaster', 'AppShell', 'Badge', 'Progress'];
|
|
1793
|
+
|
|
1794
|
+
test('🔴 the spellings exact-match missed are caught, naming the belt one', () => {
|
|
1795
|
+
setReimplementedNames(belt);
|
|
1796
|
+
try {
|
|
1797
|
+
for (const [local, shared] of [
|
|
1798
|
+
['CheckBox', 'Checkbox'],
|
|
1799
|
+
['StatusBadge', 'StatusPill'],
|
|
1800
|
+
['Stat', 'StatTile'],
|
|
1801
|
+
['ToastStack', 'Toaster'],
|
|
1802
|
+
['LocalBadge', 'Badge'],
|
|
1803
|
+
]) {
|
|
1804
|
+
const found = check('no-local-reimplementation', `export function ${local}() { return null; }`);
|
|
1805
|
+
expect(found).toHaveLength(1);
|
|
1806
|
+
expect(found[0]?.snippet).toContain(`≈ cursedbelt's ${shared}`);
|
|
1807
|
+
}
|
|
1808
|
+
} finally {
|
|
1809
|
+
setReimplementedNames([]);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
test('a different noun that merely shares a stem is not a copy', () => {
|
|
1814
|
+
setReimplementedNames(belt);
|
|
1815
|
+
try {
|
|
1816
|
+
expect(check('no-local-reimplementation', 'function ProgressBadge() { return null; }')).toHaveLength(0);
|
|
1817
|
+
expect(check('no-local-reimplementation', 'function CardStack() { return null; }')).toHaveLength(0);
|
|
1818
|
+
expect(check('no-local-reimplementation', 'const API_URL = "x";')).toHaveLength(0);
|
|
1819
|
+
} finally {
|
|
1820
|
+
setReimplementedNames([]);
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
});
|