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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -2
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2958 -0
  5. package/dist/commands-logic-DVWfSe3O.mjs +217 -0
  6. package/dist/dashboard/assets/button-N-Nucq_K.js +1 -0
  7. package/dist/dashboard/assets/cn-DMYzmXtt.js +1 -0
  8. package/dist/dashboard/assets/device-status-logic-Silf3YXA.js +1 -0
  9. package/dist/dashboard/assets/devices-5pEUg6rA.js +1 -0
  10. package/dist/dashboard/assets/dist-CnPFbxes.js +1 -0
  11. package/dist/dashboard/assets/focus._deviceId-DGel-kWw.js +1 -0
  12. package/dist/dashboard/assets/frame-layout-logic-H_x71NvW.js +1 -0
  13. package/dist/dashboard/assets/index-DNHZ-i1B.css +2 -0
  14. package/dist/dashboard/assets/index-DwPQ0eY_.js +10 -0
  15. package/dist/dashboard/assets/modal-dialog-BaO9xkRr.js +1 -0
  16. package/dist/dashboard/assets/rolldown-runtime-WkwZ5CU-.js +1 -0
  17. package/dist/dashboard/assets/routes-CUFa9tL0.js +1 -0
  18. package/dist/dashboard/assets/run-origin-badge-BeFqg0AC.js +1 -0
  19. package/dist/dashboard/assets/runs-queries-BYBE7R9S.js +2 -0
  20. package/dist/dashboard/assets/runs._runId-CokpJ1Rr.js +1 -0
  21. package/dist/dashboard/assets/runs.index-ZsynzkKU.js +1 -0
  22. package/dist/dashboard/assets/schemas-BPFqmBjG.js +55 -0
  23. package/dist/dashboard/assets/screen-boundary-BauyD9pu.js +1 -0
  24. package/dist/dashboard/assets/status-chip-yqTTTuyZ.js +1 -0
  25. package/dist/dashboard/assets/tile-status-chip-DisgbNnH.js +1 -0
  26. package/dist/dashboard/assets/toggle-nciug_di.js +1 -0
  27. package/dist/dashboard/assets/use-run-totals-B3G4yamQ.js +1 -0
  28. package/dist/dashboard/assets/useNavigate-8mkM-ffY.js +1 -0
  29. package/dist/dashboard/assets/useParams-D2lK5BEg.js +1 -0
  30. package/dist/dashboard/index.html +29 -0
  31. package/dist/devices-store-edScJDie.mjs +1228 -0
  32. package/dist/playwright-logic-QFlBFspE.d.mts +138 -0
  33. package/dist/playwright.d.mts +2 -0
  34. package/dist/playwright.mjs +18 -0
  35. package/dist/read-hub-file-N4rZ-6Wr.mjs +180 -0
  36. package/dist/report-hub-trouble-BjbcuK21.d.mts +2 -0
  37. package/dist/report-hub-trouble-CaUiyteI.mjs +41 -0
  38. package/dist/reporter.d.mts +47 -0
  39. package/dist/reporter.mjs +250 -0
  40. package/dist/run-one-at-a-time-BeV_a3Fm.mjs +48 -0
  41. package/dist/runs-store-Bd_ITPFj.mjs +283 -0
  42. package/dist/test.d.mts +11 -0
  43. package/dist/test.mjs +69 -0
  44. package/package.json +106 -4
  45. package/src/dashboard/ui/theme.css +28 -0
  46. 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,2 @@
1
+ import { t as DeviceProject } from "./playwright-logic-QFlBFspE.mjs";
2
+ export declare const listDevices: (deviceIds?: string[] | null) => DeviceProject[];
@@ -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,2 @@
1
+ type ReportHubTrouble = (error: unknown) => void;
2
+ export { ReportHubTrouble as t };
@@ -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 };