supercov 0.0.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.
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import type {
4
+ Reporter,
5
+ TestCase,
6
+ TestResult,
7
+ } from "@playwright/test/reporter";
8
+ import { inferTestProvenance } from "./provenance.ts";
9
+ import type { McdcRawTestResult } from "./types.ts";
10
+
11
+ const GENERATED_EVIDENCE_DIRECTORY =
12
+ "__SUPERCOV_EVIDENCE_DIRECTORY__";
13
+
14
+ /** Records outcomes even when browser or fixture startup fails before coverage. */
15
+ export default class SupercovPlaywrightReporter implements Reporter {
16
+ onTestEnd(test: TestCase, result: TestResult): void {
17
+ const evidenceDirectory =
18
+ process.env["SUPERCOV_EVIDENCE_DIR"] ??
19
+ (GENERATED_EVIDENCE_DIRECTORY.startsWith("__")
20
+ ? undefined
21
+ : GENERATED_EVIDENCE_DIRECTORY);
22
+ if (!evidenceDirectory) return;
23
+ const testFile = relative(process.cwd(), test.location.file)
24
+ .split(sep)
25
+ .join("/");
26
+ const payload: McdcRawTestResult = {
27
+ testId: test.id,
28
+ test: test.titlePath().filter(Boolean).join(" > "),
29
+ testFile,
30
+ title: test.title,
31
+ retry: result.retry,
32
+ status: result.status ?? "unknown",
33
+ expectedStatus: test.expectedStatus,
34
+ provenance: inferTestProvenance({
35
+ runner: "playwright",
36
+ file: testFile,
37
+ project: test.parent.project()?.name,
38
+ explicitKind: process.env["SUPERCOV_TEST_KIND"],
39
+ }),
40
+ browser: [],
41
+ server: [],
42
+ };
43
+ const safeId = test.id.replace(/[^a-zA-Z0-9_-]/g, "_");
44
+ const directory = resolve(
45
+ process.cwd(),
46
+ evidenceDirectory,
47
+ `playwright-${safeId}-${result.retry}-status`,
48
+ );
49
+ mkdirSync(directory, { recursive: true });
50
+ writeFileSync(
51
+ resolve(directory, "mcdc.json"),
52
+ `${JSON.stringify(payload)}\n`,
53
+ );
54
+ }
55
+ }
package/src/project.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export interface CoverageProject {
5
+ root: string;
6
+ sourceRoots: string[];
7
+ playwrightConfig?: string;
8
+ vitestConfig?: string;
9
+ playwrightModule: string;
10
+ essentialOffline: boolean;
11
+ buildCommand: string[];
12
+ }
13
+
14
+ const PLAYWRIGHT_CONFIG_CANDIDATES = [
15
+ "playwright.config.ts",
16
+ "playwright.config.mts",
17
+ "playwright.config.js",
18
+ "playwright.config.mjs",
19
+ "playwright.config.cts",
20
+ "playwright.config.cjs",
21
+ "tests/offline/playwright.offline.config.ts",
22
+ ];
23
+
24
+ const VITEST_CONFIG_CANDIDATES = [
25
+ "vitest.config.ts",
26
+ "vitest.config.mts",
27
+ "vitest.config.js",
28
+ "vitest.config.mjs",
29
+ "vitest.config.cts",
30
+ "vitest.config.cjs",
31
+ "vite.config.ts",
32
+ "vite.config.mts",
33
+ "vite.config.js",
34
+ "vite.config.mjs",
35
+ ];
36
+
37
+ function packageJson(root: string): {
38
+ scripts?: Record<string, string>;
39
+ dependencies?: Record<string, string>;
40
+ devDependencies?: Record<string, string>;
41
+ optionalDependencies?: Record<string, string>;
42
+ } {
43
+ try {
44
+ return JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
45
+ } catch {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ function testUsesModule(directory: string, moduleName: string): boolean {
51
+ if (!existsSync(directory)) return false;
52
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
53
+ if (
54
+ entry.name === "node_modules" ||
55
+ entry.name === "results" ||
56
+ entry.name.startsWith(".")
57
+ )
58
+ continue;
59
+ const path = resolve(directory, entry.name);
60
+ if (entry.isDirectory()) {
61
+ if (testUsesModule(path, moduleName)) return true;
62
+ continue;
63
+ }
64
+ if (!/\.[cm]?[jt]sx?$/.test(entry.name)) continue;
65
+ try {
66
+ if (readFileSync(path, "utf8").includes(moduleName)) return true;
67
+ } catch {
68
+ // Discovery is best-effort; unreadable files are not test adapters.
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+
74
+ export function discoverCoverageProject(
75
+ root = process.cwd(),
76
+ environment: NodeJS.ProcessEnv = process.env,
77
+ ): CoverageProject {
78
+ const manifest = packageJson(root);
79
+ const configuredSourceRoots = environment["SUPERCOV_SOURCE_ROOTS"]
80
+ ?.split(",")
81
+ .map((value) => value.trim())
82
+ .filter(Boolean);
83
+ const sourceRoots = (
84
+ configuredSourceRoots?.length ? configuredSourceRoots : ["app", "src"]
85
+ ).filter((directory) => existsSync(resolve(root, directory)));
86
+ if (sourceRoots.length === 0) {
87
+ throw new Error(
88
+ "No application source root found. Set SUPERCOV_SOURCE_ROOTS=src,app.",
89
+ );
90
+ }
91
+
92
+ const configuredPlaywright =
93
+ environment["SUPERCOV_PLAYWRIGHT_CONFIG"] ??
94
+ environment["TEST_PLAYWRIGHT_CONFIG"];
95
+ const playwrightConfig = configuredPlaywright
96
+ ? resolve(root, configuredPlaywright)
97
+ : PLAYWRIGHT_CONFIG_CANDIDATES.map((candidate) =>
98
+ resolve(root, candidate),
99
+ ).find((candidate) => existsSync(candidate));
100
+ const configuredVitest = environment["SUPERCOV_VITEST_CONFIG"];
101
+ const vitestConfig = configuredVitest
102
+ ? resolve(root, configuredVitest)
103
+ : VITEST_CONFIG_CANDIDATES.map((candidate) =>
104
+ resolve(root, candidate),
105
+ ).find((candidate) => existsSync(candidate));
106
+
107
+ const explicitModule = environment["SUPERCOV_PLAYWRIGHT_MODULE"];
108
+ const essentialModule = "@essential-apps/shopify-test-admin";
109
+ const playwrightModule =
110
+ explicitModule ??
111
+ (testUsesModule(resolve(root, "tests"), essentialModule)
112
+ ? essentialModule
113
+ : "@playwright/test");
114
+ const essentialOffline = playwrightModule === essentialModule;
115
+
116
+ if (!manifest.scripts?.["build"]) {
117
+ throw new Error(
118
+ "No package.json build script found; a build adapter is required to instrument application source.",
119
+ );
120
+ }
121
+ const dependencies = {
122
+ ...manifest.dependencies,
123
+ ...manifest.devDependencies,
124
+ ...manifest.optionalDependencies,
125
+ };
126
+ const hasVite =
127
+ Boolean(dependencies["vite"]) ||
128
+ [
129
+ "vite.config.ts",
130
+ "vite.config.mts",
131
+ "vite.config.js",
132
+ "vite.config.mjs",
133
+ ].some((candidate) => existsSync(resolve(root, candidate)));
134
+ if (!hasVite) {
135
+ throw new Error(
136
+ "The project is not a detected Vite build. A framework build adapter is required.",
137
+ );
138
+ }
139
+
140
+ return {
141
+ root,
142
+ sourceRoots,
143
+ ...(playwrightConfig ? { playwrightConfig } : {}),
144
+ ...(vitestConfig ? { vitestConfig } : {}),
145
+ playwrightModule,
146
+ essentialOffline,
147
+ buildCommand: ["npm", "run", "build"],
148
+ };
149
+ }
@@ -0,0 +1,69 @@
1
+ import type { TestProvenance } from "./types.ts";
2
+
3
+ export interface InferTestProvenanceOptions {
4
+ runner: string;
5
+ file?: string;
6
+ project?: string;
7
+ explicitKind?: string;
8
+ }
9
+
10
+ const NORMALIZED_KINDS = [
11
+ ["unit", /(^|[/_.-])unit([/_.-]|$)/i],
12
+ ["component", /(^|[/_.-])(component|components|ct)([/_.-]|$)/i],
13
+ ["integration", /(^|[/_.-])(integration|int)([/_.-]|$)/i],
14
+ ["e2e", /(^|[/_.-])(e2e|end-to-end|offline|online)([/_.-]|$)/i],
15
+ ] as const;
16
+
17
+ function classifiedKind(value?: string): string | undefined {
18
+ if (!value) return undefined;
19
+ return NORMALIZED_KINDS.find(([, pattern]) => pattern.test(value))?.[0];
20
+ }
21
+
22
+ export function inferTestProvenance({
23
+ runner,
24
+ file,
25
+ project,
26
+ explicitKind,
27
+ }: InferTestProvenanceOptions): TestProvenance {
28
+ if (explicitKind?.trim()) {
29
+ return {
30
+ runner,
31
+ kind: explicitKind.trim().toLowerCase(),
32
+ ...(project ? { project } : {}),
33
+ source: "explicit",
34
+ };
35
+ }
36
+
37
+ const projectKind = classifiedKind(project);
38
+ if (projectKind) {
39
+ return {
40
+ runner,
41
+ kind: projectKind,
42
+ ...(project ? { project } : {}),
43
+ source: "project",
44
+ };
45
+ }
46
+
47
+ const pathKind = classifiedKind(file);
48
+ if (pathKind) {
49
+ return {
50
+ runner,
51
+ kind: pathKind,
52
+ ...(project ? { project } : {}),
53
+ source: "path",
54
+ };
55
+ }
56
+
57
+ const defaultKind =
58
+ runner === "playwright"
59
+ ? "e2e"
60
+ : runner === "vitest" || runner === "jest" || runner === "node:test"
61
+ ? "unit"
62
+ : "unknown";
63
+ return {
64
+ runner,
65
+ kind: defaultKind,
66
+ ...(project ? { project } : {}),
67
+ source: defaultKind === "unknown" ? "unknown" : "runner-default",
68
+ };
69
+ }