# 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 ``` Error: expect(locator).toBeVisible() failed Locator: getByRole('heading', { name: 'Security Information' }) Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByRole('heading', { name: 'Security Information' }) ``` # Page snapshot ```yaml - generic [ref=e2]: - 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=e117]: - paragraph [ref=e118]: component - heading "backend-tests-go-dbafwepq" [level=1] [ref=e119]: - generic [ref=e121]: backend-tests-go-dbafwepq - article [ref=e122]: - alert [ref=e123]: - 'button "Warning: Entity not found" [ref=e124] [cursor=pointer]': - generic [ref=e125]: - img [ref=e126] - 'heading "Warning: Entity not found" [level=6] [ref=e128]' - img [ref=e131] - alert [ref=e133]: - generic [ref=e134]: Need help? Visit the Quick Start Guide by clicking on this (?) icon in the header! - button "close" [ref=e136] [cursor=pointer]: - img [ref=e137] ``` # Test source ```ts 1 | import { expect, Page } from '@playwright/test'; 2 | import { BaseCIPlugin } from './baseCIPlugin'; 3 | import { GitPO } from '../../page-objects/commonPo'; 4 | import { CiPo } from '../../page-objects/ciPo'; 5 | import { LoggerFactory, Logger } from '../../../logger/logger'; 6 | 7 | export class GitlabCIPlugin extends BaseCIPlugin { 8 | private readonly logger: Logger = LoggerFactory.getLogger('GitlabCIPlugin'); 9 | 10 | constructor(name: string, registryOrg: string) { 11 | super(name, registryOrg); 12 | } 13 | 14 | // eslint-disable-next-line no-unused-vars 15 | public async checkActions(_page: Page): Promise { 16 | // GitLab CI pipeline runs are shown in the main table, no additional actions needed 17 | } 18 | 19 | public async checkPipelineRunsTable(page: Page): Promise { 20 | const overviewUrl = page.url().replace(/\/ci(?:\?.*)?$/, ''); 21 | await page.goto(overviewUrl, { timeout: 20000 }); 22 | await page.waitForLoadState('domcontentloaded'); 23 | 24 | const viewSourceLink = page.locator(`${GitPO.gitlabLinkSelector}:has-text("${GitPO.viewSourceLinkText}")`); 25 | await expect(viewSourceLink).toBeVisible({ timeout: 20000 }); 26 | 27 | const repoUrl = await viewSourceLink.getAttribute('href'); 28 | if (!repoUrl) { 29 | throw new Error('Missing repository URL on View Source link'); 30 | } 31 | 32 | const gitlabPagePromise = page.context().waitForEvent('page', { timeout: 10000 }); 33 | await viewSourceLink.click(); 34 | const gitlabPage = await gitlabPagePromise; 35 | 36 | try { 37 | await gitlabPage.waitForLoadState('domcontentloaded'); 38 | 39 | const pipelinesUrl = this.buildPipelinesUrl(repoUrl); 40 | await gitlabPage.goto(pipelinesUrl, { timeout: 20000, waitUntil: 'domcontentloaded' }); 41 | 42 | await expect(gitlabPage).toHaveURL(pipelinesUrl, { timeout: 20000 }); 43 | } finally { 44 | await gitlabPage.close(); 45 | } 46 | } 47 | 48 | private buildPipelinesUrl(repoUrl: string): string { 49 | const url = new URL(repoUrl); 50 | // Strip branch path (e.g. /-/tree/main or /-/blob/main) to get the repo root 51 | const repoPath = url.pathname.replace(/\/-\/.+$/, ''); 52 | return `${url.origin}${repoPath}/-/pipelines`; 53 | } 54 | 55 | // eslint-disable-next-line no-unused-vars 56 | public async checkImageRegistryLinks(_page: Page): Promise { 57 | this.logger.info('Skipping checkImageRegistryLinks - not applicable for GitLab CI'); 58 | } 59 | 60 | public async checkSecurityInformation(page: Page): Promise { 61 | // Check the Security Information heading is visible 62 | const heading = page.getByRole('heading', { name: CiPo.securityInformationHeading }); > 63 | await expect(heading).toBeVisible({ timeout: 10000 }); | ^ Error: expect(locator).toBeVisible() failed 64 | 65 | // Click the "Gitlab CI" tab to activate it before checking table data 66 | const gitlabTab = page.getByRole('tab', { name: CiPo.gitlabCITabName }); 67 | await expect(gitlabTab).toBeVisible(); 68 | await gitlabTab.click(); 69 | await expect(gitlabTab).toHaveAttribute('aria-selected', 'true'); 70 | 71 | // Scope column header checks to the security table 72 | const table = page.getByRole('table').filter({ has: page.getByRole('columnheader', { name: CiPo.securityTableColumns[0] }) }); 73 | await expect(table).toBeVisible({ timeout: 15000 }); 74 | for (const column of CiPo.securityTableColumns) { 75 | await expect(table.getByRole('columnheader', { name: column })).toBeVisible(); 76 | } 77 | 78 | // Check the table has at least one data row 79 | const rows = table.locator('tbody tr').filter({ has: page.getByRole('cell') }); 80 | await expect(rows.first()).toBeVisible({ timeout: 15000 }); 81 | const rowCount = await rows.count(); 82 | this.logger.info(`Security Information table has ${rowCount} data row(s)`); 83 | 84 | // Find a row with Type "Build" (don't assume ordering) 85 | const buildRow = rows.filter({ has: page.getByRole('cell', { name: 'Build', exact: true }) }); 86 | await expect(buildRow.first()).toBeVisible({ timeout: 5000 }); 87 | 88 | const pipelineRunId = await buildRow.first().getByRole('cell').first().innerText(); 89 | expect(pipelineRunId).toBeTruthy(); 90 | this.logger.info(`First Build pipeline run ID: ${pipelineRunId}`); 91 | 92 | // Open View Logs dialog and verify its content 93 | const viewLogsButton = buildRow.first().getByTestId(CiPo.viewLogsButtonTestId); 94 | await viewLogsButton.click(); 95 | 96 | const dialog = page.getByRole('dialog'); 97 | await expect(dialog).toBeVisible({ timeout: 10000 }); 98 | 99 | // Verify the dialog heading contains the pipeline run ID 100 | const dialogHeading = dialog.getByRole('heading'); 101 | await expect(dialogHeading).toContainText(pipelineRunId); 102 | 103 | // Close the dialog 104 | await page.keyboard.press('Escape'); 105 | await expect(dialog).not.toBeVisible({ timeout: 5000 }); 106 | 107 | this.logger.info('Security Information verification completed for GitLab CI'); 108 | } 109 | } 110 | ```