@flighthq/host-capacitor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capacitorApp.d.ts +4 -0
- package/dist/capacitorApp.d.ts.map +1 -0
- package/dist/capacitorApp.js +172 -0
- package/dist/capacitorApp.js.map +1 -0
- package/dist/capacitorClipboard.d.ts +4 -0
- package/dist/capacitorClipboard.d.ts.map +1 -0
- package/dist/capacitorClipboard.js +127 -0
- package/dist/capacitorClipboard.js.map +1 -0
- package/dist/capacitorConnectivity.d.ts +4 -0
- package/dist/capacitorConnectivity.d.ts.map +1 -0
- package/dist/capacitorConnectivity.js +69 -0
- package/dist/capacitorConnectivity.js.map +1 -0
- package/dist/capacitorDevice.d.ts +4 -0
- package/dist/capacitorDevice.d.ts.map +1 -0
- package/dist/capacitorDevice.js +100 -0
- package/dist/capacitorDevice.js.map +1 -0
- package/dist/capacitorDialog.d.ts +4 -0
- package/dist/capacitorDialog.d.ts.map +1 -0
- package/dist/capacitorDialog.js +39 -0
- package/dist/capacitorDialog.js.map +1 -0
- package/dist/capacitorFileSystem.d.ts +4 -0
- package/dist/capacitorFileSystem.d.ts.map +1 -0
- package/dist/capacitorFileSystem.js +216 -0
- package/dist/capacitorFileSystem.js.map +1 -0
- package/dist/capacitorGeolocation.d.ts +4 -0
- package/dist/capacitorGeolocation.d.ts.map +1 -0
- package/dist/capacitorGeolocation.js +104 -0
- package/dist/capacitorGeolocation.js.map +1 -0
- package/dist/capacitorHaptics.d.ts +4 -0
- package/dist/capacitorHaptics.d.ts.map +1 -0
- package/dist/capacitorHaptics.js +56 -0
- package/dist/capacitorHaptics.js.map +1 -0
- package/dist/capacitorKeyboard.d.ts +4 -0
- package/dist/capacitorKeyboard.d.ts.map +1 -0
- package/dist/capacitorKeyboard.js +89 -0
- package/dist/capacitorKeyboard.js.map +1 -0
- package/dist/capacitorModule.d.ts +310 -0
- package/dist/capacitorModule.d.ts.map +1 -0
- package/dist/capacitorModule.js +31 -0
- package/dist/capacitorModule.js.map +1 -0
- package/dist/capacitorNotification.d.ts +4 -0
- package/dist/capacitorNotification.d.ts.map +1 -0
- package/dist/capacitorNotification.js +172 -0
- package/dist/capacitorNotification.js.map +1 -0
- package/dist/capacitorRegister.d.ts +3 -0
- package/dist/capacitorRegister.d.ts.map +1 -0
- package/dist/capacitorRegister.js +52 -0
- package/dist/capacitorRegister.js.map +1 -0
- package/dist/capacitorShare.d.ts +4 -0
- package/dist/capacitorShare.d.ts.map +1 -0
- package/dist/capacitorShare.js +66 -0
- package/dist/capacitorShare.js.map +1 -0
- package/dist/capacitorStatusBar.d.ts +4 -0
- package/dist/capacitorStatusBar.d.ts.map +1 -0
- package/dist/capacitorStatusBar.js +80 -0
- package/dist/capacitorStatusBar.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/capacitorApp.test.ts +81 -0
- package/src/capacitorClipboard.test.ts +71 -0
- package/src/capacitorConnectivity.test.ts +64 -0
- package/src/capacitorDevice.test.ts +99 -0
- package/src/capacitorDialog.test.ts +50 -0
- package/src/capacitorFileSystem.test.ts +88 -0
- package/src/capacitorGeolocation.test.ts +75 -0
- package/src/capacitorHaptics.test.ts +75 -0
- package/src/capacitorKeyboard.test.ts +89 -0
- package/src/capacitorNotification.test.ts +95 -0
- package/src/capacitorRegister.test.ts +104 -0
- package/src/capacitorShare.test.ts +68 -0
- package/src/capacitorStatusBar.test.ts +66 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createCapacitorFileSystemBackend } from './capacitorFileSystem';
|
|
2
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
3
|
+
|
|
4
|
+
function fakeCapacitor() {
|
|
5
|
+
const files = new Map<string, { data: string; type: string }>();
|
|
6
|
+
const capacitor = {
|
|
7
|
+
filesystem: {
|
|
8
|
+
async readFile(options: { path: string; encoding?: string }) {
|
|
9
|
+
const entry = files.get(options.path);
|
|
10
|
+
if (entry === undefined) throw new Error('missing');
|
|
11
|
+
return { data: entry.data };
|
|
12
|
+
},
|
|
13
|
+
async writeFile(options: { path: string; data: string }) {
|
|
14
|
+
files.set(options.path, { data: options.data, type: 'file' });
|
|
15
|
+
return { uri: `file://${options.path}` };
|
|
16
|
+
},
|
|
17
|
+
async appendFile(options: { path: string; data: string }) {
|
|
18
|
+
const entry = files.get(options.path);
|
|
19
|
+
files.set(options.path, { data: (entry?.data ?? '') + options.data, type: 'file' });
|
|
20
|
+
},
|
|
21
|
+
async deleteFile(options: { path: string }) {
|
|
22
|
+
if (!files.delete(options.path)) throw new Error('missing');
|
|
23
|
+
},
|
|
24
|
+
async mkdir(options: { path: string }) {
|
|
25
|
+
files.set(options.path, { data: '', type: 'directory' });
|
|
26
|
+
},
|
|
27
|
+
async rmdir(options: { path: string }) {
|
|
28
|
+
files.delete(options.path);
|
|
29
|
+
},
|
|
30
|
+
async readdir(options: { path: string }) {
|
|
31
|
+
return {
|
|
32
|
+
files: [...files.keys()]
|
|
33
|
+
.filter((key) => key.startsWith(options.path))
|
|
34
|
+
.map((key) => ({ name: key, uri: `file://${key}`, type: files.get(key)!.type, size: 0, mtime: 0 })),
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
async stat(options: { path: string }) {
|
|
38
|
+
const entry = files.get(options.path);
|
|
39
|
+
if (entry === undefined) throw new Error('missing');
|
|
40
|
+
return { type: entry.type, size: entry.data.length, mtime: 100, ctime: 50, uri: `file://${options.path}` };
|
|
41
|
+
},
|
|
42
|
+
async rename() {},
|
|
43
|
+
async copy() {},
|
|
44
|
+
},
|
|
45
|
+
} as unknown as CapacitorApi;
|
|
46
|
+
return { capacitor, files };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('createCapacitorFileSystemBackend', () => {
|
|
50
|
+
it('round-trips a text file', async () => {
|
|
51
|
+
const backend = createCapacitorFileSystemBackend(fakeCapacitor().capacitor);
|
|
52
|
+
expect(await backend.writeTextFile('/a.txt', 'hello')).toBe(true);
|
|
53
|
+
expect(await backend.readTextFile('/a.txt')).toBe('hello');
|
|
54
|
+
expect(await backend.appendTextFile('/a.txt', '!')).toBe(true);
|
|
55
|
+
expect(await backend.readTextFile('/a.txt')).toBe('hello!');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('round-trips a binary file through Base64', async () => {
|
|
59
|
+
const backend = createCapacitorFileSystemBackend(fakeCapacitor().capacitor);
|
|
60
|
+
const bytes = new Uint8Array([0, 1, 2, 254, 255]);
|
|
61
|
+
expect(await backend.writeBinaryFile('/b.bin', bytes)).toBe(true);
|
|
62
|
+
const read = await backend.readBinaryFile('/b.bin');
|
|
63
|
+
expect(read).not.toBeNull();
|
|
64
|
+
expect([...read!]).toEqual([0, 1, 2, 254, 255]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('maps exists/stat/remove and reports null for a missing file', async () => {
|
|
68
|
+
const backend = createCapacitorFileSystemBackend(fakeCapacitor().capacitor);
|
|
69
|
+
await backend.writeTextFile('/c.txt', 'x');
|
|
70
|
+
expect(await backend.fileExists('/c.txt')).toBe(true);
|
|
71
|
+
expect(await backend.directoryExists('/c.txt')).toBe(false);
|
|
72
|
+
const stat = await backend.statFile('/c.txt');
|
|
73
|
+
expect(stat).toMatchObject({ size: 1, isDirectory: false, createdTime: 50 });
|
|
74
|
+
expect(await backend.removeFile('/c.txt')).toBe(true);
|
|
75
|
+
expect(await backend.readTextFile('/c.txt')).toBeNull();
|
|
76
|
+
expect(await backend.statFile('/missing')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reports sentinels for the unmodeled surface', async () => {
|
|
80
|
+
const backend = createCapacitorFileSystemBackend(fakeCapacitor().capacitor);
|
|
81
|
+
expect(await backend.readBinaryFileRange('/x', 0, 4)).toBeNull();
|
|
82
|
+
expect(await backend.getFilePermissions('/x')).toBeNull();
|
|
83
|
+
expect(await backend.canAccessFile('/x', 'readable')).toBe(false);
|
|
84
|
+
expect(await backend.getFileSystemUsage()).toBeNull();
|
|
85
|
+
expect(backend.getPath('home')).toBe('');
|
|
86
|
+
expect(typeof backend.watch('/x', () => {})).toBe('function');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createCapacitorGeolocationBackend } from './capacitorGeolocation';
|
|
2
|
+
import type { CapacitorApi, CapacitorPosition } from './capacitorModule';
|
|
3
|
+
|
|
4
|
+
const flush = async () => {
|
|
5
|
+
await Promise.resolve();
|
|
6
|
+
await Promise.resolve();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function samplePosition(): CapacitorPosition {
|
|
10
|
+
return {
|
|
11
|
+
timestamp: 1234,
|
|
12
|
+
coords: {
|
|
13
|
+
latitude: 37.5,
|
|
14
|
+
longitude: -122.3,
|
|
15
|
+
accuracy: 5,
|
|
16
|
+
altitude: 10,
|
|
17
|
+
altitudeAccuracy: 2,
|
|
18
|
+
heading: 90,
|
|
19
|
+
speed: 1.5,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fakeCapacitor(permission = 'granted') {
|
|
25
|
+
const cleared: string[] = [];
|
|
26
|
+
let watchCallback: ((position: CapacitorPosition | null, err?: unknown) => void) | null = null;
|
|
27
|
+
const capacitor = {
|
|
28
|
+
geolocation: {
|
|
29
|
+
async getCurrentPosition() {
|
|
30
|
+
return samplePosition();
|
|
31
|
+
},
|
|
32
|
+
async watchPosition(_options: unknown, callback: (position: CapacitorPosition | null, err?: unknown) => void) {
|
|
33
|
+
watchCallback = callback;
|
|
34
|
+
return 'watch-abc';
|
|
35
|
+
},
|
|
36
|
+
async clearWatch(options: { id: string }) {
|
|
37
|
+
cleared.push(options.id);
|
|
38
|
+
},
|
|
39
|
+
async checkPermissions() {
|
|
40
|
+
return { location: permission };
|
|
41
|
+
},
|
|
42
|
+
async requestPermissions() {
|
|
43
|
+
return { location: permission };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
} as unknown as CapacitorApi;
|
|
47
|
+
return { capacitor, cleared, fire: (p: CapacitorPosition) => watchCallback?.(p) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('createCapacitorGeolocationBackend', () => {
|
|
51
|
+
it('maps a Capacitor position onto a GeoPosition', async () => {
|
|
52
|
+
const backend = createCapacitorGeolocationBackend(fakeCapacitor().capacitor);
|
|
53
|
+
const position = await backend.getCurrentPosition({});
|
|
54
|
+
expect(position).toMatchObject({ latitude: 37.5, longitude: -122.3, accuracy: 5, heading: 90, floorLevel: 0 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('maps permission state', async () => {
|
|
58
|
+
const backend = createCapacitorGeolocationBackend(fakeCapacitor('denied').capacitor);
|
|
59
|
+
expect(await backend.getPermission()).toBe('denied');
|
|
60
|
+
expect(await backend.requestPermission()).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('bridges the numeric watch id and clears the resolved string id', async () => {
|
|
64
|
+
const { capacitor, cleared, fire } = fakeCapacitor();
|
|
65
|
+
const backend = createCapacitorGeolocationBackend(capacitor);
|
|
66
|
+
let received = 0;
|
|
67
|
+
const id = backend.watchPosition(() => received++, {});
|
|
68
|
+
expect(typeof id).toBe('number');
|
|
69
|
+
await flush();
|
|
70
|
+
fire(samplePosition());
|
|
71
|
+
expect(received).toBe(1);
|
|
72
|
+
backend.clearWatch(id);
|
|
73
|
+
expect(cleared).toContain('watch-abc');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { HapticsCapabilities } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createCapacitorHapticsBackend } from './capacitorHaptics';
|
|
4
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
5
|
+
|
|
6
|
+
function fakeCapacitor() {
|
|
7
|
+
const calls: Array<{ method: string; arg?: unknown }> = [];
|
|
8
|
+
const capacitor = {
|
|
9
|
+
haptics: {
|
|
10
|
+
async impact(arg: unknown) {
|
|
11
|
+
calls.push({ method: 'impact', arg });
|
|
12
|
+
},
|
|
13
|
+
async notification(arg: unknown) {
|
|
14
|
+
calls.push({ method: 'notification', arg });
|
|
15
|
+
},
|
|
16
|
+
async selectionChanged() {
|
|
17
|
+
calls.push({ method: 'selectionChanged' });
|
|
18
|
+
},
|
|
19
|
+
async selectionStart() {
|
|
20
|
+
calls.push({ method: 'selectionStart' });
|
|
21
|
+
},
|
|
22
|
+
async selectionEnd() {
|
|
23
|
+
calls.push({ method: 'selectionEnd' });
|
|
24
|
+
},
|
|
25
|
+
async vibrate(arg: unknown) {
|
|
26
|
+
calls.push({ method: 'vibrate', arg });
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} as unknown as CapacitorApi;
|
|
30
|
+
return { capacitor, calls };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('createCapacitorHapticsBackend', () => {
|
|
34
|
+
it('maps impact styles onto Capacitor enums', () => {
|
|
35
|
+
const { capacitor, calls } = fakeCapacitor();
|
|
36
|
+
const backend = createCapacitorHapticsBackend(capacitor);
|
|
37
|
+
expect(backend.impact('soft')).toBe(true);
|
|
38
|
+
expect(backend.impact('rigid')).toBe(true);
|
|
39
|
+
expect(calls[0].arg).toEqual({ style: 'LIGHT' });
|
|
40
|
+
expect(calls[1].arg).toEqual({ style: 'HEAVY' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('maps notification, selection, and vibrate', () => {
|
|
44
|
+
const { capacitor, calls } = fakeCapacitor();
|
|
45
|
+
const backend = createCapacitorHapticsBackend(capacitor);
|
|
46
|
+
expect(backend.notification('success')).toBe(true);
|
|
47
|
+
expect(backend.selection()).toBe(true);
|
|
48
|
+
expect(backend.vibrate(200)).toBe(true);
|
|
49
|
+
expect(calls.map((c) => c.method)).toEqual(['notification', 'selectionChanged', 'vibrate']);
|
|
50
|
+
expect(calls[0].arg).toEqual({ type: 'SUCCESS' });
|
|
51
|
+
expect(calls[2].arg).toEqual({ duration: 200 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('reports capabilities and unsupported operations', () => {
|
|
55
|
+
const backend = createCapacitorHapticsBackend(fakeCapacitor().capacitor);
|
|
56
|
+
const out: HapticsCapabilities = {
|
|
57
|
+
amplitudeControl: true,
|
|
58
|
+
customEvents: true,
|
|
59
|
+
intensity: true,
|
|
60
|
+
patterns: true,
|
|
61
|
+
supported: false,
|
|
62
|
+
};
|
|
63
|
+
expect(backend.capabilities(out)).toBe(out);
|
|
64
|
+
expect(out).toEqual({
|
|
65
|
+
amplitudeControl: false,
|
|
66
|
+
customEvents: false,
|
|
67
|
+
intensity: false,
|
|
68
|
+
patterns: false,
|
|
69
|
+
supported: true,
|
|
70
|
+
});
|
|
71
|
+
expect(backend.cancel()).toBe(false);
|
|
72
|
+
expect(backend.vibratePattern([10, 20])).toBe(false);
|
|
73
|
+
expect(backend.isSupported()).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { SoftKeyboardInfo } from '@flighthq/types';
|
|
2
|
+
import { SoftKeyboardResizeBodyKind } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createCapacitorKeyboardBackend } from './capacitorKeyboard';
|
|
5
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
6
|
+
|
|
7
|
+
const flush = async () => {
|
|
8
|
+
await Promise.resolve();
|
|
9
|
+
await Promise.resolve();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function fakeCapacitor() {
|
|
13
|
+
const calls: Array<{ method: string; arg?: unknown }> = [];
|
|
14
|
+
const listeners = new Map<string, Array<(info?: unknown) => void>>();
|
|
15
|
+
const capacitor = {
|
|
16
|
+
keyboard: {
|
|
17
|
+
async show() {
|
|
18
|
+
calls.push({ method: 'show' });
|
|
19
|
+
},
|
|
20
|
+
async hide() {
|
|
21
|
+
calls.push({ method: 'hide' });
|
|
22
|
+
},
|
|
23
|
+
async setAccessoryBarVisible(arg: unknown) {
|
|
24
|
+
calls.push({ method: 'setAccessoryBarVisible', arg });
|
|
25
|
+
},
|
|
26
|
+
async setResizeMode(arg: unknown) {
|
|
27
|
+
calls.push({ method: 'setResizeMode', arg });
|
|
28
|
+
},
|
|
29
|
+
async setScroll(arg: unknown) {
|
|
30
|
+
calls.push({ method: 'setScroll', arg });
|
|
31
|
+
},
|
|
32
|
+
async setStyle(arg: unknown) {
|
|
33
|
+
calls.push({ method: 'setStyle', arg });
|
|
34
|
+
},
|
|
35
|
+
async addListener(eventName: string, listener: (info?: unknown) => void) {
|
|
36
|
+
const list = listeners.get(eventName) ?? [];
|
|
37
|
+
list.push(listener);
|
|
38
|
+
listeners.set(eventName, list);
|
|
39
|
+
return { async remove() {} };
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
} as unknown as CapacitorApi;
|
|
43
|
+
const fire = (eventName: string, info?: unknown) => listeners.get(eventName)?.forEach((l) => l(info));
|
|
44
|
+
return { capacitor, calls, fire };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function blankInfo(): SoftKeyboardInfo {
|
|
48
|
+
return { visible: false, height: 0, x: 0, y: 0, width: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('createCapacitorKeyboardBackend', () => {
|
|
52
|
+
it('maps show/hide and setters onto the plugin', () => {
|
|
53
|
+
const { capacitor, calls } = fakeCapacitor();
|
|
54
|
+
const backend = createCapacitorKeyboardBackend(capacitor);
|
|
55
|
+
backend.show();
|
|
56
|
+
backend.hide();
|
|
57
|
+
backend.setResizeMode?.(SoftKeyboardResizeBodyKind);
|
|
58
|
+
backend.setScrollAssistEnabled?.(false);
|
|
59
|
+
expect(calls.map((c) => c.method)).toEqual(['show', 'hide', 'setResizeMode', 'setScroll']);
|
|
60
|
+
expect(calls[2].arg).toEqual({ mode: 'body' });
|
|
61
|
+
expect(calls[3].arg).toEqual({ isDisabled: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('tracks the keyboard mirror from will-show/will-hide events', async () => {
|
|
65
|
+
const { capacitor, fire } = fakeCapacitor();
|
|
66
|
+
const backend = createCapacitorKeyboardBackend(capacitor);
|
|
67
|
+
await flush();
|
|
68
|
+
fire('keyboardWillShow', { keyboardHeight: 320 });
|
|
69
|
+
const shown = backend.getInfo(blankInfo());
|
|
70
|
+
expect(shown.visible).toBe(true);
|
|
71
|
+
expect(shown.height).toBe(320);
|
|
72
|
+
fire('keyboardWillHide');
|
|
73
|
+
expect(backend.getInfo(blankInfo()).visible).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('delivers will-phase transitions to a subscriber', async () => {
|
|
77
|
+
const { capacitor, fire } = fakeCapacitor();
|
|
78
|
+
const backend = createCapacitorKeyboardBackend(capacitor);
|
|
79
|
+
const events: Array<{ phase: string; height: number }> = [];
|
|
80
|
+
backend.subscribe((phase, transition) => events.push({ phase, height: transition.height }));
|
|
81
|
+
await flush();
|
|
82
|
+
fire('keyboardWillShow', { keyboardHeight: 300 });
|
|
83
|
+
fire('keyboardWillHide');
|
|
84
|
+
expect(events).toEqual([
|
|
85
|
+
{ phase: 'will', height: 300 },
|
|
86
|
+
{ phase: 'will', height: 0 },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
2
|
+
import { createCapacitorNotificationBackend } from './capacitorNotification';
|
|
3
|
+
|
|
4
|
+
const flush = async () => {
|
|
5
|
+
await Promise.resolve();
|
|
6
|
+
await Promise.resolve();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function fakeCapacitor(display = 'granted') {
|
|
10
|
+
const scheduled: Array<{ id: number; title: string }> = [];
|
|
11
|
+
const cancelled: number[] = [];
|
|
12
|
+
const actionListeners: Array<(action: { actionId: string; notification: { id: number } }) => void> = [];
|
|
13
|
+
const capacitor = {
|
|
14
|
+
localNotifications: {
|
|
15
|
+
async schedule(options: { notifications: Array<{ id: number; title: string }> }) {
|
|
16
|
+
scheduled.push(...options.notifications);
|
|
17
|
+
return { notifications: options.notifications.map((n) => ({ id: n.id })) };
|
|
18
|
+
},
|
|
19
|
+
async requestPermissions() {
|
|
20
|
+
return { display };
|
|
21
|
+
},
|
|
22
|
+
async checkPermissions() {
|
|
23
|
+
return { display };
|
|
24
|
+
},
|
|
25
|
+
async cancel(options: { notifications: Array<{ id: number }> }) {
|
|
26
|
+
cancelled.push(...options.notifications.map((n) => n.id));
|
|
27
|
+
},
|
|
28
|
+
async getPending() {
|
|
29
|
+
return { notifications: scheduled };
|
|
30
|
+
},
|
|
31
|
+
async addListener(
|
|
32
|
+
_eventName: string,
|
|
33
|
+
listener: (action: { actionId: string; notification: { id: number } }) => void,
|
|
34
|
+
) {
|
|
35
|
+
actionListeners.push(listener);
|
|
36
|
+
return { async remove() {} };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} as unknown as CapacitorApi;
|
|
40
|
+
return {
|
|
41
|
+
capacitor,
|
|
42
|
+
scheduled,
|
|
43
|
+
cancelled,
|
|
44
|
+
fire: (a: { actionId: string; notification: { id: number } }) => actionListeners.forEach((l) => l(a)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('createCapacitorNotificationBackend', () => {
|
|
49
|
+
it('schedules an immediate notification and returns the caller id', async () => {
|
|
50
|
+
const { capacitor, scheduled } = fakeCapacitor();
|
|
51
|
+
const backend = createCapacitorNotificationBackend(capacitor);
|
|
52
|
+
expect(await backend.notify({ id: 'welcome', title: 'Hi', body: 'there' })).toBe('welcome');
|
|
53
|
+
expect(scheduled).toHaveLength(1);
|
|
54
|
+
expect(scheduled[0].title).toBe('Hi');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('serves permission from the prefetch cache and updates it on request', async () => {
|
|
58
|
+
const backend = createCapacitorNotificationBackend(fakeCapacitor('granted').capacitor);
|
|
59
|
+
// Reads 'default' until the construction-time prefetch settles.
|
|
60
|
+
expect(backend.getPermission()).toBe('default');
|
|
61
|
+
await flush();
|
|
62
|
+
expect(backend.getPermission()).toBe('granted');
|
|
63
|
+
expect(await backend.requestPermission()).toBe('granted');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('cancels a scheduled notification by its caller id', async () => {
|
|
67
|
+
const { capacitor, cancelled } = fakeCapacitor();
|
|
68
|
+
const backend = createCapacitorNotificationBackend(capacitor);
|
|
69
|
+
await backend.scheduleNotification({ id: 'later', title: 'Later' }, { at: Date.now() + 1000 });
|
|
70
|
+
backend.cancelScheduledNotification('later');
|
|
71
|
+
await flush();
|
|
72
|
+
expect(cancelled).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('routes an action-performed event to click and action subscribers', async () => {
|
|
76
|
+
const { capacitor, fire } = fakeCapacitor();
|
|
77
|
+
const backend = createCapacitorNotificationBackend(capacitor);
|
|
78
|
+
let clicked = '';
|
|
79
|
+
let action = '';
|
|
80
|
+
backend.subscribeClick((id) => (clicked = id));
|
|
81
|
+
backend.subscribeAction((id, actionId) => (action = `${id}:${actionId}`));
|
|
82
|
+
await backend.notify({ id: 'welcome', title: 'Hi' });
|
|
83
|
+
await flush();
|
|
84
|
+
fire({ actionId: 'tap', notification: { id: 1 } });
|
|
85
|
+
expect(clicked).toBe('welcome');
|
|
86
|
+
expect(action).toBe('welcome:tap');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reports sentinels for the unmodeled surface', async () => {
|
|
90
|
+
const backend = createCapacitorNotificationBackend(fakeCapacitor().capacitor);
|
|
91
|
+
expect(await backend.getActiveNotifications()).toEqual([]);
|
|
92
|
+
expect(await backend.updateNotification('x', {})).toBe(false);
|
|
93
|
+
expect(await backend.getLaunchNotification()).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getAppBackend, setAppBackend } from '@flighthq/app';
|
|
2
|
+
import { readClipboardText, setClipboardBackend } from '@flighthq/clipboard';
|
|
3
|
+
import { getConnectivityBackend, setConnectivityBackend } from '@flighthq/connectivity';
|
|
4
|
+
import { getDeviceBackend, setDeviceBackend } from '@flighthq/device';
|
|
5
|
+
import { setDialogBackend } from '@flighthq/dialog';
|
|
6
|
+
import { getFileSystemBackend, setFileSystemBackend } from '@flighthq/filesystem';
|
|
7
|
+
import { getGeolocationBackend, setGeolocationBackend } from '@flighthq/geolocation';
|
|
8
|
+
import { getHapticsBackend, setHapticsBackend } from '@flighthq/haptics';
|
|
9
|
+
import { getSoftKeyboardBackend, setSoftKeyboardBackend } from '@flighthq/keyboard';
|
|
10
|
+
import { getNotificationBackend, setNotificationBackend } from '@flighthq/notification';
|
|
11
|
+
import { getShareBackend, setShareBackend } from '@flighthq/share';
|
|
12
|
+
import { getStatusBarBackend, setStatusBarBackend } from '@flighthq/statusbar';
|
|
13
|
+
|
|
14
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
15
|
+
import { registerCapacitorBackends } from './capacitorRegister';
|
|
16
|
+
|
|
17
|
+
// A fake Capacitor API broad enough that every createCapacitor*Backend constructs without touching
|
|
18
|
+
// missing members. Backends close over `capacitor` and only call in when their methods run (plus the
|
|
19
|
+
// app/notification/device/share/statusbar/connectivity prefetches), so a thin fake proves registration
|
|
20
|
+
// routes the seams to the Capacitor backends.
|
|
21
|
+
function fakeCapacitor(): CapacitorApi {
|
|
22
|
+
const asyncNoop = async () => {};
|
|
23
|
+
const asyncListener = async () => ({ async remove() {} });
|
|
24
|
+
return {
|
|
25
|
+
app: {
|
|
26
|
+
getInfo: async () => ({ name: 'FlightApp', id: 'com.flight.app', build: '1', version: '1.0.0' }),
|
|
27
|
+
exitApp: asyncNoop,
|
|
28
|
+
minimizeApp: asyncNoop,
|
|
29
|
+
addListener: asyncListener,
|
|
30
|
+
},
|
|
31
|
+
clipboard: {
|
|
32
|
+
read: async () => ({ value: 'CAP-TEXT', type: 'text/plain' }),
|
|
33
|
+
write: asyncNoop,
|
|
34
|
+
},
|
|
35
|
+
device: {
|
|
36
|
+
getInfo: async () => ({
|
|
37
|
+
model: 'M',
|
|
38
|
+
platform: 'ios',
|
|
39
|
+
operatingSystem: 'ios',
|
|
40
|
+
osVersion: '17',
|
|
41
|
+
manufacturer: 'Apple',
|
|
42
|
+
isVirtual: false,
|
|
43
|
+
webViewVersion: '17',
|
|
44
|
+
}),
|
|
45
|
+
getId: async () => ({ identifier: 'id' }),
|
|
46
|
+
},
|
|
47
|
+
dialog: {
|
|
48
|
+
alert: asyncNoop,
|
|
49
|
+
confirm: async () => ({ value: true }),
|
|
50
|
+
prompt: async () => ({ value: '', cancelled: true }),
|
|
51
|
+
},
|
|
52
|
+
filesystem: {},
|
|
53
|
+
geolocation: { checkPermissions: async () => ({ location: 'granted' }) },
|
|
54
|
+
haptics: {},
|
|
55
|
+
keyboard: { addListener: asyncListener },
|
|
56
|
+
localNotifications: {
|
|
57
|
+
schedule: async () => ({ notifications: [] }),
|
|
58
|
+
requestPermissions: async () => ({ display: 'granted' }),
|
|
59
|
+
checkPermissions: async () => ({ display: 'granted' }),
|
|
60
|
+
cancel: asyncNoop,
|
|
61
|
+
getPending: async () => ({ notifications: [] }),
|
|
62
|
+
addListener: asyncListener,
|
|
63
|
+
},
|
|
64
|
+
network: { getStatus: async () => ({ connected: true, connectionType: 'wifi' }), addListener: asyncListener },
|
|
65
|
+
share: { canShare: async () => ({ value: true }), share: async () => ({}) },
|
|
66
|
+
statusBar: { getInfo: async () => ({ visible: true, style: 'Default' }) },
|
|
67
|
+
} as unknown as CapacitorApi;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
setAppBackend(null);
|
|
72
|
+
setClipboardBackend(null);
|
|
73
|
+
setConnectivityBackend(null);
|
|
74
|
+
setDeviceBackend(null);
|
|
75
|
+
setDialogBackend(null);
|
|
76
|
+
setFileSystemBackend(null);
|
|
77
|
+
setGeolocationBackend(null);
|
|
78
|
+
setHapticsBackend(null);
|
|
79
|
+
setNotificationBackend(null);
|
|
80
|
+
setShareBackend(null);
|
|
81
|
+
setSoftKeyboardBackend(null);
|
|
82
|
+
setStatusBarBackend(null);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('registerCapacitorBackends', () => {
|
|
86
|
+
it('installs a backend for each covered capability', () => {
|
|
87
|
+
registerCapacitorBackends(fakeCapacitor());
|
|
88
|
+
expect(getAppBackend()).not.toBeNull();
|
|
89
|
+
expect(getConnectivityBackend()).not.toBeNull();
|
|
90
|
+
expect(getDeviceBackend()).not.toBeNull();
|
|
91
|
+
expect(getFileSystemBackend()).not.toBeNull();
|
|
92
|
+
expect(getGeolocationBackend()).not.toBeNull();
|
|
93
|
+
expect(getHapticsBackend()).not.toBeNull();
|
|
94
|
+
expect(getNotificationBackend()).not.toBeNull();
|
|
95
|
+
expect(getShareBackend()).not.toBeNull();
|
|
96
|
+
expect(getSoftKeyboardBackend()).not.toBeNull();
|
|
97
|
+
expect(getStatusBarBackend()).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('routes a capability call through to the Capacitor backend', async () => {
|
|
101
|
+
registerCapacitorBackends(fakeCapacitor());
|
|
102
|
+
expect(await readClipboardText()).toBe('CAP-TEXT');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
2
|
+
import { createCapacitorShareBackend } from './capacitorShare';
|
|
3
|
+
|
|
4
|
+
const flush = async () => {
|
|
5
|
+
await Promise.resolve();
|
|
6
|
+
await Promise.resolve();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function fakeCapacitor(canShare = true, shareImpl?: () => Promise<{ activityType?: string }>) {
|
|
10
|
+
const shared: Array<{ title?: string; text?: string; url?: string }> = [];
|
|
11
|
+
const capacitor = {
|
|
12
|
+
share: {
|
|
13
|
+
async canShare() {
|
|
14
|
+
return { value: canShare };
|
|
15
|
+
},
|
|
16
|
+
async share(options: { title?: string; text?: string; url?: string }) {
|
|
17
|
+
shared.push(options);
|
|
18
|
+
return shareImpl ? await shareImpl() : { activityType: 'com.apple.UIKit.activity.Mail' };
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
} as unknown as CapacitorApi;
|
|
22
|
+
return { capacitor, shared };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('createCapacitorShareBackend', () => {
|
|
26
|
+
it('reports availability from the prefetch cache once it resolves', async () => {
|
|
27
|
+
const backend = createCapacitorShareBackend(fakeCapacitor(true).capacitor);
|
|
28
|
+
// Reads false until the construction-time canShare prefetch settles.
|
|
29
|
+
expect(backend.isAvailable()).toBe(false);
|
|
30
|
+
await flush();
|
|
31
|
+
expect(backend.isAvailable()).toBe(true);
|
|
32
|
+
expect(backend.canShare({ text: 'x' })).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shares title/text/url content', async () => {
|
|
36
|
+
const { capacitor, shared } = fakeCapacitor();
|
|
37
|
+
const backend = createCapacitorShareBackend(capacitor);
|
|
38
|
+
expect(await backend.share({ title: 'T', url: 'https://flight.dev' })).toBe(true);
|
|
39
|
+
expect(shared[0]).toMatchObject({ title: 'T', url: 'https://flight.dev' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('maps a completed share to a ShareResult with the activity type', async () => {
|
|
43
|
+
const backend = createCapacitorShareBackend(fakeCapacitor().capacitor);
|
|
44
|
+
expect(await backend.shareWithResult({ text: 'x' })).toEqual({
|
|
45
|
+
completed: true,
|
|
46
|
+
activityType: 'com.apple.UIKit.activity.Mail',
|
|
47
|
+
dismissed: false,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('reports a dismissed ShareResult when the user cancels', async () => {
|
|
52
|
+
const backend = createCapacitorShareBackend(
|
|
53
|
+
fakeCapacitor(true, async () => {
|
|
54
|
+
throw new Error('cancelled');
|
|
55
|
+
}).capacitor,
|
|
56
|
+
);
|
|
57
|
+
expect(await backend.shareWithResult({ text: 'x' })).toEqual({
|
|
58
|
+
completed: false,
|
|
59
|
+
activityType: null,
|
|
60
|
+
dismissed: true,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('refuses content with no shareable text', async () => {
|
|
65
|
+
const backend = createCapacitorShareBackend(fakeCapacitor().capacitor);
|
|
66
|
+
expect(await backend.share({})).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StatusBarInfo } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import type { CapacitorApi } from './capacitorModule';
|
|
4
|
+
import { createCapacitorStatusBarBackend } from './capacitorStatusBar';
|
|
5
|
+
|
|
6
|
+
const flush = async () => {
|
|
7
|
+
await Promise.resolve();
|
|
8
|
+
await Promise.resolve();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function fakeCapacitor(info = { visible: true, style: 'Dark', color: '#112233', overlays: true }) {
|
|
12
|
+
const calls: Array<{ method: string; arg?: unknown }> = [];
|
|
13
|
+
const capacitor = {
|
|
14
|
+
statusBar: {
|
|
15
|
+
async getInfo() {
|
|
16
|
+
return info;
|
|
17
|
+
},
|
|
18
|
+
async setStyle(arg: unknown) {
|
|
19
|
+
calls.push({ method: 'setStyle', arg });
|
|
20
|
+
},
|
|
21
|
+
async setBackgroundColor(arg: unknown) {
|
|
22
|
+
calls.push({ method: 'setBackgroundColor', arg });
|
|
23
|
+
},
|
|
24
|
+
async setOverlaysWebView(arg: unknown) {
|
|
25
|
+
calls.push({ method: 'setOverlaysWebView', arg });
|
|
26
|
+
},
|
|
27
|
+
async show() {
|
|
28
|
+
calls.push({ method: 'show' });
|
|
29
|
+
},
|
|
30
|
+
async hide() {
|
|
31
|
+
calls.push({ method: 'hide' });
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
} as unknown as CapacitorApi;
|
|
35
|
+
return { capacitor, calls };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function blankInfo(): StatusBarInfo {
|
|
39
|
+
return { color: 1, height: 1, overlaysContent: false, style: 'default', visible: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('createCapacitorStatusBarBackend', () => {
|
|
43
|
+
it('maps setters onto the Capacitor plugin', () => {
|
|
44
|
+
const { capacitor, calls } = fakeCapacitor();
|
|
45
|
+
const backend = createCapacitorStatusBarBackend(capacitor);
|
|
46
|
+
backend.setStyle('light');
|
|
47
|
+
backend.setBackgroundColor(0x112233ff);
|
|
48
|
+
backend.setOverlaysContent(true);
|
|
49
|
+
backend.setVisible(false);
|
|
50
|
+
expect(calls[0].arg).toEqual({ style: 'Light' });
|
|
51
|
+
expect(calls[1].arg).toEqual({ color: '#112233' });
|
|
52
|
+
expect(calls[2].arg).toEqual({ overlay: true });
|
|
53
|
+
expect(calls[3].method).toBe('hide');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('fills the info snapshot from the prefetch once it resolves', async () => {
|
|
57
|
+
const backend = createCapacitorStatusBarBackend(fakeCapacitor().capacitor);
|
|
58
|
+
await flush();
|
|
59
|
+
const info = backend.getInfo(blankInfo());
|
|
60
|
+
expect(info.visible).toBe(true);
|
|
61
|
+
expect(info.style).toBe('dark');
|
|
62
|
+
expect(info.color).toBe(0x112233ff);
|
|
63
|
+
expect(info.overlaysContent).toBe(true);
|
|
64
|
+
expect(info.height).toBe(-1);
|
|
65
|
+
});
|
|
66
|
+
});
|