clava 0.4.0 → 0.4.2
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/CHANGELOG.md +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +120 -22
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/index.ts +179 -87
- package/src/refine-warning.ts +161 -0
- package/src/types.ts +2 -2
- package/src/utils.ts +31 -10
- package/tests/_utils.ts +6 -2
- package/tests/build.test.ts +82 -7
- package/tests/extend.test.ts +7 -2
- package/tests/refine-warning.test.ts +28 -0
- package/tests/refine.test.ts +326 -0
package/src/types.ts
CHANGED
|
@@ -76,11 +76,11 @@ type ComponentPropKey<R extends ComponentResult> =
|
|
|
76
76
|
|
|
77
77
|
// Key source types - what can be passed as additional parameters to splitProps
|
|
78
78
|
export type KeySourceArray = readonly string[];
|
|
79
|
-
export
|
|
79
|
+
export interface KeySourceComponent {
|
|
80
80
|
propKeys: readonly string[];
|
|
81
81
|
variantKeys: readonly string[];
|
|
82
82
|
getVariants: () => Record<string, unknown>;
|
|
83
|
-
}
|
|
83
|
+
}
|
|
84
84
|
export type KeySource = KeySourceArray | KeySourceComponent;
|
|
85
85
|
|
|
86
86
|
// Check if source is a component (has getVariants)
|
package/src/utils.ts
CHANGED
|
@@ -39,7 +39,9 @@ export function hyphenToCamel(str: string) {
|
|
|
39
39
|
}
|
|
40
40
|
// Fast path: no hyphen -> return as-is
|
|
41
41
|
let hyphenIndex = str.indexOf("-");
|
|
42
|
-
if (hyphenIndex === -1)
|
|
42
|
+
if (hyphenIndex === -1) {
|
|
43
|
+
return str;
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
let result = "";
|
|
45
47
|
let lastIndex = 0;
|
|
@@ -91,7 +93,9 @@ export function camelToHyphen(str: string) {
|
|
|
91
93
|
lastIndex = i + 1;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
if (lastIndex === 0)
|
|
96
|
+
if (lastIndex === 0) {
|
|
97
|
+
return str;
|
|
98
|
+
}
|
|
95
99
|
return result + str.slice(lastIndex);
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -102,7 +106,9 @@ export function camelToHyphen(str: string) {
|
|
|
102
106
|
* parseLengthValue("2em"); // "2em"
|
|
103
107
|
*/
|
|
104
108
|
export function parseLengthValue(value: string | number) {
|
|
105
|
-
if (typeof value === "string")
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
106
112
|
return `${value}px`;
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -135,7 +141,10 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
135
141
|
}
|
|
136
142
|
if (i >= len || styleString.charCodeAt(i) === 59) {
|
|
137
143
|
// No colon found - skip this declaration
|
|
138
|
-
if (i < len)
|
|
144
|
+
if (i < len) {
|
|
145
|
+
// Skip ';'.
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
139
148
|
continue;
|
|
140
149
|
}
|
|
141
150
|
let propEnd = i;
|
|
@@ -147,12 +156,17 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
147
156
|
}
|
|
148
157
|
if (propEnd === propStart) {
|
|
149
158
|
// Empty property - skip
|
|
150
|
-
while (i < len && styleString.charCodeAt(i) !== 59)
|
|
151
|
-
|
|
159
|
+
while (i < len && styleString.charCodeAt(i) !== 59) {
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
if (i < len) {
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
152
165
|
continue;
|
|
153
166
|
}
|
|
154
167
|
const property = styleString.slice(propStart, propEnd);
|
|
155
|
-
|
|
168
|
+
// Skip ':'.
|
|
169
|
+
i++;
|
|
156
170
|
// Skip whitespace before value
|
|
157
171
|
while (i < len) {
|
|
158
172
|
const c = styleString.charCodeAt(i);
|
|
@@ -160,14 +174,19 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
160
174
|
i++;
|
|
161
175
|
}
|
|
162
176
|
const valStart = i;
|
|
163
|
-
while (i < len && styleString.charCodeAt(i) !== 59)
|
|
177
|
+
while (i < len && styleString.charCodeAt(i) !== 59) {
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
164
180
|
let valEnd = i;
|
|
165
181
|
while (valEnd > valStart) {
|
|
166
182
|
const c = styleString.charCodeAt(valEnd - 1);
|
|
167
183
|
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
168
184
|
valEnd--;
|
|
169
185
|
}
|
|
170
|
-
if (i < len)
|
|
186
|
+
if (i < len) {
|
|
187
|
+
// Skip ';'.
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
171
190
|
if (valEnd === valStart) continue;
|
|
172
191
|
const value = styleString.slice(valStart, valEnd);
|
|
173
192
|
// CSS property names and values are dynamic - cast required for index access
|
|
@@ -229,7 +248,9 @@ export function styleValueToHTMLStyle(style: StyleValue): string {
|
|
|
229
248
|
if (!hasOwn.call(style, key)) continue;
|
|
230
249
|
const value = (style as Record<string, unknown>)[key];
|
|
231
250
|
if (value == null) continue;
|
|
232
|
-
if (result)
|
|
251
|
+
if (result) {
|
|
252
|
+
result += "; ";
|
|
253
|
+
}
|
|
233
254
|
result += camelToHyphen(key);
|
|
234
255
|
result += ": ";
|
|
235
256
|
result += value as string | number;
|
package/tests/_utils.ts
CHANGED
|
@@ -87,12 +87,16 @@ export function getModeComponent<
|
|
|
87
87
|
V extends Variants = {},
|
|
88
88
|
const E extends AnyComponent[] = [],
|
|
89
89
|
>(mode: M, component: CVComponent<V, E>) {
|
|
90
|
-
if (!mode)
|
|
90
|
+
if (!mode) {
|
|
91
|
+
return component;
|
|
92
|
+
}
|
|
91
93
|
return component[mode];
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
function getClass(props: ComponentResult) {
|
|
95
|
-
if ("class" in props)
|
|
97
|
+
if ("class" in props) {
|
|
98
|
+
return props.class;
|
|
99
|
+
}
|
|
96
100
|
return props.className;
|
|
97
101
|
}
|
|
98
102
|
|
package/tests/build.test.ts
CHANGED
|
@@ -1,18 +1,93 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { withPackageBuildLock } from "test-utils/build-lock";
|
|
8
|
+
import { build } from "vite";
|
|
6
9
|
import { expect, test } from "vitest";
|
|
7
10
|
|
|
8
11
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
12
|
const exec = promisify(execFile);
|
|
13
|
+
let buildPromise: Promise<unknown> | undefined;
|
|
14
|
+
|
|
15
|
+
function buildPackage() {
|
|
16
|
+
buildPromise ??= exec("pnpm", ["--dir", root, "build"]);
|
|
17
|
+
return buildPromise;
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
test("build preserves the production warning guard for consumers", async () => {
|
|
12
|
-
await
|
|
21
|
+
await withPackageBuildLock(async () => {
|
|
22
|
+
await buildPackage();
|
|
23
|
+
|
|
24
|
+
const code = await readFile(join(root, "dist/index.js"), "utf8");
|
|
25
|
+
expect(code).toMatch(
|
|
26
|
+
/warnRefineLimit[\s\S]*?process\.env\.NODE_ENV === "production"[\s\S]*?console\.warn\(/,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}, 60_000);
|
|
30
|
+
|
|
31
|
+
test("vite removes warning logic from the production bundle", async () => {
|
|
32
|
+
await withPackageBuildLock(async () => {
|
|
33
|
+
await buildPackage();
|
|
34
|
+
|
|
35
|
+
const tempDir = await mkdtemp(join(tmpdir(), "clava-vite-"));
|
|
36
|
+
try {
|
|
37
|
+
const entry = join(tempDir, "entry.js");
|
|
38
|
+
const bundle = join(tempDir, "dist/bundle.js");
|
|
39
|
+
const clavaUrl = pathToFileURL(join(root, "dist/index.js")).href;
|
|
40
|
+
|
|
41
|
+
await writeFile(
|
|
42
|
+
entry,
|
|
43
|
+
`
|
|
44
|
+
import { cv } from ${JSON.stringify(clavaUrl)};
|
|
45
|
+
|
|
46
|
+
export const button = cv({
|
|
47
|
+
variants: {
|
|
48
|
+
tone: {
|
|
49
|
+
primary: "primary",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
refine({ setVariants }) {
|
|
53
|
+
setVariants({ tone: "primary" });
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
button();
|
|
58
|
+
`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await build({
|
|
62
|
+
configFile: false,
|
|
63
|
+
logLevel: "silent",
|
|
64
|
+
root: tempDir,
|
|
65
|
+
mode: "production",
|
|
66
|
+
define: {
|
|
67
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
68
|
+
},
|
|
69
|
+
build: {
|
|
70
|
+
emptyOutDir: true,
|
|
71
|
+
lib: {
|
|
72
|
+
entry,
|
|
73
|
+
fileName: () => "bundle.js",
|
|
74
|
+
formats: ["es"],
|
|
75
|
+
},
|
|
76
|
+
minify: true,
|
|
77
|
+
outDir: "dist",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
13
80
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
81
|
+
const code = await readFile(bundle, "utf8");
|
|
82
|
+
expect(code).not.toContain("console.warn");
|
|
83
|
+
expect(code).not.toContain("Clava: Maximum refine iterations exceeded");
|
|
84
|
+
expect(code).not.toContain("Variant(s) that did not stabilize");
|
|
85
|
+
expect(code).not.toContain("Latest variant changes before warning");
|
|
86
|
+
expect(code).not.toContain("Component created at");
|
|
87
|
+
expect(code).not.toContain("captureStackTrace");
|
|
88
|
+
expect(code).not.toMatch(/\.warned\b|["']warned["']/);
|
|
89
|
+
} finally {
|
|
90
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
18
93
|
}, 60_000);
|
package/tests/extend.test.ts
CHANGED
|
@@ -333,8 +333,13 @@ describe("non-idempotent transformClass", () => {
|
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
function toUpperCase(className: string) {
|
|
337
|
+
return className.toUpperCase();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function toLowerCase(className: string) {
|
|
341
|
+
return className.toLowerCase();
|
|
342
|
+
}
|
|
338
343
|
|
|
339
344
|
describe("extend across `create()` factories", () => {
|
|
340
345
|
// The extend's own `transformClass` must apply to its own classes even when
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { formatCreationStack } from "../src/refine-warning.ts";
|
|
3
|
+
|
|
4
|
+
test("formatCreationStack returns the first app frame", () => {
|
|
5
|
+
const stack = [
|
|
6
|
+
"Error",
|
|
7
|
+
" at captureCreationFrame (/repo/packages/clava/src/refine-warning.ts:31:10)",
|
|
8
|
+
" at cv (/repo/packages/clava/src/index.ts:734:9)",
|
|
9
|
+
" at eval (/repo/app/src/button.ts:12:21)",
|
|
10
|
+
" at ModuleJob.run (node:internal/modules/esm/module_job:343:25)",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
13
|
+
expect(formatCreationStack({ stack })).toBe(
|
|
14
|
+
" at eval (/repo/app/src/button.ts:12:21)",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("formatCreationStack omits dependency and runtime frames", () => {
|
|
19
|
+
const stack = [
|
|
20
|
+
"Error",
|
|
21
|
+
" at captureCreationFrame (/repo/packages/clava/src/refine-warning.ts:31:10)",
|
|
22
|
+
" at cv(/repo/packages/clava/src/index.ts:734:9)",
|
|
23
|
+
" at eval (/repo/node_modules/package/index.js:1:1)",
|
|
24
|
+
" at ModuleJob.run (node:internal/modules/esm/module_job:343:25)",
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
expect(formatCreationStack({ stack })).toBeUndefined();
|
|
28
|
+
});
|
package/tests/refine.test.ts
CHANGED
|
@@ -125,6 +125,125 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test("refine converges with undefined setDefaultVariants", () => {
|
|
129
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
130
|
+
const base = cv({
|
|
131
|
+
variants: {
|
|
132
|
+
invert: { true: "invert" },
|
|
133
|
+
offset: (value: boolean | undefined) =>
|
|
134
|
+
value ? "offset" : undefined,
|
|
135
|
+
push: (value: number | undefined) =>
|
|
136
|
+
value === undefined ? undefined : `push-${value}`,
|
|
137
|
+
},
|
|
138
|
+
refine: ({ variants, setDefaultVariants }) => {
|
|
139
|
+
setDefaultVariants({
|
|
140
|
+
offset: !variants.invert,
|
|
141
|
+
push: variants.invert ? 20 : undefined,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const component = getModeComponent(mode, cv({ extend: [base] }));
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const props = component();
|
|
149
|
+
expect(getStyleClass(props)).toEqual({
|
|
150
|
+
class: cls("offset"),
|
|
151
|
+
});
|
|
152
|
+
expect(component.getVariants()).toEqual({
|
|
153
|
+
offset: true,
|
|
154
|
+
push: undefined,
|
|
155
|
+
});
|
|
156
|
+
expect(warn).not.toHaveBeenCalled();
|
|
157
|
+
} finally {
|
|
158
|
+
warn.mockRestore();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("refine converges with undefined setDefaultVariants in nested extends", () => {
|
|
163
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
164
|
+
const calls = {
|
|
165
|
+
layer: 0,
|
|
166
|
+
frame: 0,
|
|
167
|
+
control: 0,
|
|
168
|
+
};
|
|
169
|
+
const layer = cv({
|
|
170
|
+
variants: {
|
|
171
|
+
layer: { true: "layer" },
|
|
172
|
+
invert: { true: "invert" },
|
|
173
|
+
offset: (value: boolean | undefined) =>
|
|
174
|
+
value ? "offset" : undefined,
|
|
175
|
+
push: (value: number | undefined) =>
|
|
176
|
+
value === undefined ? undefined : `push-${value}`,
|
|
177
|
+
},
|
|
178
|
+
defaultVariants: {
|
|
179
|
+
layer: true,
|
|
180
|
+
},
|
|
181
|
+
refine: ({ variants, setDefaultVariants }) => {
|
|
182
|
+
calls.layer += 1;
|
|
183
|
+
setDefaultVariants({
|
|
184
|
+
offset: !variants.invert,
|
|
185
|
+
push: variants.invert ? 20 : undefined,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const frame = cv({
|
|
190
|
+
extend: [layer],
|
|
191
|
+
variants: {
|
|
192
|
+
frame: { true: "frame" },
|
|
193
|
+
},
|
|
194
|
+
defaultVariants: {
|
|
195
|
+
frame: true,
|
|
196
|
+
},
|
|
197
|
+
refine: () => {
|
|
198
|
+
calls.frame += 1;
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const control = cv({
|
|
202
|
+
extend: [frame],
|
|
203
|
+
variants: {
|
|
204
|
+
control: { true: "control" },
|
|
205
|
+
},
|
|
206
|
+
defaultVariants: {
|
|
207
|
+
control: true,
|
|
208
|
+
offset: true,
|
|
209
|
+
},
|
|
210
|
+
refine: () => {
|
|
211
|
+
calls.control += 1;
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const component = getModeComponent(mode, cv({ extend: [control] }));
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const props = component();
|
|
218
|
+
expect(getStyleClass(props)).toEqual({
|
|
219
|
+
class: cls("layer offset frame control"),
|
|
220
|
+
});
|
|
221
|
+
expect(calls).toEqual({
|
|
222
|
+
layer: 2,
|
|
223
|
+
frame: 2,
|
|
224
|
+
control: 2,
|
|
225
|
+
});
|
|
226
|
+
calls.layer = 0;
|
|
227
|
+
calls.frame = 0;
|
|
228
|
+
calls.control = 0;
|
|
229
|
+
expect(component.getVariants()).toEqual({
|
|
230
|
+
layer: true,
|
|
231
|
+
offset: true,
|
|
232
|
+
push: undefined,
|
|
233
|
+
frame: true,
|
|
234
|
+
control: true,
|
|
235
|
+
});
|
|
236
|
+
expect(calls).toEqual({
|
|
237
|
+
layer: 2,
|
|
238
|
+
frame: 2,
|
|
239
|
+
control: 2,
|
|
240
|
+
});
|
|
241
|
+
expect(warn).not.toHaveBeenCalled();
|
|
242
|
+
} finally {
|
|
243
|
+
warn.mockRestore();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
128
247
|
test("refine with setDefaultVariants", () => {
|
|
129
248
|
const component = getModeComponent(
|
|
130
249
|
mode,
|
|
@@ -714,6 +833,19 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
714
833
|
expect(warn).toHaveBeenCalledWith(
|
|
715
834
|
expect.stringContaining("Maximum refine iterations exceeded"),
|
|
716
835
|
);
|
|
836
|
+
expect(warn).toHaveBeenCalledWith(
|
|
837
|
+
expect.stringMatching(
|
|
838
|
+
/Variant\(s\) that did not stabilize: [^\n]*\bsize\b/,
|
|
839
|
+
),
|
|
840
|
+
);
|
|
841
|
+
expect(warn).toHaveBeenCalledWith(
|
|
842
|
+
expect.stringMatching(
|
|
843
|
+
/Latest variant changes before warning: [^\n]*\bsize: "(sm|lg)" -> "(sm|lg)"/,
|
|
844
|
+
),
|
|
845
|
+
);
|
|
846
|
+
expect(warn).toHaveBeenCalledWith(
|
|
847
|
+
expect.stringContaining("Component created at:"),
|
|
848
|
+
);
|
|
717
849
|
} finally {
|
|
718
850
|
warn.mockRestore();
|
|
719
851
|
}
|
|
@@ -737,6 +869,19 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
737
869
|
expect(warn).toHaveBeenCalledWith(
|
|
738
870
|
expect.stringContaining("Maximum refine iterations exceeded"),
|
|
739
871
|
);
|
|
872
|
+
expect(warn).toHaveBeenCalledWith(
|
|
873
|
+
expect.stringMatching(
|
|
874
|
+
/Variant\(s\) that did not stabilize: [^\n]*\bsize\b/,
|
|
875
|
+
),
|
|
876
|
+
);
|
|
877
|
+
expect(warn).toHaveBeenCalledWith(
|
|
878
|
+
expect.stringMatching(
|
|
879
|
+
/Latest variant changes before warning: [^\n]*\bsize: "(sm|lg)" -> "(sm|lg)"/,
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
expect(warn).toHaveBeenCalledWith(
|
|
883
|
+
expect.stringContaining("Component created at:"),
|
|
884
|
+
);
|
|
740
885
|
} finally {
|
|
741
886
|
warn.mockRestore();
|
|
742
887
|
}
|
|
@@ -765,6 +910,187 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
765
910
|
}
|
|
766
911
|
});
|
|
767
912
|
|
|
913
|
+
test("refine warning names the variant key that did not stabilize", () => {
|
|
914
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
915
|
+
const component = getModeComponent(
|
|
916
|
+
mode,
|
|
917
|
+
cv({
|
|
918
|
+
variants: {
|
|
919
|
+
size: { sm: "sm", lg: "lg" },
|
|
920
|
+
color: { red: "red", blue: "blue" },
|
|
921
|
+
},
|
|
922
|
+
defaultVariants: { size: "sm", color: "red" },
|
|
923
|
+
refine: ({ variants, setVariants }) => {
|
|
924
|
+
setVariants({ size: variants.size === "sm" ? "lg" : "sm" });
|
|
925
|
+
},
|
|
926
|
+
}),
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
component();
|
|
931
|
+
expect(warn).toHaveBeenCalledWith(
|
|
932
|
+
expect.stringMatching(
|
|
933
|
+
/Variant\(s\) that did not stabilize: [^\n]*\bsize\b/,
|
|
934
|
+
),
|
|
935
|
+
);
|
|
936
|
+
expect(warn).toHaveBeenCalledWith(
|
|
937
|
+
expect.not.stringMatching(
|
|
938
|
+
/Variant\(s\) that did not stabilize: [^\n]*\bcolor\b/,
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
} finally {
|
|
942
|
+
warn.mockRestore();
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("refine warning reports keys that oscillate at different cadences", () => {
|
|
947
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
948
|
+
// `size` flips every iteration, `color` flips every other iteration, so
|
|
949
|
+
// the final pair may agree on one of them — the warning must still name
|
|
950
|
+
// both keys because each contributed to a transition.
|
|
951
|
+
const component = getModeComponent(
|
|
952
|
+
mode,
|
|
953
|
+
cv({
|
|
954
|
+
variants: {
|
|
955
|
+
size: { sm: "sm", lg: "lg" },
|
|
956
|
+
color: { red: "red", blue: "blue" },
|
|
957
|
+
},
|
|
958
|
+
defaultVariants: { size: "sm", color: "red" },
|
|
959
|
+
refine: ({ variants, setVariants }) => {
|
|
960
|
+
setVariants({
|
|
961
|
+
size: variants.size === "sm" ? "lg" : "sm",
|
|
962
|
+
color:
|
|
963
|
+
variants.size === "sm"
|
|
964
|
+
? variants.color === "red"
|
|
965
|
+
? "blue"
|
|
966
|
+
: "red"
|
|
967
|
+
: variants.color,
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
}),
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
component();
|
|
975
|
+
expect(warn).toHaveBeenCalledWith(
|
|
976
|
+
expect.stringMatching(
|
|
977
|
+
/Variant\(s\) that did not stabilize: [^\n]*\bsize\b/,
|
|
978
|
+
),
|
|
979
|
+
);
|
|
980
|
+
expect(warn).toHaveBeenCalledWith(
|
|
981
|
+
expect.stringMatching(
|
|
982
|
+
/Variant\(s\) that did not stabilize: [^\n]*\bcolor\b/,
|
|
983
|
+
),
|
|
984
|
+
);
|
|
985
|
+
expect(warn).toHaveBeenCalledWith(
|
|
986
|
+
expect.stringMatching(
|
|
987
|
+
/Latest variant changes before warning: [^\n]*\bsize\b[^\n]*\bcolor\b/,
|
|
988
|
+
),
|
|
989
|
+
);
|
|
990
|
+
} finally {
|
|
991
|
+
warn.mockRestore();
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
test("refine warning includes the component creation stack", () => {
|
|
996
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
997
|
+
const component = getModeComponent(
|
|
998
|
+
mode,
|
|
999
|
+
cv({
|
|
1000
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
1001
|
+
defaultVariants: { size: "sm" },
|
|
1002
|
+
refine: ({ variants, setVariants }) => {
|
|
1003
|
+
setVariants({ size: variants.size === "sm" ? "lg" : "sm" });
|
|
1004
|
+
},
|
|
1005
|
+
}),
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
try {
|
|
1009
|
+
component();
|
|
1010
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1011
|
+
expect.stringContaining("Component created at:"),
|
|
1012
|
+
);
|
|
1013
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1014
|
+
expect.stringContaining("refine.test.ts"),
|
|
1015
|
+
);
|
|
1016
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1017
|
+
expect.not.stringContaining("node_modules"),
|
|
1018
|
+
);
|
|
1019
|
+
} finally {
|
|
1020
|
+
warn.mockRestore();
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
test("refine warning fallback stack skips internal creation frames", () => {
|
|
1025
|
+
const ErrorWithCaptureStackTrace = Error as ErrorConstructor & {
|
|
1026
|
+
captureStackTrace?: (
|
|
1027
|
+
targetObject: object,
|
|
1028
|
+
constructorOpt?: Function,
|
|
1029
|
+
) => void;
|
|
1030
|
+
};
|
|
1031
|
+
const captureStackTrace = ErrorWithCaptureStackTrace.captureStackTrace;
|
|
1032
|
+
Reflect.deleteProperty(ErrorWithCaptureStackTrace, "captureStackTrace");
|
|
1033
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1034
|
+
const component = getModeComponent(
|
|
1035
|
+
mode,
|
|
1036
|
+
cv({
|
|
1037
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
1038
|
+
defaultVariants: { size: "sm" },
|
|
1039
|
+
refine: ({ variants, setVariants }) => {
|
|
1040
|
+
setVariants({ size: variants.size === "sm" ? "lg" : "sm" });
|
|
1041
|
+
},
|
|
1042
|
+
}),
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
component();
|
|
1047
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1048
|
+
expect.stringContaining("Component created at:"),
|
|
1049
|
+
);
|
|
1050
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1051
|
+
expect.stringContaining("refine.test.ts"),
|
|
1052
|
+
);
|
|
1053
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1054
|
+
expect.not.stringContaining("captureCreationFrame"),
|
|
1055
|
+
);
|
|
1056
|
+
} finally {
|
|
1057
|
+
if (captureStackTrace) {
|
|
1058
|
+
ErrorWithCaptureStackTrace.captureStackTrace = captureStackTrace;
|
|
1059
|
+
}
|
|
1060
|
+
warn.mockRestore();
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
test("refine warning omits the creation stack when cv ran in production", () => {
|
|
1065
|
+
let captured = "";
|
|
1066
|
+
const warn = vi
|
|
1067
|
+
.spyOn(console, "warn")
|
|
1068
|
+
.mockImplementation((message: unknown) => {
|
|
1069
|
+
captured = String(message);
|
|
1070
|
+
});
|
|
1071
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
1072
|
+
const component = getModeComponent(
|
|
1073
|
+
mode,
|
|
1074
|
+
cv({
|
|
1075
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
1076
|
+
defaultVariants: { size: "sm" },
|
|
1077
|
+
refine: ({ variants, setVariants }) => {
|
|
1078
|
+
setVariants({ size: variants.size === "sm" ? "lg" : "sm" });
|
|
1079
|
+
},
|
|
1080
|
+
}),
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
try {
|
|
1084
|
+
vi.unstubAllEnvs();
|
|
1085
|
+
component();
|
|
1086
|
+
expect(captured).toContain("Maximum refine iterations exceeded");
|
|
1087
|
+
expect(captured).not.toContain("Component created at:");
|
|
1088
|
+
} finally {
|
|
1089
|
+
vi.unstubAllEnvs();
|
|
1090
|
+
warn.mockRestore();
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
|
|
768
1094
|
test("refine setDefaultVariants when explicitly passing undefined", () => {
|
|
769
1095
|
const component = getModeComponent(
|
|
770
1096
|
mode,
|