-
Notifications
You must be signed in to change notification settings - Fork 702
CONSOLE-5233: Playwright-test-migration-for-console/app #16449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,4 +12,5 @@ Godeps | |
| dynamic-demo-plugin | ||
| .eslintrc.js | ||
| tsconfig.json | ||
| e2e/.eslintrc.json | ||
| e2e/tsconfig.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "rules": { | ||
| "testing-library/prefer-screen-queries": "off" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { expect } from '@playwright/test'; | ||
| import type { Locator } from '@playwright/test'; | ||
|
|
||
| import BasePage from './base-page'; | ||
|
|
||
| export class DetailsPage extends BasePage { | ||
| private readonly pageHeading = this.page.locator('[data-test="page-heading"]'); | ||
| private readonly resourceTitle = this.page.locator('[data-test-id="resource-title"]'); | ||
| private readonly skeletonView = this.page.getByTestId('skeleton-detail-view'); | ||
| private readonly actionsMenuButton = this.page.locator('[data-test-id="actions-menu-button"]'); | ||
| readonly breadcrumbLink0 = this.page.locator('[data-test-id="breadcrumb-link-0"]'); | ||
| readonly statusPopoverButton = this.page.getByTestId('popover-status-button'); | ||
| readonly enableAutoscaleButton = this.page.getByTestId('enable-autoscale'); | ||
| readonly xtermViewport = this.page.locator('.xterm-viewport'); | ||
| readonly resourcesSuccessMessage = this.page.getByTestId('resources-successfully-created'); | ||
| readonly eventTotals = this.page.getByTestId('event-totals'); | ||
| admissionWarning(testId: string): Locator { | ||
| return this.page.getByTestId(testId); | ||
| } | ||
|
|
||
| debugContainerLink(containerName?: string): Locator { | ||
| const testId = containerName | ||
| ? `popup-debug-container-link-${containerName}` | ||
| : 'debug-container-link'; | ||
| return this.page.getByTestId(testId); | ||
| } | ||
|
|
||
| async titleShouldContain(title: string): Promise<void> { | ||
| await this.pageHeading.waitFor({ state: 'visible', timeout: 30_000 }); | ||
| await expect(this.pageHeading).toContainText(title, { timeout: 30_000 }); | ||
| } | ||
|
|
||
| async sectionHeaderShouldExist(sectionHeading: string): Promise<void> { | ||
| await expect( | ||
| this.page.locator(`[data-test-section-heading="${sectionHeading}"]`), | ||
| ).toBeVisible(); | ||
| } | ||
|
|
||
| async isLoaded(): Promise<void> { | ||
| await expect(this.skeletonView).toBeHidden({ timeout: 30_000 }); | ||
| await this.resourceTitle.waitFor({ state: 'visible', timeout: 30_000 }); | ||
| await expect(this.resourceTitle).not.toBeEmpty(); | ||
| } | ||
|
|
||
| async selectTab(name: string): Promise<void> { | ||
| const tab = this.page.locator(`[data-test-id="horizontal-link-${name}"]`); | ||
| await this.robustClick(tab); | ||
| } | ||
|
|
||
| async clickPageActionFromDropdown(actionID: string): Promise<void> { | ||
| await this.robustClick(this.actionsMenuButton); | ||
| const action = this.page.locator(`[data-test-action="${actionID}"]:not([disabled])`); | ||
| await this.robustClick(action); | ||
| } | ||
|
|
||
| async clickBreadcrumb(): Promise<void> { | ||
| await this.robustClick(this.breadcrumbLink0); | ||
| } | ||
|
|
||
| async clickStatusPopover(): Promise<void> { | ||
| await this.robustClick(this.statusPopoverButton, { timeout: 60_000 }); | ||
| } | ||
|
|
||
| async clickDebugContainerLink(containerName?: string): Promise<void> { | ||
| await this.robustClick(this.debugContainerLink(containerName)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,161 @@ | ||||||||||||||||||||||
| import { expect } from '@playwright/test'; | ||||||||||||||||||||||
| import type { Locator } from '@playwright/test'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import BasePage from './base-page'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export class ListPage extends BasePage { | ||||||||||||||||||||||
| private readonly heading = this.page.locator('[data-test="page-heading"] h1'); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async titleShouldHaveText(title: string): Promise<void> { | ||||||||||||||||||||||
| await expect(this.heading).toContainText(title); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // --- Resource row helpers (older VirtualizedTable) --- | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async rowsShouldExist(resourceName: string): Promise<void> { | ||||||||||||||||||||||
| await expect( | ||||||||||||||||||||||
| this.page.locator('[data-test-rows="resource-row"]').filter({ hasText: resourceName }), | ||||||||||||||||||||||
| ).toBeVisible({ timeout: 60_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async rowsShouldNotExist(resourceName: string): Promise<void> { | ||||||||||||||||||||||
| await expect(this.page.locator(`[data-test-id="${resourceName}"]`)).toBeHidden({ | ||||||||||||||||||||||
| timeout: 90_000, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async rowsClickKebabAction(resourceName: string, actionName: string): Promise<void> { | ||||||||||||||||||||||
| const row = this.page | ||||||||||||||||||||||
| .locator('[data-test-rows="resource-row"]') | ||||||||||||||||||||||
| .filter({ hasText: resourceName }); | ||||||||||||||||||||||
| const kebab = row.locator('[data-test-id="kebab-button"]'); | ||||||||||||||||||||||
| await this.robustClick(kebab); | ||||||||||||||||||||||
| const action = this.page.locator(`[data-test-action="${actionName}"]:not([disabled])`); | ||||||||||||||||||||||
| await this.robustClick(action); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async rowsClickStatusButton(resourceName: string): Promise<void> { | ||||||||||||||||||||||
| const row = this.page | ||||||||||||||||||||||
| .locator('[data-test-rows="resource-row"]') | ||||||||||||||||||||||
| .filter({ hasText: resourceName }); | ||||||||||||||||||||||
| const statusButton = row.getByTestId('popover-status-button'); | ||||||||||||||||||||||
| await this.robustClick(statusButton, { timeout: 60_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async filterByStatus(status: string): Promise<void> { | ||||||||||||||||||||||
| const filterToggle = this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]'); | ||||||||||||||||||||||
| if (await filterToggle.isVisible().catch(() => false)) { | ||||||||||||||||||||||
| await this.robustClick(filterToggle); | ||||||||||||||||||||||
| const filterItem = this.page.locator( | ||||||||||||||||||||||
| `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${status}"]`, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| await this.robustClick(filterItem); | ||||||||||||||||||||||
| await this.robustClick(filterToggle); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| const filterDropdownToggle = this.page.locator( | ||||||||||||||||||||||
| '[data-test-id="filter-dropdown-toggle"] button', | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| if (await filterDropdownToggle.isVisible().catch(() => false)) { | ||||||||||||||||||||||
| await this.robustClick(filterDropdownToggle); | ||||||||||||||||||||||
| await this.page.locator(`#${status}`).click(); | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unvalidated CSS ID selector construction. Constructing a CSS ID selector with 🛡️ Proposed fix using attribute selector- await this.page.locator(`#${status}`).click();
+ await this.page.locator(`[id="${status}"]`).click();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| await this.robustClick(filterDropdownToggle); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // --- DataView row helpers (ConsoleDataView) --- | ||||||||||||||||||||||
| // These use generic table locators that work even if data-test attributes | ||||||||||||||||||||||
| // are not forwarded to the DOM by PatternFly DataView components. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private dvCell(resourceName: string, cellName = 'name'): Locator { | ||||||||||||||||||||||
| return this.page.locator(`[data-test="data-view-cell-${resourceName}-${cellName}"]`); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private dvRow(resourceName: string): Locator { | ||||||||||||||||||||||
| return this.page.locator('table tbody tr').filter({ | ||||||||||||||||||||||
| has: this.page.getByRole('link', { name: resourceName, exact: true }), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvRowsShouldBeLoaded(): Promise<void> { | ||||||||||||||||||||||
| await expect(this.page.getByTestId('data-view-table')).toBeVisible({ timeout: 60_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private async resolveRow(resourceName: string): Promise<Locator> { | ||||||||||||||||||||||
| const cell = this.dvCell(resourceName); | ||||||||||||||||||||||
| if (await cell.isVisible({ timeout: 5_000 }).catch(() => false)) { | ||||||||||||||||||||||
| return cell.locator('xpath=ancestor::tr'); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return this.dvRow(resourceName); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvRowsShouldExist(resourceName: string, cellName = 'name'): Promise<void> { | ||||||||||||||||||||||
| const cell = this.dvCell(resourceName, cellName); | ||||||||||||||||||||||
| const row = this.dvRow(resourceName); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| await expect(cell).toBeVisible({ timeout: 30_000 }); | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| await this.page.reload({ waitUntil: 'domcontentloaded' }); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| await expect(cell).toBeVisible({ timeout: 30_000 }); | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| await expect(row).toBeVisible({ timeout: 30_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+92
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excessive retry logic with page reload masks underlying issues. Triple-nested try-catch with a full page reload is a strong signal that the underlying selectors or timing assumptions are fragile. This pattern will make test failures non-deterministic and harder to diagnose. Consider:
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvRowsShouldNotExist(resourceName: string): Promise<void> { | ||||||||||||||||||||||
| const cell = this.dvCell(resourceName); | ||||||||||||||||||||||
| await expect(cell).toBeHidden({ timeout: 90_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvRowsCountShouldBe(count: number): Promise<void> { | ||||||||||||||||||||||
| await expect(this.page.locator('table tbody tr')).toHaveCount(count, { timeout: 60_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvRowsClickKebabAction(resourceName: string, actionName: string): Promise<void> { | ||||||||||||||||||||||
| const row = await this.resolveRow(resourceName); | ||||||||||||||||||||||
| const kebab = row.locator('[data-test-id="kebab-button"]'); | ||||||||||||||||||||||
| await this.robustClick(kebab); | ||||||||||||||||||||||
| const action = this.page.locator(`[data-test-action="${actionName}"]:not([disabled])`); | ||||||||||||||||||||||
| await this.robustClick(action); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvRowsClickStatusButton(resourceName: string): Promise<void> { | ||||||||||||||||||||||
| const row = await this.resolveRow(resourceName); | ||||||||||||||||||||||
| const statusButton = row.getByTestId('popover-status-button'); | ||||||||||||||||||||||
| await this.robustClick(statusButton, { timeout: 60_000 }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvFilterByName(name: string): Promise<void> { | ||||||||||||||||||||||
| const filters = this.page.locator('[data-ouia-component-id="DataViewFilters"]'); | ||||||||||||||||||||||
| await this.robustClick(filters.locator('.pf-v6-c-menu-toggle').first()); | ||||||||||||||||||||||
| await this.robustClick( | ||||||||||||||||||||||
| this.page.locator('.pf-v6-c-menu__list-item').filter({ hasText: 'Name' }), | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| const input = this.page.locator('[aria-label="Filter by name"]'); | ||||||||||||||||||||||
| await input.clear(); | ||||||||||||||||||||||
| await input.fill(name); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async dvFilterBy(filterName: string, checkboxLabel: string): Promise<void> { | ||||||||||||||||||||||
| await this.dvRowsShouldBeLoaded(); | ||||||||||||||||||||||
| const filters = this.page.locator('[data-ouia-component-id="DataViewFilters"]'); | ||||||||||||||||||||||
| await this.robustClick(filters.locator('.pf-v6-c-menu-toggle').first()); | ||||||||||||||||||||||
| await this.robustClick( | ||||||||||||||||||||||
| this.page.locator('.pf-v6-c-menu__list-item').filter({ hasText: filterName }), | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]')); | ||||||||||||||||||||||
| const filterItem = this.page.locator( | ||||||||||||||||||||||
| `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| await expect(filterItem).toBeVisible(); | ||||||||||||||||||||||
| await this.robustClick(filterItem); | ||||||||||||||||||||||
| await expect(this.page).toHaveURL(new RegExp(`=${checkboxLabel}`), { timeout: 10_000 }); | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regular expression constructed from user input enables ReDoS. Constructing Escape the input or use a static pattern with string matching. 🔒 Proposed fix to escape regex input+ // Helper to escape regex special characters
+ private escapeRegex(str: string): string {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
async dvFilterBy(filterName: string, checkboxLabel: string): Promise<void> {
// ... existing code ...
- await expect(this.page).toHaveURL(new RegExp(`=${checkboxLabel}`), { timeout: 10_000 });
+ await expect(this.page).toHaveURL(new RegExp(`=${this.escapeRegex(checkboxLabel)}`), { timeout: 10_000 });
await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]'));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]')); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| get clickCreateYAMLButton(): Locator { | ||||||||||||||||||||||
| return this.page.getByTestId('item-create'); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+158
to
+160
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Misleading getter name implies action but returns locator. The getter is named Rename to ♻️ Proposed rename- get clickCreateYAMLButton(): Locator {
+ get createYAMLButton(): Locator {
return this.page.getByTestId('item-create');
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||
| import BasePage from './base-page'; | ||||||
|
|
||||||
| export class LoginPage extends BasePage { | ||||||
| private readonly loginButton = this.page.locator('[data-test-id="login"]'); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test-id attribute mismatch will break selector. Line 4 uses
If the HTML uses only 🔧 Proposed fix- private readonly loginButton = this.page.locator('[data-test-id="login"]');
+ private readonly loginButton = this.page.getByTestId('login');This ensures consistent use of the configured test-id attribute. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| private readonly usernameInput = this.page.locator('#inputUsername'); | ||||||
| private readonly passwordInput = this.page.locator('#inputPassword'); | ||||||
| private readonly submitButton = this.page.locator('button[type="submit"]'); | ||||||
| private readonly userDropdownToggle = this.page.getByTestId('user-dropdown-toggle'); | ||||||
|
|
||||||
| providerButton(provider: string) { | ||||||
| return this.page.getByText(provider, { exact: true }); | ||||||
| } | ||||||
|
|
||||||
| async loginAs(provider: string, username: string, password: string): Promise<boolean> { | ||||||
| const baseURL = process.env.WEB_CONSOLE_URL || 'http://localhost:9000'; | ||||||
| await this.page.goto(baseURL, { timeout: 90_000, waitUntil: 'domcontentloaded' }); | ||||||
|
|
||||||
| const authDisabled = await this.page | ||||||
| .evaluate(() => (window as any).SERVER_FLAGS?.authDisabled) | ||||||
| .catch(() => false); | ||||||
|
|
||||||
| if (authDisabled) { | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| const providerBtn = this.providerButton(provider); | ||||||
| await this.loginButton | ||||||
| .or(this.usernameInput) | ||||||
| .or(providerBtn) | ||||||
| .first() | ||||||
| .waitFor({ state: 'visible', timeout: 30_000 }); | ||||||
|
|
||||||
| if ((await providerBtn.count()) > 0 && (await providerBtn.isVisible())) { | ||||||
| await providerBtn.click(); | ||||||
| await this.usernameInput.waitFor({ state: 'visible', timeout: 30_000 }); | ||||||
| } | ||||||
|
|
||||||
| await this.usernameInput.fill(username); | ||||||
| await this.passwordInput.fill(password); | ||||||
| await this.submitButton.click(); | ||||||
| await this.userDropdownToggle.waitFor({ state: 'visible', timeout: 60_000 }); | ||||||
| return true; | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { expect } from '@playwright/test'; | ||
| import type { Locator } from '@playwright/test'; | ||
|
|
||
| import BasePage from './base-page'; | ||
|
|
||
| export class MachineConfigPage extends BasePage { | ||
| readonly configFilePath = this.page.getByTestId('config-file-path-0'); | ||
| readonly copyToClipboard = this.page.locator('.co-copy-to-clipboard__text'); | ||
|
|
||
| sectionHeading(heading: string): Locator { | ||
| return this.page.locator(`[data-test-section-heading="${heading}"]`); | ||
| } | ||
|
|
||
| errorHeading(text: string): Locator { | ||
| return this.page.getByText(text); | ||
| } | ||
|
|
||
| async checkConfigFileDetails(mode: number, overwrite: boolean, content: string): Promise<void> { | ||
| await this.configFilePath.scrollIntoViewIfNeeded(); | ||
| await this.page.locator('button[aria-label="Info"]').first().click(); | ||
| const descriptionList = this.page.locator('[class*="description-list"]'); | ||
| await expect(descriptionList.getByText(String(mode), { exact: true })).toBeVisible(); | ||
| await expect(descriptionList.getByText(String(overwrite), { exact: true })).toBeVisible(); | ||
| const decoded = decodeURIComponent(content) | ||
| .replace(/^(data:,)/, '') | ||
| .slice(0, 30); | ||
| const codeBlock = this.page.locator('code').first(); | ||
| await expect(codeBlock).toContainText(decoded); | ||
| } | ||
|
Comment on lines
+18
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multiple selector fragilities and magic numbers.
🛡️ Recommended improvements async checkConfigFileDetails(mode: number, overwrite: boolean, content: string): Promise<void> {
await this.configFilePath.scrollIntoViewIfNeeded();
- await this.page.locator('button[aria-label="Info"]').first().click();
+ // Scope to the config file section to avoid ambiguity
+ await this.configFilePath.locator('..').getByRole('button', { name: 'Info' }).click();
- const descriptionList = this.page.locator('[class*="description-list"]');
+ const descriptionList = this.page.getByTestId('config-file-description-list');
- await expect(descriptionList.getByText(String(mode), { exact: true })).toBeVisible();
- await expect(descriptionList.getByText(String(overwrite), { exact: true })).toBeVisible();
+ await expect(descriptionList.getByTestId('config-file-mode')).toHaveText(String(mode));
+ await expect(descriptionList.getByTestId('config-file-overwrite')).toHaveText(String(overwrite));
const decoded = decodeURIComponent(content)
.replace(/^(data:,)/, '')
- .slice(0, 30);
- const codeBlock = this.page.locator('code').first();
+ .slice(0, 30); // Truncate to avoid full-content comparison; adjust if needed
+ const codeBlock = this.configFilePath.locator('..').locator('code').first();
await expect(codeBlock).toContainText(decoded);
}Note: Requires adding 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { expect } from '@playwright/test'; | ||
|
|
||
| import BasePage from './base-page'; | ||
|
|
||
| export class MastheadPage extends BasePage { | ||
| readonly loadingIndicator = this.page.getByTestId('loading-indicator'); | ||
| readonly globalNotifications = this.page.getByTestId('global-notifications'); | ||
|
|
||
| async usernameShouldHaveText(text: string): Promise<void> { | ||
| const toggle = this.page.getByTestId('user-dropdown-toggle'); | ||
| await expect(toggle).toHaveText(text); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||||
| import { expect } from '@playwright/test'; | ||||||||||
|
|
||||||||||
| import BasePage from './base-page'; | ||||||||||
|
|
||||||||||
| export class ModalPage extends BasePage { | ||||||||||
| private get cancelButton() { | ||||||||||
| return this.page.locator('[data-test-id="modal-cancel-action"]'); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test-id attribute mismatch will break selector. Line 7 uses 🔧 Proposed fix- private get cancelButton() {
- return this.page.locator('[data-test-id="modal-cancel-action"]');
- }
+ private get cancelButton() {
+ return this.page.getByTestId('modal-cancel-action');
+ }This ensures consistent use of the configured test-id attribute. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } | ||||||||||
|
|
||||||||||
| private get submitButton() { | ||||||||||
| return this.page.locator('button[type=submit]'); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async shouldBeOpened(): Promise<void> { | ||||||||||
| await this.cancelButton.scrollIntoViewIfNeeded(); | ||||||||||
| await expect(this.cancelButton).toBeVisible({ timeout: 20_000 }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async shouldBeClosed(): Promise<void> { | ||||||||||
| await expect(this.cancelButton).toBeHidden(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async submit(): Promise<void> { | ||||||||||
| await this.submitButton.click(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async cancel(): Promise<void> { | ||||||||||
| await this.cancelButton.click(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async submitShouldBeDisabled(): Promise<void> { | ||||||||||
| await expect(this.submitButton).toBeDisabled(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async submitShouldBeEnabled(): Promise<void> { | ||||||||||
| await expect(this.submitButton).toBeEnabled(); | ||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent selector strategy between existence checks.
rowsShouldExistfilters bydata-test-rows="resource-row"+ text match, whilerowsShouldNotExistusesdata-test-id="${resourceName}". This mismatch means you're checking different DOM elements for presence vs. absence, which could produce false positives if the row exists but thedata-test-idattribute is missing or named differently.Align both methods to use the same locator strategy. If
data-test-idis more reliable, use it for both; otherwise use thedata-test-rows+ text filter for both.🔧 Proposed fix to align selector strategies
async rowsShouldExist(resourceName: string): Promise<void> { await expect( - this.page.locator('[data-test-rows="resource-row"]').filter({ hasText: resourceName }), + this.page.locator(`[data-test-id="${resourceName}"]`), ).toBeVisible({ timeout: 60_000 }); }📝 Committable suggestion
🤖 Prompt for AI Agents