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

const declarationsFor = (css: string, className: string) => {
const rules = new Map(compile(css).stylesheet().s ?? []).get(className);

return (rules ?? []).flatMap((rule) => rule.d ?? []);
};

const variablesFor = (css: string, className: string) => {
const rules = new Map(compile(css).stylesheet().s ?? []).get(className);

return (rules ?? []).flatMap((rule) => rule.v ?? []);
};

describe("the typed path", () => {
test("a literal stack narrows to its first family", () => {
expect(
declarationsFor(`.a { font-family: Inter, Helvetica, sans-serif; }`, "a"),
).toStrictEqual([{ fontFamily: "Inter" }]);
});

test("the `font` shorthand narrows to its first family", () => {
expect(
declarationsFor(`.a { font: italic 12px Inter, Helvetica; }`, "a"),
).toStrictEqual([
{
fontFamily: "Inter",
fontSize: 12,
fontStyle: "italic",
fontWeight: "normal",
},
]);
});

test("a name that is not a bare ident survives whole", () => {
// LightningCSS unquotes a quoted family, and a multi-ident name is one family
expect(
declarationsFor(`.a { font-family: "Helvetica Neue", Arial; }`, "a"),
).toStrictEqual([{ fontFamily: "Helvetica Neue" }]);
expect(
declarationsFor(`.a { font-family: "Foo, Bar", Arial; }`, "a"),
).toStrictEqual([{ fontFamily: "Foo, Bar" }]);
expect(
declarationsFor(`.a { font-family: Helvetica Neue, Arial; }`, "a"),
).toStrictEqual([{ fontFamily: "Helvetica Neue" }]);
});

test("no warning is emitted for the families that are dropped", () => {
expect(
compile(`.a { font-family: Inter, Helvetica, sans-serif; }`).warnings(),
).toStrictEqual({});
expect(
compile(`.a { font-family: Inter, Helvetica,; }`).warnings(),
).toStrictEqual({});
});
});

describe("the unparsed path", () => {
test.each([
["a trailing comma", `.a { font-family: Inter, Helvetica,; }`],
["a leading comma", `.a { font-family: ,Inter, Helvetica; }`],
["a doubled comma", `.a { font-family: Inter,,Helvetica; }`],
["quoted families", `.a { font-family: "Inter", "Helvetica",; }`],
])("%s still narrows to the first family", (_name, css) => {
expect(declarationsFor(css, "a")).toStrictEqual([{ fontFamily: "Inter" }]);
});

test("a quoted name keeps its spaces and its commas", () => {
expect(
declarationsFor(`.a { font-family: "Helvetica Neue", Arial,; }`, "a"),
).toStrictEqual([{ fontFamily: "Helvetica Neue" }]);
expect(
declarationsFor(`.a { font-family: "Foo, Bar", Arial,; }`, "a"),
).toStrictEqual([{ fontFamily: "Foo, Bar" }]);
});

test("an entry that cannot name a family is skipped", () => {
expect(
declarationsFor(`.a { font-family: 12, Inter; }`, "a"),
).toStrictEqual([{ fontFamily: "Inter" }]);
});

test("a stack with nothing usable emits no declaration at all", () => {
expect(declarationsFor(`.a { font-family: ,; }`, "a")).toStrictEqual([]);
});

test("a keyframe narrows too", () => {
expect(
compile(
`@keyframes k { from { font-family: Inter, Helvetica,; } to { font-family: Georgia; } }`,
).stylesheet().k,
).toStrictEqual([
[
"k",
[
["from", [{ fontFamily: "Inter" }]],
["to", [{ fontFamily: "Georgia" }]],
],
],
]);
});
});

describe("the var() path", () => {
// A variable declared once is inlined by the compiler, so these declare --stack twice

test("a stack the compiler cannot see is left for the runtime", () => {
expect(
declarationsFor(
`:root { --stack: Inter, Helvetica; } .other { --stack: Georgia, serif; } .a { font-family: var(--stack); }`,
"a",
),
).toStrictEqual([[[{}, "var", "stack", 1], "fontFamily", 1]]);
});

test("a var() behind a literal is narrowed away", () => {
expect(
declarationsFor(
`:root { --x: Georgia; } .other { --x: Verdana; } .a { font-family: Inter, var(--x); }`,
"a",
),
).toStrictEqual([{ fontFamily: "Inter" }]);
});

test("a var() in front of a literal is left whole", () => {
expect(
declarationsFor(
`:root { --x: Georgia; } .other { --x: Verdana; } .a { font-family: var(--x), Helvetica; }`,
"a",
),
).toStrictEqual([[[[{}, "var", "x", 1], "Helvetica"], "fontFamily"]]);
});

test("a fallback is emitted whole, whatever shape it has", () => {
expect(
declarationsFor(`.a { font-family: var(--missing, Helvetica); }`, "a"),
).toStrictEqual([
[[{}, "var", ["missing", "Helvetica"], 1], "fontFamily", 1],
]);

expect(
declarationsFor(
`.a { font-family: var(--missing, Inter, Helvetica); }`,
"a",
),
).toStrictEqual([
[[{}, "var", ["missing", ["Inter", "Helvetica"]], 1], "fontFamily", 1],
]);

expect(
declarationsFor(
`.a { font-family: var(--missing-a, var(--missing-b, serif)); }`,
"a",
),
).toStrictEqual([
[
[{}, "var", ["missing-a", [{}, "var", ["missing-b", "serif"], 1]], 1],
"fontFamily",
1,
],
]);
});
});

describe("what a var() cannot carry", () => {
test("KNOWN LIMIT: a space group and a comma group compile to the same value", () => {
const spaceGroup = variablesFor(
`.b { --f: Helvetica Neue; } .c { --f: x; }`,
"b",
);
const commaGroup = variablesFor(
`.b { --f: Inter, Helvetica; } .c { --f: x; }`,
"b",
);

expect(spaceGroup).toStrictEqual([["f", ["Helvetica", "Neue"]]]);
expect(commaGroup).toStrictEqual([["f", ["Inter", "Helvetica"]]]);
expect(spaceGroup.map(([, value]) => typeof value)).toStrictEqual(
commaGroup.map(([, value]) => typeof value),
);

expect(
variablesFor(`.b { --f: "Helvetica Neue"; } .c { --f: x; }`, "b"),
).toStrictEqual([["f", "Helvetica Neue"]]);
});
});
83 changes: 83 additions & 0 deletions src/__tests__/native/font-family-stack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { applyValue } from "../../native/objects";

/** A real Tailwind `--font-sans`, which is why the stack is the common case. */
const FONT_SANS_STACK = [
"Inter",
"Inter Fallback",
"ui-sans-serif",
"system-ui",
"sans-serif",
] as const;

const applyFontFamily = (value: unknown): Record<string, unknown> => {
const target: Record<string, unknown> = {};
applyValue(target, "fontFamily", value);
return target;
};

describe("the reduction", () => {
test("a resolved stack reduces to its first family, as a string", () => {
const target = applyFontFamily([...FONT_SANS_STACK]);

expect(target.fontFamily).toBe("Inter");
expect(typeof target.fontFamily).toBe("string");
});

test("a nested stack is flattened, not descended into", () => {
expect(applyFontFamily([[...FONT_SANS_STACK]]).fontFamily).toBe("Inter");
expect(applyFontFamily([[], "Arial"]).fontFamily).toBe("Arial");
expect(applyFontFamily([[[]], "Arial"]).fontFamily).toBe("Arial");
expect(applyFontFamily([["Inter"], "Arial"]).fontFamily).toBe("Inter");
});

test("an entry that cannot name a family is skipped", () => {
expect(applyFontFamily([12, "Inter"]).fontFamily).toBe("Inter");
expect(applyFontFamily([null, "Arial"]).fontFamily).toBe("Arial");
expect(applyFontFamily([undefined, "Arial"]).fontFamily).toBe("Arial");
expect(applyFontFamily([true, "Arial"]).fontFamily).toBe("Arial");
});

test("a single family passes through untouched", () => {
expect(applyFontFamily("brand-icons").fontFamily).toBe("brand-icons");
});

test("a stack with nothing usable sets nothing", () => {
expect("fontFamily" in applyFontFamily([])).toBe(false);
expect("fontFamily" in applyFontFamily([12])).toBe(false);
expect("fontFamily" in applyFontFamily([[], [null]])).toBe(false);

const inherited: Record<string, unknown> = { fontFamily: "Inter" };
applyValue(inherited, "fontFamily", []);
expect(inherited.fontFamily).toBe("Inter");
});
});

describe("what the reduction must not disturb", () => {
test("the reduction is scoped to fontFamily", () => {
// fontVariant is a list on React Native, so only fontFamily is reduced
const target: Record<string, unknown> = {};
applyValue(target, "fontVariant", ["small-caps"]);

expect(target.fontVariant).toStrictEqual(["small-caps"]);
});

test("both sentinel meanings survive the reduction", () => {
const untouched: Record<string, unknown> = {};
applyValue(untouched, "fontFamily", undefined);
expect("fontFamily" in untouched).toBe(false);

const cleared: Record<string, unknown> = { fontFamily: "Inter" };
applyValue(cleared, "fontFamily", null);
expect("fontFamily" in cleared).toBe(true);
expect(cleared.fontFamily).toBeUndefined();
});

test("the delayed-style marker passes through by identity", () => {
// applyDeclarations parks { fontFamily: true } on the target while a delayed value resolves
const marker = { fontFamily: true };
const target: Record<string, unknown> = {};
applyValue(target, "fontFamily", marker);

expect(target.fontFamily).toBe(marker);
});
});
Loading