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
This commit is contained in:
Johannes Millan 2025-12-19 09:59:44 +01:00
parent d54156dda3
commit ab0371fac6
5 changed files with 106 additions and 30 deletions

View file

@ -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

View file

@ -67,7 +67,18 @@ export class SuperSyncPage extends BasePage {
*/
async setupSuperSync(config: SuperSyncConfig): Promise<void> {
// 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 {

View file

@ -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');

View file

@ -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"]
}

View file

@ -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',