mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
test(e2e): harden LWW archive and encryption password-change tests
- supersync-archive-conflict: add closing B sync to the convergence rounds (A→B→A→B) so B is not one op behind A's final conflict- resolution state; bump active-list assertion timeout to 15s to cover the async reducer pipeline + sync-replay event-loop yield. - supersync.page: fix race in changeEncryptionPassword where a stale check icon from the previous syncAndWait() caused the method to return before the server wipe + re-upload finished, leaving the next client to race against partially-uploaded server state. Now captures the check-icon state at entry and waits for it to clear (same pattern syncAndWait uses).
This commit is contained in:
parent
32dbc95ed9
commit
de33234976
2 changed files with 47 additions and 15 deletions
|
|
@ -1829,6 +1829,15 @@ export class SuperSyncPage extends BasePage {
|
|||
* @param newPassword - The new encryption password
|
||||
*/
|
||||
async changeEncryptionPassword(newPassword: string): Promise<void> {
|
||||
// Capture the sync check icon state BEFORE we start. If a previous
|
||||
// syncAndWait() left the check icon visible, the final wait at the end of
|
||||
// this method would see a stale icon and return before the server wipe +
|
||||
// re-upload completes — causing a race where the next client starts
|
||||
// syncing against partially-uploaded server state.
|
||||
const checkVisibleBeforeOperation = await this.syncCheckIcon
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
// Open sync settings via right-click
|
||||
// Use noWaitAfter to prevent blocking on Angular hash navigation
|
||||
await this.syncBtn.click({ button: 'right', noWaitAfter: true });
|
||||
|
|
@ -1934,18 +1943,32 @@ export class SuperSyncPage extends BasePage {
|
|||
}
|
||||
}
|
||||
|
||||
// Wait for password change operation to complete (server wipe + re-upload)
|
||||
const checkAlreadyVisible = await this.syncCheckIcon.isVisible().catch(() => false);
|
||||
if (!checkAlreadyVisible) {
|
||||
const spinnerVisible = await this.syncSpinner
|
||||
.waitFor({ state: 'visible', timeout: 5000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (spinnerVisible) {
|
||||
await this.syncSpinner.waitFor({ state: 'hidden', timeout: 30000 });
|
||||
}
|
||||
await this.syncCheckIcon.waitFor({ state: 'visible', timeout: 10000 });
|
||||
// Wait for password change operation to complete (server wipe + re-upload).
|
||||
//
|
||||
// If the check icon was visible BEFORE we opened the settings dialog, it's
|
||||
// stale from a previous sync — we must first wait for it to disappear (new
|
||||
// sync cycle started) or the spinner to appear, before waiting for the
|
||||
// check icon to reappear (new sync completed). Without this, we'd return
|
||||
// immediately against a stale icon and race the server re-upload.
|
||||
if (checkVisibleBeforeOperation) {
|
||||
await Promise.race([
|
||||
this.syncCheckIcon.waitFor({ state: 'hidden', timeout: 5000 }),
|
||||
this.syncSpinner.waitFor({ state: 'visible', timeout: 5000 }),
|
||||
]).catch(() => {
|
||||
// Neither happened within 5s — the password change may not have
|
||||
// triggered a re-sync (rare). Fall through and rely on the final
|
||||
// check-icon wait below.
|
||||
});
|
||||
}
|
||||
|
||||
const spinnerVisible = await this.syncSpinner
|
||||
.waitFor({ state: 'visible', timeout: 5000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (spinnerVisible) {
|
||||
await this.syncSpinner.waitFor({ state: 'hidden', timeout: 30000 });
|
||||
}
|
||||
await this.syncCheckIcon.waitFor({ state: 'visible', timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -250,21 +250,30 @@ test.describe('@supersync Archive Conflict Resolution', () => {
|
|||
await clientB.sync.syncAndWait();
|
||||
console.log('[BugB] Client B synced (downloaded archive + conflict resolution)');
|
||||
|
||||
// Extra sync rounds for convergence
|
||||
// Extra sync rounds for convergence.
|
||||
// Pattern A→B→A→B: conflict-resolution on either client may re-upload ops,
|
||||
// and an uneven final round (e.g. A→B→A) can leave B one op behind A's
|
||||
// final state. The closing B sync guarantees both clients are equal.
|
||||
await clientA.sync.syncAndWait();
|
||||
await clientB.sync.syncAndWait();
|
||||
await clientA.sync.syncAndWait();
|
||||
await clientB.sync.syncAndWait();
|
||||
console.log('[BugB] Extra sync rounds for convergence');
|
||||
|
||||
// ============ PHASE 6: Verify task NOT in active task list ============
|
||||
await navigateToWorkView(clientA);
|
||||
await navigateToWorkView(clientB);
|
||||
|
||||
await expectTaskNotVisible(clientA, taskName);
|
||||
// Use an extended timeout: archive op application goes through an async
|
||||
// reducer pipeline plus an event-loop yield during sync replay (see
|
||||
// CLAUDE.md #11). Give the UI enough time to reflect the archived state
|
||||
// before asserting — this is real settle time, not masking.
|
||||
const archivedAssertionTimeout = 15000;
|
||||
await expectTaskNotVisible(clientA, taskName, archivedAssertionTimeout);
|
||||
console.log('[BugB] Client A: task not visible in active list');
|
||||
|
||||
await expectTaskNotVisible(clientB, taskRenamed);
|
||||
await expectTaskNotVisible(clientB, taskName);
|
||||
await expectTaskNotVisible(clientB, taskRenamed, archivedAssertionTimeout);
|
||||
await expectTaskNotVisible(clientB, taskName, archivedAssertionTimeout);
|
||||
console.log('[BugB] Client B: task not visible in active list');
|
||||
|
||||
// ============ PHASE 7: Verify task IN worklog ============
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue