mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
fix: resolve build issues and update E2E tests for sync import conflict
- Fix TypeScript error in passkey.ts (Buffer to Uint8Array conversion) - Update Dockerfile.test to use npm install instead of npm ci for workspace sync - Update E2E test to not setup sync before import - Add additional unit tests for sync-wrapper.service Note: E2E tests for sync import conflict dialog need further work due to automatic upload of SYNC_IMPORT when sync is enabled. The core dialog functionality is implemented and unit tested.
This commit is contained in:
parent
4780f08333
commit
6501950760
4 changed files with 97 additions and 13 deletions
|
|
@ -75,8 +75,7 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
console.log('[Conflict Dialog] Phase 2: Client B imports backup');
|
||||
|
||||
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
// DO NOT sync before import - B doesn't know about A's task
|
||||
// DO NOT setup sync before import - we want B to have no knowledge of server state
|
||||
|
||||
// Navigate to import page
|
||||
const importPage = new ImportPage(clientB.page);
|
||||
|
|
@ -91,14 +90,14 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
await clientB.page.reload();
|
||||
await clientB.page.waitForLoadState('networkidle');
|
||||
|
||||
// Re-enable sync after import (import overwrites globalConfig)
|
||||
// NOW setup sync - B has local SYNC_IMPORT but no knowledge of server ops
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
|
||||
// ============ PHASE 3: Client B Syncs (Should See Dialog) ============
|
||||
console.log('[Conflict Dialog] Phase 3: Client B syncs (should see dialog)');
|
||||
|
||||
// Trigger sync - this should cause the dialog to appear
|
||||
await clientB.page.locator('button.trigger-sync-button').click();
|
||||
await clientB.sync.triggerSync();
|
||||
|
||||
// Wait for the conflict dialog to appear
|
||||
const dialog = clientB.page.locator('mat-dialog-container');
|
||||
|
|
@ -173,7 +172,7 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
console.log('[USE_LOCAL] Phase 2: Client B imports backup');
|
||||
|
||||
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
// DO NOT setup sync before import
|
||||
|
||||
const importPage = new ImportPage(clientB.page);
|
||||
await importPage.navigateToImportPage();
|
||||
|
|
@ -183,12 +182,13 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
|
||||
await clientB.page.reload();
|
||||
await clientB.page.waitForLoadState('networkidle');
|
||||
// Setup sync AFTER import
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
|
||||
// ============ PHASE 3: Client B Syncs and Chooses USE_LOCAL ============
|
||||
console.log('[USE_LOCAL] Phase 3: Client B syncs and chooses USE_LOCAL');
|
||||
|
||||
await clientB.page.locator('button.trigger-sync-button').click();
|
||||
await clientB.sync.triggerSync();
|
||||
|
||||
const dialog = clientB.page.locator('mat-dialog-container');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
|
@ -276,7 +276,7 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
console.log('[USE_REMOTE] Phase 2: Client B imports backup');
|
||||
|
||||
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
// DO NOT setup sync before import
|
||||
|
||||
const importPage = new ImportPage(clientB.page);
|
||||
await importPage.navigateToImportPage();
|
||||
|
|
@ -286,12 +286,13 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
|
||||
await clientB.page.reload();
|
||||
await clientB.page.waitForLoadState('networkidle');
|
||||
// Setup sync AFTER import
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
|
||||
// ============ PHASE 3: Client B Syncs and Chooses USE_REMOTE ============
|
||||
console.log('[USE_REMOTE] Phase 3: Client B syncs and chooses USE_REMOTE');
|
||||
|
||||
await clientB.page.locator('button.trigger-sync-button').click();
|
||||
await clientB.sync.triggerSync();
|
||||
|
||||
const dialog = clientB.page.locator('mat-dialog-container');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
|
@ -366,7 +367,7 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
console.log('[CANCEL] Phase 2: Client B imports backup');
|
||||
|
||||
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
// DO NOT setup sync before import
|
||||
|
||||
const importPage = new ImportPage(clientB.page);
|
||||
await importPage.navigateToImportPage();
|
||||
|
|
@ -376,12 +377,13 @@ test.describe('@supersync @import-conflict Sync Import Conflict Dialog', () => {
|
|||
|
||||
await clientB.page.reload();
|
||||
await clientB.page.waitForLoadState('networkidle');
|
||||
// Setup sync AFTER import
|
||||
await clientB.sync.setupSuperSync(syncConfig);
|
||||
|
||||
// ============ PHASE 3: Client B Syncs and Chooses CANCEL ============
|
||||
console.log('[CANCEL] Phase 3: Client B syncs and chooses CANCEL');
|
||||
|
||||
await clientB.page.locator('button.trigger-sync-button').click();
|
||||
await clientB.sync.triggerSync();
|
||||
|
||||
const dialog = clientB.page.locator('mat-dialog-container');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ COPY packages/shared-schema/package.json ./packages/shared-schema/
|
|||
COPY packages/super-sync-server/package.json ./packages/super-sync-server/
|
||||
|
||||
# Install dependencies (ignore scripts to skip husky/prepare from root)
|
||||
RUN npm ci --workspace=packages/shared-schema --workspace=packages/super-sync-server --ignore-scripts
|
||||
# Using npm install instead of npm ci due to workspace lock file sync issues
|
||||
RUN npm install --workspace=packages/shared-schema --workspace=packages/super-sync-server --ignore-scripts
|
||||
|
||||
# Copy source files
|
||||
COPY packages/shared-schema/ ./packages/shared-schema/
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ export const verifyAuthentication = async (
|
|||
requireUserVerification: false, // We use 'preferred', not 'required'
|
||||
credential: {
|
||||
id: passkey.credentialId.toString('base64url'),
|
||||
publicKey: passkey.publicKey,
|
||||
publicKey: new Uint8Array(passkey.publicKey),
|
||||
counter: Number(passkey.counter),
|
||||
transports: passkey.transports ? JSON.parse(passkey.transports) : undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ describe('SyncWrapperService', () => {
|
|||
expect(mockProviderManager.setSyncStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set ERROR and return HANDLED_ERROR when upload has rejected ops', async () => {
|
||||
it('should set ERROR and return HANDLED_ERROR when upload has rejected ops with "Payload too complex"', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(
|
||||
Promise.resolve({
|
||||
uploadedCount: 0,
|
||||
|
|
@ -411,6 +411,24 @@ describe('SyncWrapperService', () => {
|
|||
expect(mockProviderManager.setSyncStatus).not.toHaveBeenCalledWith('IN_SYNC');
|
||||
});
|
||||
|
||||
it('should set ERROR and return HANDLED_ERROR when upload has rejected ops with "Payload too large"', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(
|
||||
Promise.resolve({
|
||||
uploadedCount: 0,
|
||||
rejectedCount: 1,
|
||||
piggybackedOps: [],
|
||||
rejectedOps: [{ opId: 'test-op', error: 'Payload too large' }],
|
||||
localWinOpsCreated: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.sync();
|
||||
|
||||
expect(result).toBe('HANDLED_ERROR');
|
||||
expect(mockProviderManager.setSyncStatus).toHaveBeenCalledWith('ERROR');
|
||||
expect(mockProviderManager.setSyncStatus).not.toHaveBeenCalledWith('IN_SYNC');
|
||||
});
|
||||
|
||||
it('should set ERROR for non-payload rejected ops', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(
|
||||
Promise.resolve({
|
||||
|
|
@ -427,6 +445,69 @@ describe('SyncWrapperService', () => {
|
|||
expect(result).toBe('HANDLED_ERROR');
|
||||
expect(mockProviderManager.setSyncStatus).toHaveBeenCalledWith('ERROR');
|
||||
});
|
||||
|
||||
it('should set IN_SYNC when some ops uploaded and none rejected', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(
|
||||
Promise.resolve({
|
||||
uploadedCount: 5,
|
||||
rejectedCount: 0,
|
||||
piggybackedOps: [],
|
||||
rejectedOps: [],
|
||||
localWinOpsCreated: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.sync();
|
||||
|
||||
expect(result).toBe(SyncStatus.InSync);
|
||||
expect(mockProviderManager.setSyncStatus).toHaveBeenCalledWith('IN_SYNC');
|
||||
});
|
||||
|
||||
it('should set ERROR when multiple ops rejected', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(
|
||||
Promise.resolve({
|
||||
uploadedCount: 3,
|
||||
rejectedCount: 2,
|
||||
piggybackedOps: [],
|
||||
rejectedOps: [
|
||||
{ opId: 'op1', error: 'Conflict' },
|
||||
{ opId: 'op2', error: 'Validation failed' },
|
||||
],
|
||||
localWinOpsCreated: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.sync();
|
||||
|
||||
expect(result).toBe('HANDLED_ERROR');
|
||||
expect(mockProviderManager.setSyncStatus).toHaveBeenCalledWith('ERROR');
|
||||
});
|
||||
|
||||
it('should set IN_SYNC when uploadResult is null (fresh client)', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(Promise.resolve(null));
|
||||
|
||||
const result = await service.sync();
|
||||
|
||||
expect(result).toBe(SyncStatus.InSync);
|
||||
expect(mockProviderManager.setSyncStatus).toHaveBeenCalledWith('IN_SYNC');
|
||||
});
|
||||
|
||||
it('should set IN_SYNC when rejectedCount is 0 even with empty rejectedOps array', async () => {
|
||||
mockSyncService.uploadPendingOps.and.returnValue(
|
||||
Promise.resolve({
|
||||
uploadedCount: 0,
|
||||
rejectedCount: 0,
|
||||
piggybackedOps: [],
|
||||
rejectedOps: [],
|
||||
localWinOpsCreated: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.sync();
|
||||
|
||||
expect(result).toBe(SyncStatus.InSync);
|
||||
expect(mockProviderManager.setSyncStatus).toHaveBeenCalledWith('IN_SYNC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue