/** @jsxImportSource nativewind */ import { Platform, View } from "react-native"; import tailwindcssContainerQueries from "@tailwindcss/container-queries"; import postcss from "postcss"; import { getWarnings, render as interopRender, RenderOptions as InteropRenderOptions, resetData, screen, setupAllComponents, } from "react-native-css-interop/test"; import tailwind, { Config } from "tailwindcss"; import { cssToReactNativeRuntimeOptions } from "./metro/common"; export { act, createMockComponent, screen, fireEvent, within, native, INTERNAL_SET, } from "react-native-css-interop/test"; export * from "./index"; const testID = "nativewind"; beforeEach(() => { resetData(); setupAllComponents(); }); // I don't know why I can't use Omit. It narrows the type too much? export type ConfigWithoutContent = { [K in keyof Config as K extends "content" ? never : K]: Config[K]; }; export interface RenderOptions extends InteropRenderOptions { config?: ConfigWithoutContent; css?: string; layers?: { base?: boolean; components?: boolean; utilities?: boolean; }; } export type RenderCurrentTestOptions = RenderOptions & { className?: string; }; process.env.NATIVEWIND_OS = Platform.OS; export async function renderCurrentTest({ className = expect.getState().currentTestName?.split(/\s+/).at(-1), ...options }: RenderCurrentTestOptions = {}) { if (!className) { throw new Error( "unable to detect className, please manually set a className", ); } await render(, options); const component = screen.getByTestId(testID, { hidden: true }); // Strip the testID and the children const { testID: _testID, children, ...props } = component.props; const invalid = getInvalid(); if (invalid) { return { props, invalid }; } else { return { props }; } } renderCurrentTest.debug = (options: RenderCurrentTestOptions = {}) => { return renderCurrentTest({ ...options, debugCompiled: true }); }; export async function render( component: React.ReactElement, { config, css, layers = {}, debugCompiled = process.env.NODE_OPTIONS?.includes("--inspect"), ...options }: RenderOptions = {}, ) { // Compile the base CSS, e.g: // @tailwind base // @tailwind components // @tailwind utilities css ??= Object.entries({ base: true, components: true, utilities: true, ...layers, }).reduce((acc, [layer, enabled]) => { return enabled ? `${acc}@tailwind ${layer};` : acc; }, ""); const content = getClassNames(component); if (debugCompiled) { const classNames = content.map(({ raw }) => ` ${raw}`); console.log(`Detected classNames:\n${classNames.join("\n")}\n\n`); if (config?.safelist) { console.log(`Detected safelist:\n${config.safelist.join("\n")}\n\n`); } } // Process the TailwindCSS let { css: output } = await postcss([ tailwind({ theme: {}, ...config, presets: [require("./tailwind")], plugins: [tailwindcssContainerQueries, ...(config?.plugins || [])], content, }), ]).process(css, { from: undefined }); return interopRender(component, { ...options, css: output, cssOptions: cssToReactNativeRuntimeOptions, debugCompiled: debugCompiled, }); } render.debug = ( component: React.ReactElement, options: RenderOptions = {}, ) => { return render(component, { ...options, debugCompiled: true }); }; render.noDebug = ( component: React.ReactElement, options: RenderOptions = {}, ) => { return render(component, { ...options, debugCompiled: false }); }; function getClassNames( component: React.ReactElement, ): Array<{ raw: string; extension?: string }> { const classNames: Array<{ raw: string; extension?: string }> = []; if (component.props?.className) { classNames.push({ raw: component.props.className }); } if (component.props?.children) { const children: React.ReactElement[] = Array.isArray( component.props.children, ) ? component.props.children : [component.props.children]; classNames.push(...children.flatMap((c) => getClassNames(c))); } return classNames; } function getInvalid() { const style: Record = {}; const properties: string[] = []; let hasStyles = false; for (const warnings of getWarnings().values()) { for (const warning of warnings) { switch (warning.type) { case "IncompatibleNativeProperty": properties.push(warning.property); break; case "IncompatibleNativeValue": { hasStyles = true; style[warning.property] = warning.value; break; } case "IncompatibleNativeFunctionValue": // TODO } } } if (properties.length && hasStyles) { return { style, properties, }; } else if (properties.length) { return { properties }; } else if (hasStyles) { return { style }; } } export function invalidProperty(...properties: string[]) { return properties.map((property) => ({ type: "IncompatibleNativeProperty", property, })); } export function invalidValue(value: Record) { return Object.entries(value).map(([property, value]) => ({ type: "IncompatibleNativeValue", property, value, })); }