mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
Replace unreliable networkidle waits with explicit element/URL waits, improve keyboard focus handling with retry logic, fix strict mode violations, and add hover before note menu interactions.
236 lines
7.7 KiB
TypeScript
236 lines
7.7 KiB
TypeScript
import { type Locator, type Page } from '@playwright/test';
|
|
import { BasePage } from './base.page';
|
|
|
|
export class NotePage extends BasePage {
|
|
readonly toggleNotesBtn: Locator;
|
|
readonly addNoteBtn: Locator;
|
|
readonly notesSection: Locator;
|
|
readonly notesList: Locator;
|
|
readonly noteDialog: Locator;
|
|
readonly noteTextarea: Locator;
|
|
readonly saveNoteBtn: Locator;
|
|
|
|
constructor(page: Page, testPrefix: string = '') {
|
|
super(page, testPrefix);
|
|
this.toggleNotesBtn = page.locator('.e2e-toggle-notes-btn');
|
|
// Use multiple selectors for addNoteBtn - the button text or ID
|
|
this.addNoteBtn = page.locator(
|
|
'#add-note-btn, button:has-text("Add new Note"), [role="button"]:has-text("Add new Note")',
|
|
);
|
|
// notes section can be the Angular component or the panel containing the add note button
|
|
this.notesSection = page.locator(
|
|
'notes, .notes-panel, [class*="notes"]:has(button:has-text("Add new Note"))',
|
|
);
|
|
this.notesList = page.locator('notes .notes, .notes-list');
|
|
// Use dialog-fullscreen-markdown specifically as it's the note edit component
|
|
this.noteDialog = page.locator('dialog-fullscreen-markdown');
|
|
this.noteTextarea = page.locator('dialog-fullscreen-markdown textarea');
|
|
this.saveNoteBtn = page.locator(
|
|
'#T-save-note, button:has(mat-icon:has-text("save"))',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Ensures notes section is visible
|
|
*/
|
|
async ensureNotesVisible(): Promise<void> {
|
|
// Wait for the page to be ready
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
|
// Check if "Add new Note" button is visible as indicator that notes panel is open
|
|
const addNoteBtnVisible = await this.addNoteBtn
|
|
.first()
|
|
.isVisible({ timeout: 2000 })
|
|
.catch(() => false);
|
|
|
|
if (addNoteBtnVisible) {
|
|
// Notes panel is already visible
|
|
return;
|
|
}
|
|
|
|
// Also check if notesSection is visible
|
|
const isNotesVisible = await this.notesSection
|
|
.first()
|
|
.isVisible({ timeout: 1000 })
|
|
.catch(() => false);
|
|
if (isNotesVisible) {
|
|
return;
|
|
}
|
|
|
|
// Toggle notes panel via header button
|
|
const isToggleBtnVisible = await this.toggleNotesBtn
|
|
.isVisible({ timeout: 3000 })
|
|
.catch(() => false);
|
|
if (isToggleBtnVisible) {
|
|
await this.toggleNotesBtn.click();
|
|
// Wait for either the section or the add note button to become visible
|
|
await this.page
|
|
.locator('notes, button:has-text("Add new Note")')
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 5000 });
|
|
}
|
|
// If toggle button not visible, notes might already be visible or on a page without notes
|
|
}
|
|
|
|
/**
|
|
* Adds a new note with the given content
|
|
*/
|
|
async addNote(content: string): Promise<void> {
|
|
await this.ensureNotesVisible();
|
|
|
|
// Wait for add note button to be visible
|
|
await this.addNoteBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Move mouse away first to dismiss any tooltip, then click
|
|
await this.page.mouse.move(0, 0);
|
|
await this.page.waitForTimeout(300);
|
|
await this.addNoteBtn.click();
|
|
|
|
// Wait for dialog
|
|
await this.noteDialog.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Fill content
|
|
const textarea = this.page.locator('textarea').first();
|
|
await textarea.waitFor({ state: 'visible', timeout: 3000 });
|
|
await textarea.fill(content);
|
|
|
|
// Save
|
|
let saveBtn = this.page.locator('#T-save-note');
|
|
let saveBtnVisible = await saveBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
|
|
|
if (!saveBtnVisible) {
|
|
saveBtn = this.page.locator('button:has(mat-icon:has-text("save"))');
|
|
saveBtnVisible = await saveBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
|
}
|
|
|
|
if (saveBtnVisible) {
|
|
await saveBtn.click();
|
|
} else {
|
|
// Fallback: use keyboard shortcut
|
|
await textarea.press('Control+Enter');
|
|
}
|
|
|
|
// Wait for dialog to close
|
|
await this.noteDialog.waitFor({ state: 'hidden', timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Gets a note by its content
|
|
* Uses specific selector for the note component
|
|
*/
|
|
getNoteByContent(content: string): Locator {
|
|
// Use the Angular note component tag with the specific content
|
|
// The structure is: <note><div class="note">content</div></note>
|
|
return this.page.locator(`note:has-text("${content}")`);
|
|
}
|
|
|
|
/**
|
|
* Edits a note's content
|
|
*/
|
|
async editNote(note: Locator, newContent: string): Promise<void> {
|
|
// Click specifically on the markdown content area which has the editFullscreen handler
|
|
await note.locator('.markdown-preview').click();
|
|
|
|
// Wait for dialog
|
|
await this.noteDialog.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Clear and fill new content
|
|
const textarea = this.page.locator('textarea').first();
|
|
await textarea.waitFor({ state: 'visible', timeout: 3000 });
|
|
await textarea.clear();
|
|
await textarea.fill(newContent);
|
|
|
|
// Save
|
|
let saveBtn = this.page.locator('#T-save-note');
|
|
const saveBtnVisible = await saveBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
|
|
|
if (!saveBtnVisible) {
|
|
saveBtn = this.page.locator('button:has(mat-icon:has-text("save"))');
|
|
}
|
|
|
|
await saveBtn.click();
|
|
|
|
// Wait for dialog to close
|
|
await this.noteDialog.waitFor({ state: 'hidden', timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Deletes a note via menu button
|
|
*/
|
|
async deleteNote(note: Locator): Promise<void> {
|
|
// Hover to reveal the menu button
|
|
await note.hover();
|
|
await this.page.waitForTimeout(200);
|
|
|
|
// Find the menu button (more_vert) on the note
|
|
const menuBtn = note.locator('button:has(mat-icon:has-text("more_vert"))');
|
|
const menuBtnVisible = await menuBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|
|
|
if (menuBtnVisible) {
|
|
// Click menu button to open menu
|
|
await menuBtn.click();
|
|
} else {
|
|
// Fallback: right-click on note
|
|
await note.click({ button: 'right' });
|
|
}
|
|
|
|
// Click delete in menu
|
|
const deleteBtn = this.page.locator(
|
|
'.mat-mdc-menu-content button.color-warn, .mat-mdc-menu-content button:has(mat-icon:has-text("delete"))',
|
|
);
|
|
await deleteBtn.waitFor({ state: 'visible', timeout: 3000 });
|
|
await deleteBtn.click();
|
|
|
|
// Handle confirmation dialog if it appears
|
|
const confirmDialog = this.page.locator('dialog-confirm');
|
|
const confirmVisible = await confirmDialog
|
|
.isVisible({ timeout: 3000 })
|
|
.catch(() => false);
|
|
if (confirmVisible) {
|
|
const warnBtn = confirmDialog.locator('button[color="warn"]');
|
|
await warnBtn.waitFor({ state: 'visible', timeout: 3000 });
|
|
await warnBtn.click();
|
|
await confirmDialog.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {});
|
|
}
|
|
|
|
// Wait for note to be removed from DOM
|
|
await note.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
/**
|
|
* Checks if a note with the given content exists
|
|
* Uses Playwright's waitFor for reliable detection
|
|
*/
|
|
async noteExists(content: string, timeout = 15000): Promise<boolean> {
|
|
await this.ensureNotesVisible();
|
|
|
|
// Wait a bit for the UI to settle
|
|
await this.page.waitForTimeout(1000);
|
|
|
|
// Fast path: if "no notes" is visible, return false
|
|
const noNotesVisible = await this.page
|
|
.getByText('There are currently no notes')
|
|
.isVisible()
|
|
.catch(() => false);
|
|
if (noNotesVisible) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Try to find the content - if found within timeout, return true
|
|
await this.page.getByText(content).first().waitFor({ state: 'visible', timeout });
|
|
return true;
|
|
} catch {
|
|
// Content not found within timeout - return false
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the count of notes
|
|
*/
|
|
async getNoteCount(): Promise<number> {
|
|
await this.ensureNotesVisible();
|
|
return this.notesSection.locator('note').count();
|
|
}
|
|
}
|