Skip to content

Commit 9fb64e2

Browse files
committed
ci(e2e): parallelize tests with Nx, block non-essential assets, add timing logs
1 parent 3627373 commit 9fb64e2

File tree

13 files changed

+146
-35
lines changed

13 files changed

+146
-35
lines changed

.github/workflows/e2e-opportunistic-matrix.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,12 @@ jobs:
3939
fetch-depth: 0
4040
- name: Setup Tools
4141
uses: tanstack/config/.github/setup@main
42-
- name: Build
43-
run: pnpm build
4442
- name: Install Playwright Chrome
4543
run: pnpm --filter @tanstack/cli exec playwright install --with-deps chrome
4644
- name: Run Matrix Scenario
4745
env:
4846
E2E_MATRIX_SCENARIO: ${{ matrix.scenario }}
49-
run: pnpm --filter @tanstack/cli test:e2e:matrix
47+
run: pnpm nx run @tanstack/cli:test:e2e:matrix
5048
- name: Upload Playwright Report
5149
if: always()
5250
uses: actions/upload-artifact@v4

.github/workflows/pr.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ jobs:
5050
fetch-depth: 0
5151
- name: Setup Tools
5252
uses: tanstack/config/.github/setup@main
53-
- name: Build
54-
run: pnpm build
53+
- name: Get base and head commits for `nx affected`
54+
uses: nrwl/nx-set-shas@v4.4.0
55+
with:
56+
main-branch-name: main
5557
- name: Install Playwright Chrome
5658
run: pnpm --filter @tanstack/cli exec playwright install --with-deps chrome
5759
- name: Test E2E Blocking
58-
run: pnpm test:e2e
60+
run: pnpm nx affected --target=test:e2e --parallel=3

nx.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,25 @@
1010
},
1111
"targetDefaults": {
1212
"build": {
13+
"cache": true,
1314
"inputs": ["production", "^production"],
15+
"outputs": ["{projectRoot}/dist"],
1416
"dependsOn": ["^build"]
17+
},
18+
"test:e2e": {
19+
"cache": true,
20+
"dependsOn": ["^build", "build"],
21+
"inputs": ["default", "^production"]
22+
},
23+
"test:e2e:blocking": {
24+
"cache": true,
25+
"dependsOn": ["^build", "build"],
26+
"inputs": ["default", "^production"]
27+
},
28+
"test:e2e:matrix": {
29+
"cache": true,
30+
"dependsOn": ["^build", "build"],
31+
"inputs": ["default", "^production"]
1532
}
1633
}
1734
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"dev": "nx run-many --target=dev --parallel 20",
1414
"test": "pnpm test:unit && pnpm test:e2e",
1515
"test:unit": "nx run-many --target=test",
16-
"test:e2e": "pnpm --filter @tanstack/cli test:e2e",
17-
"test:e2e:matrix": "pnpm --filter @tanstack/cli test:e2e:matrix",
16+
"test:e2e": "nx run @tanstack/cli:test:e2e",
17+
"test:e2e:matrix": "nx run @tanstack/cli:test:e2e:matrix",
1818
"check-outdated": "node scripts/check-outdated-packages.js",
1919
"update-outdated": "node scripts/check-outdated-packages.js --update",
2020
"prepare": "husky install",

packages/cli/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
"build": "tsc",
1717
"dev": "tsc --watch",
1818
"test:e2e": "npm run test:e2e:blocking",
19-
"test:e2e:blocking": "npm run build && playwright test --grep @blocking",
20-
"test:e2e:matrix": "npm run build && playwright test --grep @matrix",
21-
"test:e2e:debug": "npm run build && playwright test --debug",
22-
"test:e2e:headed": "npm run build && playwright test --headed",
19+
"test:e2e:blocking": "playwright test --grep @blocking",
20+
"test:e2e:matrix": "playwright test --grep @matrix",
21+
"test:e2e:debug": "playwright test --debug",
22+
"test:e2e:headed": "playwright test --headed",
23+
"test:e2e:local": "npm run build && npm run test:e2e:blocking",
24+
"test:e2e:matrix:local": "npm run build && npm run test:e2e:matrix",
2325
"test:lint": "eslint ./src",
2426
"test": "vitest run",
2527
"test:watch": "vitest",

packages/cli/playwright.config.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { defineConfig, devices } from '@playwright/test'
22

3+
const workers = process.env.PLAYWRIGHT_WORKERS
4+
? Number.isNaN(Number(process.env.PLAYWRIGHT_WORKERS))
5+
? process.env.PLAYWRIGHT_WORKERS
6+
: Number(process.env.PLAYWRIGHT_WORKERS)
7+
: process.env.CI
8+
? 4
9+
: '50%'
10+
311
export default defineConfig({
412
testDir: './tests-e2e',
5-
fullyParallel: false,
13+
fullyParallel: true,
614
forbidOnly: !!process.env.CI,
715
retries: process.env.CI ? 1 : 0,
8-
workers: 1,
16+
workers,
917
timeout: 10 * 60 * 1000,
1018
expect: {
1119
timeout: 20 * 1000,
@@ -14,8 +22,9 @@ export default defineConfig({
1422
use: {
1523
...devices['Desktop Chrome'],
1624
channel: 'chrome',
25+
serviceWorkers: 'block',
1726
trace: 'on-first-retry',
1827
screenshot: 'only-on-failure',
19-
video: 'retain-on-failure',
28+
video: 'off',
2029
},
2130
})

packages/cli/tests-e2e/addons-smoke.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from '@playwright/test'
22

3-
import { attachRuntimeGuards, createReactAppFixture } from './helpers'
3+
import { attachRuntimeGuards, createReactAppFixture, optimizePageForFastE2E } from './helpers'
44

55
test('@blocking creates app with multiple add-ons and renders demo routes', async ({ page }) => {
66
const fixture = await createReactAppFixture({
@@ -10,6 +10,7 @@ test('@blocking creates app with multiple add-ons and renders demo routes', asyn
1010
const guards = attachRuntimeGuards(page, fixture.url)
1111

1212
try {
13+
await optimizePageForFastE2E(page)
1314
await page.goto(`${fixture.url}/demo/form/simple`)
1415
await expect(page.getByText('Title', { exact: true })).toBeVisible()
1516
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible()

packages/cli/tests-e2e/create-smoke.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { expect, test } from '@playwright/test'
22

3-
import { attachRuntimeGuards, createReactAppFixture } from './helpers'
3+
import { attachRuntimeGuards, createReactAppFixture, optimizePageForFastE2E } from './helpers'
44

55
test('@blocking creates a React app and navigates core starter routes', async ({ page }) => {
66
const fixture = await createReactAppFixture({
77
appName: 'react-smoke-app',
8+
runQualityGatesChecks: true,
89
})
910
const guards = attachRuntimeGuards(page, fixture.url)
1011

1112
try {
13+
await optimizePageForFastE2E(page)
1214
await page.goto(fixture.url)
1315
await expect(
1416
page.getByRole('heading', {

packages/cli/tests-e2e/helpers.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type CreateAppFixtureOptions = {
2828
addOns?: Array<string>
2929
postCreateAddOns?: Array<string>
3030
skipDevServer?: boolean
31+
runQualityGatesChecks?: boolean
3132
}
3233

3334
export type RuntimeGuards = {
@@ -291,18 +292,68 @@ async function runQualityGates(
291292
})
292293
}
293294

295+
function envBoolean(name: string, defaultValue: boolean) {
296+
const value = process.env[name]
297+
if (value == null) {
298+
return defaultValue
299+
}
300+
301+
const normalized = value.trim().toLowerCase()
302+
return normalized === '1' || normalized === 'true' || normalized === 'yes'
303+
}
304+
305+
const IMAGE_GLOB = '**/*.{png,jpg,jpeg,gif,webp,avif,ico}'
306+
const FONT_GLOB = '**/*.{woff,woff2,ttf,otf,eot}'
307+
const MEDIA_GLOB = '**/*.{mp4,webm,mp3,wav,ogg,mov,m4a}'
308+
const CSS_GLOB = '**/*.css'
309+
310+
const abortRoute = (route: { abort: (reason?: string) => Promise<void> }) =>
311+
route.abort('blockedbyclient')
312+
313+
export async function optimizePageForFastE2E(page: Page) {
314+
const blockAssets = envBoolean('E2E_BLOCK_NON_ESSENTIAL', true)
315+
if (!blockAssets) {
316+
return
317+
}
318+
319+
await page.route(IMAGE_GLOB, abortRoute)
320+
await page.route(FONT_GLOB, abortRoute)
321+
await page.route(MEDIA_GLOB, abortRoute)
322+
323+
if (envBoolean('E2E_BLOCK_CSS', false)) {
324+
await page.route(CSS_GLOB, abortRoute)
325+
}
326+
}
327+
294328
export async function createAppFixture(
295329
options: CreateAppFixtureOptions,
296330
): Promise<E2EApp> {
297331
await access(cliDistPath)
298332

333+
const timings: Array<[string, number]> = []
334+
const now = () => performance.now()
335+
const start = now()
336+
const mark = (label: string, startedAt: number) => {
337+
timings.push([label, now() - startedAt])
338+
}
339+
const logTimings = () => {
340+
if (!envBoolean('E2E_TIMINGS', true)) {
341+
return
342+
}
343+
344+
const totalMs = now() - start
345+
const summary = timings.map(([label, ms]) => `${label}=${Math.round(ms)}ms`).join(' | ')
346+
console.log(`[e2e:timing] ${options.appName} :: ${summary} | total=${Math.round(totalMs)}ms`)
347+
}
348+
299349
const rootDir = await mkdtemp(join(tmpdir(), 'tanstack-cli-e2e-'))
300350
const {
301351
appName,
302352
template,
303353
addOns,
304354
postCreateAddOns,
305355
skipDevServer,
356+
runQualityGatesChecks = false,
306357
framework = 'react',
307358
packageManager = 'pnpm',
308359
routerOnly = false,
@@ -332,6 +383,7 @@ export async function createAppFixture(
332383
createArgs.push('--add-ons', addOns.join(','))
333384
}
334385

386+
const createStartedAt = now()
335387
await runCommand(
336388
'node',
337389
createArgs,
@@ -342,23 +394,32 @@ export async function createAppFixture(
342394
},
343395
},
344396
)
397+
mark('create', createStartedAt)
345398

346399
await patchViteConfigForE2E(appDir)
347400

348401
if (postCreateAddOns?.length) {
402+
const postAddOnsStartedAt = now()
349403
await runCommand('node', [cliDistPath, 'add', ...postCreateAddOns], {
350404
cwd: appDir,
351405
env: {
352406
CI: '1',
353407
},
354408
})
355409

410+
mark('post-add-ons', postAddOnsStartedAt)
411+
356412
await patchViteConfigForE2E(appDir)
357413
}
358414

359-
await runQualityGates(appDir, packageManager)
415+
if (runQualityGatesChecks) {
416+
const qualityGatesStartedAt = now()
417+
await runQualityGates(appDir, packageManager)
418+
mark('quality-gates', qualityGatesStartedAt)
419+
}
360420

361421
if (skipDevServer) {
422+
logTimings()
362423
return {
363424
rootDir,
364425
appDir,
@@ -374,6 +435,7 @@ export async function createAppFixture(
374435

375436
const dev = getPackageManagerCommandForScript(packageManager, 'dev')
376437

438+
const devServerStartedAt = now()
377439
const server = spawn(dev.command, dev.args, {
378440
cwd: appDir,
379441
env: {
@@ -398,13 +460,16 @@ export async function createAppFixture(
398460
try {
399461
url = await waitForDevServerURL(() => serverStdout)
400462
await waitForServer(url)
463+
mark('dev-server-ready', devServerStartedAt)
401464
} catch (error) {
402465
await stopChild(server)
403466
throw new Error(
404467
`Failed to start app server at ${url}\nstdout:\n${serverStdout}\n\nstderr:\n${serverStderr}\n\n${error}`,
405468
)
406469
}
407470

471+
logTimings()
472+
408473
return {
409474
rootDir,
410475
appDir,
@@ -429,6 +494,10 @@ function toSameOrigin(url: string, appOrigin: URL) {
429494
}
430495
}
431496

497+
function isBlockedByClientError(value: string) {
498+
return value.includes('ERR_BLOCKED_BY_CLIENT')
499+
}
500+
432501
export function attachRuntimeGuards(page: Page, appUrl: string): RuntimeGuards {
433502
const appOrigin = new URL(appUrl)
434503
const pageErrors: Array<string> = []
@@ -442,7 +511,11 @@ export function attachRuntimeGuards(page: Page, appUrl: string): RuntimeGuards {
442511

443512
const onConsole = (message: { type: () => string; text: () => string }) => {
444513
if (message.type() === 'error') {
445-
consoleErrors.push(message.text())
514+
const text = message.text()
515+
if (isBlockedByClientError(text)) {
516+
return
517+
}
518+
consoleErrors.push(text)
446519
}
447520
}
448521

@@ -457,7 +530,7 @@ export function attachRuntimeGuards(page: Page, appUrl: string): RuntimeGuards {
457530
}
458531
const errorText = request.failure()?.errorText || 'unknown error'
459532

460-
if (errorText.includes('ERR_ABORTED')) {
533+
if (errorText.includes('ERR_ABORTED') || isBlockedByClientError(errorText)) {
461534
return
462535
}
463536

packages/cli/tests-e2e/matrix-opportunistic.spec.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
attachRuntimeGuards,
66
createAppFixture,
77
getRepoPath,
8+
optimizePageForFastE2E,
89
type E2EApp,
910
} from './helpers'
1011

@@ -19,16 +20,16 @@ type MatrixScenario = {
1920
assert: (fixture: E2EApp, page: Page) => Promise<void>
2021
}
2122

23+
const reactStarterHeading = /Island hours, but for product teams\.|Start simple, ship quickly\./
24+
2225
const scenarios: Array<MatrixScenario> = [
2326
{
2427
id: 'react-base-pnpm',
2528
framework: 'react',
2629
packageManager: 'pnpm',
2730
visits: ['/'],
2831
assert: async (_, page) => {
29-
await expect(
30-
page.getByRole('heading', { name: 'Island hours, but for product teams.' }),
31-
).toBeVisible()
32+
await expect(page.getByRole('heading', { name: reactStarterHeading })).toBeVisible()
3233
},
3334
},
3435
{
@@ -37,9 +38,7 @@ const scenarios: Array<MatrixScenario> = [
3738
packageManager: 'npm',
3839
visits: ['/'],
3940
assert: async (_, page) => {
40-
await expect(
41-
page.getByRole('heading', { name: 'Island hours, but for product teams.' }),
42-
).toBeVisible()
41+
await expect(page.getByRole('heading', { name: reactStarterHeading })).toBeVisible()
4342
},
4443
},
4544
{
@@ -97,9 +96,7 @@ const scenarios: Array<MatrixScenario> = [
9796
addOns: ['biome', 'netlify'],
9897
visits: ['/'],
9998
assert: async (_, page) => {
100-
await expect(
101-
page.getByRole('heading', { name: 'Island hours, but for product teams.' }),
102-
).toBeVisible()
99+
await expect(page.getByRole('heading', { name: reactStarterHeading })).toBeVisible()
103100
},
104101
},
105102
]
@@ -123,6 +120,7 @@ test.describe('@matrix opportunistic matrix', () => {
123120
const guards = attachRuntimeGuards(page, fixture.url)
124121

125122
try {
123+
await optimizePageForFastE2E(page)
126124
for (const visit of scenario.visits || ['/']) {
127125
await page.goto(`${fixture.url}${visit}`)
128126
await expect(page.locator('body')).toBeVisible()

0 commit comments

Comments
 (0)