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

import { serializeStyleSheet } from "../../metro/injection-code";

/** The media conditions of every rule compiled for `className`. */
function mediaConditions(css: string, className: string) {
const rules =
compile(css)
.stylesheet()
.s?.find(([name]) => name === className)?.[1] ?? [];

return rules.map((rule): MediaCondition[] | undefined => rule.m);
}

describe.skip("platform media queries", () => {
test("android", () => {
const compiled = compile(`
Expand Down Expand Up @@ -62,6 +75,138 @@ describe.skip("platform media queries", () => {
});
});

describe("comma-separated media query lists", () => {
test("compile to a union, not an intersection", () => {
expect(
mediaConditions(
`@media (min-width: 100px), (min-width: 9999px) {
.my-class { background-color: red; }
}`,
"my-class",
),
).toStrictEqual([
[
[
"|",
[
[">=", "width", 100],
[">=", "width", 9999],
],
],
],
]);
});

test("a single query is not wrapped", () => {
expect(
mediaConditions(
`@media (min-width: 100px) {
.my-class { background-color: red; }
}`,
"my-class",
),
).toStrictEqual([[[">=", "width", 100]]]);
});

test("a comma list and an `or` condition compile identically", () => {
const comma = mediaConditions(
`@media (min-width: 100px), (min-width: 9999px) {
.my-class { background-color: red; }
}`,
"my-class",
);

const or = mediaConditions(
`@media ((min-width: 100px) or (min-width: 9999px)) {
.my-class { background-color: red; }
}`,
"my-class",
);

expect(comma).toStrictEqual(or);
});

test("nested @media rules still intersect", () => {
expect(
mediaConditions(
`@media (min-width: 100px) {
@media (min-height: 200px) {
.my-class { background-color: red; }
}
}`,
"my-class",
),
).toStrictEqual([
[
[">=", "width", 100],
[">=", "height", 200],
],
]);
});
});

describe("an operand the compiler cannot resolve", () => {
// `env()` has no compile-time value. The operand compiles to `null`, the one
// spelling of "no value" that survives `JSON.stringify` into a native bundle,
// and it has to survive into the condition: a condition that is absent applies
// unconditionally, so dropping the query is the opposite of refusing it.
test("compiles to null beside a sibling operand", () => {
expect(
mediaConditions(
`@media ((orientation: env(safe-area-inset-top)) and (min-width: 0px)) {
.my-class { background-color: red; }
}`,
"my-class",
),
).toStrictEqual([
[
[
"&",
[
["=", "orientation", null],
[">=", "width", 0],
],
],
],
]);
});

test("compiles to null as the only operand", () => {
expect(
mediaConditions(
`@media (orientation: env(safe-area-inset-top)) {
.my-class { background-color: red; }
}`,
"my-class",
),
).toStrictEqual([[["=", "orientation", null]]]);
});

test("survives the serializer that carries it to a device", () => {
const conditions = mediaConditions(
`@media (orientation: env(safe-area-inset-top)) {
.my-class { background-color: red; }
}`,
"my-class",
);

expect(JSON.parse(serializeStyleSheet(conditions))).toStrictEqual(
conditions,
);
});
});

test("a boolean feature compiles to a boolean condition", () => {
expect(
mediaConditions(
`@media (width) {
.my-class { background-color: red; }
}`,
"my-class",
),
).toStrictEqual([[["!!", "width"]]]);
});

test("@media (hover: hover)", () => {
const compiled = compile(`
@media (hover: hover) {
Expand Down
118 changes: 118 additions & 0 deletions src/__tests__/compiler/unknown-condition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
compile,
type ReactNativeCssStyleSheet,
} from "react-native-css/compiler";

import { serializeStyleSheet } from "../../metro/injection-code";

/**
* The compiler half of the three-valued contract: a term it cannot compile is
* emitted as `["?"]` rather than dropped, and that marker has to survive the
* JSON transport a native bundle carries the stylesheet through.
*/

function conditionsFor(css: string) {
const rules = compile(css).stylesheet().s?.[0]?.[1];

if (!Array.isArray(rules)) {
throw new Error("expected compiled rules");
}

return rules.map((rule) =>
typeof rule === "object" ? (rule.cq ?? rule.m) : rule,
);
}

const body = `{ .child { color: red; } }`;

describe("an unsupported container feature compiles to an unknown term", () => {
test("style() alone", () => {
expect(conditionsFor(`@container style(--foo: bar) ${body}`)).toStrictEqual(
[[{ m: ["?"] }]],
);
});

test("style() inside a conjunction keeps its slot", () => {
expect(
conditionsFor(
`@container (min-width: 100px) and style(--foo: bar) ${body}`,
),
).toStrictEqual([[{ m: ["&", [[">=", "width", 100], ["?"]]] }]]);
});

test("style() inside a disjunction keeps its slot", () => {
expect(
conditionsFor(
`@container (min-width: 100px) or style(--foo: bar) ${body}`,
),
).toStrictEqual([[{ m: ["|", [[">=", "width", 100], ["?"]]] }]]);
});

test("a negated style() keeps the negation and the term", () => {
expect(
conditionsFor(`@container not style(--foo: bar) ${body}`),
).toStrictEqual([[{ m: ["!", ["?"]] }]]);
});
});

/**
* The marker exists in this shape rather than as `undefined` because the
* transport cannot carry `undefined`: `JSON.stringify` writes it as `null`
* inside an array and drops the key entirely on an object. A guard written
* against `undefined` would hold in a test that injected the compiler's own
* object and never fire on a device.
*/
test("the unknown marker survives the JSON transport unchanged", () => {
const stylesheet = compile(
`@container (min-width: 100px) and style(--foo: bar) ${body}`,
).stylesheet();

const transported = JSON.parse(
serializeStyleSheet(stylesheet),
) as ReactNativeCssStyleSheet;

expect(transported).toStrictEqual(stylesheet);

const rules = transported.s?.[0]?.[1];
if (!Array.isArray(rules)) {
throw new Error("expected compiled rules");
}

expect(rules[0]?.cq).toStrictEqual([
{ m: ["&", [[">=", "width", 100], ["?"]]] },
]);
});

/**
* `undefined` in the same slot is what the marker exists to avoid. This pins
* the transport's behaviour, so the reason for the marker cannot quietly stop
* being true.
*/
test("undefined in an array slot becomes null across the transport", () => {
expect(JSON.parse(serializeStyleSheet([1, undefined, 3]))).toStrictEqual([
1,
null,
3,
]);

expect(JSON.parse(serializeStyleSheet({ m: undefined }))).toStrictEqual({});
});

describe("a media condition the compiler cannot compile keeps its slot", () => {
test("every media feature form compiles to a term, so no media prelude is dropped", () => {
// Each of these reaches the runtime as a term rather than as an absent
// condition: an unknown <mf-name>, a <general-enclosed>, and an operand
// with no compile-time value.
expect(
conditionsFor(`@media (fictional-feature: 3) ${body}`),
).toStrictEqual([[["=", "fictional-feature", 3]]]);

expect(conditionsFor(`@media (fictional-thing) ${body}`)).toStrictEqual([
[["!!", "fictional-thing"]],
]);

expect(
conditionsFor(`@media (min-aspect-ratio: 3/4) ${body}`),
).toStrictEqual([[[">=", "aspect-ratio", null]]]);
});
});
Loading