mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
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:
parent
e2d73b234e
commit
cd79ea0940
7 changed files with 184 additions and 47 deletions
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
121
e2e-playwright/tests/debug-test.spec.ts
Normal file
121
e2e-playwright/tests/debug-test.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue