Skip to content

Commit 742267c

Browse files
authored
better playwright fixtures: cwd, edit, $ (#14402)
1 parent 81dffec commit 742267c

File tree

4 files changed

+877
-714
lines changed

4 files changed

+877
-714
lines changed

integration/helpers/fixtures.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { ChildProcess } from "node:child_process";
2+
import * as fs from "node:fs/promises";
3+
import { fileURLToPath } from "node:url";
4+
5+
import { test as base } from "@playwright/test";
6+
import {
7+
execa,
8+
ExecaError,
9+
type Options,
10+
parseCommandString,
11+
type ResultPromise,
12+
} from "execa";
13+
import * as Path from "pathe";
14+
15+
import type { TemplateName } from "./vite.js";
16+
17+
declare module "@playwright/test" {
18+
interface Page {
19+
errors: Error[];
20+
}
21+
}
22+
23+
const __filename = fileURLToPath(import.meta.url);
24+
const ROOT = Path.join(__filename, "../../..");
25+
const TMP = Path.join(ROOT, ".tmp/integration");
26+
const templatePath = (templateName: string) =>
27+
Path.resolve(ROOT, "integration/helpers", templateName);
28+
29+
type Edits = Record<string, string | ((contents: string) => string)>;
30+
31+
async function applyEdits(cwd: string, edits: Edits) {
32+
const promises = Object.entries(edits).map(async ([file, transform]) => {
33+
const filepath = Path.join(cwd, file);
34+
await fs.writeFile(
35+
filepath,
36+
typeof transform === "function"
37+
? transform(await fs.readFile(filepath, "utf8"))
38+
: transform,
39+
"utf8",
40+
);
41+
return;
42+
});
43+
await Promise.all(promises);
44+
}
45+
46+
export const test = base.extend<{
47+
template: TemplateName;
48+
files: Edits;
49+
cwd: string;
50+
edit: (edits: Edits) => Promise<void>;
51+
$: (
52+
command: string,
53+
options?: Pick<Options, "env" | "timeout">,
54+
) => ResultPromise<{ reject: false }> & {
55+
buffer: { stdout: string; stderr: string };
56+
};
57+
}>({
58+
template: ["vite-6-template", { option: true }],
59+
files: [{}, { option: true }],
60+
page: async ({ page }, use) => {
61+
page.errors = [];
62+
page.on("pageerror", (error: Error) => page.errors.push(error));
63+
await use(page);
64+
},
65+
66+
cwd: async ({ template, files }, use, testInfo) => {
67+
await fs.mkdir(TMP, { recursive: true });
68+
const cwd = await fs.mkdtemp(Path.join(TMP, template + "-"));
69+
testInfo.attach("cwd", { body: cwd });
70+
71+
await fs.cp(templatePath(template), cwd, {
72+
errorOnExist: true,
73+
recursive: true,
74+
});
75+
76+
await applyEdits(cwd, files);
77+
78+
await use(cwd);
79+
},
80+
81+
edit: async ({ cwd }, use) => {
82+
await use(async (edits) => applyEdits(cwd, edits));
83+
},
84+
85+
$: async ({ cwd }, use) => {
86+
const spawn = execa({
87+
cwd,
88+
env: {
89+
NO_COLOR: "1",
90+
FORCE_COLOR: "0",
91+
},
92+
reject: false,
93+
});
94+
95+
let testHasEnded = false;
96+
const processes: Array<ResultPromise> = [];
97+
const unexpectedErrors: Array<Error> = [];
98+
99+
await use((command, options = {}) => {
100+
const [file, ...args] = parseCommandString(command);
101+
102+
const p = spawn(file, args, options);
103+
if (p instanceof ChildProcess) {
104+
processes.push(p);
105+
}
106+
107+
p.then((result) => {
108+
if (!(result instanceof Error)) return result;
109+
110+
// Once the test has ended, this process will be killed as part of its teardown resulting in an ExecaError.
111+
// We only care about surfacing errors that occurred during test execution, not during teardown.
112+
const expectedError = testHasEnded && result instanceof ExecaError;
113+
if (expectedError) return result;
114+
unexpectedErrors.push(result);
115+
});
116+
117+
const buffer = { stdout: "", stderr: "" };
118+
p.stdout?.on("data", (data) => (buffer.stdout += data.toString()));
119+
p.stderr?.on("data", (data) => (buffer.stderr += data.toString()));
120+
return Object.assign(p, { buffer });
121+
});
122+
123+
testHasEnded = true;
124+
processes.forEach((p) => p.kill());
125+
126+
// Throw any unexpected errors that occurred during test execution
127+
if (unexpectedErrors.length > 0) {
128+
const errorMessage =
129+
unexpectedErrors.length === 1
130+
? `Unexpected process error: ${unexpectedErrors[0].message}`
131+
: `${unexpectedErrors.length} unexpected process errors:\n${unexpectedErrors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`;
132+
133+
const error = new Error(errorMessage);
134+
error.stack = unexpectedErrors[0].stack;
135+
throw error;
136+
}
137+
},
138+
});

integration/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"cheerio": "^1.0.0-rc.12",
2626
"cross-spawn": "^7.0.3",
2727
"dedent": "^0.7.0",
28-
"execa": "^5.1.1",
28+
"execa": "^9.6.0",
2929
"express": "^4.19.2",
3030
"get-port": "^5.1.1",
3131
"glob": "8.0.3",

0 commit comments

Comments
 (0)