Skip to content
Draft
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
26 changes: 23 additions & 3 deletions .claude/skills/add-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ allowed-tools: Read, Grep, Glob, Edit, Write

## Context

react-native-css has three test domains in `src/__tests__/`:
react-native-css has these test domains in `src/__tests__/`:

- `babel/` — Babel plugin tests using `babel-plugin-tester`
- `compiler/` — CSS compilation tests verifying JSON output
- `e2e/` — Maestro flows for the example app
- `native/` — Runtime tests for style application
- `types/` — Type tests that build a TypeScript program over the package's declarations
- `vendor/` — Tests of third-party CSS, such as Tailwind's output
- `web/` — Runtime tests for the web

## Conventions by domain

Expand Down Expand Up @@ -41,14 +45,30 @@ Verify CSS → JSON compilation output structure.

Test runtime style application on native platform.

### Web tests (`src/__tests__/web/`)

Test the web runtime, such as `useCssElement` and `assignStyle`.

### Vendor tests (`src/__tests__/vendor/`)

Compile a third party's CSS, such as Tailwind's through `vendor/tailwind/_tailwind.tsx`'s `render`, and assert the styles it produces.

### End-to-end flows (`src/__tests__/e2e/`)

Maestro flows that drive the example app on a device.

### Type tests (`src/__tests__/types/`)

A case `tsc` alone decides goes in `src/__tests__/native/_styled-types.tsx` or `src/__fixtures__/types/component-mapping.tsx`, which jest skips and `yarn typecheck` checks, with `@ts-expect-error` above a call the types must reject. A case that needs the compiler API, such as an editor's completions or a walk over the declarations, builds a TypeScript program in `src/__tests__/types/`.

## Steps

1. **Identify the feature**: What needs testing? Use `$ARGUMENTS` as the starting point.

2. **Determine the domain**: Is this a babel transform, compiler output, or runtime behavior?
2. **Determine the domain**: Is this a babel transform, compiler output, native or web runtime behavior, a third party's CSS, a flow on a device, or a type?

3. **Find existing tests**: Search the appropriate `src/__tests__/` subdirectory for similar tests.

4. **Write the test**: Follow the conventions of the domain.

5. **Run the test**: Execute `yarn test`. Note: ignore `ExperimentalWarning: VM Modules` warnings — they're expected.
5. **Run the test**: Execute `yarn test`, and `yarn typecheck` for a case `tsc` alone decides. Note: ignore `ExperimentalWarning: VM Modules` warnings — they're expected.
2 changes: 1 addition & 1 deletion .claude/skills/triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Create a minimal reproduction. The approach depends on the repo and what's being

### react-native-css issues

Tests are organized by domain in `src/__tests__/` with three subdirectories: `native/`, `compiler/`, `babel/`. Before writing a test, read a few existing tests in the relevant subdirectory to match the conventions exactly.
Tests are organized by domain in `src/__tests__/`: `babel/`, `compiler/`, `e2e/`, `native/`, `types/`, `vendor/` and `web/`, as the `add-test` skill describes. Before writing a test, read a few existing tests in the relevant subdirectory to match the conventions exactly.

**For native styling issues** (most common, e.g., wrong style output, className not working):

Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ yarn example start:debug # Rebuild + start with debug logging
## Testing

- **Runner:** Jest with experimental ESM support (`NODE_OPTIONS="--experimental-vm-modules"`)
- **Convention:** Tests in `src/__tests__/` organized by domain (`babel/`, `compiler/`, `native/`)
- **Convention:** Tests in `src/__tests__/` organized by domain (`babel/`, `compiler/`, `e2e/`, `native/`, `types/`, `vendor/`, `web/`)
- **Babel tests** use `babel-plugin-tester`
- **Compiler tests** verify JSON output structure
- **Run specific suites:** `yarn test babel`, `yarn test compiler`
Expand Down
77 changes: 77 additions & 0 deletions src/__tests__/native/_styled-types.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { ComponentProps } from "react";
import {
ActivityIndicator,
Image,
Modal,
ScrollView,
StatusBar,
Text,
TextInput,
View,
type StyleProp,
Expand Down Expand Up @@ -60,6 +64,79 @@ export const rejected = [
// @ts-expect-error An explicit mapping must name a real target property.
rootStyled(Base, { customClassName: "missing" });

// A mapping takes any style key, a -rn- declaration's among them, an entry left undefined, and a value naming a prop
export const mappings = [
rootStyled(View, {
className: {
target: "style",
nativeStyleMapping: { color: "accessibilityLabel" },
},
}),
rootStyled(ActivityIndicator, {
className: { target: "style", nativeStyleMapping: { color: "color" } },
}),
rootStyled(TextInput, {
className: {
target: "style",
nativeStyleMapping: { selectionColor: true },
},
}),
rootStyled(Image, {
className: { target: "style", nativeStyleMapping: { tintColor: true } },
}),
rootStyled(View, {
className: {
target: "style",
nativeStyleMapping: { backgroundColor: undefined },
},
}),
rootStyled(View, {
labelClassName: {
target: false,
nativeStyleMapping: { color: "style.borderColor" },
},
}),
rootStyled(Text, {
className: {
target: "style",
nativeStyleToProp: { color: "accessibilityLabel" },
},
}),
];

export const rejectedMappings = [
// @ts-expect-error A mapping's value must name a prop.
rootStyled(View, {
className: {
target: "style",
nativeStyleMapping: { color: "accessibilityLabl" },
},
}),
// @ts-expect-error A target:false mapping's value must name a prop too.
rootStyled(View, {
labelClassName: {
target: false,
nativeStyleMapping: { color: "style.borderColour" },
},
}),
// @ts-expect-error A mapping's value is true or the path of a prop.
rootStyled(View, {
className: {
target: "style",
nativeStyleMapping: { color: false },
},
}),
];

// A styled component's class props are writable, as the component's own props are
export function writeClassNames(
rootProps: ComponentProps<typeof Root>,
mappedProps: ComponentProps<typeof Mapped>,
) {
rootProps.className = "p-4";
mappedProps.customClassName = "p-4";
}

export const components = [
<CssView className="p-4" />,
<CssText className="text-red-500">Hello</CssText>,
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/native/styled.test.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,25 @@ test.each(["nativeStyleMapping", "nativeStyleToProp"] as const)(
},
);

test("a mapping entry set to undefined maps nothing", () => {
registerCSS(`.subject { color: blue; background-color: red; }`);
const StyledView = styled(View, {
className: {
target: "style",
nativeStyleMapping: {
color: "accessibilityLabel",
backgroundColor: undefined,
},
},
});

render(<StyledView testID={testID} className="subject" />);

const props = screen.getByTestId(testID).props;
expect(props.accessibilityLabel).toBe("#00f");
expect(props.style).toEqual({ backgroundColor: "#f00" });
});

test.each([false, true])(
"current mapping wins over deprecated alias (empty: %s)",
(empty) => {
Expand Down
69 changes: 69 additions & 0 deletions src/__tests__/types/_program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { join } from "node:path";

import ts from "typescript";

export const root = join(__dirname, "../../..").replaceAll("\\", "/");

// The repository's own compiler options with exactOptionalPropertyTypes on, and a fixture's unused names allowed
export function compilerOptions(): ts.CompilerOptions {
const { config } = ts.readConfigFile(`${root}/tsconfig.json`, (path) =>
ts.sys.readFile(path),
);
const { options } = ts.parseJsonConfigFileContent(config, ts.sys, root);
return {
...options,
exactOptionalPropertyTypes: true,
noUnusedLocals: false,
noUnusedParameters: false,
};
}

export function createProgram(
rootNames: readonly string[],
virtualFiles: Readonly<Record<string, string>> = {},
): ts.Program {
const options = compilerOptions();
const host = ts.createCompilerHost(options, true);

return ts.createProgram(rootNames, options, {
...host,
getCurrentDirectory: () => root,
fileExists: (name) => name in virtualFiles || host.fileExists(name),
readFile: (name) => virtualFiles[name] ?? host.readFile(name),
getSourceFile: (name, languageVersion, ...rest) => {
const text = virtualFiles[name];
return text === undefined
? host.getSourceFile(name, languageVersion, ...rest)
: ts.createSourceFile(name, text, languageVersion, true);
},
});
}

export function createLanguageService(
virtualFiles: Readonly<Record<string, string>>,
): ts.LanguageService {
const options = compilerOptions();

return ts.createLanguageService(
{
getScriptFileNames: () => Object.keys(virtualFiles),
getScriptVersion: () => "1",
getScriptSnapshot: (name) => {
const text = virtualFiles[name] ?? ts.sys.readFile(name);
return text === undefined
? undefined
: ts.ScriptSnapshot.fromString(text);
},
getCurrentDirectory: () => root,
getCompilationSettings: () => options,
getDefaultLibFileName: ts.getDefaultLibFilePath,
fileExists: (name) => name in virtualFiles || ts.sys.fileExists(name),
readFile: (name) => virtualFiles[name] ?? ts.sys.readFile(name),
readDirectory: (path, extensions, excludes, includes, depth) =>
ts.sys.readDirectory(path, extensions, excludes, includes, depth),
directoryExists: (name) => ts.sys.directoryExists(name),
getDirectories: (name) => ts.sys.getDirectories(name),
},
ts.createDocumentRegistry(),
);
}
Loading