mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
284 lines
11 KiB
TypeScript
284 lines
11 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');
|
|
this.workCtxTitle = page.locator('.current-work-context-title');
|
|
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');
|
|
|
|
// Find the Projects group item and wait for it to be visible
|
|
const projectsGroup = this.page.locator('nav-item button:has-text("Projects")');
|
|
await projectsGroup.waitFor({ state: 'visible', timeout: 3000 }); // Reduced from 5s to 3s
|
|
|
|
// 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) in additional buttons
|
|
const createProjectBtn = this.page.locator(
|
|
'nav-list .additional-btns button[mat-icon-button]:has(mat-icon:text("add"))',
|
|
);
|
|
await createProjectBtn.waitFor({ state: 'visible', timeout: 1500 }); // Reduced from 2s to 1.5s
|
|
await createProjectBtn.click();
|
|
} 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();
|
|
await addButton.waitFor({ state: 'visible', timeout: 2000 }); // Reduced from 3s to 2s
|
|
await addButton.click();
|
|
}
|
|
|
|
// 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: 1500 }); // Reduced from 2s to 1.5s
|
|
}
|
|
|
|
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 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
|
|
|
|
// First ensure Projects group is expanded
|
|
const projectsGroup = this.page.locator('nav-item button:has-text("Projects")');
|
|
await projectsGroup.waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
// Check if projects group is expanded, if not click to expand
|
|
const projectsGroupExpanded = await projectsGroup.getAttribute('aria-expanded');
|
|
if (projectsGroupExpanded !== 'true') {
|
|
await projectsGroup.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';
|
|
|
|
// Ensure Projects section is still expanded after creation
|
|
await projectsGroup.waitFor({ state: 'visible', timeout: 3000 });
|
|
const isStillExpanded = await projectsGroup.getAttribute('aria-expanded');
|
|
if (isStillExpanded !== 'true') {
|
|
await projectsGroup.click();
|
|
await this.page.waitForTimeout(500); // Wait for expansion animation
|
|
}
|
|
|
|
// Wait for the project to appear in the navigation
|
|
const newProject = this.page.locator(`nav-item button:has-text("${projectName}")`);
|
|
await newProject.waitFor({ state: 'visible', timeout: 3000 }); // Reduced from 5s to 3s
|
|
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): Promise<void> {
|
|
// 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 ready
|
|
await this.page.locator('.page-project').waitFor({ state: 'visible' });
|
|
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 });
|
|
}
|
|
|
|
// 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');
|
|
const isAddBtnVisible = await addNoteBtn
|
|
.isVisible({ timeout: 2000 })
|
|
.catch(() => false);
|
|
if (isAddBtnVisible) {
|
|
await addNoteBtn.click();
|
|
dialogOpened = true;
|
|
}
|
|
|
|
// Approach 2: If button not visible, try keyboard shortcut
|
|
if (!dialogOpened) {
|
|
// Focus on the main content area first
|
|
await this.page.locator('body').click();
|
|
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, mat-dialog-container').waitFor({
|
|
state: 'visible',
|
|
timeout: 5000,
|
|
});
|
|
|
|
// 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, mat-dialog-container').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 } });
|
|
}
|
|
}
|