Skip to content

Commit aebab16

Browse files
committed
improve progressive e2e tests
1 parent b702326 commit aebab16

File tree

4 files changed

+188
-45
lines changed

4 files changed

+188
-45
lines changed

packages/db-collection-e2e/src/suites/progressive.suite.ts

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,35 @@ export function createProgressiveTestSuite(
1818
) {
1919
describe(`Progressive Mode Suite (Electric only)`, () => {
2020
describe(`Basic Progressive Mode`, () => {
21-
it(`should validate snapshot phase behavior and atomic swap with status transition`, async () => {
21+
it(`should explicitly validate snapshot phase and atomic swap transition`, async () => {
2222
const config = await getConfig()
23-
if (!config.collections.progressive) {
24-
return // Skip if progressive collections not available
23+
if (!config.collections.progressive || !config.progressiveTestControl) {
24+
return // Skip if progressive collections or test control not available
2525
}
2626
const progressiveUsers = config.collections.progressive.users
2727

28-
// Create a query - this will trigger a snapshot fetch if still in snapshot phase
28+
// Start sync for this test
29+
progressiveUsers.startSyncImmediate()
30+
31+
// === PHASE 1: SNAPSHOT PHASE ===
32+
// Collection might already be ready from a previous test
33+
// (progressive collections are shared and not cleaned up between tests)
34+
await new Promise((resolve) => setTimeout(resolve, 50))
35+
36+
const initialStatus = progressiveUsers.status
37+
38+
// If already ready, we can't test the explicit transition
39+
if (initialStatus === `ready`) {
40+
console.log(
41+
`Progressive collection already ready, skipping explicit transition test`
42+
)
43+
return
44+
}
45+
46+
// Should be loading or idle (hook prevents marking ready)
47+
expect([`idle`, `loading`]).toContain(initialStatus)
48+
49+
// Create a query - this triggers fetchSnapshot
2950
const query = createLiveQueryCollection((q) =>
3051
q
3152
.from({ user: progressiveUsers })
@@ -35,49 +56,47 @@ export function createProgressiveTestSuite(
3556
await query.preload()
3657
await waitForQueryData(query, { minSize: 1, timeout: 10000 })
3758

38-
const querySize = query.size
39-
const queryItems = Array.from(query.values())
59+
// Validate snapshot data
60+
const snapshotQuerySize = query.size
61+
const snapshotItems = Array.from(query.values())
4062

41-
// Validate query data
42-
expect(querySize).toBeGreaterThan(0)
43-
queryItems.forEach((user) => {
63+
expect(snapshotQuerySize).toBeGreaterThan(0)
64+
snapshotItems.forEach((user) => {
4465
expect(user.age).toBe(25)
4566
expect(user.id).toBeDefined()
67+
expect(user.name).toBeDefined()
4668
})
4769

48-
// If we're still loading, we should be in snapshot phase
49-
// Base collection should have data from snapshot (query subset)
50-
const statusDuringQuery = progressiveUsers.status
51-
if (statusDuringQuery === `loading`) {
52-
// We're in snapshot phase! Validate snapshot behavior
53-
// Collection should have the snapshot data
54-
expect(progressiveUsers.size).toBeGreaterThan(0)
55-
56-
// But collection size should be <= query size (only snapshot loaded)
57-
// Actually it might have multiple snapshots if other tests ran, so just verify we have data
58-
expect(progressiveUsers.size).toBeGreaterThan(0)
59-
}
70+
// Collection should STILL be loading (paused before atomic swap)
71+
expect(progressiveUsers.status).toBe(`loading`)
72+
73+
// Collection has snapshot data
74+
const snapshotCollectionSize = progressiveUsers.size
75+
expect(snapshotCollectionSize).toBeGreaterThan(0)
6076

61-
// Wait for full sync to complete
77+
// === PHASE 2: TRIGGER ATOMIC SWAP ===
78+
config.progressiveTestControl.releaseInitialSync()
79+
80+
// === PHASE 3: POST-SWAP (FULLY SYNCED) ===
81+
// Wait for ready (atomic swap complete)
6282
await waitFor(() => progressiveUsers.status === `ready`, {
6383
timeout: 30000,
6484
message: `Progressive collection did not complete sync`,
6585
})
6686

67-
// After atomic swap to full synced state
68-
// Collection should have ALL users (not just age=25)
87+
// Collection now has full dataset (more than just snapshot)
6988
const finalCollectionSize = progressiveUsers.size
70-
expect(finalCollectionSize).toBeGreaterThan(querySize) // More than just our query subset
89+
expect(finalCollectionSize).toBeGreaterThan(snapshotQuerySize)
7190

72-
// Query should still work with consistent data
91+
// Query still works with consistent data
7392
const finalQueryItems = Array.from(query.values())
7493
finalQueryItems.forEach((user) => {
7594
expect(user.age).toBe(25) // Still matches predicate
7695
expect(user.id).toBeDefined()
7796
})
7897

79-
// Verify some of the original snapshot items are still present
80-
queryItems.forEach((originalUser) => {
98+
// Verify original snapshot items are still present after swap
99+
snapshotItems.forEach((originalUser) => {
81100
const foundInCollection = progressiveUsers.get(originalUser.id)
82101
expect(foundInCollection).toBeDefined()
83102
expect(foundInCollection?.age).toBe(25)
@@ -93,11 +112,10 @@ export function createProgressiveTestSuite(
93112
}
94113
const progressiveUsers = config.collections.progressive.users
95114

96-
// Progressive collections should only be marked ready AFTER first up-to-date
97-
// If already ready, the full sync completed very fast - we can still test the end state
98-
const wasStillLoading = progressiveUsers.status === `loading`
115+
// Start sync for this test
116+
progressiveUsers.startSyncImmediate()
99117

100-
// Query a subset
118+
// Query a subset (triggers snapshot fetch)
101119
const query = createLiveQueryCollection((q) =>
102120
q
103121
.from({ user: progressiveUsers })
@@ -106,9 +124,17 @@ export function createProgressiveTestSuite(
106124

107125
await query.preload()
108126

109-
// Wait for query to have data (either from snapshot during loading, or from final state if already ready)
127+
// Wait for query to have snapshot data
110128
await waitForQueryData(query, { minSize: 1, timeout: 10000 })
111129

130+
// Collection should still be loading if test control is active
131+
const wasStillLoading = progressiveUsers.status === `loading`
132+
133+
// Release the initial sync to allow atomic swap
134+
if (config.progressiveTestControl) {
135+
config.progressiveTestControl.releaseInitialSync()
136+
}
137+
112138
const beforeSwapSize = query.size
113139
const beforeSwapItems = Array.from(query.values())
114140

@@ -158,6 +184,9 @@ export function createProgressiveTestSuite(
158184
}
159185
const progressiveUsers = config.collections.progressive.users
160186

187+
// Start sync for this test
188+
progressiveUsers.startSyncImmediate()
189+
161190
// Create multiple queries with different predicates
162191
const query1 = createLiveQueryCollection((q) =>
163192
q
@@ -179,6 +208,11 @@ export function createProgressiveTestSuite(
179208
waitForQueryData(query2, { minSize: 1, timeout: 10000 }),
180209
])
181210

211+
// Release initial sync to allow atomic swap
212+
if (config.progressiveTestControl) {
213+
config.progressiveTestControl.releaseInitialSync()
214+
}
215+
182216
expect(query1.size).toBeGreaterThan(0)
183217
expect(query2.size).toBeGreaterThan(0)
184218

@@ -223,7 +257,12 @@ export function createProgressiveTestSuite(
223257
}
224258
const progressiveUsers = config.collections.progressive.users
225259

226-
// Wait for full sync first
260+
// Release initial sync immediately (we don't care about snapshot phase for this test)
261+
if (config.progressiveTestControl) {
262+
config.progressiveTestControl.releaseInitialSync()
263+
}
264+
265+
// Wait for full sync
227266
await waitFor(() => progressiveUsers.status === `ready`, {
228267
timeout: 30000,
229268
message: `Progressive collection did not complete sync`,
@@ -287,6 +326,11 @@ export function createProgressiveTestSuite(
287326

288327
const snapshotPhaseSize = query.size
289328

329+
// Release initial sync to allow atomic swap
330+
if (config.progressiveTestControl) {
331+
config.progressiveTestControl.releaseInitialSync()
332+
}
333+
290334
// Wait for atomic swap
291335
await waitFor(() => progressiveUsers.status === `ready`, {
292336
timeout: 30000,
@@ -337,6 +381,11 @@ export function createProgressiveTestSuite(
337381
)
338382
)
339383

384+
// Release initial sync to allow completion
385+
if (config.progressiveTestControl) {
386+
config.progressiveTestControl.releaseInitialSync()
387+
}
388+
340389
// All should have the same size and same data
341390
const sizes = queries.map((q) => q.size)
342391
expect(new Set(sizes).size).toBe(1) // All sizes are identical
@@ -366,8 +415,6 @@ export function createProgressiveTestSuite(
366415
const progressiveUsers = config.collections.progressive.users
367416

368417
// This test verifies the collection can be cleaned up even during snapshot phase
369-
// and that the atomic swap doesn't cause issues
370-
371418
const query = createLiveQueryCollection((q) =>
372419
q
373420
.from({ user: progressiveUsers })
@@ -376,6 +423,11 @@ export function createProgressiveTestSuite(
376423

377424
await query.preload()
378425

426+
// Release initial sync
427+
if (config.progressiveTestControl) {
428+
config.progressiveTestControl.releaseInitialSync()
429+
}
430+
379431
// Don't wait for data, just cleanup immediately
380432
await query.cleanup()
381433

packages/db-collection-e2e/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export interface E2ETestConfig {
8080
// When true, tests will wait for mutations to propagate before proceeding
8181
hasReplicationLag?: boolean
8282

83+
// Test control for progressive mode (Electric only)
84+
// Allows explicit control over when initial sync completes for deterministic testing
85+
progressiveTestControl?: {
86+
releaseInitialSync: () => void
87+
}
88+
8389
// Lifecycle hooks
8490
setup: () => Promise<void>
8591
teardown: () => Promise<void>

packages/electric-db-collection/e2e/electric.e2e.test.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { afterAll, afterEach, beforeAll, describe, inject } from "vitest"
88
import { createCollection } from "@tanstack/db"
9-
import { electricCollectionOptions } from "../src/electric"
9+
import { ELECTRIC_TEST_HOOKS, electricCollectionOptions } from "../src/electric"
1010
import { makePgClient } from "../../db-collection-e2e/support/global-setup"
1111
import {
1212
createCollationTestSuite,
@@ -312,6 +312,31 @@ describe(`Electric Collection E2E Tests`, () => {
312312
})
313313
)
314314

315+
// Create control mechanisms for progressive collections
316+
// These allow tests to explicitly control when the atomic swap happens
317+
// We use a ref object so each test can get a fresh promise
318+
const usersUpToDateControl = {
319+
current: null as (() => void) | null,
320+
createPromise: () =>
321+
new Promise<void>((resolve) => {
322+
usersUpToDateControl.current = resolve
323+
}),
324+
}
325+
const postsUpToDateControl = {
326+
current: null as (() => void) | null,
327+
createPromise: () =>
328+
new Promise<void>((resolve) => {
329+
postsUpToDateControl.current = resolve
330+
}),
331+
}
332+
const commentsUpToDateControl = {
333+
current: null as (() => void) | null,
334+
createPromise: () =>
335+
new Promise<void>((resolve) => {
336+
commentsUpToDateControl.current = resolve
337+
}),
338+
}
339+
315340
const progressiveUsers = createCollection(
316341
electricCollectionOptions({
317342
id: `electric-e2e-users-progressive-${testId}`,
@@ -323,7 +348,10 @@ describe(`Electric Collection E2E Tests`, () => {
323348
},
324349
syncMode: `progressive`,
325350
getKey: (item: any) => item.id,
326-
startSync: true,
351+
startSync: false, // Don't start immediately - tests will start when ready
352+
[ELECTRIC_TEST_HOOKS]: {
353+
beforeMarkingReady: () => usersUpToDateControl.createPromise(),
354+
},
327355
})
328356
)
329357

@@ -338,7 +366,10 @@ describe(`Electric Collection E2E Tests`, () => {
338366
},
339367
syncMode: `progressive`,
340368
getKey: (item: any) => item.id,
341-
startSync: true,
369+
startSync: false, // Don't start immediately - tests will start when ready
370+
[ELECTRIC_TEST_HOOKS]: {
371+
beforeMarkingReady: () => postsUpToDateControl.createPromise(),
372+
},
342373
})
343374
)
344375

@@ -353,7 +384,10 @@ describe(`Electric Collection E2E Tests`, () => {
353384
},
354385
syncMode: `progressive`,
355386
getKey: (item: any) => item.id,
356-
startSync: true,
387+
startSync: false, // Don't start immediately - tests will start when ready
388+
[ELECTRIC_TEST_HOOKS]: {
389+
beforeMarkingReady: () => commentsUpToDateControl.createPromise(),
390+
},
357391
})
358392
)
359393

@@ -367,10 +401,9 @@ describe(`Electric Collection E2E Tests`, () => {
367401
await onDemandPosts.preload()
368402
await onDemandComments.preload()
369403

370-
// Progressive collections start syncing in background, just preload to get started
371-
await progressiveUsers.preload()
372-
await progressivePosts.preload()
373-
await progressiveComments.preload()
404+
// Progressive collections start syncing in background
405+
// Note: We DON'T call preload() here because the test hooks will block
406+
// Individual progressive tests will handle preload and release as needed
374407

375408
config = {
376409
collections: {
@@ -391,6 +424,13 @@ describe(`Electric Collection E2E Tests`, () => {
391424
},
392425
},
393426
hasReplicationLag: true, // Electric has async replication lag
427+
progressiveTestControl: {
428+
releaseInitialSync: () => {
429+
usersUpToDateControl.current?.()
430+
postsUpToDateControl.current?.()
431+
commentsUpToDateControl.current?.()
432+
},
433+
},
394434
mutations: {
395435
// Use direct SQL for Electric tests (simulates external changes)
396436
// This tests that Electric sync picks up database changes

0 commit comments

Comments
 (0)