diff --git a/docs/.astro/types.d.ts b/docs/.astro/types.d.ts index 1190ddfc..ccc2a977 100644 --- a/docs/.astro/types.d.ts +++ b/docs/.astro/types.d.ts @@ -297,6 +297,13 @@ declare module 'astro:content' { collection: "docs"; data: InferEntrySchema<"docs"> } & { render(): Render[".mdx"] }; +"reference/testing.mdx": { + id: "reference/testing.mdx"; + slug: "reference/testing"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".mdx"] }; "reference/theming.mdx": { id: "reference/theming.mdx"; slug: "reference/theming"; diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 2173934f..eed51f64 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -122,6 +122,11 @@ export default defineConfig({ label: 'Debugging', link: '/reference/debugging/' }, + { + label: 'Testing', + link: '/reference/testing/', + badge: 'New' + }, { label: 'Errors', link: '/reference/errors/' diff --git a/docs/src/content/docs/reference/testing.mdx b/docs/src/content/docs/reference/testing.mdx new file mode 100644 index 00000000..d8868073 --- /dev/null +++ b/docs/src/content/docs/reference/testing.mdx @@ -0,0 +1,66 @@ +--- +title: Testing +--- + +import Seo from '../../../components/Seo.astro' +import Badge from '../../../components/Badge.astro' + + + + + + +Unistyles ships with own mocks that will help you to test your components that consume `useStyles` and `createStyleSheet`. + +### Using mocks + +In order to use the library mocks you need to either use `jest` or set `process.env.NODE_ENV` to `test`. + +```json /NODE_ENV=test/ +"scripts": { + "start": "react-native start", + "test": "NODE_ENV=test vitest" +}, +``` + +### NativeEventEmitter + +To mock `NativeEventEmitter` that is imported internally from `react-native` you can use the following code: + +1. Create `jest.setup.js` file: + +```javascript /NativeEventEmitter/ +jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter'); +``` + +2. If you use `jest.config.js`: + +```diff lang="js" +module.exports = { + preset: 'react-native', ++ setupFiles: ['./jest.setup.js'], +}; +``` + +3. If you use `jest` in `package.json`: + +```diff lang="json" +{ + "jest": { + "preset": "react-native", ++ "setupFiles": ["./jest.setup.js"] + } +} +``` + +### Support + +Mocks support basic operations and are designed to mimic the setting of themes and breakpoints. +However, some operations, such as using `plugins` or `adaptiveThemes`, are not supported. + + diff --git a/src/common.ts b/src/common.ts index 06714935..c71955e5 100644 --- a/src/common.ts +++ b/src/common.ts @@ -10,6 +10,7 @@ export const isAndroid = Platform.OS === 'android' export const isMobile = isIOS || isAndroid export const isServer = typeof window === 'undefined' export const isDev = process.env.NODE_ENV !== 'production' +export const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined || typeof jest !== 'undefined' export const ScreenOrientation = { Landscape: 'landscape', diff --git a/src/core/Unistyles.ts b/src/core/Unistyles.ts index b1b3dc06..21065c06 100644 --- a/src/core/Unistyles.ts +++ b/src/core/Unistyles.ts @@ -2,7 +2,8 @@ import { UnistylesModule } from './UnistylesModule' import { UnistylesRuntime } from './UnistylesRuntime' import { UnistyleRegistry } from './UnistyleRegistry' import type { UnistylesBridge } from '../types' -import { UnistylesError, isWeb } from '../common' +import { UnistylesError, isTest, isWeb } from '../common' +import { UnistylesMockedBridge, UnistylesMockedRegistry, UnistylesMockedRuntime } from './mocks' class Unistyles { private _runtime: UnistylesRuntime @@ -10,6 +11,14 @@ class Unistyles { private _bridge: UnistylesBridge constructor() { + if (isTest) { + this._bridge = new UnistylesMockedBridge() as unknown as UnistylesBridge + this._registry = new UnistylesMockedRegistry(this._bridge) as unknown as UnistyleRegistry + this._runtime = new UnistylesMockedRuntime(this._bridge, this._registry) as unknown as UnistylesRuntime + + return + } + const isInstalled = UnistylesModule?.install() ?? false if (!isInstalled) { diff --git a/src/core/mocks/UnistylesMockedBridge.ts b/src/core/mocks/UnistylesMockedBridge.ts new file mode 100644 index 00000000..d167a5ad --- /dev/null +++ b/src/core/mocks/UnistylesMockedBridge.ts @@ -0,0 +1,26 @@ +// @ts-nocheck +import type { UnistylesThemes, UnistylesBreakpoints } from '../../global' + +export class UnistylesMockedBridge { + constructor() {} + + public screenWidth() {} + public screenHeight() {} + public enabledPlugins() {} + public hasAdaptiveThemes() {} + public themeName() {} + public breakpoint() {} + public colorScheme() {} + public contentSizeCategory() {} + public sortedBreakpointPairs() {} + public insets() {} + public statusBar() {} + public navigationBar() {} + public themes(themes: Array) {} + public useBreakpoints(breakpoints: UnistylesBreakpoints) {} + public useTheme(name: keyof UnistylesThemes) {} + public updateTheme(name: keyof UnistylesThemes) {} + public useAdaptiveThemes(enable: boolean) {} + public addPlugin(pluginName: string, notify: boolean) {} + public removePlugin(pluginName: string) {} +} diff --git a/src/core/mocks/UnistylesMockedRegistry.ts b/src/core/mocks/UnistylesMockedRegistry.ts new file mode 100644 index 00000000..2552e60d --- /dev/null +++ b/src/core/mocks/UnistylesMockedRegistry.ts @@ -0,0 +1,47 @@ +// @ts-nocheck +import type { UnistylesBreakpoints, UnistylesThemes } from '../../global' +import type { UnistylesPlugin, UnistylesConfig, UnistylesBridge } from '../../types' + +export class UnistylesMockedRegistry { + public config: UnistylesConfig = {} + public breakpoints: UnistylesBreakpoints = {} as UnistylesBreakpoints + public sortedBreakpointPairs: Array<[keyof UnistylesBreakpoints, UnistylesBreakpoints[keyof UnistylesBreakpoints]]> = [] + public plugins: Array = [] + public themes: UnistylesThemes = {} as UnistylesThemes + public themeNames: Array = [] + + constructor(private unistylesBridge: UnistylesBridge) {} + + public addThemes = (themes: UnistylesThemes) => { + this.themes = themes + this.themeNames = Object.keys(themes) as Array + + return this + } + public addBreakpoints = (breakpoints: UnistylesBreakpoints) => { + this.breakpoints = breakpoints + this.sortedBreakpointPairs = Object + .entries(breakpoints) + .sort((breakpoint1, breakpoint2) => { + const [, value1] = breakpoint1 + const [, value2] = breakpoint2 + + return value1 - value2 + }) as Array<[keyof UnistylesBreakpoints, UnistylesBreakpoints[keyof UnistylesBreakpoints]]> + + return this + } + public addConfig = (config: UnistylesConfig) => {} + public getTheme = (forName: keyof UnistylesThemes) => { + if (this.themeNames.length === 0) { + return {} as UnistylesThemes[keyof UnistylesThemes] + } + + return this.themes[forName] + } + + public addPlugin = (plugin: UnistylesPlugin, notify: boolean = true) => {} + public removePlugin = (plugin: UnistylesPlugin) => {} + public updateTheme = (name: keyof UnistylesThemes, theme: UnistylesThemes[keyof UnistylesThemes]) => {} + public hasTheme = (name: keyof UnistylesThemes) => true +} diff --git a/src/core/mocks/UnistylesMockedRuntime.ts b/src/core/mocks/UnistylesMockedRuntime.ts new file mode 100644 index 00000000..b76e20ba --- /dev/null +++ b/src/core/mocks/UnistylesMockedRuntime.ts @@ -0,0 +1,92 @@ +// @ts-nocheck +import { ScreenOrientation } from '../../common' +import type { UnistylesBridge, UnistylesPlugin } from '../../types' +import type { UnistylesThemes } from '../../global' +import type { UnistylesMockedRegistry } from './UnistylesMockedRegistry' +import type { UnistyleRegistry } from '../UnistyleRegistry' + +export class UnistylesMockedRuntime { + private unistylesRegistry: UnistylesMockedRegistry + + constructor(private unistylesBridge: UnistylesBridge, private unistylesRegistry: UnistyleRegistry) { + this.unistylesRegistry = unistylesRegistry as unknown as UnistylesMockedRegistry + } + + public get colorScheme() { + return 'dark' + } + + public get hasAdaptiveThemes() { + return true + } + + public get themeName() { + return this.unistylesRegistry.themeNames.length > 0 + ? this.unistylesRegistry.themeNames.at(0) + : undefined + } + + public get contentSizeCategory() { + return 'unspecified' + } + + public get breakpoint() { + if (this.unistylesRegistry.sortedBreakpointPairs.length === 0) { + return undefined + } + + const firstBreakpoint = this.unistylesRegistry.sortedBreakpointPairs.at(0) + + return firstBreakpoint + ? firstBreakpoint.at(0) + : undefined + } + + public get breakpoints() { + return this.unistylesRegistry.breakpoints + } + + public get enabledPlugins() { + return this.unistylesRegistry.plugins + } + + public get screen() { + return { + width: 360, + height: 800 + } + } + + public get insets() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + } + + public get statusBar() { + return { + height: 24, + width: 800 + } + } + + public get navigationBar() { + return { + height: 0, + width: 0 + } + } + + public get orientation() { + return ScreenOrientation.Portrait + } + + public setTheme = (name: keyof UnistylesThemes) => true + public updateTheme = (name: keyof UnistylesThemes, theme: UnistylesThemes[keyof UnistylesThemes]) => {} + public setAdaptiveThemes = (enabled: boolean) => {} + public addPlugin = (plugin: UnistylesPlugin) => {} + public removePlugin = (plugin: UnistylesPlugin) => {} +} diff --git a/src/core/mocks/index.ts b/src/core/mocks/index.ts new file mode 100644 index 00000000..5af4f25a --- /dev/null +++ b/src/core/mocks/index.ts @@ -0,0 +1,3 @@ +export { UnistylesMockedBridge } from './UnistylesMockedBridge' +export { UnistylesMockedRegistry } from './UnistylesMockedRegistry' +export { UnistylesMockedRuntime } from './UnistylesMockedRuntime'