From e0d3a899f5f332cbaa6162d9727b22211de051b8 Mon Sep 17 00:00:00 2001 From: Divine Niiquaye Ibok Date: Sat, 24 Jan 2026 20:45:12 +0000 Subject: [PATCH 1/2] feat: CSS variables with media queries handling --- packages/uniwind/src/metro/processor/mq.ts | 20 ++-- .../uniwind/src/metro/processor/processor.ts | 96 ++++++++++++++----- packages/uniwind/src/metro/types.ts | 4 +- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/packages/uniwind/src/metro/processor/mq.ts b/packages/uniwind/src/metro/processor/mq.ts index 5405b8f6..7533a57a 100644 --- a/packages/uniwind/src/metro/processor/mq.ts +++ b/packages/uniwind/src/metro/processor/mq.ts @@ -1,4 +1,4 @@ -import { MediaQuery, QueryFeatureFor_MediaFeatureId } from 'lightningcss' +import { MediaCondition, MediaQuery, QueryFeatureFor_MediaFeatureId } from 'lightningcss' import { ColorScheme, Orientation } from '../../types' import { MediaQueryResolver, Platform } from '../types' import type { ProcessorBuilder } from './processor' @@ -18,10 +18,18 @@ export class MQ { return } - if (condition?.type !== 'feature') { - return - } + if (condition) this.processCondition(condition, mq) + }) + + return mq + } + private processCondition(condition: MediaCondition, mq: MediaQueryResolver) { + if (condition.type === 'operation') { + condition.conditions.forEach(nestedCondition => { + this.processCondition(nestedCondition, mq) + }) + } else if (condition.type === 'feature') { if (condition.value.type === 'range') { this.processWidthMediaQuery(condition.value, mq) } @@ -29,9 +37,7 @@ export class MQ { if (condition.value.type === 'plain') { this.processPlainMediaQuery(condition.value, mq) } - }) - - return mq + } } private processWidthMediaQuery(query: QueryFeatureFor_MediaFeatureId & { type: 'range' }, mq: MediaQueryResolver) { diff --git a/packages/uniwind/src/metro/processor/processor.ts b/packages/uniwind/src/metro/processor/processor.ts index 15387247..9aa81615 100644 --- a/packages/uniwind/src/metro/processor/processor.ts +++ b/packages/uniwind/src/metro/processor/processor.ts @@ -1,5 +1,5 @@ import { Declaration, MediaQuery, Rule, transform } from 'lightningcss' -import { Polyfills, ProcessMetaValues } from '../types' +import { MediaQueryResolver, Polyfills, ProcessMetaValues } from '../types' import { Color } from './color' import { CSS } from './css' import { Functions } from './functions' @@ -21,6 +21,9 @@ export class ProcessorBuilder { Functions = new Functions(this) meta = {} as ProcessMetaValues + private varsWithMediaQueries = {} as Record> + private pendingVarReferences = new Map>() + private declarationConfig = this.getDeclarationConfig() constructor(private readonly themes: Array, readonly polyfills: Polyfills | undefined) { @@ -32,11 +35,27 @@ export class ProcessorBuilder { filename: 'tailwind.css', code: Buffer.from(css), visitor: { - StyleSheet: styleSheet => + StyleSheet: styleSheet => { styleSheet.rules.forEach(rule => { this.declarationConfig = this.getDeclarationConfig() this.parseRuleRec(rule) - }), + }) + + for (const [className, varNames] of this.pendingVarReferences) { + for (const varName of varNames) { + const varStyles = this.varsWithMediaQueries[varName] + if (!varStyles || varStyles.length === 0) { + continue + } + + for (const varStyle of varStyles) { + this.stylesheets[className]!.push(varStyle) + } + } + + this.pendingVarReferences.delete(className) + } + }, }, }) } @@ -54,9 +73,14 @@ export class ProcessorBuilder { }) } + private hasMediaQuery(mq: MediaQueryResolver): boolean { + return mq.minWidth !== 0 || mq.maxWidth !== Number.MAX_VALUE || mq.orientation !== null || mq.colorScheme !== null + } + private addDeclaration(declaration: Declaration, important = false) { const isVar = this.declarationConfig.root || this.declarationConfig.className === null const mq = this.MQ.processMediaQueries(this.declarationConfig.mediaQueries) + const { property, value } = this.parseDeclaration(declaration) const style = (() => { if (!isVar) { return this.stylesheets[this.declarationConfig.className!]?.at(-1) @@ -69,6 +93,12 @@ export class ProcessorBuilder { return this.scopedVars[platformKey] } + if (this.hasMediaQuery(mq)) { + this.varsWithMediaQueries[property] ??= [] + this.varsWithMediaQueries[property].push({}) + return this.varsWithMediaQueries[property].at(-1) + } + if (this.declarationConfig.theme === null) { return this.vars } @@ -79,41 +109,65 @@ export class ProcessorBuilder { return this.scopedVars[themeKey] })() - if (!isVar) { + if (!isVar || this.hasMediaQuery(mq)) { Object.assign(style, mq) style.importantProperties ??= [] style.rtl = this.declarationConfig.rtl style.theme = mq.colorScheme ?? this.declarationConfig.theme + style.maxWidth = mq.maxWidth + style.minWidth = mq.minWidth + style.orientation = mq.orientation style.active = this.declarationConfig.active style.focus = this.declarationConfig.focus style.disabled = this.declarationConfig.disabled this.meta.className = this.declarationConfig.className } - if (declaration.property === 'unparsed') { - style[declaration.value.propertyId.property] = this.CSS.processValue(declaration.value.value) + style[property] = value + if (!isVar && important) { + style.importantProperties.push(property) + } + + // Track variable references for later processing (even if media queries don't exist yet) + const match = typeof value === 'string' ? value.match(/this\[`(.*?)`\]/) : null - if (!isVar && important) { - style.importantProperties.push(declaration.value.propertyId.property) + if (match && !isVar) { + const className = this.declarationConfig.className + if (className === null) { + return } - return - } + if (!this.pendingVarReferences.has(className)) { + this.pendingVarReferences.set(className, []) + } - if (declaration.property === 'custom') { - style[declaration.value.name] = this.CSS.processValue(declaration.value.value) + const classVars = this.pendingVarReferences.get(className)! + const varName = match[1]! - if (!isVar && important) { - style.importantProperties.push(declaration.value.name) + if (!classVars.includes(varName)) { + classVars.push(varName) } + } + } - return + private parseDeclaration(declaration: Declaration) { + if (declaration.property === 'unparsed') { + return { + property: declaration.value.propertyId.property, + value: this.CSS.processValue(declaration.value.value), + } } - style[declaration.property] = this.CSS.processValue(declaration.value) + if (declaration.property === 'custom') { + return { + property: declaration.value.name, + value: this.CSS.processValue(declaration.value.value), + } + } - if (!isVar && important) { - style.importantProperties.push(declaration.property) + return { + property: declaration.property, + value: this.CSS.processValue(declaration.value), } } @@ -221,10 +275,8 @@ export class ProcessorBuilder { const { mediaQueries } = rule.value.query this.declarationConfig.mediaQueries.push(...mediaQueries) - rule.value.rules.forEach(rule => { - this.parseRuleRec(rule) - this.declarationConfig = this.getDeclarationConfig() - }) + rule.value.rules.forEach(rule => this.parseRuleRec(rule)) + this.declarationConfig = this.getDeclarationConfig() return } diff --git a/packages/uniwind/src/metro/types.ts b/packages/uniwind/src/metro/types.ts index 4f5a12a8..747fd333 100644 --- a/packages/uniwind/src/metro/types.ts +++ b/packages/uniwind/src/metro/types.ts @@ -27,8 +27,8 @@ export type UniwindConfig = { } export type MediaQueryResolver = { - maxWidth: any - minWidth: any + maxWidth: number | null + minWidth: number | null platform: Platform | null rtl: boolean | null important: boolean From fa11129099cc7a6ba4a1305a0870e0689a308a39 Mon Sep 17 00:00:00 2001 From: Divine Niiquaye Ibok Date: Sat, 24 Jan 2026 20:51:11 +0000 Subject: [PATCH 2/2] feat: add jest tests for parsing media queries --- .../tests/media-queries/color-scheme.test.css | 19 ++ .../tests/media-queries/color-scheme.test.tsx | 84 ++++++++ .../tests/media-queries/combined.test.css | 30 +++ .../tests/media-queries/combined.test.tsx | 121 +++++++++++ .../tests/media-queries/max-width.test.css | 30 +++ .../tests/media-queries/max-width.test.tsx | 126 ++++++++++++ .../tests/media-queries/min-width.test.css | 54 +++++ .../tests/media-queries/min-width.test.tsx | 188 ++++++++++++++++++ .../tests/media-queries/orientation.test.css | 25 +++ .../tests/media-queries/orientation.test.tsx | 114 +++++++++++ .../tests/media-queries/variables.test.css | 16 ++ .../tests/media-queries/variables.test.tsx | 146 ++++++++++++++ 12 files changed, 953 insertions(+) create mode 100644 packages/uniwind/tests/media-queries/color-scheme.test.css create mode 100644 packages/uniwind/tests/media-queries/color-scheme.test.tsx create mode 100644 packages/uniwind/tests/media-queries/combined.test.css create mode 100644 packages/uniwind/tests/media-queries/combined.test.tsx create mode 100644 packages/uniwind/tests/media-queries/max-width.test.css create mode 100644 packages/uniwind/tests/media-queries/max-width.test.tsx create mode 100644 packages/uniwind/tests/media-queries/min-width.test.css create mode 100644 packages/uniwind/tests/media-queries/min-width.test.tsx create mode 100644 packages/uniwind/tests/media-queries/orientation.test.css create mode 100644 packages/uniwind/tests/media-queries/orientation.test.tsx create mode 100644 packages/uniwind/tests/media-queries/variables.test.css create mode 100644 packages/uniwind/tests/media-queries/variables.test.tsx diff --git a/packages/uniwind/tests/media-queries/color-scheme.test.css b/packages/uniwind/tests/media-queries/color-scheme.test.css new file mode 100644 index 00000000..60d390b3 --- /dev/null +++ b/packages/uniwind/tests/media-queries/color-scheme.test.css @@ -0,0 +1,19 @@ +@import 'tailwindcss'; +@import 'uniwind'; + +@layer theme { + :root { + --color-primary: #000000; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --color-primary: #ffffff; + } +} + +.text-color-primary { + color: var(--color-primary); +} + diff --git a/packages/uniwind/tests/media-queries/color-scheme.test.tsx b/packages/uniwind/tests/media-queries/color-scheme.test.tsx new file mode 100644 index 00000000..60423cf3 --- /dev/null +++ b/packages/uniwind/tests/media-queries/color-scheme.test.tsx @@ -0,0 +1,84 @@ +import { act } from '@testing-library/react-native' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import * as React from 'react' +import View from '../../src/components/native/View' +import { UniwindListener } from '../../src/core/listener' +import { UniwindStore } from '../../src/core/native/store' +import { compileVirtual } from '../../src/metro/compileVirtual' +import { Platform } from '../../src/metro/types' +import { ColorScheme, Orientation, StyleDependency } from '../../src/types' +import { renderUniwind } from '../utils' + +describe('CSS Variables - Color Scheme Media Queries', () => { + beforeAll(async () => { + const cssPath = resolve('./tests/media-queries/color-scheme.test.css') + const css = readFileSync(cssPath, 'utf-8') + const virtualCode = await compileVirtual({ + css, + cssPath, + debug: true, + platform: Platform.iOS, + themes: ['light', 'dark'], + polyfills: undefined, + }) + + eval( + `const { Uniwind } = require('../../src/core/config/config.native'); + Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); + `, + ) + }) + + beforeEach(() => { + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.runtime.colorScheme = ColorScheme.Light + UniwindStore.runtime.currentThemeName = ColorScheme.Light + UniwindStore.reinit() + }) + + test('uses default value in light color scheme', () => { + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('color-primary').color).toBe('#000000') + }) + + test('uses media query value in dark color scheme', () => { + UniwindStore.runtime.currentThemeName = ColorScheme.Dark + UniwindStore.runtime.colorScheme = ColorScheme.Dark + UniwindListener.notify([StyleDependency.Theme, StyleDependency.ColorScheme]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('color-primary').color).toBe('#ffffff') + }) + + test('updates when color scheme changes', () => { + UniwindStore.runtime.currentThemeName = ColorScheme.Light + UniwindStore.runtime.colorScheme = ColorScheme.Light + UniwindListener.notify([StyleDependency.Theme, StyleDependency.ColorScheme]) + + let { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('color-primary').color).toBe('#000000') + + UniwindStore.runtime.currentThemeName = ColorScheme.Dark + UniwindStore.runtime.colorScheme = ColorScheme.Dark + act(() => { + UniwindListener.notify([StyleDependency.Theme, StyleDependency.ColorScheme]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + + expect(getStylesFromId2('color-primary').color).toBe('#ffffff') + }) +}) diff --git a/packages/uniwind/tests/media-queries/combined.test.css b/packages/uniwind/tests/media-queries/combined.test.css new file mode 100644 index 00000000..5308373c --- /dev/null +++ b/packages/uniwind/tests/media-queries/combined.test.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; +@import 'uniwind'; + +@layer theme { + :root { + --text-xl: 1.5rem; + --border-width: 1px; + } +} + +@media (min-width: 768px) and (orientation: landscape) { + :root { + --text-xl: 6rem; + } +} + +@media (min-width: 640px) and (prefers-color-scheme: dark) { + :root { + --border-width: 2px; + } +} + +.text-xl { + font-size: var(--text-xl); +} + +.border-custom { + border-width: var(--border-width); +} + diff --git a/packages/uniwind/tests/media-queries/combined.test.tsx b/packages/uniwind/tests/media-queries/combined.test.tsx new file mode 100644 index 00000000..5b095394 --- /dev/null +++ b/packages/uniwind/tests/media-queries/combined.test.tsx @@ -0,0 +1,121 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' +import * as React from 'react' +import View from '../../src/components/native/View' +import { UniwindListener } from '../../src/core/listener' +import { UniwindStore } from '../../src/core/native/store' +import { compileVirtual } from '../../src/metro/compileVirtual' +import { Platform } from '../../src/metro/types' +import { ColorScheme, Orientation, StyleDependency } from '../../src/types' +import { renderUniwind } from '../utils' + +describe('CSS Variables - Combined Media Queries', () => { + beforeAll(async () => { + const cssPath = resolve('./tests/media-queries/combined.test.css') + const css = readFileSync(cssPath, 'utf-8') + const virtualCode = await compileVirtual({ + css, + cssPath, + debug: true, + platform: Platform.iOS, + themes: ['light', 'dark'], + polyfills: undefined, + }) + + eval( + `const { Uniwind } = require('../../src/core/config/config.native'); + Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); + `, + ) + }) + + beforeEach(() => { + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.runtime.colorScheme = ColorScheme.Light + UniwindStore.runtime.currentThemeName = ColorScheme.Light + UniwindStore.reinit() + }) + + test('matches when both min-width and orientation conditions are met', () => { + UniwindStore.runtime.screen = { width: 800, height: 375 } + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindListener.notify([StyleDependency.Dimensions, StyleDependency.Orientation]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-xl').fontSize).toBe(96) + }) + + test('uses default when min-width matches but orientation does not', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-xl').fontSize).toBe(24) + }) + + test('matches when both min-width and color-scheme conditions are met', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindStore.runtime.colorScheme = ColorScheme.Dark + UniwindStore.runtime.currentThemeName = ColorScheme.Dark + UniwindListener.notify([StyleDependency.Dimensions, StyleDependency.ColorScheme, StyleDependency.Theme]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('border-width').borderWidth).toBe(2) + }) + + test('uses default when min-width matches but color-scheme does not', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('border-width').borderWidth).toBe(1) + }) + + test('uses default when orientation matches but min-width does not', () => { + UniwindStore.runtime.screen = { width: 500, height: 300 } + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindListener.notify([StyleDependency.Dimensions, StyleDependency.Orientation]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-xl').fontSize).toBe(24) + }) + + test('handles complex scenario with all conditions', () => { + UniwindStore.runtime.screen = { width: 800, height: 400 } + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindStore.runtime.colorScheme = ColorScheme.Dark + UniwindStore.runtime.currentThemeName = ColorScheme.Dark + UniwindListener.notify([ + StyleDependency.Dimensions, + StyleDependency.Orientation, + StyleDependency.ColorScheme, + StyleDependency.Theme, + ]) + + const { getStylesFromId } = renderUniwind( + + + + , + ) + + expect(getStylesFromId('text-xl').fontSize).toBe(96) + expect(getStylesFromId('border-width').borderWidth).toBe(2) + }) +}) diff --git a/packages/uniwind/tests/media-queries/max-width.test.css b/packages/uniwind/tests/media-queries/max-width.test.css new file mode 100644 index 00000000..3d258a1f --- /dev/null +++ b/packages/uniwind/tests/media-queries/max-width.test.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; +@import 'uniwind'; + +@layer theme { + :root { + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --text-base: 1rem; + } +} + +@media (max-width: 480px) { + :root { + --spacing-sm: 0.25rem; + --spacing-md: 0.75rem; + } +} + +.p-spacing-sm { + padding: var(--spacing-sm); +} + +.p-spacing-md { + padding: var(--spacing-md); +} + +.text-base { + font-size: var(--text-base); +} + diff --git a/packages/uniwind/tests/media-queries/max-width.test.tsx b/packages/uniwind/tests/media-queries/max-width.test.tsx new file mode 100644 index 00000000..94cb3aba --- /dev/null +++ b/packages/uniwind/tests/media-queries/max-width.test.tsx @@ -0,0 +1,126 @@ +import { act } from '@testing-library/react-native' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import * as React from 'react' +import View from '../../src/components/native/View' +import { UniwindListener } from '../../src/core/listener' +import { UniwindStore } from '../../src/core/native/store' +import { compileVirtual } from '../../src/metro/compileVirtual' +import { Platform } from '../../src/metro/types' +import { Orientation, StyleDependency } from '../../src/types' +import { renderUniwind } from '../utils' + +describe('CSS Variables - Max-Width Media Queries', () => { + beforeAll(async () => { + const cssPath = resolve('./tests/media-queries/max-width.test.css') + const css = readFileSync(cssPath, 'utf-8') + const virtualCode = await compileVirtual({ + css, + cssPath, + debug: true, + platform: Platform.iOS, + themes: ['light', 'dark'], + polyfills: undefined, + }) + + eval( + `const { Uniwind } = require('../../src/core/config/config.native'); + Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); + `, + ) + }) + + beforeEach(() => { + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.reinit() + }) + + test('uses default value when screen width is above max-width threshold', () => { + UniwindStore.runtime.screen = { width: 600, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('spacing-sm').padding).toBe(8) + }) + + test('uses media query value when screen width is below max-width threshold', () => { + UniwindStore.runtime.screen = { width: 400, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('spacing-sm').padding).toBe(4) + }) + + test('uses media query value when screen width equals max-width threshold', () => { + UniwindStore.runtime.screen = { width: 480, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('spacing-sm').padding).toBe(4) + }) + + test('handles multiple variables with max-width', () => { + UniwindStore.runtime.screen = { width: 400, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + + + + , + ) + + expect(getStylesFromId('spacing-sm').padding).toBe(4) + expect(getStylesFromId('spacing-md').padding).toBe(12) + }) + + test('updates when screen width changes from above to below threshold', () => { + UniwindStore.runtime.screen = { width: 600, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + let { getStylesFromId } = renderUniwind( + , + ) + expect(getStylesFromId('spacing-sm').padding).toBe(8) + + UniwindStore.runtime.screen = { width: 400, height: 667 } + act(() => { + UniwindListener.notify([StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + expect(getStylesFromId2('spacing-sm').padding).toBe(4) + }) + + test('updates when screen width changes from below to above threshold', () => { + UniwindStore.runtime.screen = { width: 400, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + let { getStylesFromId } = renderUniwind( + , + ) + expect(getStylesFromId('spacing-sm').padding).toBe(4) + + UniwindStore.runtime.screen = { width: 600, height: 667 } + act(() => { + UniwindListener.notify([StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + expect(getStylesFromId2('spacing-sm').padding).toBe(8) + }) +}) diff --git a/packages/uniwind/tests/media-queries/min-width.test.css b/packages/uniwind/tests/media-queries/min-width.test.css new file mode 100644 index 00000000..03775037 --- /dev/null +++ b/packages/uniwind/tests/media-queries/min-width.test.css @@ -0,0 +1,54 @@ +@import 'tailwindcss'; +@import 'uniwind'; + +@layer theme { + :root { + --text-base: 1rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + } +} + +@media (min-width: 320px) { + :root { + --text-base: 1.5rem; + } +} + +@media (min-width: 640px) { + :root { + --text-base: 2rem; + --text-lg: 3rem; + } +} + +@media (min-width: 1024px) { + :root { + --text-base: 3rem; + --text-lg: 4rem; + --text-xl: 5rem; + } +} + +.text-base { + font-size: var(--text-base); +} + +.text-lg { + font-size: var(--text-lg); +} + +.text-xl { + font-size: var(--text-xl); +} + +.p-spacing-sm { + padding: var(--spacing-sm); +} + +.p-spacing-md { + padding: var(--spacing-md); +} + diff --git a/packages/uniwind/tests/media-queries/min-width.test.tsx b/packages/uniwind/tests/media-queries/min-width.test.tsx new file mode 100644 index 00000000..b36cc28b --- /dev/null +++ b/packages/uniwind/tests/media-queries/min-width.test.tsx @@ -0,0 +1,188 @@ +import { act } from '@testing-library/react-native' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import * as React from 'react' +import View from '../../src/components/native/View' +import { UniwindListener } from '../../src/core/listener' +import { UniwindStore } from '../../src/core/native/store' +import { compileVirtual } from '../../src/metro/compileVirtual' +import { Platform } from '../../src/metro/types' +import { Orientation, StyleDependency } from '../../src/types' +import { renderUniwind } from '../utils' + +describe('CSS Variables - Min-Width Media Queries', () => { + beforeAll(async () => { + const cssPath = resolve('./tests/media-queries/min-width.test.css') + const css = readFileSync(cssPath, 'utf-8') + const virtualCode = await compileVirtual({ + css, + cssPath, + debug: true, + platform: Platform.iOS, + themes: ['light', 'dark'], + polyfills: undefined, + }) + + eval( + `const { Uniwind } = require('../../src/core/config/config.native'); + Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); + `, + ) + }) + + beforeEach(() => { + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.reinit() + }) + + test('uses default value when screen width is below first threshold', () => { + UniwindStore.runtime.screen = { width: 300, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + }) + + test('uses media query value when screen width is above first threshold', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + }) + + test('uses media query value when screen width equals threshold', () => { + UniwindStore.runtime.screen = { width: 640, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + }) + + test('uses highest matching breakpoint value', () => { + UniwindStore.runtime.screen = { width: 1200, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + + + + + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(48) + expect(getStylesFromId('text-lg').fontSize).toBe(64) + expect(getStylesFromId('text-xl').fontSize).toBe(80) + }) + + test('handles multiple variables with same media query', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + + + + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + expect(getStylesFromId('text-lg').fontSize).toBe(48) + }) + + test('handles multiple breakpoints with different values', () => { + UniwindStore.runtime.screen = { width: 500, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(24) + }) + + test('prefers higher minWidth when multiple media queries match', () => { + UniwindStore.runtime.screen = { width: 900, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + }) + + test('updates when screen width changes across multiple breakpoints', () => { + UniwindStore.runtime.screen = { width: 400, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + expect(getStylesFromId('text-base').fontSize).toBe(24) + + UniwindStore.runtime.screen = { width: 800, height: 667 } + act(() => { + UniwindListener.notify([StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + expect(getStylesFromId2('text-base').fontSize).toBe(32) + + UniwindStore.runtime.screen = { width: 1200, height: 667 } + act(() => { + UniwindListener.notify([StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId3 } = renderUniwind( + , + ) + expect(getStylesFromId3('text-base').fontSize).toBe(48) + }) + + test('handles very small screen widths', () => { + UniwindStore.runtime.screen = { width: 200, height: 300 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + }) + + test('handles very large screen widths', () => { + UniwindStore.runtime.screen = { width: 2000, height: 1500 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(48) + }) + + test('handles exact breakpoint boundaries', () => { + UniwindStore.runtime.screen = { width: 1024, height: 768 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(48) + }) +}) diff --git a/packages/uniwind/tests/media-queries/orientation.test.css b/packages/uniwind/tests/media-queries/orientation.test.css new file mode 100644 index 00000000..ab2d824d --- /dev/null +++ b/packages/uniwind/tests/media-queries/orientation.test.css @@ -0,0 +1,25 @@ +@import 'tailwindcss'; +@import 'uniwind'; + +@layer theme { + :root { + --text-base: 1rem; + --spacing-md: 1rem; + } +} + +@media (orientation: landscape) { + :root { + --text-base: 2.5rem; + --spacing-md: 1.5rem; + } +} + +.text-base { + font-size: var(--text-base); +} + +.p-spacing-md { + padding: var(--spacing-md); +} + diff --git a/packages/uniwind/tests/media-queries/orientation.test.tsx b/packages/uniwind/tests/media-queries/orientation.test.tsx new file mode 100644 index 00000000..b8a00a4e --- /dev/null +++ b/packages/uniwind/tests/media-queries/orientation.test.tsx @@ -0,0 +1,114 @@ +import { act } from '@testing-library/react-native' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import * as React from 'react' +import View from '../../src/components/native/View' +import { UniwindListener } from '../../src/core/listener' +import { UniwindStore } from '../../src/core/native/store' +import { compileVirtual } from '../../src/metro/compileVirtual' +import { Platform } from '../../src/metro/types' +import { Orientation, StyleDependency } from '../../src/types' +import { renderUniwind } from '../utils' + +describe('CSS Variables - Orientation Media Queries', () => { + beforeAll(async () => { + const cssPath = resolve('./tests/media-queries/orientation.test.css') + const css = readFileSync(cssPath, 'utf-8') + const virtualCode = await compileVirtual({ + css, + cssPath, + debug: true, + platform: Platform.iOS, + themes: ['light', 'dark'], + polyfills: undefined, + }) + + eval( + `const { Uniwind } = require('../../src/core/config/config.native'); + Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); + `, + ) + }) + + beforeEach(() => { + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.reinit() + }) + + test('uses default value in portrait orientation', () => { + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindListener.notify([StyleDependency.Orientation, StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + }) + + test('uses media query value in landscape orientation', () => { + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindStore.runtime.screen = { width: 667, height: 375 } + UniwindListener.notify([StyleDependency.Orientation, StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(40) + }) + + test('updates when orientation changes', () => { + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindListener.notify([StyleDependency.Orientation, StyleDependency.Dimensions]) + + let { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindStore.runtime.screen = { width: 667, height: 375 } + act(() => { + UniwindListener.notify([StyleDependency.Orientation, StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + + expect(getStylesFromId2('text-base').fontSize).toBe(40) + }) + + test('handles multiple variables with orientation', () => { + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindStore.runtime.screen = { width: 667, height: 375 } + UniwindListener.notify([StyleDependency.Orientation, StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + + + + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(40) + expect(getStylesFromId('spacing-md').padding).toBe(24) + }) + + test('orientation takes precedence over min-width when both match separately', () => { + UniwindStore.runtime.screen = { width: 500, height: 300 } + UniwindStore.runtime.orientation = Orientation.Landscape + UniwindListener.notify([StyleDependency.Orientation, StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(40) + }) +}) diff --git a/packages/uniwind/tests/media-queries/variables.test.css b/packages/uniwind/tests/media-queries/variables.test.css new file mode 100644 index 00000000..e6146d1f --- /dev/null +++ b/packages/uniwind/tests/media-queries/variables.test.css @@ -0,0 +1,16 @@ +@import 'tailwindcss'; +@import 'uniwind'; + +@layer theme { + :root { + --text-base: 1rem; + --text-lg: 1.25rem; + } +} + +@media (min-width: 640px) { + :root { + --text-base: 2rem; + --text-lg: 3rem; + } +} \ No newline at end of file diff --git a/packages/uniwind/tests/media-queries/variables.test.tsx b/packages/uniwind/tests/media-queries/variables.test.tsx new file mode 100644 index 00000000..386ebd9c --- /dev/null +++ b/packages/uniwind/tests/media-queries/variables.test.tsx @@ -0,0 +1,146 @@ +import { act } from '@testing-library/react-native' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import * as React from 'react' +import View from '../../src/components/native/View' +import { UniwindListener } from '../../src/core/listener' +import { UniwindStore } from '../../src/core/native/store' +import { compileVirtual } from '../../src/metro/compileVirtual' +import { Platform } from '../../src/metro/types' +import { Orientation, StyleDependency } from '../../src/types' +import { renderUniwind } from '../utils' + +describe('CSS Variables in Media Queries', () => { + beforeAll(async () => { + const cssPath = resolve('./tests/media-queries/variables.test.css') + const css = readFileSync(cssPath, 'utf-8') + const virtualCode = await compileVirtual({ + css, + cssPath, + debug: true, + platform: Platform.iOS, + themes: ['light', 'dark'], + polyfills: undefined, + }) + + eval( + `const { Uniwind } = require('../../src/core/config/config.native'); + Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); + `, + ) + }) + + beforeEach(() => { + UniwindStore.runtime.screen = { width: 375, height: 667 } + UniwindStore.runtime.orientation = Orientation.Portrait + UniwindStore.reinit() + }) + + describe('min-width media queries', () => { + test('uses default value when screen width is below media query threshold', () => { + UniwindStore.runtime.screen = { width: 400, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + }) + + test('uses media query value when screen width is above threshold', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + }) + + test('uses media query value when screen width equals threshold', () => { + UniwindStore.runtime.screen = { width: 640, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + }) + + test('handles multiple variables with media queries', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + + const { getStylesFromId } = renderUniwind( + + + + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + expect(getStylesFromId('text-lg').fontSize).toBe(48) + }) + }) + + describe('dynamic screen width changes', () => { + test('updates variable value when screen width changes from below to above threshold', () => { + UniwindStore.runtime.screen = { width: 400, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + let { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + + UniwindStore.runtime.screen = { width: 800, height: 667 } + act(() => { + UniwindListener.notify([StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + + expect(getStylesFromId2('text-base').fontSize).toBe(32) + }) + + test('updates variable value when screen width changes from above to below threshold', () => { + UniwindStore.runtime.screen = { width: 800, height: 667 } + UniwindStore.reinit() // Force a style rebuild + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(32) + + UniwindStore.runtime.screen = { width: 400, height: 667 } + act(() => { + UniwindListener.notify([StyleDependency.Dimensions]) + }) + + const { getStylesFromId: getStylesFromId2 } = renderUniwind( + , + ) + + expect(getStylesFromId2('text-base').fontSize).toBe(16) + }) + }) + + describe('fallback to default values', () => { + test('falls back to default when no media query matches', () => { + UniwindStore.runtime.screen = { width: 300, height: 667 } + UniwindListener.notify([StyleDependency.Dimensions]) + + const { getStylesFromId } = renderUniwind( + , + ) + + expect(getStylesFromId('text-base').fontSize).toBe(16) + }) + }) +})