Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/__tests__/compiler/inline-variables.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { compileWithAutoDebug } from "react-native-css/jest";

// The inliner folds a property declared once, so a precedence test declares it twice to reach the runtime

/** The compiled rules for one class, or a throw naming the class that is missing. */
const rulesFor = (css: string, className: string) => {
const stylesheet = compileWithAutoDebug(css).stylesheet();
const rules = stylesheet.s?.find((rule) => rule[0] === className)?.[1];

if (!rules) {
throw new Error(`No rule found for .${className}`);
}

return rules;
};

/** The names a rule declares for itself, ignoring the compiler's own channels. */
const authoredVariableNames = (css: string, className: string) =>
rulesFor(css, className)
.flatMap((rule) => (typeof rule === "object" ? (rule.v ?? []) : []))
.map(([name]) => name)
.filter((name) => !name.startsWith("__rn-css-"));

/** True when the rule still has to resolve a `var()` at runtime. */
const readsAVariableAtRuntime = (css: string, className: string) =>
rulesFor(css, className).some(
(rule) => typeof rule === "object" && rule.dv === 1,
);

const CONSUMER = `.consumer { color: var(--my-var); }`;
const OWN = `.own { --my-var: blue; color: var(--my-var); }`;
const SECOND_DEFINITION = `.elsewhere { --my-var: seed; }`;

test("a singly-declared custom property is folded into the declaration that reads it", () => {
expect(rulesFor(OWN, "own")).toStrictEqual([
{ s: [1, 1], d: [{ color: "#00f" }], v: [["__rn-css-color", "#00f"]] },
]);
expect(authoredVariableNames(OWN, "own")).toStrictEqual([]);
expect(readsAVariableAtRuntime(OWN, "own")).toBe(false);
});

test("a second declaration anywhere in the sheet defeats the fold", () => {
const css = `${SECOND_DEFINITION} ${OWN}`;

expect(authoredVariableNames(css, "own")).toStrictEqual(["my-var"]);
expect(readsAVariableAtRuntime(css, "own")).toBe(true);
});

test("a rule that only reads a custom property declares none of its own", () => {
expect(authoredVariableNames(CONSUMER, "consumer")).toStrictEqual([]);
expect(readsAVariableAtRuntime(CONSUMER, "consumer")).toBe(true);
});

test("declaring and reading in separate rules leaves both halves in the sheet", () => {
const css = `${SECOND_DEFINITION} .declares { --my-var: blue; } ${CONSUMER}`;

expect(authoredVariableNames(css, "declares")).toStrictEqual(["my-var"]);
expect(readsAVariableAtRuntime(css, "declares")).toBe(false);
expect(authoredVariableNames(css, "consumer")).toStrictEqual([]);
expect(readsAVariableAtRuntime(css, "consumer")).toBe(true);
});
121 changes: 121 additions & 0 deletions src/__tests__/native/variables.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,124 @@ test("variable overriding with classes", () => {
const component = screen.getByTestId(testID);
expect(component.props.style).toStrictEqual({ color: "#f00" });
});

// Inheritance applies only where the cascade yields no declared value (css-cascade-4 §7.2); each sheet declares --my-var twice so it is not inlined
const DEFEAT_INLINING = `.elsewhere { --my-var: seed; }`;

test("an element's own rule outranks an ancestor's rule", () => {
registerCSS(`
${DEFEAT_INLINING}
.ancestor { --my-var: red; }
.own { --my-var: blue; color: var(--my-var); }
`);

render(
<View className="ancestor">
<View testID={testID} className="own" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "blue",
});
});

test("an element's own rule outranks a VariableContextProvider", () => {
registerCSS(`
${DEFEAT_INLINING}
.own { --my-var: blue; color: var(--my-var); }
`);

render(
<VariableContextProvider value={{ "--my-var": "red" }}>
<View testID={testID} className="own" />
</VariableContextProvider>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "blue",
});
});

test("an element's own rule outranks :root", () => {
registerCSS(`
:root { --my-var: red; }
${DEFEAT_INLINING}
.own { --my-var: blue; color: var(--my-var); }
`);

render(<View testID={testID} className="own" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "blue",
});
});

