Skip to content

Commit c25555b

Browse files
wmertensshairez
authored andcommitted
e2e cli tests for preview
1 parent 4a1ef4f commit c25555b

File tree

5 files changed

+139
-59
lines changed

5 files changed

+139
-59
lines changed

.changeset/fancy-eagles-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'create-qwik': patch
3+
---
4+
5+
FIX: starter app missing package and added preview cli test

e2e/qwik-cli-e2e/tests/serve.spec.ts

Lines changed: 97 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,117 @@
1-
import { assert, test, beforeAll, expect } from 'vitest';
1+
/* eslint-disable no-console */
2+
import { existsSync, readFileSync, writeFileSync } from 'fs';
3+
import { join } from 'path';
4+
import { assert, beforeAll, beforeEach, describe, expect, test } from 'vitest';
25
import {
36
assertHostUnused,
7+
DEFAULT_TIMEOUT,
48
getPageHtml,
5-
promisifiedTreeKill,
69
killAllRegisteredProcesses,
10+
log,
11+
promisifiedTreeKill,
712
runCommandUntil,
813
scaffoldQwikProject,
9-
DEFAULT_TIMEOUT,
14+
type QwikProjectType,
1015
} from '../utils';
11-
import { existsSync, readFileSync, writeFileSync } from 'fs';
12-
import { join } from 'path';
1316

1417
let SERVE_PORT = 3535;
15-
beforeAll(() => {
16-
const config = scaffoldQwikProject();
17-
global.tmpDir = config.tmpDir;
18-
19-
return async () => {
20-
await killAllRegisteredProcesses();
21-
config.cleanupFn();
22-
};
18+
beforeEach(() => {
19+
// the port doesn't clear immediately after the previous test
20+
SERVE_PORT++;
2321
});
22+
for (const type of ['empty', 'playground'] as QwikProjectType[]) {
23+
describe(`template: ${type}`, () => {
24+
beforeAll(() => {
25+
console.log('================================================ scaffolding', type);
26+
const config = scaffoldQwikProject(type);
27+
global.tmpDir = config.tmpDir;
2428

25-
test(
26-
'Should serve the app in dev mode and update the content on hot reload',
27-
{ timeout: DEFAULT_TIMEOUT },
28-
async () => {
29-
const host = `http://localhost:${SERVE_PORT}/`;
30-
await assertHostUnused(host);
31-
const p = await runCommandUntil(
32-
`npm run dev -- --port ${SERVE_PORT}`,
33-
global.tmpDir,
34-
(output) => {
35-
return output.includes(host);
36-
}
37-
);
38-
assert.equal(existsSync(global.tmpDir), true);
29+
return async () => {
30+
try {
31+
await killAllRegisteredProcesses();
32+
} catch (e) {
33+
log(`Error during process cleanup: ${e.message}`);
34+
}
35+
config.cleanupFn();
36+
};
37+
});
3938

40-
await expectHtmlOnARootPage(host);
39+
if (type === 'playground') {
40+
test(
41+
'Should serve the app in dev mode and update the content on hot reload',
42+
{ timeout: DEFAULT_TIMEOUT },
43+
async () => {
44+
const host = `http://localhost:${SERVE_PORT}/`;
45+
await assertHostUnused(host);
46+
const p = await runCommandUntil(
47+
`npm run dev -- --port ${SERVE_PORT}`,
48+
global.tmpDir,
49+
(output) => {
50+
return output.includes(host);
51+
}
52+
);
53+
assert.equal(existsSync(global.tmpDir), true);
4154

42-
await promisifiedTreeKill(p.pid!, 'SIGKILL');
43-
}
44-
);
55+
await expectHtmlOnARootPage(host);
4556

46-
test('Should preview the app', { timeout: DEFAULT_TIMEOUT }, async () => {
47-
// the port doesn't clear immediately after the previous test
48-
SERVE_PORT++;
49-
const host = `http://localhost:${SERVE_PORT}/`;
50-
await assertHostUnused(host);
51-
const p = await runCommandUntil(
52-
`npm run preview -- --port ${SERVE_PORT}`,
53-
global.tmpDir,
54-
(output) => {
55-
return output.includes(host);
57+
// Don't let process termination errors fail the test
58+
try {
59+
await promisifiedTreeKill(p.pid!, 'SIGKILL');
60+
} catch (e) {
61+
log(`Error terminating dev server: ${e.message}`);
62+
}
63+
}
64+
);
5665
}
57-
);
58-
assert.equal(existsSync(global.tmpDir), true);
5966

60-
await expectHtmlOnARootPage(host);
67+
test('Should preview the app', { timeout: DEFAULT_TIMEOUT }, async () => {
68+
const host = `http://localhost:${SERVE_PORT}/`;
69+
await assertHostUnused(host);
6170

62-
await promisifiedTreeKill(p.pid!, 'SIGKILL');
63-
});
71+
// First build the app
72+
const buildProcess = await runCommandUntil(`npm run build`, global.tmpDir, (output) => {
73+
return output.includes('dist/build') || output.includes('built in');
74+
});
75+
76+
try {
77+
await promisifiedTreeKill(buildProcess.pid!, 'SIGKILL');
78+
} catch (e) {
79+
log(`Error terminating build process: ${e.message}`);
80+
}
81+
82+
// Now run the preview
83+
const p = await runCommandUntil(
84+
`npm run preview -- --no-open --port ${SERVE_PORT}`,
85+
global.tmpDir,
86+
(output) => {
87+
return output.includes(host);
88+
}
89+
);
90+
91+
assert.equal(existsSync(global.tmpDir), true);
92+
93+
// Wait a bit for the server to fully start
94+
await new Promise((resolve) => setTimeout(resolve, 2000));
95+
96+
const res = await fetch(host, { headers: { accept: 'text/html' } }).then((r) => r.text());
97+
console.log('** res', res);
98+
99+
// Check for the appropriate content based on template type
100+
if (type === 'playground') {
101+
expect(res).toContain('fantastic');
102+
} else if (type === 'empty') {
103+
expect(res).toContain('Hi');
104+
expect(res).toContain('qwik');
105+
}
106+
107+
try {
108+
await promisifiedTreeKill(p.pid!, 'SIGKILL');
109+
} catch (e) {
110+
log(`Error terminating preview server: ${e.message}`);
111+
}
112+
});
113+
});
114+
}
64115

65116
async function expectHtmlOnARootPage(host: string) {
66117
expect((await getPageHtml(host)).querySelector('.container h1')?.textContent).toBe(

e2e/qwik-cli-e2e/utils/index.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ import treeKill from 'tree-kill';
77
import { promisify } from 'util';
88
import { createDocument } from '../../../packages/qwik/src/testing/document';
99

10-
export function scaffoldQwikProject(): { tmpDir: string; cleanupFn: () => void } {
11-
const tmpHostDirData = getTmpDirSync(process.env.TEMP_E2E_PATH);
10+
export type QwikProjectType = 'playground' | 'library' | 'empty';
11+
export function scaffoldQwikProject(type: QwikProjectType): {
12+
tmpDir: string;
13+
cleanupFn: () => void;
14+
} {
15+
const tmpHostDirData = getTmpDirSync(
16+
process.env.TEMP_E2E_PATH ? `${process.env.TEMP_E2E_PATH}/${type}` : undefined
17+
);
1218
const cleanupFn = () => {
1319
if (!tmpHostDirData.overridden) {
1420
cleanup(tmpHostDirData.path);
@@ -17,7 +23,7 @@ export function scaffoldQwikProject(): { tmpDir: string; cleanupFn: () => void }
1723
}
1824
};
1925
try {
20-
const tmpDir = runCreateQwikCommand(tmpHostDirData.path);
26+
const tmpDir = runCreateQwikCommand(tmpHostDirData.path, type);
2127
log(`Created test application at "${tmpDir}"`);
2228
replacePackagesWithLocalOnes(tmpDir);
2329
return { cleanupFn, tmpDir };
@@ -52,10 +58,10 @@ function getTmpDirSync(tmpDirOverride?: string) {
5258
return { path: dirSync({ prefix: 'qwik_e2e' }).name, overridden: false };
5359
}
5460

55-
function runCreateQwikCommand(tmpDir: string): string {
61+
function runCreateQwikCommand(tmpDir: string, type: 'playground' | 'library' | 'empty'): string {
5662
const appDir = 'e2e-app';
5763
execSync(
58-
`node "${workspaceRoot}/packages/create-qwik/create-qwik.cjs" playground "${join(tmpDir, appDir)}"`
64+
`node "${workspaceRoot}/packages/create-qwik/create-qwik.cjs" ${type} "${join(tmpDir, appDir)}"`
5965
);
6066
return join(tmpDir, appDir);
6167
}
@@ -153,11 +159,15 @@ export async function assertHostUnused(host: string): Promise<void> {
153159
// promisify fails to get the proper type overload, so manually enforcing the type
154160
const _promisifiedTreeKill = promisify(treeKill) as (pid: number, signal: string) => Promise<void>;
155161

156-
export const promisifiedTreeKill = (pid: number, signal: string) => {
162+
export const promisifiedTreeKill = async (pid: number, signal: string) => {
157163
try {
158-
return _promisifiedTreeKill(pid, signal);
164+
return await _promisifiedTreeKill(pid, signal);
159165
} catch (error) {
160-
console.error('Failed to kill the process ' + pid, error);
166+
// Don't treat process termination failures as test failures
167+
// This is especially important on Windows where processes may already be gone
168+
// or may not be properly terminated with tree-kill
169+
log(`Process ${pid} could not be killed, but continuing: ${error.message}`);
170+
return Promise.resolve();
161171
}
162172
};
163173

e2e/qwik-cli-e2e/vite.config.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1-
import { defineConfig } from 'vitest/config';
2-
import tsconfigPaths from 'vite-tsconfig-paths';
1+
import { existsSync, mkdirSync } from 'fs';
32
import { resolve } from 'path';
4-
import { mkdirSync } from 'fs';
3+
import tsconfigPaths from 'vite-tsconfig-paths';
4+
import { defineConfig } from 'vitest/config';
55

66
// if we're running in github CI
77
if (process.env.CI) {
88
// Workaround for npm/pnpm crashing in scaffoldQwikProject because "name is too long"
99
const testPath = resolve(process.cwd(), 'e2e-test-tmp');
10-
mkdirSync(testPath);
10+
11+
// Create base directory if it doesn't exist
12+
if (!existsSync(testPath)) {
13+
mkdirSync(testPath);
14+
}
15+
16+
// Create subdirectories for each template type
17+
const templateTypes = ['empty', 'playground'];
18+
for (const type of templateTypes) {
19+
const templatePath = resolve(testPath, type);
20+
if (!existsSync(templatePath)) {
21+
mkdirSync(templatePath);
22+
}
23+
}
24+
1125
process.env.TEMP_E2E_PATH = testPath;
1226
}
1327

packages/qwik/src/cli/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function limitLength(hint: string, maxLength: number = 50) {
117117
}
118118

119119
export function getPackageManager() {
120-
return detectPackageManager()?.name || 'npm';
120+
return detectPackageManager()?.name || 'pnpm';
121121
}
122122

123123
export function pmRunCmd() {

0 commit comments

Comments
 (0)