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

const RED = { color: "#f00" } as const;
const INHERITED_COLOR = [["__rn-css-color", "#f00"]] as const;

function rulesFor(css: string, className = "subject") {
return compile(css)
.stylesheet()
.s?.find(([name]) => name === className)?.[1];
}

describe("an ancestor identified by a condition alone", () => {
test("an attribute compiles to a container query carrying it", () => {
expect(
rulesFor(`[data-state="on"] .subject { color: red; }`),
).toStrictEqual([
{
s: [1, 2],
d: [RED],
v: INHERITED_COLOR,
cq: [{ a: [["d", "state", "=", "on"]] }],
},
]);
});

test("an attribute with no operation compiles to a presence query", () => {
expect(rulesFor(`[aria-busy] .subject { color: red; }`)).toStrictEqual([
{
s: [1, 2],
d: [RED],
v: INHERITED_COLOR,
cq: [{ a: [["a", "ariaBusy"]] }],
},
]);
});

test("every pseudo-class the builder answers compiles to a container query", () => {
expect(rulesFor(`:hover .subject { color: red; }`)).toStrictEqual([
{ s: [1, 2], d: [RED], v: INHERITED_COLOR, cq: [{ p: { h: 1 } }] },
]);
expect(rulesFor(`:active .subject { color: red; }`)).toStrictEqual([
{ s: [1, 2], d: [RED], v: INHERITED_COLOR, cq: [{ p: { a: 1 } }] },
]);
expect(rulesFor(`:focus .subject { color: red; }`)).toStrictEqual([
{ s: [1, 2], d: [RED], v: INHERITED_COLOR, cq: [{ p: { f: 1 } }] },
]);
expect(rulesFor(`:disabled .subject { color: red; }`)).toStrictEqual([
{
s: [1, 2],
d: [RED],
v: INHERITED_COLOR,
cq: [{ a: [["a", "disabled"]] }],
},
]);
expect(rulesFor(`:empty .subject { color: red; }`)).toStrictEqual([
{
s: [1, 2],
d: [RED],
v: INHERITED_COLOR,
cq: [{ a: [["a", "children", "!"]] }],
},
]);
});

test("the descendant combinator compiles to what the :where() spelling of it already did", () => {
const combinator = rulesFor(`[data-state="on"] .subject { color: red; }`);
const isWhere = rulesFor(
`.subject:where([data-state="on"] *) { color: red; }`,
);

expect(combinator?.[0]?.cq).toStrictEqual(isWhere?.[0]?.cq);
});

test("each conditioned ancestor in a chain contributes its own query", () => {
expect(
rulesFor(`[data-state="on"] [aria-busy] .subject { color: red; }`),
).toStrictEqual([
{
s: [1, 3],
d: [RED],
v: INHERITED_COLOR,
cq: [{ a: [["d", "state", "=", "on"]] }, { a: [["a", "ariaBusy"]] }],
},
]);
});
});

describe("a named ancestor is unchanged", () => {
test("its conditions still land on the query its class names", () => {
expect(
rulesFor(`.group[data-state="on"] .subject { color: red; }`),
).toStrictEqual([
{
s: [1, 3],
d: [RED],
v: INHERITED_COLOR,
cq: [{ a: [["d", "state", "=", "on"]], n: "g:group" }],
},
]);
});

test("a compound naming several classes still yields ONE query", () => {
expect(rulesFor(`.a.b .subject { color: red; }`)).toStrictEqual([
{ s: [1, 3], d: [RED], v: INHERITED_COLOR, cq: [{ n: "g:b.a" }] },
]);
});

test("a named and a classless ancestor each keep their own query", () => {
expect(
rulesFor(`.group [data-state="on"] .subject { color: red; }`),
).toStrictEqual([
{
s: [1, 3],
d: [RED],
v: INHERITED_COLOR,
cq: [{ n: "g:group" }, { a: [["d", "state", "=", "on"]] }],
},
]);
});
});

describe("what is not an ancestor query", () => {
test("a condition on the SUBJECT stays on the rule", () => {
expect(rulesFor(`.subject[data-state="on"] { color: red; }`)).toStrictEqual(
[
{
s: [1, 2],
d: [RED],
v: INHERITED_COLOR,
aq: [["d", "state", "=", "on"]],
},
],
);
expect(rulesFor(`.subject:hover { color: red; }`)).toStrictEqual([
{ s: [1, 2], d: [RED], v: INHERITED_COLOR, p: { h: 1 } },
]);
});

test("an unconditioned ancestor contributes nothing to compile", () => {
expect(rulesFor(`div .subject { color: red; }`)).toStrictEqual(undefined);
});

test("a compound on ONE element is the subject's, not an ancestor's", () => {
expect(rulesFor(`[data-state="on"].subject { color: red; }`)).toStrictEqual(
[
{
s: [1, 2],
d: [RED],
v: INHERITED_COLOR,
aq: [["d", "state", "=", "on"]],
},
],
);
});

test("a combinator the builder does not answer drops the rule rather than widening it", () => {
expect(
rulesFor(`[data-state="on"] > .subject { color: red; }`),
).toStrictEqual(undefined);
expect(rulesFor(`:hover > .subject { color: red; }`)).toStrictEqual(
undefined,
);
});
});

describe("determinism", () => {
test("compiling the same stylesheet twice yields equal output", () => {
const css = `
[data-state="on"] .subject { color: red; }
.group:hover [aria-busy] .subject { color: blue; }
`;

expect(compile(css).stylesheet()).toStrictEqual(compile(css).stylesheet());
});
});
129 changes: 129 additions & 0 deletions src/__tests__/native/ancestor-conditions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { fireEvent, render, screen } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS } from "react-native-css/jest";

const RED = { color: "#f00" };

/** Any rule declaring a container makes its element the default container a nameless query resolves against. */
const CONTAINER_CSS = `.container { container-type: inline-size; }`;

test("a rule scoped to a hovered ancestor does not apply under an ancestor that is not hovered", () => {
registerCSS(`
${CONTAINER_CSS}
:hover .subject { color: red; }
`);

render(
<View testID="container" className="container">
<View testID="subject" className="subject" />
</View>,
);

expect(screen.getByTestId("subject")).not.toHaveStyle(RED);

fireEvent(screen.getByTestId("container"), "hoverIn");

expect(screen.getByTestId("subject")).toHaveStyle(RED);
});

test("the same rule does not apply where there is no ancestor to answer it", () => {
registerCSS(`:hover .subject { color: red; }`);

render(<View testID="subject" className="subject" />);

expect(screen.getByTestId("subject").props.style).toStrictEqual(undefined);
});

test("an ancestor condition is withheld from a sibling of the subject's ancestor", () => {
registerCSS(`
${CONTAINER_CSS}
:active .subject { color: red; }
`);

render(
<>
<View testID="pressed" className="container">
<View testID="under-pressed" className="subject" />
</View>
<View testID="idle" className="container">
<View testID="under-idle" className="subject" />
</View>
</>,
);

fireEvent(screen.getByTestId("pressed"), "pressIn");

expect(screen.getByTestId("under-pressed")).toHaveStyle(RED);
expect(screen.getByTestId("under-idle")).not.toHaveStyle(RED);
});

test("an attribute-identified ancestor is withheld where no ancestor can answer it", () => {
registerCSS(`[data-state="on"] .subject { color: red; }`);

render(
<View>
<View testID="subject" className="subject" />
</View>,
);

expect(screen.getByTestId("subject").props.style).toStrictEqual(undefined);
});

test("a named ancestor still answers from the element its class names", () => {
registerCSS(`.group:hover .subject { color: red; }`);

render(
<View testID="group" className="group">
<View testID="subject" className="subject" />
</View>,
);

expect(screen.getByTestId("subject")).not.toHaveStyle(RED);

fireEvent(screen.getByTestId("group"), "hoverIn");

expect(screen.getByTestId("subject")).toHaveStyle(RED);
});

test("the ancestor itself is not a subject, even when it carries the class", () => {
registerCSS(`
${CONTAINER_CSS}
:hover .subject { color: red; }
`);

render(
<View testID="container" className="container subject">
<View testID="descendant" className="subject" />
</View>,
);

fireEvent(screen.getByTestId("container"), "hoverIn");

expect(screen.getByTestId("descendant")).toHaveStyle(RED);
expect(screen.getByTestId("container")).not.toHaveStyle(RED);
});

test("a hovered ancestor moves every subject beneath it, and only those", () => {
registerCSS(`
${CONTAINER_CSS}
:hover .subject { color: red; }
`);

render(
<>
<View testID="container" className="container">
<View testID="direct" className="subject" />
<View>
<View testID="nested" className="subject" />
</View>
</View>
<View testID="outside" className="subject" />
</>,
);

fireEvent(screen.getByTestId("container"), "hoverIn");

expect(screen.getByTestId("direct")).toHaveStyle(RED);
expect(screen.getByTestId("nested")).toHaveStyle(RED);
expect(screen.getByTestId("outside")).not.toHaveStyle(RED);
});
Loading