@flighthq/host-tauri 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/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/tauriApp.d.ts +4 -0
- package/dist/tauriApp.d.ts.map +1 -0
- package/dist/tauriApp.js +159 -0
- package/dist/tauriApp.js.map +1 -0
- package/dist/tauriClipboard.d.ts +4 -0
- package/dist/tauriClipboard.d.ts.map +1 -0
- package/dist/tauriClipboard.js +108 -0
- package/dist/tauriClipboard.js.map +1 -0
- package/dist/tauriDialog.d.ts +4 -0
- package/dist/tauriDialog.d.ts.map +1 -0
- package/dist/tauriDialog.js +83 -0
- package/dist/tauriDialog.js.map +1 -0
- package/dist/tauriMenu.d.ts +4 -0
- package/dist/tauriMenu.d.ts.map +1 -0
- package/dist/tauriMenu.js +68 -0
- package/dist/tauriMenu.js.map +1 -0
- package/dist/tauriModule.d.ts +213 -0
- package/dist/tauriModule.d.ts.map +1 -0
- package/dist/tauriModule.js +25 -0
- package/dist/tauriModule.js.map +1 -0
- package/dist/tauriNotification.d.ts +4 -0
- package/dist/tauriNotification.d.ts.map +1 -0
- package/dist/tauriNotification.js +102 -0
- package/dist/tauriNotification.js.map +1 -0
- package/dist/tauriPlatform.d.ts +4 -0
- package/dist/tauriPlatform.d.ts.map +1 -0
- package/dist/tauriPlatform.js +33 -0
- package/dist/tauriPlatform.js.map +1 -0
- package/dist/tauriRegister.d.ts +3 -0
- package/dist/tauriRegister.d.ts.map +1 -0
- package/dist/tauriRegister.js +44 -0
- package/dist/tauriRegister.js.map +1 -0
- package/dist/tauriShell.d.ts +4 -0
- package/dist/tauriShell.d.ts.map +1 -0
- package/dist/tauriShell.js +67 -0
- package/dist/tauriShell.js.map +1 -0
- package/dist/tauriShortcut.d.ts +4 -0
- package/dist/tauriShortcut.d.ts.map +1 -0
- package/dist/tauriShortcut.js +53 -0
- package/dist/tauriShortcut.js.map +1 -0
- package/dist/tauriTray.d.ts +4 -0
- package/dist/tauriTray.d.ts.map +1 -0
- package/dist/tauriTray.js +167 -0
- package/dist/tauriTray.js.map +1 -0
- package/dist/tauriWindow.d.ts +4 -0
- package/dist/tauriWindow.d.ts.map +1 -0
- package/dist/tauriWindow.js +166 -0
- package/dist/tauriWindow.js.map +1 -0
- package/package.json +48 -0
- package/src/tauriApp.test.ts +80 -0
- package/src/tauriClipboard.test.ts +69 -0
- package/src/tauriDialog.test.ts +83 -0
- package/src/tauriMenu.test.ts +129 -0
- package/src/tauriNotification.test.ts +59 -0
- package/src/tauriPlatform.test.ts +56 -0
- package/src/tauriRegister.test.ts +67 -0
- package/src/tauriShell.test.ts +63 -0
- package/src/tauriShortcut.test.ts +77 -0
- package/src/tauriTray.test.ts +123 -0
- package/src/tauriWindow.test.ts +144 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { MenuItemTemplate } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createTauriMenuBackend } from './tauriMenu';
|
|
4
|
+
import type { TauriApi, TauriMenuItemOptions } from './tauriModule';
|
|
5
|
+
|
|
6
|
+
function fakeTauri() {
|
|
7
|
+
const state = {
|
|
8
|
+
appMenuSet: 0,
|
|
9
|
+
popups: 0,
|
|
10
|
+
// Action callbacks captured from built MenuItems, keyed by their id.
|
|
11
|
+
actions: new Map<string, (id: string) => void>(),
|
|
12
|
+
predefined: 0,
|
|
13
|
+
};
|
|
14
|
+
const makeMenu = () => ({
|
|
15
|
+
async setAsAppMenu() {
|
|
16
|
+
state.appMenuSet++;
|
|
17
|
+
},
|
|
18
|
+
async popup() {
|
|
19
|
+
state.popups++;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const tauri = {
|
|
23
|
+
menu: {
|
|
24
|
+
Menu: {
|
|
25
|
+
async new() {
|
|
26
|
+
return makeMenu();
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
MenuItem: {
|
|
30
|
+
async new(options?: TauriMenuItemOptions) {
|
|
31
|
+
if (options?.id && options.action) state.actions.set(options.id, options.action);
|
|
32
|
+
return { id: options?.id ?? '' };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
Submenu: {
|
|
36
|
+
async new() {
|
|
37
|
+
return { id: 'submenu' };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
PredefinedMenuItem: {
|
|
41
|
+
async new() {
|
|
42
|
+
state.predefined++;
|
|
43
|
+
return { id: 'separator' };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
window: {
|
|
48
|
+
LogicalPosition: class {
|
|
49
|
+
constructor(
|
|
50
|
+
public x: number,
|
|
51
|
+
public y: number,
|
|
52
|
+
) {}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
} as unknown as TauriApi;
|
|
56
|
+
return { tauri, state };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const template: MenuItemTemplate[] = [
|
|
60
|
+
{ id: 'open', label: 'Open' },
|
|
61
|
+
{ type: 'separator' },
|
|
62
|
+
{ label: 'More', submenu: [{ id: 'nested', label: 'Nested' }] },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
describe('createTauriMenuBackend', () => {
|
|
66
|
+
it('installs an application menu and routes item clicks to the select listener', async () => {
|
|
67
|
+
const { tauri, state } = fakeTauri();
|
|
68
|
+
const backend = createTauriMenuBackend(tauri);
|
|
69
|
+
const selected: string[] = [];
|
|
70
|
+
backend.subscribeSelect((id) => selected.push(id));
|
|
71
|
+
expect(backend.setApplicationMenu(template)).toBe(true);
|
|
72
|
+
// Let the async build + setAsAppMenu settle.
|
|
73
|
+
await flush();
|
|
74
|
+
expect(state.appMenuSet).toBe(1);
|
|
75
|
+
expect(state.predefined).toBe(1);
|
|
76
|
+
state.actions.get('open')!('open');
|
|
77
|
+
expect(selected).toEqual(['open']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('resolves popupContextMenu with the clicked item id', async () => {
|
|
81
|
+
const { tauri, state } = fakeTauri();
|
|
82
|
+
const backend = createTauriMenuBackend(tauri);
|
|
83
|
+
const pending = backend.popupContextMenu([{ id: 'cut', label: 'Cut' }], 10, 20);
|
|
84
|
+
await flush();
|
|
85
|
+
expect(state.popups).toBe(1);
|
|
86
|
+
state.actions.get('cut')!('cut');
|
|
87
|
+
expect(await pending).toBe('cut');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('resolves popupContextMenu null when the build throws', async () => {
|
|
91
|
+
const tauri = {
|
|
92
|
+
menu: {
|
|
93
|
+
Menu: {
|
|
94
|
+
async new() {
|
|
95
|
+
throw new Error('no menu');
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
MenuItem: {
|
|
99
|
+
async new() {
|
|
100
|
+
return { id: 'x' };
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
Submenu: {
|
|
104
|
+
async new() {
|
|
105
|
+
return { id: 's' };
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
PredefinedMenuItem: {
|
|
109
|
+
async new() {
|
|
110
|
+
return { id: 'sep' };
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
window: {
|
|
115
|
+
LogicalPosition: class {
|
|
116
|
+
constructor(
|
|
117
|
+
public x: number,
|
|
118
|
+
public y: number,
|
|
119
|
+
) {}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
} as unknown as TauriApi;
|
|
123
|
+
expect(await createTauriMenuBackend(tauri).popupContextMenu([{ id: 'a', label: 'A' }], 0, 0)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
async function flush(): Promise<void> {
|
|
128
|
+
for (let i = 0; i < 24; i++) await Promise.resolve();
|
|
129
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { TauriApi, TauriNotificationOptions, TauriNotificationPermission } from './tauriModule';
|
|
2
|
+
import { createTauriNotificationBackend } from './tauriNotification';
|
|
3
|
+
|
|
4
|
+
function fakeTauri(granted = true, permission: TauriNotificationPermission = 'granted') {
|
|
5
|
+
const sent: TauriNotificationOptions[] = [];
|
|
6
|
+
const tauri = {
|
|
7
|
+
notification: {
|
|
8
|
+
async isPermissionGranted() {
|
|
9
|
+
return granted;
|
|
10
|
+
},
|
|
11
|
+
async requestPermission() {
|
|
12
|
+
return permission;
|
|
13
|
+
},
|
|
14
|
+
sendNotification(options: TauriNotificationOptions) {
|
|
15
|
+
sent.push(options);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} as unknown as TauriApi;
|
|
19
|
+
return { tauri, sent };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('createTauriNotificationBackend', () => {
|
|
23
|
+
it('sends a notification and resolves the request id', async () => {
|
|
24
|
+
const { tauri, sent } = fakeTauri();
|
|
25
|
+
const backend = createTauriNotificationBackend(tauri);
|
|
26
|
+
const id = await backend.notify({ title: 'Hi', body: 'there', id: 'n1' });
|
|
27
|
+
expect(id).toBe('n1');
|
|
28
|
+
expect(sent[0]).toEqual({ title: 'Hi', body: 'there', icon: undefined });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('generates an id when the request omits one', async () => {
|
|
32
|
+
const backend = createTauriNotificationBackend(fakeTauri().tauri);
|
|
33
|
+
expect(await backend.notify({ title: 'Hi' })).toMatch(/^notification-/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('maps requestPermission and then reads it back synchronously', async () => {
|
|
37
|
+
const backend = createTauriNotificationBackend(fakeTauri(false, 'granted').tauri);
|
|
38
|
+
expect(await backend.requestPermission()).toBe('granted');
|
|
39
|
+
expect(backend.getPermission()).toBe('granted');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reflects the prefetched permission after it resolves', async () => {
|
|
43
|
+
const backend = createTauriNotificationBackend(fakeTauri(true).tauri);
|
|
44
|
+
// Let the construction-time isPermissionGranted() prefetch settle.
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
expect(backend.getPermission()).toBe('granted');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('reports conservative capabilities and unsupported-surface sentinels', async () => {
|
|
51
|
+
const backend = createTauriNotificationBackend(fakeTauri().tauri);
|
|
52
|
+
expect(backend.isSupported()).toBe(true);
|
|
53
|
+
expect(backend.getCapabilities().scheduling).toBe(false);
|
|
54
|
+
expect(await backend.getActiveNotifications()).toEqual([]);
|
|
55
|
+
expect(await backend.scheduleNotification({ title: 'x' }, { at: 0 })).toBe('');
|
|
56
|
+
expect(await backend.updateNotification('n1', {})).toBe(false);
|
|
57
|
+
expect(typeof backend.subscribeClick(() => {})).toBe('function');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PlatformInfo } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import type { TauriApi } from './tauriModule';
|
|
4
|
+
import { createTauriPlatformBackend } from './tauriPlatform';
|
|
5
|
+
|
|
6
|
+
function fakeTauri(platform: string): TauriApi {
|
|
7
|
+
return {
|
|
8
|
+
os: {
|
|
9
|
+
arch: () => 'aarch64',
|
|
10
|
+
locale: () => 'en-US',
|
|
11
|
+
platform: () => platform,
|
|
12
|
+
version: () => '14.2.1',
|
|
13
|
+
},
|
|
14
|
+
} as unknown as TauriApi;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('createTauriPlatformBackend', () => {
|
|
18
|
+
it('fills platform info from the os plugin', () => {
|
|
19
|
+
const backend = createTauriPlatformBackend(fakeTauri('macos'));
|
|
20
|
+
const out = {} as PlatformInfo;
|
|
21
|
+
const result = backend.getInfo(out);
|
|
22
|
+
expect(result).toBe(out);
|
|
23
|
+
expect(out.name).toBe('macos');
|
|
24
|
+
expect(out.kind).toBe('desktop');
|
|
25
|
+
expect(out.version).toBe('14.2.1');
|
|
26
|
+
expect(out.arch).toBe('aarch64');
|
|
27
|
+
expect(out.locale).toBe('en-US');
|
|
28
|
+
expect(out.isTouch).toBe(false);
|
|
29
|
+
expect(out.runtime).toBe('tauri');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('maps each known Tauri platform string to a PlatformName', () => {
|
|
33
|
+
const cases: Record<string, string> = {
|
|
34
|
+
windows: 'windows',
|
|
35
|
+
macos: 'macos',
|
|
36
|
+
linux: 'linux',
|
|
37
|
+
ios: 'ios',
|
|
38
|
+
android: 'android',
|
|
39
|
+
freebsd: 'unknown',
|
|
40
|
+
};
|
|
41
|
+
for (const [tauriName, expected] of Object.entries(cases)) {
|
|
42
|
+
const out = {} as PlatformInfo;
|
|
43
|
+
createTauriPlatformBackend(fakeTauri(tauriName)).getInfo(out);
|
|
44
|
+
expect(out.name).toBe(expected);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to empty locale when the os plugin reports null', () => {
|
|
49
|
+
const tauri = {
|
|
50
|
+
os: { arch: () => '', locale: () => null, platform: () => 'linux', version: () => '' },
|
|
51
|
+
} as unknown as TauriApi;
|
|
52
|
+
const out = {} as PlatformInfo;
|
|
53
|
+
createTauriPlatformBackend(tauri).getInfo(out);
|
|
54
|
+
expect(out.locale).toBe('');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { setAppBackend } from '@flighthq/app';
|
|
2
|
+
import { setWindowBackend } from '@flighthq/application';
|
|
3
|
+
import { readClipboardText, setClipboardBackend } from '@flighthq/clipboard';
|
|
4
|
+
import { setDialogBackend } from '@flighthq/dialog';
|
|
5
|
+
import { setMenuBackend } from '@flighthq/menu';
|
|
6
|
+
import { setNotificationBackend } from '@flighthq/notification';
|
|
7
|
+
import { getPlatformName, setPlatformBackend } from '@flighthq/platform';
|
|
8
|
+
import { setShellBackend } from '@flighthq/shell';
|
|
9
|
+
import { setShortcutBackend } from '@flighthq/shortcut';
|
|
10
|
+
import { setTrayBackend } from '@flighthq/tray';
|
|
11
|
+
|
|
12
|
+
import type { TauriApi } from './tauriModule';
|
|
13
|
+
import { registerTauriBackends } from './tauriRegister';
|
|
14
|
+
|
|
15
|
+
// A fake Tauri API broad enough that every createTauri*Backend constructs without touching missing
|
|
16
|
+
// members. Backends close over `tauri` and only call in when their methods run (plus the app/
|
|
17
|
+
// notification prefetches), so a thin fake proves registration routes the seams to the Tauri backends.
|
|
18
|
+
function fakeTauri(): TauriApi {
|
|
19
|
+
const asyncNoop = async () => {};
|
|
20
|
+
return {
|
|
21
|
+
app: {
|
|
22
|
+
getName: async () => 'FlightApp',
|
|
23
|
+
getVersion: async () => '1.0.0',
|
|
24
|
+
hide: asyncNoop,
|
|
25
|
+
show: asyncNoop,
|
|
26
|
+
},
|
|
27
|
+
clipboard: {
|
|
28
|
+
readText: async () => 'TAURI-TEXT',
|
|
29
|
+
writeText: asyncNoop,
|
|
30
|
+
clear: asyncNoop,
|
|
31
|
+
},
|
|
32
|
+
dialog: {},
|
|
33
|
+
globalShortcut: {},
|
|
34
|
+
menu: {},
|
|
35
|
+
notification: {
|
|
36
|
+
isPermissionGranted: async () => true,
|
|
37
|
+
requestPermission: async () => 'granted',
|
|
38
|
+
sendNotification: () => {},
|
|
39
|
+
},
|
|
40
|
+
opener: {},
|
|
41
|
+
os: { arch: () => 'x86_64', locale: () => 'en-US', platform: () => 'linux', version: () => '' },
|
|
42
|
+
process: {},
|
|
43
|
+
tray: {},
|
|
44
|
+
window: { getCurrentWindow: () => ({}), LogicalPosition: class {}, LogicalSize: class {} },
|
|
45
|
+
} as unknown as TauriApi;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
setPlatformBackend(null);
|
|
50
|
+
setAppBackend(null);
|
|
51
|
+
setWindowBackend(null);
|
|
52
|
+
setDialogBackend(null);
|
|
53
|
+
setClipboardBackend(null);
|
|
54
|
+
setMenuBackend(null);
|
|
55
|
+
setTrayBackend(null);
|
|
56
|
+
setShortcutBackend(null);
|
|
57
|
+
setNotificationBackend(null);
|
|
58
|
+
setShellBackend(null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('registerTauriBackends', () => {
|
|
62
|
+
it('routes capability seams to the Tauri backends without throwing', async () => {
|
|
63
|
+
registerTauriBackends(fakeTauri());
|
|
64
|
+
expect(getPlatformName()).toBe('linux');
|
|
65
|
+
expect(await readClipboardText()).toBe('TAURI-TEXT');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { TauriApi } from './tauriModule';
|
|
2
|
+
import { createTauriShellBackend } from './tauriShell';
|
|
3
|
+
|
|
4
|
+
function fakeTauri(reject = false) {
|
|
5
|
+
const calls: { openUrl: string[]; openPath: string[]; reveal: string[] } = {
|
|
6
|
+
openUrl: [],
|
|
7
|
+
openPath: [],
|
|
8
|
+
reveal: [],
|
|
9
|
+
};
|
|
10
|
+
const guard = async () => {
|
|
11
|
+
if (reject) throw new Error('boom');
|
|
12
|
+
};
|
|
13
|
+
const tauri = {
|
|
14
|
+
opener: {
|
|
15
|
+
async openUrl(url: string) {
|
|
16
|
+
calls.openUrl.push(url);
|
|
17
|
+
await guard();
|
|
18
|
+
},
|
|
19
|
+
async openPath(path: string) {
|
|
20
|
+
calls.openPath.push(path);
|
|
21
|
+
await guard();
|
|
22
|
+
},
|
|
23
|
+
async revealItemInDir(path: string) {
|
|
24
|
+
calls.reveal.push(path);
|
|
25
|
+
await guard();
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
} as unknown as TauriApi;
|
|
29
|
+
return { tauri, calls };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('createTauriShellBackend', () => {
|
|
33
|
+
it('opens URLs and paths through the opener plugin', async () => {
|
|
34
|
+
const { tauri, calls } = fakeTauri();
|
|
35
|
+
const backend = createTauriShellBackend(tauri);
|
|
36
|
+
expect(await backend.openExternal('https://x.test')).toBe(true);
|
|
37
|
+
expect(await backend.openPath('/tmp/a')).toBe(true);
|
|
38
|
+
expect(await backend.showItemInFolder('/tmp/a')).toBe(true);
|
|
39
|
+
expect(calls.openUrl).toEqual(['https://x.test']);
|
|
40
|
+
expect(calls.openPath).toEqual(['/tmp/a']);
|
|
41
|
+
expect(calls.reveal).toEqual(['/tmp/a']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('reports the error message from openPathResult', async () => {
|
|
45
|
+
expect(await createTauriShellBackend(fakeTauri().tauri).openPathResult('/tmp/a')).toBe('');
|
|
46
|
+
expect(await createTauriShellBackend(fakeTauri(true).tauri).openPathResult('/tmp/a')).toBe('boom');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('resolves false when the opener rejects', async () => {
|
|
50
|
+
const backend = createTauriShellBackend(fakeTauri(true).tauri);
|
|
51
|
+
expect(await backend.openExternal('https://x.test')).toBe(false);
|
|
52
|
+
expect(await backend.showItemInFolder('/tmp/a')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('reports sentinels for trash, shortcut links, and beep', async () => {
|
|
56
|
+
const backend = createTauriShellBackend(fakeTauri().tauri);
|
|
57
|
+
expect(await backend.moveToTrash('/tmp/a')).toBe(false);
|
|
58
|
+
expect(await backend.moveItemsToTrash(['/a', '/b'])).toEqual([false, false]);
|
|
59
|
+
expect(await backend.readShortcutLink('/a.lnk')).toBeNull();
|
|
60
|
+
expect(await backend.writeShortcutLink('/a.lnk', { target: '/t' })).toBe(false);
|
|
61
|
+
expect(() => backend.beep()).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TauriApi, TauriShortcutEvent } from './tauriModule';
|
|
2
|
+
import { createTauriShortcutBackend } from './tauriShortcut';
|
|
3
|
+
|
|
4
|
+
function fakeTauri() {
|
|
5
|
+
const handlers = new Map<string, (event: Readonly<TauriShortcutEvent>) => void>();
|
|
6
|
+
const calls: { register: string[]; unregister: string[]; unregisterAll: number } = {
|
|
7
|
+
register: [],
|
|
8
|
+
unregister: [],
|
|
9
|
+
unregisterAll: 0,
|
|
10
|
+
};
|
|
11
|
+
const tauri = {
|
|
12
|
+
globalShortcut: {
|
|
13
|
+
async register(shortcut: string, handler: (event: Readonly<TauriShortcutEvent>) => void) {
|
|
14
|
+
calls.register.push(shortcut);
|
|
15
|
+
handlers.set(shortcut, handler);
|
|
16
|
+
},
|
|
17
|
+
async unregister(shortcut: string) {
|
|
18
|
+
calls.unregister.push(shortcut);
|
|
19
|
+
handlers.delete(shortcut);
|
|
20
|
+
},
|
|
21
|
+
async unregisterAll() {
|
|
22
|
+
calls.unregisterAll++;
|
|
23
|
+
handlers.clear();
|
|
24
|
+
},
|
|
25
|
+
async isRegistered(shortcut: string) {
|
|
26
|
+
return handlers.has(shortcut);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} as unknown as TauriApi;
|
|
30
|
+
return { tauri, handlers, calls };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('createTauriShortcutBackend', () => {
|
|
34
|
+
it('optimistically mirrors registrations synchronously', () => {
|
|
35
|
+
const { tauri, calls } = fakeTauri();
|
|
36
|
+
const backend = createTauriShortcutBackend(tauri);
|
|
37
|
+
expect(backend.register('CmdOrCtrl+K', () => {})).toBe(true);
|
|
38
|
+
expect(backend.isRegistered('CmdOrCtrl+K')).toBe(true);
|
|
39
|
+
expect(backend.getRegistered()).toEqual(['CmdOrCtrl+K']);
|
|
40
|
+
expect(calls.register).toEqual(['CmdOrCtrl+K']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('delivers a ShortcutEvent only on the Pressed state', async () => {
|
|
44
|
+
const { tauri, handlers } = fakeTauri();
|
|
45
|
+
const backend = createTauriShortcutBackend(tauri);
|
|
46
|
+
let fired = 0;
|
|
47
|
+
let seen = '';
|
|
48
|
+
backend.register('Alt+P', (event) => {
|
|
49
|
+
fired++;
|
|
50
|
+
seen = event.accelerator;
|
|
51
|
+
});
|
|
52
|
+
await Promise.resolve();
|
|
53
|
+
const handler = handlers.get('Alt+P')!;
|
|
54
|
+
handler({ shortcut: 'Alt+P', state: 'Released' });
|
|
55
|
+
handler({ shortcut: 'Alt+P', state: 'Pressed' });
|
|
56
|
+
expect(fired).toBe(1);
|
|
57
|
+
expect(seen).toBe('Alt+P');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('unregisters individually and en masse', () => {
|
|
61
|
+
const { tauri, calls } = fakeTauri();
|
|
62
|
+
const backend = createTauriShortcutBackend(tauri);
|
|
63
|
+
backend.register('A', () => {});
|
|
64
|
+
backend.register('B', () => {});
|
|
65
|
+
expect(backend.unregister('A')).toBe(true);
|
|
66
|
+
expect(backend.isRegistered('A')).toBe(false);
|
|
67
|
+
backend.unregisterAll();
|
|
68
|
+
expect(backend.getRegistered()).toEqual([]);
|
|
69
|
+
expect(calls.unregisterAll).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('reports the enable-toggle sentinels', () => {
|
|
73
|
+
const backend = createTauriShortcutBackend(fakeTauri().tauri);
|
|
74
|
+
expect(backend.setEnabled('A', true)).toBe(false);
|
|
75
|
+
expect(() => backend.setAllEnabled(true)).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { TauriApi, TauriTrayIconEvent, TauriTrayIconOptions } from './tauriModule';
|
|
2
|
+
import { createTauriTrayBackend } from './tauriTray';
|
|
3
|
+
|
|
4
|
+
function fakeTauri() {
|
|
5
|
+
const state = {
|
|
6
|
+
created: 0,
|
|
7
|
+
closed: 0,
|
|
8
|
+
lastTitle: '' as string | null,
|
|
9
|
+
lastIcon: '' as string | null,
|
|
10
|
+
menusSet: 0,
|
|
11
|
+
// The action callback the most recently created tray registered.
|
|
12
|
+
action: null as ((event: Readonly<TauriTrayIconEvent>) => void) | null,
|
|
13
|
+
};
|
|
14
|
+
const tauri = {
|
|
15
|
+
tray: {
|
|
16
|
+
TrayIcon: {
|
|
17
|
+
async new(options?: TauriTrayIconOptions) {
|
|
18
|
+
state.created++;
|
|
19
|
+
state.action = options?.action ?? null;
|
|
20
|
+
return {
|
|
21
|
+
async setIcon(icon: string | null) {
|
|
22
|
+
state.lastIcon = icon;
|
|
23
|
+
},
|
|
24
|
+
async setTitle(title: string | null) {
|
|
25
|
+
state.lastTitle = title;
|
|
26
|
+
},
|
|
27
|
+
async setTooltip() {},
|
|
28
|
+
async setMenu() {
|
|
29
|
+
state.menusSet++;
|
|
30
|
+
},
|
|
31
|
+
async close() {
|
|
32
|
+
state.closed++;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
menu: {
|
|
39
|
+
Menu: {
|
|
40
|
+
async new() {
|
|
41
|
+
return { async setAsAppMenu() {}, async popup() {} };
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
MenuItem: {
|
|
45
|
+
async new(o?: { id?: string }) {
|
|
46
|
+
return { id: o?.id ?? '' };
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
Submenu: {
|
|
50
|
+
async new() {
|
|
51
|
+
return { id: 's' };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
PredefinedMenuItem: {
|
|
55
|
+
async new() {
|
|
56
|
+
return { id: 'sep' };
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
} as unknown as TauriApi;
|
|
61
|
+
return { tauri, state };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function flush(): Promise<void> {
|
|
65
|
+
for (let i = 0; i < 24; i++) await Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('createTauriTrayBackend', () => {
|
|
69
|
+
it('allocates a numeric id synchronously and builds the tray asynchronously', async () => {
|
|
70
|
+
const { tauri, state } = fakeTauri();
|
|
71
|
+
const backend = createTauriTrayBackend(tauri);
|
|
72
|
+
const id = backend.create({ title: 'Flight', tooltip: 'tip' });
|
|
73
|
+
expect(typeof id).toBe('number');
|
|
74
|
+
expect(backend.getTitle(id)).toBe('Flight');
|
|
75
|
+
expect(backend.getTooltip(id)).toBe('tip');
|
|
76
|
+
expect(backend.listIds()).toContain(id);
|
|
77
|
+
await flush();
|
|
78
|
+
expect(state.created).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('forwards clicks as tray events, discriminating left/right', async () => {
|
|
82
|
+
const { tauri, state } = fakeTauri();
|
|
83
|
+
const backend = createTauriTrayBackend(tauri);
|
|
84
|
+
const events: string[] = [];
|
|
85
|
+
backend.subscribe((event) => events.push(event.type));
|
|
86
|
+
const id = backend.create({});
|
|
87
|
+
await flush();
|
|
88
|
+
state.action!({ type: 'Click', button: 'Left' });
|
|
89
|
+
state.action!({ type: 'Click', button: 'Right' });
|
|
90
|
+
state.action!({ type: 'DoubleClick' });
|
|
91
|
+
state.action!({ type: 'Enter' });
|
|
92
|
+
expect(events).toEqual(['click', 'rightClick', 'doubleClick']);
|
|
93
|
+
expect(id).toBeGreaterThanOrEqual(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('applies title/icon changes and a context menu to the resolved handle', async () => {
|
|
97
|
+
const { tauri, state } = fakeTauri();
|
|
98
|
+
const backend = createTauriTrayBackend(tauri);
|
|
99
|
+
const id = backend.create({});
|
|
100
|
+
await flush();
|
|
101
|
+
backend.setTitle(id, 'New');
|
|
102
|
+
backend.setIcon(id, '/icon.png');
|
|
103
|
+
backend.setContextMenu(id, [{ id: 'a', label: 'A' }, { type: 'separator' }]);
|
|
104
|
+
await flush();
|
|
105
|
+
expect(state.lastTitle).toBe('New');
|
|
106
|
+
expect(state.lastIcon).toBe('/icon.png');
|
|
107
|
+
expect(state.menusSet).toBe(1);
|
|
108
|
+
expect(backend.getTitle(id)).toBe('New');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('destroys the tray and reports sentinels for unsupported surface', async () => {
|
|
112
|
+
const { tauri, state } = fakeTauri();
|
|
113
|
+
const backend = createTauriTrayBackend(tauri);
|
|
114
|
+
const id = backend.create({});
|
|
115
|
+
await flush();
|
|
116
|
+
expect(backend.getBounds(id)).toBeNull();
|
|
117
|
+
expect(backend.getCapabilities().balloon).toBe(false);
|
|
118
|
+
backend.destroy(id);
|
|
119
|
+
await flush();
|
|
120
|
+
expect(state.closed).toBe(1);
|
|
121
|
+
expect(backend.isDestroyed(id)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|