# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: ui/component.test.ts >> Component UI Test Suite >> Verify CI >> verify CI provider on CI tab - Location: tests/ui/component.test.ts:83:5 # Error details ``` TimeoutError: Step timeout of 40000ms exceeded. ``` ``` Error: locator.click: Test ended. Call log: - waiting for getByRole('button', { name: 'Fit to Screen' }) - locator resolved to - attempting click action - waiting for element to be visible, enabled and stable ``` # Page snapshot ```yaml - generic [ref=e3]: - navigation [ref=e5]: - generic [ref=e6]: - link "Home" [ref=e8] [cursor=pointer]: - /url: / - img [ref=e9] - generic [ref=e18]: - img [ref=e20] - combobox "Search..." [ref=e22] - generic "Self-service" [ref=e25]: - link "Self-service" [ref=e26] [cursor=pointer]: - /url: /create - img [ref=e28] - button "Your starred items" [ref=e31] [cursor=pointer]: - img [ref=e32] - button "Application launcher" [ref=e35] [cursor=pointer]: - img [ref=e36] - button "Help" [ref=e39] [cursor=pointer]: - img [ref=e40] - separator [ref=e42] - button "Admin" [ref=e44] [cursor=pointer]: - generic [ref=e45]: - img [ref=e46] - paragraph [ref=e49]: Admin - img [ref=e50] - generic [ref=e53]: - navigation "sidebar nav": - generic [ref=e55]: - generic [ref=e58]: - link "Home" [ref=e60] [cursor=pointer]: - /url: / - img [ref=e64] - generic [ref=e66]: Home - link "Catalog" [ref=e68] [cursor=pointer]: - /url: /catalog - img [ref=e72] - generic [ref=e74]: Catalog - link "APIs" [ref=e76] [cursor=pointer]: - /url: /api-docs - img [ref=e80] - generic [ref=e82]: APIs - link "Learning Paths" [ref=e84] [cursor=pointer]: - /url: /learning-paths - img [ref=e88] - generic [ref=e90]: Learning Paths - separator [ref=e91] - link "Docs" [ref=e94] [cursor=pointer]: - /url: /docs - img [ref=e98] - generic [ref=e100]: Docs - generic [ref=e101]: - separator [ref=e102] - button "Administration" [ref=e103] [cursor=pointer]: - generic [ref=e104]: - img [ref=e108] - generic [ref=e111]: Administration - img [ref=e113] - main [ref=e115]: - generic [ref=e116]: - generic [ref=e117]: - paragraph [ref=e118]: component — service - heading "e2e-tests-python-sacnsehv Add to favorites" [level=1] [ref=e119]: - generic [ref=e120]: - generic "component:default/e2e-tests-python-sacnsehv | service | Secure Supply Chain Example for Python is an interpreted, object-oriented, high-level programming language with dynamic semantics. This sample demonstrates software supply chain security functionalty using an advanced continuous integration pipeline covering building, CVE scanning, security scanning, signatures, attestations, SLSA provenance and SBOM along with Gitops-based continuous deployment." [ref=e122]: e2e-tests-python-sacnsehv - button "Add to favorites" [ref=e123] [cursor=pointer]: - img [ref=e126] - generic [ref=e128]: - generic [ref=e130]: - paragraph [ref=e131]: Owner - paragraph [ref=e132]: - link "user:guest" [ref=e133] [cursor=pointer]: - /url: /catalog/default/user/guest - generic "user:default/guest" [ref=e134]: - img [ref=e136] - text: user:guest - generic [ref=e139]: - paragraph [ref=e140]: Lifecycle - paragraph [ref=e141]: experimental - button "more" [ref=e142] [cursor=pointer]: - img [ref=e144] - tablist "tabs" [ref=e150]: - tab "Overview" [ref=e151] [cursor=pointer] - tab "Topology" [ref=e152] [cursor=pointer] - tab "CI" [selected] [ref=e153] [cursor=pointer] - tab "CD" [ref=e154] [cursor=pointer] - tab "Kubernetes" [ref=e155] [cursor=pointer] - tab "Image Registry" [ref=e156] [cursor=pointer] - tab "API" [ref=e157] [cursor=pointer] - tab "Dependencies" [ref=e158] [cursor=pointer] - tab "Docs" [ref=e159] [cursor=pointer] - article [ref=e161]: - generic [ref=e164]: - generic [ref=e168]: - generic [ref=e169]: - paragraph [ref=e170]: Cluster - generic [ref=e173]: - button "rhdh-cluster" [ref=e174] [cursor=pointer]: - paragraph [ref=e175]: rhdh-cluster - textbox: rhdh-cluster - img - generic [ref=e176]: - paragraph [ref=e177]: Status - generic [ref=e180]: - button "All" [ref=e181] [cursor=pointer]: - paragraph [ref=e182]: All - textbox: All - img - generic [ref=e183]: - generic "Collapse all" [ref=e184]: - button [disabled]: - generic: - img - generic "Expand all" [ref=e185]: - button [ref=e186] [cursor=pointer]: - img [ref=e188] - separator [ref=e190] - generic [ref=e193]: - generic [ref=e194]: - heading "Pipeline Runs" [level=2] [ref=e195] - generic "search" [ref=e197]: - img [ref=e199] - textbox "Search" [ref=e201] - generic [ref=e202]: - button "clear search" [disabled]: - generic: - img - table [ref=e203]: - rowgroup [ref=e204]: - row "NAME VULNERABILITIES STATUS TASK STATUS STARTED DURATION ACTIONS" [ref=e205]: - columnheader [ref=e206] - columnheader "NAME" [ref=e207]: - button "NAME" [ref=e208] [cursor=pointer]: - text: NAME - img [ref=e209] - columnheader "VULNERABILITIES" [ref=e211]: - button "VULNERABILITIES" [ref=e212] [cursor=pointer]: - text: VULNERABILITIES - img [ref=e213] - columnheader "STATUS" [ref=e215]: - button "STATUS" [ref=e216] [cursor=pointer]: - text: STATUS - img [ref=e217] - columnheader "TASK STATUS" [ref=e219]: - button "TASK STATUS" [ref=e220] [cursor=pointer]: - text: TASK STATUS - img [ref=e221] - columnheader "STARTED" [ref=e223]: - button "STARTED" [ref=e224] [cursor=pointer]: - text: STARTED - img [ref=e225] - columnheader "DURATION" [ref=e227]: - button "DURATION" [ref=e228] [cursor=pointer]: - text: DURATION - img [ref=e229] - columnheader "ACTIONS" [ref=e231] - rowgroup [ref=e232]: - row "expand row PLR e2e-tests-python-sacnsehv-gitops-on-pull-request-zp6wk - Succeeded 4/20/2026, 1:31:32 AM 45 seconds" [ref=e233]: - cell "expand row" [ref=e234]: - button "expand row" [ref=e235] [cursor=pointer]: - img [ref=e237] - cell "PLR e2e-tests-python-sacnsehv-gitops-on-pull-request-zp6wk" [ref=e239]: - generic [ref=e241]: - generic [ref=e243]: PLR - generic [ref=e244]: e2e-tests-python-sacnsehv-gitops-on-pull-request-zp6wk - img [ref=e248] - cell "-" [ref=e251]: - generic [ref=e252]: "-" - cell "Succeeded" [ref=e253]: - paragraph [ref=e254]: - img [ref=e257] - generic [ref=e259]: Succeeded - cell [ref=e260] - cell "4/20/2026, 1:31:32 AM" [ref=e261]: - time [ref=e263]: 4/20/2026, 1:31:32 AM - cell "45 seconds" [ref=e264] - cell [ref=e265]: - generic [ref=e266]: - button [ref=e269] [cursor=pointer]: - img [ref=e271] - button [ref=e275] [cursor=pointer]: - img [ref=e277] - button [disabled] [ref=e281]: - img [ref=e283] - button [disabled] [ref=e287]: - img [ref=e289] - row [ref=e291]: - cell [ref=e292] - row "expand row PLR e2e-tests-python-sacnsehv-gitops-on-pull-request-8llqc - Succeeded 4/20/2026, 1:29:51 AM 43 seconds" [ref=e293]: - cell "expand row" [ref=e294]: - button "expand row" [ref=e295] [cursor=pointer]: - img [ref=e297] - cell "PLR e2e-tests-python-sacnsehv-gitops-on-pull-request-8llqc" [ref=e299]: - generic [ref=e301]: - generic [ref=e303]: PLR - generic [ref=e304]: e2e-tests-python-sacnsehv-gitops-on-pull-request-8llqc - img [ref=e308] - cell "-" [ref=e311]: - generic [ref=e312]: "-" - cell "Succeeded" [ref=e313]: - paragraph [ref=e314]: - img [ref=e317] - generic [ref=e319]: Succeeded - cell [ref=e320] - cell "4/20/2026, 1:29:51 AM" [ref=e321]: - time [ref=e323]: 4/20/2026, 1:29:51 AM - cell "43 seconds" [ref=e324] - cell [ref=e325]: - generic [ref=e326]: - button [ref=e329] [cursor=pointer]: - img [ref=e331] - button [ref=e335] [cursor=pointer]: - img [ref=e337] - button [disabled] [ref=e341]: - img [ref=e343] - button [disabled] [ref=e347]: - img [ref=e349] - row [ref=e351]: - cell [ref=e352] - row "expand row PLR e2e-tests-python-sacnsehv-on-push-wnljm Critical 0 High 11 Medium 119 Low 228 Succeeded 4/20/2026, 1:26:21 AM 2 minutes 32 seconds" [ref=e353]: - cell "expand row" [ref=e354]: - button "expand row" [active] [ref=e355] [cursor=pointer]: - img [ref=e357] - cell "PLR e2e-tests-python-sacnsehv-on-push-wnljm" [ref=e359]: - generic [ref=e361]: - generic [ref=e363]: PLR - generic [ref=e364]: e2e-tests-python-sacnsehv-on-push-wnljm - img [ref=e368] - cell "Critical 0 High 11 Medium 119 Low 228" [ref=e371]: - generic [ref=e372]: - generic [ref=e373]: - img "Critical" [ref=e376] - generic [ref=e378]: "0" - generic [ref=e379]: - img "High" [ref=e382] - generic [ref=e385]: "11" - generic [ref=e386]: - img "Medium" [ref=e389] - generic [ref=e391]: "119" - generic [ref=e392]: - img "Low" [ref=e395] - generic [ref=e398]: "228" - cell "Succeeded" [ref=e399]: - paragraph [ref=e400]: - img [ref=e403] - generic [ref=e405]: Succeeded - cell [ref=e406] - cell "4/20/2026, 1:26:21 AM" [ref=e407]: - time [ref=e409]: 4/20/2026, 1:26:21 AM - cell "2 minutes 32 seconds" [ref=e410] - cell [ref=e411]: - generic [ref=e412]: - button [ref=e415] [cursor=pointer]: - img [ref=e417] - button [ref=e421] [cursor=pointer]: - img [ref=e423] - button [ref=e427] [cursor=pointer]: - img [ref=e429] - button [ref=e433] [cursor=pointer]: - img [ref=e435] - row "Zoom In Zoom Out Fit to Screen Reset View" [ref=e437]: - cell "Zoom In Zoom Out Fit to Screen Reset View" [ref=e438]: - generic [ref=e447]: - img [ref=e449]: - generic [ref=e453]: - generic [ref=e479] [cursor=pointer]: - generic [ref=e482]: clone-repository - img [ref=e485] - generic [ref=e487]: - generic: 2/2 - generic [ref=e493] [cursor=pointer]: - generic [ref=e496]: build - img [ref=e499] - generic [ref=e501]: - generic: 2/2 - generic [ref=e507] [cursor=pointer]: - generic [ref=e510]: deploy - img [ref=e513] - generic [ref=e515]: - generic: 1/1 - generic [ref=e523] [cursor=pointer]: - generic [ref=e526]: deployment-check - img [ref=e529] - generic [ref=e531]: - generic: 1/1 - generic [ref=e539] [cursor=pointer]: - generic [ref=e542]: scan - img [ref=e545] - generic [ref=e547]: - generic: 1/1 - generic [ref=e553] [cursor=pointer]: - generic [ref=e556]: show-sbom - img [ref=e559] - generic [ref=e561]: - generic: 1/1 - generic [ref=e567] [cursor=pointer]: - generic [ref=e570]: summarize - img [ref=e573] - generic [ref=e575]: - generic: 1/1 - generic [ref=e596]: - button "Zoom In" [ref=e599] [cursor=pointer]: - generic [ref=e600]: - img [ref=e601] - generic [ref=e603]: Zoom In - button "Zoom Out" [ref=e606] [cursor=pointer]: - generic [ref=e607]: - img [ref=e608] - generic [ref=e610]: Zoom Out - button "Fit to Screen" [ref=e613] [cursor=pointer]: - generic [ref=e614]: - img [ref=e615] - generic [ref=e617]: Fit to Screen - button "Reset View" [ref=e620] [cursor=pointer]: - generic [ref=e621]: - img [ref=e622] - generic [ref=e624]: Reset View - row "expand row PLR e2e-tests-python-sacnsehv-on-pull-request-75m4d Critical 0 High 11 Medium 119 Low 228 Succeeded 4/20/2026, 1:23:34 AM 2 minutes 40 seconds" [ref=e625]: - cell "expand row" [ref=e626]: - button "expand row" [ref=e627] [cursor=pointer]: - img [ref=e629] - cell "PLR e2e-tests-python-sacnsehv-on-pull-request-75m4d" [ref=e631]: - generic [ref=e633]: - generic [ref=e635]: PLR - generic [ref=e636]: e2e-tests-python-sacnsehv-on-pull-request-75m4d - img [ref=e640] - cell "Critical 0 High 11 Medium 119 Low 228" [ref=e643]: - generic [ref=e644]: - generic [ref=e645]: - img "Critical" [ref=e648] - generic [ref=e650]: "0" - generic [ref=e651]: - img "High" [ref=e654] - generic [ref=e657]: "11" - generic [ref=e658]: - img "Medium" [ref=e661] - generic [ref=e663]: "119" - generic [ref=e664]: - img "Low" [ref=e667] - generic [ref=e670]: "228" - cell "Succeeded" [ref=e671]: - paragraph [ref=e672]: - img [ref=e675] - generic [ref=e677]: Succeeded - cell [ref=e678] - cell "4/20/2026, 1:23:34 AM" [ref=e679]: - time [ref=e681]: 4/20/2026, 1:23:34 AM - cell "2 minutes 40 seconds" [ref=e682] - cell [ref=e683]: - generic [ref=e684]: - button [ref=e687] [cursor=pointer]: - img [ref=e689] - button [ref=e693] [cursor=pointer]: - img [ref=e695] - button [ref=e699] [cursor=pointer]: - img [ref=e701] - button [ref=e705] [cursor=pointer]: - img [ref=e707] - row [ref=e709]: - cell [ref=e710] - row "5 rows 1-4 of 4 Previous page Next page" [ref=e711]: - cell "5 rows 1-4 of 4 Previous page Next page" [ref=e712]: - generic [ref=e713]: - paragraph - generic [ref=e714]: - button "5 rows" [ref=e715] [cursor=pointer] - textbox: "5" - img - paragraph [ref=e716]: 1-4 of 4 - generic [ref=e717]: - button "Previous page" [disabled]: - generic: - img - button "Next page" [disabled]: - generic: - img ``` # Test source ```ts 1 | import { expect, Page, Locator } from '@playwright/test'; 2 | import { BaseCIPlugin } from './baseCIPlugin'; 3 | import { TektonPO } from '../../page-objects/tektonPo'; 4 | import { CiPo } from '../../page-objects/ciPo'; 5 | import { CommonPO } from '../../page-objects/commonPo'; 6 | 7 | export class TektonPlugin extends BaseCIPlugin { 8 | constructor(name: string, registryOrg: string) { 9 | super(name, registryOrg); 10 | } 11 | 12 | private async checkActionButtons(onPushRow: Locator): Promise { 13 | for (const testId of [TektonPO.logsIconTestId, TektonPO.sbomIconTestId, TektonPO.viewOutputTestId]) { 14 | const button = onPushRow.getByTestId(testId); 15 | await expect(button).toBeVisible(); 16 | } 17 | } 18 | 19 | private async checkLogsPopup(page: Page, row: Locator): Promise { 20 | const logsButton = row.getByTestId(TektonPO.logsIconTestId); 21 | await logsButton.click(); 22 | 23 | const logsPopup = page.getByTitle(TektonPO.logsDialogTitle); 24 | await expect(logsPopup).toBeVisible(); 25 | 26 | for (const task of TektonPO.sourceTasks) { 27 | const button = page.getByRole('heading', { name: task, exact: true }); 28 | await expect(button).toBeVisible(); 29 | } 30 | 31 | const button = page.getByRole('heading', { name: TektonPO.sourceTasks[0] }); 32 | await button.click(); 33 | 34 | // Check the log is visible by looking for the word 'STEP' 35 | const span = page.getByText(TektonPO.logStepRegex).first(); 36 | await expect(span).toBeVisible(); 37 | 38 | // Close popup 39 | const closeButton = page.getByRole('dialog').getByTestId(CommonPO.closeIconTestId); 40 | await closeButton.click(); 41 | } 42 | 43 | async checkSBOMpopup(page: Page, row: Locator): Promise { 44 | const sbomButton = row.getByTestId(TektonPO.sbomIconTestId); 45 | await sbomButton.click(); 46 | 47 | const searchBox = page.getByRole('textbox', { name: TektonPO.searchBoxName }); 48 | await searchBox.fill(TektonPO.sbomStepName); 49 | 50 | const span = page.getByText(TektonPO.sbomStepName); 51 | await expect(span).toBeVisible(); 52 | 53 | // Close popup 54 | const closeButton = page.getByRole('dialog').getByTestId(CommonPO.closeIconTestId); 55 | await closeButton.click(); 56 | } 57 | 58 | private async checkGraph(page: Page, row: Locator): Promise { 59 | const expandButton = row.getByRole('button', { name: TektonPO.expandButtonName }); 60 | const graph = page.locator(TektonPO.graphSelector); 61 | 62 | // Expand the row 63 | await expandButton.click(); 64 | 65 | // Check the graph is visible 66 | await expect(graph).toBeVisible(); 67 | 68 | // Fit to screen > 69 | await page.getByRole('button', { name: TektonPO.fitToScreenButtonName }).click(); | ^ Error: locator.click: Test ended. 70 | 71 | // Check all the tasks are visible 72 | for (const taskName of TektonPO.sourceTasks) { 73 | const task = page.locator(`g[data-test="task ${taskName}"]`); 74 | await expect(task).toBeVisible(); 75 | } 76 | 77 | // Check the graph buttons are visible 78 | for(const buttonName of [TektonPO.zoomInButtonName, TektonPO.zoomOutButtonName, TektonPO.fitToScreenButtonName, TektonPO.resetViewButtonName]) { 79 | const button = page.getByRole('button', { name: buttonName }); 80 | await expect(button).toBeVisible(); 81 | } 82 | 83 | // Collapse the row 84 | await expandButton.click(); 85 | 86 | await expect(graph).not.toBeVisible(); 87 | } 88 | 89 | async checkPipelineRunsTable(page: Page): Promise { 90 | // Wait for the Pipeline Runs section to be visible 91 | await expect(page.getByRole('heading', { name: /pipeline runs/i })).toBeVisible(); 92 | 93 | // Find the table and on-push row 94 | const table = page.locator('table').filter({ has: page.getByRole('columnheader', { name: 'NAME' }) }); 95 | const firstRow = table.locator('tbody tr').filter({ hasText: TektonPO.onPushRowRegex }).first(); 96 | await expect(firstRow).toBeVisible(); 97 | 98 | // 1. Shield icon next to name (look for shield icon with specific path, not the expand arrow) 99 | const shieldIcon = firstRow.locator('.signed-indicator svg'); 100 | await expect(shieldIcon).toBeVisible(); 101 | 102 | // 2. Vulnerabilities are shown (look for vulnerability severity levels) 103 | await expect(firstRow.getByRole('cell').filter({ hasText: TektonPO.vulnerabilitySeverityRegex }).first()).toBeVisible(); 104 | 105 | // 3. Status is Succeeded and has a tick 106 | await expect(firstRow).toContainText(CiPo.statusSucceededText); 107 | await expect(firstRow.locator(`[data-testid="${CiPo.statusOkTestId}"]`)).toBeVisible(); 108 | 109 | // 4. Started column has a date and time format (look for date pattern in any cell) 110 | await expect(firstRow.getByRole('cell').filter({ hasText: /\d{1,2}\/\d{1,2}\/\d{4}/ })).toBeVisible(); 111 | 112 | // 5. Task status has a visible bar (look for progress elements) 113 | // Skipping until https://redhat.atlassian.net/browse/SSCUI-82 is fixed 114 | //await expect(firstRow.locator('[role="progressbar"], [class*="bar"], [data-testid*="progress"]').first()).toBeVisible(); 115 | 116 | // 6. Duration is visible (e.g. `3 minutes 20 seconds`, `3 minutes`, or `45 seconds`) 117 | await expect(firstRow.getByRole('cell').filter({ hasText: TektonPO.durationRegex })).toBeVisible(); 118 | } 119 | 120 | async checkActions(page: Page): Promise { 121 | // Find the Pipeline Runs table specifically to avoid conflicts with other tables (e.g., ArgoCD) 122 | const pipelineRunsTable = page.locator('table').filter({ has: page.getByRole('columnheader', { name: 'NAME' }) }); 123 | 124 | // Scroll to the action column header within the Pipeline Runs table 125 | await pipelineRunsTable.getByRole('columnheader', { name: TektonPO.actionsColumnHeader, exact: true }).scrollIntoViewIfNeeded(); 126 | 127 | const onPushRow = pipelineRunsTable.locator('tr').filter({ hasText: TektonPO.onPushRowRegex }).first(); 128 | 129 | await this.checkActionButtons(onPushRow); 130 | await this.checkLogsPopup(page, onPushRow); 131 | await this.checkSBOMpopup(page, onPushRow); 132 | await this.checkViewOutputPopup(page, onPushRow); 133 | await this.checkGraph(page, onPushRow); 134 | } 135 | 136 | /** 137 | * Verifies that registry links in both Image Scan and Image Check tabs 138 | * are actual clickable links that lead to an external registry (outside Developer Hub). 139 | * 140 | * This method: 141 | * 1. Opens the "View Output" dialog for an on-push pipeline row 142 | * 2. Checks Image Scan tab - verifies the image link is a real link (not just text) 143 | * 3. Checks Image Check tab - verifies the image link is a real link (not just text) 144 | * 4. Confirms links open in new tabs and navigate outside Developer Hub 145 | * 146 | * @param page - Playwright Page object 147 | */ 148 | public async checkImageRegistryLinks(page: Page): Promise { 149 | // Find the Pipeline Runs table (same pattern as checkActions) 150 | const pipelineRunsTable = page.locator('table').filter({ has: page.getByRole('columnheader', { name: 'NAME' }) }); 151 | 152 | // Scroll to the action column header 153 | await pipelineRunsTable.getByRole('columnheader', { name: TektonPO.actionsColumnHeader, exact: true }).scrollIntoViewIfNeeded(); 154 | 155 | // Find the on-push row within the table 156 | const onPushRow = pipelineRunsTable.locator('tr').filter({ hasText: TektonPO.onPushRowRegex }).first(); 157 | await expect(onPushRow).toBeVisible(); 158 | 159 | // Click "View Output" button 160 | const viewOutputButton = onPushRow.getByTestId(CommonPO.viewOutputIconTestId); 161 | await viewOutputButton.click(); 162 | 163 | const dialog = page.getByRole('dialog'); 164 | await expect(dialog).toBeVisible(); 165 | 166 | const closeButton = dialog.getByTestId(CommonPO.closeIconTestId); 167 | await closeButton.click(); 168 | } 169 | } ```