Skip to content

Commit edeb6eb

Browse files
authored
ci(e2e): 稳定 hosted 多端验证链路 (#1143)
* ci(e2e): stabilize hosted multiplatform verification * ci(coverage): separate hosted diagnostics from certificates * fix(ci): use localhost for iOS simulator Metro * fix(e2e): preserve style watcher file identity * fix(e2e): settle style updates from CSS outputs * fix(e2e): wait for complete watch artifacts * fix(ci): bound windows uni-app watch matrix * fix(ci): reduce windows uni-app watch cases
1 parent b3902ab commit edeb6eb

12 files changed

Lines changed: 203 additions & 58 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ jobs:
169169
runs-on: ubuntu-latest
170170
strategy:
171171
fail-fast: false
172+
# 静态 E2E 会写入共享 demo 产物目录;同一 checkout 内并发 shard
173+
# 会互相覆盖 dist 和快照,导致非确定性的产物断言失败。
174+
max-parallel: 1
172175
matrix:
173176
shard: [1, 2, 3]
174177
shard_total: [3]

.github/workflows/e2e-coverage-nightly.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ jobs:
3030
cache-dependency-path: pnpm-lock.yaml
3131
- run: pnpm install --frozen-lockfile
3232
- run: pnpm e2e:coverage:contract
33-
- run: pnpm e2e:coverage:report --source nightly --include-baselines --out e2e/.artifacts/coverage/nightly-report.json
33+
- name: Generate hosted coverage diagnostic
34+
run: pnpm e2e:coverage:report --source aggregate --include-baselines --out e2e/.artifacts/coverage/nightly-diagnostic-report.json
3435
- uses: actions/upload-artifact@v4
3536
if: always()
3637
with:
37-
name: coverage-certificate-${{ github.sha }}
38+
name: coverage-diagnostic-${{ github.sha }}
3839
path: e2e/.artifacts/coverage/**
3940
if-no-files-found: warn
4041
retention-days: 14
@@ -43,6 +44,8 @@ jobs:
4344
name: Framework static matrix (${{ matrix.os }}, Node ${{ matrix.node }})
4445
runs-on: ${{ matrix.os }}
4546
timeout-minutes: 45
47+
env:
48+
E2E_SKIP_OPEN_AUTOMATOR: '1'
4649
strategy:
4750
fail-fast: false
4851
matrix:
@@ -64,7 +67,16 @@ jobs:
6467
cache: pnpm
6568
cache-dependency-path: pnpm-lock.yaml
6669
- run: pnpm install --frozen-lockfile
67-
- run: pnpm e2e:static
70+
- name: Build workspace test dependencies
71+
run: pnpm build:ci
72+
- name: Install Playwright browsers
73+
shell: bash
74+
run: pnpm exec playwright install chromium chromium-headless-shell
75+
- name: Run hosted static suite
76+
run: >-
77+
pnpm e2e:static
78+
--exclude e2e/uni-app-vite-vue3-hbuilderx-tailwindcss-v4.test.ts
79+
--exclude e2e/uni-app-x-vdom-tailwindcss-v4.test.ts
6880
- run: pnpm e2e:frameworks:matrix
6981
- run: pnpm e2e:multiplatform-build
7082

.github/workflows/e2e-coverage-pr.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ jobs:
3434
run: pnpm e2e:runner:health
3535
- name: Coverage contract
3636
run: pnpm e2e:coverage:contract
37-
- name: Generate strict PR certificate
38-
run: pnpm e2e:coverage:report --source ci --out e2e/.artifacts/coverage/pr-report.json
39-
- name: Upload coverage evidence
37+
- name: Generate hosted coverage diagnostic
38+
run: pnpm e2e:coverage:report --source aggregate --include-baselines --out e2e/.artifacts/coverage/pr-diagnostic-report.json
39+
- name: Upload coverage diagnostic
4040
if: always()
4141
uses: actions/upload-artifact@v4
4242
with:
43-
name: coverage-certificate-${{ github.sha }}
43+
name: coverage-diagnostic-${{ github.sha }}
4444
path: e2e/.artifacts/coverage/**
4545
if-no-files-found: error
4646
retention-days: 14

.github/workflows/e2e-watch.yml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,12 @@ jobs:
715715
runner_label: windows
716716
watch_case: uni-app-vite-tailwindcss-v4:mp-weixin
717717
artifact_case: uni-app-vite-tailwindcss-v4-mp-weixin
718-
round_profile: default
718+
round_profile: main-style
719719
watch_save_snapshots: '1'
720+
# Windows uni-app 每次变更都会触发完整平台构建;PR 保留主包与两个分包的主样式契约,完整轮次由 Linux/macOS 与 nightly 覆盖。
721+
watch_main_style_only: '1'
722+
watch_main_style_subpackage_limit: '2'
723+
watch_max_attempts: '1'
720724
timeout_minutes: 30
721725
watch_timeout_ms: '420000'
722726
watch_poll_ms: '40'
@@ -726,8 +730,12 @@ jobs:
726730
runner_label: windows
727731
watch_case: uni-app-vite-tailwindcss-v4:mp-alipay
728732
artifact_case: uni-app-vite-tailwindcss-v4-mp-alipay
729-
round_profile: default
733+
round_profile: main-style
730734
watch_save_snapshots: '1'
735+
# Windows uni-app 每次变更都会触发完整平台构建;PR 保留主包与两个分包的主样式契约,完整轮次由 Linux/macOS 与 nightly 覆盖。
736+
watch_main_style_only: '1'
737+
watch_main_style_subpackage_limit: '2'
738+
watch_max_attempts: '1'
731739
timeout_minutes: 30
732740
watch_timeout_ms: '420000'
733741
watch_poll_ms: '40'
@@ -737,8 +745,12 @@ jobs:
737745
runner_label: windows
738746
watch_case: uni-app-vite-tailwindcss-v4:mp-qq
739747
artifact_case: uni-app-vite-tailwindcss-v4-mp-qq
740-
round_profile: default
748+
round_profile: main-style
741749
watch_save_snapshots: '1'
750+
# Windows uni-app 每次变更都会触发完整平台构建;PR 保留主包与两个分包的主样式契约,完整轮次由 Linux/macOS 与 nightly 覆盖。
751+
watch_main_style_only: '1'
752+
watch_main_style_subpackage_limit: '2'
753+
watch_max_attempts: '1'
742754
timeout_minutes: 30
743755
watch_timeout_ms: '420000'
744756
watch_poll_ms: '40'
@@ -748,8 +760,12 @@ jobs:
748760
runner_label: windows
749761
watch_case: uni-app-vite-tailwindcss-v4:mp-toutiao
750762
artifact_case: uni-app-vite-tailwindcss-v4-mp-toutiao
751-
round_profile: default
763+
round_profile: main-style
752764
watch_save_snapshots: '1'
765+
# Windows uni-app 每次变更都会触发完整平台构建;PR 保留主包与两个分包的主样式契约,完整轮次由 Linux/macOS 与 nightly 覆盖。
766+
watch_main_style_only: '1'
767+
watch_main_style_subpackage_limit: '2'
768+
watch_max_attempts: '1'
753769
timeout_minutes: 60
754770
watch_timeout_ms: '420000'
755771
watch_poll_ms: '40'

.github/workflows/react-native-compatibility.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ jobs:
117117
echo "RN_IOS_DEVICE_ID=$udid" >> "$GITHUB_ENV"
118118
xcrun simctl boot "$udid"
119119
xcrun simctl bootstatus "$udid" -b
120-
- run: pnpm e2e:react-native:ios
120+
- name: Run Expo iOS compatibility
121+
env:
122+
RN_IOS_HOST: 127.0.0.1
123+
run: pnpm e2e:react-native:ios
121124
- uses: actions/upload-artifact@v4
122125
if: always()
123126
with:

e2e/__snapshots__/e2e/weapp-vite-tailwindcss-v4/app.wxss

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -430,30 +430,6 @@ wx-root-portal-content {
430430
transition-timing-function: var(--tw-ease, initial);
431431
transition-duration: var(--tw-duration, initial);
432432
}
433-
.weapp-tw-user-ui-card {
434-
display: inline-flex;
435-
align-items: center;
436-
gap: 8rpx;
437-
color: var(--weapp-tw-user-ui-color, #175e75);
438-
animation: weappTwUserUiBreathe 1.2s ease-in-out infinite;
439-
}
440-
.weapp-tw-user-ui-loading {
441-
display: inline-block;
442-
width: 32rpx;
443-
height: 32rpx;
444-
animation: weappTwUserUiRotation 1s linear infinite;
445-
}
446-
@keyframes weappTwUserUiRotation {
447-
to {
448-
transform: rotate(360deg);
449-
}
450-
}
451-
@keyframes weappTwUserUiBreathe {
452-
50% {
453-
opacity: 0.65;
454-
transform: scale(0.96);
455-
}
456-
}
457433
button::after,
458434
wx-button::after {
459435
display: none;

e2e/react-native-compatibility.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { runWebRuntime } from './react-native/web-runtime'
1111
const repoRoot = path.resolve(import.meta.dirname, '..')
1212
const examplePackage = '@weapp-tailwindcss/example-react-native-expo'
1313
const reactNativePackage = '@weapp-tailwindcss/react-native'
14+
const corePackage = 'weapp-tailwindcss'
1415

1516
async function findBundle(root: string, platform: string) {
1617
const metadata = JSON.parse(await fs.readFile(path.join(root, 'metadata.json'), 'utf8')) as { fileMetadata: Record<string, { bundle?: string }> }
@@ -90,7 +91,9 @@ describe('React Native Expo static exports', () => {
9091
it('exports Web, Android and iOS bundles through Metro', async () => {
9192
const outputRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'weapp-tailwindcss-rn-export-'))
9293
try {
93-
// 通用 E2E shard 不保证 workspace 包已按示例依赖拓扑构建。
94+
// Metro 运行时会通过 generator 兼容入口加载根包;先构建根包,避免
95+
// workspace 的 dist 状态决定测试结果。
96+
await execa('pnpm', ['--filter', corePackage, 'build'], { cwd: repoRoot })
9497
await execa('pnpm', ['--filter', reactNativePackage, 'build'], { cwd: repoRoot })
9598
for (const platform of ['web', 'android', 'ios'] as const) {
9699
const exportResult = await execa('pnpm', ['--filter', examplePackage, 'exec', 'expo', 'export', '--clear', '--platform', platform, '--output-dir', path.join(outputRoot, platform)], {

e2e/react-native/run-native.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ async function main() {
329329
...(platform === 'android' ? { ANDROID_HOME: androidHome } : {}),
330330
}
331331
if (javaHome) { env.PATH = `${path.join(javaHome, 'bin')}${path.delimiter}${process.env['PATH'] ?? ''}` }
332-
const metroHost = platform === 'ios' ? '--lan' : '--localhost'
332+
// iOS Simulator 与宿主机共享网络栈时使用 localhost,避免 CI 的虚拟网卡地址无法被 simctl openurl 访问。
333+
const metroHost = platform === 'ios' && runtimeHost !== '127.0.0.1' ? '--lan' : '--localhost'
333334
const metro = execa('pnpm', ['--filter', '@weapp-tailwindcss/example-react-native-expo', 'exec', 'expo', 'start', metroHost, '--port', '8081', '--clear'], {
334335
cwd: repoRoot,
335336
env,

packages/weapp-tailwindcss/test/ci/workflows.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,18 @@ describe('ci workflows', () => {
170170
expect(packageJson.scripts['up:pkg']).toBe('node scripts/pnpm-smart-proxy.mjs up -ri')
171171
})
172172

173+
it('uses a simulator-reachable Metro host for Expo iOS CI', () => {
174+
const { workflow } = readWorkflow('react-native-compatibility.yml')
175+
const iosSteps: Array<Record<string, unknown>> = workflow.jobs.ios.steps
176+
const runStep = iosSteps.find(step => step.name === 'Run Expo iOS compatibility')
177+
178+
expect(runStep).toMatchObject({
179+
env: { RN_IOS_HOST: '127.0.0.1' },
180+
run: 'pnpm e2e:react-native:ios',
181+
})
182+
expect(readText('e2e/react-native/run-native.ts')).toContain("runtimeHost !== '127.0.0.1' ? '--lan' : '--localhost'")
183+
})
184+
173185
it('keeps the core CI quality gate on package changes', () => {
174186
const { workflow } = readWorkflow('ci.yml')
175187

@@ -219,6 +231,7 @@ describe('ci workflows', () => {
219231
expect(hasStepRunCommand(qualityRuns, 'pnpm build:ci')).toBe(true)
220232
expect(staticJob['timeout-minutes']).toBe(15)
221233
expect(staticJob.strategy['fail-fast']).toBe(false)
234+
expect(staticJob.strategy['max-parallel']).toBe(1)
222235
expect(staticJob.strategy.matrix.shard).toEqual([1, 2, 3])
223236
expect(staticJob.strategy.matrix.shard_total).toEqual([3])
224237
expect(staticRuns.join('\n')).toContain('pnpm exec playwright install chromium chromium-headless-shell')
@@ -414,6 +427,20 @@ describe('ci workflows', () => {
414427
expect(compatibilityRuns).toContain('test/watch-hmr-coverage-matrix.unit.test.ts')
415428
})
416429

430+
it('keeps hosted coverage reports diagnostic until complete evidence executors are available', () => {
431+
const { workflow: prWorkflow, source: prSource } = readWorkflow('e2e-coverage-pr.yml')
432+
const { workflow: nightlyWorkflow, source: nightlySource } = readWorkflow('e2e-coverage-nightly.yml')
433+
const prRuns = stepRuns(prWorkflow, 'coverage-gate').join('\n')
434+
const nightlyRuns = stepRuns(nightlyWorkflow, 'contract').join('\n')
435+
436+
expect(prRuns).toContain('pnpm e2e:coverage:report --source aggregate --include-baselines')
437+
expect(nightlyRuns).toContain('pnpm e2e:coverage:report --source aggregate --include-baselines')
438+
expect(prSource).toContain('coverage-diagnostic-${{ github.sha }}')
439+
expect(nightlySource).toContain('coverage-diagnostic-${{ github.sha }}')
440+
expect(prSource).not.toContain('--source ci')
441+
expect(nightlySource).not.toContain('--source nightly')
442+
})
443+
417444
it('keeps a lightweight cross-platform e2e watch gate in CI', () => {
418445
const { workflow } = readWorkflow('ci.yml')
419446
const job = workflow.jobs['e2e-watch']
@@ -917,7 +944,9 @@ describe('e2e watch workflow', () => {
917944
}
918945
for (const runner of ['linux', 'windows']) {
919946
for (const watchCase of completeMiniProgramCases) {
920-
const profile = watchCase.startsWith('taro-')
947+
const windowsReducedCase = runner === 'windows'
948+
&& watchCase.startsWith('uni-app-vite-tailwindcss-v4:mp-')
949+
const profile = windowsReducedCase || watchCase.startsWith('taro-')
921950
? 'main-style'
922951
: 'default'
923952
expect(cases, `${runner} should cover ${watchCase}`).toContain(`${runner}:22:${watchCase}:${profile}`)

packages/weapp-tailwindcss/test/watch-hmr-regression.unit.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,37 @@ describe('watch-hmr regression text helpers', () => {
901901
expect(elapsed).toBeGreaterThanOrEqual(0)
902902
})
903903

904+
it('allows style compile settle to observe CSS output mtimes', async () => {
905+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-watch-style-settle-'))
906+
tempDirs.push(tempDir)
907+
const styleFile = path.join(tempDir, 'index.wxss')
908+
await writeFilePreserveEol(styleFile, '.anchor{}', '.anchor{}')
909+
const phaseStartedAt = Date.now()
910+
911+
await new Promise(resolve => setTimeout(resolve, 20))
912+
await writeFilePreserveEol(styleFile, '.anchor{color:red}', '.anchor{}')
913+
914+
const elapsed = await waitForCompileSettled(
915+
{
916+
label: 'demo/weapp-vite-tailwindcss-v4',
917+
outputWxml: '/missing/index.wxml',
918+
outputJs: '/missing/index.js',
919+
} as any,
920+
{
921+
timeoutMs: 2_000,
922+
pollMs: 20,
923+
} as CliOptions,
924+
{
925+
ensureRunning() {},
926+
lastCompileSuccessAt: () => 0,
927+
} as any,
928+
phaseStartedAt,
929+
[styleFile],
930+
)
931+
932+
expect(elapsed).toBeGreaterThanOrEqual(0)
933+
})
934+
904935
it('allows style-only compile settle to use a stable plugin total sample', async () => {
905936
const pluginTotalAt = Date.now() - 700
906937
const elapsed = await waitForCompileSettled(
@@ -930,6 +961,56 @@ describe('watch-hmr regression text helpers', () => {
930961
expect(elapsed).toBeGreaterThanOrEqual(0)
931962
})
932963

964+
it('waits for CSS outputs when a compile success signal arrives first', async () => {
965+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-watch-css-lag-'))
966+
tempDirs.push(tempDir)
967+
const wxmlFile = path.join(tempDir, 'index.wxml')
968+
const jsFile = path.join(tempDir, 'index.js')
969+
const styleFile = path.join(tempDir, 'app.wxss')
970+
await Promise.all([
971+
writeFilePreserveEol(wxmlFile, '<view />', '<view />'),
972+
writeFilePreserveEol(jsFile, 'Page({})', 'Page({})'),
973+
writeFilePreserveEol(styleFile, '.baseline{}', '.baseline{}'),
974+
])
975+
976+
await new Promise(resolve => setTimeout(resolve, 40))
977+
const phaseStartedAt = Date.now()
978+
let compileSuccessAt = 0
979+
setTimeout(() => {
980+
compileSuccessAt = Date.now()
981+
void writeFilePreserveEol(jsFile, 'Page({ ready: true })', 'Page({})').catch(() => undefined)
982+
}, 20)
983+
setTimeout(() => {
984+
void rm(styleFile, { force: true }).catch(() => undefined)
985+
}, 100)
986+
setTimeout(() => {
987+
void writeFilePreserveEol(styleFile, '.updated{color:red}', '.baseline{}').catch(() => undefined)
988+
}, 900)
989+
990+
await waitForCompileSettled(
991+
{
992+
label: 'demo/uni-app-vite-tailwindcss-v4',
993+
outputWxml: wxmlFile,
994+
outputJs: jsFile,
995+
outputStyleCandidates: [styleFile],
996+
globalStyleCandidates: [styleFile],
997+
} as any,
998+
{
999+
timeoutMs: 3_000,
1000+
pollMs: 20,
1001+
} as CliOptions,
1002+
{
1003+
ensureRunning() {},
1004+
lastCompileSuccessAt: () => compileSuccessAt,
1005+
pluginProcessSamplesSince: () => [],
1006+
} as any,
1007+
phaseStartedAt,
1008+
)
1009+
1010+
expect(compileSuccessAt).toBeGreaterThan(phaseStartedAt)
1011+
await expect(readFile(styleFile, 'utf8')).resolves.toBe('.updated{color:red}')
1012+
})
1013+
9331014
it('waits for added-class rollback compilation to settle before returning', async () => {
9341015
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-added-class-rollback-'))
9351016
tempDirs.push(tempDir)

0 commit comments

Comments
 (0)