Skip to content

Commit 0f17a26

Browse files
feat: enhance CI workflow with separate unit and E2E test stages, add fixture isolation utilities, and update test configurations
1 parent 9ddc579 commit 0f17a26

16 files changed

+218
-125
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ jobs:
5353
- name: 🚀 Build
5454
run: pnpm build
5555

56-
- name: 🧪 Test
57-
run: pnpm test:run --coverage
56+
- name: 🧪 Test (Unit + Integration)
57+
run: pnpm test:unit --coverage
58+
59+
- name: 🧪 Test (E2E)
60+
run: pnpm test:e2e
5861

5962
- name: 📊 Vitest Coverage Report
6063
uses: davelosert/vitest-coverage-report-action@v2

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ native/index.d.ts
2222
.nuxt
2323

2424
# External projects (not part of this repo)
25-
vercube/
25+
vercube/
26+
27+
# Test temp directories
28+
tests/.temp/

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@
126126
"lint:fix": "eslint . --fix",
127127
"test": "vitest",
128128
"test:ui": "vitest --ui",
129-
"test:run": "vitest run",
129+
"test:run": "pnpm test:unit && pnpm test:e2e",
130+
"test:unit": "vitest run",
131+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
130132
"test:coverage": "vitest run --coverage",
131133
"typecheck": "tsc --noEmit"
132134
},
@@ -197,7 +199,6 @@
197199
"changelogen": "catalog:",
198200
"crossws": "catalog:",
199201
"eslint": "catalog:",
200-
"get-port": "catalog:",
201202
"graphql": "catalog:",
202203
"graphql-yoga": "catalog:",
203204
"nitro": "catalog:",

pnpm-lock.yaml

Lines changed: 0 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ catalog:
4040
crossws: ^0.4.1
4141
defu: ^6.1.4
4242
eslint: ^9.39.2
43-
get-port: ^7.1.0
4443
giget: ^2.0.0
4544
graphql: ^16.12.0
4645
graphql-config: ^5.1.5

src/core/watcher/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface CoreWatcherConfig {
2828
persistent?: boolean
2929
/** Ignore initial scan (default: true) */
3030
ignoreInitial?: boolean
31+
/** Use polling mode (default: false, but auto-enabled in CI/test environments) */
32+
usePolling?: boolean
3133
}
3234

3335
/**
@@ -128,8 +130,12 @@ export function createIgnoredFunction(): (path: string) => boolean {
128130
return true
129131
}
130132

131-
// Allow directory traversal
132-
if (!path.includes('.') || path.endsWith('/')) {
133+
// Get the filename from the path
134+
const filename = path.split('/').pop() || ''
135+
136+
// Allow directory traversal (paths without extensions in filename)
137+
// A file has an extension if the filename contains a dot
138+
if (!filename.includes('.') || path.endsWith('/')) {
133139
return false
134140
}
135141

@@ -195,12 +201,23 @@ export function createCoreWatcher(
195201
debounceMs = 150,
196202
persistent = true,
197203
ignoreInitial = true,
204+
usePolling,
198205
} = config
199206

207+
// Auto-enable polling in CI/test environments or when explicitly requested
208+
const shouldUsePolling = usePolling ?? (process.env.CI === 'true' || process.env.VITE_TEST === 'true')
209+
200210
const watcher = watch(watchDirs, {
201211
persistent,
202212
ignoreInitial,
203213
ignored: createIgnoredFunction(),
214+
usePolling: shouldUsePolling,
215+
interval: shouldUsePolling ? 100 : undefined,
216+
// awaitWriteFinish helps with detecting file changes properly
217+
awaitWriteFinish: shouldUsePolling ? {
218+
stabilityThreshold: 100,
219+
pollInterval: 50,
220+
} : false,
204221
})
205222

206223
// Track pending changes

tests/e2e/extend-client-dir.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,31 @@ import { build, createNitro, prepare } from 'nitro/builder'
1515
import { join, resolve } from 'pathe'
1616
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
1717
import graphql from '../../src'
18+
import { cleanupIsolatedFixture, createIsolatedFixture } from '../utils/fixture'
1819

1920
const fixturesDir = resolve(__dirname, '../fixtures')
21+
const originalFixtureDir = resolve(fixturesDir, 'extend-multi')
22+
23+
// Will be set in beforeAll after creating isolated fixture
24+
let isolatedFixtureDir: string
2025

2126
describe('extend Package clientDir E2E', () => {
2227
let nitro: Nitro
2328

2429
beforeAll(async () => {
30+
// Create isolated copy of fixture to avoid conflicts with parallel tests
31+
isolatedFixtureDir = createIsolatedFixture(originalFixtureDir)
32+
2533
nitro = await createNitro({
26-
rootDir: resolve(fixturesDir, 'extend-multi/main-project'),
34+
rootDir: resolve(isolatedFixtureDir, 'main-project'),
2735
dev: true,
2836
modules: [
2937
graphql({
3038
framework: 'graphql-yoga',
3139
skipLocalScan: true,
3240
extend: [
33-
resolve(fixturesDir, 'extend-multi/auth'),
34-
resolve(fixturesDir, 'extend-multi/ecommerce'),
41+
resolve(isolatedFixtureDir, 'auth'),
42+
resolve(isolatedFixtureDir, 'ecommerce'),
3543
],
3644
}),
3745
],
@@ -48,6 +56,8 @@ describe('extend Package clientDir E2E', () => {
4856

4957
afterAll(async () => {
5058
await nitro?.close()
59+
// Clean up isolated fixture
60+
cleanupIsolatedFixture(isolatedFixtureDir)
5161
})
5262

5363
describe('clientDir scanning from extend packages', () => {

tests/e2e/extend-clientdir-watcher.test.ts

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import { build, createNitro, prepare } from 'nitro/builder'
1313
import { join, resolve } from 'pathe'
1414
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
1515
import graphql from '../../src'
16+
import { cleanupIsolatedFixture, createIsolatedFixture } from '../utils/fixture'
1617

1718
const fixturesDir = resolve(__dirname, '../fixtures')
18-
const mainProjectDir = resolve(fixturesDir, 'extend-multi/main-project')
19-
const ecommerceDir = resolve(fixturesDir, 'extend-multi/ecommerce')
20-
const ecommerceClientDir = resolve(ecommerceDir, 'client/graphql')
21-
const clientQueryPath = join(ecommerceClientDir, 'products.graphql')
19+
const originalFixtureDir = resolve(fixturesDir, 'extend-multi')
2220

23-
// Note: nitro-graphql uses .graphql as the output directory for types
24-
const graphqlBuildDir = join(mainProjectDir, '.graphql')
25-
const clientTypesPath = join(graphqlBuildDir, 'nitro-graphql-client.d.ts')
21+
// These will be set in beforeAll after creating isolated fixture
22+
let isolatedFixtureDir: string
23+
let mainProjectDir: string
24+
let ecommerceDir: string
25+
let ecommerceClientDir: string
26+
let clientQueryPath: string
27+
let graphqlBuildDir: string
28+
let clientTypesPath: string
2629

2730
// Initial client query
2831
const initialQuery = `query GetProducts {
@@ -52,7 +55,7 @@ query GetAllProducts {
5255
}
5356
`
5457

55-
// Clean up generated files
58+
// Clean up generated files within isolated fixture
5659
function cleanupGeneratedFiles() {
5760
const dirsToClean = [
5861
join(mainProjectDir, '.nitro'),
@@ -73,22 +76,28 @@ function resetFixtureFiles() {
7376
writeFileSync(clientQueryPath, initialQuery, 'utf-8')
7477
}
7578

76-
// Helper to wait for file to be updated
79+
// Helper to wait for file to be updated with retries
7780
async function waitForFileChange(
7881
filePath: string,
7982
expectedContent: string,
80-
timeout = 5000,
83+
timeout = 10000,
8184
): Promise<boolean> {
8285
const startTime = Date.now()
86+
const pollInterval = 200
8387

8488
while (Date.now() - startTime < timeout) {
85-
if (existsSync(filePath)) {
86-
const content = readFileSync(filePath, 'utf-8')
87-
if (content.includes(expectedContent)) {
88-
return true
89+
try {
90+
if (existsSync(filePath)) {
91+
const content = readFileSync(filePath, 'utf-8')
92+
if (content.includes(expectedContent)) {
93+
return true
94+
}
8995
}
9096
}
91-
await new Promise(r => setTimeout(r, 100))
97+
catch {
98+
// File might be being written, ignore and retry
99+
}
100+
await new Promise(r => setTimeout(r, pollInterval))
92101
}
93102

94103
return false
@@ -98,6 +107,17 @@ describe('extend Package clientDir File Watcher E2E', () => {
98107
let nitro: Nitro
99108

100109
beforeAll(async () => {
110+
// Create isolated copy of fixture to avoid conflicts with parallel tests
111+
isolatedFixtureDir = createIsolatedFixture(originalFixtureDir)
112+
113+
// Set up paths based on isolated fixture
114+
mainProjectDir = resolve(isolatedFixtureDir, 'main-project')
115+
ecommerceDir = resolve(isolatedFixtureDir, 'ecommerce')
116+
ecommerceClientDir = resolve(ecommerceDir, 'client/graphql')
117+
clientQueryPath = join(ecommerceClientDir, 'products.graphql')
118+
graphqlBuildDir = join(mainProjectDir, '.graphql')
119+
clientTypesPath = join(graphqlBuildDir, 'nitro-graphql-client.d.ts')
120+
101121
cleanupGeneratedFiles()
102122
resetFixtureFiles()
103123

@@ -110,8 +130,8 @@ describe('extend Package clientDir File Watcher E2E', () => {
110130
framework: 'graphql-yoga',
111131
skipLocalScan: true,
112132
extend: [
113-
resolve(fixturesDir, 'extend-multi/auth'),
114-
resolve(fixturesDir, 'extend-multi/ecommerce'),
133+
resolve(isolatedFixtureDir, 'auth'),
134+
resolve(isolatedFixtureDir, 'ecommerce'),
115135
],
116136
}),
117137
],
@@ -128,6 +148,8 @@ describe('extend Package clientDir File Watcher E2E', () => {
128148
await nitro?.close()
129149
cleanupGeneratedFiles()
130150
resetFixtureFiles()
151+
// Clean up isolated fixture
152+
cleanupIsolatedFixture(isolatedFixtureDir)
131153
})
132154

133155
it('should include extend package clientDir in watchDirs', () => {
@@ -159,14 +181,14 @@ describe('extend Package clientDir File Watcher E2E', () => {
159181
writeFileSync(clientQueryPath, updatedQuery, 'utf-8')
160182

161183
// Wait for file watcher to detect change and regenerate types
162-
// File watcher has 150ms debounce + processing time
163-
const found = await waitForFileChange(clientTypesPath, 'GetAllProducts', 5000)
184+
// File watcher has 150ms debounce + processing time + codegen time
185+
const found = await waitForFileChange(clientTypesPath, 'GetAllProducts', 15000)
164186

165187
expect(found).toBe(true)
166188

167189
// Verify the new query type is present
168190
const updatedContent = readFileSync(clientTypesPath, 'utf-8')
169191
expect(updatedContent).toContain('GetAllProducts')
170192
expect(updatedContent).toContain('GetProducts')
171-
}, 10000)
193+
}, 20000)
172194
})

0 commit comments

Comments
 (0)