fix(e2e): fix failing tags test and flaky late-join test

- Tags test: Use right-click context menu approach instead of 'g' shortcut
  to avoid typing into editable task title
- Tags test: Add robust dismissAllOverlays helper with multiple Escape
  presses and backdrop click fallback
- Late-join test: Improve conflict dialog handling with retry loop and
  wait for dialog to close
- Late-join test: Add extra sync cycle after conflict resolution for
  more reliable data propagation
- All files: Fix incorrect port in warning messages (1900 -> 1901)

All 48 supersync e2e tests pass consistently.
This commit is contained in:
Johannes Millan 2025-12-17 21:56:53 +01:00
parent 380fc9127e
commit 0b998459aa
12 changed files with 93 additions and 95 deletions

View file

@ -17,7 +17,7 @@ import { ImportPage } from '../../pages/import.page';
* This includes active tasks, archived tasks (worklog), and projects.
*
* Prerequisites:
* - super-sync-server running on localhost:1900 with TEST_MODE=true
* - super-sync-server running on localhost:1901 with TEST_MODE=true
* - Frontend running on localhost:4242
*
* Run with: npm run e2e:playwright -- --grep @importsync
@ -35,7 +35,7 @@ base.describe('@importsync @supersync Import + Sync E2E', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -33,7 +33,7 @@ base.describe('@supersync SuperSync Advanced Edge Cases', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -29,7 +29,7 @@ base.describe('@supersync SuperSync Advanced', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -33,7 +33,7 @@ base.describe('@supersync Daily Summary Sync', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -88,7 +88,7 @@ base.describe('@supersync SuperSync Edge Cases', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -53,7 +53,7 @@ base.describe('@supersync SuperSync Encryption', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -27,7 +27,7 @@ base.describe('@supersync SuperSync Late Join', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}
@ -92,37 +92,45 @@ base.describe('@supersync SuperSync Late Join', () => {
// Since B has local data and server has remote data, and they might touch global settings or similar,
// a conflict might occur. However, tasks are distinct IDs, so they should merge cleanly.
// If a conflict dialog appears for Global Config or similar, we should handle it.
// The helper `syncAndWait` doesn't handle conflicts automatically, but the tests in `supersync.spec.ts`
// seem to expect `Conflict dialog detected` in logs and handle it?
// Let's check if conflict dialog is visible and resolve it if so.
// Usually "Use Remote" or "Use Local" is fine if keys don't overlap.
// For distinct tasks, no conflict should occur on the tasks themselves.
// Check multiple times as dialog might appear with slight delay
const conflictDialog = clientB.page.locator('dialog-conflict-resolution');
if (await conflictDialog.isVisible({ timeout: 2000 })) {
console.log('Conflict dialog detected on B, resolving...');
// Click "Use Local" or "Use Remote" or "Merge"
// Since we want to keep B's local tasks and A's remote tasks, valid merge should happen.
// But if it's a conflict on a singleton model (like Global Config), we just pick one.
// Let's pick Remote for config.
const useRemoteBtn = conflictDialog
.locator('button')
.filter({ hasText: 'Remote' })
.first();
if (await useRemoteBtn.isVisible()) {
await useRemoteBtn.click();
} else {
// Fallback
await clientB.page.keyboard.press('Escape');
for (let attempt = 0; attempt < 3; attempt++) {
try {
await conflictDialog.waitFor({ state: 'visible', timeout: 2000 });
console.log('Conflict dialog detected on B, resolving...');
// Pick Remote for singleton models (like Global Config)
const useRemoteBtn = conflictDialog
.locator('button')
.filter({ hasText: 'Remote' })
.first();
if (await useRemoteBtn.isVisible()) {
await useRemoteBtn.click();
// Wait for dialog to close
await conflictDialog.waitFor({ state: 'hidden', timeout: 5000 });
} else {
// Fallback: dismiss dialog
await clientB.page.keyboard.press('Escape');
}
// Brief wait for any subsequent dialogs
await clientB.page.waitForTimeout(500);
} catch {
// No conflict dialog visible, proceed
break;
}
}
// Wait for sync to settle
await clientB.page.waitForTimeout(1000);
// Wait for sync to fully settle after conflict resolution
await clientB.page.waitForTimeout(2000);
// Trigger another sync to ensure all data is propagated
await clientB.sync.syncAndWait();
// A Syncs to get B's data
await clientA.sync.syncAndWait();
// Brief wait for state to propagate
await clientA.page.waitForTimeout(1000);
// VERIFICATION
// Both clients should have A1, A2, A3, B1, B2, B3

View file

@ -32,7 +32,7 @@ base.describe('@supersync SuperSync LWW Conflict Resolution', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -155,7 +155,7 @@ base.describe('@supersync SuperSync Models', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}
@ -259,62 +259,57 @@ base.describe('@supersync SuperSync Models', () => {
// Create task
await clientA.workView.addTask(taskName);
// Add tag to task via task detail
// Helper to dismiss all overlays reliably
const dismissAllOverlays = async (): Promise<void> => {
// Dismiss backdrops by pressing Escape multiple times
for (let j = 0; j < 3; j++) {
await clientA.page.keyboard.press('Escape');
await clientA.page.waitForTimeout(100);
}
// Wait for any overlays to disappear
const overlayBackdrop = clientA.page.locator('.cdk-overlay-backdrop');
try {
await overlayBackdrop.waitFor({ state: 'hidden', timeout: 2000 });
} catch {
// If still visible, force click to dismiss
if (await overlayBackdrop.isVisible()) {
await overlayBackdrop.click({ force: true });
await clientA.page.waitForTimeout(200);
}
}
};
// Dismiss any existing overlays first
await dismissAllOverlays();
// Add tag to task via context menu (most reliable method)
const taskLocator = clientA.page.locator(`task:has-text("${taskName}")`);
await taskLocator.click();
await taskLocator.waitFor({ state: 'visible' });
// Hover to ensure buttons appear
await taskLocator.hover();
await clientA.page.waitForTimeout(500);
// Click "Edit Tags" button
const editTagsBtn = clientA.page
.locator('button[title="Edit tags"], button[aria-label="Edit tags"]')
.first();
// Retry loop for opening tags menu
// Retry loop for opening tags menu via context menu
let menuOpened = false;
for (let i = 0; i < 3; i++) {
try {
if (await editTagsBtn.isVisible()) {
await editTagsBtn.click();
} else {
// Fallback: Try context menu
await taskLocator.click({ button: 'right' });
// The context menu item is named "Toggle Tags" in English (from T.F.TASK.CMP.TOGGLE_TAGS)
// But we also check for "Edit tags" just in case
const ctxEditTags = clientA.page.locator('.e2e-edit-tags-btn').first();
// Ensure clean state before each attempt
await dismissAllOverlays();
if (await ctxEditTags.isVisible()) {
await ctxEditTags.click();
} else {
// Last resort: shortcut
console.log('Context menu item not found, trying shortcut...');
// Close context menu if open
const backdrop = clientA.page.locator('.cdk-overlay-backdrop');
if (await backdrop.isVisible()) {
await backdrop.click({ force: true });
} else {
await clientA.page.keyboard.press('Escape');
}
await clientA.page.waitForTimeout(200);
// Right-click on task to open context menu (doesn't trigger edit mode)
await taskLocator.click({ button: 'right' });
await clientA.page.waitForTimeout(300);
// Ensure task is focused/selected
await taskLocator.click({ force: true });
await clientA.page.waitForTimeout(200);
// Use keyboard shortcut to open the tag menu (default: "G")
await clientA.page.keyboard.press('g');
}
}
// Find and click the "Edit tags" button in context menu
const ctxEditTags = clientA.page.locator('.e2e-edit-tags-btn').first();
await ctxEditTags.waitFor({ state: 'visible', timeout: 3000 });
await ctxEditTags.click();
// Dialog "Edit Tags" - now it's a menu
const menuPanel = clientA.page.locator('.mat-mdc-menu-panel');
await menuPanel.waitFor({ state: 'visible', timeout: 3000 });
// Wait for tag submenu to appear
const menuPanel = clientA.page.locator('.mat-mdc-menu-panel').last();
await menuPanel.waitFor({ state: 'visible', timeout: 5000 });
menuOpened = true;
break; // Success
break;
} catch (e) {
console.log(`Attempt ${i + 1} to open tags menu failed: ${e}, retrying...`);
await clientA.page.keyboard.press('Escape'); // Reset state
await dismissAllOverlays();
await clientA.page.waitForTimeout(500);
}
}
@ -323,23 +318,18 @@ base.describe('@supersync SuperSync Models', () => {
throw new Error('Failed to open tags menu after 3 attempts');
}
const menuPanel = clientA.page.locator('.mat-mdc-menu-panel');
// Select the tag from list
const tagOption = menuPanel
// Select the tag from the submenu (last menu panel is the tag submenu)
const tagMenu = clientA.page.locator('.mat-mdc-menu-panel').last();
const tagOption = tagMenu
.locator('button[mat-menu-item]')
.filter({ hasText: tagName })
.first();
if (await tagOption.isVisible()) {
await tagOption.click();
} else {
console.log('Tag not found in menu, creating new via prompt if needed');
// If not found, maybe we need to scroll or it's not in the list
}
await tagOption.waitFor({ state: 'visible', timeout: 3000 });
await tagOption.click();
// Close menu if still open (clicking option might close it)
if (await menuPanel.isVisible()) {
await clientA.page.keyboard.press('Escape');
}
// Wait for menus to close after selection
await clientA.page.waitForTimeout(500);
await dismissAllOverlays();
// Ensure tag is shown on Client A before syncing
await expect(taskLocator).toContainText(tagName);

View file

@ -65,7 +65,7 @@ base.describe('@supersync SuperSync Repeatable Task Sync', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -38,7 +38,7 @@ base.describe.serial('@supersync SuperSync Server Migration', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}

View file

@ -18,7 +18,7 @@ import { waitForAppReady } from '../../utils/waits';
* the full stack including UI, network, and real IndexedDB isolation.
*
* Prerequisites:
* - super-sync-server running on localhost:1900 with TEST_MODE=true
* - super-sync-server running on localhost:1901 with TEST_MODE=true
* - Frontend running on localhost:4242
*
* Run with: npm run e2e:supersync
@ -41,7 +41,7 @@ base.describe('@supersync SuperSync E2E', () => {
serverHealthy = await isServerHealthy();
if (!serverHealthy) {
console.warn(
'SuperSync server not healthy at http://localhost:1900 - skipping tests',
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
);
}
}