|
1 | 1 | import os from 'node:os';
|
2 | 2 | import fs from 'node:fs/promises';
|
3 | 3 | import path from 'node:path';
|
| 4 | +import { defaultFetchMockConfig, FetchMock } from 'fetch-mock'; |
4 | 5 | import { test } from 'vitest';
|
5 |
| -import { column, PowerSyncDatabase, Schema, Table } from '../lib'; |
| 6 | +import { AbstractPowerSyncDatabase, AbstractRemoteOptions, column, NodePowerSyncDatabaseOptions, PowerSyncBackendConnector, PowerSyncCredentials, PowerSyncDatabase, Schema, StreamingSyncLine, SyncStatus, Table } from '../lib'; |
6 | 7 |
|
7 | 8 | export async function createTempDir() {
|
8 | 9 | const ostmpdir = os.tmpdir();
|
@@ -37,15 +38,115 @@ export const tempDirectoryTest = test.extend<{ tmpdir: string }>({
|
37 | 38 | }
|
38 | 39 | });
|
39 | 40 |
|
40 |
| -export const databaseTest = tempDirectoryTest.extend<{ database: PowerSyncDatabase }>({ |
41 |
| - database: async ({ tmpdir }, use) => { |
| 41 | +function createDatabaseFixture(options: Partial<NodePowerSyncDatabaseOptions> = {}) { |
| 42 | + return async ({ tmpdir }, use) => { |
42 | 43 | const database = new PowerSyncDatabase({
|
| 44 | + ...options, |
43 | 45 | schema: AppSchema,
|
44 | 46 | database: {
|
45 | 47 | dbFilename: 'test.db',
|
46 | 48 | dbLocation: tmpdir
|
47 | 49 | }
|
48 | 50 | });
|
49 | 51 | await use(database);
|
50 |
| - } |
| 52 | + await database.close(); |
| 53 | + }; |
| 54 | +} |
| 55 | + |
| 56 | +export const databaseTest = tempDirectoryTest.extend<{ database: PowerSyncDatabase }>({ |
| 57 | + database: async ({tmpdir}, use) => { |
| 58 | + await createDatabaseFixture()({tmpdir}, use); |
| 59 | + }, |
| 60 | +}); |
| 61 | + |
| 62 | +// TODO: Unify this with the test setup for the web SDK. |
| 63 | +export const mockSyncServiceTest = tempDirectoryTest.extend<{syncService: MockSyncService}>({ |
| 64 | + syncService: async ({}, use) => { |
| 65 | + // Don't install global fetch mocks, we want tests to be isolated! |
| 66 | + const fetchMock = new FetchMock(defaultFetchMockConfig); |
| 67 | + const listeners: ReadableStreamDefaultController<StreamingSyncLine>[] = []; |
| 68 | + |
| 69 | + fetchMock.route('path:/sync/stream', async () => { |
| 70 | + let thisController: ReadableStreamDefaultController<StreamingSyncLine> | null = null; |
| 71 | + |
| 72 | + const syncLines = new ReadableStream<StreamingSyncLine>({ |
| 73 | + start(controller) { |
| 74 | + thisController = controller; |
| 75 | + listeners.push(controller); |
| 76 | + }, |
| 77 | + cancel() { |
| 78 | + listeners.splice(listeners.indexOf(thisController!), 1); |
| 79 | + }, |
| 80 | + }); |
| 81 | + |
| 82 | + |
| 83 | + const encoder = new TextEncoder(); |
| 84 | + const asLines = new TransformStream<StreamingSyncLine, Uint8Array>({ |
| 85 | + transform: (chunk, controller) => { |
| 86 | + const line = `${JSON.stringify(chunk)}\n`; |
| 87 | + controller.enqueue(encoder.encode(line)); |
| 88 | + }, |
| 89 | + }); |
| 90 | + |
| 91 | + return new Response(syncLines.pipeThrough(asLines), {status: 200}); |
| 92 | + }); |
| 93 | + fetchMock.catch(404); |
| 94 | + |
| 95 | + await use({ |
| 96 | + clientOptions: { |
| 97 | + fetchImplementation: fetchMock.fetchHandler.bind(fetchMock), |
| 98 | + }, |
| 99 | + get connectedListeners() { |
| 100 | + return listeners.length; |
| 101 | + }, |
| 102 | + pushLine(line) { |
| 103 | + for (const listener of listeners) { |
| 104 | + listener.enqueue(line); |
| 105 | + } |
| 106 | + }, |
| 107 | + }); |
| 108 | + }, |
| 109 | +}); |
| 110 | + |
| 111 | +export const connectedDatabaseTest = mockSyncServiceTest.extend<{ database: PowerSyncDatabase }>({ |
| 112 | + database: async ({ tmpdir, syncService }, use) => { |
| 113 | + const fixture = createDatabaseFixture({remoteOptions: syncService.clientOptions}); |
| 114 | + await fixture({ tmpdir }, use); |
| 115 | + }, |
51 | 116 | });
|
| 117 | + |
| 118 | +export interface MockSyncService { |
| 119 | + clientOptions: Partial<AbstractRemoteOptions>, |
| 120 | + pushLine: (line: StreamingSyncLine) => void, |
| 121 | + connectedListeners: number, |
| 122 | +} |
| 123 | + |
| 124 | +export class TestConnector implements PowerSyncBackendConnector { |
| 125 | + async fetchCredentials(): Promise<PowerSyncCredentials> { |
| 126 | + return { |
| 127 | + endpoint: '', |
| 128 | + token: '' |
| 129 | + }; |
| 130 | + } |
| 131 | + async uploadData(database: AbstractPowerSyncDatabase): Promise<void> { |
| 132 | + const tx = await database.getNextCrudTransaction(); |
| 133 | + await tx?.complete(); |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +export function waitForSyncStatus(database: AbstractPowerSyncDatabase, matcher: (status: SyncStatus) => boolean): Promise<void> { |
| 138 | + return new Promise((resolve) => { |
| 139 | + if (matcher(database.currentStatus)) { |
| 140 | + return resolve(); |
| 141 | + } |
| 142 | + |
| 143 | + const dispose = database.registerListener({ |
| 144 | + statusChanged: (status) => { |
| 145 | + if (matcher(status)) { |
| 146 | + dispose(); |
| 147 | + resolve(); |
| 148 | + } |
| 149 | + } |
| 150 | + }); |
| 151 | + }); |
| 152 | +} |
0 commit comments