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
32 changes: 32 additions & 0 deletions src/__tests__/compiler/compiler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,38 @@ test("animations preserve named keyframes and their timing contract", () => {
]);
});

test.each([
"animation: var(--v)",
"animation-delay: var(--v)",
"animation-direction: var(--v)",
"animation-duration: var(--v)",
"animation-fill-mode: var(--v)",
"animation-iteration-count: var(--v)",
"animation-name: var(--v)",
"animation-play-state: var(--v)",
"animation-timing-function: var(--v)",
"animation-timing-function: cubic-bezier(0.25, 0.5, 0.75, 1)",
"animation-timing-function: steps(4)",
"transition: var(--v)",
"transition-delay: var(--v)",
"transition-duration: var(--v)",
"transition-duration: var(--v) !important",
"transition-property: var(--v)",
"transition-timing-function: var(--v)",
"transition-timing-function: cubic-bezier(0.25, 0.5, 0.75, 1)",
"transition-timing-function: steps(4)",
"transition-behavior: allow-discrete",
"transition-behavior: var(--v)",
])("a rule writing %s carries the animated rule marker", (declaration) => {
const rule = compile(`.test { ${declaration}; }`).stylesheet().s?.[0]?.[1][0];
expect(rule?.a).toBe(true);
});

test("a rule writing no animation or transition carries no animated rule marker", () => {
const { s } = compile(`.test { width: var(--v); }`).stylesheet();
expect(s?.[0]?.[1][0]?.a).toBeUndefined();
});

test("breaks apart comma separated variables", () => {
const compiled = compile(
`
Expand Down
77 changes: 77 additions & 0 deletions src/__tests__/native/_reanimated-routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { StyleSheet } from "react-native";

import { render, screen } from "@testing-library/react-native";
import { compile } from "react-native-css/compiler";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";
import { StyleCollection } from "react-native-css/native";

import { animatedComponentFamily } from "../../native/reanimated";

/** As written, through a variable the compiler folds, through one the runtime keeps, and kept through the JSON a device reads. */
export const routes = ["written", "folded", "kept", "wire"] as const;

export type Route = (typeof routes)[number];

export const variableRoutes = ["folded", "kept", "wire"] as const;

/** The routes on which the runtime, not lightningcss, reads the value. */
export const runtimeRoutes = ["kept", "wire"] as const;

/** The routes on which the declaration itself holds the value's comma groups. */
export const declarationRoutes = ["written", "folded"] as const;

function registerOnRoute(
route: Route,
property: string,
value: string,
reference: string,
declarations: string,
) {
const css =
route === "written"
? `.a { ${declarations} ${property}: ${value}; }`
: `.a { ${declarations} --v: ${value}; ${property}: ${reference}; }`;

switch (route) {
case "written":
case "folded":
registerCSS(css);
return;
case "kept":
registerCSS(css, { inlineVariables: false });
return;
case "wire": {
const sheet = compile(css, { inlineVariables: false }).stylesheet();
StyleCollection.inject(JSON.parse(JSON.stringify(sheet)) as typeof sheet);
return;
}
default:
route satisfies never;
}
}

/**
* Whether `.a { property: value }` hands its element to Reanimated on a route, and the style it hands over; a variable route holds `value` in `--v` and writes `reference`, after the rule's other `declarations`.
* The calling test file mocks `../../native/reanimated` with a `jest.fn` that returns the component it is given.
*/
export function renderOnRoute(
route: Route,
property: string,
value: string,
reference = "var(--v)",
declarations = "",
) {
registerOnRoute(route, property, value, reference, declarations);
jest.mocked(animatedComponentFamily).mockClear();
render(<View testID={testID} className="a" />);

const reached = {
animated: jest.mocked(animatedComponentFamily).mock.calls.length > 0,
style: (StyleSheet.flatten(screen.getByTestId(testID).props.style) ??
{}) as Readonly<Record<string, unknown>>,
};

screen.unmount();
return reached;
}
75 changes: 74 additions & 1 deletion src/__tests__/native/animations.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ 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 { renderOnRoute, routes, runtimeRoutes } from "./_reanimated-routes";

const reanimated = jest.requireActual<typeof import("react-native-reanimated")>(
"react-native-reanimated",
);

// Check the engine output at the Reanimated boundary. Native interpolation is
// covered by the Release renderer suite, not by a fake getAnimatedStyle helper.
jest.mock("../../native/reanimated", () => ({
animatedComponentFamily: (component: unknown) => component,
animatedComponentFamily: jest.fn((component: unknown) => component),
}));
const style = () =>
StyleSheet.flatten(screen.getByTestId(testID).props.style) as
Expand Down Expand Up @@ -96,6 +102,73 @@ test.each([
},
);

describe.each([
["animation-delay", "200ms", { animationDelay: [200] }],
["animation-delay", "-1s", { animationDelay: [-1000] }],
["animation-direction", "alternate", { animationDirection: ["alternate"] }],
["animation-direction", "ALTERNATE", { animationDirection: ["alternate"] }],
["animation-duration", "3s", { animationDuration: [3000] }],
["animation-duration", "1s, 2s", { animationDuration: [1000, 2000] }],
["animation-fill-mode", "both", { animationFillMode: ["both"] }],
["animation-iteration-count", "2", { animationIterationCount: [2] }],
[
"animation-iteration-count",
"infinite",
{ animationIterationCount: ["infinite"] },
],
[
"animation-iteration-count",
"INFINITE",
{ animationIterationCount: ["infinite"] },
],
["animation-play-state", "paused", { animationPlayState: ["paused"] }],
[
"animation-timing-function",
"ease-in",
{ animationTimingFunction: "ease-in" },
],
[
"animation-timing-function",
"EASE-IN",
{ animationTimingFunction: "ease-in" },
],
[
"animation-timing-function",
"cubic-bezier(0.25, 0.5, 0.75, 1)",
{ animationTimingFunction: reanimated.cubicBezier(0.25, 0.5, 0.75, 1) },
],
[
"animation-timing-function",
"steps(4, end)",
{ animationTimingFunction: reanimated.steps(4, "end") },
],
])("%s: %s", (property, value, style) => {
test.each(routes)(
"%s reaches Reanimated with the written animation",
(route) => {
expect(renderOnRoute(route, property, value)).toStrictEqual({
animated: true,
style,
});
},
);
});

describe.each([
["animation-duration", "-1s"],
["animation-delay", "paused"],
["animation-direction", "sideways"],
["animation-fill-mode", "2"],
["animation-iteration-count", "-1"],
["animation-play-state", "running, stopped"],
["animation-timing-function", "ease-in, sideways"],
["animation-timing-function", "scale(2)"],
])("%s held in a variable over %s is invalid", (property, value) => {
test.each(runtimeRoutes)("and writes no animation %s", (route) => {
expect(renderOnRoute(route, property, value).style).toStrictEqual({});
});
});

test("an unresolved animation name is omitted and a later variable restores it", () => {
registerCSS(
`${keyframes} .motion { animation-name: var(--animation-name); animation-duration: 1s; }
Expand Down
Loading