super-productivity/e2e/pages/task.page.ts
Maikel Hajiabadi c109ad1fa5
Update recurrent task calendar and general calendar design (#8017)
* feat(task-repeat-cfg): use schedule dialog picker for recurring task start date

* refactor(calendar): extract shared DateTimePickerComponent and use in schedule/deadline dialogs

* style(calendar): make primary button gray when disabled and stronger green when enabled

* style(calendar): upgrade date picker calendar visuals and remove outlines

* fix(task-repeat-cfg): prevent premature duration formatting during typing

* fix: resolve merge conflict and fix lint errors in dialog components

* test(e2e): update recurring task tests to match new schedule dialog UI

* test(e2e): fix regressions and ambiguous locators in recurring task tests

Resolves strict mode violations for 'Schedule' button and updates locators to handle the new separate schedule dialog robustly using the datetime-picker filter.

* fix: hide unschedule button in schedule dialog when opened from recurring task cfg

* fix(datetime-picker): restore calendar first-week weekday alignment by using visibility:hidden instead of display:none for body-label cell

* fix(i18n): use active UI language for date locale fallback, show full month names, and translate hardcoded Time label

* test: fix DateTimeFormatService unit tests by mocking TranslateService

* fix(task-repeat): add default reminders to recurring tasks

* test(sync): increase lock reentry timeout to prevent flakiness

* style(ui): align calendar cell shapes and make hover color darker

* style(ui): style inputs and action buttons in schedule and deadline dialogs

* feat(task-repeat): enable quick access scheduling for recurring tasks

* feat(ui): separate month and year into distinct header buttons in datetime picker

* style(ui): show dropdown arrow next to the year only

* fix(locale): map UI fallbacks to parse-safe locales and sync DateAdapter

* fix(task-repeat): fix option caching, DST normalization, and schedule dialog time preservation

* test(task-repeat): mock formatTime in repeat dialog spec

* fix(ui): improve datetime-picker month/year picking, arrow direction, transitions, and highlighter sync

* fix(ui): defer year selection view transition and avoid year picker pagination updating highlighter

* style(ui): restore focus outline and default material look

* style(ui): drop custom hover circle and pill shape, and revert month-grid names to default short format

* fix(ui): gate schedule warnings on isSelectDueOnly

* style(ui): remove custom button color overrides

* style(ui): restore default Material hover and focus behavior

* fix(ui): add calendar header aria-labels and mirror arrows in RTL

* test(e2e): fix recurring task start date specs for new datetime picker

* revert(date-time): restore master's browser-region-first fallback in DateTimeFormatService

* fix(ui): make quick-access tooltips configurable on datetime-picker and preserve deadline labels

* fix(ui): exclude disabled previous/next calendar buttons from custom hover style

* fix(ui): align period header button aria-labels with actual view transitions

* style(ui): refactor recurring start date button styling with logical CSS and remove any type

* fix(ui): preserve selected year when navigating in year picker and toggling back

* style(ui): nudge default calendar cell hover with a shared token

* fix(datetime-picker): sync hover selection with keyboard navigation and focus on open

* fix(deadline): grey out past days in deadline calendar

* style(datetime-picker): use shared calendar hover background for header buttons

* style(calendar): use primary mixed color globally for calendar hover background

* style(datetime-picker): hide mouse on arrow key nav and unify hover/focus colors

* style(datetime-picker): resolve selected cell highlight bug and update hover outlines

* style(datetime-picker): style current date/month/year selectors with accent color

* fix(task-repeat-cfg): guard repeat schedule opening with isValidSplitTime

* fix(task-repeat-cfg): only persist remindAt when a valid time is present

* fix(ui): add disabled guards for calendar cells and improve scheduling result

* fix(ui): improve focus management and navigation in datetime-picker

* fix(ui): prevent invalid 24:00 time in datetime-picker and remove debug log

* refactor(ui): cleanup datetime-picker and improve calendar header logic

* feat(ui): enable year navigation and consistent month selection in datetime-picker

* fix(ui): align multi-year boundary logic with Angular Material anchoring

* fix(repeat): restore past start-date floor for existing repeat configs

* test(e2e): fix setRecurStartDate to correctly navigate month and year

* test(e2e): remove duplicate helper declarations causing SyntaxError

* refactor(calendar): de-risk calendar by reverting brittle hover sync and custom header

* test(calendar): update unit tests for datetime-picker de-risking

* fix(calendar): restore standard year-to-month drill-down navigation

* test(e2e): fix recurring task and planner task compatibility

- Update setRecurStartDate helper to use standard Material calendar
  drill-down navigation, fixing compatibility with the new UI.
- Update TaskPage helper to support both 'task' and 'planner-task'
  elements in board and planner views.

* test(e2e): fix recurring task date selection and calendar navigation

- Update setRecurStartDate helper to use correct click sequence for the new date selector header.
- Remove minDate restriction in recurring task edit dialog to allow moving start dates earlier (#7423).

* style(ui): remove stale calendar overrides and localize header labels

* test(e2e): fix failing task detail date test due to calendar i18n label change

* fix(ui): remove redundant global locale mutation from DateTimePickerComponent

* fix(ui): prevent calendar navigation reset when date value hasn't changed

* fix(planner): wire showQuickAccess and disable auto-submit in select-due-only mode

* fix(repeat): allow past dates when editing repeat configurations

* fix(repeat): use DateService for logical today check

* fix(test): add coverage for navigation guard and interactive flows in DateTimePickerComponent

* fix(test): restore time handling coverage for select-due-only mode in DialogScheduleTaskComponent

* fix(tasks): implement robust reminder quantization using mid-points and add tests

* feat(planner): add stable data-test-id locators for schedule dialog buttons

* fix(e2e): replace fragile translated 'Schedule' locators with data-test-id

* fix(ui): improve calendar styling and i18n consistency

* refactor(ui): cleanup unused code in DateTimePickerComponent

* fix(planner): restore auto-submit on quick-access in DialogScheduleTaskComponent

* fix(repeat): refine minDate strategy for recurring configurations

* chore(i18n): restore de.json to master
2026-06-09 16:30:22 +02:00

270 lines
7.2 KiB
TypeScript

import { Locator, Page } from '@playwright/test';
import { BasePage } from './base.page';
import { cssSelectors } from '../constants/selectors';
import { waitForAngularStability } from '../utils/waits';
const { TASK, FIRST_TASK, TASK_DONE_BTN, TASK_TEXTAREA, SUB_TASK } = cssSelectors;
export class TaskPage extends BasePage {
constructor(page: Page, testPrefix: string = '') {
super(page, testPrefix);
}
/**
* Get a task by index (1-based)
*/
getTask(index: number = 1): Locator {
if (index === 1) {
return this.page.locator(FIRST_TASK);
}
return this.page.locator(TASK).nth(index - 1);
}
/**
* Get a task by text content
*/
getTaskByText(text: string): Locator {
return this.page.locator(TASK).filter({ hasText: text });
}
/**
* Get all tasks
*/
getAllTasks(): Locator {
return this.page.locator(TASK);
}
/**
* Get task count
*/
async getTaskCount(): Promise<number> {
return await this.page.locator(TASK).count();
}
/**
* Get task title element for a specific task
*/
getTaskTitle(task: Locator): Locator {
return task.locator('task-title');
}
/**
* Mark a task as done
*/
async markTaskAsDone(task: Locator): Promise<void> {
await task.waitFor({ state: 'visible' });
// `data-task-id` is only bound on the <task> host. Wrapper components such
// as <planner-task> (Boards) render the same done-toggle but expose no id,
// so the done-confirmation strategy is chosen based on its presence.
const taskId = await task.getAttribute('data-task-id');
await task.hover();
// Give hover effects time to settle
await this.page.waitForTimeout(100);
const doneBtn = task.locator(TASK_DONE_BTN);
await doneBtn.waitFor({ state: 'visible', timeout: 5000 });
await doneBtn.click();
if (taskId) {
// <task> rows: the done animation can briefly render old and new rows
// with the same id, so wait until every matching row reflects done.
await this.page.waitForFunction(
(id) => {
const matchingTasks = Array.from(document.querySelectorAll('task')).filter(
(el) => el.getAttribute('data-task-id') === id,
);
return (
matchingTasks.length > 0 &&
matchingTasks.every((el) => el.classList.contains('isDone'))
);
},
taskId,
{ timeout: 10000 },
);
} else {
// Wrappers like <planner-task> (Boards) can relocate the task to another
// panel on done (e.g. IN_PROGRESS → DONE), detaching this locator — so a
// done-state assertion here is unreliable. Let the toggle's 200ms
// animation and state change settle; callers assert the resulting panel.
await this.page.waitForTimeout(300);
}
await waitForAngularStability(this.page);
}
/**
* Mark the first task as done
*/
async markFirstTaskAsDone(): Promise<void> {
const firstTask = this.getTask(1);
await this.markTaskAsDone(firstTask);
}
/**
* Edit task title
*/
async editTaskTitle(task: Locator, newTitle: string): Promise<void> {
const titleElement = this.getTaskTitle(task);
await titleElement.waitFor({ state: 'visible' });
await titleElement.click();
const textarea = task.locator(TASK_TEXTAREA);
await textarea.waitFor({ state: 'visible', timeout: 3000 });
await textarea.clear();
await this.page.waitForTimeout(50);
await textarea.fill(newTitle);
await this.page.keyboard.press('Tab'); // Blur to save
await waitForAngularStability(this.page);
}
/**
* Open task detail panel (additional info)
*/
async openTaskDetail(task: Locator): Promise<void> {
await task.waitFor({ state: 'visible' });
await task.hover();
const showDetailBtn = this.page.getByRole('button', {
name: 'Show/hide task panel',
});
await showDetailBtn.waitFor({ state: 'visible', timeout: 3000 });
await showDetailBtn.click();
await this.page.waitForTimeout(300);
}
/**
* Open detail panel for the first task
*/
async openFirstTaskDetail(): Promise<void> {
const firstTask = this.getTask(1);
await this.openTaskDetail(firstTask);
}
/**
* Get subtasks for a task
*/
getSubTasks(task: Locator): Locator {
return task.locator(SUB_TASK);
}
/**
* Get subtask count for a task
*/
async getSubTaskCount(task: Locator): Promise<number> {
return await this.getSubTasks(task).count();
}
/**
* Check if a task is marked as done
*/
async isTaskDone(task: Locator): Promise<boolean> {
const classes = await task.getAttribute('class');
return classes?.includes('isDone') || false;
}
/**
* Get done tasks
*/
getDoneTasks(): Locator {
return this.page.locator(`${TASK}.isDone`);
}
/**
* Get undone tasks
*/
getUndoneTasks(): Locator {
return this.page.locator(`${TASK}:not(.isDone)`);
}
/**
* Get done task count
*/
async getDoneTaskCount(): Promise<number> {
return await this.getDoneTasks().count();
}
/**
* Get undone task count
*/
async getUndoneTaskCount(): Promise<number> {
return await this.getUndoneTasks().count();
}
/**
* Wait for a task to appear with specific text
*/
async waitForTaskWithText(text: string, timeout: number = 10000): Promise<Locator> {
const task = this.getTaskByText(text);
await task.waitFor({ state: 'visible', timeout });
return task;
}
/**
* Wait for task count to match expected count
*/
async waitForTaskCount(expectedCount: number, timeout: number = 10000): Promise<void> {
await this.page.waitForFunction(
(args) => {
const currentCount = document.querySelectorAll('task, planner-task').length;
return currentCount === args.expectedCount;
},
{ expectedCount },
{ timeout },
);
}
/**
* Get task tags
*/
getTaskTags(task: Locator): Locator {
return task.locator('tag');
}
/**
* Check if task has a specific tag
*/
async taskHasTag(task: Locator, tagName: string): Promise<boolean> {
const tags = this.getTaskTags(task);
const tagCount = await tags.count();
for (let i = 0; i < tagCount; i++) {
const tagText = await tags.nth(i).textContent();
if (tagText?.includes(tagName)) {
return true;
}
}
return false;
}
/**
* Toggle task detail panel
*/
async toggleTaskDetail(task: Locator): Promise<void> {
await task.hover();
const toggleBtn = this.page.getByRole('button', {
name: 'Show/hide task panel',
});
await toggleBtn.click();
await this.page.waitForTimeout(300);
}
/**
* Start/Stop task time tracking
*/
async toggleTaskTimeTracking(task: Locator): Promise<void> {
await task.waitFor({ state: 'visible' });
await task.hover();
const playBtn = task.locator('.play-btn, .pause-btn').first();
await playBtn.waitFor({ state: 'visible', timeout: 3000 });
await playBtn.click();
await waitForAngularStability(this.page);
}
/**
* Get the date info element (created/completed) in task detail
*/
getDateInfo(infoPrefix: string): Locator {
return this.page
.locator('.edit-date-info')
.filter({ hasText: new RegExp(infoPrefix) });
}
}