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 waitForOverlaysToClose() helper with multiple
fallback strategies (natural close, Escape key, retry) to ensure clean state.

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:
Johannes Millan 2026-01-16 19:15:11 +01:00
parent 7f4e5381d0
commit 5b1a843196
2 changed files with 56 additions and 18 deletions

View file

@ -27,6 +27,48 @@ export abstract class BasePage {
return `${this.testPrefix}-${value}`;
}
/**
* Waits for all overlay backdrops to be removed from the DOM.
* This is critical before interacting with elements that might be blocked by overlays.
*/
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);
// 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);
});
}
// Additional wait for any animations to complete
await this.page.waitForTimeout(200);
}
async addTask(taskName: string, skipClose = false): Promise<void> {
// Add test prefix to task name for isolation
const prefixedTaskName = this.applyPrefix(taskName);

View file

@ -67,6 +67,9 @@ export class TagPage extends BasePage {
* Assigns a tag to a task via context menu
*/
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);
@ -113,19 +116,17 @@ export class TagPage extends BasePage {
await tagNameInput.waitFor({ state: 'hidden', timeout: 3000 });
}
// Wait for menu to close and overlay to disappear
// The stopPropagation wrapper can prevent proper menu closing, so explicitly wait for overlay removal
await this.page
.locator('.cdk-overlay-backdrop')
.waitFor({ state: 'detached', timeout: 3000 })
.catch(() => {});
await this.page.waitForTimeout(300);
// Wait for all overlays to close before returning
await this.waitForOverlaysToClose();
}
/**
* Removes a tag from a task via context menu
*/
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);
@ -150,13 +151,8 @@ export class TagPage extends BasePage {
await tagOption.waitFor({ state: 'visible', timeout: 3000 });
await tagOption.click();
// Wait for menu to close and overlay to disappear
// The stopPropagation wrapper can prevent proper menu closing, so explicitly wait for overlay removal
await this.page
.locator('.cdk-overlay-backdrop')
.waitFor({ state: 'detached', timeout: 3000 })
.catch(() => {});
await this.page.waitForTimeout(300);
// Wait for all overlays to close before returning
await this.waitForOverlaysToClose();
}
/**
@ -225,10 +221,7 @@ export class TagPage extends BasePage {
*/
async deleteTag(tagName: string): Promise<void> {
// Ensure any open menus/overlays are closed before starting
await this.page
.locator('.cdk-overlay-backdrop')
.waitFor({ state: 'detached', timeout: 3000 })
.catch(() => {});
await this.waitForOverlaysToClose();
// Ensure Tags section is expanded
const tagsGroupBtn = this.tagsGroup
@ -270,5 +263,8 @@ 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();
}
}