@red-hat-developer-hub/e2e-test-utils 2.1.7 → 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.
- package/dist/playwright/coverage.d.ts +70 -0
- package/dist/playwright/coverage.d.ts.map +1 -0
- package/dist/playwright/coverage.js +108 -0
- package/dist/playwright/coverage.test.d.ts +2 -0
- package/dist/playwright/coverage.test.d.ts.map +1 -0
- package/dist/playwright/coverage.test.js +240 -0
- package/dist/playwright/fixtures/test.d.ts.map +1 -1
- package/dist/playwright/fixtures/test.js +35 -14
- package/dist/utils/common.d.ts +9 -1
- package/dist/utils/common.d.ts.map +1 -1
- package/dist/utils/common.js +11 -1
- package/dist/utils/plugin-metadata.d.ts.map +1 -1
- package/dist/utils/plugin-metadata.js +2 -2
- package/package.json +1 -1
|
@@ -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 @@
|
|
|
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;
|
|
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
|
|
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
|
-
|
|
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 (
|
|
90
|
+
if (!isCoverageEnabled())
|
|
65
91
|
return;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
],
|
package/dist/utils/common.d.ts
CHANGED
|
@@ -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
|
-
|
|
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"}
|
package/dist/utils/common.js
CHANGED
|
@@ -13,4 +13,14 @@ function requireEnv(...varNames) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
|
|
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":"
|
|
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 =
|
|
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;
|