tempest-react-sdk 0.27.0 → 0.28.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 +24 -0
- package/bin/lib/css/analyze.mjs +149 -0
- package/bin/lib/css/analyze.test.mjs +136 -0
- package/bin/lib/css/collect.mjs +152 -0
- package/bin/lib/css/findings.mjs +79 -0
- package/bin/lib/css/fix.e2e.test.mjs +99 -0
- package/bin/lib/css/fix.mjs +138 -0
- package/bin/lib/css/fix.test.mjs +108 -0
- package/bin/lib/css/index.mjs +16 -0
- package/bin/lib/css/parse.mjs +398 -0
- package/bin/lib/css/parse.test.mjs +146 -0
- package/bin/lib/css/properties.mjs +399 -0
- package/bin/lib/css/repetition.mjs +213 -0
- package/bin/lib/css/repetition.test.mjs +166 -0
- package/bin/lib/css/semantic.mjs +327 -0
- package/bin/lib/css/semantic.test.mjs +201 -0
- package/bin/lib/css/tokens.mjs +119 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +45 -0
- package/bin/tempest.mjs +169 -7
- package/dist/charts/scales.cjs +2 -0
- package/dist/charts/scales.cjs.map +1 -0
- package/dist/charts/scales.js +29 -0
- package/dist/charts/scales.js.map +1 -0
- package/dist/charts.cjs +1 -1
- package/dist/charts.d.ts +91 -0
- package/dist/charts.js +9 -8
- package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
- package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.module.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs +3 -0
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.js +53 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.module.cjs +2 -0
- package/dist/components/CodeBlock/CodeBlock.module.cjs.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.module.js +29 -0
- package/dist/components/CodeBlock/CodeBlock.module.js.map +1 -0
- package/dist/components/CodeBlock/tokenize.cjs +3 -0
- package/dist/components/CodeBlock/tokenize.cjs.map +1 -0
- package/dist/components/CodeBlock/tokenize.js +267 -0
- package/dist/components/CodeBlock/tokenize.js.map +1 -0
- package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.module.js.map +1 -1
- package/dist/components/QRCode/QRCode.cjs +2 -0
- package/dist/components/QRCode/QRCode.cjs.map +1 -0
- package/dist/components/QRCode/QRCode.js +49 -0
- package/dist/components/QRCode/QRCode.js.map +1 -0
- package/dist/components/QRCode/QRCode.module.cjs +2 -0
- package/dist/components/QRCode/QRCode.module.cjs.map +1 -0
- package/dist/components/QRCode/QRCode.module.js +9 -0
- package/dist/components/QRCode/QRCode.module.js.map +1 -0
- package/dist/components/QRCode/qr-encode.cjs +2 -0
- package/dist/components/QRCode/qr-encode.cjs.map +1 -0
- package/dist/components/QRCode/qr-encode.js +295 -0
- package/dist/components/QRCode/qr-encode.js.map +1 -0
- package/dist/components/QRCode/qr-tables.cjs +2 -0
- package/dist/components/QRCode/qr-tables.cjs.map +1 -0
- package/dist/components/QRCode/qr-tables.js +366 -0
- package/dist/components/QRCode/qr-tables.js.map +1 -0
- package/dist/components/ScrollArea/ScrollArea.cjs +1 -1
- package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.js +22 -16
- package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.module.cjs.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.module.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs +2 -0
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -0
- package/dist/components/Sparkline/Sparkline.js +64 -0
- package/dist/components/Sparkline/Sparkline.js.map +1 -0
- package/dist/components/Sparkline/Sparkline.module.cjs +2 -0
- package/dist/components/Sparkline/Sparkline.module.cjs.map +1 -0
- package/dist/components/Sparkline/Sparkline.module.js +10 -0
- package/dist/components/Sparkline/Sparkline.module.js.map +1 -0
- package/dist/components/Sparkline/sparkline-geometry.cjs +2 -0
- package/dist/components/Sparkline/sparkline-geometry.cjs.map +1 -0
- package/dist/components/Sparkline/sparkline-geometry.js +56 -0
- package/dist/components/Sparkline/sparkline-geometry.js.map +1 -0
- package/dist/components/Table/Table.cjs +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js +37 -30
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Table/Table.module.cjs.map +1 -1
- package/dist/components/Table/Table.module.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js +13 -11
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.module.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.module.js.map +1 -1
- package/dist/hooks/use-scroll-overflow.cjs +2 -0
- package/dist/hooks/use-scroll-overflow.cjs.map +1 -0
- package/dist/hooks/use-scroll-overflow.js +22 -0
- package/dist/hooks/use-scroll-overflow.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +445 -2
- package/dist/tempest-react-sdk.js +197 -190
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +52 -28
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/data-viz-ramps.cjs +2 -0
- package/dist/theme/data-viz-ramps.cjs.map +1 -0
- package/dist/theme/data-viz-ramps.js +58 -0
- package/dist/theme/data-viz-ramps.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Cross-file analysis: the same block of declarations, written again and again
|
|
2
|
+
// in different CSS Modules.
|
|
3
|
+
//
|
|
4
|
+
// This is the check CSS Modules cannot do for you. Scoping guarantees `.card` in
|
|
5
|
+
// one module never collides with `.card` in another, and the price is that
|
|
6
|
+
// nothing tells you the two are byte-identical — the duplication is invisible by
|
|
7
|
+
// design. Finding it needs a pass over every sheet at once, which is what this is.
|
|
8
|
+
//
|
|
9
|
+
// It suggests, it never rewrites: whether five identical blocks become one
|
|
10
|
+
// global class, one utility or stay as they are is a design call about coupling
|
|
11
|
+
// between screens, and the CLI is not entitled to make it.
|
|
12
|
+
import { finding } from "./findings.mjs";
|
|
13
|
+
import { declSignature } from "./semantic.mjs";
|
|
14
|
+
|
|
15
|
+
/** A repeated block is only worth reporting from this many declarations up. */
|
|
16
|
+
const MIN_DECLS = 3;
|
|
17
|
+
|
|
18
|
+
/** Occurrences needed before a repeated block is reported. */
|
|
19
|
+
const MIN_OCCURRENCES = 3;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Layout idioms `utilities.css` already ships, most specific first.
|
|
23
|
+
*
|
|
24
|
+
* `require` maps a property to the value it must have (`null` = any value, which
|
|
25
|
+
* is how a gap of `8px`, `var(--tempest-space-2)` or `1rem` all count as the same
|
|
26
|
+
* idiom). `forbid` rules out the neighbours: a column flex container is a stack,
|
|
27
|
+
* not a row, and both start with the same two declarations.
|
|
28
|
+
*/
|
|
29
|
+
const UTILITIES = [
|
|
30
|
+
{
|
|
31
|
+
name: "tempest-center",
|
|
32
|
+
require: {
|
|
33
|
+
display: /^flex$/,
|
|
34
|
+
"align-items": /^center$/,
|
|
35
|
+
"justify-content": /^center$/,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "tempest-spread",
|
|
40
|
+
require: {
|
|
41
|
+
display: /^flex$/,
|
|
42
|
+
"align-items": /^center$/,
|
|
43
|
+
"justify-content": /^space-between$/,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "tempest-cluster",
|
|
48
|
+
require: {
|
|
49
|
+
display: /^flex$/,
|
|
50
|
+
"flex-wrap": /^wrap$/,
|
|
51
|
+
"align-items": /^center$/,
|
|
52
|
+
gap: null,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "tempest-stack",
|
|
57
|
+
require: { display: /^flex$/, "flex-direction": /^column$/, gap: null },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "tempest-row",
|
|
61
|
+
require: { display: /^flex$/, "align-items": /^center$/, gap: null },
|
|
62
|
+
forbid: { "flex-direction": /^column/, "flex-wrap": /^wrap$/ },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "tempest-truncate",
|
|
66
|
+
require: {
|
|
67
|
+
overflow: /^hidden$/,
|
|
68
|
+
"text-overflow": /^ellipsis$/,
|
|
69
|
+
"white-space": /^nowrap$/,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "tempest-grid-auto",
|
|
74
|
+
require: { display: /^grid$/, "grid-template-columns": /auto-fi[lt]l?/, gap: null },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "tempest-card",
|
|
78
|
+
require: {
|
|
79
|
+
background: null,
|
|
80
|
+
border: null,
|
|
81
|
+
"border-radius": null,
|
|
82
|
+
"box-shadow": null,
|
|
83
|
+
padding: null,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/** Lowercased property → value map of a block's declarations. */
|
|
89
|
+
function declMap(block) {
|
|
90
|
+
const map = new Map();
|
|
91
|
+
for (const decl of block.decls) {
|
|
92
|
+
map.set(decl.prop.toLowerCase(), decl.value.replace(/\s+/g, " ").trim().toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
return map;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The utility a rule re-implements, if any.
|
|
99
|
+
*
|
|
100
|
+
* @param {{ decls: Array<{ prop: string, value: string }> }} block
|
|
101
|
+
* @returns {string | null} Utility class name, without the dot.
|
|
102
|
+
*/
|
|
103
|
+
export function matchUtility(block) {
|
|
104
|
+
const map = declMap(block);
|
|
105
|
+
for (const utility of UTILITIES) {
|
|
106
|
+
const required = Object.entries(utility.require);
|
|
107
|
+
const ok = required.every(([prop, pattern]) => {
|
|
108
|
+
const value = map.get(prop);
|
|
109
|
+
if (value === undefined) return false;
|
|
110
|
+
return pattern ? pattern.test(value) : true;
|
|
111
|
+
});
|
|
112
|
+
if (!ok) continue;
|
|
113
|
+
const forbidden = Object.entries(utility.forbid ?? {}).some(([prop, pattern]) => {
|
|
114
|
+
const value = map.get(prop);
|
|
115
|
+
return value !== undefined && pattern.test(value);
|
|
116
|
+
});
|
|
117
|
+
if (!forbidden) return utility.name;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Short human list of the declarations a repeated block shares. */
|
|
123
|
+
function describe(block, max = 3) {
|
|
124
|
+
const parts = block.decls
|
|
125
|
+
.slice(0, max)
|
|
126
|
+
.map((d) => `${d.prop}: ${d.value.replace(/\s+/g, " ")}`);
|
|
127
|
+
const rest = block.decls.length - parts.length;
|
|
128
|
+
return `${parts.join("; ")}${rest > 0 ? `; …+${rest}` : ""}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `a.css`, `b.css` and 2 more — file list for a report line. */
|
|
132
|
+
function fileList(files, max = 3) {
|
|
133
|
+
const shown = files.slice(0, max).join(", ");
|
|
134
|
+
return files.length > max ? `${shown} and ${files.length - max} more` : shown;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Find declaration blocks that repeat across the project.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} params
|
|
141
|
+
* @param {Array<{ file: string, isModule: boolean, parsed: ReturnType<import("./parse.mjs").parseCss> }>} params.sheets
|
|
142
|
+
* @param {Set<string>} [params.utilities] - Utility classes the installed SDK ships.
|
|
143
|
+
* @returns {{ findings: Array<ReturnType<typeof finding>>, groups: Array<object> }}
|
|
144
|
+
*/
|
|
145
|
+
export function repetitionFindings({ sheets, utilities = new Set() }) {
|
|
146
|
+
const bySignature = new Map();
|
|
147
|
+
const byUtility = new Map();
|
|
148
|
+
|
|
149
|
+
for (const sheet of sheets) {
|
|
150
|
+
for (const block of sheet.parsed.blocks) {
|
|
151
|
+
if (block.kind !== "rule" || block.decls.length < MIN_DECLS) continue;
|
|
152
|
+
// A `.tempest-*` rule is the shared class, not a copy of it: counting
|
|
153
|
+
// `utilities.css` as one of the duplicates would tell the SDK's own
|
|
154
|
+
// stylesheet to stop re-implementing itself.
|
|
155
|
+
if (block.prelude.includes(".tempest-")) continue;
|
|
156
|
+
|
|
157
|
+
const signature = declSignature(block);
|
|
158
|
+
const group = bySignature.get(signature) ?? { occurrences: [], block };
|
|
159
|
+
group.occurrences.push({ file: sheet.file, line: block.line, prelude: block.prelude });
|
|
160
|
+
bySignature.set(signature, group);
|
|
161
|
+
|
|
162
|
+
const utility = matchUtility(block);
|
|
163
|
+
if (!utility || !utilities.has(utility)) continue;
|
|
164
|
+
const uGroup = byUtility.get(utility) ?? { occurrences: [] };
|
|
165
|
+
uGroup.occurrences.push({ file: sheet.file, line: block.line });
|
|
166
|
+
byUtility.set(utility, uGroup);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const findings = [];
|
|
171
|
+
const groups = [];
|
|
172
|
+
|
|
173
|
+
for (const [, group] of bySignature) {
|
|
174
|
+
const { occurrences, block } = group;
|
|
175
|
+
const files = [...new Set(occurrences.map((o) => o.file))];
|
|
176
|
+
if (occurrences.length < MIN_OCCURRENCES) continue;
|
|
177
|
+
if (files.length < 2 && occurrences.length < MIN_OCCURRENCES + 1) continue;
|
|
178
|
+
const first = occurrences[0];
|
|
179
|
+
groups.push({ kind: "block", occurrences, files, sample: describe(block) });
|
|
180
|
+
findings.push(
|
|
181
|
+
finding("global-candidate", {
|
|
182
|
+
file: first.file,
|
|
183
|
+
line: first.line,
|
|
184
|
+
message:
|
|
185
|
+
`${occurrences.length} rules in ${files.length} file(s) declare the same ${block.decls.length} ` +
|
|
186
|
+
`properties (${describe(block)}) — one global class beats ${occurrences.length} local copies. ` +
|
|
187
|
+
`In: ${fileList(files)}`,
|
|
188
|
+
extra: { occurrences: occurrences.length, files: files.length },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [utility, group] of byUtility) {
|
|
194
|
+
const { occurrences } = group;
|
|
195
|
+
const files = [...new Set(occurrences.map((o) => o.file))];
|
|
196
|
+
if (occurrences.length < MIN_OCCURRENCES || files.length < 2) continue;
|
|
197
|
+
const first = occurrences[0];
|
|
198
|
+
groups.push({ kind: "utility", utility, occurrences, files });
|
|
199
|
+
findings.push(
|
|
200
|
+
finding("utility-candidate", {
|
|
201
|
+
file: first.file,
|
|
202
|
+
line: first.line,
|
|
203
|
+
message:
|
|
204
|
+
`${occurrences.length} rules in ${files.length} file(s) re-implement \`.${utility}\` from ` +
|
|
205
|
+
'utilities.css — import "tempest-react-sdk/utilities.css" once and use the class ' +
|
|
206
|
+
`(keep the rule for whatever is genuinely local). In: ${fileList(files)}`,
|
|
207
|
+
extra: { occurrences: occurrences.length, files: files.length, utility },
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { findings, groups };
|
|
213
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { parseCss } from "./parse.mjs";
|
|
4
|
+
import { matchUtility, repetitionFindings } from "./repetition.mjs";
|
|
5
|
+
|
|
6
|
+
const ROW = `.row {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
const sheet = (file, text, isModule = true) => ({ file, isModule, parsed: parseCss(text) });
|
|
14
|
+
const rule = (text) => parseCss(text).blocks[0];
|
|
15
|
+
|
|
16
|
+
const UTILITIES = new Set([
|
|
17
|
+
"tempest-row",
|
|
18
|
+
"tempest-stack",
|
|
19
|
+
"tempest-center",
|
|
20
|
+
"tempest-cluster",
|
|
21
|
+
"tempest-truncate",
|
|
22
|
+
"tempest-card",
|
|
23
|
+
"tempest-grid-auto",
|
|
24
|
+
"tempest-spread",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
describe("matchUtility", () => {
|
|
28
|
+
it("matches a row regardless of how the gap is written", () => {
|
|
29
|
+
expect(matchUtility(rule(".a { display: flex; align-items: center; gap: 1rem; }"))).toBe(
|
|
30
|
+
"tempest-row",
|
|
31
|
+
);
|
|
32
|
+
expect(
|
|
33
|
+
matchUtility(
|
|
34
|
+
rule(".a { display: flex; align-items: center; gap: var(--tempest-space-3); }"),
|
|
35
|
+
),
|
|
36
|
+
).toBe("tempest-row");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("calls a column flex container a stack, not a row", () => {
|
|
40
|
+
expect(
|
|
41
|
+
matchUtility(
|
|
42
|
+
rule(
|
|
43
|
+
".a { display: flex; flex-direction: column; align-items: center; gap: 4px; }",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
).toBe("tempest-stack");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("prefers center and spread over row", () => {
|
|
50
|
+
expect(
|
|
51
|
+
matchUtility(
|
|
52
|
+
rule(".a { display: flex; align-items: center; justify-content: center; }"),
|
|
53
|
+
),
|
|
54
|
+
).toBe("tempest-center");
|
|
55
|
+
expect(
|
|
56
|
+
matchUtility(
|
|
57
|
+
rule(
|
|
58
|
+
".a { display: flex; align-items: center; justify-content: space-between; gap: 8px; }",
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
).toBe("tempest-spread");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("matches a wrapping cluster", () => {
|
|
65
|
+
expect(
|
|
66
|
+
matchUtility(
|
|
67
|
+
rule(".a { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }"),
|
|
68
|
+
),
|
|
69
|
+
).toBe("tempest-cluster");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("matches truncate and card", () => {
|
|
73
|
+
expect(
|
|
74
|
+
matchUtility(
|
|
75
|
+
rule(".a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }"),
|
|
76
|
+
),
|
|
77
|
+
).toBe("tempest-truncate");
|
|
78
|
+
expect(
|
|
79
|
+
matchUtility(
|
|
80
|
+
rule(
|
|
81
|
+
".a { background: #fff; border: 1px solid #eee; border-radius: 8px; box-shadow: 0 1px 2px #0001; padding: 16px; }",
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
).toBe("tempest-card");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns null for a rule that is not an idiom", () => {
|
|
88
|
+
expect(matchUtility(rule(".a { color: red; padding: 0; margin: 0; }"))).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("repetitionFindings", () => {
|
|
93
|
+
it("reports a block repeated across files as a global candidate", () => {
|
|
94
|
+
const { findings } = repetitionFindings({
|
|
95
|
+
sheets: [
|
|
96
|
+
sheet("a.module.css", ROW),
|
|
97
|
+
sheet("b.module.css", ROW),
|
|
98
|
+
sheet("c.module.css", ROW),
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const global = findings.find((f) => f.code === "global-candidate");
|
|
102
|
+
expect(global).toMatchObject({ severity: "info", file: "a.module.css", line: 1 });
|
|
103
|
+
expect(global.message).toContain("3 rules in 3 file(s)");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("names the utility when the SDK ships one", () => {
|
|
107
|
+
const { findings } = repetitionFindings({
|
|
108
|
+
sheets: [
|
|
109
|
+
sheet("a.module.css", ROW),
|
|
110
|
+
sheet("b.module.css", ROW),
|
|
111
|
+
sheet("c.module.css", ROW),
|
|
112
|
+
],
|
|
113
|
+
utilities: UTILITIES,
|
|
114
|
+
});
|
|
115
|
+
const utility = findings.find((f) => f.code === "utility-candidate");
|
|
116
|
+
expect(utility.message).toContain(".tempest-row");
|
|
117
|
+
expect(utility.extra ?? utility.utility).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("says nothing about a utility the installed SDK does not ship", () => {
|
|
121
|
+
const { findings } = repetitionFindings({
|
|
122
|
+
sheets: [
|
|
123
|
+
sheet("a.module.css", ROW),
|
|
124
|
+
sheet("b.module.css", ROW),
|
|
125
|
+
sheet("c.module.css", ROW),
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
expect(findings.some((f) => f.code === "utility-candidate")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("stays quiet below the occurrence threshold", () => {
|
|
132
|
+
const { findings } = repetitionFindings({
|
|
133
|
+
sheets: [sheet("a.module.css", ROW), sheet("b.module.css", ROW)],
|
|
134
|
+
utilities: UTILITIES,
|
|
135
|
+
});
|
|
136
|
+
expect(findings).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("stays quiet for a block with fewer than three declarations", () => {
|
|
140
|
+
const small = ".a { color: red; padding: 0; }\n";
|
|
141
|
+
const { findings } = repetitionFindings({
|
|
142
|
+
sheets: [sheet("a.css", small), sheet("b.css", small), sheet("c.css", small)],
|
|
143
|
+
});
|
|
144
|
+
expect(findings).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("needs a fourth copy before reporting repetition inside a single file", () => {
|
|
148
|
+
const three = `${ROW}.b {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.c {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n`;
|
|
149
|
+
expect(repetitionFindings({ sheets: [sheet("a.css", three)] }).findings).toEqual([]);
|
|
150
|
+
const four = `${three}.d {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n`;
|
|
151
|
+
expect(
|
|
152
|
+
repetitionFindings({ sheets: [sheet("a.css", four)] }).findings.map((f) => f.code),
|
|
153
|
+
).toEqual(["global-candidate"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("groups by declarations, not by selector name", () => {
|
|
157
|
+
const { groups } = repetitionFindings({
|
|
158
|
+
sheets: [
|
|
159
|
+
sheet("a.module.css", ROW),
|
|
160
|
+
sheet("b.module.css", ROW.replace(".row", ".line")),
|
|
161
|
+
sheet("c.module.css", ROW.replace(".row", ".bar")),
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
expect(groups.find((g) => g.kind === "block").occurrences).toHaveLength(3);
|
|
165
|
+
});
|
|
166
|
+
});
|