`)
).toEqual(
renderTemplate(
div
link
)
);
});
test("collapse any spacing characters inside text", () => {
expect(
generateFragmentFromHtml(`
line
another line
`)
).toEqual(
renderTemplate({"line another line"})
);
});
test("collapse any spacing characters inside rich text", () => {
expect(
generateFragmentFromHtml(`
line another line
text
`)
).toEqual(
renderTemplate(
{"line "}another line text
)
);
});
test("preserve leading space in inline element following text without trailing space", () => {
expect(generateFragmentFromHtml(`
About Us
`)).toEqual(
renderTemplate(
{"About"}
{" Us"}
)
);
});
test("generate style attribute as local styles", () => {
expect(
generateFragmentFromHtml(`
`)
).toEqual(
renderTemplate(
)
);
});
test("generate nested css math functions as unparsed local styles", () => {
const value =
"clamp(1rem, calc(1rem + (2rem - 1rem) * ((100vw - 20rem) / (80rem - 20rem))), 2rem)";
const fragment = generateFragmentFromHtml(`
`)
).toEqual(
renderTemplate(
Hello
)
);
});
test("reuse same token when class appears on multiple elements", () => {
const cardToken = token(
"card",
css`
display: flex;
`
);
expect(
generateFragmentFromHtml(`
Hello
`)
).toEqual(
renderTemplate(
Hello
)
);
});
test("preserve unresolved class names as class prop", () => {
const cardToken = token(
"card",
css`
display: flex;
`
);
expect(
generateFragmentFromHtml(`
Hello
`)
).toEqual(
renderTemplate(
Hello
)
);
});
test("remove class prop entirely when all classes resolved to tokens", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
expect(fragment.props.find((p) => p.name === "class")).toBeUndefined();
});
test("combine inline style attribute with class token", () => {
const fragment = generateFragmentFromHtml(`
`);
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
});
test("keep style tag as html embed when it has no class rules", () => {
expect(
generateFragmentFromHtml(`
Hello
`)
).toEqual(
renderTemplate(
<>
<$.HtmlEmbed
ws:label="Style"
code={``}
/>
Hello
>
)
);
});
test("extract class rules and keep remaining rules as html embed", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// token for .card should be created
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
// remaining #hero rule should stay as HtmlEmbed
const htmlEmbed = fragment.instances.find(
(i) => i.component === "HtmlEmbed"
);
expect(htmlEmbed).toBeDefined();
const codeProp = fragment.props.find(
(p) => p.instanceId === htmlEmbed!.id && p.name === "code"
);
expect(codeProp).toBeDefined();
expect(codeProp!.type === "string" && codeProp!.value).toContain("#hero");
// class prop should not be set since .card was resolved
expect(fragment.props.find((p) => p.name === "class")).toBeUndefined();
});
test("extract classes from multiple style tags", () => {
const cardToken = token(
"card",
css`
display: flex;
`
);
const titleToken = token(
"title",
css`
font-size: 24px;
`
);
expect(
generateFragmentFromHtml(`
`)
).toEqual(
renderTemplate(
Hello
)
);
});
test("ignore element selectors in style tag", () => {
// element selectors like `div { }` should not become tokens
expect(
generateFragmentFromHtml(`
`)
).toEqual(
renderTemplate(
Hello
)
);
});
test("empty style tag produces no html embed", () => {
expect(
generateFragmentFromHtml(`
Hello
`)
).toEqual(renderTemplate(Hello));
});
test("class attribute without any style tag stays as prop", () => {
// existing behavior: class is just a prop when no style tag is present
expect(
generateFragmentFromHtml(`
Hello
`)
).toEqual(
renderTemplate(
Hello
)
);
});
test("style tag with only whitespace produces no html embed", () => {
expect(
generateFragmentFromHtml(`
`)
).toEqual(
renderTemplate(
One
Two
Three
)
);
});
test("element with both resolved and unresolved classes plus inline style", () => {
const fragment = generateFragmentFromHtml(`
`);
const tokenNames = fragment.styleSources
.filter((s) => s.type === "token")
.map((s) => (s as { name: string }).name);
expect(tokenNames).toContain("my-component");
expect(tokenNames).toContain("text-lg");
expect(tokenNames).toContain("bg_primary");
});
test("class defined in style tag but not used by any element", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// all class rules become tokens, even if not referenced by elements
const tokenNames = fragment.styleSources
.filter((s) => s.type === "token")
.map((s) => (s as { name: string }).name);
expect(tokenNames).toContain("used");
expect(tokenNames).toContain("unused");
// only "used" is assigned to the instance
const divInstance = fragment.instances.find((i) => i.tag === "div");
const sel = fragment.styleSourceSelections.find(
(s) => s.instanceId === divInstance?.id
);
const usedToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "used"
);
const unusedToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "unused"
);
expect(sel?.values).toContain(usedToken?.id);
expect(sel?.values).not.toContain(unusedToken?.id);
// no HtmlEmbed since all rules are class rules
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
});
test("duplicate class rules across multiple style tags are merged", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// should create a single token with both properties
const tokens = fragment.styleSources.filter((s) => s.type === "token");
expect(tokens).toHaveLength(1);
const tokenId = tokens[0].id;
const tokenStyles = fragment.styles.filter(
(s) => s.styleSourceId === tokenId
);
const properties = tokenStyles.map((s) => s.property);
expect(properties).toContain("display");
expect(properties).toContain("paddingTop");
});
test("script tag still becomes html embed alongside token extraction", () => {
const cardToken = token(
"card",
css`
display: flex;
`
);
expect(
generateFragmentFromHtml(`
Hello
`)
).toEqual(
renderTemplate(
<>
<$.HtmlEmbed
ws:label="Script"
clientOnly={true}
code={``}
/>
Hello
>
)
);
});
test("style tag with pseudo-class creates token with state", () => {
const fragment = generateFragmentFromHtml(`
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "btn"
);
expect(tokenSource).toBeDefined();
// base style
const baseStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "backgroundColor" &&
s.state === undefined
);
expect(baseStyle).toBeDefined();
// hover state style
const hoverStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "backgroundColor" &&
s.state === ":hover"
);
expect(hoverStyle).toBeDefined();
});
test("style tag with media query creates token with breakpoint styles", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "container"
);
expect(tokenSource).toBeDefined();
// should have styles on different breakpoints
const tokenStyles = fragment.styles.filter(
(s) => s.styleSourceId === tokenSource!.id
);
const breakpointIds = new Set(tokenStyles.map((s) => s.breakpointId));
// should have at least 2 different breakpoints (base + media query)
expect(breakpointIds.size).toBeGreaterThanOrEqual(2);
});
test("handle real-world html snippet with style tag", () => {
const heroToken = token(
"hero",
css`
display: flex;
flex-direction: column;
align-items: center;
padding: 64px;
`
);
const heroTitleToken = token(
"hero-title",
css`
font-size: 48px;
font-weight: bold;
line-height: 1.2;
`
);
const heroTextToken = token(
"hero-text",
css`
font-size: 18px;
color: gray;
`
);
expect(
generateFragmentFromHtml(`
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
// base style
const baseStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "display" &&
s.state === undefined
);
expect(baseStyle).toBeDefined();
// breakpoint style
const bpStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id && s.property === "flexDirection"
);
expect(bpStyle).toBeDefined();
expect(bpStyle!.breakpointId).not.toBe(baseStyle!.breakpointId);
// breakpoint should be created with minWidth
const bp = fragment.breakpoints.find(
(b) => b.id === bpStyle!.breakpointId
);
expect(bp).toBeDefined();
expect(bp!.minWidth).toBe(768);
});
test("extract class with max-width media query into breakpoint", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "container"
);
expect(tokenSource).toBeDefined();
const bpStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "paddingTop" &&
s.breakpointId !== "base"
);
expect(bpStyle).toBeDefined();
const bp = fragment.breakpoints.find(
(b) => b.id === bpStyle!.breakpointId
);
expect(bp).toBeDefined();
expect(bp!.maxWidth).toBe(480);
});
test("extract class across multiple media queries", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
const tokenStyles = fragment.styles.filter(
(s) => s.styleSourceId === tokenSource!.id
);
const breakpointIds = new Set(tokenStyles.map((s) => s.breakpointId));
// base + 768px + 1024px = 3 breakpoints
expect(breakpointIds.size).toBe(3);
});
test("media query with non-class selectors stays as html embed", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
true
);
expect(
fragment.styleSources.filter((s) => s.type === "token")
).toHaveLength(0);
});
test("media query with mixed class and non-class selectors", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// .card in media query should become token
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
// #hero in media query should remain as html embed
const htmlEmbed = fragment.instances.find(
(i) => i.component === "HtmlEmbed"
);
expect(htmlEmbed).toBeDefined();
});
test("same class in base and media query produces single token", () => {
const fragment = generateFragmentFromHtml(`
`);
const tokens = fragment.styleSources.filter((s) => s.type === "token");
expect(tokens).toHaveLength(1);
expect(tokens[0].type === "token" && tokens[0].name).toBe("btn");
});
test("multiple classes in same media query", () => {
const fragment = generateFragmentFromHtml(`
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "tablet-only"
);
expect(tokenSource).toBeDefined();
const tabletStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "display" &&
s.breakpointId !== "base"
);
expect(tabletStyle).toBeDefined();
const bp = fragment.breakpoints.find(
(b) => b.id === tabletStyle!.breakpointId
);
expect(bp).toBeDefined();
// Combined width queries use both minWidth and maxWidth
expect(bp!.minWidth).toBe(768);
expect(bp!.maxWidth).toBe(1024);
expect(bp!.condition).toBeUndefined();
});
test("overlapping min/max-width breakpoints get rounded to non-overlapping values", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "a"
);
expect(tokenSource).toBeDefined();
const tokenStyles = fragment.styles.filter(
(s) => s.styleSourceId === tokenSource!.id && s.property === "fontSize"
);
// base + 2 range breakpoints = 3 styles
expect(tokenStyles).toHaveLength(3);
const rangeBreakpoints = tokenStyles
.filter((s) => s.breakpointId !== "base")
.map((s) => fragment.breakpoints.find((b) => b.id === s.breakpointId))
.filter(Boolean)
.sort((a, b) => (a!.minWidth ?? 0) - (b!.minWidth ?? 0));
expect(rangeBreakpoints).toHaveLength(2);
// ranges should not overlap: first.maxWidth < second.minWidth
const first = rangeBreakpoints[0]!;
const second = rangeBreakpoints[1]!;
expect(first.maxWidth).toBeDefined();
expect(second.minWidth).toBeDefined();
expect(first.maxWidth!).toBeLessThan(second.minWidth!);
});
test("non-overlapping range breakpoints stay unchanged", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "a"
);
expect(tokenSource).toBeDefined();
const rangeBreakpoints = fragment.breakpoints
.filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)
.sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));
expect(rangeBreakpoints).toHaveLength(2);
// values should remain exactly as authored since they don't overlap
expect(rangeBreakpoints[0].minWidth).toBe(400);
expect(rangeBreakpoints[0].maxWidth).toBe(700);
expect(rangeBreakpoints[1].minWidth).toBe(800);
expect(rangeBreakpoints[1].maxWidth).toBe(1200);
});
test("adjacent range breakpoints (touching) stay unchanged", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const rangeBreakpoints = fragment.breakpoints
.filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)
.sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));
expect(rangeBreakpoints).toHaveLength(2);
// touching but not overlapping — keep as is
expect(rangeBreakpoints[0].maxWidth).toBe(799);
expect(rangeBreakpoints[1].minWidth).toBe(800);
});
test("overlapping range: first.maxWidth rounded down to second.minWidth - 1", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const rangeBreakpoints = fragment.breakpoints
.filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)
.sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));
expect(rangeBreakpoints).toHaveLength(2);
// first range's maxWidth should be adjusted to not overlap second's minWidth
expect(rangeBreakpoints[0].minWidth).toBe(600);
expect(rangeBreakpoints[0].maxWidth!).toBeLessThan(
rangeBreakpoints[1].minWidth!
);
// second range should keep its original values
expect(rangeBreakpoints[1].minWidth).toBe(900);
expect(rangeBreakpoints[1].maxWidth).toBe(1400);
});
test("three overlapping ranges all get adjusted", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const rangeBreakpoints = fragment.breakpoints
.filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)
.sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));
expect(rangeBreakpoints).toHaveLength(3);
// no pair should overlap
expect(rangeBreakpoints[0].maxWidth!).toBeLessThan(
rangeBreakpoints[1].minWidth!
);
expect(rangeBreakpoints[1].maxWidth!).toBeLessThan(
rangeBreakpoints[2].minWidth!
);
});
test("exact duplicate range breakpoints get deduplicated", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// same range should reuse a single breakpoint
const rangeBreakpoints = fragment.breakpoints.filter(
(b) => b.minWidth !== undefined && b.maxWidth !== undefined
);
expect(rangeBreakpoints).toHaveLength(1);
expect(rangeBreakpoints[0].minWidth).toBe(768);
expect(rangeBreakpoints[0].maxWidth).toBe(1024);
});
test("range breakpoint overlapping with simple min-width breakpoint", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "a"
);
expect(tokenSource).toBeDefined();
const tokenStyles = fragment.styles.filter(
(s) => s.styleSourceId === tokenSource!.id && s.property === "fontSize"
);
// base + min-width + range = 3 breakpoints
expect(tokenStyles).toHaveLength(3);
// simple min-width breakpoint should exist
const minWidthBp = fragment.breakpoints.find(
(b) =>
b.minWidth !== undefined &&
b.maxWidth === undefined &&
b.condition === undefined
);
expect(minWidthBp).toBeDefined();
// range breakpoint should exist and not overlap min-width
const rangeBp = fragment.breakpoints.find(
(b) => b.minWidth !== undefined && b.maxWidth !== undefined
);
expect(rangeBp).toBeDefined();
// range's maxWidth must be less than the min-width breakpoint's minWidth
// so they don't overlap
expect(rangeBp!.maxWidth!).toBeLessThan(minWidthBp!.minWidth!);
});
test("range breakpoint overlapping with simple max-width breakpoint", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "a"
);
expect(tokenSource).toBeDefined();
const tokenStyles = fragment.styles.filter(
(s) => s.styleSourceId === tokenSource!.id && s.property === "fontSize"
);
// base + max-width + range = 3 breakpoints
expect(tokenStyles).toHaveLength(3);
// simple max-width breakpoint should exist
const maxWidthBp = fragment.breakpoints.find(
(b) =>
b.maxWidth !== undefined &&
b.minWidth === undefined &&
b.condition === undefined
);
expect(maxWidthBp).toBeDefined();
// range breakpoint should exist and not overlap max-width
const rangeBp = fragment.breakpoints.find(
(b) => b.minWidth !== undefined && b.maxWidth !== undefined
);
expect(rangeBp).toBeDefined();
// range's minWidth must be greater than the max-width breakpoint's maxWidth
expect(rangeBp!.minWidth!).toBeGreaterThan(maxWidthBp!.maxWidth!);
});
test("completely contained range gets clamped", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const rangeBreakpoints = fragment.breakpoints
.filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)
.sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));
expect(rangeBreakpoints).toHaveLength(2);
// the outer range should be split/adjusted so inner doesn't overlap
expect(rangeBreakpoints[0].maxWidth!).toBeLessThan(
rangeBreakpoints[1].minWidth!
);
});
test("combined width and feature uses condition breakpoint", () => {
const fragment = generateFragmentFromHtml(`
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "accessible"
);
expect(tokenSource).toBeDefined();
const conditionStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "fontSize" &&
s.breakpointId !== "base"
);
expect(conditionStyle).toBeDefined();
const bp = fragment.breakpoints.find(
(b) => b.id === conditionStyle!.breakpointId
);
expect(bp).toBeDefined();
expect(bp!.condition).toBeDefined();
// condition should contain both features
expect(bp!.condition).toContain("prefers-color-scheme");
expect(bp!.condition).toContain("prefers-contrast");
});
test("same condition breakpoint reused for multiple classes", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const cardToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
const textToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "text"
);
expect(cardToken).toBeDefined();
expect(textToken).toBeDefined();
// both dark styles should reference the same breakpoint
const cardDarkStyle = fragment.styles.find(
(s) =>
s.styleSourceId === cardToken!.id &&
s.property === "backgroundColor" &&
s.breakpointId !== "base"
);
const textDarkStyle = fragment.styles.find(
(s) =>
s.styleSourceId === textToken!.id &&
s.property === "color" &&
s.breakpointId !== "base"
);
expect(cardDarkStyle).toBeDefined();
expect(textDarkStyle).toBeDefined();
expect(cardDarkStyle!.breakpointId).toBe(textDarkStyle!.breakpointId);
});
});
describe("nested at-rules", () => {
test("nested @media rules extract class with flattened condition", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
// nested @media should flatten into a combined condition
const nestedStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "backgroundColor"
);
expect(nestedStyle).toBeDefined();
const bp = fragment.breakpoints.find(
(b) => b.id === nestedStyle!.breakpointId
);
expect(bp).toBeDefined();
// flattened condition should include both queries
expect(bp!.condition).toBeDefined();
});
test("deeply nested @media still extracts class tokens", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
const deepStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "paddingTop" &&
s.breakpointId !== "base"
);
expect(deepStyle).toBeDefined();
const bp = fragment.breakpoints.find(
(b) => b.id === deepStyle!.breakpointId
);
expect(bp).toBeDefined();
expect(bp!.condition).toBeDefined();
});
test("nested @media with same feature type combines correctly", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "responsive"
);
expect(tokenSource).toBeDefined();
// outer @media (min-width: 768px) should have simple breakpoint
const outerStyle = fragment.styles.find(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "fontSize" &&
s.breakpointId !== "base" &&
fragment.breakpoints.find(
(b) => b.id === s.breakpointId && b.minWidth === 768
)
);
expect(outerStyle).toBeDefined();
// nested @media (min-width: 1024px) inside @media (min-width: 768px)
// should still produce a breakpoint (the effective min-width is 1024px)
const innerStyles = fragment.styles.filter(
(s) =>
s.styleSourceId === tokenSource!.id &&
s.property === "fontSize" &&
s.breakpointId !== "base" &&
s.breakpointId !== outerStyle!.breakpointId
);
expect(innerStyles).toHaveLength(1);
});
test("@supports nested inside @media keeps class in embed", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// base class should become token
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "grid"
);
expect(tokenSource).toBeDefined();
// @supports should remain as html embed (not a media query)
const htmlEmbed = fragment.instances.find(
(i) => i.component === "HtmlEmbed"
);
expect(htmlEmbed).toBeDefined();
});
});
describe("selector edge cases", () => {
test("class with compound selector .card.active is extracted as combo token", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// compound selectors become combo tokens (Webflow-style naming)
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card.active"
);
expect(tokenSource).toBeDefined();
// no leftover for pure class rule
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
});
test("class with attribute selector .btn[disabled] is extracted with state", () => {
const fragment = generateFragmentFromHtml(`
`);
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "btn"
);
expect(tokenSource).toBeDefined();
// the style should have [disabled] state
const style = fragment.styles.find(
(s) => s.styleSourceId === tokenSource!.id
);
expect(style).toBeDefined();
expect(style!.state).toBe("[disabled]");
// no leftover
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
});
test("class with sibling combinator .a + .b is not extracted", () => {
const fragment = generateFragmentFromHtml(`
A
B
`);
expect(
fragment.styleSources.filter((s) => s.type === "token")
).toHaveLength(0);
});
test("class with general sibling .a ~ .b is not extracted", () => {
const fragment = generateFragmentFromHtml(`
A
B
`);
expect(
fragment.styleSources.filter((s) => s.type === "token")
).toHaveLength(0);
});
test(":root selector is extracted as local styles on the root instance, not as html embed", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// .card should become token
const tokenSource = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(tokenSource).toBeDefined();
// :root should NOT produce an html embed
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
// :root styles should be on the root instance selection
const rootSelection = fragment.styleSourceSelections.find(
(sel) => sel.instanceId === ":root"
);
expect(rootSelection).toBeDefined();
const rootStyleSourceId = rootSelection!.values[0];
const rootStyle = fragment.styles.find(
(s) => s.styleSourceId === rootStyleSourceId && s.property === "--color"
);
expect(rootStyle).toBeDefined();
});
test(":root only styles are extracted as local styles on root instance", () => {
const fragment = generateFragmentFromHtml(`
`);
// no html embed
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
// style on :root instance
const rootSelection = fragment.styleSourceSelections.find(
(sel) => sel.instanceId === ":root"
);
expect(rootSelection).toBeDefined();
const rootStyleSourceId = rootSelection!.values[0];
const rootStyle = fragment.styles.find(
(s) => s.styleSourceId === rootStyleSourceId && s.property === "color"
);
expect(rootStyle).toBeDefined();
expect(rootStyle!.value).toMatchObject({ type: "keyword", value: "red" });
});
test("cross-selector gradient vars stay as var() references in pasted tokens", () => {
const fragment = generateFragmentFromHtml(`
`);
expect(
fragment.styleSources.filter((s) => s.type === "token")
).toHaveLength(0);
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
true
);
});
});
describe("compound class selectors", () => {
test("compound .card.active extracts as combo token when element has both classes", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const cardToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
const comboToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card.active"
);
expect(cardToken).toBeDefined();
expect(comboToken).toBeDefined();
// instance should reference both tokens
const sel = fragment.styleSourceSelections.find(
(s) =>
s.values.includes(cardToken!.id) && s.values.includes(comboToken!.id)
);
expect(sel).toBeDefined();
});
test("compound .a.b.c extracts as triple combo token", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const comboToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "a.b.c"
);
expect(comboToken).toBeDefined();
const style = fragment.styles.find(
(s) => s.styleSourceId === comboToken!.id && s.property === "color"
);
expect(style).toBeDefined();
});
test("compound class not matched when element is missing a class", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
// token is created for all class rules, even if not applied to elements
const comboToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card.active"
);
expect(comboToken).toBeDefined();
// but NOT assigned to the div (missing "active" class)
const divInstance = fragment.instances.find((i) => i.tag === "div");
const sel = fragment.styleSourceSelections.find(
(s) => s.instanceId === divInstance?.id
);
// No selection for this instance, or if one exists, it shouldn't contain the combo token
expect(sel?.values ?? []).not.toContain(comboToken?.id);
});
test("compound class with :hover pseudo extracts correctly", () => {
const fragment = generateFragmentFromHtml(`
`);
const comboToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "btn.primary"
);
expect(comboToken).toBeDefined();
const style = fragment.styles.find(
(s) => s.styleSourceId === comboToken!.id
);
expect(style).toBeDefined();
expect(style!.state).toBe(":hover");
});
test("compound class inside @media query creates breakpoint style", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const comboToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card.featured"
);
expect(comboToken).toBeDefined();
// Should have both base and breakpoint styles
const comboStyles = fragment.styles.filter(
(s) => s.styleSourceId === comboToken!.id
);
expect(comboStyles.length).toBe(2);
const bpIds = new Set(comboStyles.map((s) => s.breakpointId));
expect(bpIds.size).toBe(2);
});
});
describe("attribute selectors as state", () => {
test(".btn[disabled] extracts with [disabled] state", () => {
const fragment = generateFragmentFromHtml(`
`);
const btnToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "btn"
);
expect(btnToken).toBeDefined();
const baseStyle = fragment.styles.find(
(s) => s.styleSourceId === btnToken!.id && s.state === undefined
);
expect(baseStyle).toBeDefined();
expect(baseStyle!.property).toBe("paddingTop");
const stateStyle = fragment.styles.find(
(s) => s.styleSourceId === btnToken!.id && s.state === "[disabled]"
);
expect(stateStyle).toBeDefined();
expect(stateStyle!.property).toBe("opacity");
});
test(".input[readonly] extracts with [readonly] state", () => {
const fragment = generateFragmentFromHtml(`
`);
const inputToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "input"
);
expect(inputToken).toBeDefined();
const stateStyle = fragment.styles.find(
(s) => s.styleSourceId === inputToken!.id && s.state === "[readonly]"
);
expect(stateStyle).toBeDefined();
});
test("attribute selector combined with pseudo keeps pseudo state", () => {
const fragment = generateFragmentFromHtml(`
`);
const btnToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "btn"
);
expect(btnToken).toBeDefined();
const hoverStyle = fragment.styles.find(
(s) => s.styleSourceId === btnToken!.id && s.state === ":hover"
);
expect(hoverStyle).toBeDefined();
const disabledStyle = fragment.styles.find(
(s) => s.styleSourceId === btnToken!.id && s.state === "[disabled]"
);
expect(disabledStyle).toBeDefined();
});
});
describe("mixed comma selectors (partial extraction)", () => {
test(".card, div {} extracts .card token, keeps div in leftover", () => {
const fragment = generateFragmentFromHtml(`
Hello
`);
const cardToken = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "card"
);
expect(cardToken).toBeDefined();
// div part should stay as html embed
const htmlEmbed = fragment.instances.find(
(i) => i.component === "HtmlEmbed"
);
expect(htmlEmbed).toBeDefined();
const codeProp = fragment.props.find(
(p) => p.instanceId === htmlEmbed!.id && p.name === "code"
);
expect(codeProp).toBeDefined();
// The leftover should only contain the div selector, not .card
expect(codeProp!.type === "string" && codeProp!.value).toContain("div");
expect(codeProp!.type === "string" && codeProp!.value).not.toMatch(
/\.card/
);
});
test(".a, .b, #id {} extracts .a and .b, keeps #id in leftover", () => {
const fragment = generateFragmentFromHtml(`
A
B
`);
const tokenA = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "a"
);
const tokenB = fragment.styleSources.find(
(s) => s.type === "token" && s.name === "b"
);
expect(tokenA).toBeDefined();
expect(tokenB).toBeDefined();
// #id should stay in embed
const htmlEmbed = fragment.instances.find(
(i) => i.component === "HtmlEmbed"
);
expect(htmlEmbed).toBeDefined();
});
test(".a, .b {} with all class selectors produces no leftover", () => {
const fragment = generateFragmentFromHtml(`
A
B
`);
// Both should become tokens
expect(
fragment.styleSources.filter((s) => s.type === "token")
).toHaveLength(2);
// No leftover embed
expect(fragment.instances.some((i) => i.component === "HtmlEmbed")).toBe(
false
);
});
});
describe("breakpoint labels", () => {
test("condition breakpoint gets human-readable label", () => {
const fragment = generateFragmentFromHtml(`