mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 01:17:31 +00:00
* test: harden e2e failure signals Fail otherwise-passing E2E tests on browser runtime errors, keep Playwright retries disabled, preserve Docker E2E exit codes, and make plugin/WebDAV setup failures hard failures instead of logged or skipped conditions. * test: harden provider e2e runners Make WebDAV and SuperSync runner scripts require provider readiness, preserve cleanup and argument forwarding, and fail manual sync clients on uncaught page errors. * ci: require providers for scheduled e2e Set required-provider flags in scheduled WebDAV and SuperSync jobs, and remove the duplicate provider runner scripts while keeping local npm aliases inline. * test: catch e2e teardown pageerrors and tighten fixture - closeClient now asserts runtime errors AFTER context.close() so pageerrors emitted during teardown (Angular destroy hooks, late RxJS errors) are captured instead of dropped. Matches the pattern in guardContextCloseWithRuntimeErrorCheck. - test.fixture.ts isolatedContext now spreads Playwright's merged contextOptions instead of destructuring 23 fields by hand. Future option additions propagate automatically; the page fixture uses the shared attachPageErrorCollector and only fails on pageerror (not console.error, which is too noisy). Guards against a configured 0 timeout being treated as undefined. - plugin-loading.spec.ts second test now hard-asserts that the plugin menu entry reappears after re-enable, matching the first test instead of silently logging when not visible. * test(sync): stabilize ImmediateUploadService spec Two complementary fixes for flaky failures observed under full-suite random-order runs where the upload pipeline silently never fires: - Pin navigator.onLine = true in beforeEach (restored in afterEach). isOnline() inside _canUpload reads navigator.onLine directly. The keyboard-layout spec replaces the whole navigator and the is-online spec spies on it; if order or restoration ever drifts, every "should fire upload" test fails trivially while the "should NOT" tests pass. - Replace tick(2100) with tick(2000); flush(). The await chain inside withSession() (provider.isReady, withSession entry, uploadPendingOps, optional LWW re-upload) requires more microtask drain than tick's fixed-time window reliably provides under load. flush() drains the pipeline regardless. * test(e2e): guard skipOnboarding init script against data: frames The new page-error collector started failing plugin specs because addInitScript runs in every frame — including the empty data:text/html iframe that plugin-index swaps in on destroy — and localStorage access in a data: URL throws SecurityError. Wrap the four setItem calls in try/catch so the helper noops in storage-less frames.
247 lines
7.7 KiB
TypeScript
247 lines
7.7 KiB
TypeScript
import { test, expect } from '../../fixtures/webdav.fixture';
|
|
import { SyncPage } from '../../pages/sync.page';
|
|
import { WorkViewPage } from '../../pages/work-view.page';
|
|
import {
|
|
WEBDAV_CONFIG_TEMPLATE,
|
|
createUniqueSyncFolder,
|
|
createWebDavFolder,
|
|
setupClient,
|
|
waitForSync,
|
|
simulateNetworkFailure,
|
|
restoreNetwork,
|
|
} from '../../utils/sync-helpers';
|
|
import { waitForStatePersistence } from '../../utils/waits';
|
|
|
|
test.describe('@webdav WebDAV Sync Error Handling', () => {
|
|
// Run sync tests serially to avoid WebDAV server contention
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test('should handle server unavailable during sync', async ({
|
|
browser,
|
|
baseURL,
|
|
request,
|
|
}) => {
|
|
test.slow();
|
|
const SYNC_FOLDER_NAME = createUniqueSyncFolder('error-network');
|
|
await createWebDavFolder(request, SYNC_FOLDER_NAME);
|
|
const WEBDAV_CONFIG = {
|
|
...WEBDAV_CONFIG_TEMPLATE,
|
|
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
|
|
};
|
|
|
|
const url = baseURL || 'http://localhost:4242';
|
|
|
|
// --- Client A ---
|
|
const { context: contextA, page: pageA } = await setupClient(browser, url);
|
|
const syncPageA = new SyncPage(pageA);
|
|
const workViewPageA = new WorkViewPage(pageA);
|
|
await workViewPageA.waitForTaskList();
|
|
|
|
// Configure Sync on Client A
|
|
await syncPageA.setupWebdavSync(WEBDAV_CONFIG);
|
|
await expect(syncPageA.syncBtn).toBeVisible();
|
|
|
|
// First, verify sync works normally
|
|
await syncPageA.triggerSync();
|
|
await waitForSync(pageA, syncPageA);
|
|
|
|
// Create a task
|
|
const taskName = `Network Test Task ${Date.now()}`;
|
|
await workViewPageA.addTask(taskName);
|
|
await waitForStatePersistence(pageA);
|
|
|
|
// Simulate network failure
|
|
await simulateNetworkFailure(pageA);
|
|
|
|
// Trigger sync - should fail
|
|
await syncPageA.triggerSync();
|
|
|
|
// Wait for error indication (snackbar or sync icon change)
|
|
// The sync should fail gracefully
|
|
const startTime = Date.now();
|
|
let errorFound = false;
|
|
while (Date.now() - startTime < 15000 && !errorFound) {
|
|
// Check for error snackbar
|
|
const snackBars = pageA.locator('.mat-mdc-snack-bar-container');
|
|
const count = await snackBars.count();
|
|
for (let i = 0; i < count; ++i) {
|
|
const text = await snackBars.nth(i).innerText();
|
|
if (
|
|
text.toLowerCase().includes('error') ||
|
|
text.toLowerCase().includes('fail') ||
|
|
text.toLowerCase().includes('network')
|
|
) {
|
|
errorFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for error icon on sync button
|
|
const errorIcon = syncPageA.syncBtn.locator(
|
|
'mat-icon.error, mat-icon:text("error"), mat-icon:text("sync_problem")',
|
|
);
|
|
if (await errorIcon.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
errorFound = true;
|
|
}
|
|
|
|
if (!errorFound) {
|
|
await pageA.waitForTimeout(500);
|
|
}
|
|
}
|
|
expect(errorFound).toBe(true);
|
|
|
|
// App should not crash - verify we can still interact
|
|
const taskLocator = pageA.locator('task', { hasText: taskName });
|
|
await expect(taskLocator).toBeVisible();
|
|
|
|
// Restore network
|
|
await restoreNetwork(pageA);
|
|
|
|
// Wait a moment for route to be fully removed
|
|
await pageA.waitForTimeout(1000);
|
|
|
|
// Dismiss any visible error snackbars before retrying
|
|
const snackBarDismiss = pageA.locator(
|
|
'.mat-mdc-snack-bar-container button, .mat-mdc-snack-bar-action',
|
|
);
|
|
if (await snackBarDismiss.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await snackBarDismiss.click().catch(() => {});
|
|
await pageA.waitForTimeout(500);
|
|
}
|
|
|
|
// Dismiss any open dialogs that might be blocking
|
|
const dialogBackdrop = pageA.locator('.cdk-overlay-backdrop');
|
|
if (await dialogBackdrop.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
// Press Escape to close any open dialogs
|
|
await pageA.keyboard.press('Escape');
|
|
await pageA.waitForTimeout(500);
|
|
}
|
|
|
|
// Sync should work again
|
|
await syncPageA.triggerSync();
|
|
|
|
await expect(syncPageA.syncCheckIcon).toBeVisible({ timeout: 30000 });
|
|
|
|
// Cleanup
|
|
await contextA.close();
|
|
});
|
|
|
|
test('should handle authentication failure', async ({ browser, baseURL, request }) => {
|
|
test.slow();
|
|
const SYNC_FOLDER_NAME = createUniqueSyncFolder('error-auth');
|
|
await createWebDavFolder(request, SYNC_FOLDER_NAME);
|
|
|
|
const url = baseURL || 'http://localhost:4242';
|
|
|
|
// --- Client A with wrong password ---
|
|
const { context: contextA, page: pageA } = await setupClient(browser, url);
|
|
const syncPageA = new SyncPage(pageA);
|
|
const workViewPageA = new WorkViewPage(pageA);
|
|
await workViewPageA.waitForTaskList();
|
|
|
|
// Configure Sync with wrong password
|
|
const WRONG_CONFIG = {
|
|
baseUrl: WEBDAV_CONFIG_TEMPLATE.baseUrl,
|
|
username: 'admin',
|
|
password: 'wrongpassword',
|
|
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
|
|
};
|
|
|
|
await syncPageA.setupWebdavSync(WRONG_CONFIG);
|
|
await expect(syncPageA.syncBtn).toBeVisible();
|
|
|
|
// Trigger sync - should fail with auth error
|
|
await syncPageA.triggerSync();
|
|
|
|
// Wait for error indication
|
|
const startTime = Date.now();
|
|
let authErrorFound = false;
|
|
while (Date.now() - startTime < 15000 && !authErrorFound) {
|
|
// Check for error snackbar
|
|
const snackBars = pageA.locator('.mat-mdc-snack-bar-container');
|
|
const count = await snackBars.count();
|
|
for (let i = 0; i < count; ++i) {
|
|
const text = await snackBars
|
|
.nth(i)
|
|
.innerText()
|
|
.catch(() => '');
|
|
const textLower = text.toLowerCase();
|
|
if (
|
|
textLower.includes('401') ||
|
|
textLower.includes('auth') ||
|
|
textLower.includes('unauthorized') ||
|
|
textLower.includes('error') ||
|
|
textLower.includes('fail')
|
|
) {
|
|
authErrorFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!authErrorFound) {
|
|
await pageA.waitForTimeout(500);
|
|
}
|
|
}
|
|
expect(authErrorFound).toBe(true);
|
|
|
|
// App should not crash
|
|
const taskList = pageA.locator('task-list');
|
|
await expect(taskList).toBeVisible();
|
|
|
|
// Cleanup
|
|
await contextA.close();
|
|
});
|
|
|
|
test('should handle double sync trigger gracefully', async ({
|
|
browser,
|
|
baseURL,
|
|
request,
|
|
}) => {
|
|
test.slow();
|
|
const SYNC_FOLDER_NAME = createUniqueSyncFolder('error-double');
|
|
await createWebDavFolder(request, SYNC_FOLDER_NAME);
|
|
const WEBDAV_CONFIG = {
|
|
...WEBDAV_CONFIG_TEMPLATE,
|
|
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
|
|
};
|
|
|
|
const url = baseURL || 'http://localhost:4242';
|
|
|
|
// --- Client A ---
|
|
const { context: contextA, page: pageA } = await setupClient(browser, url);
|
|
const syncPageA = new SyncPage(pageA);
|
|
const workViewPageA = new WorkViewPage(pageA);
|
|
await workViewPageA.waitForTaskList();
|
|
|
|
// Configure Sync on Client A
|
|
await syncPageA.setupWebdavSync(WEBDAV_CONFIG);
|
|
await expect(syncPageA.syncBtn).toBeVisible();
|
|
|
|
// Create some tasks
|
|
const taskName = `Double Sync Task ${Date.now()}`;
|
|
await workViewPageA.addTask(taskName);
|
|
await waitForStatePersistence(pageA);
|
|
|
|
// Trigger sync twice rapidly (simulating double-click)
|
|
await syncPageA.syncBtn.click();
|
|
await pageA.waitForTimeout(100);
|
|
await syncPageA.syncBtn.click();
|
|
|
|
// Wait for sync to complete
|
|
await waitForSync(pageA, syncPageA);
|
|
|
|
// App should not crash and task should still be visible
|
|
const taskLocator = pageA.locator('task', { hasText: taskName });
|
|
await expect(taskLocator).toBeVisible();
|
|
|
|
// Verify sync button is in normal state (not stuck)
|
|
await expect(syncPageA.syncBtn).toBeEnabled();
|
|
|
|
// Try another sync to confirm everything works
|
|
await syncPageA.triggerSync();
|
|
await waitForSync(pageA, syncPageA);
|
|
|
|
// Cleanup
|
|
await contextA.close();
|
|
});
|
|
});
|