@flighthq/menu 0.5.1-next.1069.554a2ac → 0.5.1-next.1201.9b49dee
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/README.md +2 -2
- package/dist/menu.d.ts +6 -6
- package/dist/menu.d.ts.map +1 -1
- package/dist/menu.js +16 -17
- package/dist/menu.js.map +1 -1
- package/package.json +4 -4
- package/src/menu.test.ts +61 -37
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/menu`. The `@flighth
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/menu@0.5.1-next.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/menu@0.5.1-next.1201.9b49dee](https://github.com/flighthq/flight/tree/9b49dee3f22f6b2838c3d237edcc77bca65734f2/packages/menu)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/9b49dee3f22f6b2838c3d237edcc77bca65734f2/LICENSE.md)
|
package/dist/menu.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { EntityConstruction,
|
|
2
|
-
export declare function attachMenuHighlight(
|
|
3
|
-
export declare function attachMenuSelect(
|
|
1
|
+
import type { EntityConstruction, HostMenuApplicationProvider, HostMenuHighlightProvider, HostMenuPopupProvider, HostMenuSelectProvider, MenuHighlight, MenuItemTemplate, MenuSelect, MenuSignals, NonEntityCreateResult } from '@flighthq/types/contract';
|
|
2
|
+
export declare function attachMenuHighlight(hostMenuHighlight: Readonly<HostMenuHighlightProvider>, highlight: MenuHighlight): void;
|
|
3
|
+
export declare function attachMenuSelect(hostMenuSelect: Readonly<HostMenuSelectProvider>, select: MenuSelect): void;
|
|
4
4
|
export declare function cloneMenuTemplate(template: Readonly<MenuItemTemplate>): MenuItemTemplate;
|
|
5
5
|
export declare function createMenuHighlight(): MenuHighlight;
|
|
6
6
|
export declare function createMenuItemTemplate(template?: Readonly<Partial<MenuItemTemplate>>): NonEntityCreateResult<MenuItemTemplate, 'descriptor'>;
|
|
7
7
|
export declare function createMenuSelect(): MenuSelect;
|
|
8
|
-
export declare function destroyMenuApplication(...
|
|
8
|
+
export declare function destroyMenuApplication(...hostMenuApplication: readonly Readonly<HostMenuApplicationProvider>[]): void;
|
|
9
9
|
export declare function detachMenuHighlight(highlight: MenuHighlight): void;
|
|
10
10
|
export declare function detachMenuSelect(select: MenuSelect): void;
|
|
11
11
|
export declare function disposeMenuHighlight(highlight: MenuHighlight): void;
|
|
@@ -14,7 +14,7 @@ export declare function enableMenuSignals(): MenuSignals;
|
|
|
14
14
|
export declare function getMenuSignals(): Readonly<MenuSignals> | null;
|
|
15
15
|
export declare function initializeMenuHighlight(out: EntityConstruction<MenuHighlight>): void;
|
|
16
16
|
export declare function initializeMenuSelect(out: EntityConstruction<MenuSelect>): void;
|
|
17
|
-
export declare function setApplicationMenu(
|
|
18
|
-
export declare function showContextMenu(
|
|
17
|
+
export declare function setApplicationMenu(hostMenuApplication: Readonly<HostMenuApplicationProvider>, items: readonly MenuItemTemplate[]): boolean;
|
|
18
|
+
export declare function showContextMenu(hostMenuPopup: Readonly<HostMenuPopupProvider>, items: readonly MenuItemTemplate[], x: number, y: number): Promise<string | null>;
|
|
19
19
|
export declare function validateMenuItemTemplate(template: Readonly<MenuItemTemplate>): string | null;
|
|
20
20
|
//# sourceMappingURL=menu.d.ts.map
|
package/dist/menu.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,2BAA2B,EAC3B,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAQlC,wBAAgB,mBAAmB,CACjC,iBAAiB,EAAE,QAAQ,CAAC,yBAAyB,CAAC,EACtD,SAAS,EAAE,aAAa,GACvB,IAAI,CAIN;AAID,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAI3G;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAMxF;AAED,wBAAgB,mBAAmB,IAAI,aAAa,CAInD;AAKD,wBAAgB,sBAAsB,CACpC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAC7C,qBAAqB,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAUvD;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAI7C;AAYD,wBAAgB,sBAAsB,CAAC,GAAG,mBAAmB,EAAE,SAAS,QAAQ,CAAC,2BAA2B,CAAC,EAAE,GAAG,IAAI,CAgBrH;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAGlE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGzD;AAID,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAGnE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAG1D;AAMD,wBAAgB,iBAAiB,IAAI,WAAW,CAQ/C;AAED,wBAAgB,cAAc,IAAI,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAE7D;AAID,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,kBAAkB,CAAC,aAAa,CAAC,GAAG,IAAI,CAEpF;AAGD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,CAAC,GAAG,IAAI,CAE9E;AAKD,wBAAgB,kBAAkB,CAChC,mBAAmB,EAAE,QAAQ,CAAC,2BAA2B,CAAC,EAC1D,KAAK,EAAE,SAAS,gBAAgB,EAAE,GACjC,OAAO,CAET;AAID,wBAAgB,eAAe,CAC7B,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EAC9C,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAClC,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB;AAKD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,MAAM,GAAG,IAAI,CAE5F"}
|
package/dist/menu.js
CHANGED
|
@@ -5,17 +5,17 @@ import { clearSignal, createSignal, emitSignal } from '@flighthq/signals/contrac
|
|
|
5
5
|
//
|
|
6
6
|
// The unsubscribe is ORIGIN-PINNED: it is stored beside the entity that opened it, so detach ends
|
|
7
7
|
// exactly the subscription this attach created. Under the old ambient model a rebind could leave an
|
|
8
|
-
// earlier subscription live against a replaced
|
|
9
|
-
export function attachMenuHighlight(
|
|
8
|
+
// earlier subscription live against a replaced provider with nothing holding its unsubscribe.
|
|
9
|
+
export function attachMenuHighlight(hostMenuHighlight, highlight) {
|
|
10
10
|
detachMenuHighlight(highlight);
|
|
11
|
-
const unsubscribe =
|
|
11
|
+
const unsubscribe = hostMenuHighlight.subscribe((id) => emitSignal(highlight.onMenuItemHighlight, id));
|
|
12
12
|
_highlightUnsubscribe.set(highlight, unsubscribe);
|
|
13
13
|
}
|
|
14
14
|
// Starts delivering application menu-bar selections from the host's provider into `select`. Same
|
|
15
15
|
// origin-pinned unsubscribe contract as attachMenuHighlight.
|
|
16
|
-
export function attachMenuSelect(
|
|
16
|
+
export function attachMenuSelect(hostMenuSelect, select) {
|
|
17
17
|
detachMenuSelect(select);
|
|
18
|
-
const unsubscribe =
|
|
18
|
+
const unsubscribe = hostMenuSelect.subscribe((id) => emitSignal(select.onMenuItemSelect, id));
|
|
19
19
|
_selectUnsubscribe.set(select, unsubscribe);
|
|
20
20
|
}
|
|
21
21
|
// Deep-clones a MenuItemTemplate tree. The returned tree has the same shape and values. Safe to call
|
|
@@ -52,7 +52,7 @@ export function createMenuSelect() {
|
|
|
52
52
|
return finishEntity(out);
|
|
53
53
|
}
|
|
54
54
|
// Stops delivery without discarding the entity: runs this entity's own unsubscribe, if it has one.
|
|
55
|
-
// Safe to call when never attached. Does NOT touch the provider —
|
|
55
|
+
// Safe to call when never attached. Does NOT touch the provider — provider teardown is `destroy` on the
|
|
56
56
|
// slot, a separate host/provider lifecycle concern.
|
|
57
57
|
// FINAL RELEASE for the one menu slot that owns a whole-provider resource: the installed native menu.
|
|
58
58
|
// Destroys every DISTINCT application provider exactly once — alias-safe, because two hosts may share
|
|
@@ -61,10 +61,9 @@ export function createMenuSelect() {
|
|
|
61
61
|
// Attempt-all: every obligation is tried even after one throws, and the first error is rethrown once the
|
|
62
62
|
// siblings have run. A provider whose destroy threw is RETAINED, so a later call retries only the
|
|
63
63
|
// failures; the ones that succeeded are forgotten and never destroyed twice.
|
|
64
|
-
export function destroyMenuApplication(...
|
|
64
|
+
export function destroyMenuApplication(...hostMenuApplication) {
|
|
65
65
|
const pending = new Set();
|
|
66
|
-
for (const
|
|
67
|
-
const provider = host.menu.application;
|
|
66
|
+
for (const provider of hostMenuApplication) {
|
|
68
67
|
if (!_destroyedApplication.has(provider))
|
|
69
68
|
pending.add(provider);
|
|
70
69
|
}
|
|
@@ -102,7 +101,7 @@ export function disposeMenuSelect(select) {
|
|
|
102
101
|
clearSignal(select.onMenuItemSelect);
|
|
103
102
|
}
|
|
104
103
|
// Activates the core context-menu dispatcher signals and returns the group. These are NOT host
|
|
105
|
-
// capabilities: the dispatcher below emits them around the popup call, so no
|
|
104
|
+
// capabilities: the dispatcher below emits them around the popup call, so no provider can deliver them.
|
|
106
105
|
// The module-level identity/enable state here is package state, not ambient capability resolution —
|
|
107
106
|
// nothing about which provider serves a call is decided by it.
|
|
108
107
|
export function enableMenuSignals() {
|
|
@@ -129,16 +128,16 @@ export function initializeMenuSelect(out) {
|
|
|
129
128
|
// Installs the application menu bar through the host's provider. Returns false when the install did
|
|
130
129
|
// not take effect. A host without a native menu bar omits the slot entirely, so this cannot be reached
|
|
131
130
|
// with a stub that always answers false.
|
|
132
|
-
export function setApplicationMenu(
|
|
133
|
-
return
|
|
131
|
+
export function setApplicationMenu(hostMenuApplication, items) {
|
|
132
|
+
return hostMenuApplication.setApplicationMenu(items);
|
|
134
133
|
}
|
|
135
134
|
// Pops up a context menu through the host's provider and resolves the chosen item id, or null when
|
|
136
135
|
// dismissed. Emits the core open/close dispatcher signals around the call when they are enabled.
|
|
137
|
-
export function showContextMenu(
|
|
136
|
+
export function showContextMenu(hostMenuPopup, items, x, y) {
|
|
138
137
|
const signals = _menuSignals;
|
|
139
138
|
if (signals !== null)
|
|
140
139
|
emitSignal(signals.onContextMenuOpen);
|
|
141
|
-
const promise =
|
|
140
|
+
const promise = hostMenuPopup.popup(items, x, y);
|
|
142
141
|
if (signals !== null)
|
|
143
142
|
void promise.then(() => emitSignal(signals.onContextMenuClose));
|
|
144
143
|
return promise;
|
|
@@ -169,8 +168,8 @@ function _validateItem(item, seen) {
|
|
|
169
168
|
return `item type "${item.type ?? 'normal'}" has a submenu (only type "submenu" should carry children)`;
|
|
170
169
|
}
|
|
171
170
|
// `checked` only means something on the two toggle types. Setting it elsewhere is the mistake that
|
|
172
|
-
// renders silently: the
|
|
173
|
-
// checked: true grows a tick it can never clear, and native
|
|
171
|
+
// renders silently: the Web popup provider draws the checkmark from `checked` alone, so a normal item with
|
|
172
|
+
// checked: true grows a tick it can never clear, and native providers typically drop it instead —
|
|
174
173
|
// same descriptor, two different wrong results, which is exactly what validation is for.
|
|
175
174
|
if (item.checked !== undefined && item.type !== 'checkbox' && item.type !== 'radio') {
|
|
176
175
|
return `item type "${item.type ?? 'normal'}" has "checked" (only "checkbox" and "radio" items are checkable)`;
|
|
@@ -191,7 +190,7 @@ function _validateItem(item, seen) {
|
|
|
191
190
|
}
|
|
192
191
|
// A radio group is a run of adjacent radio items — any other item type, including a separator, starts
|
|
193
192
|
// a new one. Exactly one member of a run may be checked; two checked members describe a state the
|
|
194
|
-
// widget cannot represent, and each
|
|
193
|
+
// widget cannot represent, and each provider picks a different winner. Checked on a non-radio item is
|
|
195
194
|
// caught per-item above; this is the rule that only exists across siblings, which is why it runs where
|
|
196
195
|
// the child list is known rather than inside the per-item walk.
|
|
197
196
|
function _validateRadioGroups(items) {
|
package/dist/menu.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/menu",
|
|
3
|
-
"version": "0.5.1-next.
|
|
3
|
+
"version": "0.5.1-next.1201.9b49dee",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/entity": "0.5.1-next.
|
|
42
|
-
"@flighthq/signals": "0.5.1-next.
|
|
43
|
-
"@flighthq/types": "0.5.1-next.
|
|
41
|
+
"@flighthq/entity": "0.5.1-next.1201.9b49dee",
|
|
42
|
+
"@flighthq/signals": "0.5.1-next.1201.9b49dee",
|
|
43
|
+
"@flighthq/types": "0.5.1-next.1201.9b49dee"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"typescript": "^5.3.0"
|
package/src/menu.test.ts
CHANGED
|
@@ -2,15 +2,11 @@ import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
|
|
|
2
2
|
import { connectSignal } from '@flighthq/signals/contract';
|
|
3
3
|
import type {
|
|
4
4
|
EntityWithoutRuntime,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
MenuApplicationBackend,
|
|
10
|
-
MenuHighlightBackend,
|
|
5
|
+
HostMenuApplicationProvider,
|
|
6
|
+
HostMenuHighlightProvider,
|
|
7
|
+
HostMenuPopupProvider,
|
|
8
|
+
HostMenuSelectProvider,
|
|
11
9
|
MenuItemTemplate,
|
|
12
|
-
MenuPopupBackend,
|
|
13
|
-
MenuSelectBackend,
|
|
14
10
|
} from '@flighthq/types/contract';
|
|
15
11
|
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
16
12
|
|
|
@@ -37,7 +33,10 @@ import {
|
|
|
37
33
|
|
|
38
34
|
// A host exposing exactly the slots a test needs. Nothing is installed anywhere: two of these can be
|
|
39
35
|
// live at once, which is the property the ambient model could not express.
|
|
40
|
-
function popupHost(
|
|
36
|
+
function popupHost(
|
|
37
|
+
result: string | null,
|
|
38
|
+
calls: string[] = [],
|
|
39
|
+
): { readonly menu: { readonly popup: HostMenuPopupProvider } } & { calls: string[] } {
|
|
41
40
|
return {
|
|
42
41
|
calls,
|
|
43
42
|
menu: {
|
|
@@ -53,7 +52,10 @@ function popupHost(result: string | null, calls: string[] = []): HasMenuPopup &
|
|
|
53
52
|
};
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
function applicationHost(
|
|
55
|
+
function applicationHost(
|
|
56
|
+
accepted: boolean,
|
|
57
|
+
seen: MenuItemTemplate[][] = [],
|
|
58
|
+
): { readonly menu: { readonly application: HostMenuApplicationProvider } } {
|
|
57
59
|
return {
|
|
58
60
|
menu: {
|
|
59
61
|
application: (() => {
|
|
@@ -68,7 +70,10 @@ function applicationHost(accepted: boolean, seen: MenuItemTemplate[][] = []): Ha
|
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function selectHost():
|
|
73
|
+
function selectHost(): { readonly menu: { readonly select: HostMenuSelectProvider } } & {
|
|
74
|
+
emit(id: string): void;
|
|
75
|
+
subscriberCount(): number;
|
|
76
|
+
} {
|
|
72
77
|
const listeners = new Set<(id: string) => void>();
|
|
73
78
|
return {
|
|
74
79
|
emit(id: string): void {
|
|
@@ -92,7 +97,9 @@ function selectHost(): HasMenuSelect & { emit(id: string): void; subscriberCount
|
|
|
92
97
|
};
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
function highlightHost():
|
|
100
|
+
function highlightHost(): { readonly menu: { readonly highlight: HostMenuHighlightProvider } } & {
|
|
101
|
+
emit(id: string): void;
|
|
102
|
+
} {
|
|
96
103
|
const listeners = new Set<(id: string) => void>();
|
|
97
104
|
return {
|
|
98
105
|
emit(id: string): void {
|
|
@@ -119,7 +126,7 @@ describe('attachMenuHighlight', () => {
|
|
|
119
126
|
const highlight = createMenuHighlight();
|
|
120
127
|
const seen: string[] = [];
|
|
121
128
|
connectSignal(highlight.onMenuItemHighlight, (id) => seen.push(id));
|
|
122
|
-
attachMenuHighlight(host, highlight);
|
|
129
|
+
attachMenuHighlight(host.menu.highlight, highlight);
|
|
123
130
|
host.emit('open');
|
|
124
131
|
expect(seen).toEqual(['open']);
|
|
125
132
|
});
|
|
@@ -132,7 +139,7 @@ describe('attachMenuSelect', () => {
|
|
|
132
139
|
const select = createMenuSelect();
|
|
133
140
|
const seen: string[] = [];
|
|
134
141
|
connectSignal(select.onMenuItemSelect, (id) => seen.push(id));
|
|
135
|
-
attachMenuSelect(host, select);
|
|
142
|
+
attachMenuSelect(host.menu.select, select);
|
|
136
143
|
host.emit('save');
|
|
137
144
|
expect(seen).toEqual(['save']);
|
|
138
145
|
});
|
|
@@ -143,8 +150,8 @@ describe('attachMenuSelect', () => {
|
|
|
143
150
|
const select = createMenuSelect();
|
|
144
151
|
const seen: string[] = [];
|
|
145
152
|
connectSignal(select.onMenuItemSelect, (id) => seen.push(id));
|
|
146
|
-
attachMenuSelect(host, select);
|
|
147
|
-
attachMenuSelect(host, select);
|
|
153
|
+
attachMenuSelect(host.menu.select, select);
|
|
154
|
+
attachMenuSelect(host.menu.select, select);
|
|
148
155
|
expect(host.subscriberCount()).toBe(1);
|
|
149
156
|
host.emit('save');
|
|
150
157
|
expect(seen).toEqual(['save']);
|
|
@@ -201,24 +208,26 @@ describe('createMenuSelect', () => {
|
|
|
201
208
|
});
|
|
202
209
|
|
|
203
210
|
describe('destroyMenuApplication', () => {
|
|
204
|
-
function applicationHostWith(provider: EntityWithoutRuntime<
|
|
211
|
+
function applicationHostWith(provider: EntityWithoutRuntime<HostMenuApplicationProvider>): {
|
|
212
|
+
readonly menu: { readonly application: HostMenuApplicationProvider };
|
|
213
|
+
} {
|
|
205
214
|
(provider as Record<symbol, unknown>)[EntityRuntimeKey] = undefined;
|
|
206
|
-
return { menu: { application: provider as
|
|
215
|
+
return { menu: { application: provider as HostMenuApplicationProvider } };
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
it('destroys each distinct provider exactly once, even when hosts alias one', () => {
|
|
210
219
|
let destroyed = 0;
|
|
211
220
|
const shared = { destroy: () => destroyed++, setApplicationMenu: () => true };
|
|
212
221
|
// ★ ALIAS SAFETY: two hosts, ONE provider object. Destroying it twice would clear a successor's menu.
|
|
213
|
-
destroyMenuApplication(applicationHostWith(shared), applicationHostWith(shared));
|
|
222
|
+
destroyMenuApplication(applicationHostWith(shared).menu.application, applicationHostWith(shared).menu.application);
|
|
214
223
|
expect(destroyed).toBe(1);
|
|
215
224
|
});
|
|
216
225
|
|
|
217
226
|
it('never destroys an already-released provider a second time', () => {
|
|
218
227
|
let destroyed = 0;
|
|
219
228
|
const provider = { destroy: () => destroyed++, setApplicationMenu: () => true };
|
|
220
|
-
destroyMenuApplication(applicationHostWith(provider));
|
|
221
|
-
destroyMenuApplication(applicationHostWith(provider));
|
|
229
|
+
destroyMenuApplication(applicationHostWith(provider).menu.application);
|
|
230
|
+
destroyMenuApplication(applicationHostWith(provider).menu.application);
|
|
222
231
|
expect(destroyed).toBe(1);
|
|
223
232
|
});
|
|
224
233
|
|
|
@@ -232,9 +241,12 @@ describe('destroyMenuApplication', () => {
|
|
|
232
241
|
setApplicationMenu: () => true,
|
|
233
242
|
};
|
|
234
243
|
const healthy = { destroy: () => (secondDestroyed = true), setApplicationMenu: () => true };
|
|
235
|
-
expect(() =>
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
expect(() =>
|
|
245
|
+
destroyMenuApplication(
|
|
246
|
+
applicationHostWith(failing).menu.application,
|
|
247
|
+
applicationHostWith(healthy).menu.application,
|
|
248
|
+
),
|
|
249
|
+
).toThrow('first failed');
|
|
238
250
|
expect(secondDestroyed).toBe(true);
|
|
239
251
|
});
|
|
240
252
|
|
|
@@ -250,14 +262,26 @@ describe('destroyMenuApplication', () => {
|
|
|
250
262
|
setApplicationMenu: () => true,
|
|
251
263
|
};
|
|
252
264
|
const healthy = { destroy: () => healthyDestroyed++, setApplicationMenu: () => true };
|
|
253
|
-
expect(() =>
|
|
254
|
-
|
|
265
|
+
expect(() =>
|
|
266
|
+
destroyMenuApplication(
|
|
267
|
+
applicationHostWith(flaky).menu.application,
|
|
268
|
+
applicationHostWith(healthy).menu.application,
|
|
269
|
+
),
|
|
270
|
+
).toThrow();
|
|
271
|
+
expect(() =>
|
|
272
|
+
destroyMenuApplication(
|
|
273
|
+
applicationHostWith(flaky).menu.application,
|
|
274
|
+
applicationHostWith(healthy).menu.application,
|
|
275
|
+
),
|
|
276
|
+
).not.toThrow();
|
|
255
277
|
expect(attempts).toBe(2);
|
|
256
278
|
expect(healthyDestroyed).toBe(1);
|
|
257
279
|
});
|
|
258
280
|
|
|
259
281
|
it('tolerates a provider that declares no destroy', () => {
|
|
260
|
-
expect(() =>
|
|
282
|
+
expect(() =>
|
|
283
|
+
destroyMenuApplication(applicationHostWith({ setApplicationMenu: () => true }).menu.application),
|
|
284
|
+
).not.toThrow();
|
|
261
285
|
});
|
|
262
286
|
});
|
|
263
287
|
|
|
@@ -274,7 +298,7 @@ describe('detachMenuSelect', () => {
|
|
|
274
298
|
const select = createMenuSelect();
|
|
275
299
|
const seen: string[] = [];
|
|
276
300
|
connectSignal(select.onMenuItemSelect, (id) => seen.push(id));
|
|
277
|
-
attachMenuSelect(host, select);
|
|
301
|
+
attachMenuSelect(host.menu.select, select);
|
|
278
302
|
host.emit('first');
|
|
279
303
|
detachMenuSelect(select);
|
|
280
304
|
host.emit('second');
|
|
@@ -289,8 +313,8 @@ describe('detachMenuSelect', () => {
|
|
|
289
313
|
const dropped = createMenuSelect();
|
|
290
314
|
const keptSeen: string[] = [];
|
|
291
315
|
connectSignal(kept.onMenuItemSelect, (id) => keptSeen.push(id));
|
|
292
|
-
attachMenuSelect(host, kept);
|
|
293
|
-
attachMenuSelect(host, dropped);
|
|
316
|
+
attachMenuSelect(host.menu.select, kept);
|
|
317
|
+
attachMenuSelect(host.menu.select, dropped);
|
|
294
318
|
detachMenuSelect(dropped);
|
|
295
319
|
host.emit('still-here');
|
|
296
320
|
expect(keptSeen).toEqual(['still-here']);
|
|
@@ -305,7 +329,7 @@ describe('disposeMenuHighlight', () => {
|
|
|
305
329
|
const highlight = createMenuHighlight();
|
|
306
330
|
const seen: string[] = [];
|
|
307
331
|
connectSignal(highlight.onMenuItemHighlight, (id) => seen.push(id));
|
|
308
|
-
attachMenuHighlight(host, highlight);
|
|
332
|
+
attachMenuHighlight(host.menu.highlight, highlight);
|
|
309
333
|
disposeMenuHighlight(highlight);
|
|
310
334
|
host.emit('after-dispose');
|
|
311
335
|
expect(seen).toEqual([]);
|
|
@@ -320,7 +344,7 @@ describe('disposeMenuSelect', () => {
|
|
|
320
344
|
const select = createMenuSelect();
|
|
321
345
|
const seen: string[] = [];
|
|
322
346
|
connectSignal(select.onMenuItemSelect, (id) => seen.push(id));
|
|
323
|
-
attachMenuSelect(host, select);
|
|
347
|
+
attachMenuSelect(host.menu.select, select);
|
|
324
348
|
disposeMenuSelect(select);
|
|
325
349
|
host.emit('after-dispose');
|
|
326
350
|
expect(seen).toEqual([]);
|
|
@@ -360,12 +384,12 @@ describe('setApplicationMenu', () => {
|
|
|
360
384
|
it('reports the provider result and delivers the items', () => {
|
|
361
385
|
const seen: MenuItemTemplate[][] = [];
|
|
362
386
|
const items = [{ id: 'quit', label: 'Quit' }];
|
|
363
|
-
expect(setApplicationMenu(applicationHost(true, seen), items)).toBe(true);
|
|
387
|
+
expect(setApplicationMenu(applicationHost(true, seen).menu.application, items)).toBe(true);
|
|
364
388
|
expect(seen).toEqual([items]);
|
|
365
389
|
});
|
|
366
390
|
|
|
367
391
|
it('reports a refused install as false', () => {
|
|
368
|
-
expect(setApplicationMenu(applicationHost(false), [{ id: 'quit', label: 'Quit' }])).toBe(false);
|
|
392
|
+
expect(setApplicationMenu(applicationHost(false).menu.application, [{ id: 'quit', label: 'Quit' }])).toBe(false);
|
|
369
393
|
});
|
|
370
394
|
});
|
|
371
395
|
describe('showContextMenu', () => {
|
|
@@ -374,8 +398,8 @@ describe('showContextMenu', () => {
|
|
|
374
398
|
it('routes each call to the host it was given', async () => {
|
|
375
399
|
const first = popupHost('from-first');
|
|
376
400
|
const second = popupHost('from-second');
|
|
377
|
-
await expect(showContextMenu(first, [], 1, 2)).resolves.toBe('from-first');
|
|
378
|
-
await expect(showContextMenu(second, [], 3, 4)).resolves.toBe('from-second');
|
|
401
|
+
await expect(showContextMenu(first.menu.popup, [], 1, 2)).resolves.toBe('from-first');
|
|
402
|
+
await expect(showContextMenu(second.menu.popup, [], 3, 4)).resolves.toBe('from-second');
|
|
379
403
|
expect(first.calls).toEqual(['popup@1,2']);
|
|
380
404
|
expect(second.calls).toEqual(['popup@3,4']);
|
|
381
405
|
});
|
|
@@ -385,7 +409,7 @@ describe('showContextMenu', () => {
|
|
|
385
409
|
const order: string[] = [];
|
|
386
410
|
connectSignal(signals.onContextMenuOpen, () => order.push('open'));
|
|
387
411
|
connectSignal(signals.onContextMenuClose, () => order.push('close'));
|
|
388
|
-
await showContextMenu(popupHost('x'), [], 0, 0);
|
|
412
|
+
await showContextMenu(popupHost('x').menu.popup, [], 0, 0);
|
|
389
413
|
expect(order).toEqual(['open', 'close']);
|
|
390
414
|
});
|
|
391
415
|
});
|