# 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('button', { name: /Sign In|Log In/i }) Expected: visible Timeout: 15000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 15000ms - waiting for getByRole('button', { name: /Sign In|Log In/i }) ``` # 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-eyqdzkla" [level=1] [ref=e119]: - generic [ref=e121]: backend-tests-go-eyqdzkla - 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] - generic [ref=e135]: - generic [ref=e136]: - generic [ref=e138]: - paragraph [ref=e139]: Let's get you started with Developer Hub - paragraph [ref=e140]: We'll guide you through a few quick steps - separator [ref=e141] - list [ref=e143]: - button "Expand Set up authentication details" [ref=e145] [cursor=pointer]: - img [ref=e148] - generic [ref=e154]: Set up authentication - button "Expand item" [ref=e155]: - img [ref=e156] - button "Expand Configure RBAC details" [ref=e159] [cursor=pointer]: - img [ref=e162] - generic [ref=e165]: Configure RBAC - button "Expand item" [ref=e166]: - img [ref=e167] - button "Expand Configure Git details" [ref=e170] [cursor=pointer]: - img [ref=e173] - generic [ref=e176]: Configure Git - button "Expand item" [ref=e177]: - img [ref=e178] - button "Expand Manage plugins details" [ref=e181] [cursor=pointer]: - img [ref=e184] - generic [ref=e187]: Manage plugins - button "Expand item" [ref=e188]: - img [ref=e189] - generic [ref=e191]: - progressbar [ref=e192] - generic [ref=e194]: - paragraph [ref=e195]: Not started - button "Hide" [ref=e196] [cursor=pointer]: Hide ``` # Test source ```ts 1 | /** 2 | * GitHub UI Plugin 3 | * 4 | * Implements UI automation for GitHub-specific operations. 5 | * Handles GitHub login flow including 2FA authentication. 6 | */ 7 | 8 | import { GitPlugin } from './gitUiInterface'; 9 | import { expect, Page } from '@playwright/test'; 10 | import { loadFromEnv } from '../../../utils/util'; 11 | import { DHLoginPO, GhLoginPO } from '../../page-objects/loginPo'; 12 | import { GitPO } from '../../page-objects/commonPo'; 13 | import { TOTP, NobleCryptoPlugin, ScureBase32Plugin, createGuardrails } from 'otplib'; 14 | import retry from 'async-retry'; 15 | import { GitUi } from './gitUi'; 16 | import { AuthUi } from '../auth/authUi'; 17 | import { blurLocator } from '../../commonUi'; 18 | import { LoggerFactory } from '../../../logger/logger'; 19 | import type { Logger } from '../../../logger/logger'; 20 | import type { Git } from '../../../rhtap/core/integration/git'; 21 | 22 | export class GithubUiPlugin extends GitUi implements GitPlugin, AuthUi { 23 | protected readonly logger: Logger; 24 | 25 | constructor(git: Git) { 26 | super(git); 27 | this.logger = LoggerFactory.getLogger(GithubUiPlugin); 28 | } 29 | private totp = new TOTP({ 30 | crypto: new NobleCryptoPlugin(), 31 | base32: new ScureBase32Plugin(), 32 | guardrails: createGuardrails({ MIN_SECRET_BYTES: 1 }), 33 | }); 34 | 35 | /** 36 | * Performs GitHub login through the Developer Hub UI. 37 | * Handles the complete login flow including: 38 | * - Initial sign-in button click 39 | * - GitHub credentials input 40 | * - 2FA authentication 41 | * - Authorization confirmation 42 | * 43 | * @param page - Playwright Page object for UI interactions 44 | */ 45 | async login(page: Page): Promise { 46 | const button = page.getByRole('button', { name: DHLoginPO.signInButtonName }); > 47 | await expect(button).toBeVisible({ timeout: 15000 }) | ^ Error: expect(locator).toBeVisible() failed 48 | 49 | const authorizeAppPagePromise = page.context().waitForEvent('page'); 50 | await button.click(); 51 | const authorizeAppPage = await authorizeAppPagePromise; 52 | await authorizeAppPage.bringToFront(); 53 | await authorizeAppPage.waitForLoadState(); 54 | await authorizeAppPage.locator(GhLoginPO.githubLoginField).fill(loadFromEnv("GH_USERNAME")); 55 | await authorizeAppPage.locator(GhLoginPO.githubPasswordField).fill(loadFromEnv('GH_PASSWORD')); 56 | await authorizeAppPage.locator(GhLoginPO.githubSignInButton).click(); 57 | await authorizeAppPage.waitForLoadState(); 58 | 59 | const twoFactorField = authorizeAppPage.locator(GhLoginPO.github2FAField); 60 | 61 | // Retry inserting 2FA token for cases when it was already used 62 | const maxRetries = 5; 63 | const timeout = 30000; // token resets every 30 seconds 64 | await retry( 65 | async (): Promise => { 66 | const token = await this.getGitHub2FAOTP(); 67 | // blur the field to avoid 2FA token being captured by screenshot or video 68 | await blurLocator(twoFactorField); 69 | await twoFactorField.fill(token); 70 | // The field should detach after successful auth 71 | await twoFactorField.waitFor({ state: 'detached', timeout: 5000 }); 72 | }, 73 | { 74 | retries: maxRetries, 75 | minTimeout: timeout, 76 | maxTimeout: timeout, 77 | onRetry: (_error: Error, attemptNumber: number) => { 78 | this.logger.warn(`2FA token entry failed, retrying (attempt ${attemptNumber}/${maxRetries}), waiting ${timeout}ms`); 79 | }, 80 | } 81 | ); 82 | 83 | const authorizeButton = authorizeAppPage.getByRole('button', { name: 'authorize' }); 84 | 85 | // Click authorize button if app is not authorized, skip otherwise 86 | try { 87 | await authorizeButton.waitFor({ state: 'visible', timeout: 3000 }); 88 | await authorizeButton.click(); 89 | this.logger.info('Authorization button clicked successfully'); 90 | } catch (error: unknown) { 91 | if (error instanceof Error && !error.message.includes('locator.waitFor')) { 92 | throw error; 93 | } 94 | this.logger.debug('Authorization button not found or not needed, continuing...'); 95 | } 96 | } 97 | 98 | async checkViewSourceLink(page: Page): Promise { 99 | const githubLink = page.locator(`${GitPO.githubLinkSelector}:has-text("${GitPO.viewSourceLinkText}")`); 100 | await super.checkGitLink(page, githubLink); 101 | } 102 | 103 | /** 104 | * Generates a 2FA token for GitHub authentication. 105 | * Uses the TOTP secret from environment variables. 106 | * 107 | * @returns Promise resolving to the generated 2FA token 108 | */ 109 | private async getGitHub2FAOTP(): Promise { 110 | const secret = loadFromEnv("GH_SECRET"); 111 | const token = await this.totp.generate({ secret }); 112 | return token; 113 | } 114 | } 115 | ```