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
7 changes: 4 additions & 3 deletions src/__tests__/native/box-shadow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ describe("@property defaults with shadow variables", () => {
]);
});

test("currentcolor resolves to platform color object", () => {
test("currentcolor resolves to the concrete root seed", () => {
registerCSS(`
@property --my-shadow {
syntax: "*";
Expand Down Expand Up @@ -827,8 +827,9 @@ describe("@property defaults with shadow variables", () => {
blurRadius: 0,
spreadDistance: 2,
});
// currentcolor resolves to a platform color object, not a string
expect(typeof component.props.style.boxShadow[0].color).toBe("object");
// currentcolor resolves through the root seed, which is a concrete colour on every
// platform — a value a paint path can take, rather than an object it must resolve first.
expect(component.props.style.boxShadow[0].color).toBe("#000000");
});

test("three vars with two transparent (Tailwind ring pattern)", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/native/filters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ describe("filter: drop-shadow()", () => {
render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

// currentcolor resolves to a PlatformColor object — requires
// "color" type (not "string") in the shorthand handler pattern
// currentcolor resolves through the root seed, which is a concrete
// scheme-aware colour on every platform — the light arm here.
expect(component.props.style.filter).toStrictEqual([
{
dropShadow: {
offsetX: 0,
offsetY: 4,
standardDeviation: 6,
color: { semantic: ["label", "labelColor"] },
color: "#000000",
},
},
]);
Expand Down
121 changes: 121 additions & 0 deletions src/__tests__/native/root-color-seed-android.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { act, render, screen } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";
import { colorScheme } from "react-native-css/runtime";

// The root `__rn-css-color` seed is a module-load side effect gated on
// Platform.OS, and jest-expo defaults to ios. Mock android so the runtime
// (react-native-css/jest -> native-internal/root) takes the Android branch;
// babel-jest hoists this jest.mock above the imports above.
jest.mock("react-native", () => {
const ReactNative =
jest.requireActual<typeof import("react-native")>("react-native");
ReactNative.Platform.OS = "android";
return ReactNative;
});

describe("android root __rn-css-color seed", () => {
test("currentcolor with no ancestor resolves to a concrete color", () => {
// The bug: the Android seed was PlatformColor('?attr/textColorPrimary'),
// which resolves to a ColorStateList reference that never paints — so
// currentcolor / default rings / text-current rendered nothing. It is now a
// concrete color that always paints.
colorScheme.set("light");
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);

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

test("the seed is scheme-aware — white in dark mode — via prefers-color-scheme", () => {
// Reactivity comes from the root observable's existing media-query
// evaluation reading the `colorScheme` observable — no extra Appearance
// listener is added.
colorScheme.set("dark");
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);

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

test("an ancestor-published color still overrides the root seed", () => {
// The seed is only the *ultimate* fallback; a nearer published color wins,
// so the fix changes nothing for content that already has a color context.
colorScheme.set("light");
registerCSS(`
.parent { color: red; }
.child { color: currentcolor; }
`);
render(
<View testID="parent" className="parent">
<View testID={testID} className="child" />
</View>,
);

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

test("responds live to a colorScheme change — reactive, not seeded once", () => {
// Proves the reactivity claim: the same element re-resolves on a scheme
// flip, through the root observable's media-query evaluation alone.
colorScheme.set("light");
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);
const element = screen.getByTestId(testID);

expect(element.props.style).toStrictEqual({ color: "#000000" });

act(() => {
colorScheme.set("dark");
});

expect(element.props.style).toStrictEqual({ color: "#FFFFFF" });
});

test("a default ring (box-shadow currentcolor) paints with the seed color", () => {
// The reported symptom: Tailwind's default ring color is currentcolor, so
// with the old ColorStateList seed every ring was invisible on Android.
colorScheme.set("light");
registerCSS(
`.ring { --my-ring: 0 0 0 2px currentcolor; box-shadow: var(--my-ring); }`,
);
render(<View testID={testID} className="ring" />);

const boxShadow = screen.getByTestId(testID).props.style.boxShadow as [
{ color: string },
];
expect(boxShadow[0].color).toBe("#000000");
});

test("a default inset-ring paints with the seed color", () => {
colorScheme.set("light");
registerCSS(
`.ir { --my-ring: inset 0 0 0 2px currentcolor; box-shadow: var(--my-ring); }`,
);
render(<View testID={testID} className="ir" />);

const boxShadow = screen.getByTestId(testID).props.style.boxShadow as [
{ inset: boolean; color: string },
];
expect(boxShadow[0].inset).toBe(true);
expect(boxShadow[0].color).toBe("#000000");
});

test("no color-scheme preference (null) falls back to the light default", () => {
// Appearance.getColorScheme() can be null; the dark media query then fails,
// so the seed resolves to its unconditioned light value rather than nothing.
colorScheme.set(null);
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#000000",
});
});
});
47 changes: 47 additions & 0 deletions src/__tests__/native/root-color-seed-override.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 { colorScheme } from "react-native-css/runtime";

// The seed is only defensible if an app can override it, so these pin that it can.
// They live in their own file because `inject` replaces a root variable outright and
// nothing puts it back — `react-native-css/jest`'s beforeEach clears
// StyleCollection.styles, not the root registry — so a `:root { color }` here would
// otherwise leak into every later test in the same file.
jest.mock("react-native", () => {
const ReactNative =
jest.requireActual<typeof import("react-native")>("react-native");
ReactNative.Platform.OS = "android";
return ReactNative;
});

test("a stylesheet :root color replaces the seed outright", () => {
// An app that themes its text colour has to win, and a :root rule is how it does
// that — not via an ancestor element, which is all the sibling suite covers
colorScheme.set("light");
registerCSS(`
:root { color: #123456; }
.c { color: currentcolor; }
`);
render(<View testID={testID} className="c" />);

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

test("a scheme-conditioned :root color overrides the seed per scheme", () => {
colorScheme.set("dark");
registerCSS(`
:root { color: #123456; }
@media (prefers-color-scheme: dark) {
:root { color: #eeeeee; }
}
.c { color: currentcolor; }
`);
render(<View testID={testID} className="c" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#eee",
});
});
57 changes: 57 additions & 0 deletions src/__tests__/native/root-color-seed.test.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 { colorScheme } from "react-native-css/runtime";

// jest-expo runs with Platform.OS === "ios" by default (no mock needed), so this
// file is the seed as iOS actually loads it. There is no longer an iOS BRANCH to
// take — that is what it exists to prove.
describe("ios root __rn-css-color seed", () => {
test("resolves to a concrete color, the same one every other platform gets", () => {
// This file used to assert that PlatformColor's descriptor reached props.style.
// That pins ARRIVAL, not paint, and cannot tell the two apart: pre-fix, the
// Android sibling's props bag held the same shape on the build that painted no
// ring and invisible text-current on a device. A concrete color is asserted
// instead, because a value that is wrong is then visibly wrong here.
//
// The scheme is pinned because the seed is scheme-aware on iOS for the first
// time — PlatformColor used to absorb that, and now the observable answers it.
colorScheme.set("light");
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);

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

test("follows the scheme, so neither arm is a constant", () => {
// A seed that had stopped resolving and simply held one branch would satisfy the
// test above whenever it asked for that branch. Driving both refutes it.
colorScheme.set("dark");
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);

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

test("an ancestor-published color still overrides the seed", () => {
// The resolution machinery is platform-agnostic; a nearer published color
// wins on iOS too, so the seed remains only the ultimate fallback.
registerCSS(`
.parent { color: red; }
.child { color: currentcolor; }
`);
render(
<View testID="parent" className="parent">
<View testID={testID} className="child" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#f00",
});
});
});
5 changes: 2 additions & 3 deletions src/__tests__/native/text-shadow.test.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ describe("text-shadow", () => {
render(<Text testID={testID} className="my-class" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
textShadowColor: {
semantic: ["label", "labelColor"],
},
// The root seed, concrete on every platform — light arm.
textShadowColor: "#000000",
textShadowOffset: {
height: 10,
width: 10,
Expand Down
5 changes: 2 additions & 3 deletions src/__tests__/vendor/tailwind/interactivity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ describe("Interactivity - Caret Color", () => {
test("caret-current", async () => {
expect(await renderCurrentTest()).toStrictEqual({
props: {
cursorColor: {
semantic: ["label", "labelColor"],
},
// The root seed, concrete on every platform — light arm.
cursorColor: "#000000",
style: {},
},
});
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/vendor/tailwind/ring-color-seed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { colorScheme } from "react-native-css/runtime";

import { renderSimple } from "./_tailwind";

// The root `__rn-css-color` seed is a module-load side effect gated on
// Platform.OS, and jest-expo defaults to ios. Mock android so the runtime takes
// the Android branch, then drive it with *real* Tailwind output — the default
// `ring-*` color is `currentcolor`, which resolves through the root seed.
jest.mock("react-native", () => {
const ReactNative =
jest.requireActual<typeof import("react-native")>("react-native");
ReactNative.Platform.OS = "android";
return ReactNative;
});

const ringColor = (props: { style?: unknown }): string => {
const style = props.style as { boxShadow: [{ color: string }] };
return style.boxShadow[0].color;
};

describe("android default ring paints (real Tailwind → root color seed)", () => {
test("ring-2 with no explicit color resolves to the seed, not an invisible ColorStateList", async () => {
// The reported bug: Tailwind's default ring color is currentcolor, and with
// the old PlatformColor('?attr/textColorPrimary') seed the ring never
// painted on Android. It now resolves to the concrete seed.
colorScheme.set("light");
const { props } = await renderSimple({ className: "ring-2" });
expect(ringColor(props)).toBe("#000000");
});

test("the default ring is scheme-aware (white in dark mode)", async () => {
colorScheme.set("dark");
const { props } = await renderSimple({ className: "ring" });
expect(ringColor(props)).toBe("#FFFFFF");
});

test("an explicit ring color still wins over the currentcolor default", async () => {
colorScheme.set("light");
const { props } = await renderSimple({ className: "ring-2 ring-red-500" });
expect(ringColor(props)).toBe("#fb2c36");
});
});
5 changes: 2 additions & 3 deletions src/__tests__/vendor/tailwind/typography.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,8 @@ describe("Typography - Text Color", () => {
expect(await renderCurrentTest()).toStrictEqual({
props: {
style: {
color: {
semantic: ["label", "labelColor"],
},
// The root seed, concrete on every platform — light arm.
color: "#000000",
},
},
});
Expand Down
Loading