fix(mobile): support QR scanning without Play Services
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
import { encodePairingConnectionPayload, buildPairingConnectionPayload } from '@/lib/connectionPayload';
|
||||
|
||||
import { parseConnectionPayload } from './mobileQrScan';
|
||||
import { parseConnectionPayload, scanConnectionQr } from './mobileQrScan';
|
||||
|
||||
const hostEncPubJwk = { kty: 'EC', crv: 'P-256', x: 'eHhY', y: 'eVlZ' } as const;
|
||||
|
||||
@@ -40,3 +40,126 @@ describe('parseConnectionPayload', () => {
|
||||
expect(parseConnectionPayload('openchamber://connect?v=1&mode=relay#offer=eyJ2IjoxfQ')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('scanConnectionQr on Android', () => {
|
||||
const originalWindow = globalThis.window;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow });
|
||||
});
|
||||
|
||||
test('uses the bundled startScan flow and cleans up after a result', async () => {
|
||||
const listeners = new Map<string, (event: { barcodes?: Array<{ rawValue?: string }> }) => void>();
|
||||
let removeCalls = 0;
|
||||
let stopCalls = 0;
|
||||
let scanCalls = 0;
|
||||
let startOptions: unknown;
|
||||
const remove = () => { removeCalls += 1; };
|
||||
const stopScan = async () => { stopCalls += 1; };
|
||||
const scan = async () => { scanCalls += 1; return { barcodes: [] }; };
|
||||
const startScan = async (options?: unknown) => {
|
||||
startOptions = options;
|
||||
listeners.get('barcodesScanned')?.({ barcodes: [{ rawValue: 'https://oc.example' }] });
|
||||
};
|
||||
const plugin = {
|
||||
requestPermissions: mock(async () => ({ camera: 'granted' })),
|
||||
scan,
|
||||
startScan,
|
||||
stopScan,
|
||||
addListener: mock((event: string, callback: (info: { barcodes?: Array<{ rawValue?: string }> }) => void) => {
|
||||
listeners.set(event, callback);
|
||||
return Promise.resolve({ remove });
|
||||
}),
|
||||
};
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: { Capacitor: { getPlatform: () => 'android', Plugins: { BarcodeScanner: plugin } } },
|
||||
});
|
||||
|
||||
expect(await scanConnectionQr()).toEqual({ status: 'ok', url: 'https://oc.example' });
|
||||
expect(startOptions).toEqual({ formats: ['QR_CODE'] });
|
||||
expect(scanCalls).toBe(0);
|
||||
expect(stopCalls).toBe(1);
|
||||
expect(removeCalls).toBe(2);
|
||||
});
|
||||
|
||||
test('stops scanning when the caller aborts', async () => {
|
||||
let stopCalls = 0;
|
||||
const stopScan = async () => { stopCalls += 1; };
|
||||
const plugin = {
|
||||
requestPermissions: mock(async () => ({ camera: 'granted' })),
|
||||
startScan: mock(async () => undefined),
|
||||
stopScan,
|
||||
addListener: mock(async () => ({ remove: mock(() => undefined) })),
|
||||
};
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: { Capacitor: { getPlatform: () => 'android', Plugins: { BarcodeScanner: plugin } } },
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const result = scanConnectionQr({ signal: controller.signal });
|
||||
await Promise.resolve();
|
||||
controller.abort();
|
||||
|
||||
expect(await result).toEqual({ status: 'cancelled' });
|
||||
expect(stopCalls).toBe(1);
|
||||
});
|
||||
|
||||
test('waits for listener setup to finish before cleaning up an aborted scan', async () => {
|
||||
let finishListenerSetup: (() => void) | undefined;
|
||||
let removeCalls = 0;
|
||||
let startCalls = 0;
|
||||
let stopCalls = 0;
|
||||
const listenerSetup = new Promise<void>((resolve) => { finishListenerSetup = resolve; });
|
||||
const remove = () => { removeCalls += 1; };
|
||||
const startScan = async () => { startCalls += 1; };
|
||||
const plugin = {
|
||||
requestPermissions: mock(async () => ({ camera: 'granted' })),
|
||||
startScan,
|
||||
stopScan: async () => { stopCalls += 1; },
|
||||
addListener: mock(async () => {
|
||||
await listenerSetup;
|
||||
return { remove };
|
||||
}),
|
||||
};
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: { Capacitor: { getPlatform: () => 'android', Plugins: { BarcodeScanner: plugin } } },
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const result = scanConnectionQr({ signal: controller.signal });
|
||||
await Promise.resolve();
|
||||
controller.abort();
|
||||
finishListenerSetup?.();
|
||||
|
||||
expect(await result).toEqual({ status: 'cancelled' });
|
||||
expect(startCalls).toBe(0);
|
||||
expect(removeCalls).toBe(2);
|
||||
expect(stopCalls).toBe(1);
|
||||
});
|
||||
|
||||
test('cleans up successful listener registration when the other listener fails', async () => {
|
||||
let removeCalls = 0;
|
||||
let startCalls = 0;
|
||||
let stopCalls = 0;
|
||||
const remove = () => { removeCalls += 1; };
|
||||
const plugin = {
|
||||
requestPermissions: mock(async () => ({ camera: 'granted' })),
|
||||
startScan: async () => { startCalls += 1; },
|
||||
stopScan: async () => { stopCalls += 1; },
|
||||
addListener: mock(async (event: string) => {
|
||||
if (event === 'scanError') throw new Error('listener setup failed');
|
||||
return { remove };
|
||||
}),
|
||||
};
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: { Capacitor: { getPlatform: () => 'android', Plugins: { BarcodeScanner: plugin } } },
|
||||
});
|
||||
|
||||
expect(await scanConnectionQr()).toEqual({ status: 'failed' });
|
||||
expect(startCalls).toBe(0);
|
||||
expect(removeCalls).toBe(1);
|
||||
expect(stopCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user