super-productivity/e2e/tests/sync/supersync-multi-tab.spec.ts
Johannes Millan 2f8d871042 feat(shepherd): remove all tours except keyboard navigation
Remove the full welcome tour (Welcome, AddTask, DeleteTask, Projects,
Sync, IssueProviders, ProductivityHelper, FinalCongrats, StartTourAgain)
and keep only the KeyboardNav tour, triggered from the Help menu.

- Delete ShepherdComponent (auto-start on load)
- Strip shepherd-steps.const.ts to KeyboardNav steps only
- Simplify ShepherdService with _initPromise guard and fresh tour on re-open
- Remove 3 tour menu items from Help menu, keep keyboard tour
- Remove waitForEl/waitForElObs$ helpers (unused by KeyboardNav)
- Delete e2e/utils/tour-helpers.ts and all tour dismissal calls
- Clean up stale tour comments across e2e tests
- Fix pre-existing build error: add isFinishDayEnabled to AppFeaturesConfig
  type, defaults, form toggle, component signal, and translations
2026-03-22 17:00:42 +01:00

174 lines
6.4 KiB
TypeScript

import { test, expect } from '../../fixtures/supersync.fixture';
import {
createTestUser,
getSuperSyncConfig,
createSimulatedClient,
closeClient,
waitForTask,
type SimulatedE2EClient,
} from '../../utils/supersync-helpers';
import { SuperSyncPage } from '../../pages/supersync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { waitForAppReady } from '../../utils/waits';
/**
* SuperSync Multi-Tab Same Account E2E Tests
*
* Tests that two browser tabs sharing the same IndexedDB (same BrowserContext)
* don't corrupt data when using SuperSync.
*
* NOTE: The app prevents two tabs from running simultaneously via BroadcastChannel.
* This test works around that by navigating the inactive tab to about:blank before
* opening the next tab, then reloading it later. This simulates the real-world
* scenario of closing and reopening a tab.
*
* Prerequisites:
* - super-sync-server running on localhost:1901 with TEST_MODE=true
* - Frontend running on localhost:4242
*
* Run with: npm run e2e:supersync:file e2e/tests/sync/supersync-multi-tab.spec.ts
*/
test.describe('@supersync Multi-Tab Same Account', () => {
/**
* Scenario: Two tabs share IndexedDB, tasks created in one tab appear in other after reload
*
* Flow:
* 1. Create a SHARED browser context (same IndexedDB)
* 2. Open Tab 1, set up SuperSync, create task
* 3. Tab 1 syncs
* 4. Navigate Tab 1 away (avoid multi-instance blocker)
* 5. Open Tab 2 → should see Tab 1's task (from shared IndexedDB)
* 6. Tab 2 creates a task, syncs
* 7. Navigate Tab 2 away, reload Tab 1 → should see both tasks
* 8. External Client C (separate context) syncs → should see consistent state
*/
test('Two tabs sharing IndexedDB converge with external client', async ({
browser,
baseURL,
testRunId,
}) => {
test.setTimeout(180000);
const appUrl = baseURL || 'http://localhost:4242';
let clientC: SimulatedE2EClient | null = null;
let sharedContext: import('@playwright/test').BrowserContext | null = null;
try {
const user = await createTestUser(testRunId);
const syncConfig = getSuperSyncConfig(user);
// ============ PHASE 1: Setup shared context with Tab 1 ============
console.log('[MultiTab] Phase 1: Setup shared context with Tab 1');
sharedContext = await browser.newContext({
storageState: undefined,
viewport: { width: 1920, height: 1080 },
// Set PLAYWRIGHT user agent for test identification and to skip onboarding
userAgent: 'PLAYWRIGHT MULTI-TAB-TEST',
});
const tab1 = await sharedContext.newPage();
tab1.on('console', (msg) => {
if (msg.type() === 'error') {
console.error(`[Tab1] Console error:`, msg.text());
}
});
await tab1.goto(appUrl);
await waitForAppReady(tab1);
const tab1Sync = new SuperSyncPage(tab1);
const tab1WorkView = new WorkViewPage(tab1, `Tab1-${testRunId}`);
await tab1Sync.setupSuperSync(syncConfig);
console.log('[MultiTab] Tab 1 setup complete');
// ============ PHASE 2: Tab 1 creates task and syncs ============
console.log('[MultiTab] Phase 2: Tab 1 creates task');
const taskFromTab1 = `Tab1-Task-${testRunId}`;
await tab1WorkView.addTask(taskFromTab1);
await waitForTask(tab1, taskFromTab1);
await tab1Sync.syncAndWait();
console.log('[MultiTab] Tab 1 created task and synced');
// ============ PHASE 3: Open Tab 2 in same context ============
console.log('[MultiTab] Phase 3: Open Tab 2 in same context');
// Navigate Tab 1 away to avoid multi-instance blocker
// (app uses BroadcastChannel to prevent two tabs from running simultaneously)
await tab1.goto('about:blank');
const tab2 = await sharedContext.newPage();
tab2.on('console', (msg) => {
if (msg.type() === 'error') {
console.error(`[Tab2] Console error:`, msg.text());
}
});
await tab2.goto(appUrl);
await waitForAppReady(tab2);
await tab2.waitForTimeout(2000); // Let Angular hydrate from IndexedDB
// Tab 2 should see Tab 1's task from shared IndexedDB
await waitForTask(tab2, taskFromTab1);
console.log("[MultiTab] ✓ Tab 2 sees Tab 1's task from shared IndexedDB");
// ============ PHASE 4: Tab 2 creates task ============
console.log('[MultiTab] Phase 4: Tab 2 creates task');
const tab2WorkView = new WorkViewPage(tab2, `Tab2-${testRunId}`);
const taskFromTab2 = `Tab2-Task-${testRunId}`;
await tab2WorkView.addTask(taskFromTab2);
await waitForTask(tab2, taskFromTab2);
console.log('[MultiTab] Tab 2 created task');
// Trigger sync from Tab 2 (SuperSync config is shared via IndexedDB)
const tab2Sync = new SuperSyncPage(tab2);
await tab2Sync.syncAndWait();
console.log('[MultiTab] Tab 2 synced');
// ============ PHASE 5: Tab 1 reloads and verifies ============
console.log('[MultiTab] Phase 5: Tab 1 reloads');
// Navigate Tab 2 away before reloading Tab 1
await tab2.goto('about:blank');
await tab1.goto(appUrl);
await waitForAppReady(tab1);
await tab1.waitForTimeout(2000);
// Tab 1 should see both tasks
await waitForTask(tab1, taskFromTab1);
await waitForTask(tab1, taskFromTab2);
console.log('[MultiTab] ✓ Tab 1 sees both tasks after reload');
// ============ PHASE 6: External Client C verifies consistent state ============
console.log('[MultiTab] Phase 6: External Client C verifies');
// Navigate Tab 1 away before creating external client
await tab1.goto('about:blank');
clientC = await createSimulatedClient(browser, appUrl, 'C', testRunId);
await clientC.sync.setupSuperSync(syncConfig);
await clientC.sync.syncAndWait();
await waitForTask(clientC.page, taskFromTab1);
await waitForTask(clientC.page, taskFromTab2);
const cTask1 = clientC.page.locator(`task:has-text("${taskFromTab1}")`);
const cTask2 = clientC.page.locator(`task:has-text("${taskFromTab2}")`);
await expect(cTask1).toBeVisible();
await expect(cTask2).toBeVisible();
console.log('[MultiTab] ✓ External Client C sees consistent state');
console.log('[MultiTab] ✓ Multi-tab test PASSED!');
} finally {
if (clientC) await closeClient(clientC);
if (sharedContext) {
await sharedContext.close().catch(() => {});
}
}
});
});