Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@ jobs:
- name: Check circular dependencies
run: bun run circular:check

- name: Install Playwright browsers
run: bunx playwright install --with-deps chromium
working-directory: packages/uniwind

- name: Run tests
run: bun run test
250 changes: 184 additions & 66 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"eslint": "9.39.1",
"eslint-config-codemask": "2.2.1",
"husky": "9.1.7",
"turbo": "2.8.12"
"turbo": "2.8.13"
},
"packageManager": "bun@1.3.10",
"trustedDependencies": [
Expand Down
3 changes: 3 additions & 0 deletions packages/uniwind/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
LICENSE
assets/
readme.md
tests/e2e/.generated/
playwright-report/
test-results/
1 change: 0 additions & 1 deletion packages/uniwind/jest.config.web.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export default {
preset: 'ts-jest',
moduleNameMapper: {
'^react-native$': 'react-native-web',
'\\.css$': '<rootDir>/tests/__mocks__/styleMock.js',
},
transformIgnorePatterns: [
'node_modules/(?!(react-native-web)/)',
Expand Down
7 changes: 5 additions & 2 deletions packages/uniwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"circular:check": "dpdm --no-warning --no-tree -T --exit-code circular:1 'src/**/*.ts' 'src/**/*.tsx'",
"test:native": "jest --config jest.config.native.js",
"test:web": "jest --config jest.config.web.js",
"test:e2e": "playwright test",
"release": "release-it"
},
"keywords": [
Expand Down Expand Up @@ -89,6 +90,7 @@
"tailwindcss": ">=4"
},
"devDependencies": {
"@playwright/test": "1.58.2",
"@react-native/babel-preset": "0.84.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/jest-native": "5.4.3",
Expand All @@ -99,17 +101,18 @@
"@types/jest": "30.0.0",
"@types/react": "catalog:",
"dpdm": "4.0.1",
"git-cliff": "2.12.0",
"jest": "30.2.0",
"jest-environment-jsdom": "30.2.0",
"metro": "0.84.2",
"prettier": "3.8.1",
"react-native-web": "catalog:",
"react-test-renderer": "19.2.4",
"release-it": "19.2.4",
"ts-jest": "29.4.6",
"typescript": "catalog:",
"unbuild": "3.6.1",
"vite": "7.3.1",
"git-cliff": "2.12.0",
"release-it": "19.2.4"
"esbuild": "0.27.3"
}
}
17 changes: 17 additions & 0 deletions packages/uniwind/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: 0,
workers: 1,
reporter: 'list',
globalSetup: './tests/e2e/global-setup.ts',
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
65 changes: 65 additions & 0 deletions packages/uniwind/tests/e2e/getWebStyles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect, test } from '@playwright/test'
import { readFileSync } from 'fs'
import type { UniwindContextType } from '../../src/core/types'
import { BUNDLE_PATH, CSS_PATH } from './global-setup'
import './window.d.ts'

// Load the compiled artifacts produced by global-setup.ts
const compiledCSS = readFileSync(CSS_PATH, 'utf-8')
const bundle = readFileSync(BUNDLE_PATH, 'utf-8')

/**
* Calls getWebStyles inside the browser context and returns the RN style object.
*/
async function getWebStyles(
page: import('@playwright/test').Page,
className: string,
context: UniwindContextType = { scopedTheme: null },
) {
return page.evaluate(
([cls, ctx]) => {
return window.__uniwind.getWebStyles(cls, ctx)
},
[className, context],
)
}

test.beforeEach(async ({ page }) => {
await page.setContent(`
<!DOCTYPE html>
<html class="light">
<head>
<style>${compiledCSS}</style>
</head>
<body></body>
</html>
`)
await page.addScriptTag({ content: bundle })
})

test.describe('getWebStyles — background color', () => {
test('bg-red-500 → backgroundColor #fb2c36', async ({ page }) => {
const styles = await getWebStyles(page, 'bg-red-500')
expect(styles.backgroundColor).toBe('#fb2c36')
})
})

test.describe('getWebStyles — scoped theme', () => {
test('bg-background in dark theme → backgroundColor black', async ({ page }) => {
const styles = await getWebStyles(page, 'bg-background', { scopedTheme: 'dark' })
expect(styles.backgroundColor).toBe('#000000')
})

test('bg-background in light theme → backgroundColor white', async ({ page }) => {
const styles = await getWebStyles(page, 'bg-background', { scopedTheme: 'light' })
expect(styles.backgroundColor).toBe('#ffffff')
})
})

test.describe('getWebStyles - html default styles', () => {
// TODO fix html default styles in getWebStyles
test.skip('text-base -> fontSize 16px', async ({ page }) => {
const styles = await getWebStyles(page, 'text-base')
expect(styles.fontSize).toBe('16px')
})
})
61 changes: 61 additions & 0 deletions packages/uniwind/tests/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { build } from 'esbuild'
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path'
import { Platform } from '../../src/common/consts'
import { compileVirtual } from '../../src/metro/compileVirtual'

// Playwright runs globalSetup with cwd = directory containing playwright.config.ts
// which is packages/uniwind/
const ROOT = resolve(process.cwd())

export const GENERATED_DIR = resolve(ROOT, 'tests/e2e/.generated')
export const CSS_PATH = resolve(GENERATED_DIR, 'uniwind.css')
export const BUNDLE_PATH = resolve(GENERATED_DIR, 'getWebStyles.iife.js')

export default async function globalSetup() {
mkdirSync(GENERATED_DIR, { recursive: true })

// 1. Compile test.css → real Tailwind CSS for web
const cssPath = resolve(ROOT, 'tests/test.css')
const css = readFileSync(cssPath, 'utf-8')

const compiledCSS = await compileVirtual({
css,
cssPath,
debug: false,
platform: Platform.Web,
themes: ['light', 'dark'],
polyfills: undefined,
})

writeFileSync(CSS_PATH, compiledCSS, 'utf-8')
console.log(`[e2e setup] Compiled CSS written to ${CSS_PATH}`)

// 2. Bundle getWebStyles.ts into a browser IIFE via esbuild
// The bundle exports getWebStyles and getWebVariable as globals on window.__uniwind
const getWebStylesPath = resolve(ROOT, 'src/core/web/getWebStyles')
const entryContent = [
`import { getWebStyles, getWebVariable } from ${JSON.stringify(getWebStylesPath)}`,
'window.__uniwind = { getWebStyles, getWebVariable }',
].join('\n')
const entryPath = resolve(GENERATED_DIR, '_entry.ts')
writeFileSync(entryPath, entryContent, 'utf-8')

await build({
entryPoints: [entryPath],
bundle: true,
format: 'iife',
platform: 'browser',
outfile: BUNDLE_PATH,
// getWebStyles uses document/window at module load time,
// so we must NOT tree-shake the side-effectful top-level code
treeShaking: false,
// culori is an ESM-only package; esbuild handles it fine with bundle:true
mainFields: ['module', 'browser', 'main'],
conditions: ['browser', 'import', 'default'],
tsconfig: resolve(ROOT, 'tsconfig.json'),
logLevel: 'warning',
})

console.log(`[e2e setup] Browser bundle written to ${BUNDLE_PATH}`)
}
10 changes: 10 additions & 0 deletions packages/uniwind/tests/e2e/window.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { getWebStyles, getWebVariable } from '../../src/core/web/getWebStyles'

declare global {
interface Window {
__uniwind: {
getWebStyles: typeof getWebStyles
getWebVariable: typeof getWebVariable
}
}
}
5 changes: 4 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
"dependsOn": ["lint", "check:typescript", "circular:check", "test:native", "test:web", "//#check:format"]
},
"test": {
"dependsOn": ["test:native", "test:web"]
"dependsOn": ["test:native", "test:web", "test:e2e"]
},
"test:native": {
"cache": false
},
"test:web": {
"cache": false
},
"test:e2e": {
"cache": false
}
}
}