@red-hat-developer-hub/e2e-test-utils 2.1.7 → 2.1.9
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/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml +1 -1
- package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +1 -1
- 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 +2 -2
- package/dist/utils/plugin-metadata.d.ts.map +1 -1
- package/dist/utils/plugin-metadata.js +5 -5
- package/dist/utils/tests/plugin-metadata.nightly.test.js +20 -18
- package/dist/utils/tests/plugin-metadata.test.js +4 -4
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
plugins:
|
|
2
|
-
- package: oci://
|
|
2
|
+
- package: oci://quay.io/rhdh/backstage-community-plugin-catalog-backend-module-keycloak:{{inherit}}
|
|
3
3
|
disabled: false
|
|
4
4
|
pluginConfig:
|
|
5
5
|
catalog:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
includes:
|
|
2
2
|
- dynamic-plugins.default.yaml
|
|
3
3
|
plugins:
|
|
4
|
-
- package: oci://
|
|
4
|
+
- package: oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-quickstart:{{inherit}}
|
|
5
5
|
disabled: true
|
|
@@ -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 };
|
|
@@ -44,7 +44,7 @@ export declare function fetchDefaultPackages(): Promise<Set<string>>;
|
|
|
44
44
|
* Resolution priority:
|
|
45
45
|
* 1. NIGHTLY_DPDY_OCI_REGISTRY_MAP — JSON object mapping registry → array of package names
|
|
46
46
|
* 2. NIGHTLY_DPDY_OCI_REGISTRY — blanket override for all plugins using {{inherit}}
|
|
47
|
-
* 3. Default:
|
|
47
|
+
* 3. Default: quay.io/rhdh
|
|
48
48
|
*/
|
|
49
49
|
export declare function getDpdyRegistry(packageName: string): string;
|
|
50
50
|
/**
|
|
@@ -101,7 +101,7 @@ export declare function normalizeDisablePluginName(plugin: string): string;
|
|
|
101
101
|
* mountPoints / config conflicts with the workspace's PR OCI image.
|
|
102
102
|
*
|
|
103
103
|
* Registry resolution: NIGHTLY_DPDY_OCI_REGISTRY, else
|
|
104
|
-
*
|
|
104
|
+
* quay.io/rhdh.
|
|
105
105
|
*
|
|
106
106
|
* @param plugins plugin names, wrapper paths, and/or OCI refs to disable
|
|
107
107
|
* @returns Dynamic plugins configuration that disables listed plugins
|
|
@@ -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
|
/**
|
|
@@ -36,7 +37,7 @@ export function isNightlyJob() {
|
|
|
36
37
|
const DEFAULT_PACKAGES_BASE_URL = "https://raw.githubusercontent.com/redhat-developer/rhdh-plugin-export-overlays/refs/heads";
|
|
37
38
|
// release-1.10 still hosts default.packages.yaml in the rhdh repo (pre-migration)
|
|
38
39
|
const LEGACY_DEFAULT_PACKAGES_BASE_URL = "https://raw.githubusercontent.com/redhat-developer/rhdh/refs/heads";
|
|
39
|
-
const DEFAULT_DPDY_OCI_REGISTRY = "
|
|
40
|
+
const DEFAULT_DPDY_OCI_REGISTRY = "quay.io/rhdh";
|
|
40
41
|
/**
|
|
41
42
|
* Fetches the list of packages from default.packages.yaml (source for the
|
|
42
43
|
* dynamic-plugins.default.yaml — DPDY — in the catalog index image shipped
|
|
@@ -85,7 +86,7 @@ export async function fetchDefaultPackages() {
|
|
|
85
86
|
* Resolution priority:
|
|
86
87
|
* 1. NIGHTLY_DPDY_OCI_REGISTRY_MAP — JSON object mapping registry → array of package names
|
|
87
88
|
* 2. NIGHTLY_DPDY_OCI_REGISTRY — blanket override for all plugins using {{inherit}}
|
|
88
|
-
* 3. Default:
|
|
89
|
+
* 3. Default: quay.io/rhdh
|
|
89
90
|
*/
|
|
90
91
|
export function getDpdyRegistry(packageName) {
|
|
91
92
|
const map = process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP;
|
|
@@ -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;
|
|
@@ -398,7 +398,7 @@ export function normalizeDisablePluginName(plugin) {
|
|
|
398
398
|
* mountPoints / config conflicts with the workspace's PR OCI image.
|
|
399
399
|
*
|
|
400
400
|
* Registry resolution: NIGHTLY_DPDY_OCI_REGISTRY, else
|
|
401
|
-
*
|
|
401
|
+
* quay.io/rhdh.
|
|
402
402
|
*
|
|
403
403
|
* @param plugins plugin names, wrapper paths, and/or OCI refs to disable
|
|
404
404
|
* @returns Dynamic plugins configuration that disables listed plugins
|
|
@@ -234,7 +234,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
234
234
|
};
|
|
235
235
|
const dpdyPackages = new Set(["@backstage-community/plugin-tekton"]);
|
|
236
236
|
const result = await processPluginsForDeployment(config, metadataDir, dpdyPackages);
|
|
237
|
-
assert.strictEqual(result.plugins[0].package, "oci://
|
|
237
|
+
assert.strictEqual(result.plugins[0].package, "oci://quay.io/rhdh/backstage-community-plugin-tekton:{{inherit}}", "DPDY OCI plugin must resolve to {{inherit}} with default quay.io registry");
|
|
238
238
|
}
|
|
239
239
|
finally {
|
|
240
240
|
await fs.remove(metadataDir);
|
|
@@ -258,7 +258,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
258
258
|
],
|
|
259
259
|
};
|
|
260
260
|
const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-topology"]));
|
|
261
|
-
assert.strictEqual(result.plugins[0].package, "oci://
|
|
261
|
+
assert.strictEqual(result.plugins[0].package, "oci://quay.io/rhdh/backstage-community-plugin-topology:{{inherit}}", "{{inherit}} ref must use default RHEC registry with no alias suffix");
|
|
262
262
|
assert.ok(!result.plugins[0].package.includes("!"), "{{inherit}} ref must NOT include !alias suffix");
|
|
263
263
|
}
|
|
264
264
|
finally {
|
|
@@ -283,7 +283,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
283
283
|
],
|
|
284
284
|
};
|
|
285
285
|
const result = await processPluginsForDeployment(config, metadataDir, new Set(["@red-hat-developer-hub/backstage-plugin-orchestrator"]));
|
|
286
|
-
assert.strictEqual(result.plugins[0].package, "oci://
|
|
286
|
+
assert.strictEqual(result.plugins[0].package, "oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", "{{inherit}} must use default RHEC registry regardless of metadata's ghcr.io");
|
|
287
287
|
}
|
|
288
288
|
finally {
|
|
289
289
|
await fs.remove(metadataDir);
|
|
@@ -296,14 +296,14 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
296
296
|
{
|
|
297
297
|
name: "backstage-community-plugin-tekton",
|
|
298
298
|
packageName: "@backstage-community/plugin-tekton",
|
|
299
|
-
dynamicArtifact: "oci://
|
|
299
|
+
dynamicArtifact: "oci://quay.io/rhdh/backstage-community-plugin-tekton@sha256:abc",
|
|
300
300
|
},
|
|
301
301
|
]);
|
|
302
302
|
try {
|
|
303
303
|
const config = {
|
|
304
304
|
plugins: [
|
|
305
305
|
{
|
|
306
|
-
package: "oci://
|
|
306
|
+
package: "oci://quay.io/rhdh/backstage-community-plugin-tekton@sha256:abc",
|
|
307
307
|
disabled: false,
|
|
308
308
|
},
|
|
309
309
|
],
|
|
@@ -325,7 +325,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
325
325
|
{
|
|
326
326
|
name: "backstage-community-plugin-tekton",
|
|
327
327
|
packageName: "@backstage-community/plugin-tekton",
|
|
328
|
-
dynamicArtifact: "oci://
|
|
328
|
+
dynamicArtifact: "oci://quay.io/rhdh/backstage-community-plugin-tekton@sha256:abc",
|
|
329
329
|
},
|
|
330
330
|
{
|
|
331
331
|
name: "red-hat-developer-hub-backstage-plugin-orchestrator",
|
|
@@ -337,7 +337,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
337
337
|
const config = {
|
|
338
338
|
plugins: [
|
|
339
339
|
{
|
|
340
|
-
package: "oci://
|
|
340
|
+
package: "oci://quay.io/rhdh/backstage-community-plugin-tekton@sha256:abc",
|
|
341
341
|
disabled: false,
|
|
342
342
|
},
|
|
343
343
|
{
|
|
@@ -352,7 +352,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
352
352
|
]);
|
|
353
353
|
const result = await processPluginsForDeployment(config, metadataDir, dpdyPackages);
|
|
354
354
|
assert.strictEqual(result.plugins[0].package, "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:{{inherit}}", "tekton must use ghcr.io from NIGHTLY_DPDY_OCI_REGISTRY_MAP");
|
|
355
|
-
assert.strictEqual(result.plugins[1].package, "oci://
|
|
355
|
+
assert.strictEqual(result.plugins[1].package, "oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", "orchestrator must use default RHEC (not in map)");
|
|
356
356
|
}
|
|
357
357
|
finally {
|
|
358
358
|
await fs.remove(metadataDir);
|
|
@@ -369,23 +369,23 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
369
369
|
{
|
|
370
370
|
name: "backstage-community-plugin-tekton",
|
|
371
371
|
packageName: "@backstage-community/plugin-tekton",
|
|
372
|
-
dynamicArtifact: "oci://
|
|
372
|
+
dynamicArtifact: "oci://quay.io/rhdh/backstage-community-plugin-tekton@sha256:abc",
|
|
373
373
|
},
|
|
374
374
|
{
|
|
375
375
|
name: "red-hat-developer-hub-backstage-plugin-orchestrator",
|
|
376
376
|
packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator",
|
|
377
|
-
dynamicArtifact: "oci://
|
|
377
|
+
dynamicArtifact: "oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:def",
|
|
378
378
|
},
|
|
379
379
|
]);
|
|
380
380
|
try {
|
|
381
381
|
const config = {
|
|
382
382
|
plugins: [
|
|
383
383
|
{
|
|
384
|
-
package: "oci://
|
|
384
|
+
package: "oci://quay.io/rhdh/backstage-community-plugin-tekton@sha256:abc",
|
|
385
385
|
disabled: false,
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
|
-
package: "oci://
|
|
388
|
+
package: "oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:def",
|
|
389
389
|
disabled: false,
|
|
390
390
|
},
|
|
391
391
|
],
|
|
@@ -547,7 +547,7 @@ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
|
|
|
547
547
|
// Only tekton is in DPDY
|
|
548
548
|
const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tekton"]));
|
|
549
549
|
// Tekton: DPDY → {{inherit}} with default RHEC, no config injection
|
|
550
|
-
assert.strictEqual(result.plugins[0].package, "oci://
|
|
550
|
+
assert.strictEqual(result.plugins[0].package, "oci://quay.io/rhdh/backstage-community-plugin-tekton:{{inherit}}", "DPDY plugin must use {{inherit}} with default RHEC registry");
|
|
551
551
|
assert.strictEqual(result.plugins[0].pluginConfig, undefined, "DPDY plugin must not have config injected");
|
|
552
552
|
// Scorecard: non-DPDY → full OCI ref, config injected
|
|
553
553
|
assert.ok(result.plugins[1].package.includes("bs_1.49.4__1.0.0"), "non-DPDY plugin must use full metadata ref");
|
|
@@ -566,7 +566,7 @@ describe("getDpdyRegistry", () => {
|
|
|
566
566
|
it("returns default RHEC registry when no env vars set", () => {
|
|
567
567
|
delete process.env.NIGHTLY_DPDY_OCI_REGISTRY;
|
|
568
568
|
delete process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP;
|
|
569
|
-
assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "
|
|
569
|
+
assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "quay.io/rhdh");
|
|
570
570
|
});
|
|
571
571
|
it("NIGHTLY_DPDY_OCI_REGISTRY overrides default for all plugins", () => {
|
|
572
572
|
process.env.NIGHTLY_DPDY_OCI_REGISTRY =
|
|
@@ -593,7 +593,7 @@ describe("getDpdyRegistry", () => {
|
|
|
593
593
|
"@backstage-community/plugin-tekton",
|
|
594
594
|
],
|
|
595
595
|
});
|
|
596
|
-
assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "
|
|
596
|
+
assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "quay.io/rhdh", "unlisted plugin must fall back to default RHEC");
|
|
597
597
|
});
|
|
598
598
|
it("NIGHTLY_DPDY_OCI_REGISTRY_MAP takes precedence over NIGHTLY_DPDY_OCI_REGISTRY", () => {
|
|
599
599
|
process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh";
|
|
@@ -609,10 +609,12 @@ describe("getDpdyRegistry", () => {
|
|
|
609
609
|
"ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [
|
|
610
610
|
"@backstage-community/plugin-tekton",
|
|
611
611
|
],
|
|
612
|
-
"
|
|
612
|
+
"registry.access.redhat.com": [
|
|
613
|
+
"@red-hat-developer-hub/backstage-plugin-orchestrator",
|
|
614
|
+
],
|
|
613
615
|
});
|
|
614
616
|
assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "ghcr.io/redhat-developer/rhdh-plugin-export-overlays");
|
|
615
|
-
assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "
|
|
616
|
-
assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-argocd"), "
|
|
617
|
+
assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "registry.access.redhat.com");
|
|
618
|
+
assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-argocd"), "quay.io/rhdh", "unlisted plugin falls back to default");
|
|
617
619
|
});
|
|
618
620
|
});
|
|
@@ -86,7 +86,7 @@ describe("disablePlugins", () => {
|
|
|
86
86
|
disabled: true,
|
|
87
87
|
});
|
|
88
88
|
assert.deepStrictEqual(result.plugins[1], {
|
|
89
|
-
package: "oci://
|
|
89
|
+
package: "oci://quay.io/rhdh/backstage-community-plugin-tech-radar:{{inherit}}",
|
|
90
90
|
disabled: true,
|
|
91
91
|
});
|
|
92
92
|
assert.deepStrictEqual(result.plugins[2], {
|
|
@@ -94,7 +94,7 @@ describe("disablePlugins", () => {
|
|
|
94
94
|
disabled: true,
|
|
95
95
|
});
|
|
96
96
|
assert.deepStrictEqual(result.plugins[3], {
|
|
97
|
-
package: "oci://
|
|
97
|
+
package: "oci://quay.io/rhdh/backstage-plugin-kubernetes:{{inherit}}",
|
|
98
98
|
disabled: true,
|
|
99
99
|
});
|
|
100
100
|
});
|
|
@@ -103,7 +103,7 @@ describe("disablePlugins", () => {
|
|
|
103
103
|
const result = disablePlugins([
|
|
104
104
|
"./dynamic-plugins/dist/backstage-plugin-kubernetes-dynamic",
|
|
105
105
|
"backstage-plugin-kubernetes",
|
|
106
|
-
"oci://
|
|
106
|
+
"oci://quay.io/rhdh/backstage-plugin-kubernetes:{{inherit}}",
|
|
107
107
|
]);
|
|
108
108
|
assert.strictEqual(result.plugins.length, 2);
|
|
109
109
|
assert.deepStrictEqual(result.plugins[0], {
|
|
@@ -111,7 +111,7 @@ describe("disablePlugins", () => {
|
|
|
111
111
|
disabled: true,
|
|
112
112
|
});
|
|
113
113
|
assert.deepStrictEqual(result.plugins[1], {
|
|
114
|
-
package: "oci://
|
|
114
|
+
package: "oci://quay.io/rhdh/backstage-plugin-kubernetes:{{inherit}}",
|
|
115
115
|
disabled: true,
|
|
116
116
|
});
|
|
117
117
|
});
|