From 9aa5e32ba5c007a2f09642ad18229857d0c03f34 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Sun, 11 Oct 2026 17:04:24 +0300 Subject: [PATCH 1/3] test: give each Tailwind render a compiler of its own @tailwindcss/postcss reuses one compiler per `from` and `base`, and an incremental rebuild keeps the first input's `@source inline()` classes. The harness passed a fixed `from` and `base: Date.now()`, so two renders that read the same millisecond shared a compiler and the second one compiled the first one's classes. A per-render counter keys each render on its own. --- src/__tests__/vendor/tailwind/_tailwind.tsx | 6 ++++-- .../vendor/tailwind/harness.test.tsx | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/vendor/tailwind/harness.test.tsx diff --git a/src/__tests__/vendor/tailwind/_tailwind.tsx b/src/__tests__/vendor/tailwind/_tailwind.tsx index 3ba5db5e..4f8cd107 100644 --- a/src/__tests__/vendor/tailwind/_tailwind.tsx +++ b/src/__tests__/vendor/tailwind/_tailwind.tsx @@ -13,6 +13,8 @@ import { registerCSS } from "react-native-css/jest"; const testID = "tailwind"; +let renderCount = 0; + export type NativewindRenderOptions = RenderOptions & { /** Replace the generated CSS*/ css?: string; @@ -76,8 +78,8 @@ export async function render( // Process the TailwindCSS const { css: output } = await postcss([ - /* Tailwind seems to internally cache things, so we need a random value to cache bust */ - tailwind({ base: Date.now().toString() }), + /* The plugin reuses one compiler per `from` and `base`, so every render needs a `base` no other render has */ + tailwind({ base: `render-${++renderCount}` }), ]).process(css, { from: __dirname, }); diff --git a/src/__tests__/vendor/tailwind/harness.test.tsx b/src/__tests__/vendor/tailwind/harness.test.tsx new file mode 100644 index 00000000..bf001e71 --- /dev/null +++ b/src/__tests__/vendor/tailwind/harness.test.tsx @@ -0,0 +1,19 @@ +import { View } from "react-native-css/components"; + +import { render } from "./_tailwind"; + +async function compileClassName(className: string) { + const { stylesheet } = await render(); + return stylesheet().s?.map(([name]) => name); +} + +test("renders that read the same millisecond compile their own classes", async () => { + const now = jest.spyOn(Date, "now").mockReturnValue(0); + + try { + expect(await compileClassName("text-red-500")).toEqual(["text-red-500"]); + expect(await compileClassName("text-blue-500")).toEqual(["text-blue-500"]); + } finally { + now.mockRestore(); + } +}); From 8598d7450d8b2bde7ea2cc9ec97b6b62a572d6d2 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 20 Oct 2026 13:48:31 +0300 Subject: [PATCH 2/3] test: assert each same-millisecond render applies its own style too The case already checked that each render compiles only its own class; it now also checks the style each render applies, so it fails on what a vendor case sees as well as on what the compiler produced. --- .../vendor/tailwind/harness.test.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/__tests__/vendor/tailwind/harness.test.tsx b/src/__tests__/vendor/tailwind/harness.test.tsx index bf001e71..58b6e553 100644 --- a/src/__tests__/vendor/tailwind/harness.test.tsx +++ b/src/__tests__/vendor/tailwind/harness.test.tsx @@ -2,17 +2,21 @@ import { View } from "react-native-css/components"; import { render } from "./_tailwind"; -async function compileClassName(className: string) { - const { stylesheet } = await render(); - return stylesheet().s?.map(([name]) => name); -} - -test("renders that read the same millisecond compile their own classes", async () => { +test("renders that read the same millisecond compile and apply their own classes", async () => { const now = jest.spyOn(Date, "now").mockReturnValue(0); try { - expect(await compileClassName("text-red-500")).toEqual(["text-red-500"]); - expect(await compileClassName("text-blue-500")).toEqual(["text-blue-500"]); + for (const [className, style] of [ + ["left-0", { left: 0 }], + ["left-1", { left: 3.5 }], + ] as const) { + const { stylesheet, getByTestId } = await render( + , + ); + + expect(stylesheet().s?.map(([name]) => name)).toStrictEqual([className]); + expect(getByTestId(className).props.style).toStrictEqual(style); + } } finally { now.mockRestore(); } From 0d193cfdbd9327d62392ea718e1504176676d221 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 20 Oct 2026 17:01:24 +0300 Subject: [PATCH 3/3] test: compile each Tailwind render afresh, and fail on a compile error The plugin reuses a cached compiler until a file it read changes, and compiles afresh whenever no file is at `from`, so a `from` with no file behind it compiles each render's own CSS in one cache entry, where a `base` per render kept a compiler per render. A failed compile reached only `console.error` and an empty stylesheet; it now fails the render. `render.debug` takes the harness's options, and `render` no longer lists `plugin` and `className`, which it never read. --- src/__tests__/vendor/tailwind/_tailwind.tsx | 57 ++++++++++++++----- .../vendor/tailwind/harness.test.tsx | 6 ++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/__tests__/vendor/tailwind/_tailwind.tsx b/src/__tests__/vendor/tailwind/_tailwind.tsx index 4f8cd107..3d283ffd 100644 --- a/src/__tests__/vendor/tailwind/_tailwind.tsx +++ b/src/__tests__/vendor/tailwind/_tailwind.tsx @@ -1,5 +1,7 @@ import type { PropsWithChildren, ReactElement } from "react"; +import { join } from "node:path"; + import tailwind from "@tailwindcss/postcss"; import { screen, @@ -13,23 +15,22 @@ import { registerCSS } from "react-native-css/jest"; const testID = "tailwind"; -let renderCount = 0; +// The plugin reuses a cached compiler until a file it read changes, and compiles afresh whenever no file is at `from`, so `from` names none: https://github.com/tailwindlabs/tailwindcss/blob/v4.1.13/packages/@tailwindcss-postcss/src/index.ts#L182-L196 +const from = join(__dirname, "input.css"); +// Without `source(none)` the plugin scans `base` for classes, so `base` names no folder: https://github.com/tailwindlabs/tailwindcss/blob/v4.1.13/packages/@tailwindcss-postcss/src/index.ts#L219-L221 +const base = join(__dirname, "no-sources"); export type NativewindRenderOptions = RenderOptions & { /** Replace the generated CSS*/ css?: string; /** Appended after the generated CSS */ extraCss?: string; - /** Specify the className to use for the component @default sourceInline */ - className?: string; /** Add `@source inline('')` to the CSS. @default Values are extracted from the component's className */ sourceInline?: string[]; /** Whether to include the theme in the generated CSS @default true */ theme?: boolean; /** Whether to include the preflight in the generated CSS @default false */ preflight?: boolean; - /** Whether to include the plugin in the generated CSS. @default true */ - plugin?: boolean; /** Enable debug logging. @default false - Set process.env.NATIVEWIND_TEST_AUTO_DEBUG and run tests with the node inspector */ debug?: boolean | "verbose"; }; @@ -76,13 +77,7 @@ export async function render( console.log(`Input CSS:\n---\n${css}\n---\n`); } - // Process the TailwindCSS - const { css: output } = await postcss([ - /* The plugin reuses one compiler per `from` and `base`, so every render needs a `base` no other render has */ - tailwind({ base: `render-${++renderCount}` }), - ]).process(css, { - from: __dirname, - }); + const output = await compileTailwind(css); if (debug) { console.log(`Output CSS:\n---\n${output}\n---\n`); @@ -101,11 +96,38 @@ export async function render( render.debug = ( component: ReactElement, - options: RenderOptions = {}, + options: NativewindRenderOptions = {}, ) => { return render(component, { ...options, debug: true }); }; +async function compileTailwind(css: string): Promise { + const compileErrors: unknown[] = []; + // The plugin logs a failed compile and hands back an empty stylesheet: https://github.com/tailwindlabs/tailwindcss/blob/v4.1.13/packages/@tailwindcss-postcss/src/index.ts#L334-L352 + const consoleError = jest + .spyOn(console, "error") + .mockImplementation((error: unknown) => { + compileErrors.push(error); + }); + + try { + const { css: output } = await postcss([tailwind({ base })]).process(css, { + from, + }); + + if (compileErrors.length > 0) { + throw new Error( + `Tailwind could not compile the render's CSS: ${String(compileErrors[0])}`, + { cause: compileErrors[0] }, + ); + } + + return output; + } finally { + consoleError.mockRestore(); + } +} + function getClassNames( component: ReactElement, classNames = new Set(), @@ -174,6 +196,11 @@ renderSimple.debug = ( return renderSimple({ ...options, debug: true }); }; +type CurrentTestRenderOptions = NativewindRenderOptions & { + /** Specify the className to use for the component @default sourceInline */ + className?: string; +}; + /** * Helper method that uses the current test name to render the component * Doesn't not support multiple components or changing the component type @@ -182,7 +209,7 @@ export async function renderCurrentTest({ sourceInline = [expect.getState().currentTestName?.split(/\s+/).at(-1) ?? ""], className = sourceInline.join(" "), ...options -}: NativewindRenderOptions = {}) { +}: CurrentTestRenderOptions = {}) { return renderSimple({ ...options, sourceInline, @@ -190,6 +217,6 @@ export async function renderCurrentTest({ }); } -renderCurrentTest.debug = (options: NativewindRenderOptions = {}) => { +renderCurrentTest.debug = (options: CurrentTestRenderOptions = {}) => { return renderCurrentTest({ ...options, debug: true }); }; diff --git a/src/__tests__/vendor/tailwind/harness.test.tsx b/src/__tests__/vendor/tailwind/harness.test.tsx index 58b6e553..a2a60984 100644 --- a/src/__tests__/vendor/tailwind/harness.test.tsx +++ b/src/__tests__/vendor/tailwind/harness.test.tsx @@ -21,3 +21,9 @@ test("renders that read the same millisecond compile and apply their own classes now.mockRestore(); } }); + +test("a render whose CSS fails to compile rejects", async () => { + await expect( + render(, { css: '@import "./missing.css";' }), + ).rejects.toThrow("Can't resolve './missing.css'"); +});