refactor(e2e): replace waitForTimeout with condition-based waits

- Replace ~100 waitForTimeout calls with proper condition-based waits
- Extract shared utilities for time input and task scheduling
- Add timeout constants for consistent wait times
- Add new selectors for reminder dialogs and detail panels

Files refactored across 25+ test files including:
- Plugin tests (lifecycle, upload, loading, enable, structure)
- Reminder tests (view-task, schedule-page, default-options)
- Work view, planner, focus mode, and app feature tests
- Task dragdrop, autocomplete, and daily summary tests

New utilities created:
- e2e/utils/time-input-helper.ts - Robust time input filling
- e2e/utils/schedule-task-helper.ts - Task scheduling helpers
- e2e/constants/timeouts.ts - Standardized timeout values
This commit is contained in:
Johannes Millan 2026-01-03 15:29:38 +01:00
parent 24c008df92
commit 11d85208e5
28 changed files with 571 additions and 620 deletions

View file

@ -59,6 +59,19 @@ export const cssSelectors = {
MAT_DIALOG: 'mat-dialog-container',
DIALOG_FULLSCREEN_MARKDOWN: 'dialog-fullscreen-markdown',
DIALOG_CREATE_PROJECT: 'dialog-create-project',
DIALOG_SCHEDULE_TASK: 'dialog-schedule-task',
DIALOG_ACTIONS: 'mat-dialog-actions',
DIALOG_SUBMIT: 'mat-dialog-actions button:last-child',
// ============================================================================
// REMINDER DIALOG SELECTORS
// ============================================================================
REMINDER_DIALOG: 'dialog-view-task-reminder',
REMINDER_DIALOG_TASKS: 'dialog-view-task-reminder .tasks',
REMINDER_DIALOG_TASK: 'dialog-view-task-reminder .task',
REMINDER_DIALOG_TASK_1: 'dialog-view-task-reminder .task:first-of-type',
REMINDER_DIALOG_TASK_2: 'dialog-view-task-reminder .task:nth-of-type(2)',
REMINDER_DIALOG_TASK_3: 'dialog-view-task-reminder .task:nth-of-type(3)',
// ============================================================================
// SETTINGS PAGE SELECTORS
@ -108,4 +121,16 @@ export const cssSelectors = {
// DATE/TIME SELECTORS
// ============================================================================
EDIT_DATE_INFO: '.edit-date-info',
TIME_INPUT: 'input[type="time"]',
MAT_TIME_INPUT: 'mat-form-field input[type="time"]',
// ============================================================================
// TASK DETAIL PANEL SELECTORS
// ============================================================================
RIGHT_PANEL: '.right-panel',
DETAIL_PANEL: 'dialog-task-detail-panel, task-detail-panel',
DETAIL_PANEL_BTN: '.show-additional-info-btn',
SCHEDULE_TASK_ITEM:
'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))',
TASK_SCHEDULE_BTN: '.ico-btn.schedule-btn',
};

34
e2e/constants/timeouts.ts Normal file
View file

@ -0,0 +1,34 @@
/**
* Standardized timeout constants for e2e tests.
* Use these to ensure consistent timeout handling across all tests.
*/
export const TIMEOUTS = {
/** Standard wait for dialogs to appear/disappear */
DIALOG: 5000,
/** Standard wait for navigation changes */
NAVIGATION: 30000,
/** Wait for sync operations to complete */
SYNC: 30000,
/** Maximum wait for scheduled reminders to trigger */
SCHEDULE_MAX: 60000,
/** Wait for tasks to become visible */
TASK_VISIBLE: 10000,
/** Wait for UI animations to complete */
ANIMATION: 500,
/** Wait for Angular stability after state changes */
ANGULAR_STABILITY: 3000,
/** Wait for elements to be enabled/clickable */
ELEMENT_ENABLED: 5000,
/** Extended timeout for complex operations */
EXTENDED: 20000,
} as const;
export type TimeoutKey = keyof typeof TIMEOUTS;

View file

@ -10,38 +10,31 @@ test.describe('All Basic Routes Without Error', () => {
// Wait for magic-side-nav to be fully loaded
await page.locator('magic-side-nav').waitFor({ state: 'visible' });
await page.waitForTimeout(1000); // Give extra time for navigation items to load
// Helper to navigate and wait for route to load
const navigateAndWait = async (route: string): Promise<void> => {
await page.goto(route);
await page.locator('.route-wrapper').waitFor({ state: 'visible', timeout: 10000 });
};
// Navigate to schedule
await page.goto('/#/tag/TODAY/schedule');
await navigateAndWait('/#/tag/TODAY/schedule');
// Test that key navigation elements are visible and functional
// Wait for navigation to be fully loaded
await page.waitForSelector('magic-side-nav', { state: 'visible' });
// Test navigation to different routes by URL (the main goal of this test)
await page.goto('/#/schedule');
await page.waitForTimeout(500);
await page.goto('/#/tag/TODAY/tasks');
await page.waitForTimeout(500);
await page.goto('/#/config');
await page.waitForTimeout(500);
await navigateAndWait('/#/schedule');
await navigateAndWait('/#/tag/TODAY/tasks');
await navigateAndWait('/#/config');
// Navigate to different routes
await page.goto('/#/tag/TODAY/quick-history');
await page.waitForTimeout(500);
await page.goto('/#/tag/TODAY/worklog');
await page.waitForTimeout(500);
await page.goto('/#/tag/TODAY/metrics');
await page.waitForTimeout(500);
await page.goto('/#/tag/TODAY/planner');
await page.waitForTimeout(500);
await page.goto('/#/tag/TODAY/daily-summary');
await page.waitForTimeout(500);
await page.goto('/#/tag/TODAY/settings');
await page.waitForTimeout(500);
await navigateAndWait('/#/tag/TODAY/quick-history');
await navigateAndWait('/#/tag/TODAY/worklog');
await navigateAndWait('/#/tag/TODAY/metrics');
await navigateAndWait('/#/tag/TODAY/planner');
await navigateAndWait('/#/tag/TODAY/daily-summary');
await navigateAndWait('/#/tag/TODAY/settings');
// Send 'n' key to open notes dialog
await page.keyboard.press('n');

View file

@ -75,9 +75,6 @@ test.describe('App Features', () => {
await appFeaturesSection.click();
await expect(featureSwitch).toBeVisible();
// Wait a moment for the toggle to be fully interactive after expansion animation
await page.waitForTimeout(100);
// Click toggle button to enable and verify state change
await featureSwitch.click();
await expect(featureSwitch).toBeChecked();

View file

@ -51,8 +51,8 @@ test.describe('App Features - Focus Mode', () => {
// send shortcut for focus mode, ensure that focus overlay is not showing
await page.keyboard.press('F');
await page.waitForTimeout(500);
expect(focusModeOverlay).not.toBeAttached();
// Verify overlay doesn't appear after a brief moment
await expect(focusModeOverlay).not.toBeAttached({ timeout: 1000 });
// Re-enable the feature
await page.goto('/#/config');

View file

@ -11,9 +11,6 @@ test.describe('Autocomplete Dropdown', () => {
// Add task with tag syntax, skipClose=true to keep input open
await workViewPage.addTask('some task <3 #basicTag', true);
// Small delay to let the tag creation dialog appear
await page.waitForTimeout(500);
// Wait for and click the confirm create tag button with increased timeout
await page.waitForSelector(CONFIRM_CREATE_TAG_BTN, {
state: 'visible',

View file

@ -24,8 +24,8 @@ test.describe('Daily Summary', () => {
const taskName = 'test task hohoho 1h/1h';
await workViewPage.addTask(taskName);
// Wait a moment for task to be saved
await page.waitForTimeout(500);
// Wait for task to appear
await expect(page.locator('task')).toHaveCount(1, { timeout: 5000 });
// Navigate to daily summary
await page.goto('/#/tag/TODAY/daily-summary');

View file

@ -84,18 +84,14 @@ test.describe('Bug #5117: Flowtime timer stops at Countdown duration', () => {
const taskSelectorOverlay = page.locator('.task-selector-overlay');
await expect(taskSelectorOverlay).toBeVisible({ timeout: 3000 });
// Wait a bit for the autocomplete to show suggestions
await page.waitForTimeout(500);
// Click on the first suggested task (mat-option is in CDK overlay panel)
const suggestedTask = page.locator('mat-option, .mat-mdc-option').first();
await expect(suggestedTask).toBeVisible({ timeout: 5000 });
await suggestedTask.click();
await page.waitForTimeout(500);
}
// Wait for focus mode main component to be ready (after task selection)
await page.waitForTimeout(500);
// Wait for task selector overlay to close
await expect(taskSelectorOverlay).not.toBeVisible({ timeout: 5000 });
}
// Step 2: Switch to Countdown mode
await countdownButton.click();
@ -116,10 +112,7 @@ test.describe('Bug #5117: Flowtime timer stops at Countdown duration', () => {
await expect(durationSlider).not.toBeVisible({ timeout: 2000 });
// Verify clock shows 0:00 (Flowtime starts at 0 and counts up)
await page.waitForTimeout(300);
const clockText = await clockTime.textContent();
console.log('Clock text in Flowtime mode:', clockText);
expect(clockText?.trim()).toBe('0:00');
await expect(clockTime).toHaveText('0:00', { timeout: 3000 });
// Step 5: Start the focus session by clicking play button
await expect(playButton).toBeVisible({ timeout: 2000 });
@ -218,15 +211,13 @@ test.describe('Bug #5117: Flowtime timer stops at Countdown duration', () => {
const taskSelectorOverlay = page.locator('.task-selector-overlay');
await expect(taskSelectorOverlay).toBeVisible({ timeout: 3000 });
await page.waitForTimeout(500);
const suggestedTask = page.locator('mat-option, .mat-mdc-option').first();
await expect(suggestedTask).toBeVisible({ timeout: 5000 });
await suggestedTask.click();
await page.waitForTimeout(500);
}
await page.waitForTimeout(500);
// Wait for task selector overlay to close
await expect(taskSelectorOverlay).not.toBeVisible({ timeout: 5000 });
}
// Step 1: Switch to Countdown mode
await countdownButton.click();
@ -242,10 +233,7 @@ test.describe('Bug #5117: Flowtime timer stops at Countdown duration', () => {
await expect(flowtimeButton).toHaveClass(/is-active/, { timeout: 2000 });
// Clock should show 0:00 for Flowtime
await page.waitForTimeout(300);
const flowTimeDisplay = await clockTime.textContent();
console.log('Flowtime initial display:', flowTimeDisplay);
expect(flowTimeDisplay?.trim()).toBe('0:00');
await expect(clockTime).toHaveText('0:00', { timeout: 3000 });
// Step 4: Start the Flowtime session
await expect(playButton).toBeVisible({ timeout: 2000 });

View file

@ -15,8 +15,11 @@ 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' });
// Wait for the setup overview to be fully loaded
await page.waitForTimeout(1000);
// Wait for buttons to be ready
await page
.locator('issue-provider-setup-overview button')
.first()
.waitFor({ state: 'visible', timeout: 5000 });
// Get all buttons in the issue provider setup overview
const setupButtons = page.locator('issue-provider-setup-overview button');

View file

@ -86,11 +86,11 @@ test.describe('Planner Navigation', () => {
await projectPage.createAndGoToTestProject();
// Wait for project to be fully loaded
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle');
// Add a task with schedule to ensure planner has content
await workViewPage.addTask('Scheduled task for planner');
await page.waitForTimeout(500);
await expect(page.locator('task')).toHaveCount(1, { timeout: 10000 });
// Navigate to planner using the button
await plannerPage.navigateToPlanner();

View file

@ -34,7 +34,10 @@ test.describe('Enable Plugin Test', () => {
// Navigate to plugin settings
await page.click(SETTINGS_BTN);
await page.waitForTimeout(1000);
await page
.locator('.page-settings')
.first()
.waitFor({ state: 'visible', timeout: 10000 });
await page.evaluate(() => {
const configPage = document.querySelector('.page-settings');
@ -70,10 +73,12 @@ test.describe('Enable Plugin Test', () => {
}
});
await page.waitForTimeout(1000);
await expect(page.locator('plugin-management')).toBeVisible({ timeout: 5000 });
await page.waitForTimeout(2000);
await expect(page.locator('plugin-management')).toBeVisible({ timeout: 10000 });
// Wait for plugin cards to be loaded
await page
.locator('plugin-management mat-card')
.first()
.waitFor({ state: 'attached', timeout: 10000 });
// Check if plugin-management has any content
await page.evaluate(() => {
@ -95,8 +100,6 @@ test.describe('Enable Plugin Test', () => {
};
});
await page.waitForTimeout(1000);
// Try to find and enable the API Test Plugin (which exists by default)
const enableResult = await page.evaluate(() => {
const pluginCards = document.querySelectorAll('plugin-management mat-card');
@ -128,7 +131,25 @@ test.describe('Enable Plugin Test', () => {
// console.log('Plugin enablement result:', enableResult);
expect(enableResult.foundApiTestPlugin).toBe(true);
await page.waitForTimeout(3000); // Wait for plugin to initialize
// Wait for toggle state to change to enabled
if (enableResult.toggleClicked) {
await page.waitForFunction(
() => {
const cards = Array.from(
document.querySelectorAll('plugin-management mat-card'),
);
const apiTestCard = cards.find((card) => {
const title = card.querySelector('mat-card-title')?.textContent || '';
return title.includes('API Test Plugin');
});
const toggle = apiTestCard?.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggle?.getAttribute('aria-checked') === 'true';
},
{ timeout: 10000 },
);
}
// Now check if plugin menu has buttons
await page.evaluate(() => {

View file

@ -39,14 +39,11 @@ test.describe('Plugin Lifecycle', () => {
const settingsBtn = page.locator(SETTINGS_BTN);
await settingsBtn.waitFor({ state: 'visible' });
await settingsBtn.click();
// Wait for navigation to settings page
await page.waitForTimeout(500); // Give time for navigation
// Wait for settings page to be fully visible - use first() to avoid multiple matches
await page.locator('.page-settings').first().waitFor({ state: 'visible' });
await page.waitForTimeout(50); // Small delay for UI settling
// Wait for page to stabilize
await page.waitForTimeout(300);
await page
.locator('.page-settings')
.first()
.waitFor({ state: 'visible', timeout: 10000 });
await page.evaluate(() => {
const configPage = document.querySelector('.page-settings');
@ -71,9 +68,6 @@ test.describe('Plugin Lifecycle', () => {
}
});
// Wait for expansion animation
await page.waitForTimeout(300);
// Scroll plugin-management into view
await page.evaluate(() => {
const pluginMgmt = document.querySelector('plugin-management');
@ -84,7 +78,6 @@ test.describe('Plugin Lifecycle', () => {
// Wait for plugin management section to be attached
await page.locator('plugin-management').waitFor({ state: 'attached', timeout: 5000 });
await page.waitForTimeout(50); // Small delay for UI settling
// Enable the plugin
const enableResult = await page.evaluate((pluginName: string) => {
@ -117,15 +110,10 @@ test.describe('Plugin Lifecycle', () => {
expect(enableResult.found).toBe(true);
// Wait for plugin to initialize
await page.waitForTimeout(100); // Small delay for plugin initialization
// Go back to work view
await page.goto('/#/tag/TODAY');
// Wait for navigation and work view to be ready
await page.waitForTimeout(500); // Give time for navigation
await page.locator('.route-wrapper').waitFor({ state: 'visible' });
await page.waitForTimeout(50); // Small delay for UI settling
await page.locator('.route-wrapper').waitFor({ state: 'visible', timeout: 10000 });
// Wait for task list to be visible
await page.waitForSelector('task-list', { state: 'visible', timeout: 10000 });
@ -135,7 +123,6 @@ test.describe('Plugin Lifecycle', () => {
test.setTimeout(20000); // Increase timeout
// Wait for magic-side-nav to be ready
await page.locator(SIDENAV).waitFor({ state: 'visible' });
await page.waitForTimeout(50); // Small delay for plugins to initialize
// Plugin doesn't show snack bar on load, check plugin nav item instead
await expect(page.locator(API_TEST_PLUGIN_NAV_ITEM)).toBeVisible({ timeout: 10000 });
@ -148,13 +135,10 @@ test.describe('Plugin Lifecycle', () => {
// Click on the plugin nav item to navigate to plugin
await expect(page.locator(API_TEST_PLUGIN_NAV_ITEM)).toBeVisible();
await page.click(API_TEST_PLUGIN_NAV_ITEM);
// Wait for navigation to plugin page
await page.waitForTimeout(500); // Give time for navigation
await page.waitForTimeout(50); // Small delay for UI settling
// Verify we navigated to the plugin page
await expect(page).toHaveURL(/\/plugins\/api-test-plugin\/index/);
await expect(page.locator('iframe')).toBeVisible();
await expect(page).toHaveURL(/\/plugins\/api-test-plugin\/index/, { timeout: 10000 });
await expect(page.locator('iframe')).toBeVisible({ timeout: 10000 });
// Go back to work view
await page.goto('/#/tag/TODAY');
@ -165,14 +149,11 @@ test.describe('Plugin Lifecycle', () => {
// Navigate to settings
await page.click(SETTINGS_BTN);
// Wait for navigation to settings page
await page.waitForTimeout(500); // Give time for navigation
// Wait for settings page to be visible - use first() to avoid multiple matches
await page.locator('.page-settings').first().waitFor({ state: 'visible' });
await page.waitForTimeout(200); // Small delay for UI settling
// Wait for page to stabilize
await page.waitForTimeout(300);
await page
.locator('.page-settings')
.first()
.waitFor({ state: 'visible', timeout: 10000 });
// Expand plugin section
await page.evaluate(() => {
@ -190,9 +171,6 @@ test.describe('Plugin Lifecycle', () => {
}
});
// Wait for expansion animation
await page.waitForTimeout(300);
// Scroll plugin-management into view
await page.evaluate(() => {
const pluginMgmt = document.querySelector('plugin-management');
@ -205,7 +183,11 @@ test.describe('Plugin Lifecycle', () => {
await page
.locator('plugin-management')
.waitFor({ state: 'attached', timeout: 10000 });
await page.waitForTimeout(500); // Give time for plugins to load
// Wait for plugin cards to be available
await page
.locator('plugin-management mat-card')
.first()
.waitFor({ state: 'attached', timeout: 10000 });
// Check current state of the plugin and enable if needed
const currentState = await page.evaluate((pluginName: string) => {
@ -250,7 +232,6 @@ test.describe('Plugin Lifecycle', () => {
'API Test Plugin',
{ timeout: 5000 },
);
await page.waitForTimeout(1000); // Wait for plugin to fully initialize
}
// Now disable the plugin
@ -290,14 +271,11 @@ test.describe('Plugin Lifecycle', () => {
'API Test Plugin',
{ timeout: 5000 },
);
await page.waitForTimeout(1000); // Wait for plugin to fully disable
// Go back to work view
await page.goto('/#/tag/TODAY');
// Wait for navigation and work view to be ready
await page.waitForTimeout(500); // Give time for navigation
await page.locator('.route-wrapper').waitFor({ state: 'visible' });
await page.waitForTimeout(500); // Small delay for UI settling
await page.locator('.route-wrapper').waitFor({ state: 'visible', timeout: 10000 });
// Check if the magic-side-nav exists and verify the API Test Plugin is not in it
const sideNavExists = (await page.locator(SIDENAV).count()) > 0;

View file

@ -71,7 +71,26 @@ test.describe.serial('Plugin Loading', () => {
expect(enableResult.found).toBe(true);
await page.waitForTimeout(2000); // Wait for plugin to initialize
// Wait for toggle state to change to enabled
if (enableResult.clicked) {
await page.waitForFunction(
(name) => {
const cards = Array.from(
document.querySelectorAll('plugin-management mat-card'),
);
const targetCard = cards.find((card) => {
const title = card.querySelector('mat-card-title')?.textContent || '';
return title.includes(name);
});
const toggle = targetCard?.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggle?.getAttribute('aria-checked') === 'true';
},
'API Test Plugin',
{ timeout: 10000 },
);
}
// Ensure plugin management is visible in viewport
await page.evaluate(() => {
@ -83,7 +102,6 @@ test.describe.serial('Plugin Loading', () => {
// Navigate to plugin management - check for attachment first
await expect(page.locator(PLUGIN_CARD).first()).toBeAttached({ timeout: 20000 });
await page.waitForTimeout(500);
// Check example plugin is loaded and enabled
const pluginCardsResult = await page.evaluate(() => {
@ -122,13 +140,12 @@ test.describe.serial('Plugin Loading', () => {
// Try to open plugin iframe view if menu is available
if (pluginMenuVisible) {
await pluginNavItem.click();
await expect(page.locator(PLUGIN_IFRAME)).toBeVisible();
await expect(page.locator(PLUGIN_IFRAME)).toBeVisible({ timeout: 10000 });
await expect(page).toHaveURL(/\/plugins\/api-test-plugin\/index/);
await page.waitForTimeout(1000); // Wait for iframe to load
// Switch to iframe context and verify content
const frame = page.frameLocator(PLUGIN_IFRAME);
await expect(frame.locator('h1')).toBeVisible();
await expect(frame.locator('h1')).toBeVisible({ timeout: 10000 });
await expect(frame.locator('h1')).toContainText('API Test Plugin');
} else {
console.log('Skipping iframe test - plugin menu not available');
@ -178,7 +195,22 @@ test.describe.serial('Plugin Loading', () => {
}
}, 'API Test Plugin');
await page.waitForTimeout(2000); // Wait for plugin to initialize
// Wait for toggle state to change to enabled
await page.waitForFunction(
(name) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const targetCard = cards.find((card) => {
const title = card.querySelector('mat-card-title')?.textContent || '';
return title.includes(name);
});
const toggle = targetCard?.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggle?.getAttribute('aria-checked') === 'true';
},
'API Test Plugin',
{ timeout: 10000 },
);
// Ensure plugin management is visible in viewport
await page.evaluate(() => {
@ -217,10 +249,22 @@ test.describe.serial('Plugin Loading', () => {
return result;
});
await page.waitForTimeout(2000); // Give more time for plugin to unload
// Stay on the settings page, just wait for state to update
await page.waitForTimeout(2000);
// Wait for toggle state to change to disabled
await page.waitForFunction(
(name) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const targetCard = cards.find((card) => {
const title = card.querySelector('mat-card-title')?.textContent || '';
return title.includes(name);
});
const toggle = targetCard?.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggle?.getAttribute('aria-checked') === 'false';
},
'API Test Plugin',
{ timeout: 10000 },
);
// Re-enable the plugin - we should still be on settings page
// Just make sure plugin section is visible
@ -231,8 +275,6 @@ test.describe.serial('Plugin Loading', () => {
}
});
await page.waitForTimeout(1000);
await page.evaluate(() => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const apiTestCard = cards.find((card) => {
@ -258,12 +300,26 @@ test.describe.serial('Plugin Loading', () => {
return result;
});
await page.waitForTimeout(2000); // Give time for plugin to reload
// Wait for toggle state to change to enabled again
await page.waitForFunction(
(name) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const targetCard = cards.find((card) => {
const title = card.querySelector('mat-card-title')?.textContent || '';
return title.includes(name);
});
const toggle = targetCard?.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggle?.getAttribute('aria-checked') === 'true';
},
'API Test Plugin',
{ timeout: 10000 },
);
// Navigate back to main view
await page.click('text=Today'); // Click on Today navigation
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await expect(page).toHaveURL(/\/#\/tag\/TODAY/, { timeout: 10000 });
// Check if menu entry is back (gracefully handle if not visible)
const pluginNavItemReEnabled = page

View file

@ -34,7 +34,10 @@ test.describe.serial('Plugin Structure Test', () => {
// Navigate to plugin settings (implementing navigateToPluginSettings inline)
await page.click(SETTINGS_BTN);
await page.waitForTimeout(1000);
await page
.locator('.page-settings')
.first()
.waitFor({ state: 'visible', timeout: 10000 });
// Execute script to navigate to plugin section
await page.evaluate(() => {
@ -75,8 +78,7 @@ test.describe.serial('Plugin Structure Test', () => {
}
});
await page.waitForTimeout(1000);
await expect(page.locator('plugin-management')).toBeVisible({ timeout: 5000 });
await expect(page.locator('plugin-management')).toBeVisible({ timeout: 10000 });
// Check plugin card structure
await page.evaluate(() => {

View file

@ -22,7 +22,10 @@ test.describe.serial('Plugin Upload', () => {
test.setTimeout(process.env.CI ? 90000 : 60000);
// Navigate to plugin management
await page.click(SETTINGS_BTN);
await page.waitForTimeout(1000);
await page
.locator('.page-settings')
.first()
.waitFor({ state: 'visible', timeout: 10000 });
await page.evaluate(() => {
const configPage = document.querySelector('.page-settings');
@ -48,8 +51,7 @@ test.describe.serial('Plugin Upload', () => {
}
});
await page.waitForTimeout(1000);
await expect(page.locator('plugin-management')).toBeVisible({ timeout: 5000 });
await expect(page.locator('plugin-management')).toBeVisible({ timeout: 10000 });
// Upload plugin ZIP file
const testPluginPath = path.resolve(__dirname, '../../../src/assets/test-plugin.zip');
@ -69,11 +71,19 @@ test.describe.serial('Plugin Upload', () => {
});
await page.locator(FILE_INPUT).setInputFiles(testPluginPath);
await page.waitForTimeout(3000); // Wait for file processing
// Wait for uploaded plugin to appear in list
await page.waitForFunction(
(pluginId) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
return cards.some((card) => card.textContent?.includes(pluginId));
},
TEST_PLUGIN_ID,
{ timeout: 15000 },
);
// Verify uploaded plugin appears in list (there are multiple cards, so check first)
await expect(page.locator(PLUGIN_CARD).first()).toBeVisible();
await page.waitForTimeout(1000);
const pluginExists = await page.evaluate((pluginName: string) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
@ -114,7 +124,23 @@ test.describe.serial('Plugin Upload', () => {
}, TEST_PLUGIN_ID);
expect(enableResult).toBeTruthy();
await page.waitForTimeout(2000); // Longer pause to ensure DOM update completes
// Wait for toggle state to change to enabled
await page.waitForFunction(
(pluginId) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const targetCard = cards.find((card) => card.textContent?.includes(pluginId));
if (targetCard) {
const toggleButton = targetCard.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggleButton?.getAttribute('aria-checked') === 'true';
}
return false;
},
TEST_PLUGIN_ID,
{ timeout: 10000 },
);
// Verify plugin is now enabled
const enabledStatus = await page.evaluate((pluginId: string) => {
@ -148,7 +174,23 @@ test.describe.serial('Plugin Upload', () => {
}, TEST_PLUGIN_ID);
expect(disableResult).toBeTruthy();
await page.waitForTimeout(1000);
// Wait for toggle state to change to disabled
await page.waitForFunction(
(pluginId) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const targetCard = cards.find((card) => card.textContent?.includes(pluginId));
if (targetCard) {
const toggleButton = targetCard.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggleButton?.getAttribute('aria-checked') === 'false';
}
return false;
},
TEST_PLUGIN_ID,
{ timeout: 10000 },
);
// Verify plugin is now disabled
const disabledStatus = await page.evaluate((pluginId: string) => {
@ -182,7 +224,23 @@ test.describe.serial('Plugin Upload', () => {
}, TEST_PLUGIN_ID);
expect(reEnableResult).toBeTruthy();
await page.waitForTimeout(1000);
// Wait for toggle state to change to enabled again
await page.waitForFunction(
(pluginId) => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
const targetCard = cards.find((card) => card.textContent?.includes(pluginId));
if (targetCard) {
const toggleButton = targetCard.querySelector(
'mat-slide-toggle button[role="switch"]',
) as HTMLButtonElement;
return toggleButton?.getAttribute('aria-checked') === 'true';
}
return false;
},
TEST_PLUGIN_ID,
{ timeout: 10000 },
);
// Verify plugin is enabled again
const reEnabledStatus = await page.evaluate((pluginId: string) => {
@ -218,9 +276,15 @@ test.describe.serial('Plugin Upload', () => {
return false;
}, TEST_PLUGIN_ID);
await page.waitForTimeout(500);
await page.waitForTimeout(3000); // Longer pause for removal to complete
// Wait for plugin to be removed from the list
await page.waitForFunction(
(pluginId) => {
const items = Array.from(document.querySelectorAll('plugin-management mat-card'));
return !items.some((item) => item.textContent?.includes(pluginId));
},
TEST_PLUGIN_ID,
{ timeout: 15000 },
);
// Verify plugin is removed
const removalResult = await page.evaluate((pluginId: string) => {

View file

@ -12,8 +12,6 @@ test.describe('Project', () => {
// Wait for app to be ready
await workViewPage.waitForTaskList();
// Additional wait for stability in parallel execution
await page.waitForTimeout(50);
});
test('move done tasks to archive without error', async ({ page }) => {
@ -65,47 +63,39 @@ test.describe('Project', () => {
const isExpanded = await projectsGroupBtn.getAttribute('aria-expanded');
if (isExpanded !== 'true') {
await projectsGroupBtn.click();
await page.waitForTimeout(500); // Wait for expansion animation
// Wait for expansion by checking aria-expanded attribute
await page.waitForFunction(
(btn) => btn?.getAttribute('aria-expanded') === 'true',
await projectsGroupBtn.elementHandle(),
{ timeout: 5000 },
);
}
}
// Create a new project
await projectPage.createProject('Cool Test Project');
// Wait for project creation to complete and navigation to update
// Wait for project creation to complete
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000); // Increased wait time for DOM updates
// After creating, ensure Projects section exists and is expanded
await projectsGroupBtn.waitFor({ state: 'visible', timeout: 5000 });
// Check if Projects section needs to be expanded
let isExpanded = await projectsGroupBtn.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 = projectsGroupBtn
.locator('mat-icon, .expand-icon, [class*="expand"]')
.first();
if (await expandIcon.isVisible({ timeout: 1000 }).catch(() => false)) {
await expandIcon.click();
await page.waitForTimeout(1500);
isExpanded = await projectsGroupBtn.getAttribute('aria-expanded');
}
// If still not expanded, try clicking the main button
if (isExpanded !== 'true') {
// Wait for Projects section to be expanded (the project creation should auto-expand it)
await page
.waitForFunction(
() => {
const btn = document.querySelector(
'nav-list-tree:has(nav-item button:has-text("Projects")) nav-item button',
);
return btn?.getAttribute('aria-expanded') === 'true';
},
{ timeout: 10000 },
)
.catch(async () => {
// If not expanded, try clicking the main button
await projectsGroupBtn.click();
await page.waitForTimeout(1500);
isExpanded = await projectsGroupBtn.getAttribute('aria-expanded');
}
// If still not expanded, try double-clicking as last resort
if (isExpanded !== 'true') {
await projectsGroupBtn.dblclick();
await page.waitForTimeout(1500);
}
}
});
// Find the newly created project directly (with test prefix)
const expectedProjectName = testPrefix
@ -122,10 +112,6 @@ test.describe('Project', () => {
// Projects section might not have expanded properly - continue with fallback approaches
}
// Look for the newly created project
// Wait a moment for the project to fully appear in the list
await page.waitForTimeout(1000);
let newProject;
let projectFound = false;
@ -178,10 +164,11 @@ test.describe('Project', () => {
// Wait for navigation to complete
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500); // Brief wait for any animations
// Verify we're in the new project
await expect(projectPage.workCtxTitle).toContainText(expectedProjectName);
await expect(projectPage.workCtxTitle).toContainText(expectedProjectName, {
timeout: 10000,
});
});
test('navigate to project settings', async ({ page }) => {

View file

@ -45,7 +45,6 @@ test.describe('Default task reminder option', () => {
// Scroll into view and hover over the task to reveal action buttons
await task.scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
await task.hover({ force: true });
// Open the detail panel to access the schedule action
@ -74,8 +73,7 @@ test.describe('Default task reminder option', () => {
await timeInput.click();
// Wait for the reminder dropdown to appear and check the default option
await page.waitForTimeout(500);
await expect(page.getByText(changedOptionText)).toBeVisible();
await expect(page.getByText(changedOptionText)).toBeVisible({ timeout: 5000 });
});
test('should apply when scheduling a task using short syntax', async ({
@ -96,7 +94,6 @@ test.describe('Default task reminder option', () => {
const addBtn = page.locator('.tour-addBtn');
await addBtn.waitFor({ state: 'visible', timeout: 10000 });
await addBtn.click();
await page.waitForTimeout(300);
}
// Wait for the global add-task input to be available
@ -113,7 +110,6 @@ test.describe('Default task reminder option', () => {
}
// Wait for task to be created and reschedule button to appear
await page.waitForTimeout(500);
const rescheduleBtn = page.getByTitle('Reschedule').first();
await rescheduleBtn.waitFor({ state: 'visible', timeout: 10000 });
await rescheduleBtn.click();

View file

@ -1,77 +1,13 @@
import type { Locator, Page } from '@playwright/test';
import { test, expect } from '../../fixtures/test.fixture';
import { scheduleTaskViaDetailPanel } from '../../utils/schedule-task-helper';
const TASK = 'task';
const TASK_SCHEDULE_BTN = '.ico-btn.schedule-btn';
const SCHEDULE_DIALOG = 'dialog-schedule-task';
const SCHEDULE_DIALOG_TIME_INPUT = 'dialog-schedule-task input[type="time"]';
const SCHEDULE_DIALOG_CONFIRM = 'mat-dialog-actions button:last-child';
const SCHEDULE_ROUTE_BTN = 'magic-side-nav a[href="#/scheduled-list"]';
const SCHEDULE_PAGE_CMP = 'scheduled-list-page';
const SCHEDULE_PAGE_TASKS = `${SCHEDULE_PAGE_CMP} .tasks planner-task`;
const SCHEDULE_PAGE_TASK_1 = `${SCHEDULE_PAGE_TASKS}:first-of-type`;
const SCHEDULE_PAGE_TASK_1_TITLE_EL = `${SCHEDULE_PAGE_TASK_1} .title`;
const DETAIL_PANEL_BTN = '.show-additional-info-btn';
const DETAIL_PANEL_SELECTOR = 'dialog-task-detail-panel, task-detail-panel';
const DETAIL_PANEL_SCHEDULE_ITEM =
'task-detail-item:has(mat-icon:text("alarm")), ' +
'task-detail-item:has(mat-icon:text("today")), ' +
'task-detail-item:has(mat-icon:text("schedule"))';
const fillScheduleDialogTime = async (
page: Page,
scheduleTime: number,
): Promise<void> => {
const dialog = page.locator(SCHEDULE_DIALOG);
await dialog.waitFor({ state: 'visible', timeout: 10000 });
const timeInput = page.locator(SCHEDULE_DIALOG_TIME_INPUT);
await timeInput.waitFor({ state: 'visible', timeout: 10000 });
const date = new Date(scheduleTime);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
await timeInput.fill('');
await timeInput.fill(`${hours}:${minutes}`);
const confirmBtn = page.locator(SCHEDULE_DIALOG_CONFIRM);
await confirmBtn.waitFor({ state: 'visible', timeout: 5000 });
await confirmBtn.click();
await dialog.waitFor({ state: 'hidden', timeout: 10000 });
};
const closeDetailPanelIfOpen = async (page: Page): Promise<void> => {
const detailPanel = page.locator(DETAIL_PANEL_SELECTOR).first();
if (await detailPanel.isVisible()) {
await page.keyboard.press('Escape');
await detailPanel.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
}
};
const scheduleTaskViaDetailPanel = async (
page: Page,
task: Locator,
scheduleTime: number,
): Promise<void> => {
await task.waitFor({ state: 'visible' });
await task.scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
await task.hover({ force: true });
const detailBtn = task.locator(DETAIL_PANEL_BTN).first();
await detailBtn.waitFor({ state: 'visible', timeout: 5000 });
await detailBtn.click();
const scheduleItem = page.locator(DETAIL_PANEL_SCHEDULE_ITEM).first();
await scheduleItem.waitFor({ state: 'visible', timeout: 5000 });
await scheduleItem.click();
await fillScheduleDialogTime(page, scheduleTime);
await closeDetailPanelIfOpen(page);
};
test.describe('Reminders Schedule Page', () => {
test('should add a scheduled tasks', async ({ page, workViewPage, testPrefix }) => {
@ -88,9 +24,6 @@ test.describe('Reminders Schedule Page', () => {
const targetTask = page.locator(TASK).filter({ hasText: title }).first();
await targetTask.waitFor({ state: 'visible' });
// Hover to reveal schedule button
await targetTask.hover();
// Open detail panel to access schedule action
await scheduleTaskViaDetailPanel(page, targetTask, scheduleTime);
@ -125,9 +58,6 @@ test.describe('Reminders Schedule Page', () => {
test.setTimeout(90000); // Increase timeout for multiple operations
await workViewPage.waitForTaskList();
// Wait a bit for the page to stabilize
await page.waitForTimeout(1000);
// Helper function to schedule a task
const scheduleTask = async (
taskTitle: string,
@ -151,13 +81,12 @@ test.describe('Reminders Schedule Page', () => {
await workViewPage.addTask(title1);
// Wait for first task to be visible and stable
// Wait for first task to be visible
await page
.locator(TASK)
.filter({ hasText: title1 })
.first()
.waitFor({ state: 'visible' });
await page.waitForTimeout(500); // Let the task fully render
await scheduleTask(title1, scheduleTime1);
@ -167,18 +96,16 @@ test.describe('Reminders Schedule Page', () => {
await workViewPage.addTask(title2);
// Wait for second task to be visible and stable
// Wait for second task to be visible
await page
.locator(TASK)
.filter({ hasText: title2 })
.first()
.waitFor({ state: 'visible' });
await page.waitForTimeout(500); // Let the task fully render
await scheduleTask(title2, scheduleTime2);
// Verify both tasks have schedule indicators
// Use first() to avoid multiple element issues if there are duplicates
const task1 = page.locator(TASK).filter({ hasText: title1 }).first();
const task2 = page.locator(TASK).filter({ hasText: title2 }).first();
@ -198,9 +125,6 @@ test.describe('Reminders Schedule Page', () => {
// Wait for scheduled page to load
await page.waitForSelector(SCHEDULE_PAGE_CMP, { state: 'visible', timeout: 10000 });
// Wait for the scheduled tasks to render
await page.waitForTimeout(1000);
// Verify both tasks appear in scheduled list with retry
await expect(async () => {
const scheduledTasks = page.locator(SCHEDULE_PAGE_TASKS);

View file

@ -1,24 +1,10 @@
import { expect, test } from '../../fixtures/test.fixture';
import { addTaskWithReminder } from '../../utils/schedule-task-helper';
const DIALOG = 'dialog-view-task-reminder';
const DIALOG_TASK = `${DIALOG} .task`;
const DIALOG_TASK1 = `${DIALOG_TASK}:first-of-type`;
const SCHEDULE_MAX_WAIT_TIME = 60000; // Reduced from 180s to 60s
// Helper selectors from addTaskWithReminder
const TASK = 'task';
const SCHEDULE_TASK_ITEM =
'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))';
const DIALOG_CONTAINER = 'mat-dialog-container';
const DIALOG_SUBMIT = `${DIALOG_CONTAINER} mat-dialog-actions button:last-of-type`;
const TIME_INP = 'input[type="time"]';
const getTimeVal = (d: Date): string => {
const hours = d.getHours().toString().padStart(2, '0');
const minutes = d.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
const SCHEDULE_MAX_WAIT_TIME = 60000;
test.describe('Reminders View Task', () => {
test('should display a modal with a scheduled task if due', async ({
@ -26,81 +12,16 @@ test.describe('Reminders View Task', () => {
workViewPage,
testPrefix,
}) => {
test.setTimeout(SCHEDULE_MAX_WAIT_TIME + 20000); // Add extra time for test setup
test.setTimeout(SCHEDULE_MAX_WAIT_TIME + 20000);
// Wait for work view to be ready
await workViewPage.waitForTaskList();
const taskTitle = `${testPrefix}-0 A task`;
const scheduleTime = Date.now() + 10000; // Add 10 seconds buffer
const d = new Date(scheduleTime);
const timeValue = getTimeVal(d);
// Add task
await workViewPage.addTask(taskTitle);
// Open panel for task
const taskEl = page.locator(TASK).first();
await taskEl.hover();
const detailPanelBtn = page.locator('.show-additional-info-btn').first();
await detailPanelBtn.waitFor({ state: 'visible' });
await detailPanelBtn.click();
// Wait for and click schedule task item with better error handling
const scheduleItem = page.locator(SCHEDULE_TASK_ITEM);
await scheduleItem.waitFor({ state: 'visible', timeout: 5000 });
await scheduleItem.click();
// Wait for dialog with improved timeout
const dialogContainer = page.locator(DIALOG_CONTAINER);
await dialogContainer.waitFor({ state: 'visible', timeout: 10000 });
await page.waitForTimeout(200); // Allow dialog animation to complete
// Set time - use more robust selector and approach
const timeInput = page
.locator('mat-form-field input[type="time"]')
.or(page.locator(TIME_INP));
await timeInput.waitFor({ state: 'visible', timeout: 10000 });
// Multiple approaches to ensure the time input is ready
await timeInput.click();
await page.waitForTimeout(100);
// Clear existing value if any
await timeInput.fill('');
await page.waitForTimeout(100);
// Set the time value
await timeInput.fill(timeValue);
await page.waitForTimeout(100);
// Verify the value was set
const inputValue = await timeInput.inputValue();
if (inputValue !== timeValue) {
// Fallback: use evaluate to set value directly
await page.evaluate(
({ value }) => {
const timeInputEl = document.querySelector(
'mat-form-field input[type="time"]',
) as HTMLInputElement;
if (timeInputEl) {
timeInputEl.value = value;
timeInputEl.dispatchEvent(new Event('input', { bubbles: true }));
timeInputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
},
{ value: timeValue },
);
}
// Ensure focus moves away to commit the value
await page.keyboard.press('Tab');
await page.waitForTimeout(100);
// Submit dialog
await page.waitForSelector(DIALOG_SUBMIT, { state: 'visible' });
await page.click(DIALOG_SUBMIT);
await page.waitForSelector(DIALOG_CONTAINER, { state: 'hidden' });
// Add task with reminder using shared helper
await addTaskWithReminder(page, workViewPage, taskTitle, scheduleTime);
// Wait for reminder dialog to appear
await page.waitForSelector(DIALOG, {

View file

@ -1,105 +1,20 @@
import { expect, test } from '../../fixtures/test.fixture';
import { addTaskWithReminder } from '../../utils/schedule-task-helper';
const DIALOG = 'dialog-view-task-reminder';
const DIALOG_TASKS_WRAPPER = `${DIALOG} .tasks`;
const DIALOG_TASK = `${DIALOG} .task`;
const DIALOG_TASK1 = `${DIALOG_TASK}:first-of-type`;
const DIALOG_TASK2 = `${DIALOG_TASK}:nth-of-type(2)`;
const SCHEDULE_MAX_WAIT_TIME = 60000; // Reduced from 180s to 60s
// Helper selectors for task scheduling
const TASK = 'task';
const SCHEDULE_TASK_ITEM =
'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))';
const SCHEDULE_DIALOG = 'mat-dialog-container';
const DIALOG_SUBMIT = `${SCHEDULE_DIALOG} mat-dialog-actions button:last-of-type`;
const TIME_INP = 'input[type="time"]';
const SIDE_INNER = '.right-panel';
const DEFAULT_DELTA = 5000; // 5 seconds instead of 1.2 minutes
const SCHEDULE_MAX_WAIT_TIME = 60000;
test.describe.serial('Reminders View Task 2', () => {
const addTaskWithReminder = async (
page: any,
workViewPage: any,
title: string,
scheduleTime: number = Date.now() + DEFAULT_DELTA,
): Promise<void> => {
// Add task
await workViewPage.addTask(title);
// Open task panel by hovering and clicking the detail button
const taskSel = page.locator(TASK).first();
await taskSel.waitFor({ state: 'visible' });
await taskSel.hover();
const detailPanelBtn = page.locator('.show-additional-info-btn').first();
await detailPanelBtn.waitFor({ state: 'visible' });
await detailPanelBtn.click();
await page.waitForSelector(SIDE_INNER, { state: 'visible' });
// Click schedule item with better error handling
const scheduleItem = page.locator(SCHEDULE_TASK_ITEM);
await scheduleItem.waitFor({ state: 'visible', timeout: 5000 });
await scheduleItem.click();
const scheduleDialog = page.locator(SCHEDULE_DIALOG);
await scheduleDialog.waitFor({ state: 'visible', timeout: 10000 });
await page.waitForTimeout(200); // Allow dialog animation
// Set time with improved robustness
const d = new Date(scheduleTime);
const timeValue = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
// Use more robust selector and multiple fallback approaches
const timeInput = page
.locator('mat-form-field input[type="time"]')
.or(page.locator(TIME_INP));
await timeInput.waitFor({ state: 'visible', timeout: 10000 });
await timeInput.click();
await page.waitForTimeout(100);
// Clear and set value
await timeInput.fill('');
await page.waitForTimeout(100);
await timeInput.fill(timeValue);
await page.waitForTimeout(100);
// Verify the value was set
const inputValue = await timeInput.inputValue();
if (inputValue !== timeValue) {
// Fallback: use evaluate to set value directly
await page.evaluate(
({ value }) => {
const timeInputEl = document.querySelector(
'mat-form-field input[type="time"]',
) as HTMLInputElement;
if (timeInputEl) {
timeInputEl.value = value;
timeInputEl.dispatchEvent(new Event('input', { bubbles: true }));
timeInputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
},
{ value: timeValue },
);
}
await page.keyboard.press('Tab');
await page.waitForTimeout(100);
// Submit with better handling
const submitBtn = page.locator(DIALOG_SUBMIT);
await submitBtn.waitFor({ state: 'visible', timeout: 5000 });
await submitBtn.click();
await scheduleDialog.waitFor({ state: 'hidden', timeout: 10000 });
};
test('should display a modal with 2 scheduled task if due', async ({
page,
workViewPage,
testPrefix,
}) => {
test.setTimeout(SCHEDULE_MAX_WAIT_TIME + 30000); // Add extra buffer
test.setTimeout(SCHEDULE_MAX_WAIT_TIME + 30000);
await workViewPage.waitForTaskList();

View file

@ -1,4 +1,5 @@
import { expect, test } from '../../fixtures/test.fixture';
import { addTaskWithReminder } from '../../utils/schedule-task-helper';
const DIALOG = 'dialog-view-task-reminder';
const DIALOG_TASKS_WRAPPER = `${DIALOG} .tasks`;
@ -7,147 +8,34 @@ const DIALOG_TASK1 = `${DIALOG_TASK}:first-of-type`;
const DIALOG_TASK2 = `${DIALOG_TASK}:nth-of-type(2)`;
const DIALOG_TASK3 = `${DIALOG_TASK}:nth-of-type(3)`;
const TO_TODAY_SUF = ' .actions button:last-of-type';
const SCHEDULE_MAX_WAIT_TIME = 60000; // Reduced from 180s to 60s
// Helper selectors for task scheduling
const SCHEDULE_TASK_ITEM =
'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))';
const DIALOG_CONTAINER = 'mat-dialog-container';
const DIALOG_SUBMIT = `${DIALOG_CONTAINER} mat-dialog-actions button:last-of-type`;
const TIME_INP = 'input[type="time"]';
const RIGHT_PANEL = '.right-panel';
const DEFAULT_DELTA = 5000; // 5 seconds instead of 1.2 minutes
const SCHEDULE_MAX_WAIT_TIME = 60000;
test.describe.serial('Reminders View Task 4', () => {
const addTaskWithReminder = async (
page: any,
workViewPage: any,
title: string,
scheduleTime: number = Date.now() + DEFAULT_DELTA,
): Promise<void> => {
// Add task (title should already include test prefix)
await workViewPage.addTask(title);
// Wait for task to be fully rendered before proceeding
await page.waitForTimeout(800);
// Open task panel by hovering and clicking the detail button
// Find the specific task by title to ensure we're working with the right one
const specificTaskSelector =
`task:has-text("${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}")`.substring(
0,
200,
); // Limit selector length
const taskSel = page.locator(specificTaskSelector).first();
await taskSel.waitFor({ state: 'visible', timeout: 10000 });
// Ensure task is fully loaded by checking for task content and that it's not moving
await page.waitForTimeout(500);
await taskSel.scrollIntoViewIfNeeded();
await taskSel.hover();
const detailPanelBtn = taskSel.locator('.show-additional-info-btn').first();
await detailPanelBtn.waitFor({ state: 'visible', timeout: 5000 });
await detailPanelBtn.click();
await page.waitForSelector(RIGHT_PANEL, { state: 'visible', timeout: 10000 });
// Wait for and click schedule task item with better error handling
const scheduleItem = page.locator(SCHEDULE_TASK_ITEM);
await scheduleItem.waitFor({ state: 'visible', timeout: 10000 });
// Ensure the schedule item is clickable
await scheduleItem.waitFor({ state: 'attached' });
await page.waitForTimeout(200);
await scheduleItem.click();
// Wait for dialog with improved timeout
const dialogContainer = page.locator(DIALOG_CONTAINER);
await dialogContainer.waitFor({ state: 'visible', timeout: 10000 });
await page.waitForTimeout(200); // Allow dialog animation to complete
// Set time - use more robust selector and approach
const d = new Date(scheduleTime);
const timeValue = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
const timeInput = page
.locator('mat-form-field input[type="time"]')
.or(page.locator(TIME_INP));
await timeInput.waitFor({ state: 'visible', timeout: 10000 });
// Multiple approaches to ensure the time input is ready
await timeInput.click();
await page.waitForTimeout(100);
// Clear existing value if any
await timeInput.fill('');
await page.waitForTimeout(100);
// Set the time value
await timeInput.fill(timeValue);
await page.waitForTimeout(100);
// Verify the value was set
const inputValue = await timeInput.inputValue();
if (inputValue !== timeValue) {
// Fallback: use evaluate to set value directly
await page.evaluate(
({ value }: { value: string }) => {
const timeInputEl = document.querySelector(
'mat-form-field input[type="time"]',
) as HTMLInputElement;
if (timeInputEl) {
timeInputEl.value = value;
timeInputEl.dispatchEvent(new Event('input', { bubbles: true }));
timeInputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
},
{ value: timeValue },
);
}
// Ensure focus moves away to commit the value
await page.keyboard.press('Tab');
await page.waitForTimeout(100);
// Submit dialog
await page.waitForSelector(DIALOG_SUBMIT, { state: 'visible' });
await page.click(DIALOG_SUBMIT);
await page.waitForSelector(DIALOG_CONTAINER, { state: 'hidden' });
// Wait for UI to fully settle after dialog closes
await page.waitForTimeout(500);
};
test('should manually empty list via add to today', async ({
page,
workViewPage,
testPrefix,
}) => {
test.setTimeout(SCHEDULE_MAX_WAIT_TIME + 60000); // Reduced extra time
test.setTimeout(SCHEDULE_MAX_WAIT_TIME + 60000);
await workViewPage.waitForTaskList();
const start = Date.now() + 10000; // Reduce from 100 seconds to 10 seconds
const start = Date.now() + 10000;
// Add three tasks with reminders using test prefix
const task1Name = `${testPrefix}-0 D task xyz`;
const task2Name = `${testPrefix}-1 D task xyz`;
const task3Name = `${testPrefix}-2 D task xyz`;
// Add tasks with proper spacing to avoid interference
// Add tasks - the helper now handles all the complexity
await addTaskWithReminder(page, workViewPage, task1Name, start);
await page.waitForTimeout(2000); // Ensure first task is fully processed
await addTaskWithReminder(page, workViewPage, task2Name, start);
await page.waitForTimeout(2000); // Ensure second task is fully processed
await addTaskWithReminder(page, workViewPage, task3Name, Date.now() + 5000);
await page.waitForTimeout(2000); // Ensure third task is fully processed
// Wait for reminder dialog
await page.waitForSelector(DIALOG, {
state: 'visible',
timeout: SCHEDULE_MAX_WAIT_TIME + 60000, // Reduced timeout
timeout: SCHEDULE_MAX_WAIT_TIME + 60000,
});
// Wait for all tasks to be present
@ -160,19 +48,28 @@ test.describe.serial('Reminders View Task 4', () => {
await expect(page.locator(DIALOG_TASKS_WRAPPER)).toContainText(task2Name);
await expect(page.locator(DIALOG_TASKS_WRAPPER)).toContainText(task3Name);
// Click "add to today" buttons with proper waits
// Click "add to today" buttons - wait for each to process before next
const button1 = page.locator(DIALOG_TASK1 + TO_TODAY_SUF);
await button1.waitFor({ state: 'visible', timeout: 5000 });
await button1.click();
await page.waitForTimeout(500); // Allow first click to process
// Wait for task count to reduce before clicking next
await expect(async () => {
const count = await page.locator(DIALOG_TASK).count();
expect(count).toBeLessThanOrEqual(3);
}).toPass({ timeout: 5000 });
const button2 = page.locator(DIALOG_TASK2 + TO_TODAY_SUF);
await button2.waitFor({ state: 'visible', timeout: 5000 });
await button2.click();
await page.waitForTimeout(500); // Allow second click to process
// Wait for task count to reduce
await expect(async () => {
const count = await page.locator(DIALOG_TASK).count();
expect(count).toBeLessThanOrEqual(2);
}).toPass({ timeout: 5000 });
// Verify remaining task contains 'D task xyz'
await page.waitForTimeout(1000); // Allow dialog state to update
await expect(page.locator(DIALOG_TASK1)).toContainText('D task xyz');
});
});

View file

@ -11,9 +11,6 @@ test.describe('Short Syntax', () => {
// Add a task with project short syntax
await workViewPage.addTask('0 test task koko +i');
// Wait a moment for the task to be processed
await page.waitForTimeout(500);
// Verify task is visible
const task = page.locator('task').first();
await expect(task).toBeVisible({ timeout: 10000 });

View file

@ -10,8 +10,6 @@ test.describe('Drag Task to change project and labels', () => {
// Wait for app to be ready
await workViewPage.waitForTaskList();
// Additional wait for stability in parallel execution
await page.waitForTimeout(50);
});
test('should be able to move task to project by dragging to project link in magic-side-nav', async ({
@ -54,32 +52,28 @@ test.describe('Drag Task to change project and labels', () => {
// find drag handle of task
const firstTask = page.locator('task').first();
const dragHandle = firstTask.locator('.drag-handle');
const tagList = firstTask.locator('tag-list');
// Drag and drop to first project
// Drag and drop to first project - wait for tag to appear
await dragHandle.dragTo(project1NavItem);
await page.waitForTimeout(500); // Wait for drag animation and state update
await expect(firstTask.locator('tag-list')).toContainText(
`${testPrefix}-TestProject 1`,
);
await expect(tagList).toContainText(`${testPrefix}-TestProject 1`, {
timeout: 5000,
});
// Drag and drop to second project
// Drag and drop to second project - wait for tag change
await dragHandle.dragTo(project2NavItem);
await page.waitForTimeout(500); // Wait for drag animation and state update
await expect(firstTask.locator('tag-list')).not.toContainText(
`${testPrefix}-TestProject 1`,
);
await expect(firstTask.locator('tag-list')).toContainText(
`${testPrefix}-TestProject 2`,
);
await expect(tagList).not.toContainText(`${testPrefix}-TestProject 1`, {
timeout: 5000,
});
await expect(tagList).toContainText(`${testPrefix}-TestProject 2`);
// Drag and drop back to inbox
// Drag and drop back to inbox - wait for tag change
const inboxNavItem = page.getByRole('menuitem').filter({ hasText: 'Inbox' });
await dragHandle.dragTo(inboxNavItem);
await page.waitForTimeout(500); // Wait for drag animation and state update
await expect(firstTask.locator('tag-list')).not.toContainText(
`${testPrefix}-TestProject 2`,
);
await expect(firstTask.locator('tag-list')).toContainText('Inbox');
await expect(tagList).not.toContainText(`${testPrefix}-TestProject 2`, {
timeout: 5000,
});
await expect(tagList).toContainText('Inbox');
});
test('should be able to add and remove tags by dragging task to the tag link in magic-side-nav', async ({
@ -132,22 +126,20 @@ test.describe('Drag Task to change project and labels', () => {
// find drag handle of task
const firstTask = page.locator('task').first();
const dragHandle = firstTask.locator('.drag-handle');
const tagList = firstTask.locator('tag-list');
// Drag and drop to first tag
// Drag and drop to first tag - wait for tag to appear
await dragHandle.dragTo(tag1NavItem);
await page.waitForTimeout(500); // Wait for drag animation and state update
await expect(firstTask.locator('tag-list')).toContainText(`${testPrefix}-Tag1`);
await expect(tagList).toContainText(`${testPrefix}-Tag1`, { timeout: 5000 });
// Drag and drop to second tag
// Drag and drop to second tag - wait for tag to appear
await dragHandle.dragTo(tag2NavItem);
await page.waitForTimeout(500); // Wait for drag animation and state update
await expect(firstTask.locator('tag-list')).toContainText(`${testPrefix}-Tag1`);
await expect(firstTask.locator('tag-list')).toContainText(`${testPrefix}-Tag2`);
await expect(tagList).toContainText(`${testPrefix}-Tag1`);
await expect(tagList).toContainText(`${testPrefix}-Tag2`, { timeout: 5000 });
// Drag and drop again to first tag to remove it
// Drag and drop again to first tag to remove it - wait for tag to disappear
await dragHandle.dragTo(tag1NavItem);
await page.waitForTimeout(500); // Wait for drag animation and state update
await expect(firstTask.locator('tag-list')).not.toContainText(`${testPrefix}-Tag1`);
await expect(firstTask.locator('tag-list')).toContainText(`${testPrefix}-Tag2`);
await expect(tagList).not.toContainText(`${testPrefix}-Tag1`, { timeout: 5000 });
await expect(tagList).toContainText(`${testPrefix}-Tag2`);
});
});

View file

@ -19,11 +19,8 @@ test.describe('Task List - Start/Stop', () => {
await playBtn.waitFor({ state: 'visible' });
await playBtn.click();
// Wait a moment for the class to be applied
await page.waitForTimeout(200);
// Verify the task has the 'isCurrent' class
await expect(firstTask).toHaveClass(/isCurrent/);
// Verify the task has the 'isCurrent' class (auto-waits)
await expect(firstTask).toHaveClass(/isCurrent/, { timeout: 5000 });
// Hover again to ensure button is visible
await firstTask.hover();
@ -31,10 +28,7 @@ test.describe('Task List - Start/Stop', () => {
// Click the play button again to stop the task
await playBtn.click();
// Wait a moment for the class to be removed
await page.waitForTimeout(200);
// Verify the task no longer has the 'isCurrent' class
await expect(firstTask).not.toHaveClass(/isCurrent/);
// Verify the task no longer has the 'isCurrent' class (auto-waits)
await expect(firstTask).not.toHaveClass(/isCurrent/, { timeout: 5000 });
});
});

View file

@ -14,27 +14,20 @@ test.describe('Work View Features', () => {
workViewPage,
testPrefix,
}) => {
test.setTimeout(30000); // Increase timeout
test.setTimeout(30000);
// Wait for work view to be ready
await workViewPage.waitForTaskList();
// Wait for any dialogs to be dismissed
await page.waitForTimeout(2000);
// Verify undone task list is visible
await expect(page.locator(UNDONE_TASK_LIST)).toBeVisible({ timeout: 8000 }); // Reduced from 10s to 8s
await expect(page.locator(UNDONE_TASK_LIST)).toBeVisible({ timeout: 8000 });
// Create tasks
await workViewPage.addTask('Task 1');
await page.waitForSelector(TASK, { state: 'visible', timeout: 4000 }); // Reduced from 5s to 4s
await page.waitForTimeout(500);
await page.locator(TASK).first().waitFor({ state: 'visible', timeout: 5000 });
await workViewPage.addTask('Task 2');
await page.waitForTimeout(1000);
// Verify we have 2 tasks
await expect(page.locator(TASK)).toHaveCount(2);
await expect(page.locator(TASK)).toHaveCount(2, { timeout: 5000 });
// Mark first task as done
const firstTask = page.locator(FIRST_TASK);
@ -48,10 +41,12 @@ test.describe('Work View Features', () => {
await doneBtn.waitFor({ state: 'visible' });
await doneBtn.click();
// Wait a bit for the transition
await page.waitForTimeout(2000);
// Wait for task count in undone list to decrease
await expect(page.locator(`${UNDONE_TASK_LIST} ${TASK}`)).toHaveCount(1, {
timeout: 5000,
});
// Check if done section exists (it might not show if there are no done tasks)
// Check if done section exists
const doneSectionExists = await page
.locator(DONE_TASKS_SECTION)
.isVisible({ timeout: 5000 })
@ -62,7 +57,7 @@ test.describe('Work View Features', () => {
const toggleBtn = page.locator(TOGGLE_DONE_TASKS_BTN);
if (await toggleBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await toggleBtn.click();
await page.waitForTimeout(1000);
await expect(page.locator(DONE_TASK_LIST)).toBeVisible({ timeout: 5000 });
}
// Verify done task list is visible
@ -82,17 +77,19 @@ test.describe('Work View Features', () => {
// Wait for work view to be ready
await workViewPage.waitForTaskList();
await page.waitForTimeout(1000);
// Create multiple tasks
// Create multiple tasks - wait for each to appear before adding next
await workViewPage.addTask('First created');
await page.waitForTimeout(500);
await expect(page.locator(TASK)).toHaveCount(1, { timeout: 5000 });
await workViewPage.addTask('Second created');
await page.waitForTimeout(500);
await expect(page.locator(TASK)).toHaveCount(2, { timeout: 5000 });
await workViewPage.addTask('Third created');
await page.waitForTimeout(500);
await expect(page.locator(TASK)).toHaveCount(3, { timeout: 5000 });
await workViewPage.addTask('Fourth created');
await page.waitForTimeout(500);
await expect(page.locator(TASK)).toHaveCount(4, { timeout: 5000 });
// Verify order (newest first)
await expect(page.locator('task:nth-of-type(1) task-title')).toContainText(

View file

@ -92,10 +92,7 @@ test.describe('Work View', () => {
await page.keyboard.press('Enter');
// Wait for first task to be created
await page.waitForFunction(() => document.querySelectorAll('task').length >= 1, {
timeout: 10000,
});
await page.waitForTimeout(300);
await page.locator('task').first().waitFor({ state: 'visible', timeout: 10000 });
// Add second task
await workViewPage.addTaskGlobalInput.clear();
@ -137,8 +134,8 @@ test.describe('Work View', () => {
// Add two tasks - the addTask method now properly waits for each one
await workViewPage.addTask('test task hihi');
// Wait a bit between tasks to ensure proper state update
await page.waitForTimeout(500);
// Wait for first task to be visible before adding second
await page.locator('task').first().waitFor({ state: 'visible', timeout: 10000 });
await workViewPage.addTask('some other task here');

View file

@ -0,0 +1,117 @@
import type { Locator, Page } from '@playwright/test';
import type { WorkViewPage } from '../pages/work-view.page';
import { fillTimeInput } from './time-input-helper';
// Selectors for scheduling
const DETAIL_PANEL_BTN = '.show-additional-info-btn';
const DETAIL_PANEL_SELECTOR = 'dialog-task-detail-panel, task-detail-panel';
const DETAIL_PANEL_SCHEDULE_ITEM =
'task-detail-item:has(mat-icon:text("alarm")), ' +
'task-detail-item:has(mat-icon:text("today")), ' +
'task-detail-item:has(mat-icon:text("schedule"))';
const RIGHT_PANEL = '.right-panel';
const DIALOG_CONTAINER = 'mat-dialog-container';
const DIALOG_SUBMIT = 'mat-dialog-actions button:last-child';
/**
* Closes the task detail panel if it's currently open.
*/
export const closeDetailPanelIfOpen = async (page: Page): Promise<void> => {
const detailPanel = page.locator(DETAIL_PANEL_SELECTOR).first();
const isVisible = await detailPanel.isVisible().catch(() => false);
if (isVisible) {
await page.keyboard.press('Escape');
await detailPanel.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
}
};
/**
* Opens the detail panel for a task by hovering and clicking the detail button.
*
* @param page - Playwright page object
* @param task - Locator for the task element
*/
export const openTaskDetailPanel = async (page: Page, task: Locator): Promise<void> => {
await task.waitFor({ state: 'visible' });
await task.scrollIntoViewIfNeeded();
await task.hover();
const detailBtn = task.locator(DETAIL_PANEL_BTN).first();
await detailBtn.waitFor({ state: 'visible', timeout: 5000 });
await detailBtn.click();
// Wait for detail panel to be visible
await page
.locator(RIGHT_PANEL)
.or(page.locator(DETAIL_PANEL_SELECTOR))
.first()
.waitFor({ state: 'visible', timeout: 10000 });
};
/**
* Schedules a task via the detail panel.
*
* @param page - Playwright page object
* @param task - Locator for the task element
* @param scheduleTime - Date object or timestamp for when to schedule
*/
export const scheduleTaskViaDetailPanel = async (
page: Page,
task: Locator,
scheduleTime: Date | number,
): Promise<void> => {
await openTaskDetailPanel(page, task);
// Click the schedule item
const scheduleItem = page.locator(DETAIL_PANEL_SCHEDULE_ITEM).first();
await scheduleItem.waitFor({ state: 'visible', timeout: 5000 });
await scheduleItem.click();
// Wait for schedule dialog
const dialogContainer = page.locator(DIALOG_CONTAINER);
await dialogContainer.waitFor({ state: 'visible', timeout: 10000 });
// Fill time input
await fillTimeInput(page, scheduleTime);
// Submit dialog
const submitBtn = page.locator(DIALOG_SUBMIT);
await submitBtn.waitFor({ state: 'visible', timeout: 5000 });
await submitBtn.click();
// Wait for dialog to close
await dialogContainer.waitFor({ state: 'hidden', timeout: 10000 });
// Close detail panel if open
await closeDetailPanelIfOpen(page);
};
// Default schedule delta: 5 seconds from now
const DEFAULT_SCHEDULE_DELTA = 5000;
/**
* Adds a task and schedules it with a reminder.
* This is a convenience function combining task creation and scheduling.
*
* @param page - Playwright page object
* @param workViewPage - WorkViewPage instance
* @param taskTitle - Title for the new task
* @param scheduleTime - Date object or timestamp for when to schedule (defaults to 5s from now)
*/
export const addTaskWithReminder = async (
page: Page,
workViewPage: WorkViewPage,
taskTitle: string,
scheduleTime: Date | number = Date.now() + DEFAULT_SCHEDULE_DELTA,
): Promise<void> => {
// Add the task
await workViewPage.addTask(taskTitle);
// Find the task by title
const escapedTitle = taskTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const task = page.locator(`task:has-text("${escapedTitle}")`).first();
await task.waitFor({ state: 'visible', timeout: 10000 });
// Schedule it
await scheduleTaskViaDetailPanel(page, task, scheduleTime);
};

View file

@ -0,0 +1,59 @@
import { type Page, expect } from '@playwright/test';
/**
* Fills a time input field with the specified time.
* Handles both mat-form-field wrapped inputs and plain time inputs.
* Uses retry logic to ensure the value is properly set.
*
* @param page - Playwright page object
* @param scheduleTime - Date object or timestamp for the desired time
*/
export const fillTimeInput = async (
page: Page,
scheduleTime: Date | number,
): Promise<void> => {
const d = new Date(scheduleTime);
const timeValue = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
// Try multiple selectors for the time input
const timeInput = page
.locator('mat-dialog-container input[type="time"]')
.or(page.locator('mat-form-field input[type="time"]'))
.or(page.locator('input[type="time"]'))
.first();
await timeInput.waitFor({ state: 'visible', timeout: 10000 });
// Click and focus the input
await timeInput.click();
// Clear and fill with small delays for stability
await timeInput.clear();
await timeInput.fill(timeValue);
// Verify with retry - if fill() didn't work, use evaluate fallback
const inputValue = await timeInput.inputValue();
if (inputValue !== timeValue) {
await page.evaluate(
({ value }: { value: string }) => {
const timeInputEl = document.querySelector(
'mat-form-field input[type="time"]',
) as HTMLInputElement;
if (timeInputEl) {
timeInputEl.value = value;
timeInputEl.dispatchEvent(new Event('input', { bubbles: true }));
timeInputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
},
{ value: timeValue },
);
}
// Verify the value was set
await expect(async () => {
const value = await timeInput.inputValue();
expect(value).toBe(timeValue);
}).toPass({ timeout: 5000 });
// Tab out to commit the value
await page.keyboard.press('Tab');
};