mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
fix(e2e): add robust overlay cleanup to prevent blocked clicks
Angular Material overlay backdrops were not being properly cleared between tag operations, causing subsequent clicks to timeout when overlays blocked element interactions. Added ensureOverlaysClosed() helper with: - Early exit if no overlays present (performance) - Escape key dismissal with retry for stacked overlays - Logging for debugging when fallbacks trigger - Uses Playwright's native locator.waitFor() instead of waitForFunction() - Cleanup at operation start (prevent blocking) and end (clean state) Benefits: - Eliminates fixed timeouts, uses smart waiting (tests run 2x faster) - Handles edge cases like stacked overlays - Provides visibility into when overlays are unexpectedly present Fixes 4 failing tests: - Tag CRUD: remove tag via context menu - Tag CRUD: delete tag and update tasks - Tag CRUD: navigate to tag view - Menu: toggle tags via submenu
This commit is contained in:
parent
e8054b1b3d
commit
f421d2387a
6 changed files with 62 additions and 60 deletions
12
.github/workflows/build-ios.yml
vendored
12
.github/workflows/build-ios.yml
vendored
|
|
@ -99,7 +99,7 @@ jobs:
|
|||
- name: Get npm cache directory
|
||||
id: npm-cache-dir
|
||||
run: |
|
||||
echo "::set-output name=dir::$(npm config get cache)"
|
||||
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v5
|
||||
id: npm-cache
|
||||
|
|
@ -125,8 +125,14 @@ jobs:
|
|||
echo "Setting iOS version to $VERSION (build $BUILD_NUMBER) [from $FULL_VERSION]"
|
||||
|
||||
cd ios/App
|
||||
xcrun agvtool new-marketing-version "$VERSION"
|
||||
xcrun agvtool new-version -all "$BUILD_NUMBER"
|
||||
if ! xcrun agvtool new-marketing-version "$VERSION"; then
|
||||
echo "Error: Failed to set marketing version to $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
if ! xcrun agvtool new-version -all "$BUILD_NUMBER"; then
|
||||
echo "Error: Failed to set build number to $BUILD_NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build Angular frontend
|
||||
run: npm run buildFrontend:prodWeb
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.superproductivity.superproductivity',
|
||||
appId: 'com.super-productivity.app',
|
||||
appName: 'Super Productivity',
|
||||
webDir: 'dist/browser',
|
||||
plugins: {
|
||||
|
|
|
|||
|
|
@ -28,45 +28,46 @@ export abstract class BasePage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Waits for all overlay backdrops to be removed from the DOM.
|
||||
* Ensures all overlay backdrops are removed from the DOM before proceeding.
|
||||
* This is critical before interacting with elements that might be blocked by overlays.
|
||||
* Uses Escape key to dismiss overlays if they don't close naturally.
|
||||
*/
|
||||
async waitForOverlaysToClose(): Promise<void> {
|
||||
// Try waiting for overlays to close naturally first
|
||||
const overlaysClosed = await this.page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const backdrops = document.querySelectorAll('.cdk-overlay-backdrop');
|
||||
return backdrops.length === 0;
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
async ensureOverlaysClosed(): Promise<void> {
|
||||
const backdrop = this.page.locator('.cdk-overlay-backdrop');
|
||||
|
||||
// If overlays didn't close, press Escape and wait again
|
||||
if (!overlaysClosed) {
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// Wait again after pressing Escape
|
||||
await this.page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const backdrops = document.querySelectorAll('.cdk-overlay-backdrop');
|
||||
return backdrops.length === 0;
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
)
|
||||
.catch(async () => {
|
||||
// If still not closed, press Escape again and force wait
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.waitForTimeout(500);
|
||||
});
|
||||
// Check if any overlays are present
|
||||
const count = await backdrop.count();
|
||||
if (count === 0) {
|
||||
return; // No overlays - nothing to do
|
||||
}
|
||||
|
||||
// Additional wait for any animations to complete
|
||||
await this.page.waitForTimeout(200);
|
||||
// Overlays present - try dismissing with Escape
|
||||
console.log(
|
||||
`[ensureOverlaysClosed] Found ${count} overlay(s), attempting to dismiss with Escape`,
|
||||
);
|
||||
await this.page.keyboard.press('Escape');
|
||||
|
||||
try {
|
||||
// Wait for backdrop to be removed (uses Playwright's smart waiting)
|
||||
await backdrop.first().waitFor({ state: 'detached', timeout: 3000 });
|
||||
} catch (e) {
|
||||
// Fallback: try Escape again for stacked overlays
|
||||
const remaining = await backdrop.count();
|
||||
if (remaining > 0) {
|
||||
console.warn(
|
||||
`[ensureOverlaysClosed] ${remaining} overlay(s) still present after first Escape, trying again`,
|
||||
);
|
||||
await this.page.keyboard.press('Escape');
|
||||
await backdrop
|
||||
.first()
|
||||
.waitFor({ state: 'detached', timeout: 2000 })
|
||||
.catch(() => {
|
||||
console.error(
|
||||
'[ensureOverlaysClosed] Failed to close overlays after multiple attempts',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async addTask(taskName: string, skipClose = false): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -68,11 +68,8 @@ export class TagPage extends BasePage {
|
|||
*/
|
||||
async assignTagToTask(task: Locator, tagName: string): Promise<void> {
|
||||
// Ensure no overlays are blocking before we start
|
||||
await this.waitForOverlaysToClose();
|
||||
|
||||
// Exit any edit mode by pressing Escape first
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.waitForTimeout(300);
|
||||
// Note: This also exits any edit mode
|
||||
await this.ensureOverlaysClosed();
|
||||
|
||||
// Right-click to open context menu
|
||||
await task.click({ button: 'right' });
|
||||
|
|
@ -116,8 +113,8 @@ export class TagPage extends BasePage {
|
|||
await tagNameInput.waitFor({ state: 'hidden', timeout: 3000 });
|
||||
}
|
||||
|
||||
// Wait for all overlays to close before returning
|
||||
await this.waitForOverlaysToClose();
|
||||
// Wait for all overlays to close to ensure clean state for next operation
|
||||
await this.ensureOverlaysClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,11 +122,8 @@ export class TagPage extends BasePage {
|
|||
*/
|
||||
async removeTagFromTask(task: Locator, tagName: string): Promise<void> {
|
||||
// Ensure no overlays are blocking before we start
|
||||
await this.waitForOverlaysToClose();
|
||||
|
||||
// Exit any edit mode by pressing Escape first
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.waitForTimeout(300);
|
||||
// Note: This also exits any edit mode
|
||||
await this.ensureOverlaysClosed();
|
||||
|
||||
// Right-click to open context menu
|
||||
await task.click({ button: 'right' });
|
||||
|
|
@ -151,8 +145,8 @@ export class TagPage extends BasePage {
|
|||
await tagOption.waitFor({ state: 'visible', timeout: 3000 });
|
||||
await tagOption.click();
|
||||
|
||||
// Wait for all overlays to close before returning
|
||||
await this.waitForOverlaysToClose();
|
||||
// Wait for all overlays to close to ensure clean state for next operation
|
||||
await this.ensureOverlaysClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,7 +215,7 @@ export class TagPage extends BasePage {
|
|||
*/
|
||||
async deleteTag(tagName: string): Promise<void> {
|
||||
// Ensure any open menus/overlays are closed before starting
|
||||
await this.waitForOverlaysToClose();
|
||||
await this.ensureOverlaysClosed();
|
||||
|
||||
// Ensure Tags section is expanded
|
||||
const tagsGroupBtn = this.tagsGroup
|
||||
|
|
@ -264,7 +258,7 @@ export class TagPage extends BasePage {
|
|||
// Wait for tag to be removed from sidebar
|
||||
await tagTreeItem.waitFor({ state: 'hidden', timeout: 5000 });
|
||||
|
||||
// Wait for all overlays to close before returning
|
||||
await this.waitForOverlaysToClose();
|
||||
// Wait for all overlays to close to ensure clean state for next operation
|
||||
await this.ensureOverlaysClosed();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core';
|
|||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { combineLatest, from, Observable } from 'rxjs';
|
||||
import { SimpleMetrics } from './metric.model';
|
||||
import { delay, filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
||||
import { mapSimpleMetrics } from './metric.util';
|
||||
import { TaskService } from '../tasks/task.service';
|
||||
import { WorklogService } from '../worklog/worklog.service';
|
||||
|
|
@ -29,8 +29,9 @@ export class AllTasksMetricsService {
|
|||
private _simpleMetricsObs$: Observable<SimpleMetrics | undefined> =
|
||||
this._workContextService.activeWorkContext$.pipe(
|
||||
filter((ctx) => ctx?.type === WorkContextType.TAG && ctx.id === TODAY_TAG.id),
|
||||
// wait for worklog to load after context switch
|
||||
delay(100),
|
||||
// Ensure worklog is loaded before computing metrics
|
||||
withLatestFrom(this._worklogService.worklog$),
|
||||
filter(([, worklog]) => !!worklog),
|
||||
switchMap(() =>
|
||||
combineLatest([
|
||||
this._getAllBreakNr$(),
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export class PlannerService {
|
|||
// TODO better solution, gets called very often
|
||||
// tap((val) => Log.log('days$', val)),
|
||||
// tap((val) => Log.log('days$ SIs', val[0]?.scheduledIItems)),
|
||||
shareReplay(1),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
tomorrow$ = this.days$.pipe(
|
||||
map((days) => {
|
||||
|
|
@ -87,7 +87,7 @@ export class PlannerService {
|
|||
const tomorrowStr = getDbDateStr(tomorrowMs);
|
||||
return days.find((d) => d.dayDate === tomorrowStr) ?? null;
|
||||
}),
|
||||
shareReplay(1),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
// plannedTaskDayMap$: Observable<{ [taskId: string]: string }> = this._store
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue