# 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 | }
```