test("a changed inherited value does not displace the element's own declaration", () => {
registerCSS(`
${DEFEAT_INLINING}
.own { --my-var: blue; color: var(--my-var); }
`);

const tree = (inherited: string) => (
<VariableContextProvider value={{ "--my-var": inherited }}>
<View testID={testID} className="own" />
</VariableContextProvider>
);

render(tree("red"));
const component = screen.getByTestId(testID);
expect(component.props.style).toStrictEqual({ color: "blue" });

screen.rerender(tree("green"));
expect(component.props.style).toStrictEqual({ color: "blue" });
});

test("a declaring element's value still reaches its descendants", () => {
registerCSS(`
${DEFEAT_INLINING}
.own { --my-var: blue; color: var(--my-var); }
.reader { color: var(--my-var); }
`);

render(
<View testID="parent" className="own">
<View testID="child" className="reader" />
</View>,
);

expect(screen.getByTestId("parent").props.style).toStrictEqual({
color: "blue",
});
expect(screen.getByTestId("child").props.style).toStrictEqual({
color: "blue",
});
});

test("a universal variable applies where nothing above declares the name", () => {
registerCSS(`
* { --universal: 7px; }
.elsewhere { --universal: 99px; }
.uses-universal { width: var(--universal); }
`);

render(<View testID={testID} className="uses-universal" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 7 });
});

test("a universal declaration outranks an inherited one", () => {
registerCSS(`
* { --universal: 7px; }
.defines { --universal: 11px; }
.uses-universal { width: var(--universal); }
`);

render(
<View className="defines">
<View testID={testID} className="uses-universal" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 7 });
});
109 changes: 108 additions & 1 deletion src/__tests__/native/vars.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { render, screen } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";
import { vars } from "react-native-css/runtime";
import { VariableContextProvider, vars } from "react-native-css/runtime";

test("vars", () => {
registerCSS(
Expand Down Expand Up @@ -36,3 +36,110 @@ test("vars", () => {
color: "blue",
});
});

test("an element's own vars() outranks an inherited value", () => {
// Declared twice so the inliner cannot fold it
registerCSS(
`.decoy { --my-var: seed; }
.other-decoy { --my-var: seed2; }
.my-class { color: var(--my-var); }`,
);

render(
<VariableContextProvider value={{ "--my-var": "red" }}>
<View
testID={testID}
className="my-class"
style={vars({ "--my-var": "blue" })}
/>
</VariableContextProvider>,
);

// Inheritance applies only where the cascade yields no declared value (css-cascade-4 §7.2)
expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "blue",
});
});

test("an inherited value still applies when the element declares nothing", () => {
registerCSS(
`.decoy { --my-var: seed; }
.other-decoy { --my-var: seed2; }
.my-class { color: var(--my-var); }`,
);

render(
<VariableContextProvider value={{ "--my-var": "red" }}>
<View testID={testID} className="my-class" />
</VariableContextProvider>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "red",
});
});

test("vars() outranks the element's own class declaration", () => {
registerCSS(
`.decoy { --my-var: seed; }
.my-class { --my-var: blue; color: var(--my-var); }`,
);

render(
<VariableContextProvider value={{ "--my-var": "red" }}>
<View
testID={testID}
className="my-class"
style={vars({ "--my-var": "green" })}
/>
</VariableContextProvider>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "green",
});
});

test("siblings under one provider each resolve against their own declaration", () => {
registerCSS(
`.decoy { --my-var: seed; }
.other-decoy { --my-var: seed2; }
.my-class { color: var(--my-var); }`,
);

render(
<VariableContextProvider value={{ "--my-var": "red" }}>
<View testID="inherits" className="my-class" />
<View
testID="declares"
className="my-class"
style={vars({ "--my-var": "blue" })}
/>
</VariableContextProvider>,
);

expect(screen.getByTestId("inherits").props.style).toStrictEqual({
color: "red",
});
expect(screen.getByTestId("declares").props.style).toStrictEqual({
color: "blue",
});
});

test("vars() on a parent still reaches a descendant", () => {
registerCSS(
`.decoy { --my-var: seed; }
.other-decoy { --my-var: seed2; }
.my-class { color: var(--my-var); }`,
);

render(
<View className="my-class" style={vars({ "--my-var": "blue" })}>
<View testID={testID} className="my-class" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "blue",
});
});