From ab0371fac6a5470b6e80ebe240e9cccb97d24f9f Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 19 Dec 2025 09:59:44 +0100 Subject: [PATCH] fix(e2e): improve supersync test stability and build compatibility E2E test improvements: - Increase timeouts for sync button and add task bar visibility checks - Add retry logic for sync button wait in setupSuperSync - Handle dialog close race conditions in save button click - Fix simple counter test to work with collapsible sections and inline forms Build fixes: - Add es2022 lib/target and baseUrl to electron tsconfig - Include window-ea.d.ts for proper type resolution - Add @ts-ignore for import.meta.url in reminder service for Electron build --- e2e/pages/base.page.ts | 18 +++--- e2e/pages/supersync.page.ts | 49 ++++++++++++++- .../sync/supersync-simple-counter.spec.ts | 59 +++++++++++++------ electron/tsconfig.electron.json | 9 ++- src/app/features/reminder/reminder.service.ts | 1 + 5 files changed, 106 insertions(+), 30 deletions(-) diff --git a/e2e/pages/base.page.ts b/e2e/pages/base.page.ts index 492eb0674..fceabd307 100644 --- a/e2e/pages/base.page.ts +++ b/e2e/pages/base.page.ts @@ -27,18 +27,22 @@ export abstract class BasePage { const inputEl = this.page.locator('add-task-bar.global input'); - // If the global input is not present, open the Add Task Bar first - const inputCount = await inputEl.count(); - if (inputCount === 0) { + // Check if input is visible - if not, try clicking the add button + const isInputVisible = await inputEl + .first() + .isVisible() + .catch(() => false); + if (!isInputVisible) { const addBtn = this.page.locator('.tour-addBtn'); - await addBtn.waitFor({ state: 'visible', timeout: 10000 }); + // Wait for add button with longer timeout - it depends on config loading + await addBtn.waitFor({ state: 'visible', timeout: 20000 }); await addBtn.click(); // Wait for input to appear after clicking - await this.page.waitForTimeout(300); + await this.page.waitForTimeout(500); } - // Ensure input is visible and interactable - await inputEl.first().waitFor({ state: 'visible', timeout: 15000 }); + // Ensure input is visible and interactable with longer timeout + await inputEl.first().waitFor({ state: 'visible', timeout: 20000 }); await inputEl.first().waitFor({ state: 'attached', timeout: 5000 }); // Wait for Angular to stabilize before interacting diff --git a/e2e/pages/supersync.page.ts b/e2e/pages/supersync.page.ts index ea1446eed..7de3ae4fe 100644 --- a/e2e/pages/supersync.page.ts +++ b/e2e/pages/supersync.page.ts @@ -67,7 +67,18 @@ export class SuperSyncPage extends BasePage { */ async setupSuperSync(config: SuperSyncConfig): Promise { // Wait for sync button to be ready first - await this.syncBtn.waitFor({ state: 'visible', timeout: 10000 }); + // The sync button depends on globalConfig being loaded (isSyncIconEnabled), + // which can take time after initial app load. Use longer timeout and retry. + const syncBtnTimeout = 30000; + try { + await this.syncBtn.waitFor({ state: 'visible', timeout: syncBtnTimeout }); + } catch { + // If sync button not visible, the app might not be fully loaded + // Wait a bit more and try once more + console.log('[SuperSyncPage] Sync button not found initially, waiting longer...'); + await this.page.waitForTimeout(2000); + await this.syncBtn.waitFor({ state: 'visible', timeout: syncBtnTimeout }); + } // Use right-click to always open sync settings dialog // (left-click triggers sync if already configured) @@ -157,8 +168,40 @@ export class SuperSyncPage extends BasePage { } } - // Save - await this.saveBtn.click(); + // Save - use a robust click that handles element detachment during dialog close + // The dialog may close and navigation may start before click completes + try { + // Wait for button to be stable before clicking + await this.saveBtn.waitFor({ state: 'visible', timeout: 5000 }); + await this.page.waitForTimeout(100); // Brief settle + + // Click and don't wait for navigation to complete - just initiate the action + await Promise.race([ + this.saveBtn.click({ timeout: 5000 }), + // If dialog closes quickly, the click may fail - that's OK if dialog is gone + this.page + .locator('mat-dialog-container') + .waitFor({ state: 'detached', timeout: 5000 }), + ]); + } catch (e) { + // If click failed but dialog is already closed, that's fine + const dialogStillOpen = await this.page + .locator('mat-dialog-container') + .isVisible() + .catch(() => false); + if (dialogStillOpen) { + // Dialog still open - the click actually failed + throw e; + } + // Dialog closed - click worked or was unnecessary + console.log('[SuperSyncPage] Dialog closed (click may have been interrupted)'); + } + + // Wait for dialog to fully close + await this.page + .locator('mat-dialog-container') + .waitFor({ state: 'detached', timeout: 5000 }) + .catch(() => {}); // Check if sync starts automatically (it should if enabled) try { diff --git a/e2e/tests/sync/supersync-simple-counter.spec.ts b/e2e/tests/sync/supersync-simple-counter.spec.ts index c1efced13..6581972fd 100644 --- a/e2e/tests/sync/supersync-simple-counter.spec.ts +++ b/e2e/tests/sync/supersync-simple-counter.spec.ts @@ -48,32 +48,51 @@ base.describe('@supersync Simple Counter Sync', () => { ); await settingsBtn.waitFor({ state: 'visible', timeout: 15000 }); await settingsBtn.click(); - await client.page.waitForURL(/settings/); + await client.page.waitForURL(/config/); await client.page.waitForTimeout(500); - // Click on Simple Counters section + // Click on Simple Counters section (it's inside a collapsible component) + // The translated title is "Simple Counters & Habit Tracking" + // It's under "Productivity Helper" section, may need to scroll to see it const simpleCountersSection = client.page.locator( - 'section-header:has-text("Simple Counters")', + '.collapsible-header:has-text("Simple Counter")', ); + + // Scroll to section and wait for it + await simpleCountersSection.scrollIntoViewIfNeeded(); await simpleCountersSection.waitFor({ state: 'visible', timeout: 10000 }); await simpleCountersSection.click(); - await client.page.waitForTimeout(300); - // Click Add Counter button - const addBtn = client.page.locator('button:has-text("Add Counter")'); + // Wait for collapsible to expand + await client.page.waitForTimeout(500); + + // Click Add Counter button - text is "Add simple counter/ habit" + // This is a formly repeat type that adds fields inline (not a dialog) + // The repeat section type has a footer with the add button + const addBtn = client.page.locator( + 'repeat-section-type .footer button, button:has-text("Add simple counter")', + ); + await addBtn.scrollIntoViewIfNeeded(); await addBtn.waitFor({ state: 'visible', timeout: 5000 }); await addBtn.click(); - // Wait for dialog - const dialog = client.page.locator('dialog-simple-counter-edit'); - await expect(dialog).toBeVisible({ timeout: 5000 }); + // Wait for inline form fields to appear + await client.page.waitForTimeout(500); - // Fill title - const titleInput = dialog.locator('input[formcontrolname="title"]'); + // Find the newly added counter row (last one in the list) + // The repeat section type creates .row elements inside .list-wrapper + const counterRows = client.page.locator('repeat-section-type .row'); + const lastCounterRow = counterRows.last(); + + // Fill title - find the title input in the last counter row + const titleInput = lastCounterRow.locator('input').first(); + await titleInput.scrollIntoViewIfNeeded(); + await titleInput.waitFor({ state: 'visible', timeout: 5000 }); await titleInput.fill(title); - // Select type - const typeSelect = dialog.locator('mat-select[formcontrolname="type"]'); + // Select type - find the select in the last counter row + const typeSelect = lastCounterRow.locator('mat-select').first(); + await typeSelect.scrollIntoViewIfNeeded(); await typeSelect.click(); await client.page.waitForTimeout(300); const typeOption = client.page.locator( @@ -81,12 +100,18 @@ base.describe('@supersync Simple Counter Sync', () => { ); await typeOption.click(); - // Save - const saveBtn = dialog.locator('button:has-text("Save")'); + // Wait for dropdown to close + await client.page.waitForTimeout(300); + + // Save the form - the simple counter cfg has a Save button + const saveBtn = client.page.locator( + 'simple-counter-cfg button:has-text("Save"), .submit-button:has-text("Save")', + ); + await saveBtn.scrollIntoViewIfNeeded(); await saveBtn.click(); - // Wait for dialog to close - await expect(dialog).not.toBeVisible({ timeout: 5000 }); + // Wait for save to complete + await client.page.waitForTimeout(500); // Navigate back to work view using home button or similar await client.page.goto('/#/tag/TODAY/tasks'); diff --git a/electron/tsconfig.electron.json b/electron/tsconfig.electron.json index 2a9693743..2765bae26 100644 --- a/electron/tsconfig.electron.json +++ b/electron/tsconfig.electron.json @@ -10,9 +10,12 @@ "skipLibCheck": true, "typeRoots": ["node_modules/@types"], "downlevelIteration": true, - "lib": ["dom"], - "esModuleInterop": true + "lib": ["dom", "es2022"], + "target": "es2022", + "module": "commonjs", + "esModuleInterop": true, + "baseUrl": ".." }, - "include": ["main.ts", "**/*.ts"], + "include": ["main.ts", "**/*.ts", "../src/app/core/window-ea.d.ts"], "exclude": ["../node_modules", "**/*.spec.ts"] } diff --git a/src/app/features/reminder/reminder.service.ts b/src/app/features/reminder/reminder.service.ts index 2c01cc91e..67ef15c24 100644 --- a/src/app/features/reminder/reminder.service.ts +++ b/src/app/features/reminder/reminder.service.ts @@ -56,6 +56,7 @@ export class ReminderService { throw new Error('No service workers supported :('); } + // @ts-ignore - import.meta.url works in browser ES modules; ignore for electron CommonJS build this._w = new Worker(new URL('./reminder.worker', import.meta.url), { name: 'reminder', type: 'module',