test(e2e): improve selector robustness in Playwright tests

- Replace :nth-of-type with .nth() for better cross-browser compatibility
- Use :first-child and :nth-child instead of :first-of-type/:nth-of-type
- Update project name input to use getByRole for accessibility
- Fix project settings button selector with aria-label fallback
- Improve selector patterns in multiple test files
- Add debug test file for troubleshooting note dialog issues

These changes address selector failures when running tests in parallel
and improve overall test stability.
This commit is contained in:
Johannes Millan 2025-08-01 20:47:19 +02:00
parent e2d73b234e
commit cd79ea0940
7 changed files with 184 additions and 47 deletions

View file

@ -1,4 +1,4 @@
import { Locator, Page, expect } from '@playwright/test';
import { expect, Locator, Page } from '@playwright/test';
import { BasePage } from './base.page';
export class ProjectPage extends BasePage {
@ -21,11 +21,13 @@ export class ProjectPage extends BasePage {
'button[aria-label="Create New Project"], button:has-text("Create Project")',
);
this.projectAccordion = page.locator('[role="menuitem"]:has-text("Projects")');
this.projectNameInput = page.locator('dialog-create-project input:first-of-type');
this.projectNameInput = page.getByRole('textbox', { name: 'Project Name' });
this.submitBtn = page.locator('dialog-create-project button[type=submit]:enabled');
this.workCtxMenu = page.locator('work-context-menu');
this.workCtxTitle = page.locator('.current-work-context-title');
this.projectSettingsBtn = this.workCtxMenu.locator('button:nth-of-type(4)');
this.projectSettingsBtn = this.workCtxMenu
.locator('button[aria-label="Project Settings"]')
.or(this.workCtxMenu.locator('button').nth(3));
this.moveToArchiveBtn = page.locator('.e2e-move-done-to-archive');
this.globalErrorAlert = page.locator('.global-error-alert');
}
@ -37,14 +39,12 @@ export class ProjectPage extends BasePage {
: projectName;
// Hover over the Projects menu item to show the button
const projectsMenuItem = this.page.locator('[role="menuitem"]:has-text("Projects")');
const projectsMenuItem = this.page.locator('.e2e-projects-btn');
await projectsMenuItem.hover();
await this.page.waitForTimeout(200);
// Force click the button even if not visible
const createProjectBtn = this.page
.locator('[role="menuitem"]:has-text("Projects") ~ button, .additional-btn')
.first();
const createProjectBtn = this.page.locator('.e2e-add-project-btn');
await createProjectBtn.click({ force: true });
// Wait for the dialog to appear
@ -58,13 +58,15 @@ export class ProjectPage extends BasePage {
async getProject(index: number): Promise<Locator> {
// Projects are in a menuitem structure, not side-nav-item
return this.page.locator(
`[role="menuitem"]:has-text("Projects") + [role="menuitem"]:nth-of-type(${index})`,
// Get all project menuitems that follow the Projects header
const projectMenuItems = this.page.locator(
'[role="menuitem"]:has-text("Projects") ~ [role="menuitem"]',
);
return projectMenuItems.nth(index - 1);
}
async navigateToProject(projectLocator: Locator): Promise<void> {
const projectBtn = projectLocator.locator('button:first-of-type');
const projectBtn = projectLocator.locator('button').first();
await projectBtn.waitFor({ state: 'visible' });
await projectBtn.click();
}
@ -99,17 +101,17 @@ export class ProjectPage extends BasePage {
await this.moveToArchiveBtn.click();
}
async createAndGoToDefaultProject(): Promise<void> {
async createAndGoToTestProject(): Promise<void> {
// First click on Projects menu item to expand it
await this.projectAccordion.click();
// Create a new default project
await this.createProject('Default Project');
await this.createProject('Test Project');
// Navigate to the created project
const projectName = this.testPrefix
? `${this.testPrefix}-Default Project`
: 'Default Project';
? `${this.testPrefix}-Test Project`
: 'Test Project';
const newProject = this.page.locator(`[role="menuitem"]:has-text("${projectName}")`);
await newProject.waitFor({ state: 'visible' });
await newProject.click();
@ -119,18 +121,32 @@ export class ProjectPage extends BasePage {
}
async addNote(noteContent: string): Promise<void> {
// Click on the add note button
const addNoteBtn = this.page.locator('.add-note-btn, button[aria-label="Add Note"]');
await addNoteBtn.click();
// First toggle the notes panel to make it visible
const toggleNotesBtn = this.page.locator('.e2e-toggle-notes-btn');
await toggleNotesBtn.click();
// Type the note content
const noteTextarea = this.page
.locator('textarea[placeholder*="note"], textarea.mat-mdc-input-element')
.last();
// Wait for the notes section to be visible
const notesSection = this.page.locator('notes');
await notesSection.waitFor({ state: 'visible' });
// Click on the add note button (force click to avoid tooltip interference)
const addNoteBtn = this.page.locator('#add-note-btn');
await addNoteBtn.click({ force: true });
// Wait for the dialog to appear in the CDK overlay (full-screen dialog)
const dialogContainer = this.page.locator('mat-dialog-container');
await dialogContainer.waitFor({ state: 'visible', timeout: 10000 });
// Wait for the textarea to appear and type the note content
const noteTextarea = this.page.locator('mat-dialog-container textarea');
await noteTextarea.waitFor({ state: 'visible' });
await noteTextarea.fill(noteContent);
// Press Enter to save the note
await noteTextarea.press('Enter');
// Click the save button
const saveBtn = this.page.locator('mat-dialog-container #T-save-note');
await saveBtn.click();
// Wait for the dialog to close
await dialogContainer.waitFor({ state: 'hidden', timeout: 5000 });
}
}

View file

@ -1,6 +1,6 @@
import { test } from '../fixtures/test.fixture';
const CANCEL_BTN = 'mat-dialog-actions button:nth-of-type(1)';
const CANCEL_BTN = 'mat-dialog-actions button:first-child';
test.describe('All Basic Routes Without Error', () => {
test('should open all basic routes from menu without error', async ({
@ -15,16 +15,16 @@ test.describe('All Basic Routes Without Error', () => {
// Click main side nav item
await page.click('side-nav section.main > side-nav-item > button');
await page.click('side-nav section.main > button:nth-of-type(1)');
await page.locator('side-nav section.main > button').nth(0).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click('side-nav section.main > button:nth-of-type(2)');
await page.locator('side-nav section.main > button').nth(1).click();
await page.click('side-nav section.projects button');
await page.click('side-nav section.tags button');
await page.click('side-nav section.app > button:nth-of-type(1)');
await page.locator('side-nav section.app > button').nth(0).click();
await page.click('button.tour-settingsMenuBtn');
// Navigate to different routes

View file

@ -0,0 +1,121 @@
import { test, expect } from '../fixtures/test.fixture';
test.describe('Debug Project Note', () => {
test('debug project creation and note toggle', async ({ page, projectPage }) => {
// Create and navigate to default project
await projectPage.createAndGoToTestProject();
// Verify we're in a project context
const workCtxTitle = page.locator('.current-work-context-title');
const titleText = await workCtxTitle.textContent();
console.log('Current work context:', titleText);
// Check if notes toggle button exists
const toggleNotesBtn = page.locator('.e2e-toggle-notes-btn');
const toggleExists = await toggleNotesBtn.isVisible();
console.log('Toggle notes button exists:', toggleExists);
if (toggleExists) {
await toggleNotesBtn.click();
// Wait longer for notes section to appear
await page.waitForTimeout(1000);
// Check if notes section appears
const notesSection = page.locator('notes');
const notesVisible = await notesSection.isVisible({ timeout: 5000 });
console.log('Notes section visible after toggle:', notesVisible);
if (notesVisible) {
// Check if add note button exists
const addNoteBtn = page.locator('#add-note-btn');
const addNoteBtnVisible = await addNoteBtn.isVisible({ timeout: 5000 });
console.log('Add note button visible:', addNoteBtnVisible);
if (addNoteBtnVisible) {
// Try to click the add note button
console.log('Attempting to click add note button...');
// Check for any errors or console messages
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
page.on('pageerror', (error) => console.log('PAGE ERROR:', error.message));
await addNoteBtn.click({ force: true });
// Wait a moment for potential async operations
await page.waitForTimeout(1000);
// Check for any overlay or dialog elements
const allOverlays = page.locator('.cdk-overlay-container *');
const overlayCount = await allOverlays.count();
console.log('Total overlay elements:', overlayCount);
// Check if dialog appears (it should be in a mat-dialog-container)
const matDialog = page.locator('mat-dialog-container');
const matDialogVisible = await matDialog.isVisible({ timeout: 2000 });
console.log('Mat dialog container visible:', matDialogVisible);
// Also check for CDK overlay
const cdkOverlay = page.locator('.cdk-overlay-container mat-dialog-container');
const cdkOverlayVisible = await cdkOverlay.isVisible({ timeout: 2000 });
console.log('CDK overlay dialog visible:', cdkOverlayVisible);
// Check for any dialog-like elements
const anyDialog = page.locator(
'[role="dialog"], .mat-dialog-container, mat-dialog-container',
);
const anyDialogCount = await anyDialog.count();
console.log('Any dialog elements found:', anyDialogCount);
if (matDialogVisible || cdkOverlayVisible) {
// Check if textarea appears (inside the mat-dialog-container)
const textarea = page.locator('mat-dialog-container textarea');
const textareaVisible = await textarea.isVisible({ timeout: 5000 });
console.log('Textarea visible:', textareaVisible);
if (textareaVisible) {
// Try to fill the textarea
await textarea.fill('Test note content');
console.log('Filled textarea with test content');
// Check if save button exists (also inside the dialog)
const saveBtn = page.locator('mat-dialog-container #T-save-note');
const saveBtnVisible = await saveBtn.isVisible({ timeout: 5000 });
console.log('Save button visible:', saveBtnVisible);
if (saveBtnVisible) {
// Click save button
console.log('Clicking save button...');
await saveBtn.click();
console.log('Save button clicked');
// Wait a moment for the dialog to close and note to be saved
await page.waitForTimeout(2000);
// Check if note appears in the notes section
const noteContent = page.locator('notes note');
const noteExists = await noteContent.count();
console.log('Number of notes found after save:', noteExists);
}
}
}
}
} else {
// Try to find notes section in different ways
const notesAlternate = page.locator(
'[data-test="notes"], .notes-wrapper, .notes-section',
);
const notesAlternateExists = await notesAlternate.count();
console.log('Alternative notes selectors found:', notesAlternateExists);
// Check if any elements with 'note' in their tag/class exist
const anyNotes = page.locator('*[class*="note"], *[id*="note"]');
const anyNotesCount = await anyNotes.count();
console.log('Any note-related elements found:', anyNotesCount);
}
}
// Add a short wait to see the final state
await page.waitForTimeout(2000);
});
});

View file

@ -1,10 +1,10 @@
import { test } from '../../fixtures/test.fixture';
const PANEL_BTN = '.e2e-toggle-issue-provider-panel';
const ITEMS1 = '.items:nth-of-type(1)';
const ITEMS2 = '.items:nth-of-type(2)';
const ITEMS1 = '.items:first-child';
const ITEMS2 = '.items:nth-child(2)';
const CANCEL_BTN = 'mat-dialog-actions button:nth-of-type(1)';
const CANCEL_BTN = 'mat-dialog-actions button:first-child';
test.describe('Issue Provider Panel', () => {
test('should open all dialogs without error', async ({ page, workViewPage }) => {
@ -18,35 +18,35 @@ test.describe('Issue Provider Panel', () => {
await page.click('mat-tab-group .mat-mdc-tab:last-child');
await page.waitForSelector('issue-provider-setup-overview', { state: 'visible' });
await page.click(`${ITEMS1} > button:nth-of-type(1)`);
await page.locator(`${ITEMS1} > button`).nth(0).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS1} > button:nth-of-type(2)`);
await page.locator(`${ITEMS1} > button`).nth(1).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS1} > button:nth-of-type(3)`);
await page.locator(`${ITEMS1} > button`).nth(2).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(1)`);
await page.locator(`${ITEMS2} > button`).nth(0).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(2)`);
await page.locator(`${ITEMS2} > button`).nth(1).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(3)`);
await page.locator(`${ITEMS2} > button`).nth(2).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(4)`);
await page.locator(`${ITEMS2} > button`).nth(3).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(5)`);
await page.locator(`${ITEMS2} > button`).nth(4).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(6)`);
await page.locator(`${ITEMS2} > button`).nth(5).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);
await page.click(`${ITEMS2} > button:nth-of-type(7)`);
await page.locator(`${ITEMS2} > button`).nth(6).click();
await page.waitForSelector(CANCEL_BTN, { state: 'visible' });
await page.click(CANCEL_BTN);

View file

@ -6,9 +6,9 @@ const FIRST_NOTE = `${NOTE}:first-of-type`;
const TOGGLE_NOTES_BTN = '.e2e-toggle-notes-btn';
test.describe('Project Note', () => {
test.skip('create a note', async ({ page, projectPage }) => {
test('create a note', async ({ page, projectPage }) => {
// Create and navigate to default project
await projectPage.createAndGoToDefaultProject();
await projectPage.createAndGoToTestProject();
// Add a note
await projectPage.addNote('Some new Note');
@ -22,12 +22,12 @@ test.describe('Project Note', () => {
await expect(firstNote).toContainText('Some new Note');
});
test.skip('new note should be still available after reload', async ({
test('new note should be still available after reload', async ({
page,
projectPage,
}) => {
// Create and navigate to default project
await projectPage.createAndGoToDefaultProject();
await projectPage.createAndGoToTestProject();
// Add a note
await projectPage.addNote('Some new Note');

View file

@ -2,8 +2,8 @@ import { test, expect } from '../../fixtures/test.fixture';
const TASK = 'task';
const TASK_TEXTAREA = 'task textarea';
const FIRST_TASK = 'task:first-of-type';
const SECOND_TASK = 'task:nth-of-type(2)';
const FIRST_TASK = 'task:first-child';
const SECOND_TASK = 'task:nth-child(2)';
const TASK_DONE_BTN = '.task-done-btn';
test.describe('Task CRUD Operations', () => {

View file

@ -56,7 +56,7 @@
@if (nonHiddenProjects$ | async; as projectList) {
<section class="projects tour-projects">
<div class="g-multi-btn-wrapper">
<div class="g-multi-btn-wrapper e2e-projects-btn">
<button
#menuEntry
#projectExpandBtn