Skip to content

Commit d079639

Browse files
committed
test: add tailwind generator order parity coverage
1 parent 5923656 commit d079639

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { mkdir, mkdtemp, symlink } from 'node:fs/promises'
2+
import { createRequire } from 'node:module'
3+
import { tmpdir } from 'node:os'
4+
import path from 'node:path'
5+
import postcss from 'postcss'
6+
import tailwindcssV3 from 'tailwindcss'
7+
import tailwindcssPostcssV4 from '@tailwindcss/postcss'
8+
import { replaceWxml } from '@/wxml/shared'
9+
import { createTailwindV3Engine, resolveTailwindV3Source } from '@/tailwindcss/v3-engine'
10+
import { createTailwindV4Engine, resolveTailwindV4Source } from '@/tailwindcss/v4-engine'
11+
12+
const require = createRequire(import.meta.url)
13+
const tailwindcssV3Version = require('tailwindcss/package.json').version as string
14+
const tailwindcssV4Root = path.dirname(require.resolve('tailwindcss4/package.json'))
15+
16+
const TAILWIND_V3_CSS = '@tailwind base; @tailwind components; @tailwind utilities;'
17+
18+
// 这些语料从 Tailwind submodule 的 sort 与 variant-order 用例提炼而来,用于锁定官方生成顺序。
19+
const TAILWIND_V3_ORDER_CORPUS = [
20+
'py-3',
21+
'p-1',
22+
'px-3',
23+
'hover:p-1',
24+
'focus:hover:p-3',
25+
'!py-4',
26+
'rtl:flex',
27+
'dark:flex',
28+
'motion-safe:animate-pulse',
29+
'md:grid-cols-3',
30+
'sm:w-4',
31+
'container',
32+
'bg-red-500',
33+
'bg-blue-500',
34+
'text-sm',
35+
'text-lg',
36+
'font-bold',
37+
'underline',
38+
'flex',
39+
'grid',
40+
'w-[123px]',
41+
'text-[#123456]',
42+
'before:content-[\'x\']',
43+
'first-letter:text-red-500',
44+
'aria-[expanded=true]:max-h-[32rem]',
45+
'data-[state=open]:opacity-100',
46+
'[--fg:#fff]',
47+
'[color:var(--fg)]',
48+
]
49+
50+
const TAILWIND_V4_ORDER_CORPUS = [
51+
'py-3',
52+
'p-1',
53+
'px-3',
54+
'hover:p-1',
55+
'focus:hover:p-3',
56+
'py-4!',
57+
'rtl:flex',
58+
'dark:flex',
59+
'starting:flex',
60+
'not-hover:flex',
61+
'md:grid-cols-3',
62+
'sm:w-4',
63+
'bg-red-500',
64+
'bg-blue-500',
65+
'text-sm',
66+
'text-lg',
67+
'font-bold',
68+
'underline',
69+
'flex',
70+
'grid',
71+
'w-[123px]',
72+
'text-[#123456]',
73+
'before:content-[\'x\']',
74+
'first-letter:text-red-500',
75+
'aria-[expanded=true]:max-h-[32rem]',
76+
'data-[state=open]:opacity-100',
77+
'[--fg:#fff]',
78+
'[color:var(--fg)]',
79+
]
80+
81+
const MINI_PROGRAM_STABLE_CORPUS = [
82+
'container',
83+
'flex',
84+
'grid',
85+
'p-1',
86+
'px-3',
87+
'py-3',
88+
'bg-red-500',
89+
'bg-blue-500',
90+
'text-sm',
91+
'text-lg',
92+
'font-bold',
93+
'underline',
94+
'w-[123px]',
95+
'text-[#123456]',
96+
]
97+
98+
function normalizeCss(css: string) {
99+
return css.replace(/\r\n/g, '\n').trim()
100+
}
101+
102+
function collectRuleOrder(css: string) {
103+
const order: string[] = []
104+
const root = postcss.parse(css)
105+
106+
root.walkRules((rule) => {
107+
const parents: string[] = []
108+
let parent = rule.parent
109+
while (parent && parent.type !== 'root') {
110+
if (parent.type === 'atrule') {
111+
parents.unshift(`@${parent.name} ${parent.params}`)
112+
}
113+
parent = parent.parent
114+
}
115+
order.push([...parents, rule.selector].join(' > '))
116+
})
117+
118+
return order
119+
}
120+
121+
function cssEscapeClassName(className: string) {
122+
return className.replace(/[^a-zA-Z0-9_-]/g, character => `\\${character}`)
123+
}
124+
125+
function collectCandidateOrder(css: string, candidates: string[]) {
126+
return candidates
127+
.map(candidate => ({
128+
candidate,
129+
index: css.indexOf(`.${cssEscapeClassName(candidate)}`),
130+
}))
131+
.filter(item => item.index >= 0)
132+
.sort((a, b) => a.index - b.index)
133+
.map(item => item.candidate)
134+
}
135+
136+
function collectTransformedCandidateOrder(css: string, candidates: string[]) {
137+
return candidates
138+
.map(candidate => ({
139+
candidate,
140+
index: css.indexOf(`.${replaceWxml(candidate)}`),
141+
}))
142+
.filter(item => item.index >= 0)
143+
.sort((a, b) => a.index - b.index)
144+
.map(item => item.candidate)
145+
}
146+
147+
function createTailwindV3Config(candidates: string[]) {
148+
return {
149+
content: [{
150+
raw: candidates.join(' '),
151+
extension: 'html',
152+
}],
153+
}
154+
}
155+
156+
function createTailwindV4Css(candidates: string[]) {
157+
return [
158+
'@import "tailwindcss" source(none);',
159+
`@source inline(${JSON.stringify(candidates.join(' '))});`,
160+
'',
161+
].join('\n')
162+
}
163+
164+
async function createTailwindV4FixtureRoot() {
165+
const root = await mkdtemp(path.join(tmpdir(), 'weapp-tw-v4-order-parity-'))
166+
const nodeModulesDir = path.join(root, 'node_modules')
167+
await mkdir(nodeModulesDir, { recursive: true })
168+
await symlink(tailwindcssV4Root, path.join(nodeModulesDir, 'tailwindcss'), 'dir')
169+
return {
170+
root,
171+
cssEntry: path.join(root, 'app.css'),
172+
}
173+
}
174+
175+
describe('generator order parity', () => {
176+
it('keeps Tailwind v3 generated rule order identical to official Tailwind', async () => {
177+
expect(tailwindcssV3Version.startsWith('3.')).toBe(true)
178+
179+
const config = createTailwindV3Config(TAILWIND_V3_ORDER_CORPUS)
180+
const official = await postcss([
181+
tailwindcssV3(config),
182+
]).process(TAILWIND_V3_CSS, {
183+
from: undefined,
184+
})
185+
const source = await resolveTailwindV3Source({
186+
css: TAILWIND_V3_CSS,
187+
base: process.cwd(),
188+
configObject: config,
189+
})
190+
const engine = createTailwindV3Engine(source)
191+
const result = await engine.generate({
192+
candidates: [...TAILWIND_V3_ORDER_CORPUS].reverse(),
193+
target: 'web',
194+
})
195+
196+
expect(collectRuleOrder(result.css)).toEqual(collectRuleOrder(official.css))
197+
expect(normalizeCss(result.css)).toBe(normalizeCss(official.css))
198+
})
199+
200+
it('keeps Tailwind v4 generated rule order identical to official Tailwind', async () => {
201+
const fixture = await createTailwindV4FixtureRoot()
202+
const css = createTailwindV4Css(TAILWIND_V4_ORDER_CORPUS)
203+
204+
const official = await postcss([
205+
tailwindcssPostcssV4({
206+
optimize: false,
207+
}),
208+
]).process(css, {
209+
from: fixture.cssEntry,
210+
})
211+
const source = await resolveTailwindV4Source({
212+
css,
213+
base: fixture.root,
214+
packageName: 'tailwindcss',
215+
})
216+
const result = await createTailwindV4Engine(source).generate({
217+
candidates: [...TAILWIND_V4_ORDER_CORPUS].reverse(),
218+
target: 'web',
219+
})
220+
221+
expect(collectRuleOrder(result.css)).toEqual(collectRuleOrder(official.css))
222+
expect(normalizeCss(result.css)).toBe(normalizeCss(official.css))
223+
})
224+
225+
it('preserves Tailwind v3 order after mini-program selector transforms', async () => {
226+
const config = createTailwindV3Config(MINI_PROGRAM_STABLE_CORPUS)
227+
const source = await resolveTailwindV3Source({
228+
css: TAILWIND_V3_CSS,
229+
base: process.cwd(),
230+
configObject: config,
231+
})
232+
const result = await createTailwindV3Engine(source).generate({
233+
candidates: [...MINI_PROGRAM_STABLE_CORPUS].reverse(),
234+
target: 'weapp',
235+
})
236+
237+
const rawOrder = collectCandidateOrder(result.rawCss, MINI_PROGRAM_STABLE_CORPUS)
238+
const transformedOrder = collectTransformedCandidateOrder(result.css, MINI_PROGRAM_STABLE_CORPUS)
239+
expect(transformedOrder).toEqual(rawOrder)
240+
})
241+
242+
it('preserves Tailwind v4 order after mini-program selector transforms', async () => {
243+
const fixture = await createTailwindV4FixtureRoot()
244+
const source = await resolveTailwindV4Source({
245+
css: createTailwindV4Css(MINI_PROGRAM_STABLE_CORPUS),
246+
base: fixture.root,
247+
packageName: 'tailwindcss',
248+
})
249+
const result = await createTailwindV4Engine(source).generate({
250+
candidates: [...MINI_PROGRAM_STABLE_CORPUS].reverse(),
251+
target: 'weapp',
252+
})
253+
254+
const rawOrder = collectCandidateOrder(result.rawCss, MINI_PROGRAM_STABLE_CORPUS)
255+
const transformedOrder = collectTransformedCandidateOrder(result.css, MINI_PROGRAM_STABLE_CORPUS)
256+
expect(transformedOrder).toEqual(rawOrder)
257+
})
258+
})

0 commit comments

Comments
 (0)