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
55 changes: 55 additions & 0 deletions src/__tests__/compiler/variable-cycles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { compile } from "react-native-css/compiler";

function firstDeclaration(css: string, className: string): unknown {
const entry = (compile(css).stylesheet().s ?? []).find(
([name]) => name === className,
);
if (!entry) {
throw new Error(`the compiler emitted no rule for .${className}`);
}
return entry[1][0]?.d;
}

const CYCLES = [
[
"a variable referencing itself",
`.t { --x: var(--x); color: var(--x) }`,
"x",
],
[
"two variables referencing each other",
`.t { --p: var(--q); --q: var(--p); color: var(--p) }`,
"p",
],
[
"a three-step cycle",
`.t { --a: var(--b); --b: var(--c); --c: var(--a); color: var(--a) }`,
"a",
],
] as const;

describe("a variable cycle is left for the runtime rather than inlined", () => {
test("the census is not empty, so the cases below are not vacuous", () => {
expect(CYCLES.length).toBeGreaterThan(0);
});

test.each(CYCLES)("%s", (_label, css, variableName) => {
expect(firstDeclaration(css, "t")).toStrictEqual([
[[{}, "var", variableName, 1], "color", 1],
]);
});
});

describe("a variable with no cycle is still inlined", () => {
test("a direct value substitutes", () => {
expect(
firstDeclaration(`.t { --n: red; color: var(--n) }`, "t"),
).toStrictEqual([{ color: "#f00" }]);
});

test("a chain substitutes through every step", () => {
expect(
firstDeclaration(`.t { --a: red; --b: var(--a); color: var(--b) }`, "t"),
).toStrictEqual([{ color: "#f00" }]);
});
});
99 changes: 99 additions & 0 deletions src/__tests__/native/variable-cycles.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS } from "react-native-css/jest";
import { useUnstableNativeVariable } from "react-native-css/native";

// Current CSS substitution rules ignore unused fallback cycles.
// https://github.com/w3c/csswg-drafts/issues/11500
Expand Down Expand Up @@ -47,3 +48,101 @@ test("repeated sibling variable references are not cycles", () => {
render(<View testID="sample" className="sample" />);
expect(screen.getByTestId("sample").props.style).toStrictEqual({ width: 40 });
});

const nested = (
<View className="parent">
<View className="mid">
<View testID="child" className="child" />
</View>
</View>
);

// A variable reaches a descendant unresolved, so these cycles close on the element that reads them
test.each([
["a variable whose value is itself", ".mid { --a: var(--a); }"],
[
"a variable reached again through a fallback",
".mid { --a: var(--nope, var(--a, blue)); }",
],
[
"two variables that name each other",
".mid { --a: var(--b); --b: var(--a); }",
],
[
"one variable reached again from two branches of one value",
".mid { --a: var(--b) var(--c); --b: var(--a); --c: var(--a); }",
],
])("%s, inherited, drops the reading declaration", (_name, cycle) => {
registerCSS(
`.parent { --a: red; --b: blue; --c: green; --unrelated: 1; }
${cycle}
.mid { --unrelated: 0.5; }
.child { color: var(--a); opacity: var(--unrelated); }`,
{ inlineVariables: false },
);
render(nested);
expect(screen.getByTestId("child").props.style).toStrictEqual({
opacity: 0.5,
});
});

test("an inherited variable read twice in one declaration is not a cycle", () => {
registerCSS(
`.parent { --shadow-color: red; }
.child { box-shadow: var(--shadow-color) 1px 1px, var(--shadow-color) 2px 2px; }`,
{ inlineVariables: false },
);
render(nested);
expect(screen.getByTestId("child").props.style).toStrictEqual({
boxShadow: [
{ color: "red", offsetX: 1, offsetY: 1 },
{ color: "red", offsetX: 2, offsetY: 2 },
],
});
});

test("an inherited non-circular chain resolves through every step", () => {
registerCSS(
`.parent { --a: var(--b); --b: var(--c); --c: var(--d); --d: red; }
.child { color: var(--a); }`,
{ inlineVariables: false },
);
render(nested);
expect(screen.getByTestId("child").props.style).toStrictEqual({
color: "red",
});
});

test("a cycle in a variable declared once is cut by the compiler", () => {
registerCSS(
`.mid { --unrelated: 0.5; }
.child { --z: var(--z); width: var(--z); opacity: var(--unrelated); }`,
);
render(nested);
expect(screen.getByTestId("child").props.style).toStrictEqual({
opacity: 0.5,
});
});

test("useUnstableNativeVariable reads a cycle as no value", () => {
registerCSS(`.parent { --a: red; } .mid { --a: var(--a); }`, {
inlineVariables: false,
});

let read: unknown = "not read";

function Probe() {
read = useUnstableNativeVariable("--a");
return <View />;
}

render(
<View className="parent">
<View className="mid">
<Probe />
</View>
</View>,
);

expect(read).toBeUndefined();
});