mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
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:
parent
24c008df92
commit
11d85208e5
28 changed files with 571 additions and 620 deletions
|
|
@ -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
34
e2e/constants/timeouts.ts
Normal 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;
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
117
e2e/utils/schedule-task-helper.ts
Normal file
117
e2e/utils/schedule-task-helper.ts
Normal 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);
|
||||
};
|
||||
59
e2e/utils/time-input-helper.ts
Normal file
59
e2e/utils/time-input-helper.ts
Normal 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');
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue