diff --git a/e2e-tests/playwright/e2e/plugins/adoption-insights/adoption-insights.spec.ts b/e2e-tests/playwright/e2e/plugins/adoption-insights/adoption-insights.spec.ts index 65ef3e6955..f909f23226 100644 --- a/e2e-tests/playwright/e2e/plugins/adoption-insights/adoption-insights.spec.ts +++ b/e2e-tests/playwright/e2e/plugins/adoption-insights/adoption-insights.spec.ts @@ -1,9 +1,10 @@ -import { test, expect } from "@playwright/test"; +import { test, expect } from "@support/shared-page"; import { Common } from "../../../utils/common"; import { UIhelper } from "../../../utils/ui-helper"; import { TestHelper } from "../../../support/pages/adoption-insights"; import { skipIfJobName } from "../../../utils/helper"; import { JOB_NAME_PATTERNS } from "../../../utils/constants"; +import type { Page } from "@playwright/test"; /* eslint-disable playwright/no-conditional-in-test */ @@ -19,8 +20,7 @@ test.describe.serial("Test Adoption Insights", () => { test.describe .serial("Test Adoption Insights plugin: load permission policies and conditions from files", () => { - let context; - let page; + let page: Page; let testHelper: TestHelper; let uiHelper: UIhelper; let initialSearchCount: number; @@ -28,20 +28,14 @@ test.describe.serial("Test Adoption Insights", () => { let catalogEntitiesFirstEntry: string[]; let techdocsFirstEntry: string[]; - // Shared setup - test.beforeAll(async ({ browser }) => { - context = await browser.newContext(); - page = await context.newPage(); + test.beforeAll(async ({ sharedPage }) => { + page = sharedPage; uiHelper = new UIhelper(page); testHelper = new TestHelper(page); await new Common(page).loginAsKeycloakUser(); await uiHelper.goToPageUrl("/", "Welcome back!"); }); - test.afterAll(async () => { - await context?.close(); - }); - test("Check UI navigation by nav bar when adoption-insights is enabled", async () => { await uiHelper.openSidebarButton("Administration"); await uiHelper.clickLink("Adoption Insights"); @@ -224,6 +218,7 @@ test.describe.serial("Test Adoption Insights", () => { .locator("table.v5-MuiTable-root tbody tr") .first(); const firstEntry = firstRow.locator("td").first(); + // @ts-expect-error PanelState.firstRow is typed as string[] but reassigned to a Locator — pre-existing issue state[title].firstRow = firstRow; let headerTxt: string; diff --git a/e2e-tests/playwright/e2e/plugins/scorecard/scorecard.spec.ts b/e2e-tests/playwright/e2e/plugins/scorecard/scorecard.spec.ts index 3c99a60c3f..c17f9646e7 100644 --- a/e2e-tests/playwright/e2e/plugins/scorecard/scorecard.spec.ts +++ b/e2e-tests/playwright/e2e/plugins/scorecard/scorecard.spec.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import { test } from "@playwright/test"; +import { test } from "@support/shared-page"; import { Common } from "../../../utils/common"; import { Catalog } from "../../../support/pages/catalog"; // TODO: Re-enable/uncomment once https://issues.redhat.com/browse/RHIDP-12130 is fixed // import { CatalogImport } from "../../../support/pages/catalog-import"; import { ScorecardPage } from "../../../support/page-objects/scorecard/scorecard-page"; -import type { BrowserContext, Page } from "@playwright/test"; +import type { Page } from "@playwright/test"; test.describe.serial("Scorecard Plugin Tests", () => { - let context: BrowserContext; let page: Page; let catalog: Catalog; // TODO: Re-enable/uncomment once https://issues.redhat.com/browse/RHIDP-12130 is fixed @@ -33,14 +32,13 @@ test.describe.serial("Scorecard Plugin Tests", () => { let initialGithubCount: number; let initialJiraCount: number; - test.beforeAll(async ({ browser }, testInfo) => { + test.beforeAll(async ({ sharedPage }, testInfo) => { testInfo.annotations.push({ type: "component", description: "scorecard", }); - context = await browser.newContext(); - page = await context.newPage(); + page = sharedPage; catalog = new Catalog(page); // TODO: Re-enable/uncomment once https://issues.redhat.com/browse/RHIDP-12130 is fixed // catalogImport = new CatalogImport(page); @@ -48,10 +46,6 @@ test.describe.serial("Scorecard Plugin Tests", () => { await new Common(page).loginAsKeycloakUser(); }); - test.afterAll(async () => { - await context?.close(); - }); - test("Setup aggregated scorecards on homepage", async () => { await scorecardPage.navigateToHome(); diff --git a/e2e-tests/playwright/support/shared-page.ts b/e2e-tests/playwright/support/shared-page.ts new file mode 100644 index 0000000000..46a88cffc4 --- /dev/null +++ b/e2e-tests/playwright/support/shared-page.ts @@ -0,0 +1,88 @@ +// Worker-scoped fixtures for test.describe.serial() blocks. +// Usage: import { test, expect } from "@support/shared-page"; + +import { + test as baseTest, + expect as baseExpect, + type BrowserContext, + type Page, +} from "@playwright/test"; +import path from "node:path"; +import fs from "node:fs"; + +type TestFixtures = { + _sharedTestHook: void; +}; + +type WorkerFixtures = { + sharedContext: BrowserContext; + sharedPage: Page; +}; + +// Each Playwright worker runs in its own process, so this flag is per-worker. +let workerHadFailure = false; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const test = baseTest.extend({ + sharedContext: [ + async ({ browser }, use, workerInfo) => { + const videoDir = path.join( + "test-results", + `shared-worker-${workerInfo.workerIndex}`, + "videos", + ); + + // Always record — Playwright's recordVideo has no retain-on-failure mode + // for manual contexts, so we record unconditionally and delete on success. + // Tracing is managed automatically by Playwright (trace: "on" in config). + const context = await browser.newContext({ + recordVideo: { + dir: videoDir, + size: { width: 1280, height: 720 }, + }, + }); + + await use(context); + + await context.close(); + + // Retain-on-failure: delete video files when all tests passed + if (!workerHadFailure && fs.existsSync(videoDir)) { + fs.rmSync(videoDir, { recursive: true, force: true }); + } + }, + { scope: "worker" }, + ], + + sharedPage: [ + async ({ sharedContext }, use) => { + const page = await sharedContext.newPage(); + await use(page); + }, + { scope: "worker" }, + ], + + _sharedTestHook: [ + async ({ sharedPage }, use, testInfo) => { + await use(); + + if (testInfo.status !== "passed" && testInfo.status !== "skipped") { + workerHadFailure = true; + try { + const screenshotPath = testInfo.outputPath("failure.png"); + await sharedPage.screenshot({ path: screenshotPath }); + await testInfo.attach("screenshot", { + path: screenshotPath, + contentType: "image/png", + }); + } catch { + // Page may have crashed — screenshot unavailable + } + } + }, + { auto: true }, + ], +}); + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const expect = baseExpect; diff --git a/e2e-tests/tsconfig.json b/e2e-tests/tsconfig.json index a5507b1218..9e7a32705d 100644 --- a/e2e-tests/tsconfig.json +++ b/e2e-tests/tsconfig.json @@ -8,7 +8,11 @@ "module": "ESNext", "moduleResolution": "node", "noEmit": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@support/*": ["playwright/support/*"] + } }, "include": ["**/*.ts"] }