@homebound/truss 2.7.0 → 2.8.1
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/build/plugin/index.js +38 -10
- package/build/plugin/index.js.map +1 -1
- package/build/runtime.js +23 -10
- package/build/runtime.js.map +1 -1
- package/build/vitest.d.ts +7 -4
- package/build/vitest.js +23 -11
- package/build/vitest.js.map +1 -1
- package/package.json +1 -1
package/build/runtime.js
CHANGED
|
@@ -7,6 +7,8 @@ var TrussDebugInfo = class {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
var shouldValidateTrussStyleValues = resolveShouldValidateTrussStyleValues();
|
|
10
|
+
var TRUSS_CSS_CHUNKS = "__trussCssChunks__";
|
|
11
|
+
var trussStyleElement = null;
|
|
10
12
|
function trussProps(...hashes) {
|
|
11
13
|
const merged = {};
|
|
12
14
|
for (const hash of hashes) {
|
|
@@ -60,17 +62,15 @@ function mergeProps(explicitClassName, explicitStyle, ...hashes) {
|
|
|
60
62
|
return result;
|
|
61
63
|
}
|
|
62
64
|
function __injectTrussCSS(cssText) {
|
|
63
|
-
if (typeof document === "undefined") return;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
document.head.appendChild(style);
|
|
70
|
-
}
|
|
71
|
-
if (!style.textContent?.includes(cssText)) {
|
|
72
|
-
style.textContent = (style.textContent ?? "") + cssText;
|
|
65
|
+
if (typeof document === "undefined" || cssText.length === 0) return;
|
|
66
|
+
const style = getOrCreateTrussStyleElement();
|
|
67
|
+
const injectedChunks = style[TRUSS_CSS_CHUNKS] ??= /* @__PURE__ */ new Set();
|
|
68
|
+
if (injectedChunks.has(cssText) || style.textContent?.includes(cssText)) {
|
|
69
|
+
injectedChunks.add(cssText);
|
|
70
|
+
return;
|
|
73
71
|
}
|
|
72
|
+
injectedChunks.add(cssText);
|
|
73
|
+
style.textContent = (style.textContent ?? "") + cssText;
|
|
74
74
|
}
|
|
75
75
|
function assertValidTrussStyleValue(key, value) {
|
|
76
76
|
if (typeof value === "string") return;
|
|
@@ -101,6 +101,19 @@ function resolveShouldValidateTrussStyleValues() {
|
|
|
101
101
|
}
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
|
+
function getOrCreateTrussStyleElement() {
|
|
105
|
+
const id = "data-truss";
|
|
106
|
+
if (trussStyleElement?.ownerDocument === document && trussStyleElement.isConnected) {
|
|
107
|
+
return trussStyleElement;
|
|
108
|
+
}
|
|
109
|
+
const style = document.querySelector(`style[${id}]`) ?? document.createElement("style");
|
|
110
|
+
if (!style.isConnected) {
|
|
111
|
+
style.setAttribute(id, "");
|
|
112
|
+
document.head.appendChild(style);
|
|
113
|
+
}
|
|
114
|
+
trussStyleElement = style;
|
|
115
|
+
return trussStyleElement;
|
|
116
|
+
}
|
|
104
117
|
export {
|
|
105
118
|
TrussDebugInfo,
|
|
106
119
|
__injectTrussCSS,
|
package/build/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/** A compact source label for a Truss CSS expression, used in debug mode. */\nexport class TrussDebugInfo {\n /** I.e. `\"FileName.tsx:line\"` */\n readonly src: string;\n\n constructor(src: string) {\n this.src = src;\n }\n}\n\n/**\n * Space-separated atomic class names, or a variable tuple with class names + CSS variable map.\n *\n * In debug mode, the transform appends a TrussDebugInfo as an extra tuple element:\n * - static with debug: `[classNames, debugInfo]`\n * - variable with debug: `[classNames, vars, debugInfo]`\n */\nexport type TrussStyleValue =\n | string\n | [classNames: string, vars: Record<string, string>]\n | [classNames: string, debugInfo: TrussDebugInfo]\n | [classNames: string, vars: Record<string, string>, debugInfo: TrussDebugInfo];\n\n/** A property-keyed style hash where each key owns one logical CSS property. */\nexport type TrussStyleHash = Record<string, TrussStyleValue>;\n\nconst shouldValidateTrussStyleValues = resolveShouldValidateTrussStyleValues();\n\n/** Merge one or more Truss style hashes into `{ className, style?, data-truss-src? }`. */\nexport function trussProps(\n ...hashes: ReadonlyArray<TrussStyleHash | false | null | undefined>\n): Record<string, unknown> {\n const merged: Record<string, TrussStyleValue> = {};\n\n for (const hash of hashes) {\n if (!hash || typeof hash !== \"object\") continue;\n Object.assign(merged, hash);\n }\n\n const classNames: string[] = [];\n const inlineStyle: Record<string, string> = {};\n const debugSources: string[] = [];\n\n for (const [key, value] of Object.entries(merged)) {\n if (shouldValidateTrussStyleValues) assertValidTrussStyleValue(key, value);\n\n // __marker is a special key — its value is a marker class name, not a CSS property\n if (key === \"__marker\") {\n if (typeof value === \"string\") {\n classNames.push(value);\n }\n continue;\n }\n\n if (typeof value === \"string\") {\n // I.e. \"df\" or \"black blue_h\"\n classNames.push(value);\n continue;\n }\n\n // Tuple: [classNames, varsOrDebug?, maybeDebug?]\n classNames.push(value[0]);\n\n for (let i = 1; i < value.length; i++) {\n const el = value[i];\n if (el instanceof TrussDebugInfo) {\n debugSources.push(el.src);\n } else if (typeof el === \"object\" && el !== null) {\n Object.assign(inlineStyle, el);\n }\n }\n }\n\n const props: Record<string, unknown> = {\n className: classNames.join(\" \"),\n };\n\n if (Object.keys(inlineStyle).length > 0) {\n props.style = inlineStyle;\n }\n\n if (debugSources.length > 0) {\n props[\"data-truss-src\"] = [...new Set(debugSources)].join(\"; \");\n }\n\n return props;\n}\n\n/** Merge explicit className/style with Truss style hashes. */\nexport function mergeProps(\n explicitClassName: string | undefined,\n explicitStyle: Record<string, unknown> | undefined,\n ...hashes: ReadonlyArray<TrussStyleHash | false | null | undefined>\n): Record<string, unknown> {\n const result = trussProps(...hashes);\n\n if (explicitClassName) {\n result.className = `${explicitClassName} ${result.className ?? \"\"}`.trim();\n }\n\n if (explicitStyle) {\n result.style = { ...explicitStyle, ...(result.style as Record<string, unknown> | undefined) };\n }\n\n return result;\n}\n\n/**\n * Inject CSS text into the document for jsdom/test environments.\n *\n * In browser dev mode, CSS is served via the Vite virtual endpoint instead.\n */\nexport function __injectTrussCSS(cssText: string): void {\n if (typeof document === \"undefined\") return;\n\n const
|
|
1
|
+
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/** A compact source label for a Truss CSS expression, used in debug mode. */\nexport class TrussDebugInfo {\n /** I.e. `\"FileName.tsx:line\"` */\n readonly src: string;\n\n constructor(src: string) {\n this.src = src;\n }\n}\n\n/**\n * Space-separated atomic class names, or a variable tuple with class names + CSS variable map.\n *\n * In debug mode, the transform appends a TrussDebugInfo as an extra tuple element:\n * - static with debug: `[classNames, debugInfo]`\n * - variable with debug: `[classNames, vars, debugInfo]`\n */\nexport type TrussStyleValue =\n | string\n | [classNames: string, vars: Record<string, string>]\n | [classNames: string, debugInfo: TrussDebugInfo]\n | [classNames: string, vars: Record<string, string>, debugInfo: TrussDebugInfo];\n\n/** A property-keyed style hash where each key owns one logical CSS property. */\nexport type TrussStyleHash = Record<string, TrussStyleValue>;\n\nconst shouldValidateTrussStyleValues = resolveShouldValidateTrussStyleValues();\nconst TRUSS_CSS_CHUNKS = \"__trussCssChunks__\";\nlet trussStyleElement: TrussStyleElement | null = null;\n\n/** Merge one or more Truss style hashes into `{ className, style?, data-truss-src? }`. */\nexport function trussProps(\n ...hashes: ReadonlyArray<TrussStyleHash | false | null | undefined>\n): Record<string, unknown> {\n const merged: Record<string, TrussStyleValue> = {};\n\n for (const hash of hashes) {\n if (!hash || typeof hash !== \"object\") continue;\n Object.assign(merged, hash);\n }\n\n const classNames: string[] = [];\n const inlineStyle: Record<string, string> = {};\n const debugSources: string[] = [];\n\n for (const [key, value] of Object.entries(merged)) {\n if (shouldValidateTrussStyleValues) assertValidTrussStyleValue(key, value);\n\n // __marker is a special key — its value is a marker class name, not a CSS property\n if (key === \"__marker\") {\n if (typeof value === \"string\") {\n classNames.push(value);\n }\n continue;\n }\n\n if (typeof value === \"string\") {\n // I.e. \"df\" or \"black blue_h\"\n classNames.push(value);\n continue;\n }\n\n // Tuple: [classNames, varsOrDebug?, maybeDebug?]\n classNames.push(value[0]);\n\n for (let i = 1; i < value.length; i++) {\n const el = value[i];\n if (el instanceof TrussDebugInfo) {\n debugSources.push(el.src);\n } else if (typeof el === \"object\" && el !== null) {\n Object.assign(inlineStyle, el);\n }\n }\n }\n\n const props: Record<string, unknown> = {\n className: classNames.join(\" \"),\n };\n\n if (Object.keys(inlineStyle).length > 0) {\n props.style = inlineStyle;\n }\n\n if (debugSources.length > 0) {\n props[\"data-truss-src\"] = [...new Set(debugSources)].join(\"; \");\n }\n\n return props;\n}\n\n/** Merge explicit className/style with Truss style hashes. */\nexport function mergeProps(\n explicitClassName: string | undefined,\n explicitStyle: Record<string, unknown> | undefined,\n ...hashes: ReadonlyArray<TrussStyleHash | false | null | undefined>\n): Record<string, unknown> {\n const result = trussProps(...hashes);\n\n if (explicitClassName) {\n result.className = `${explicitClassName} ${result.className ?? \"\"}`.trim();\n }\n\n if (explicitStyle) {\n result.style = { ...explicitStyle, ...(result.style as Record<string, unknown> | undefined) };\n }\n\n return result;\n}\n\n/**\n * Inject CSS text into the document for jsdom/test environments.\n *\n * In browser dev mode, CSS is served via the Vite virtual endpoint instead.\n */\nexport function __injectTrussCSS(cssText: string): void {\n if (typeof document === \"undefined\" || cssText.length === 0) return;\n\n const style = getOrCreateTrussStyleElement();\n\n // Track exact injected chunks on the style node so repeated execution of the\n // test bootstrap or transformed modules does not append duplicate CSS text.\n const injectedChunks = (style[TRUSS_CSS_CHUNKS] ??= new Set<string>());\n if (injectedChunks.has(cssText) || style.textContent?.includes(cssText)) {\n injectedChunks.add(cssText);\n return;\n }\n\n injectedChunks.add(cssText);\n style.textContent = (style.textContent ?? \"\") + cssText;\n}\n\n/** Fail fast when `trussProps` receives a non-Truss style value. */\nfunction assertValidTrussStyleValue(key: string, value: unknown): asserts value is TrussStyleValue {\n if (typeof value === \"string\") return;\n if (Array.isArray(value) && typeof value[0] === \"string\") {\n for (let i = 1; i < value.length; i++) {\n const el = value[i];\n if (el instanceof TrussDebugInfo) continue;\n if (typeof el === \"object\" && el !== null && !Array.isArray(el)) continue;\n throw new TypeError(invalidTrussStyleValueMessage(key));\n }\n return;\n }\n throw new TypeError(invalidTrussStyleValueMessage(key));\n}\n\nfunction invalidTrussStyleValueMessage(key: string): string {\n return `Invalid Truss style value for \\`${key}\\`. trussProps only accepts generated Truss style hashes; use mergeProps for explicit className/style merging.`;\n}\n\n/** Enable validation in dev/test environments, but skip it in production. */\nfunction resolveShouldValidateTrussStyleValues(): boolean {\n if (typeof process !== \"undefined\" && typeof process.env.NODE_ENV === \"string\") {\n return process.env.NODE_ENV !== \"production\";\n }\n const viteEnv = (import.meta as ImportMeta & { env?: { DEV?: boolean; PROD?: boolean } }).env;\n if (typeof viteEnv?.DEV === \"boolean\") {\n return viteEnv.DEV;\n }\n if (typeof viteEnv?.PROD === \"boolean\") {\n return !viteEnv.PROD;\n }\n return false;\n}\n\nfunction getOrCreateTrussStyleElement(): TrussStyleElement {\n const id = \"data-truss\";\n if (trussStyleElement?.ownerDocument === document && trussStyleElement.isConnected) {\n return trussStyleElement;\n }\n\n const style = (document.querySelector(`style[${id}]`) as TrussStyleElement | null) ?? document.createElement(\"style\");\n if (!style.isConnected) {\n style.setAttribute(id, \"\");\n document.head.appendChild(style);\n }\n trussStyleElement = style;\n return trussStyleElement;\n}\n\ntype TrussStyleElement = HTMLStyleElement & {\n [TRUSS_CSS_CHUNKS]?: Set<string>;\n};\n"],"mappings":";AACO,IAAM,iBAAN,MAAqB;AAAA;AAAA,EAEjB;AAAA,EAET,YAAY,KAAa;AACvB,SAAK,MAAM;AAAA,EACb;AACF;AAkBA,IAAM,iCAAiC,sCAAsC;AAC7E,IAAM,mBAAmB;AACzB,IAAI,oBAA8C;AAG3C,SAAS,cACX,QACsB;AACzB,QAAM,SAA0C,CAAC;AAEjD,aAAW,QAAQ,QAAQ;AACzB,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,WAAO,OAAO,QAAQ,IAAI;AAAA,EAC5B;AAEA,QAAM,aAAuB,CAAC;AAC9B,QAAM,cAAsC,CAAC;AAC7C,QAAM,eAAyB,CAAC;AAEhC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,+BAAgC,4BAA2B,KAAK,KAAK;AAGzE,QAAI,QAAQ,YAAY;AACtB,UAAI,OAAO,UAAU,UAAU;AAC7B,mBAAW,KAAK,KAAK;AAAA,MACvB;AACA;AAAA,IACF;AAEA,QAAI,OAAO,UAAU,UAAU;AAE7B,iBAAW,KAAK,KAAK;AACrB;AAAA,IACF;AAGA,eAAW,KAAK,MAAM,CAAC,CAAC;AAExB,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAM,KAAK,MAAM,CAAC;AAClB,UAAI,cAAc,gBAAgB;AAChC,qBAAa,KAAK,GAAG,GAAG;AAAA,MAC1B,WAAW,OAAO,OAAO,YAAY,OAAO,MAAM;AAChD,eAAO,OAAO,aAAa,EAAE;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAiC;AAAA,IACrC,WAAW,WAAW,KAAK,GAAG;AAAA,EAChC;AAEA,MAAI,OAAO,KAAK,WAAW,EAAE,SAAS,GAAG;AACvC,UAAM,QAAQ;AAAA,EAChB;AAEA,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,gBAAgB,IAAI,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,EAAE,KAAK,IAAI;AAAA,EAChE;AAEA,SAAO;AACT;AAGO,SAAS,WACd,mBACA,kBACG,QACsB;AACzB,QAAM,SAAS,WAAW,GAAG,MAAM;AAEnC,MAAI,mBAAmB;AACrB,WAAO,YAAY,GAAG,iBAAiB,IAAI,OAAO,aAAa,EAAE,GAAG,KAAK;AAAA,EAC3E;AAEA,MAAI,eAAe;AACjB,WAAO,QAAQ,EAAE,GAAG,eAAe,GAAI,OAAO,MAA8C;AAAA,EAC9F;AAEA,SAAO;AACT;AAOO,SAAS,iBAAiB,SAAuB;AACtD,MAAI,OAAO,aAAa,eAAe,QAAQ,WAAW,EAAG;AAE7D,QAAM,QAAQ,6BAA6B;AAI3C,QAAM,iBAAkB,MAAM,gBAAgB,MAAM,oBAAI,IAAY;AACpE,MAAI,eAAe,IAAI,OAAO,KAAK,MAAM,aAAa,SAAS,OAAO,GAAG;AACvE,mBAAe,IAAI,OAAO;AAC1B;AAAA,EACF;AAEA,iBAAe,IAAI,OAAO;AAC1B,QAAM,eAAe,MAAM,eAAe,MAAM;AAClD;AAGA,SAAS,2BAA2B,KAAa,OAAkD;AACjG,MAAI,OAAO,UAAU,SAAU;AAC/B,MAAI,MAAM,QAAQ,KAAK,KAAK,OAAO,MAAM,CAAC,MAAM,UAAU;AACxD,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAM,KAAK,MAAM,CAAC;AAClB,UAAI,cAAc,eAAgB;AAClC,UAAI,OAAO,OAAO,YAAY,OAAO,QAAQ,CAAC,MAAM,QAAQ,EAAE,EAAG;AACjE,YAAM,IAAI,UAAU,8BAA8B,GAAG,CAAC;AAAA,IACxD;AACA;AAAA,EACF;AACA,QAAM,IAAI,UAAU,8BAA8B,GAAG,CAAC;AACxD;AAEA,SAAS,8BAA8B,KAAqB;AAC1D,SAAO,mCAAmC,GAAG;AAC/C;AAGA,SAAS,wCAAiD;AACxD,MAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,IAAI,aAAa,UAAU;AAC9E,WAAO,QAAQ,IAAI,aAAa;AAAA,EAClC;AACA,QAAM,UAAW,YAAyE;AAC1F,MAAI,OAAO,SAAS,QAAQ,WAAW;AACrC,WAAO,QAAQ;AAAA,EACjB;AACA,MAAI,OAAO,SAAS,SAAS,WAAW;AACtC,WAAO,CAAC,QAAQ;AAAA,EAClB;AACA,SAAO;AACT;AAEA,SAAS,+BAAkD;AACzD,QAAM,KAAK;AACX,MAAI,mBAAmB,kBAAkB,YAAY,kBAAkB,aAAa;AAClF,WAAO;AAAA,EACT;AAEA,QAAM,QAAS,SAAS,cAAc,SAAS,EAAE,GAAG,KAAkC,SAAS,cAAc,OAAO;AACpH,MAAI,CAAC,MAAM,aAAa;AACtB,UAAM,aAAa,IAAI,EAAE;AACzB,aAAS,KAAK,YAAY,KAAK;AAAA,EACjC;AACA,sBAAoB;AACpB,SAAO;AACT;","names":[]}
|
package/build/vitest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Supported style expectations for the matcher. */
|
|
2
|
-
type StyleExpectation =
|
|
2
|
+
type StyleExpectation = Record<string, string | number>;
|
|
3
3
|
/** Minimal subset of Vitest's matcher context used for error formatting. */
|
|
4
4
|
type MatcherContext = {
|
|
5
5
|
utils?: {
|
|
@@ -15,9 +15,12 @@ type MatcherResult = {
|
|
|
15
15
|
/**
|
|
16
16
|
* Assert that an element's computed style matches the provided CSS declarations.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* This matcher is for asserting concrete CSS properties like `color` or
|
|
19
|
+
* `margin-top`, not raw custom properties like `--color`.
|
|
20
|
+
*
|
|
21
|
+
* In jsdom, computed styles sometimes leave values as `var(--token)` instead of
|
|
22
|
+
* resolving them. We follow those references so `expect(el).toHaveStyle({ color:
|
|
23
|
+
* "blue" })` still works for class rules like `.color_var { color: var(--color) }`.
|
|
21
24
|
*/
|
|
22
25
|
declare function toHaveStyle(this: MatcherContext, received: unknown, expected: StyleExpectation): MatcherResult;
|
|
23
26
|
|
package/build/vitest.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/toHaveStyle.ts
|
|
2
|
+
var wholeValueCssVariablePattern = /^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/;
|
|
2
3
|
function toHaveStyle(received, expected) {
|
|
3
4
|
if (!isElementLike(received)) {
|
|
4
5
|
return {
|
|
@@ -6,7 +7,7 @@ function toHaveStyle(received, expected) {
|
|
|
6
7
|
message: () => `expected an Element, received ${printValue(this, "printReceived", received)}`
|
|
7
8
|
};
|
|
8
9
|
}
|
|
9
|
-
const expectedStyles = parseExpectedStyles(received, expected);
|
|
10
|
+
const expectedStyles = parseExpectedStyles(this, received, expected);
|
|
10
11
|
const mismatches = [];
|
|
11
12
|
for (const [property, expectedValue] of expectedStyles) {
|
|
12
13
|
const actualValue = getActualStyleValue(received, property);
|
|
@@ -32,26 +33,37 @@ function isElementLike(value) {
|
|
|
32
33
|
function toKebabCase(property) {
|
|
33
34
|
return property.startsWith("--") ? property : property.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
34
35
|
}
|
|
35
|
-
function parseExpectedStyles(el, expected) {
|
|
36
|
+
function parseExpectedStyles(ctx, el, expected) {
|
|
36
37
|
const probe = el.ownerDocument.createElement("div");
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
for (const [property, value] of Object.entries(expected)) {
|
|
39
|
+
if (property.startsWith("--")) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`toHaveStyle does not support custom property expectations like ${printValue(ctx, "printExpected", {
|
|
42
|
+
[property]: value
|
|
43
|
+
})}; assert the custom property directly instead.`
|
|
44
|
+
);
|
|
43
45
|
}
|
|
46
|
+
probe.style.setProperty(toKebabCase(property), String(value));
|
|
44
47
|
}
|
|
48
|
+
const styles = /* @__PURE__ */ new Map();
|
|
45
49
|
for (const property of Array.from(probe.style)) {
|
|
46
50
|
styles.set(property, probe.style.getPropertyValue(property).trim());
|
|
47
51
|
}
|
|
48
52
|
return styles;
|
|
49
53
|
}
|
|
50
54
|
function getActualStyleValue(el, property) {
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
const computedStyles = el.ownerDocument.defaultView.getComputedStyle(el);
|
|
56
|
+
const actualValue = computedStyles.getPropertyValue(property).trim();
|
|
57
|
+
return resolveWholeValueCssVariable(actualValue, computedStyles);
|
|
58
|
+
}
|
|
59
|
+
function resolveWholeValueCssVariable(value, computedStyles) {
|
|
60
|
+
const match = value.match(wholeValueCssVariablePattern);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return value;
|
|
53
63
|
}
|
|
54
|
-
|
|
64
|
+
const [, variableName, fallbackValue] = match;
|
|
65
|
+
const resolvedValue = computedStyles.getPropertyValue(variableName).trim();
|
|
66
|
+
return resolvedValue || fallbackValue?.trim() || value;
|
|
55
67
|
}
|
|
56
68
|
export {
|
|
57
69
|
toHaveStyle
|
package/build/vitest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/toHaveStyle.ts"],"sourcesContent":["/** Supported style expectations for the matcher. */\nexport type StyleExpectation =
|
|
1
|
+
{"version":3,"sources":["../src/toHaveStyle.ts"],"sourcesContent":["/** Supported style expectations for the matcher. */\nexport type StyleExpectation = Record<string, string | number>;\n\n/** Minimal subset of Vitest's matcher context used for error formatting. */\ntype MatcherContext = {\n utils?: {\n printExpected?: (value: unknown) => string;\n printReceived?: (value: unknown) => string;\n };\n};\n\n/** Standard matcher result shape returned to Vitest. */\ntype MatcherResult = {\n pass: boolean;\n message: () => string;\n};\n\n/**\n * Match whole-value CSS variable references, i.e. `var(--color)` or\n * `var(--color, red)`.\n */\nconst wholeValueCssVariablePattern = /^var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*(.+))?\\)$/;\n\n/**\n * Assert that an element's computed style matches the provided CSS declarations.\n *\n * This matcher is for asserting concrete CSS properties like `color` or\n * `margin-top`, not raw custom properties like `--color`.\n *\n * In jsdom, computed styles sometimes leave values as `var(--token)` instead of\n * resolving them. We follow those references so `expect(el).toHaveStyle({ color:\n * \"blue\" })` still works for class rules like `.color_var { color: var(--color) }`.\n */\nexport function toHaveStyle(this: MatcherContext, received: unknown, expected: StyleExpectation): MatcherResult {\n if (!isElementLike(received)) {\n return {\n pass: false,\n message: () => `expected an Element, received ${printValue(this, \"printReceived\", received)}`,\n };\n }\n\n const expectedStyles = parseExpectedStyles(this, received, expected);\n const mismatches: string[] = [];\n for (const [property, expectedValue] of expectedStyles) {\n const actualValue = getActualStyleValue(received, property);\n if (actualValue !== expectedValue) {\n mismatches.push(`${property}: expected ${expectedValue}, received ${actualValue || \"<empty>\"}`);\n }\n }\n\n return {\n pass: mismatches.length === 0,\n message: () => {\n const expectedLabel = printValue(this, \"printExpected\", expected);\n return mismatches.length === 0\n ? `expected element not to have style ${expectedLabel}`\n : `expected element to have style ${expectedLabel}\\n${mismatches.join(\"\\n\")}`;\n },\n };\n}\n\n/** Format matcher values using Vitest's printers when available. */\nfunction printValue(ctx: MatcherContext, kind: \"printExpected\" | \"printReceived\", value: unknown): string {\n return ctx.utils?.[kind]?.(value) ?? JSON.stringify(value);\n}\n\n/** Narrow an unknown matcher input to a DOM element-like object. */\nfunction isElementLike(value: unknown): value is {\n ownerDocument: Document;\n} {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"ownerDocument\" in value &&\n Boolean((value as { ownerDocument?: Document }).ownerDocument?.defaultView)\n );\n}\n\n/** Convert camelCase property names into CSS kebab-case. */\nfunction toKebabCase(property: string): string {\n return property.startsWith(\"--\") ? property : property.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);\n}\n\n/**\n * Normalize the expected style input into concrete CSS property/value pairs.\n *\n * Examples:\n * - `{ backgroundColor: \"#526675\" }` becomes `background-color: rgb(82, 102, 117)`\n * so it compares cleanly with `getComputedStyle(...)`.\n * - `{ marginTop: 0 }` becomes `margin-top: 0px`/`0`, matching the browser's\n * normalized CSS string output instead of the raw JS input.\n *\n * We do this by round-tripping through a real `CSSStyleDeclaration` on a probe\n * element instead of trying to hand-normalize CSS names and values ourselves.\n */\nfunction parseExpectedStyles(\n ctx: MatcherContext,\n el: { ownerDocument: Document },\n expected: StyleExpectation,\n): Map<string, string> {\n const probe = el.ownerDocument.createElement(\"div\");\n for (const [property, value] of Object.entries(expected)) {\n if (property.startsWith(\"--\")) {\n throw new Error(\n `toHaveStyle does not support custom property expectations like ${printValue(ctx, \"printExpected\", {\n [property]: value,\n })}; assert the custom property directly instead.`,\n );\n }\n probe.style.setProperty(toKebabCase(property), String(value));\n }\n const styles = new Map<string, string>();\n for (const property of Array.from(probe.style)) {\n styles.set(property, probe.style.getPropertyValue(property).trim());\n }\n return styles;\n}\n\n/**\n * Read the current value for a CSS property from the element under test.\n *\n * We intentionally read through `getComputedStyle(...)` because the matcher is\n * asserting the final applied property value, not whether the element happens\n * to have an inline style entry.\n *\n * Examples:\n * - `<div class=\"df\" />` with `.df { display: flex }` should compare as\n * `display: flex`, even though `el.style.display` is empty.\n * - `<div class=\"black\" />` with `.black { color: #353535 }` should compare as\n * `color: rgb(53, 53, 53)`, matching the browser's computed value.\n * - `<div class=\"color_var\" style=\"--color: blue\" />` with\n * `.color_var { color: var(--color) }` should compare as `color: blue`;\n * jsdom often returns `var(--color)` here, so we resolve that indirection\n * immediately after reading the computed value.\n */\nfunction getActualStyleValue(el: { style?: CSSStyleDeclaration; ownerDocument: Document }, property: string): string {\n const computedStyles = el.ownerDocument.defaultView!.getComputedStyle(el as Element);\n const actualValue = computedStyles.getPropertyValue(property).trim();\n return resolveWholeValueCssVariable(actualValue, computedStyles);\n}\n\n/**\n * Resolve a whole-value `var(--token)` reference using the element's current\n * custom properties. jsdom leaves these unresolved in many computed values.\n *\n * This is intentionally a single-hop lookup for cases like\n * `.color_var { color: var(--color) }` plus `el.style.setProperty(\"--color\", \"blue\")`.\n */\nfunction resolveWholeValueCssVariable(\n value: string,\n computedStyles: CSSStyleDeclaration,\n): string {\n const match = value.match(wholeValueCssVariablePattern);\n if (!match) {\n return value;\n }\n const [, variableName, fallbackValue] = match;\n const resolvedValue = computedStyles.getPropertyValue(variableName).trim();\n return resolvedValue || fallbackValue?.trim() || value;\n}\n"],"mappings":";AAqBA,IAAM,+BAA+B;AAY9B,SAAS,YAAkC,UAAmB,UAA2C;AAC9G,MAAI,CAAC,cAAc,QAAQ,GAAG;AAC5B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,MAAM,iCAAiC,WAAW,MAAM,iBAAiB,QAAQ,CAAC;AAAA,IAC7F;AAAA,EACF;AAEA,QAAM,iBAAiB,oBAAoB,MAAM,UAAU,QAAQ;AACnE,QAAM,aAAuB,CAAC;AAC9B,aAAW,CAAC,UAAU,aAAa,KAAK,gBAAgB;AACtD,UAAM,cAAc,oBAAoB,UAAU,QAAQ;AAC1D,QAAI,gBAAgB,eAAe;AACjC,iBAAW,KAAK,GAAG,QAAQ,cAAc,aAAa,cAAc,eAAe,SAAS,EAAE;AAAA,IAChG;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,WAAW,WAAW;AAAA,IAC5B,SAAS,MAAM;AACb,YAAM,gBAAgB,WAAW,MAAM,iBAAiB,QAAQ;AAChE,aAAO,WAAW,WAAW,IACzB,sCAAsC,aAAa,KACnD,kCAAkC,aAAa;AAAA,EAAK,WAAW,KAAK,IAAI,CAAC;AAAA,IAC/E;AAAA,EACF;AACF;AAGA,SAAS,WAAW,KAAqB,MAAyC,OAAwB;AACxG,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,KAAK,KAAK,UAAU,KAAK;AAC3D;AAGA,SAAS,cAAc,OAErB;AACA,SACE,OAAO,UAAU,YACjB,UAAU,QACV,mBAAmB,SACnB,QAAS,MAAuC,eAAe,WAAW;AAE9E;AAGA,SAAS,YAAY,UAA0B;AAC7C,SAAO,SAAS,WAAW,IAAI,IAAI,WAAW,SAAS,QAAQ,UAAU,CAAC,SAAS,IAAI,KAAK,YAAY,CAAC,EAAE;AAC7G;AAcA,SAAS,oBACP,KACA,IACA,UACqB;AACrB,QAAM,QAAQ,GAAG,cAAc,cAAc,KAAK;AAClD,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACxD,QAAI,SAAS,WAAW,IAAI,GAAG;AAC7B,YAAM,IAAI;AAAA,QACR,kEAAkE,WAAW,KAAK,iBAAiB;AAAA,UACjG,CAAC,QAAQ,GAAG;AAAA,QACd,CAAC,CAAC;AAAA,MACJ;AAAA,IACF;AACA,UAAM,MAAM,YAAY,YAAY,QAAQ,GAAG,OAAO,KAAK,CAAC;AAAA,EAC9D;AACA,QAAM,SAAS,oBAAI,IAAoB;AACvC,aAAW,YAAY,MAAM,KAAK,MAAM,KAAK,GAAG;AAC9C,WAAO,IAAI,UAAU,MAAM,MAAM,iBAAiB,QAAQ,EAAE,KAAK,CAAC;AAAA,EACpE;AACA,SAAO;AACT;AAmBA,SAAS,oBAAoB,IAA8D,UAA0B;AACnH,QAAM,iBAAiB,GAAG,cAAc,YAAa,iBAAiB,EAAa;AACnF,QAAM,cAAc,eAAe,iBAAiB,QAAQ,EAAE,KAAK;AACnE,SAAO,6BAA6B,aAAa,cAAc;AACjE;AASA,SAAS,6BACP,OACA,gBACQ;AACR,QAAM,QAAQ,MAAM,MAAM,4BAA4B;AACtD,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,CAAC,EAAE,cAAc,aAAa,IAAI;AACxC,QAAM,gBAAgB,eAAe,iBAAiB,YAAY,EAAE,KAAK;AACzE,SAAO,iBAAiB,eAAe,KAAK,KAAK;AACnD;","names":[]}
|