@red-hat-developer-hub/e2e-test-utils 2.1.6 → 2.1.8

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.
@@ -242,7 +242,7 @@ export class RHDHDeployment {
242
242
  }
243
243
  async rolloutRestart() {
244
244
  this._log(`Restarting RHDH deployment in namespace ${this.deploymentConfig.namespace}...`);
245
- await $ `oc rollout restart deployment -l '${this._labelSelector}' -n ${this.deploymentConfig.namespace}`;
245
+ await $ `oc rollout restart deployment -l ${this._labelSelector} -n ${this.deploymentConfig.namespace}`;
246
246
  this._log(`RHDH deployment restarted successfully in namespace ${this.deploymentConfig.namespace}`);
247
247
  await this.waitUntilReady();
248
248
  }
@@ -253,9 +253,9 @@ export class RHDHDeployment {
253
253
  */
254
254
  async scaleDownAndRestart() {
255
255
  const namespace = this.deploymentConfig.namespace;
256
- await $ `oc scale deployment -l '${this._labelSelector}' --replicas=0 -n ${namespace}`;
257
- await $ `oc wait --for=delete pod -l '${this._labelSelector}' -n ${namespace} --timeout=120s || true`;
258
- await $ `oc scale deployment -l '${this._labelSelector}' --replicas=1 -n ${namespace}`;
256
+ await $ `oc scale deployment -l ${this._labelSelector} --replicas=0 -n ${namespace}`;
257
+ await $ `oc wait --for=delete pod -l ${this._labelSelector} -n ${namespace} --timeout=120s || true`;
258
+ await $ `oc scale deployment -l ${this._labelSelector} --replicas=1 -n ${namespace}`;
259
259
  }
260
260
  async waitUntilReady(timeout = 500) {
261
261
  const namespace = this.deploymentConfig.namespace;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The whole `__coverage__` object, keyed by each instrumented file's path.
3
+ *
4
+ * Named for what it holds: istanbul's own `CoverageData` is the entry for a
5
+ * single file, so calling this that would read as one file's counters.
6
+ *
7
+ * The values stay `unknown` on purpose — this module serializes them and never
8
+ * reads inside, so modelling istanbul's report shape would buy nothing and pull
9
+ * a dependency on its types into a package that has none.
10
+ */
11
+ export type CoverageMap = Record<string, unknown>;
12
+ export type PageLike = {
13
+ isClosed(): boolean;
14
+ evaluate(pageFunction: () => CoverageMap | undefined): Promise<CoverageMap | undefined>;
15
+ };
16
+ export type BrowserLike = {
17
+ contexts(): readonly {
18
+ pages(): readonly PageLike[];
19
+ }[];
20
+ };
21
+ /** One page's coverage, tagged so repeated reads of it can overwrite. */
22
+ export type CoverageEntry = {
23
+ pageId: string;
24
+ coverage: CoverageMap;
25
+ };
26
+ /**
27
+ * Every non-empty coverage map reachable in the browser, one entry per page.
28
+ *
29
+ * Only the plugin under test is instrumented, so `__coverage__` appears solely
30
+ * in pages that loaded its bundle. Which page that is depends on how the spec
31
+ * is written — some drive Playwright's `page` fixture, others open their own
32
+ * context in `beforeAll` and run every test there — so this walks every open
33
+ * context rather than assuming one.
34
+ *
35
+ * A page carrying an empty object is dropped: it is as useless as a page with
36
+ * none, and reporting it as a find would mask the "nothing was instrumented"
37
+ * case the caller reports on.
38
+ *
39
+ * Failures are handled at two levels on purpose. A single unusable page is
40
+ * skipped, because the other pages' coverage is still worth having. A failure
41
+ * reaching the browser propagates: returning an empty array would be
42
+ * indistinguishable from "nothing was instrumented", and the caller can only
43
+ * report accurately what it is allowed to see.
44
+ */
45
+ export declare function collectCoverageFromBrowser(browser: BrowserLike): Promise<CoverageEntry[]>;
46
+ /** Every reason collection can legitimately come back with nothing. */
47
+ export declare const NO_COVERAGE_FOUND_MESSAGE: string;
48
+ /**
49
+ * Collect and persist one JSON per covered page, reporting instead of throwing.
50
+ *
51
+ * Called from fixture teardown for every test, so it must never raise: an
52
+ * unwritable output directory or a browser that died mid-test would otherwise
53
+ * fail a test that had already passed. Coverage is diagnostics, not a verdict.
54
+ * `report` is how a problem stays visible without becoming one — silence is the
55
+ * failure mode this module exists to remove.
56
+ *
57
+ * Files are named per page, not per test, and deliberately overwritten. A
58
+ * context opened in `beforeAll` outlives the whole file, and `__coverage__`
59
+ * accumulates in it, so naming per test would write a growing copy of the same
60
+ * map once per test — 30 near-duplicates of a multi-megabyte report, all summed
61
+ * again by `nyc merge`. Overwriting keeps the last and most complete read, and
62
+ * costs nothing for a per-test fixture page, which is a new page with a new id
63
+ * every time. It also collapses the retry case, where a failed and a passing
64
+ * attempt would otherwise both be counted.
65
+ */
66
+ export declare function collectAndWriteCoverage(browser: BrowserLike, target: {
67
+ dir: string;
68
+ runId: string;
69
+ }, report: (detail: string) => void): Promise<void>;
70
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/playwright/coverage.ts"],"names":[],"mappings":"AASA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,IAAI,OAAO,CAAC;IAIpB,QAAQ,CACN,YAAY,EAAE,MAAM,WAAW,GAAG,SAAS,GAC1C,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,IAAI,SAAS;QAAE,KAAK,IAAI,SAAS,QAAQ,EAAE,CAAA;KAAE,EAAE,CAAC;CACzD,CAAC;AAEF,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC;AAmCtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,EAAE,CAAC,CAmB1B;AAED,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,QAGqC,CAAC;AAE5E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACtC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAC/B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -0,0 +1,108 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Stable per-page id, so a page read across many tests keeps one identity.
5
+ *
6
+ * Weakly held: pages are closed and discarded constantly, and keeping them
7
+ * alive to remember a number would be a leak that grows with the suite.
8
+ */
9
+ const pageIds = new WeakMap();
10
+ let nextPageId = 0;
11
+ function idFor(page) {
12
+ const existing = pageIds.get(page);
13
+ if (existing !== undefined)
14
+ return existing;
15
+ const id = `page${nextPageId++}`;
16
+ pageIds.set(page, id);
17
+ return id;
18
+ }
19
+ /**
20
+ * Runs in the browser, so it must not close over anything.
21
+ *
22
+ * The assertion stands in for a type that cannot exist statically: nyc defines
23
+ * `__coverage__` at runtime, in the instrumented bundle. Declaring it as a
24
+ * global would be the alternative, but this package is published, and its
25
+ * ambient declarations would land in every consumer's type environment.
26
+ */
27
+ function readCoverageGlobal() {
28
+ return globalThis.__coverage__;
29
+ }
30
+ /**
31
+ * Every non-empty coverage map reachable in the browser, one entry per page.
32
+ *
33
+ * Only the plugin under test is instrumented, so `__coverage__` appears solely
34
+ * in pages that loaded its bundle. Which page that is depends on how the spec
35
+ * is written — some drive Playwright's `page` fixture, others open their own
36
+ * context in `beforeAll` and run every test there — so this walks every open
37
+ * context rather than assuming one.
38
+ *
39
+ * A page carrying an empty object is dropped: it is as useless as a page with
40
+ * none, and reporting it as a find would mask the "nothing was instrumented"
41
+ * case the caller reports on.
42
+ *
43
+ * Failures are handled at two levels on purpose. A single unusable page is
44
+ * skipped, because the other pages' coverage is still worth having. A failure
45
+ * reaching the browser propagates: returning an empty array would be
46
+ * indistinguishable from "nothing was instrumented", and the caller can only
47
+ * report accurately what it is allowed to see.
48
+ */
49
+ export async function collectCoverageFromBrowser(browser) {
50
+ const collected = [];
51
+ for (const context of browser.contexts()) {
52
+ for (const page of context.pages()) {
53
+ if (page.isClosed())
54
+ continue;
55
+ let coverage;
56
+ try {
57
+ coverage = await page.evaluate(readCoverageGlobal);
58
+ }
59
+ catch {
60
+ // A page can crash, navigate, or close between being listed and being
61
+ // evaluated. One unusable page must not discard the others' coverage.
62
+ continue;
63
+ }
64
+ if (coverage && Object.keys(coverage).length > 0) {
65
+ collected.push({ pageId: idFor(page), coverage });
66
+ }
67
+ }
68
+ }
69
+ return collected;
70
+ }
71
+ /** Every reason collection can legitimately come back with nothing. */
72
+ export const NO_COVERAGE_FOUND_MESSAGE = "E2E_COLLECT_COVERAGE=true but no open page exposed __coverage__. The " +
73
+ "deployed bundle may not be instrumented, the spec may have closed its " +
74
+ "pages before teardown, or evaluation may have been blocked in the page.";
75
+ /**
76
+ * Collect and persist one JSON per covered page, reporting instead of throwing.
77
+ *
78
+ * Called from fixture teardown for every test, so it must never raise: an
79
+ * unwritable output directory or a browser that died mid-test would otherwise
80
+ * fail a test that had already passed. Coverage is diagnostics, not a verdict.
81
+ * `report` is how a problem stays visible without becoming one — silence is the
82
+ * failure mode this module exists to remove.
83
+ *
84
+ * Files are named per page, not per test, and deliberately overwritten. A
85
+ * context opened in `beforeAll` outlives the whole file, and `__coverage__`
86
+ * accumulates in it, so naming per test would write a growing copy of the same
87
+ * map once per test — 30 near-duplicates of a multi-megabyte report, all summed
88
+ * again by `nyc merge`. Overwriting keeps the last and most complete read, and
89
+ * costs nothing for a per-test fixture page, which is a new page with a new id
90
+ * every time. It also collapses the retry case, where a failed and a passing
91
+ * attempt would otherwise both be counted.
92
+ */
93
+ export async function collectAndWriteCoverage(browser, target, report) {
94
+ try {
95
+ const collected = await collectCoverageFromBrowser(browser);
96
+ if (collected.length === 0) {
97
+ report(NO_COVERAGE_FOUND_MESSAGE);
98
+ return;
99
+ }
100
+ fs.mkdirSync(target.dir, { recursive: true });
101
+ for (const { pageId, coverage } of collected) {
102
+ fs.writeFileSync(path.join(target.dir, `${target.runId}-${pageId}.json`), JSON.stringify(coverage));
103
+ }
104
+ }
105
+ catch (error) {
106
+ report(`collection failed: ${error}`);
107
+ }
108
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=coverage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.test.d.ts","sourceRoot":"","sources":["../../src/playwright/coverage.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,240 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import fs from "fs-extra";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { collectAndWriteCoverage, collectCoverageFromBrowser, NO_COVERAGE_FOUND_MESSAGE, } from "./coverage.js";
7
+ /** Reports read back off disk arrive in directory order, so sort by content. */
8
+ function byFirstKey(a, b) {
9
+ return Object.keys(a)[0].localeCompare(Object.keys(b)[0]);
10
+ }
11
+ /**
12
+ * A page whose `__coverage__` is whatever `coverage` says.
13
+ *
14
+ * `evaluate` receives the real callback and would normally run it in the
15
+ * browser against that page's `globalThis`. There is no browser here, so the
16
+ * fake answers for it — which is the whole point: the behaviour under test is
17
+ * which pages get asked and what happens to the answers, not how Playwright
18
+ * ships a function across the wire.
19
+ */
20
+ function fakePage(coverage, { closed = false } = {}) {
21
+ return {
22
+ isClosed: () => closed,
23
+ evaluate: async () => coverage,
24
+ };
25
+ }
26
+ function unusablePage(reason) {
27
+ return {
28
+ isClosed: () => false,
29
+ evaluate: async () => {
30
+ throw new Error(reason);
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * The one fake that really runs the callback, against a `globalThis` carrying
36
+ * what an instrumented bundle would have left behind.
37
+ *
38
+ * Every other fake answers in place of `evaluate`, which leaves the function
39
+ * that actually names `__coverage__` unexercised — rename it and the suite
40
+ * stays green while collection silently returns nothing, one level below the
41
+ * regression this module exists to end.
42
+ */
43
+ function evaluatingPage(globals) {
44
+ return {
45
+ isClosed: () => false,
46
+ evaluate: async (pageFunction) => {
47
+ const target = globalThis;
48
+ const restore = Object.keys(globals).map((key) => [key, key in target, target[key]]);
49
+ Object.assign(target, globals);
50
+ try {
51
+ return pageFunction();
52
+ }
53
+ finally {
54
+ for (const [key, existed, previous] of restore) {
55
+ if (existed)
56
+ target[key] = previous;
57
+ else
58
+ delete target[key];
59
+ }
60
+ }
61
+ },
62
+ };
63
+ }
64
+ function fakeBrowser(...contexts) {
65
+ return { contexts: () => contexts.map((pages) => ({ pages: () => pages })) };
66
+ }
67
+ /** Istanbul keys its report by the instrumented file's absolute path. */
68
+ function coverageFor(file) {
69
+ return { [file]: { path: file } };
70
+ }
71
+ const COVERAGE = coverageFor("/src/plugin.tsx");
72
+ const OTHER_COVERAGE = coverageFor("/src/widget.tsx");
73
+ /** The coverage maps alone; page identity is asserted on its own below. */
74
+ async function mapsFrom(browser) {
75
+ const collected = await collectCoverageFromBrowser(browser);
76
+ return collected.map((entry) => entry.coverage);
77
+ }
78
+ describe("collectCoverageFromBrowser", () => {
79
+ it("collects from a context the spec opened itself", async () => {
80
+ // The regression this exists for. Specs that run every test in their own
81
+ // `browser.newContext()` reported zero coverage for months while passing,
82
+ // because collection only ever looked at the fixture page.
83
+ const fixturePage = fakePage(undefined);
84
+ const specOwnedPage = fakePage(COVERAGE);
85
+ const collected = await mapsFrom(fakeBrowser([fixturePage], [specOwnedPage]));
86
+ assert.deepStrictEqual(collected, [COVERAGE]);
87
+ });
88
+ it("returns one entry per covered context so nyc merge sums them", async () => {
89
+ const collected = await mapsFrom(fakeBrowser([fakePage(COVERAGE)], [fakePage(OTHER_COVERAGE)]));
90
+ assert.deepStrictEqual(collected, [COVERAGE, OTHER_COVERAGE], "entries stay separate; merging is nyc's job, not this function's");
91
+ });
92
+ it("collects from every page in a context, not just the first", async () => {
93
+ // One `newContext()` can hold several pages — a popup, a second tab. Taking
94
+ // `pages()[0]` would satisfy every other case in this suite.
95
+ const collected = await mapsFrom(fakeBrowser([
96
+ fakePage(undefined),
97
+ fakePage(COVERAGE),
98
+ fakePage(OTHER_COVERAGE),
99
+ ]));
100
+ assert.deepStrictEqual(collected, [COVERAGE, OTHER_COVERAGE], "every page of a context is probed, in page order");
101
+ });
102
+ it("moves past a context holding no pages", async () => {
103
+ const collected = await mapsFrom(fakeBrowser([], [fakePage(COVERAGE)]));
104
+ assert.deepStrictEqual(collected, [COVERAGE]);
105
+ });
106
+ it("reads __coverage__ off the page's own globalThis", async () => {
107
+ const collected = await mapsFrom(
108
+ // eslint-disable-next-line @typescript-eslint/naming-convention
109
+ fakeBrowser([evaluatingPage({ __coverage__: COVERAGE })]));
110
+ assert.deepStrictEqual(collected, [COVERAGE], "the global nyc writes is the global this reads");
111
+ });
112
+ it("finds nothing in a page that loaded no instrumented bundle", async () => {
113
+ const collected = await mapsFrom(fakeBrowser([evaluatingPage({})]));
114
+ assert.deepStrictEqual(collected, []);
115
+ });
116
+ it("keeps other pages' coverage when one page cannot be evaluated", async () => {
117
+ const collected = await mapsFrom(fakeBrowser([unusablePage("Target page, context or browser has been closed")], [fakePage(COVERAGE)]));
118
+ assert.deepStrictEqual(collected, [COVERAGE], "a single crashed page used to discard the whole test's coverage");
119
+ });
120
+ it("skips closed pages without evaluating them", async () => {
121
+ let evaluated = false;
122
+ const closedPage = {
123
+ isClosed: () => true,
124
+ evaluate: async () => {
125
+ evaluated = true;
126
+ return COVERAGE;
127
+ },
128
+ };
129
+ const collected = await mapsFrom(fakeBrowser([closedPage]));
130
+ assert.deepStrictEqual(collected, []);
131
+ assert.strictEqual(evaluated, false, "a closed page must not be probed");
132
+ });
133
+ it("ignores a page with no coverage", async () => {
134
+ const collected = await mapsFrom(fakeBrowser([fakePage(undefined)]));
135
+ assert.deepStrictEqual(collected, []);
136
+ });
137
+ it("ignores an empty coverage object", async () => {
138
+ // An instrumented bundle that never executed leaves `{}` behind. Treating
139
+ // it as a find would write an empty JSON and suppress the warning that is
140
+ // supposed to flag exactly this.
141
+ const collected = await mapsFrom(fakeBrowser([fakePage({})]));
142
+ assert.deepStrictEqual(collected, []);
143
+ });
144
+ it("returns nothing when no context is open", async () => {
145
+ assert.deepStrictEqual(await mapsFrom(fakeBrowser()), []);
146
+ });
147
+ it("propagates a browser-level failure instead of reporting no coverage", async () => {
148
+ // The caller turns this into a diagnostic that names the real cause.
149
+ // Swallowing it here would return an empty array indistinguishable from
150
+ // "nothing was instrumented" — the confusion this module exists to end.
151
+ const deadBrowser = {
152
+ contexts: () => {
153
+ throw new Error("Browser has been closed");
154
+ },
155
+ };
156
+ await assert.rejects(() => collectCoverageFromBrowser(deadBrowser), /Browser has been closed/);
157
+ });
158
+ it("gives one page the same id across collections", async () => {
159
+ // What makes the per-page filename overwrite instead of accumulate: a
160
+ // context opened in `beforeAll` is read again on every test, and each read
161
+ // returns the same growing map.
162
+ const longLived = fakePage(COVERAGE);
163
+ const browser = fakeBrowser([longLived]);
164
+ const first = await collectCoverageFromBrowser(browser);
165
+ const second = await collectCoverageFromBrowser(browser);
166
+ assert.strictEqual(first[0].pageId, second[0].pageId);
167
+ });
168
+ it("gives distinct pages distinct ids", async () => {
169
+ const collected = await collectCoverageFromBrowser(fakeBrowser([fakePage(COVERAGE)], [fakePage(OTHER_COVERAGE)]));
170
+ assert.notStrictEqual(collected[0].pageId, collected[1].pageId, "sharing an id would make one page's file overwrite another's");
171
+ });
172
+ });
173
+ describe("collectAndWriteCoverage", () => {
174
+ async function inTempDir(run) {
175
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "coverage-test-"));
176
+ try {
177
+ await run(dir);
178
+ }
179
+ finally {
180
+ await fs.remove(dir);
181
+ }
182
+ }
183
+ it("writes one file per page, both readable back", async () => {
184
+ await inTempDir(async (dir) => {
185
+ const reported = [];
186
+ await collectAndWriteCoverage(fakeBrowser([fakePage(COVERAGE)], [fakePage(OTHER_COVERAGE)]), { dir, runId: "w0" }, (detail) => reported.push(detail));
187
+ const files = (await fs.readdir(dir)).sort();
188
+ assert.strictEqual(files.length, 2, "two pages must not collide");
189
+ const written = await Promise.all(files.map((file) => fs.readJson(path.join(dir, file))));
190
+ assert.deepStrictEqual(written.sort(byFirstKey), [
191
+ COVERAGE,
192
+ OTHER_COVERAGE,
193
+ ]);
194
+ assert.deepStrictEqual(reported, [], "a clean run reports nothing");
195
+ });
196
+ });
197
+ it("overwrites rather than accumulating when a page is read again", async () => {
198
+ // The long-lived-context case: 34 tests must not leave 34 copies of the
199
+ // same growing map for `nyc merge` to sum.
200
+ await inTempDir(async (dir) => {
201
+ const browser = fakeBrowser([fakePage(COVERAGE)]);
202
+ await collectAndWriteCoverage(browser, { dir, runId: "w0" }, () => { });
203
+ await collectAndWriteCoverage(browser, { dir, runId: "w0" }, () => { });
204
+ assert.strictEqual((await fs.readdir(dir)).length, 1);
205
+ });
206
+ });
207
+ it("reports instead of throwing when the browser is gone", async () => {
208
+ await inTempDir(async (dir) => {
209
+ const reported = [];
210
+ const deadBrowser = {
211
+ contexts: () => {
212
+ throw new Error("Browser has been closed");
213
+ },
214
+ };
215
+ await collectAndWriteCoverage(deadBrowser, { dir, runId: "w0" }, (detail) => reported.push(detail));
216
+ assert.strictEqual(reported.length, 1);
217
+ assert.match(reported[0], /Browser has been closed/, "the report must name the real cause, not just 'no coverage'");
218
+ });
219
+ });
220
+ it("reports instead of throwing when the directory cannot be created", async () => {
221
+ // Teardown runs for every test, so an unwritable outputDir must not be able
222
+ // to fail a test that already passed.
223
+ await inTempDir(async (dir) => {
224
+ const blocked = path.join(dir, "a-file-not-a-dir");
225
+ await fs.writeFile(blocked, "");
226
+ const reported = [];
227
+ await collectAndWriteCoverage(fakeBrowser([fakePage(COVERAGE)]), { dir: blocked, runId: "w0" }, (detail) => reported.push(detail));
228
+ assert.strictEqual(reported.length, 1);
229
+ assert.match(reported[0], /collection failed/);
230
+ });
231
+ });
232
+ it("reports the no-coverage diagnosis when nothing is instrumented", async () => {
233
+ await inTempDir(async (dir) => {
234
+ const reported = [];
235
+ await collectAndWriteCoverage(fakeBrowser([fakePage(undefined)]), { dir, runId: "w0" }, (detail) => reported.push(detail));
236
+ assert.deepStrictEqual(reported, [NO_COVERAGE_FOUND_MESSAGE]);
237
+ assert.strictEqual(await fs.pathExists(dir), true, "reporting must not depend on having written anything");
238
+ });
239
+ });
240
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/playwright/fixtures/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAMzC,KAAK,0BAA0B,GAAG;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,IAAI,CAAC;IAEtB,kBAAkB,EAAE,IAAI,CAAC;CAC1B,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,oBAAoB,EAAE,cAAc,CAAC;CACtC,CAAC;AA4FF,eAAO,MAAM,IAAI;;CAEf,CAAC;AAEH,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/playwright/fixtures/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAuBzC,KAAK,0BAA0B,GAAG;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,IAAI,CAAC;IAEtB,kBAAkB,EAAE,IAAI,CAAC;CAC1B,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,oBAAoB,EAAE,cAAc,CAAC;CACtC,CAAC;AA4FF,eAAO,MAAM,IAAI;;CAEf,CAAC;AAEH,cAAc,kBAAkB,CAAC"}
@@ -4,8 +4,25 @@ import { LoginHelper, UIhelper } from "../helpers/index.js";
4
4
  import { runOnce } from "../run-once.js";
5
5
  import { $ } from "../../utils/bash.js";
6
6
  import { WorkspacePaths } from "../../utils/workspace-paths.js";
7
- import fs from "node:fs";
7
+ import { collectAndWriteCoverage } from "../coverage.js";
8
+ import { isCoverageEnabled } from "../../utils/common.js";
8
9
  import path from "path";
10
+ // Asking for coverage and getting none used to look exactly like success: the
11
+ // collector returned quietly and the run stayed green while every JSON was
12
+ // lost. Reporting it is deliberately two-channel — an annotation lands in the
13
+ // HTML report beside the result, the way `autoAnnotations` below does, and a
14
+ // console line keeps it in the CI log, where a green run is still read.
15
+ //
16
+ // Deduplicated by message rather than by a single flag: one recurring
17
+ // "no coverage found" must not be what silences the first real exception.
18
+ const loggedCoverageProblems = new Set();
19
+ function reportCoverageProblem(testInfo, detail) {
20
+ testInfo.annotations.push({ type: "coverage", description: detail });
21
+ if (loggedCoverageProblems.has(detail))
22
+ return;
23
+ loggedCoverageProblems.add(detail);
24
+ console.warn(`[coverage] ${detail}`);
25
+ }
9
26
  const baseTest = base.extend({
10
27
  rhdhDeploymentWorker: [
11
28
  // eslint-disable-next-line no-empty-pattern
@@ -59,21 +76,25 @@ const baseTest = base.extend({
59
76
  ],
60
77
  // eslint-disable-next-line @typescript-eslint/naming-convention
61
78
  _coverageCollector: [
62
- async ({ page }, use, testInfo) => {
79
+ // Reads through `browser` so a context the spec opened itself is seen too,
80
+ // but must also depend on `context`. Playwright sets auto fixtures up
81
+ // before the ones a test asks for and tears them down in reverse, so
82
+ // depending on `browser` alone puts this after the context fixture has
83
+ // already closed — `browser.contexts()` comes back empty and every spec
84
+ // driving the plain `page` fixture reports nothing. Naming `context` makes
85
+ // this a dependent of it, which is what orders teardown correctly. It is
86
+ // still lighter than depending on `page`: a context is created, not a page.
87
+ async ({ browser, context }, use, testInfo) => {
88
+ void context; // Depended on for teardown ordering, not for its value.
63
89
  await use();
64
- if (process.env.E2E_COLLECT_COVERAGE !== "true")
90
+ if (!isCoverageEnabled())
65
91
  return;
66
- try {
67
- const coverage = await page.evaluate(() => globalThis.__coverage__);
68
- if (!coverage)
69
- return;
70
- const dir = path.join(testInfo.project.outputDir, "coverage");
71
- fs.mkdirSync(dir, { recursive: true });
72
- fs.writeFileSync(path.join(dir, `${testInfo.testId}-${Date.now()}.json`), JSON.stringify(coverage));
73
- }
74
- catch {
75
- // Best-effort: page may have crashed or been closed
76
- }
92
+ await collectAndWriteCoverage(browser, {
93
+ dir: path.join(testInfo.project.outputDir, "coverage"),
94
+ // Unique per worker process, so two workers writing the same
95
+ // project's outputDir cannot land on the same filename.
96
+ runId: `w${testInfo.workerIndex}`,
97
+ }, (detail) => reportCoverageProblem(testInfo, detail));
77
98
  },
78
99
  { auto: true, scope: "test" },
79
100
  ],
@@ -1,4 +1,12 @@
1
1
  declare function envsubst(str: string, env?: NodeJS.ProcessEnv): string;
2
2
  declare function requireEnv(...varNames: [string, ...string[]]): void;
3
- export { envsubst, requireEnv };
3
+ /**
4
+ * Whether this run should deploy instrumented bundles and collect coverage.
5
+ *
6
+ * Read from two layers that have to agree — plugin resolution picks the
7
+ * `__coverage` OCI tag, the Playwright fixture reads `__coverage__` back out —
8
+ * so the check lives in one place rather than as a string compare in each.
9
+ */
10
+ declare function isCoverageEnabled(): boolean;
11
+ export { envsubst, requireEnv, isCoverageEnabled };
4
12
  //# sourceMappingURL=common.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,iBAAS,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAc,UAS/C;AAED,iBAAS,UAAU,CAAC,GAAG,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAO5D;AAED,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,iBAAS,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAc,UAS/C;AAED,iBAAS,UAAU,CAAC,GAAG,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAO5D;AAED;;;;;;GAMG;AACH,iBAAS,iBAAiB,IAAI,OAAO,CAEpC;AAED,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,CAAC"}
@@ -13,4 +13,14 @@ function requireEnv(...varNames) {
13
13
  }
14
14
  }
15
15
  }
16
- export { envsubst, requireEnv };
16
+ /**
17
+ * Whether this run should deploy instrumented bundles and collect coverage.
18
+ *
19
+ * Read from two layers that have to agree — plugin resolution picks the
20
+ * `__coverage` OCI tag, the Playwright fixture reads `__coverage__` back out —
21
+ * so the check lives in one place rather than as a string compare in each.
22
+ */
23
+ function isCoverageEnabled() {
24
+ return process.env.E2E_COLLECT_COVERAGE === "true";
25
+ }
26
+ export { envsubst, requireEnv, isCoverageEnabled };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAgBD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAqBtC;AAoBD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CA+CjE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAc3D;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5D;AAYD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAQf;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CA2BzB;AAED,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AA2JD;;;;;;;GAOG;AACH;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAMT;AA0HD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAajE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAuBtE;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAwB/B;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,oBAAoB,EAC5B,YAAY,GAAE,MAA8B,EAC5C,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAsC/B"}
1
+ {"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAgBD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAqBtC;AAoBD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CA+CjE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAc3D;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5D;AAYD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAQf;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CA2BzB;AAED,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AA2JD;;;;;;;GAOG;AACH;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAMT;AAyHD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAajE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAuBtE;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAwB/B;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,oBAAoB,EAC5B,YAAY,GAAE,MAA8B,EAC5C,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAsC/B"}
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import yaml from "js-yaml";
4
4
  import { glob } from "zx";
5
5
  import { deepMerge } from "./merge-yamls.js";
6
+ import { isCoverageEnabled } from "./common.js";
6
7
  const OCI_REGISTRY_PREFIX = "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays";
7
8
  // ── Detection ─────────────────────────────────────────────────────────────────
8
9
  /**
@@ -302,8 +303,7 @@ async function resolvePluginPackages(plugins, metadataMap, metadataPath, dpdyPac
302
303
  if (prOciUrls) {
303
304
  const prUrl = prOciUrls.get(displayName);
304
305
  if (prUrl) {
305
- const usesCoverage = process.env.E2E_COLLECT_COVERAGE === "true" &&
306
- metadata.role === "frontend-plugin";
306
+ const usesCoverage = isCoverageEnabled() && metadata.role === "frontend-plugin";
307
307
  const resolved = usesCoverage
308
308
  ? prUrl.replace(/(:[^!]+)/, "$1__coverage")
309
309
  : prUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/e2e-test-utils",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "description": "Test utilities for RHDH E2E tests",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {