From db4dbb1aa9efce77d876e83d93d4be2c67ca288a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 14 Nov 2025 15:23:17 +0100 Subject: [PATCH] fix: effect errors and e2e tests --- .../add-task-bar-date-picker.spec.ts | 44 +++++++++++++++-- e2e/tests/task-dragdrop/task-dragdrop.spec.ts | 48 ++++++++----------- src/app/core/theme/global-theme.service.ts | 42 +++++++++++----- .../add-task-bar-actions.component.html | 1 + .../select-task-minimal.component.ts | 37 +++++++------- src/app/plugins/plugin.service.ts | 13 ++++- 6 files changed, 121 insertions(+), 64 deletions(-) diff --git a/e2e/tests/add-task-bar/add-task-bar-date-picker.spec.ts b/e2e/tests/add-task-bar/add-task-bar-date-picker.spec.ts index 084fdaf94d..8672535381 100644 --- a/e2e/tests/add-task-bar/add-task-bar-date-picker.spec.ts +++ b/e2e/tests/add-task-bar/add-task-bar-date-picker.spec.ts @@ -4,8 +4,9 @@ import { expect, test } from '../../fixtures/test.fixture'; const ADD_TASK_BAR = 'add-task-bar.global'; const ADD_TASK_INPUT = `${ADD_TASK_BAR} input`; const DUE_BUTTON = `${ADD_TASK_BAR} [data-test="add-task-bar-due-btn"]`; +const CLEAR_DUE_BUTTON = `${ADD_TASK_BAR} [data-test="add-task-bar-clear-due-btn"]`; const SCHEDULE_DIALOG = 'dialog-schedule-task'; -const QUICK_ACCESS_BTN = `${SCHEDULE_DIALOG} .quick-access button`; +const QUICK_ACCESS_BTN = '.quick-access button'; const ensureGlobalAddTaskBarOpen = async (page: Page): Promise => { const addTaskInput = page.locator(ADD_TASK_INPUT).first(); @@ -34,6 +35,40 @@ const openScheduleDialogFromBar = async ( return { dialog, dueButton }; }; +const resetDueDateIfNeeded = async (page: Page): Promise => { + const dueButton = page.locator(DUE_BUTTON).first(); + await dueButton.waitFor({ state: 'visible', timeout: 10000 }); + const clearButton = page.locator(CLEAR_DUE_BUTTON).first(); + if (await clearButton.isVisible().catch(() => false)) { + await clearButton.click(); + await expect(dueButton).not.toHaveClass(/has-value/); + } +}; + +const getDueStateFromLocator = async ( + dueButton: Locator, +): Promise<{ hasValue: boolean; label: string }> => { + const classAttr = (await dueButton.getAttribute('class')) || ''; + const hasValue = classAttr.includes('has-value'); + const label = ((await dueButton.textContent()) || '').replace(/\s+/g, ' ').trim(); + return { hasValue, label }; +}; + +const getDueState = async (page: Page): Promise<{ hasValue: boolean; label: string }> => { + const dueButton = page.locator(DUE_BUTTON).first(); + await dueButton.waitFor({ state: 'visible', timeout: 10000 }); + return getDueStateFromLocator(dueButton); +}; + +const expectDueStateUnchanged = async ( + dueButton: Locator, + initial: { hasValue: boolean; label: string }, +): Promise => { + const current = await getDueStateFromLocator(dueButton); + expect(current.hasValue).toBe(initial.hasValue); + expect(current.label).toBe(initial.label); +}; + test.describe('Add Task Bar date picker', () => { test.beforeEach(async ({ workViewPage }) => { await workViewPage.waitForTaskList(); @@ -44,6 +79,7 @@ test.describe('Add Task Bar date picker', () => { testPrefix, }) => { const input = await ensureGlobalAddTaskBarOpen(page); + await resetDueDateIfNeeded(page); await input.fill(`${testPrefix}-today quick date`); const { dialog, dueButton } = await openScheduleDialogFromBar(page); @@ -60,6 +96,7 @@ test.describe('Add Task Bar date picker', () => { testPrefix, }) => { const input = await ensureGlobalAddTaskBarOpen(page); + await resetDueDateIfNeeded(page); await input.fill(`${testPrefix}-tomorrow quick date`); const { dialog, dueButton } = await openScheduleDialogFromBar(page); @@ -76,14 +113,15 @@ test.describe('Add Task Bar date picker', () => { testPrefix, }) => { const input = await ensureGlobalAddTaskBarOpen(page); + await resetDueDateIfNeeded(page); await input.fill(`${testPrefix}-cancel quick date`); + const initialDueState = await getDueState(page); const { dialog, dueButton } = await openScheduleDialogFromBar(page); await dialog.locator('button:has-text("Cancel")').click(); await dialog.waitFor({ state: 'hidden', timeout: 10000 }); await expect(page.locator(ADD_TASK_INPUT)).toBeVisible(); - await expect(dueButton).not.toHaveClass(/has-value/); - await expect(dueButton).toContainText(/due/i); + await expectDueStateUnchanged(dueButton, initialDueState); }); }); diff --git a/e2e/tests/task-dragdrop/task-dragdrop.spec.ts b/e2e/tests/task-dragdrop/task-dragdrop.spec.ts index c550161ec5..7803f23813 100644 --- a/e2e/tests/task-dragdrop/task-dragdrop.spec.ts +++ b/e2e/tests/task-dragdrop/task-dragdrop.spec.ts @@ -1,3 +1,4 @@ +import { Locator } from '@playwright/test'; import { test, expect } from '../../fixtures/test.fixture'; import { WorkViewPage } from '../../pages/work-view.page'; @@ -21,7 +22,6 @@ test.describe('Drag Task to change project and labels', () => { await workViewPage.addTask('TestTask'); await page.waitForSelector('task', { state: 'visible' }); - const projectNameInput = page.locator('dialog-create-project input').first(); const project1NavItem = page .getByRole('menuitem') .filter({ hasText: `${testPrefix}-TestProject 1` }); @@ -29,35 +29,27 @@ test.describe('Drag Task to change project and labels', () => { .getByRole('menuitem') .filter({ hasText: `${testPrefix}-TestProject 2` }); - // Add first project - await page.keyboard.press('Shift+P'); + // Helper to create projects reliably by clicking the dialog's Submit button + const createProject = async ( + name: string, + expectedNavItem: Locator, + ): Promise => { + const dialog = page.locator('dialog-create-project'); + const projectNameInput = dialog.locator('input').first(); + const saveBtn = dialog.locator('button[type="submit"]'); - // Wait for dialog and input to be visible - await page.waitForSelector('dialog-create-project', { state: 'visible' }); - await projectNameInput.waitFor({ state: 'visible', timeout: 15000 }); - await projectNameInput.fill(`${testPrefix}-TestProject 1`); - await page.keyboard.press('Enter'); - // Wait for dialog to close and nav item to appear - await page.waitForSelector('dialog-create-project', { - state: 'hidden', - timeout: 5000, - }); - await project1NavItem.waitFor({ state: 'visible', timeout: 5000 }); + await page.keyboard.press('Shift+P'); + await dialog.waitFor({ state: 'visible', timeout: 10000 }); + await projectNameInput.waitFor({ state: 'visible', timeout: 10000 }); + await projectNameInput.fill(name); + await expect(saveBtn).toBeEnabled({ timeout: 5000 }); + await saveBtn.click(); + await dialog.waitFor({ state: 'hidden', timeout: 10000 }); + await expectedNavItem.waitFor({ state: 'visible', timeout: 10000 }); + }; - // Add another project - await page.keyboard.press('Shift+P'); - - // Wait for dialog and input to be visible - await page.waitForSelector('dialog-create-project', { state: 'visible' }); - await projectNameInput.waitFor({ state: 'visible', timeout: 15000 }); - await projectNameInput.fill(`${testPrefix}-TestProject 2`); - await page.keyboard.press('Enter'); - // Wait for dialog to close and nav item to appear - await page.waitForSelector('dialog-create-project', { - state: 'hidden', - timeout: 5000, - }); - await project2NavItem.waitFor({ state: 'visible', timeout: 5000 }); + await createProject(`${testPrefix}-TestProject 1`, project1NavItem); + await createProject(`${testPrefix}-TestProject 2`, project2NavItem); // find drag handle of task const firstTask = page.locator('task').first(); diff --git a/src/app/core/theme/global-theme.service.ts b/src/app/core/theme/global-theme.service.ts index a2810007a2..2b7cb23e59 100644 --- a/src/app/core/theme/global-theme.service.ts +++ b/src/app/core/theme/global-theme.service.ts @@ -1,4 +1,11 @@ -import { effect, inject, Injectable, signal } from '@angular/core'; +import { + effect, + EnvironmentInjector, + inject, + Injectable, + runInInjectionContext, + signal, +} from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { BodyClass, IS_ELECTRON } from '../../app.constants'; import { IS_MAC } from '../../util/is-mac'; @@ -43,6 +50,8 @@ export class GlobalThemeService { private _imexMetaService = inject(ImexViewService); private _http = inject(HttpClient); private _customThemeService = inject(CustomThemeService); + private _environmentInjector = inject(EnvironmentInjector); + private _hasInitialized = false; darkMode = signal( (localStorage.getItem(LS.DARK_MODE) as DarkModeCfg) || 'system', @@ -81,20 +90,27 @@ export class GlobalThemeService { backgroundImg = toSignal(this._backgroundImgObs$); init(): void { - // This is here to make web page reloads on non-work-context pages at least usable - this._setBackgroundTint(true); - this._initIcons(); - this._initHandlersForInitialBodyClasses(); - this._initThemeWatchers(); + if (this._hasInitialized) { + return; + } + this._hasInitialized = true; - // Set up dark mode persistence effect - effect(() => { - const darkMode = this.darkMode(); - localStorage.setItem(LS.DARK_MODE, darkMode); + runInInjectionContext(this._environmentInjector, () => { + // This is here to make web page reloads on non-work-context pages at least usable + this._setBackgroundTint(true); + this._initIcons(); + this._initHandlersForInitialBodyClasses(); + this._initThemeWatchers(); + + // Set up dark mode persistence effect + effect(() => { + const darkMode = this.darkMode(); + localStorage.setItem(LS.DARK_MODE, darkMode); + }); + + // Set up reactive custom theme updates + this._setupCustomThemeEffect(); }); - - // Set up reactive custom theme updates - this._setupCustomThemeEffect(); } private _setDarkTheme(isDarkTheme: boolean): void { diff --git a/src/app/features/tasks/add-task-bar/add-task-bar-actions/add-task-bar-actions.component.html b/src/app/features/tasks/add-task-bar/add-task-bar-actions/add-task-bar-actions.component.html index 9404ca1d74..e510a83619 100644 --- a/src/app/features/tasks/add-task-bar/add-task-bar-actions/add-task-bar-actions.component.html +++ b/src/app/features/tasks/add-task-bar/add-task-bar-actions/add-task-bar-actions.component.html @@ -40,6 +40,7 @@