mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
Resolve conflict in webdav-sync-expansion.spec.ts: - Use simplified sync verification without reload (sync updates NgRx directly) - Test: B marks task done -> sync -> verify A sees task as done
647 lines
23 KiB
TypeScript
647 lines
23 KiB
TypeScript
import { expect, Locator, Page } from '@playwright/test';
|
|
import { BasePage } from './base.page';
|
|
|
|
export class ProjectPage extends BasePage {
|
|
readonly sidenav: Locator;
|
|
readonly createProjectBtn: Locator;
|
|
readonly projectAccordion: Locator;
|
|
readonly projectNameInput: Locator;
|
|
readonly submitBtn: Locator;
|
|
readonly workCtxMenu: Locator;
|
|
readonly workCtxTitle: Locator;
|
|
readonly projectSettingsBtn: Locator;
|
|
readonly moveToArchiveBtn: Locator;
|
|
readonly globalErrorAlert: Locator;
|
|
|
|
constructor(page: Page, testPrefix: string = '') {
|
|
super(page, testPrefix);
|
|
|
|
this.sidenav = page.locator('magic-side-nav');
|
|
this.createProjectBtn = page.locator(
|
|
'button[aria-label="Create New Project"], button:has-text("Create Project")',
|
|
);
|
|
this.projectAccordion = page.locator('nav-item button:has-text("Projects")');
|
|
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');
|
|
// Use more specific selector to avoid matching titles in other areas
|
|
this.workCtxTitle = page
|
|
.locator('main .page-title, .route-wrapper .page-title')
|
|
.first();
|
|
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');
|
|
}
|
|
|
|
async createProject(projectName: string): Promise<void> {
|
|
// Add test prefix to project name
|
|
const prefixedProjectName = this.testPrefix
|
|
? `${this.testPrefix}-${projectName}`
|
|
: projectName;
|
|
|
|
try {
|
|
// Ensure page is stable before starting
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
|
// Check for empty state first (single "Create Project" button)
|
|
const emptyStateBtn = this.page
|
|
.locator('nav-item')
|
|
.filter({ hasText: 'Create Project' })
|
|
.locator('button');
|
|
try {
|
|
await emptyStateBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await emptyStateBtn.click();
|
|
// Continue to dialog handling below
|
|
} catch {
|
|
// Not in empty state, continue with normal flow
|
|
// Find the Projects group item and wait for it to be visible
|
|
const projectsGroup = this.page
|
|
.locator('nav-list-tree')
|
|
.filter({ hasText: 'Projects' })
|
|
.locator('nav-item button')
|
|
.first();
|
|
await projectsGroup.waitFor({ state: 'visible', timeout: 10000 }); // Increased for CI stability
|
|
|
|
// Ensure expanded
|
|
const isExpanded = await projectsGroup.getAttribute('aria-expanded');
|
|
if (isExpanded !== 'true') {
|
|
await projectsGroup.click();
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
// Get the Projects nav-list-tree container to scope the button search
|
|
const projectsTree = this.page
|
|
.locator('nav-list-tree')
|
|
.filter({ hasText: 'Projects' });
|
|
|
|
// Hover over the Projects group to show additional buttons
|
|
await projectsGroup.hover();
|
|
|
|
// Wait a bit for the hover effect to take place
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// Look for the create project button (add icon) within the Projects tree only
|
|
const createProjectBtn = projectsTree.locator(
|
|
'.additional-btns button[mat-icon-button]:has(mat-icon:text("add"))',
|
|
);
|
|
// Try to wait for visibility, but if it fails, try forcing click if attached
|
|
try {
|
|
await createProjectBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await createProjectBtn.click();
|
|
} catch (e) {
|
|
console.log('Additional button not visible via hover, trying force click...');
|
|
await createProjectBtn.click({ force: true });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// If the specific selectors fail, try a more general approach
|
|
console.warn('Primary project creation approach failed, trying fallback:', error);
|
|
|
|
// Fallback: try to find any add button near Projects text
|
|
const addButton = this.page
|
|
.locator('button[mat-icon-button]:has(mat-icon:text("add"))')
|
|
.first();
|
|
try {
|
|
await addButton.waitFor({ state: 'visible', timeout: 5000 });
|
|
await addButton.click();
|
|
} catch (e) {
|
|
console.log('Fallback button not visible, trying force click...');
|
|
await addButton.click({ force: true });
|
|
}
|
|
}
|
|
|
|
// Wait for the dialog to appear
|
|
await this.projectNameInput.waitFor({ state: 'visible' });
|
|
await this.projectNameInput.fill(prefixedProjectName);
|
|
await this.submitBtn.click();
|
|
|
|
// Wait for dialog to close by waiting for input to be hidden
|
|
await this.projectNameInput.waitFor({ state: 'hidden', timeout: 3000 }); // Increased for CI stability
|
|
}
|
|
|
|
async getProject(index: number): Promise<Locator> {
|
|
// Projects are in a menuitem structure, not side-nav-item
|
|
// 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 navigateToProjectByName(projectName: string): Promise<void> {
|
|
const fullProjectName = this.testPrefix
|
|
? `${this.testPrefix}-${projectName}`
|
|
: projectName;
|
|
|
|
// Wait for page to be fully loaded before checking
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
|
// Wait for Angular to fully render after any navigation
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Helper function to check if we're already on the project
|
|
const isAlreadyOnProject = async (): Promise<boolean> => {
|
|
try {
|
|
// Use page.evaluate for direct DOM check (most reliable)
|
|
return await this.page.evaluate((name) => {
|
|
const main = document.querySelector('main');
|
|
return main?.textContent?.includes(name) ?? false;
|
|
}, fullProjectName);
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Check if we're already on the project
|
|
if (await isAlreadyOnProject()) {
|
|
return;
|
|
}
|
|
|
|
// Wait for the nav to be fully loaded
|
|
await this.sidenav.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Get the Projects nav-list-tree container
|
|
const projectsTree = this.page
|
|
.locator('nav-list-tree')
|
|
.filter({ hasText: 'Projects' });
|
|
|
|
// Find the Projects group button (the header with expand/collapse)
|
|
const projectsGroup = projectsTree
|
|
.locator('.g-multi-btn-wrapper nav-item button')
|
|
.first();
|
|
|
|
// Ensure Projects group is expanded with retry logic
|
|
await projectsGroup.waitFor({ state: 'visible', timeout: 5000 });
|
|
for (let i = 0; i < 3; i++) {
|
|
const isExpanded = await projectsGroup.getAttribute('aria-expanded');
|
|
if (isExpanded === 'true') break;
|
|
|
|
await projectsGroup.click();
|
|
// Wait for expansion animation to complete - scoped to Projects tree
|
|
await projectsTree
|
|
.locator('.nav-children')
|
|
.waitFor({ state: 'visible', timeout: 3000 })
|
|
.catch(() => {});
|
|
}
|
|
|
|
// Wait for the project to appear in the tree (may take time after sync/reload)
|
|
// Scope to the Projects tree to avoid matching tags or other trees
|
|
const projectTreeItem = projectsTree
|
|
.locator('[role="treeitem"]')
|
|
.filter({ hasText: fullProjectName })
|
|
.first();
|
|
|
|
// Wait for the project to appear with extended timeout (projects load after sync)
|
|
await projectTreeItem.waitFor({ state: 'visible', timeout: 20000 });
|
|
|
|
// Now get the clickable menuitem inside the treeitem
|
|
const projectBtn = projectTreeItem.locator('[role="menuitem"]').first();
|
|
|
|
// Wait for the menuitem to be visible and stable
|
|
await projectBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Click with retry - catch errors and check if we're already on the project
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
try {
|
|
await projectBtn.click({ timeout: 5000 });
|
|
|
|
// Wait for navigation to complete - wait for URL to change to project route
|
|
const navigated = await this.page
|
|
.waitForURL(/\/#\/project\//, { timeout: 5000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (navigated) break;
|
|
} catch {
|
|
// Click timed out - check if we're already on the project
|
|
if (await isAlreadyOnProject()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If navigation didn't happen, wait a bit and retry
|
|
if (attempt < 2) {
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
}
|
|
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
|
// Final verification - wait for the project to appear in main
|
|
// Use a locator-based wait for better reliability
|
|
try {
|
|
await this.page
|
|
.locator('main')
|
|
.getByText(fullProjectName, { exact: false })
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 15000 });
|
|
} catch {
|
|
// If verification fails, continue anyway - the test will catch real issues
|
|
}
|
|
}
|
|
|
|
async navigateToProject(projectLocator: Locator): Promise<void> {
|
|
const projectBtn = projectLocator.locator('button').first();
|
|
await projectBtn.waitFor({ state: 'visible' });
|
|
await projectBtn.click();
|
|
}
|
|
|
|
async openProjectMenu(projectLocator: Locator): Promise<void> {
|
|
const projectBtn = projectLocator.locator('.mat-mdc-menu-item');
|
|
const advBtn = projectLocator.locator('.additional-btn');
|
|
|
|
await projectBtn.hover();
|
|
await advBtn.waitFor({ state: 'visible' });
|
|
await advBtn.click();
|
|
await this.workCtxMenu.waitFor({ state: 'visible' });
|
|
}
|
|
|
|
async navigateToProjectSettings(): Promise<void> {
|
|
await this.projectSettingsBtn.waitFor({ state: 'visible' });
|
|
await this.projectSettingsBtn.click();
|
|
}
|
|
|
|
async archiveDoneTasks(): Promise<void> {
|
|
// Check if the collapsible needs to be expanded
|
|
const moveToArchiveBtnVisible = await this.moveToArchiveBtn.isVisible();
|
|
if (!moveToArchiveBtnVisible) {
|
|
const collapsibleHeader = this.page.locator('.collapsible-header');
|
|
if ((await collapsibleHeader.count()) > 0) {
|
|
await collapsibleHeader.click();
|
|
// Wait for the section to expand
|
|
await this.moveToArchiveBtn.waitFor({ state: 'visible', timeout: 1000 });
|
|
}
|
|
}
|
|
|
|
await this.moveToArchiveBtn.waitFor({ state: 'visible' });
|
|
await this.moveToArchiveBtn.click();
|
|
}
|
|
|
|
async createAndGoToTestProject(): Promise<void> {
|
|
// Ensure the page context is stable before starting
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
|
// Wait for the nav to be fully loaded
|
|
await this.sidenav.waitFor({ state: 'visible', timeout: 3000 }); // Reduced from 5s to 3s
|
|
|
|
// Handle empty state vs existing projects scenario
|
|
const addProjectBtn = this.page
|
|
.locator('nav-item')
|
|
.filter({ hasText: 'Create Project' })
|
|
.locator('button');
|
|
const projectsGroupBtn = this.page
|
|
.locator('nav-list-tree')
|
|
.filter({ hasText: 'Projects' })
|
|
.locator('nav-item button')
|
|
.first();
|
|
|
|
// Check if we're in empty state (no projects yet) or if projects group exists
|
|
try {
|
|
await addProjectBtn.waitFor({ state: 'visible', timeout: 1000 });
|
|
// Empty state: just create project directly since button is already visible
|
|
} catch {
|
|
// Normal state: expand projects group first
|
|
await projectsGroupBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
const projectsGroupExpanded = await projectsGroupBtn.getAttribute('aria-expanded');
|
|
if (projectsGroupExpanded !== 'true') {
|
|
await projectsGroupBtn.click();
|
|
await this.page.waitForTimeout(500); // Wait for expansion animation
|
|
}
|
|
}
|
|
|
|
// Create a new default project
|
|
await this.createProject('Test Project');
|
|
|
|
// Navigate to the created project
|
|
const projectName = this.testPrefix
|
|
? `${this.testPrefix}-Test Project`
|
|
: 'Test Project';
|
|
|
|
// After creating a project, ensure Projects group is visible and expanded
|
|
await this.page.waitForTimeout(2000); // Increased wait for DOM updates
|
|
|
|
// Find the Projects group button (should exist since we have a project)
|
|
const projectsGroupAfterCreation = this.page
|
|
.locator('nav-list-tree')
|
|
.filter({ hasText: 'Projects' })
|
|
.locator('nav-item button')
|
|
.first();
|
|
await projectsGroupAfterCreation.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Check if projects group is expanded, use similar logic to project.spec.ts
|
|
let isExpanded = await projectsGroupAfterCreation.getAttribute('aria-expanded');
|
|
if (isExpanded !== 'true') {
|
|
// Multiple approaches to expand the Projects section
|
|
// First: Try clicking the expand icon within the Projects button
|
|
const expandIcon = projectsGroupAfterCreation
|
|
.locator('mat-icon, .expand-icon, [class*="expand"]')
|
|
.first();
|
|
if (await expandIcon.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await expandIcon.click();
|
|
await this.page.waitForTimeout(1500);
|
|
isExpanded = await projectsGroupAfterCreation.getAttribute('aria-expanded');
|
|
}
|
|
|
|
// If still not expanded, try clicking the main button
|
|
if (isExpanded !== 'true') {
|
|
await projectsGroupAfterCreation.click();
|
|
await this.page.waitForTimeout(1500);
|
|
isExpanded = await projectsGroupAfterCreation.getAttribute('aria-expanded');
|
|
}
|
|
|
|
// If still not expanded, try double-clicking as last resort
|
|
if (isExpanded !== 'true') {
|
|
await projectsGroupAfterCreation.dblclick();
|
|
await this.page.waitForTimeout(1500);
|
|
}
|
|
}
|
|
|
|
// Wait for the project to appear in the navigation - use improved approach from project.spec.ts
|
|
await this.page.waitForTimeout(1000); // Allow time for project to appear
|
|
|
|
let newProject;
|
|
let projectFound = false;
|
|
|
|
// Check if .nav-children container exists after expansion
|
|
const navChildren = this.page.locator('.nav-children');
|
|
const navChildrenExists = await navChildren.count();
|
|
|
|
if (navChildrenExists > 0) {
|
|
await navChildren.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
try {
|
|
// Primary approach: nav-child-item structure with nav-item button
|
|
newProject = this.page
|
|
.locator('.nav-children .nav-child-item nav-item button')
|
|
.filter({ hasText: projectName });
|
|
await newProject.waitFor({ state: 'visible', timeout: 3000 });
|
|
projectFound = true;
|
|
} catch {
|
|
try {
|
|
// Second approach: any nav-child-item with the project name
|
|
newProject = this.page
|
|
.locator('.nav-child-item')
|
|
.filter({ hasText: projectName })
|
|
.locator('button');
|
|
await newProject.waitFor({ state: 'visible', timeout: 3000 });
|
|
projectFound = true;
|
|
} catch {
|
|
// Continue to fallback approaches
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback approaches if structured approach didn't work
|
|
if (!projectFound) {
|
|
try {
|
|
// Fallback: find any button with project name in the nav area
|
|
newProject = this.page
|
|
.locator('magic-side-nav button')
|
|
.filter({ hasText: projectName });
|
|
await newProject.waitFor({ state: 'visible', timeout: 3000 });
|
|
projectFound = true;
|
|
} catch {
|
|
// Ultimate fallback: search entire page for project button
|
|
newProject = this.page.locator('button').filter({ hasText: projectName });
|
|
await newProject.waitFor({ state: 'visible', timeout: 3000 });
|
|
projectFound = true;
|
|
}
|
|
}
|
|
|
|
// Verify the project is found and click it
|
|
if (!projectFound) {
|
|
throw new Error(`Project "${projectName}" not found in navigation after creation`);
|
|
}
|
|
|
|
await newProject.click();
|
|
|
|
// Wait for navigation to complete
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
|
// Verify we're in the project
|
|
await expect(this.workCtxTitle).toContainText(projectName);
|
|
}
|
|
|
|
async addNote(noteContent: string, projectId?: string): Promise<void> {
|
|
// Ensure we are on the correct project route if an id is provided
|
|
if (projectId && !this.page.url().includes(projectId)) {
|
|
await this.page.goto(`/#/project/${projectId}`);
|
|
await this.page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
// Wait for the app to be ready
|
|
const routerWrapper = this.page.locator('.route-wrapper');
|
|
await routerWrapper.waitFor({ state: 'visible', timeout: 6000 }); // Reduced from 10s to 6s
|
|
|
|
// Wait for the page to be fully loaded
|
|
await this.page.waitForLoadState('networkidle');
|
|
// Wait for project view to be present (not necessarily visible immediately)
|
|
const workView = this.page.locator('work-view');
|
|
await workView.waitFor({ state: 'attached', timeout: 15000 });
|
|
const isWorkViewVisible = await workView
|
|
.isVisible({ timeout: 5000 })
|
|
.catch(() => false);
|
|
if (!isWorkViewVisible) {
|
|
// Allow a brief moment for the view to finish rendering
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
await this.page.waitForTimeout(100);
|
|
|
|
// First ensure notes section is visible by clicking toggle if needed
|
|
const toggleNotesBtn = this.page.locator('.e2e-toggle-notes-btn');
|
|
const isToggleBtnVisible = await toggleNotesBtn
|
|
.isVisible({ timeout: 2000 })
|
|
.catch(() => false);
|
|
if (isToggleBtnVisible) {
|
|
await toggleNotesBtn.click();
|
|
// Wait for notes section to appear after toggle
|
|
await this.page.locator('notes').waitFor({ state: 'visible', timeout: 5000 });
|
|
}
|
|
|
|
// Hover header to ensure buttons might appear
|
|
const projectHeader = this.page
|
|
.locator('.project-header, .page-title-wrapper')
|
|
.first();
|
|
if (await projectHeader.isVisible()) {
|
|
await projectHeader.hover();
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
// Retry loop to open dialog
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
try {
|
|
// Ensure any previous overlays are closed
|
|
|
|
const backdrop = this.page.locator('.cdk-overlay-backdrop');
|
|
|
|
if (await backdrop.isVisible()) {
|
|
await backdrop.click({ force: true });
|
|
|
|
await this.page.waitForTimeout(200);
|
|
}
|
|
|
|
// Try multiple approaches to open the note dialog
|
|
|
|
let dialogOpened = false;
|
|
|
|
// Approach 1: Try to click the add note button
|
|
|
|
const addNoteBtn = this.page
|
|
.locator(
|
|
'#add-note-btn, button[title="Add Note"], button[aria-label="Add Note"], button mat-icon:has-text("note_add")',
|
|
)
|
|
.first();
|
|
|
|
const isAddBtnVisible = await addNoteBtn
|
|
|
|
.isVisible({ timeout: 2000 })
|
|
|
|
.catch(() => false);
|
|
|
|
if (isAddBtnVisible) {
|
|
// Move cursor away to avoid tooltip overlays blocking the click, then force-click
|
|
|
|
await this.page.mouse.move(0, 0);
|
|
|
|
await addNoteBtn.click({ force: true });
|
|
|
|
dialogOpened = true;
|
|
} else {
|
|
// Check overflow menu - scope to work context menu to avoid picking up task menus
|
|
|
|
const moreBtn = this.workCtxMenu
|
|
.locator('button mat-icon:has-text("more_vert")')
|
|
.first();
|
|
|
|
if (await moreBtn.isVisible()) {
|
|
await moreBtn.click({ force: true });
|
|
|
|
const menuAddNote = this.page
|
|
|
|
.locator('.mat-mdc-menu-item')
|
|
|
|
.filter({ hasText: 'Add Note' })
|
|
|
|
.first();
|
|
|
|
// Wait briefly for menu
|
|
|
|
try {
|
|
await menuAddNote.waitFor({ state: 'visible', timeout: 2000 });
|
|
|
|
await menuAddNote.click({ force: true });
|
|
|
|
dialogOpened = true;
|
|
} catch {
|
|
// Close menu if item not found
|
|
|
|
await this.page.keyboard.press('Escape');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Approach 2: If button not visible, try keyboard shortcut
|
|
|
|
if (!dialogOpened) {
|
|
// Focus on the work view area first
|
|
|
|
if (await workView.isVisible()) {
|
|
await workView.click({ force: true });
|
|
} else {
|
|
await this.page.locator('body').click({ force: true });
|
|
}
|
|
|
|
await this.page.waitForTimeout(500);
|
|
|
|
await this.page.keyboard.press('n');
|
|
}
|
|
|
|
// Wait for dialog to appear with better error handling
|
|
|
|
await this.page.locator('dialog-fullscreen-markdown').waitFor({
|
|
state: 'visible',
|
|
|
|
timeout: 5000, // Short timeout for retry
|
|
});
|
|
|
|
break; // Success
|
|
} catch (e) {
|
|
console.log(`Attempt ${i + 1} to open note dialog failed, retrying...`);
|
|
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
}
|
|
|
|
// Final check/throw
|
|
|
|
await this.page.locator('dialog-fullscreen-markdown').waitFor({
|
|
state: 'visible',
|
|
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Try different selectors for the textarea
|
|
let noteTextarea = this.page.locator('dialog-fullscreen-markdown textarea').first();
|
|
let textareaVisible = await noteTextarea
|
|
.isVisible({ timeout: 2000 })
|
|
.catch(() => false);
|
|
|
|
if (!textareaVisible) {
|
|
// Try alternative selector
|
|
noteTextarea = this.page.locator('textarea').first();
|
|
textareaVisible = await noteTextarea
|
|
.isVisible({ timeout: 2000 })
|
|
.catch(() => false);
|
|
}
|
|
|
|
if (!textareaVisible) {
|
|
throw new Error('Note dialog textarea not found after trying multiple approaches');
|
|
}
|
|
|
|
await noteTextarea.fill(noteContent);
|
|
|
|
// Click the save button - try multiple selectors
|
|
let saveBtn = this.page.locator('#T-save-note');
|
|
let saveBtnVisible = await saveBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
|
|
|
if (!saveBtnVisible) {
|
|
// Try button with save icon
|
|
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: press Enter to save
|
|
await noteTextarea.press('Control+Enter');
|
|
}
|
|
|
|
// Wait for dialog to close
|
|
await this.page.locator('dialog-fullscreen-markdown').waitFor({
|
|
state: 'hidden',
|
|
timeout: 5000,
|
|
});
|
|
|
|
// After saving, check if notes panel is visible
|
|
// If not, toggle it
|
|
const notesWrapper = this.page.locator('notes');
|
|
const isNotesVisible = await notesWrapper
|
|
.isVisible({ timeout: 1000 })
|
|
.catch(() => false);
|
|
|
|
if (!isNotesVisible) {
|
|
// Toggle the notes panel
|
|
const toggleBtn = this.page.locator('.e2e-toggle-notes-btn');
|
|
await toggleBtn.waitFor({ state: 'visible' });
|
|
await toggleBtn.click();
|
|
await notesWrapper.waitFor({ state: 'visible', timeout: 5000 });
|
|
}
|
|
|
|
// Hover over the notes area like in Nightwatch
|
|
await notesWrapper.hover({ position: { x: 10, y: 50 } });
|
|
}
|
|
}
|