walkwright 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/bin/walkwright.js +3 -0
- package/build/aria.d.ts +92 -0
- package/build/aria.js +215 -0
- package/build/aria.js.map +1 -0
- package/build/browser.d.ts +3 -0
- package/build/browser.js +18 -0
- package/build/browser.js.map +1 -0
- package/build/chunk-5CHKYNP7.js +507 -0
- package/build/chunk-5CHKYNP7.js.map +1 -0
- package/build/chunk-6YRKPGUO.js +1197 -0
- package/build/chunk-6YRKPGUO.js.map +1 -0
- package/build/chunk-A452S7R3.js +35 -0
- package/build/chunk-A452S7R3.js.map +1 -0
- package/build/chunk-BODO6HIH.js +60 -0
- package/build/chunk-BODO6HIH.js.map +1 -0
- package/build/chunk-FJI2CMCE.js +439 -0
- package/build/chunk-FJI2CMCE.js.map +1 -0
- package/build/chunk-JYHZVRB2.js +80 -0
- package/build/chunk-JYHZVRB2.js.map +1 -0
- package/build/chunk-KQN5IYN6.js +97 -0
- package/build/chunk-KQN5IYN6.js.map +1 -0
- package/build/chunk-KTXNERHT.js +72 -0
- package/build/chunk-KTXNERHT.js.map +1 -0
- package/build/chunk-MFTC7XL2.js +141 -0
- package/build/chunk-MFTC7XL2.js.map +1 -0
- package/build/chunk-N4SSLHMP.js +108 -0
- package/build/chunk-N4SSLHMP.js.map +1 -0
- package/build/chunk-RNNW7OW2.js +270 -0
- package/build/chunk-RNNW7OW2.js.map +1 -0
- package/build/chunk-UAWBPTDW.js +51 -0
- package/build/chunk-UAWBPTDW.js.map +1 -0
- package/build/chunk-Z7CW5CAY.js +64 -0
- package/build/chunk-Z7CW5CAY.js.map +1 -0
- package/build/cli.d.ts +1 -0
- package/build/cli.js +253 -0
- package/build/cli.js.map +1 -0
- package/build/config.d.ts +13 -0
- package/build/config.js +15 -0
- package/build/config.js.map +1 -0
- package/build/descriptor.d.ts +96 -0
- package/build/descriptor.js +27 -0
- package/build/descriptor.js.map +1 -0
- package/build/device-auth.d.ts +6 -0
- package/build/device-auth.js +11 -0
- package/build/device-auth.js.map +1 -0
- package/build/locator.d.ts +140 -0
- package/build/locator.js +92 -0
- package/build/locator.js.map +1 -0
- package/build/paths.d.ts +11 -0
- package/build/paths.js +22 -0
- package/build/paths.js.map +1 -0
- package/build/pom-kcSMxwX2.d.ts +349 -0
- package/build/pom.d.ts +6 -0
- package/build/pom.js +37 -0
- package/build/pom.js.map +1 -0
- package/build/project.d.ts +13 -0
- package/build/project.js +19 -0
- package/build/project.js.map +1 -0
- package/build/provisioner.d.ts +34 -0
- package/build/provisioner.js +14 -0
- package/build/provisioner.js.map +1 -0
- package/build/recorder.d.ts +6 -0
- package/build/recorder.js +15 -0
- package/build/recorder.js.map +1 -0
- package/build/reporter.d.ts +16 -0
- package/build/reporter.js +426 -0
- package/build/reporter.js.map +1 -0
- package/build/schema.d.ts +49 -0
- package/build/schema.js +16 -0
- package/build/schema.js.map +1 -0
- package/build/storage-state.d.ts +22 -0
- package/build/storage-state.js +42 -0
- package/build/storage-state.js.map +1 -0
- package/build/store.d.ts +17 -0
- package/build/store.js +9 -0
- package/build/store.js.map +1 -0
- package/build/ui.d.ts +49 -0
- package/build/ui.js +56 -0
- package/build/ui.js.map +1 -0
- package/build/upload.d.ts +10 -0
- package/build/upload.js +8 -0
- package/build/upload.js.map +1 -0
- package/build/wait.d.ts +14 -0
- package/build/wait.js +50 -0
- package/build/wait.js.map +1 -0
- package/package.json +52 -0
- package/src/aria.ts +326 -0
- package/src/browser.ts +15 -0
- package/src/cli.ts +295 -0
- package/src/config.ts +16 -0
- package/src/descriptor.ts +179 -0
- package/src/device-auth.ts +188 -0
- package/src/locator.ts +683 -0
- package/src/paths.ts +70 -0
- package/src/pom.ts +2363 -0
- package/src/project.ts +77 -0
- package/src/provisioner.ts +183 -0
- package/src/recorder.ts +877 -0
- package/src/reporter.ts +561 -0
- package/src/schema.ts +37 -0
- package/src/storage-state.ts +71 -0
- package/src/store.ts +96 -0
- package/src/ui.ts +333 -0
- package/src/upload.ts +116 -0
- package/src/wait.ts +63 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import type { StoreScope } from "./paths.js";
|
|
14
|
+
import { ensureWalkwrightDir, storeDir } from "./paths.js";
|
|
15
|
+
|
|
16
|
+
export interface StoreOptions<Value> {
|
|
17
|
+
scope?: StoreScope;
|
|
18
|
+
schema?: z.ZodType<Value>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Store<Value> {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
exists: () => boolean;
|
|
24
|
+
read: () => Value | undefined;
|
|
25
|
+
persist: (value: Value) => void;
|
|
26
|
+
clear: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createStore<Value>(
|
|
30
|
+
name: string,
|
|
31
|
+
{ scope = "worker", schema }: StoreOptions<Value> = {},
|
|
32
|
+
): Store<Value> {
|
|
33
|
+
const path = (): string => join(storeDir(scope), `${name}.json`);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
get path(): string {
|
|
37
|
+
return path();
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
exists: (): boolean => existsSync(path()),
|
|
41
|
+
|
|
42
|
+
read: (): Value | undefined => {
|
|
43
|
+
const file = path();
|
|
44
|
+
|
|
45
|
+
if (!existsSync(file)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let parsed: unknown;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
console.log(`${file} is not valid JSON — ignoring the cached ${name}`);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
59
|
+
console.log(`${file} is not a ${name} — ignoring the cached ${name}`);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (schema === undefined) {
|
|
64
|
+
return parsed as Value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const decoded = z.safeDecode(schema, parsed);
|
|
68
|
+
|
|
69
|
+
if (!decoded.success) {
|
|
70
|
+
console.log(
|
|
71
|
+
`${file} did not decode as a ${name} — ignoring the cached ${name}`,
|
|
72
|
+
);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return decoded.data;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
persist: (value): void => {
|
|
80
|
+
ensureWalkwrightDir();
|
|
81
|
+
writeJson(path(), schema === undefined ? value : z.encode(schema, value));
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
clear: (): void => {
|
|
85
|
+
rmSync(path(), { force: true });
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeJson(file: string, value: unknown): void {
|
|
91
|
+
const temporary = `${file}.${String(process.pid)}.tmp`;
|
|
92
|
+
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
94
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
95
|
+
renameSync(temporary, file);
|
|
96
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
export type Tone =
|
|
2
|
+
| "accent"
|
|
3
|
+
| "text"
|
|
4
|
+
| "dim"
|
|
5
|
+
| "faint"
|
|
6
|
+
| "ok"
|
|
7
|
+
| "warn"
|
|
8
|
+
| "fail"
|
|
9
|
+
| "code"
|
|
10
|
+
| "border";
|
|
11
|
+
|
|
12
|
+
type Depth = "truecolor" | "256" | "none";
|
|
13
|
+
|
|
14
|
+
interface Swatch {
|
|
15
|
+
hex: string;
|
|
16
|
+
index: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const dark: Record<Tone, Swatch> = {
|
|
20
|
+
accent: { hex: "#b8943a", index: 178 },
|
|
21
|
+
text: { hex: "#bcac8f", index: 180 },
|
|
22
|
+
dim: { hex: "#8d8972", index: 102 },
|
|
23
|
+
faint: { hex: "#6b6858", index: 59 },
|
|
24
|
+
ok: { hex: "#8fa85e", index: 107 },
|
|
25
|
+
warn: { hex: "#e08542", index: 173 },
|
|
26
|
+
fail: { hex: "#d66848", index: 167 },
|
|
27
|
+
code: { hex: "#cc7f66", index: 173 },
|
|
28
|
+
border: { hex: "#333333", index: 236 },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const light: Record<Tone, Swatch> = {
|
|
32
|
+
accent: { hex: "#9a7820", index: 136 },
|
|
33
|
+
text: { hex: "#3a3828", index: 237 },
|
|
34
|
+
dim: { hex: "#625e55", index: 240 },
|
|
35
|
+
faint: { hex: "#948e80", index: 246 },
|
|
36
|
+
ok: { hex: "#5f8a3a", index: 64 },
|
|
37
|
+
warn: { hex: "#c0741e", index: 172 },
|
|
38
|
+
fail: { hex: "#b8442c", index: 130 },
|
|
39
|
+
code: { hex: "#a85030", index: 130 },
|
|
40
|
+
border: { hex: "#c9bfa6", index: 250 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const glyph = {
|
|
44
|
+
ok: "✓",
|
|
45
|
+
fail: "✗",
|
|
46
|
+
carried: "↺",
|
|
47
|
+
unseen: "●",
|
|
48
|
+
open: "▾",
|
|
49
|
+
closed: "▸",
|
|
50
|
+
branch: "├",
|
|
51
|
+
last: "└",
|
|
52
|
+
cursor: "▌",
|
|
53
|
+
prompt: ">",
|
|
54
|
+
filled: "▰",
|
|
55
|
+
empty: "▱",
|
|
56
|
+
dot: "·",
|
|
57
|
+
bar: "│",
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
process.stdout.on("error", (error: NodeJS.ErrnoException) => {
|
|
61
|
+
if (error.code !== "EPIPE") {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const depth = detectDepth();
|
|
67
|
+
const palette = detectLight() ? light : dark;
|
|
68
|
+
const esc = "\u001b";
|
|
69
|
+
const escapePattern = new RegExp(`${esc}\\[[\\d;]*m`, "g");
|
|
70
|
+
const escapeSplit = new RegExp(`(${esc}\\[[\\d;]*m)`);
|
|
71
|
+
const escapeOnly = new RegExp(`^${esc}\\[[\\d;]*m$`);
|
|
72
|
+
|
|
73
|
+
function detectDepth(): Depth {
|
|
74
|
+
const env = process.env;
|
|
75
|
+
|
|
76
|
+
if ((env["NO_COLOR"] ?? "") !== "" || env["FORCE_COLOR"] === "0") {
|
|
77
|
+
return "none";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const forced = (env["FORCE_COLOR"] ?? "") !== "";
|
|
81
|
+
|
|
82
|
+
if (!forced && !process.stdout.isTTY) {
|
|
83
|
+
return "none";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const colorterm = env["COLORTERM"] ?? "";
|
|
87
|
+
|
|
88
|
+
if (colorterm === "truecolor" || colorterm === "24bit") {
|
|
89
|
+
return "truecolor";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const term = env["TERM"] ?? "";
|
|
93
|
+
|
|
94
|
+
if (term === "dumb" || (term === "" && !forced)) {
|
|
95
|
+
return "none";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return "256";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectLight(): boolean {
|
|
102
|
+
const theme = process.env["WALKWRIGHT_THEME"];
|
|
103
|
+
|
|
104
|
+
if (theme === "light" || theme === "dark") {
|
|
105
|
+
return theme === "light";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fgbg = process.env["COLORFGBG"];
|
|
109
|
+
|
|
110
|
+
if (fgbg === undefined) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const background = Number(fgbg.split(";").at(-1));
|
|
115
|
+
|
|
116
|
+
return background === 7 || (background >= 9 && background <= 15);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function open(swatch: Swatch): string {
|
|
120
|
+
if (depth === "truecolor") {
|
|
121
|
+
const value = Number.parseInt(swatch.hex.slice(1), 16);
|
|
122
|
+
|
|
123
|
+
return `\u001b[38;2;${String(value >> 16)};${String((value >> 8) & 255)};${String(value & 255)}m`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `\u001b[38;5;${String(swatch.index)}m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function paint(tone: Tone, value: string): string {
|
|
130
|
+
if (depth === "none" || value === "") {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `${open(palette[tone])}${value}\u001b[39m`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function bold(value: string): string {
|
|
138
|
+
return depth === "none" ? value : `\u001b[1m${value}\u001b[22m`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function plain(value: string): string {
|
|
142
|
+
return value.replace(escapePattern, "");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function width(value: string): number {
|
|
146
|
+
return plain(value).length;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function columns(): number {
|
|
150
|
+
return process.stdout.isTTY ? process.stdout.columns : 100;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function pad(value: string, to: number): string {
|
|
154
|
+
return value + " ".repeat(Math.max(0, to - width(value)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function clip(value: string, to: number): string {
|
|
158
|
+
if (width(value) <= to) {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let seen = 0;
|
|
163
|
+
let out = "";
|
|
164
|
+
|
|
165
|
+
for (const part of value.split(escapeSplit)) {
|
|
166
|
+
if (escapeOnly.test(part)) {
|
|
167
|
+
out += part;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const char of part) {
|
|
172
|
+
if (seen === to - 1) {
|
|
173
|
+
return `${out}…${depth === "none" ? "" : "\u001b[0m"}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
out += char;
|
|
177
|
+
seen += 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function line(...parts: string[]): void {
|
|
185
|
+
process.stdout.write(`${parts.join("")}\n`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function blank(): void {
|
|
189
|
+
process.stdout.write("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function status(...segments: string[]): string {
|
|
193
|
+
return ` ${segments.filter((segment) => segment !== "").join(paint("faint", ` ${glyph.bar} `))}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function badge(count: number, tone: Tone = "dim"): string {
|
|
197
|
+
return paint(tone, `[${String(count)}]`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function key(name: string): string {
|
|
201
|
+
return paint("accent", `[${name}]`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function action(verb: string): string {
|
|
205
|
+
return paint("accent", `[${verb}]`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function prompt(text: string): string {
|
|
209
|
+
return `${paint("accent", glyph.prompt)} ${text}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function dashed(inner: number): string {
|
|
213
|
+
return paint("border", "╌".repeat(inner));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface BoxOptions {
|
|
217
|
+
legend: string;
|
|
218
|
+
title?: string;
|
|
219
|
+
primary?: boolean;
|
|
220
|
+
min?: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function box(rows: string[], options: BoxOptions): string[] {
|
|
224
|
+
const outer = Math.min(columns() - 1, 100);
|
|
225
|
+
const min = Math.max(options.min ?? 44, width(options.legend) + 10);
|
|
226
|
+
const widest = rows.reduce((max, row) => Math.max(max, width(row)), 0);
|
|
227
|
+
const inner = Math.max(min - 4, Math.min(widest, outer - 4));
|
|
228
|
+
|
|
229
|
+
const edge = (value: string): string => paint("border", value);
|
|
230
|
+
|
|
231
|
+
const legend = paint(
|
|
232
|
+
options.primary === true ? "accent" : "dim",
|
|
233
|
+
options.legend.toUpperCase(),
|
|
234
|
+
);
|
|
235
|
+
const heading =
|
|
236
|
+
options.title === undefined
|
|
237
|
+
? `${edge("─ ")}${legend}${edge(" ")}`
|
|
238
|
+
: `${edge("─ ")}${legend}${edge(" ─ ")}${paint("text", options.title)}${edge(" ")}`;
|
|
239
|
+
const top = `${edge("┌")}${clip(heading, inner + 2)}${edge("─".repeat(Math.max(0, inner + 2 - width(heading))))}${edge("┐")}`;
|
|
240
|
+
const bottom = `${edge("└")}${edge("─".repeat(inner + 2))}${edge("┘")}`;
|
|
241
|
+
|
|
242
|
+
const body = rows.map(
|
|
243
|
+
(row) =>
|
|
244
|
+
`${edge("│")} ${pad(clip(row === "" ? dashed(inner) : row, inner), inner)} ${edge("│")}`,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return [top, ...body, bottom];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function progress(done: number, total: number, cells = 5): string {
|
|
251
|
+
const filled = total === 0 ? cells : Math.round((done / total) * cells);
|
|
252
|
+
|
|
253
|
+
return paint(
|
|
254
|
+
"accent",
|
|
255
|
+
glyph.filled.repeat(filled) + glyph.empty.repeat(cells - filled),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function duration(ms: number): string {
|
|
260
|
+
if (ms < 1000) {
|
|
261
|
+
return `${String(Math.round(ms))}ms`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const seconds = Math.round(ms / 1000);
|
|
265
|
+
|
|
266
|
+
if (seconds < 60) {
|
|
267
|
+
return `${String(seconds)}s`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return `${String(Math.floor(seconds / 60))}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function count(value: number, noun: string): string {
|
|
274
|
+
return `${String(value)} ${noun}${value === 1 ? "" : "s"}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function failure(message: string, hint?: string): void {
|
|
278
|
+
process.stderr.write(`${paint("fail", `${glyph.fail} ${message}`)}\n`);
|
|
279
|
+
|
|
280
|
+
if (hint !== undefined) {
|
|
281
|
+
process.stderr.write(`${paint("dim", ` ${hint}`)}\n`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function transient(text: string): () => void {
|
|
286
|
+
if (!process.stdout.isTTY) {
|
|
287
|
+
return () => undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
process.stdout.write(`\r\u001b[2K${clip(text, columns() - 1)}`);
|
|
291
|
+
|
|
292
|
+
return () => {
|
|
293
|
+
process.stdout.write("\r\u001b[2K");
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function pulse(text: string, intervalMs = 500): () => void {
|
|
298
|
+
if (!process.stdout.isTTY) {
|
|
299
|
+
line(text);
|
|
300
|
+
return () => undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let on = true;
|
|
304
|
+
const draw = (): void => {
|
|
305
|
+
process.stdout.write(
|
|
306
|
+
`\r\u001b[2K${clip(`${text} ${on ? paint("accent", glyph.cursor) : " "}`, columns() - 1)}`,
|
|
307
|
+
);
|
|
308
|
+
on = !on;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
draw();
|
|
312
|
+
const timer = setInterval(draw, intervalMs);
|
|
313
|
+
|
|
314
|
+
return () => {
|
|
315
|
+
clearInterval(timer);
|
|
316
|
+
process.stdout.write("\r\u001b[2K");
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function wordmark(): string {
|
|
321
|
+
return bold(`${paint("accent", "MAP")}${paint("text", "WRIGHT")}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function logo(): string[] {
|
|
325
|
+
const mark = (value: string): string => paint("accent", value);
|
|
326
|
+
|
|
327
|
+
return [
|
|
328
|
+
mark(" ┌─■─┐"),
|
|
329
|
+
`${mark(" ■ ■")} ${wordmark()}`,
|
|
330
|
+
mark(" ┌┴┐"),
|
|
331
|
+
mark(" ■ ■"),
|
|
332
|
+
];
|
|
333
|
+
}
|
package/src/upload.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WalkwrightConfig } from "./project.js";
|
|
5
|
+
|
|
6
|
+
const uploadConcurrency = 8;
|
|
7
|
+
|
|
8
|
+
export type UploadProgress = (
|
|
9
|
+
done: number,
|
|
10
|
+
total: number,
|
|
11
|
+
file: string,
|
|
12
|
+
) => void;
|
|
13
|
+
|
|
14
|
+
export interface Uploaded {
|
|
15
|
+
url: string;
|
|
16
|
+
number: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function uploadRun(
|
|
20
|
+
config: WalkwrightConfig,
|
|
21
|
+
branch: string,
|
|
22
|
+
runDir: string,
|
|
23
|
+
files: string[],
|
|
24
|
+
onProgress?: UploadProgress,
|
|
25
|
+
): Promise<Uploaded> {
|
|
26
|
+
const base = config.url.replace(/\/$/, "");
|
|
27
|
+
const auth = { authorization: `Bearer ${config.token}` };
|
|
28
|
+
|
|
29
|
+
const { id } = await request<{ id: string }>(`${base}/api/reports`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: auth,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let next = 0;
|
|
35
|
+
let done = 0;
|
|
36
|
+
|
|
37
|
+
await Promise.all(
|
|
38
|
+
Array.from(
|
|
39
|
+
{ length: Math.min(uploadConcurrency, files.length) },
|
|
40
|
+
async () => {
|
|
41
|
+
while (next < files.length) {
|
|
42
|
+
const file = files[next++];
|
|
43
|
+
|
|
44
|
+
if (file === undefined) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await request(
|
|
49
|
+
`${base}/api/reports/${id}/files/${encodeURIComponent(file)}`,
|
|
50
|
+
{
|
|
51
|
+
method: "PUT",
|
|
52
|
+
headers: auth,
|
|
53
|
+
body: readFileSync(join(runDir, file)),
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
onProgress?.(++done, files.length, file);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const { url, number } = await request<{ url: string; number?: number }>(
|
|
63
|
+
`${base}/api/reports/${id}/finalize`,
|
|
64
|
+
{
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { ...auth, "x-walkwright-branch": encodeURIComponent(branch) },
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return { url, number: number ?? numberFromUrl(url) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function numberFromUrl(url: string): number | undefined {
|
|
74
|
+
const last = Number(url.replace(/\/$/, "").split("/").at(-1));
|
|
75
|
+
|
|
76
|
+
return Number.isInteger(last) ? last : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function errorDetail(response: Response): Promise<string | undefined> {
|
|
80
|
+
let body: unknown;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
body = await response.json();
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof body !== "object" || body === null) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const error = (body as Record<string, unknown>)["error"];
|
|
93
|
+
|
|
94
|
+
return typeof error === "string" ? error : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function request<Body>(
|
|
98
|
+
url: string,
|
|
99
|
+
init: { method: string; headers: Record<string, string>; body?: Buffer },
|
|
100
|
+
): Promise<Body> {
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
method: init.method,
|
|
103
|
+
headers: init.headers,
|
|
104
|
+
...(init.body === undefined ? {} : { body: new Uint8Array(init.body) }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const detail = await errorDetail(response);
|
|
109
|
+
|
|
110
|
+
throw new Error(
|
|
111
|
+
`upload failed — ${String(response.status)} on ${init.method} ${basename(url)}${detail === undefined ? "" : `: ${detail}`}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (await response.json()) as Body;
|
|
116
|
+
}
|
package/src/wait.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Locator, Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
export async function visibleWithin(
|
|
4
|
+
locator: Locator,
|
|
5
|
+
timeoutMs: number,
|
|
6
|
+
): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
await locator.waitFor({ state: "visible", timeout: timeoutMs });
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function pollUntil(
|
|
16
|
+
page: Page,
|
|
17
|
+
ready: () => Promise<boolean>,
|
|
18
|
+
options: { timeoutMs?: number; refresh?: () => Promise<void> } = {},
|
|
19
|
+
): Promise<boolean> {
|
|
20
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
21
|
+
const deadline = Date.now() + timeoutMs;
|
|
22
|
+
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (await ready()) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (Date.now() >= deadline) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await page.waitForTimeout(10_000);
|
|
33
|
+
await (options.refresh?.() ?? page.reload());
|
|
34
|
+
await page.waitForTimeout(1_000);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function revealVirtualizedRow(
|
|
39
|
+
page: Page,
|
|
40
|
+
options: { scrollOver: Locator; target: Locator; timeoutMs?: number },
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
43
|
+
const deadline = Date.now() + timeoutMs;
|
|
44
|
+
|
|
45
|
+
await options.scrollOver.hover();
|
|
46
|
+
|
|
47
|
+
for (;;) {
|
|
48
|
+
if ((await options.target.count()) > 0) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Date.now() >= deadline) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`the row did not appear within ${timeoutMs}ms of scrolling`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await page.mouse.wheel(0, 400);
|
|
59
|
+
await page.waitForTimeout(250);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await options.target.first().scrollIntoViewIfNeeded();
|
|
63
|
+
}
|