everyscreen 0.0.0-stage → 0.1.0
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 +241 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2958 -0
- package/dist/commands-logic-DVWfSe3O.mjs +217 -0
- package/dist/dashboard/assets/button-N-Nucq_K.js +1 -0
- package/dist/dashboard/assets/cn-DMYzmXtt.js +1 -0
- package/dist/dashboard/assets/device-status-logic-Silf3YXA.js +1 -0
- package/dist/dashboard/assets/devices-5pEUg6rA.js +1 -0
- package/dist/dashboard/assets/dist-CnPFbxes.js +1 -0
- package/dist/dashboard/assets/focus._deviceId-DGel-kWw.js +1 -0
- package/dist/dashboard/assets/frame-layout-logic-H_x71NvW.js +1 -0
- package/dist/dashboard/assets/index-DNHZ-i1B.css +2 -0
- package/dist/dashboard/assets/index-DwPQ0eY_.js +10 -0
- package/dist/dashboard/assets/modal-dialog-BaO9xkRr.js +1 -0
- package/dist/dashboard/assets/rolldown-runtime-WkwZ5CU-.js +1 -0
- package/dist/dashboard/assets/routes-CUFa9tL0.js +1 -0
- package/dist/dashboard/assets/run-origin-badge-BeFqg0AC.js +1 -0
- package/dist/dashboard/assets/runs-queries-BYBE7R9S.js +2 -0
- package/dist/dashboard/assets/runs._runId-CokpJ1Rr.js +1 -0
- package/dist/dashboard/assets/runs.index-ZsynzkKU.js +1 -0
- package/dist/dashboard/assets/schemas-BPFqmBjG.js +55 -0
- package/dist/dashboard/assets/screen-boundary-BauyD9pu.js +1 -0
- package/dist/dashboard/assets/status-chip-yqTTTuyZ.js +1 -0
- package/dist/dashboard/assets/tile-status-chip-DisgbNnH.js +1 -0
- package/dist/dashboard/assets/toggle-nciug_di.js +1 -0
- package/dist/dashboard/assets/use-run-totals-B3G4yamQ.js +1 -0
- package/dist/dashboard/assets/useNavigate-8mkM-ffY.js +1 -0
- package/dist/dashboard/assets/useParams-D2lK5BEg.js +1 -0
- package/dist/dashboard/index.html +29 -0
- package/dist/devices-store-edScJDie.mjs +1228 -0
- package/dist/playwright-logic-QFlBFspE.d.mts +138 -0
- package/dist/playwright.d.mts +2 -0
- package/dist/playwright.mjs +18 -0
- package/dist/read-hub-file-N4rZ-6Wr.mjs +180 -0
- package/dist/report-hub-trouble-BjbcuK21.d.mts +2 -0
- package/dist/report-hub-trouble-CaUiyteI.mjs +41 -0
- package/dist/reporter.d.mts +47 -0
- package/dist/reporter.mjs +250 -0
- package/dist/run-one-at-a-time-BeV_a3Fm.mjs +48 -0
- package/dist/runs-store-Bd_ITPFj.mjs +283 -0
- package/dist/test.d.mts +11 -0
- package/dist/test.mjs +69 -0
- package/package.json +106 -4
- package/src/dashboard/ui/theme.css +28 -0
- package/src/skill/SKILL.md +15 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import "playwright";
|
|
3
|
+
import { PlaywrightTestOptions, PlaywrightWorkerOptions, Project } from "@playwright/test";
|
|
4
|
+
import { TestStatus } from "@playwright/test/reporter";
|
|
5
|
+
declare const hubFileSchema: z.ZodObject<{
|
|
6
|
+
pid: z.ZodNumber;
|
|
7
|
+
port: z.ZodNumber;
|
|
8
|
+
devices: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
9
|
+
wsEndpoint: z.ZodString;
|
|
10
|
+
cdpUrl: z.ZodNullable<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
type HubFile = z.infer<typeof hubFileSchema>;
|
|
14
|
+
declare const liveTestStatusSchema: z.ZodEnum<{
|
|
15
|
+
failed: "failed";
|
|
16
|
+
ok: "ok";
|
|
17
|
+
running: "running";
|
|
18
|
+
}>;
|
|
19
|
+
declare const testEventSchema: z.ZodObject<{
|
|
20
|
+
deviceId: z.ZodString;
|
|
21
|
+
testId: z.ZodString;
|
|
22
|
+
title: z.ZodString;
|
|
23
|
+
specFile: z.ZodString;
|
|
24
|
+
status: z.ZodEnum<{
|
|
25
|
+
failed: "failed";
|
|
26
|
+
ok: "ok";
|
|
27
|
+
running: "running";
|
|
28
|
+
}>;
|
|
29
|
+
errorLine: z.ZodNullable<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
declare const stepEventSchema: z.ZodObject<{
|
|
32
|
+
deviceId: z.ZodString;
|
|
33
|
+
testId: z.ZodString;
|
|
34
|
+
stepIndex: z.ZodNumber;
|
|
35
|
+
stepTitle: z.ZodString;
|
|
36
|
+
status: z.ZodEnum<{
|
|
37
|
+
failed: "failed";
|
|
38
|
+
ok: "ok";
|
|
39
|
+
running: "running";
|
|
40
|
+
}>;
|
|
41
|
+
errorLine: z.ZodNullable<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
declare const runTotalsSchema: z.ZodObject<{
|
|
44
|
+
phase: z.ZodEnum<{
|
|
45
|
+
start: "start";
|
|
46
|
+
update: "update";
|
|
47
|
+
}>;
|
|
48
|
+
totalTests: z.ZodNumber;
|
|
49
|
+
startedAt: z.ZodNumber;
|
|
50
|
+
endedAt: z.ZodNullable<z.ZodNumber>;
|
|
51
|
+
okCount: z.ZodNumber;
|
|
52
|
+
failedCount: z.ZodNumber;
|
|
53
|
+
outcome: z.ZodNullable<z.ZodEnum<{
|
|
54
|
+
failed: "failed";
|
|
55
|
+
ok: "ok";
|
|
56
|
+
}>>;
|
|
57
|
+
endReason: z.ZodNullable<z.ZodEnum<{
|
|
58
|
+
reported: "reported";
|
|
59
|
+
stopped: "stopped";
|
|
60
|
+
}>>;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
declare const testsClearSchema: z.ZodObject<{
|
|
63
|
+
deviceIds: z.ZodArray<z.ZodString>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
declare const runSavedSchema: z.ZodObject<{
|
|
66
|
+
runId: z.ZodString;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
type LiveTestStatus = z.infer<typeof liveTestStatusSchema>;
|
|
69
|
+
type TestEvent = z.infer<typeof testEventSchema>;
|
|
70
|
+
type StepEvent = z.infer<typeof stepEventSchema>;
|
|
71
|
+
type RunTotals = z.infer<typeof runTotalsSchema>;
|
|
72
|
+
type TestsClear = z.infer<typeof testsClearSchema>;
|
|
73
|
+
type RunSaved = z.infer<typeof runSavedSchema>;
|
|
74
|
+
type FindingKind = "overflow" | "clipped-text" | "safe-area" | "tap-target" | "console-error" | "failed-request";
|
|
75
|
+
declare const SEVERITIES: readonly ["failed", "warn"];
|
|
76
|
+
type Severity = (typeof SEVERITIES)[number];
|
|
77
|
+
interface Finding {
|
|
78
|
+
kind: FindingKind;
|
|
79
|
+
severity: Severity;
|
|
80
|
+
deviceId: string;
|
|
81
|
+
selector: string | null;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
interface DeviceOk {
|
|
85
|
+
status: "ok";
|
|
86
|
+
deviceId: string;
|
|
87
|
+
durationMs: number;
|
|
88
|
+
shotPath: string | null;
|
|
89
|
+
}
|
|
90
|
+
interface DeviceFailure {
|
|
91
|
+
status: "failed";
|
|
92
|
+
deviceId: string;
|
|
93
|
+
durationMs: number;
|
|
94
|
+
message: string;
|
|
95
|
+
shotPath: string | null;
|
|
96
|
+
}
|
|
97
|
+
type DeviceAnswer = DeviceOk | DeviceFailure;
|
|
98
|
+
type RunStatus = "running" | "ok" | "failed";
|
|
99
|
+
type RunOrigin = "cli" | "flow" | "playwright";
|
|
100
|
+
type RunKind = "steps" | "sweep";
|
|
101
|
+
interface StepProof {
|
|
102
|
+
shotPath: string | null;
|
|
103
|
+
errorMessage: string;
|
|
104
|
+
codeSnippet: string | null;
|
|
105
|
+
codeLocation: string | null;
|
|
106
|
+
}
|
|
107
|
+
interface RunStep {
|
|
108
|
+
stepId: string;
|
|
109
|
+
title: string;
|
|
110
|
+
startedAt: number;
|
|
111
|
+
deviceAnswers: DeviceAnswer[];
|
|
112
|
+
findings: Finding[];
|
|
113
|
+
proof: StepProof | null;
|
|
114
|
+
}
|
|
115
|
+
interface Run {
|
|
116
|
+
runId: string;
|
|
117
|
+
origin: RunOrigin;
|
|
118
|
+
runKind: RunKind;
|
|
119
|
+
status: RunStatus;
|
|
120
|
+
startedAt: number;
|
|
121
|
+
steps: RunStep[];
|
|
122
|
+
}
|
|
123
|
+
interface DeviceProjectOptions {
|
|
124
|
+
everyscreenDeviceId: string | null;
|
|
125
|
+
}
|
|
126
|
+
type DeviceProject = Project<PlaywrightTestOptions & DeviceProjectOptions, PlaywrightWorkerOptions>;
|
|
127
|
+
interface FinishedTest {
|
|
128
|
+
deviceId: string;
|
|
129
|
+
titlePath: string[];
|
|
130
|
+
status: TestStatus;
|
|
131
|
+
startedAt: number;
|
|
132
|
+
durationMs: number;
|
|
133
|
+
errorMessage: string | null;
|
|
134
|
+
codeSnippet: string | null;
|
|
135
|
+
codeLocation: string | null;
|
|
136
|
+
shotPath: string | null;
|
|
137
|
+
}
|
|
138
|
+
export { RunStep as a, RunTotals as c, TestsClear as d, HubFile as f, Run as i, StepEvent as l, DeviceProjectOptions as n, LiveTestStatus as o, FinishedTest as r, RunSaved as s, DeviceProject as t, TestEvent as u };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { f as pickGridDevices, g as devicesFileSchema, t as parseDevicesYaml } from "./devices-store-edScJDie.mjs";
|
|
2
|
+
import { _ as DEVICES_FILE_PATH } from "./commands-logic-DVWfSe3O.mjs";
|
|
3
|
+
import { h as pickProjectDevices, i as createDeviceProject, r as readOptionalFile, t as readHubFile } from "./read-hub-file-N4rZ-6Wr.mjs";
|
|
4
|
+
// src/playwright/playwright-devices.ts
|
|
5
|
+
const readGridDevices = () => {
|
|
6
|
+
const devicesYaml = readOptionalFile(DEVICES_FILE_PATH);
|
|
7
|
+
const devicesFile = devicesYaml === null ? devicesFileSchema.parse({}) : parseDevicesYaml(devicesYaml);
|
|
8
|
+
return pickGridDevices(devicesFile);
|
|
9
|
+
};
|
|
10
|
+
const listDevices = (deviceIds = null) => {
|
|
11
|
+
const gridDevices = readGridDevices();
|
|
12
|
+
const hubDevices = readHubFile()?.devices ?? null;
|
|
13
|
+
return pickProjectDevices(gridDevices, deviceIds).map((projectDevice) => {
|
|
14
|
+
const hubDevice = hubDevices?.[projectDevice.id] ?? null;
|
|
15
|
+
return createDeviceProject(projectDevice, hubDevice);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
export { listDevices };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { h as hubFileSchema, m as countDeviceStatuses, r as createReplyLine, v as HUB_FILE_PATH, y as HUB_HOST } from "./commands-logic-DVWfSe3O.mjs";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import { basename, sep } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
// src/playwright/playwright-logic.ts
|
|
6
|
+
const STEP_TITLE_SEPARATOR = " › ";
|
|
7
|
+
const KEY_SEPARATOR = "\0";
|
|
8
|
+
const pickScreenshotMode = (browserName) => {
|
|
9
|
+
return browserName === "chromium" ? "only-on-failure" : "on";
|
|
10
|
+
};
|
|
11
|
+
const createDeviceProject = (devicePreset, hubDevice) => {
|
|
12
|
+
const { viewport, screen, deviceScaleFactor, isMobile, hasTouch, userAgent } = devicePreset.use;
|
|
13
|
+
const connectOptions = hubDevice !== null ? { wsEndpoint: hubDevice.wsEndpoint } : void 0;
|
|
14
|
+
return {
|
|
15
|
+
name: devicePreset.id,
|
|
16
|
+
use: {
|
|
17
|
+
everyscreenDeviceId: devicePreset.id,
|
|
18
|
+
browserName: devicePreset.browserName,
|
|
19
|
+
viewport,
|
|
20
|
+
screen,
|
|
21
|
+
deviceScaleFactor,
|
|
22
|
+
isMobile,
|
|
23
|
+
hasTouch,
|
|
24
|
+
userAgent,
|
|
25
|
+
screenshot: pickScreenshotMode(devicePreset.browserName),
|
|
26
|
+
connectOptions
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const pickProjectDevices = (gridDevices, deviceIds) => {
|
|
31
|
+
if (deviceIds === null) return gridDevices;
|
|
32
|
+
return deviceIds.map((deviceId) => {
|
|
33
|
+
const gridDevice = gridDevices.find(({ id }) => id === deviceId);
|
|
34
|
+
if (gridDevice === void 0) {
|
|
35
|
+
const knownIds = gridDevices.map(({ id }) => id).join(", ");
|
|
36
|
+
throw new Error(`Unknown device ${deviceId}. Known devices: ${knownIds}`);
|
|
37
|
+
}
|
|
38
|
+
return gridDevice;
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const isLastAttempt = (attemptSummary) => {
|
|
42
|
+
const { status, retry, retries } = attemptSummary;
|
|
43
|
+
return status === "passed" || status === "skipped" || retry >= retries;
|
|
44
|
+
};
|
|
45
|
+
const pickTestKey = (titlePath) => titlePath.join(KEY_SEPARATOR);
|
|
46
|
+
const isStepComplete = (finishedTests, expectedCount) => finishedTests.length >= expectedCount;
|
|
47
|
+
const isAnswered = (finishedTest) => finishedTest.status !== "skipped";
|
|
48
|
+
const isFailed = (finishedTest) => finishedTest.status !== "passed";
|
|
49
|
+
const pickFirstLine = (message) => {
|
|
50
|
+
const [firstLine] = stripVTControlCharacters(message).split("\n");
|
|
51
|
+
return firstLine;
|
|
52
|
+
};
|
|
53
|
+
const createDeviceAnswer = (finishedTest) => {
|
|
54
|
+
const { deviceId, durationMs, errorMessage, status, shotPath } = finishedTest;
|
|
55
|
+
if (status === "passed") return {
|
|
56
|
+
status: "ok",
|
|
57
|
+
deviceId,
|
|
58
|
+
durationMs,
|
|
59
|
+
shotPath: null
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
status: "failed",
|
|
63
|
+
deviceId,
|
|
64
|
+
durationMs,
|
|
65
|
+
message: errorMessage !== null ? pickFirstLine(errorMessage) : `Test ${status}`,
|
|
66
|
+
shotPath
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const createStepProof = (failedTest) => {
|
|
70
|
+
const { errorMessage, status, shotPath, codeSnippet, codeLocation } = failedTest;
|
|
71
|
+
return {
|
|
72
|
+
shotPath,
|
|
73
|
+
errorMessage: errorMessage !== null ? stripVTControlCharacters(errorMessage) : `Test ${status}`,
|
|
74
|
+
codeSnippet: codeSnippet !== null ? stripVTControlCharacters(codeSnippet) : null,
|
|
75
|
+
codeLocation
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const createRunStep = (stepNumber, finishedTests) => {
|
|
79
|
+
const answeredTests = finishedTests.filter(isAnswered);
|
|
80
|
+
if (answeredTests.length === 0) return null;
|
|
81
|
+
const failedTest = answeredTests.find(isFailed);
|
|
82
|
+
const isAnyTestFailed = failedTest !== void 0;
|
|
83
|
+
const startTimes = answeredTests.map(({ startedAt }) => startedAt);
|
|
84
|
+
return {
|
|
85
|
+
stepId: `step-${stepNumber}`,
|
|
86
|
+
title: answeredTests[0].titlePath.join(STEP_TITLE_SEPARATOR),
|
|
87
|
+
startedAt: Math.min(...startTimes),
|
|
88
|
+
deviceAnswers: answeredTests.map(createDeviceAnswer),
|
|
89
|
+
findings: [],
|
|
90
|
+
proof: isAnyTestFailed ? createStepProof(failedTest) : null
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const pickRunEndStatus = (suiteStatus) => {
|
|
94
|
+
return suiteStatus === "passed" ? "ok" : "failed";
|
|
95
|
+
};
|
|
96
|
+
const createSuiteReplyLine = (runSteps, runId) => {
|
|
97
|
+
const { okCount, warnCount, failedCount } = countDeviceStatuses(runSteps);
|
|
98
|
+
return createReplyLine({
|
|
99
|
+
unit: "device",
|
|
100
|
+
okCount,
|
|
101
|
+
warnCount,
|
|
102
|
+
failedCount,
|
|
103
|
+
runId
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const pickLiveTestStatus = (testStatus) => {
|
|
107
|
+
return testStatus === "passed" || testStatus === "skipped" ? "ok" : "failed";
|
|
108
|
+
};
|
|
109
|
+
const pickFirstErrorLine = (errorMessage) => {
|
|
110
|
+
if (errorMessage === null || errorMessage === void 0) return null;
|
|
111
|
+
return stripVTControlCharacters(errorMessage).split("\n").map((messageLine) => messageLine.trim()).find((messageLine) => messageLine !== "") ?? null;
|
|
112
|
+
};
|
|
113
|
+
const createTestEvent = (testAttempt) => {
|
|
114
|
+
const { deviceId, testId, titlePath, status, errorMessage } = testAttempt;
|
|
115
|
+
const [specPath, ...titleParts] = titlePath;
|
|
116
|
+
const isTestFailed = status === "failed";
|
|
117
|
+
return {
|
|
118
|
+
deviceId,
|
|
119
|
+
testId,
|
|
120
|
+
title: titleParts.join(STEP_TITLE_SEPARATOR),
|
|
121
|
+
specFile: basename(specPath ?? ""),
|
|
122
|
+
status,
|
|
123
|
+
errorLine: isTestFailed ? pickFirstErrorLine(errorMessage) : null
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const USER_STEP_CATEGORIES = ["pw:api", "test.step"];
|
|
127
|
+
const HIDDEN_PARENT_CATEGORIES = ["hook", "fixture"];
|
|
128
|
+
const isCountedStep = (category, ancestorCategories) => {
|
|
129
|
+
const isUserStep = USER_STEP_CATEGORIES.includes(category);
|
|
130
|
+
const hasHiddenAncestor = ancestorCategories.some((ancestorCategory) => HIDDEN_PARENT_CATEGORIES.includes(ancestorCategory) || USER_STEP_CATEGORIES.includes(ancestorCategory));
|
|
131
|
+
return isUserStep && !hasHiddenAncestor;
|
|
132
|
+
};
|
|
133
|
+
const isStepFromFolder = (stepFile, folderPath) => {
|
|
134
|
+
if (stepFile === void 0) return false;
|
|
135
|
+
return stepFile.startsWith(`${folderPath}${sep}`);
|
|
136
|
+
};
|
|
137
|
+
const createStepEvent = (stepReport) => {
|
|
138
|
+
const { deviceId, testId, stepIndex, stepTitle, errorMessage, isEnded } = stepReport;
|
|
139
|
+
const errorLine = pickFirstErrorLine(errorMessage);
|
|
140
|
+
return {
|
|
141
|
+
deviceId,
|
|
142
|
+
testId,
|
|
143
|
+
stepIndex,
|
|
144
|
+
stepTitle,
|
|
145
|
+
status: isEnded ? errorLine !== null ? "failed" : "ok" : "running",
|
|
146
|
+
errorLine
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
const pickRunOutcome = (suiteStatus) => {
|
|
150
|
+
return suiteStatus === "passed" ? "ok" : "failed";
|
|
151
|
+
};
|
|
152
|
+
const createHubUrl = (hubFile) => `http://${HUB_HOST}:${hubFile.port}`;
|
|
153
|
+
const pickScreenHubUrl = (hubFile, deviceId) => {
|
|
154
|
+
if (deviceId === null || hubFile === null) return null;
|
|
155
|
+
const hubDevice = hubFile.devices[deviceId];
|
|
156
|
+
if (hubDevice === void 0) return null;
|
|
157
|
+
if (hubDevice.cdpUrl !== null) return null;
|
|
158
|
+
return createHubUrl(hubFile);
|
|
159
|
+
};
|
|
160
|
+
// src/playwright/read-optional-file.ts
|
|
161
|
+
const readOptionalFile = (filePath) => {
|
|
162
|
+
if (!existsSync(filePath)) return null;
|
|
163
|
+
return readFileSync(filePath, "utf8");
|
|
164
|
+
};
|
|
165
|
+
// src/playwright/read-hub-file.ts
|
|
166
|
+
const readHubFile = () => {
|
|
167
|
+
const serializedHubFile = readOptionalFile(HUB_FILE_PATH);
|
|
168
|
+
if (serializedHubFile === null) return null;
|
|
169
|
+
const hubFileDocument = JSON.parse(serializedHubFile);
|
|
170
|
+
return hubFileSchema.parse(hubFileDocument);
|
|
171
|
+
};
|
|
172
|
+
const readHubFileSafely = (reportTrouble) => {
|
|
173
|
+
try {
|
|
174
|
+
return readHubFile();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
reportTrouble(error);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
export { pickRunOutcome as _, createHubUrl as a, createSuiteReplyLine as c, isLastAttempt as d, isStepComplete as f, pickRunEndStatus as g, pickProjectDevices as h, createDeviceProject as i, createTestEvent as l, pickLiveTestStatus as m, readHubFileSafely as n, createRunStep as o, isStepFromFolder as p, readOptionalFile as r, createStepEvent as s, readHubFile as t, isCountedStep as u, pickScreenHubUrl as v, pickTestKey as y };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { S as LIVE_TESTS_PATH } from "./commands-logic-DVWfSe3O.mjs";
|
|
2
|
+
// src/playwright/post-to-hub.ts
|
|
3
|
+
const POST_TIMEOUT_MS = 2e3;
|
|
4
|
+
const END_WAIT_TIMEOUT_MS = 5e3;
|
|
5
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
6
|
+
const postToHub = async (hubTarget, routePath, message) => {
|
|
7
|
+
const { hubUrl, reportTrouble } = hubTarget;
|
|
8
|
+
try {
|
|
9
|
+
const hubReply = await fetch(`${hubUrl}${LIVE_TESTS_PATH}${routePath}`, {
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: { "Content-Type": JSON_CONTENT_TYPE },
|
|
12
|
+
body: JSON.stringify(message),
|
|
13
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS)
|
|
14
|
+
});
|
|
15
|
+
if (!hubReply.ok) throw new Error(`The hub answered ${hubReply.status}`);
|
|
16
|
+
return true;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
reportTrouble(error);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const waitForHubPosts = async (hubPosts) => {
|
|
23
|
+
const endSignal = AbortSignal.timeout(END_WAIT_TIMEOUT_MS);
|
|
24
|
+
const endTimeout = new Promise((finish) => {
|
|
25
|
+
endSignal.addEventListener("abort", () => finish(), { once: true });
|
|
26
|
+
});
|
|
27
|
+
await Promise.race([Promise.all(hubPosts), endTimeout]);
|
|
28
|
+
};
|
|
29
|
+
// src/playwright/report-hub-trouble.ts
|
|
30
|
+
const createHubTroubleReporter = () => {
|
|
31
|
+
const reporting = { isReported: false };
|
|
32
|
+
return (error) => {
|
|
33
|
+
if (reporting.isReported) return;
|
|
34
|
+
reporting.isReported = true;
|
|
35
|
+
console.error({
|
|
36
|
+
message: "Everyscreen could not reach the hub, so the tests run on without live updates",
|
|
37
|
+
error
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export { postToHub as n, waitForHubPosts as r, createHubTroubleReporter as t };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { a as RunStep, c as RunTotals, d as TestsClear, i as Run, l as StepEvent, o as LiveTestStatus, r as FinishedTest, s as RunSaved, u as TestEvent } from "./playwright-logic-QFlBFspE.mjs";
|
|
2
|
+
import { t as ReportHubTrouble } from "./report-hub-trouble-BjbcuK21.mjs";
|
|
3
|
+
import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult, TestStep } from "@playwright/test/reporter";
|
|
4
|
+
interface HubTarget {
|
|
5
|
+
hubUrl: string;
|
|
6
|
+
reportTrouble: ReportHubTrouble;
|
|
7
|
+
}
|
|
8
|
+
interface RunEnd {
|
|
9
|
+
endedAt: number;
|
|
10
|
+
outcome: "ok" | "failed";
|
|
11
|
+
}
|
|
12
|
+
export default class EveryscreenReporter implements Reporter {
|
|
13
|
+
rootFolder: string;
|
|
14
|
+
runCreation: Promise<Run> | null;
|
|
15
|
+
expectedCounts: Map<string, number>;
|
|
16
|
+
finishedTests: FinishedTest[];
|
|
17
|
+
savedKeys: Set<string>;
|
|
18
|
+
savedSteps: RunStep[];
|
|
19
|
+
pendingTasks: Promise<void>[];
|
|
20
|
+
pendingHubPosts: Promise<boolean>[];
|
|
21
|
+
isHubGivenUp: boolean;
|
|
22
|
+
heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
23
|
+
hubTarget: HubTarget | null;
|
|
24
|
+
deviceIds: string[];
|
|
25
|
+
totalTests: number;
|
|
26
|
+
startedAt: number;
|
|
27
|
+
okCount: number;
|
|
28
|
+
failedCount: number;
|
|
29
|
+
stepIndexes: Map<TestStep, number>;
|
|
30
|
+
stepCounts: Map<TestResult, number>;
|
|
31
|
+
runTask: (task: () => Promise<void>) => void;
|
|
32
|
+
readRun: () => Promise<Run>;
|
|
33
|
+
postToLiveTests: (routePath: string, message: TestEvent | StepEvent | RunTotals | TestsClear | RunSaved) => void;
|
|
34
|
+
sendTestEvent: (testCase: TestCase, status: LiveTestStatus, testResult: TestResult) => void;
|
|
35
|
+
sendRunTotals: (phase: RunTotals["phase"], runEnd: RunEnd | null) => void;
|
|
36
|
+
sendStepEvent: (testCase: TestCase, testStep: TestStep, stepIndex: number, isEnded: boolean) => void;
|
|
37
|
+
createHubTarget: () => HubTarget | null;
|
|
38
|
+
copyShot: (runId: string, testCase: TestCase, testResult: TestResult) => Promise<string | null>;
|
|
39
|
+
saveStep: (testKey: string) => Promise<void>;
|
|
40
|
+
onBegin: (config: FullConfig, suite: Suite) => void;
|
|
41
|
+
onTestBegin: (testCase: TestCase, testResult: TestResult) => void;
|
|
42
|
+
onStepBegin: (testCase: TestCase, testResult: TestResult, testStep: TestStep) => void;
|
|
43
|
+
onStepEnd: (testCase: TestCase, _testResult: TestResult, testStep: TestStep) => void;
|
|
44
|
+
onTestEnd: (testCase: TestCase, testResult: TestResult) => void;
|
|
45
|
+
onEnd: (fullResult: FullResult) => Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { w as STORE_FOLDER } from "./commands-logic-DVWfSe3O.mjs";
|
|
2
|
+
import { t as runOneAtATime } from "./run-one-at-a-time-BeV_a3Fm.mjs";
|
|
3
|
+
import { o as saveRunEvent, r as createRun } from "./runs-store-Bd_ITPFj.mjs";
|
|
4
|
+
import { _ as pickRunOutcome, a as createHubUrl, c as createSuiteReplyLine, d as isLastAttempt, f as isStepComplete, g as pickRunEndStatus, l as createTestEvent, m as pickLiveTestStatus, n as readHubFileSafely, o as createRunStep, p as isStepFromFolder, s as createStepEvent, u as isCountedStep, y as pickTestKey } from "./read-hub-file-N4rZ-6Wr.mjs";
|
|
5
|
+
import { n as postToHub, r as waitForHubPosts, t as createHubTroubleReporter } from "./report-hub-trouble-CaUiyteI.mjs";
|
|
6
|
+
import { dirname, join, relative } from "node:path";
|
|
7
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
// src/playwright/playwright-reporter.ts
|
|
10
|
+
const OWN_FOLDER = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const SHOTS_SUBFOLDER = "shots";
|
|
12
|
+
const SCREENSHOT_ATTACHMENT_NAME = "screenshot";
|
|
13
|
+
const TEST_EVENTS_QUEUE_KEY = "test-events";
|
|
14
|
+
const HEARTBEAT_INTERVAL_MS = 5e3;
|
|
15
|
+
const RUN_SAVED_PATH = "/run-saved";
|
|
16
|
+
const TITLE_PATH_PROJECT_PARTS = 2;
|
|
17
|
+
const pickTitlePath = (testCase) => testCase.titlePath().slice(TITLE_PATH_PROJECT_PARTS);
|
|
18
|
+
const listAncestorCategories = (testStep) => {
|
|
19
|
+
const { parent } = testStep;
|
|
20
|
+
if (parent === void 0) return [];
|
|
21
|
+
return [parent.category, ...listAncestorCategories(parent)];
|
|
22
|
+
};
|
|
23
|
+
const pickDeviceId = (testCase) => {
|
|
24
|
+
const project = testCase.parent.project();
|
|
25
|
+
if (project === void 0) throw new Error(`Test ${testCase.title} belongs to no project`);
|
|
26
|
+
return project.name;
|
|
27
|
+
};
|
|
28
|
+
var EveryscreenReporter = class {
|
|
29
|
+
rootFolder = process.cwd();
|
|
30
|
+
runCreation = null;
|
|
31
|
+
expectedCounts = /* @__PURE__ */ new Map();
|
|
32
|
+
finishedTests = [];
|
|
33
|
+
savedKeys = /* @__PURE__ */ new Set();
|
|
34
|
+
savedSteps = [];
|
|
35
|
+
pendingTasks = [];
|
|
36
|
+
pendingHubPosts = [];
|
|
37
|
+
isHubGivenUp = false;
|
|
38
|
+
heartbeatTimer = null;
|
|
39
|
+
hubTarget = null;
|
|
40
|
+
deviceIds = [];
|
|
41
|
+
totalTests = 0;
|
|
42
|
+
startedAt = 0;
|
|
43
|
+
okCount = 0;
|
|
44
|
+
failedCount = 0;
|
|
45
|
+
stepIndexes = /* @__PURE__ */ new Map();
|
|
46
|
+
stepCounts = /* @__PURE__ */ new Map();
|
|
47
|
+
runTask = (task) => {
|
|
48
|
+
const trackedTask = task().catch((error) => {
|
|
49
|
+
console.error({
|
|
50
|
+
message: "Everyscreen could not save the run",
|
|
51
|
+
error
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
this.pendingTasks.push(trackedTask);
|
|
55
|
+
};
|
|
56
|
+
readRun = async () => {
|
|
57
|
+
const { runCreation } = this;
|
|
58
|
+
if (runCreation === null) throw new Error("The run was never created");
|
|
59
|
+
return runCreation;
|
|
60
|
+
};
|
|
61
|
+
postToLiveTests = (routePath, message) => {
|
|
62
|
+
const { hubTarget, isHubGivenUp } = this;
|
|
63
|
+
if (hubTarget === null || isHubGivenUp) return;
|
|
64
|
+
const hubPost = runOneAtATime(TEST_EVENTS_QUEUE_KEY, async () => {
|
|
65
|
+
if (this.isHubGivenUp) return false;
|
|
66
|
+
const isPosted = await postToHub(hubTarget, routePath, message);
|
|
67
|
+
this.isHubGivenUp = !isPosted;
|
|
68
|
+
return isPosted;
|
|
69
|
+
});
|
|
70
|
+
this.pendingHubPosts.push(hubPost);
|
|
71
|
+
};
|
|
72
|
+
sendTestEvent = (testCase, status, testResult) => {
|
|
73
|
+
const deviceId = pickDeviceId(testCase);
|
|
74
|
+
const titlePath = pickTitlePath(testCase);
|
|
75
|
+
const errorMessage = testResult.error?.message;
|
|
76
|
+
const testEvent = createTestEvent({
|
|
77
|
+
deviceId,
|
|
78
|
+
testId: testCase.id,
|
|
79
|
+
titlePath,
|
|
80
|
+
status,
|
|
81
|
+
errorMessage
|
|
82
|
+
});
|
|
83
|
+
this.postToLiveTests("/events", testEvent);
|
|
84
|
+
};
|
|
85
|
+
sendRunTotals = (phase, runEnd) => {
|
|
86
|
+
const isRunEnded = runEnd !== null;
|
|
87
|
+
this.postToLiveTests("/run", {
|
|
88
|
+
phase,
|
|
89
|
+
totalTests: this.totalTests,
|
|
90
|
+
startedAt: this.startedAt,
|
|
91
|
+
endedAt: isRunEnded ? runEnd.endedAt : null,
|
|
92
|
+
okCount: this.okCount,
|
|
93
|
+
failedCount: this.failedCount,
|
|
94
|
+
outcome: isRunEnded ? runEnd.outcome : null,
|
|
95
|
+
endReason: isRunEnded ? "reported" : null
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
sendStepEvent = (testCase, testStep, stepIndex, isEnded) => {
|
|
99
|
+
const stepEvent = createStepEvent({
|
|
100
|
+
deviceId: pickDeviceId(testCase),
|
|
101
|
+
testId: testCase.id,
|
|
102
|
+
stepIndex,
|
|
103
|
+
stepTitle: testStep.title,
|
|
104
|
+
errorMessage: testStep.error?.message,
|
|
105
|
+
isEnded
|
|
106
|
+
});
|
|
107
|
+
this.postToLiveTests("/steps", stepEvent);
|
|
108
|
+
};
|
|
109
|
+
createHubTarget = () => {
|
|
110
|
+
const reportTrouble = createHubTroubleReporter();
|
|
111
|
+
const hubFile = readHubFileSafely(reportTrouble);
|
|
112
|
+
if (hubFile === null) {
|
|
113
|
+
reportTrouble(/* @__PURE__ */ new Error("There is no hub file, is the hub running?"));
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
hubUrl: createHubUrl(hubFile),
|
|
118
|
+
reportTrouble
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
copyShot = async (runId, testCase, testResult) => {
|
|
122
|
+
const attachmentPath = testResult.attachments.find(({ name }) => name === SCREENSHOT_ATTACHMENT_NAME)?.path;
|
|
123
|
+
if (attachmentPath === void 0) return null;
|
|
124
|
+
const shotPath = join(STORE_FOLDER, SHOTS_SUBFOLDER, `${runId}-${testCase.id}.png`);
|
|
125
|
+
await mkdir(join(this.rootFolder, STORE_FOLDER, SHOTS_SUBFOLDER), { recursive: true });
|
|
126
|
+
await copyFile(attachmentPath, join(this.rootFolder, shotPath));
|
|
127
|
+
return shotPath;
|
|
128
|
+
};
|
|
129
|
+
saveStep = async (testKey) => {
|
|
130
|
+
const keyTests = this.finishedTests.filter(({ titlePath }) => pickTestKey(titlePath) === testKey);
|
|
131
|
+
const runStep = createRunStep(this.savedSteps.length + 1, keyTests);
|
|
132
|
+
if (runStep === null) return;
|
|
133
|
+
this.savedSteps.push(runStep);
|
|
134
|
+
const { runId } = await this.readRun();
|
|
135
|
+
await saveRunEvent(this.rootFolder, runId, {
|
|
136
|
+
kind: "step-end",
|
|
137
|
+
step: runStep
|
|
138
|
+
});
|
|
139
|
+
this.postToLiveTests(RUN_SAVED_PATH, { runId });
|
|
140
|
+
};
|
|
141
|
+
onBegin = (config, suite) => {
|
|
142
|
+
this.hubTarget = this.createHubTarget();
|
|
143
|
+
this.deviceIds = config.projects.map(({ name }) => name);
|
|
144
|
+
const allTests = suite.allTests();
|
|
145
|
+
this.totalTests = allTests.length;
|
|
146
|
+
this.startedAt = Date.now();
|
|
147
|
+
this.sendRunTotals("start", null);
|
|
148
|
+
this.heartbeatTimer = setInterval(() => this.sendRunTotals("update", null), HEARTBEAT_INTERVAL_MS);
|
|
149
|
+
for (const testCase of allTests) {
|
|
150
|
+
const testTitlePath = pickTitlePath(testCase);
|
|
151
|
+
const testKey = pickTestKey(testTitlePath);
|
|
152
|
+
this.expectedCounts.set(testKey, (this.expectedCounts.get(testKey) ?? 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
this.runCreation = createRun(this.rootFolder, "playwright", "steps");
|
|
155
|
+
this.runCreation.catch((error) => {
|
|
156
|
+
console.error({
|
|
157
|
+
message: "Everyscreen could not create the run",
|
|
158
|
+
error
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
onTestBegin = (testCase, testResult) => {
|
|
163
|
+
this.sendTestEvent(testCase, "running", testResult);
|
|
164
|
+
};
|
|
165
|
+
onStepBegin = (testCase, testResult, testStep) => {
|
|
166
|
+
const ancestorCategories = listAncestorCategories(testStep);
|
|
167
|
+
const isUserStep = isCountedStep(testStep.category, ancestorCategories);
|
|
168
|
+
const isFixtureStep = isStepFromFolder(testStep.location?.file, OWN_FOLDER);
|
|
169
|
+
if (!isUserStep || isFixtureStep) return;
|
|
170
|
+
const stepIndex = (this.stepCounts.get(testResult) ?? 0) + 1;
|
|
171
|
+
this.stepCounts.set(testResult, stepIndex);
|
|
172
|
+
this.stepIndexes.set(testStep, stepIndex);
|
|
173
|
+
this.sendStepEvent(testCase, testStep, stepIndex, false);
|
|
174
|
+
};
|
|
175
|
+
onStepEnd = (testCase, _testResult, testStep) => {
|
|
176
|
+
const stepIndex = this.stepIndexes.get(testStep);
|
|
177
|
+
if (stepIndex === void 0) return;
|
|
178
|
+
this.stepIndexes.delete(testStep);
|
|
179
|
+
this.sendStepEvent(testCase, testStep, stepIndex, true);
|
|
180
|
+
};
|
|
181
|
+
onTestEnd = (testCase, testResult) => {
|
|
182
|
+
const liveStatus = pickLiveTestStatus(testResult.status);
|
|
183
|
+
this.sendTestEvent(testCase, liveStatus, testResult);
|
|
184
|
+
if (!isLastAttempt({
|
|
185
|
+
status: testResult.status,
|
|
186
|
+
retry: testResult.retry,
|
|
187
|
+
retries: testCase.retries
|
|
188
|
+
})) return;
|
|
189
|
+
const isTestFailed = liveStatus === "failed";
|
|
190
|
+
const isTestGood = liveStatus === "ok";
|
|
191
|
+
if (isTestFailed) this.failedCount += 1;
|
|
192
|
+
if (isTestGood) this.okCount += 1;
|
|
193
|
+
this.sendRunTotals("update", null);
|
|
194
|
+
this.runTask(async () => {
|
|
195
|
+
const { runId } = await this.readRun();
|
|
196
|
+
const shotPath = await this.copyShot(runId, testCase, testResult);
|
|
197
|
+
const { error, startTime, duration, status } = testResult;
|
|
198
|
+
const titlePath = pickTitlePath(testCase);
|
|
199
|
+
const errorLocation = error?.location;
|
|
200
|
+
const codeLocation = errorLocation !== void 0 ? `${relative(this.rootFolder, errorLocation.file)}:${errorLocation.line}:${errorLocation.column}` : null;
|
|
201
|
+
this.finishedTests.push({
|
|
202
|
+
deviceId: pickDeviceId(testCase),
|
|
203
|
+
titlePath,
|
|
204
|
+
status,
|
|
205
|
+
startedAt: startTime.getTime(),
|
|
206
|
+
durationMs: duration,
|
|
207
|
+
errorMessage: error?.message ?? null,
|
|
208
|
+
codeSnippet: error?.snippet ?? null,
|
|
209
|
+
codeLocation,
|
|
210
|
+
shotPath
|
|
211
|
+
});
|
|
212
|
+
const testKey = pickTestKey(titlePath);
|
|
213
|
+
const keyTests = this.finishedTests.filter((finishedTest) => pickTestKey(finishedTest.titlePath) === testKey);
|
|
214
|
+
const expectedCount = this.expectedCounts.get(testKey);
|
|
215
|
+
if (expectedCount === void 0) throw new Error(`Test ${testCase.title} was not counted at the start`);
|
|
216
|
+
const isStepSaved = this.savedKeys.has(testKey);
|
|
217
|
+
if (!isStepComplete(keyTests, expectedCount) || isStepSaved) return;
|
|
218
|
+
this.savedKeys.add(testKey);
|
|
219
|
+
await this.saveStep(testKey);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
onEnd = async (fullResult) => {
|
|
223
|
+
const { heartbeatTimer } = this;
|
|
224
|
+
if (heartbeatTimer !== null) clearInterval(heartbeatTimer);
|
|
225
|
+
this.heartbeatTimer = null;
|
|
226
|
+
this.sendRunTotals("update", {
|
|
227
|
+
endedAt: Date.now(),
|
|
228
|
+
outcome: pickRunOutcome(fullResult.status)
|
|
229
|
+
});
|
|
230
|
+
this.postToLiveTests("/clear", { deviceIds: this.deviceIds });
|
|
231
|
+
await waitForHubPosts(this.pendingHubPosts);
|
|
232
|
+
await Promise.all(this.pendingTasks);
|
|
233
|
+
const unsavedKeys = [...this.expectedCounts.keys()].filter((testKey) => !this.savedKeys.has(testKey));
|
|
234
|
+
for (const testKey of unsavedKeys) {
|
|
235
|
+
this.savedKeys.add(testKey);
|
|
236
|
+
await this.saveStep(testKey);
|
|
237
|
+
}
|
|
238
|
+
const { runId } = await this.readRun();
|
|
239
|
+
const runEndStatus = pickRunEndStatus(fullResult.status);
|
|
240
|
+
await saveRunEvent(this.rootFolder, runId, {
|
|
241
|
+
kind: "run-end",
|
|
242
|
+
status: runEndStatus
|
|
243
|
+
});
|
|
244
|
+
this.postToLiveTests(RUN_SAVED_PATH, { runId });
|
|
245
|
+
await waitForHubPosts(this.pendingHubPosts);
|
|
246
|
+
const replyLine = createSuiteReplyLine(this.savedSteps, runId);
|
|
247
|
+
console.log(replyLine);
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
export { EveryscreenReporter as default };
|