@caperjs/core 0.2.4 → 0.4.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/build/assetpack.mjs +90 -0
- package/build/index.mjs +3 -1
- package/build/internal/discovery.mjs +1 -1
- package/build/internal/manifest.mjs +8 -3
- package/build/internal/validate.mjs +3 -1
- package/build/plugins/assetTypes.mjs +4 -4
- package/build/plugins/caperConfig.mjs +12 -5
- package/build/plugins/lists.mjs +29 -9
- package/build/plugins/pruneFallbacks.mjs +25 -2
- package/cli/create.mjs +57 -6
- package/cli/create.test.mjs +36 -0
- package/cli/update.mjs +1 -1
- package/lib/{CaptionsPlugin-WPAcfAkx.js → CaptionsPlugin-cSJj-R9M.js} +5 -5
- package/lib/{CaptionsPlugin-WPAcfAkx.js.map → CaptionsPlugin-cSJj-R9M.js.map} +1 -1
- package/lib/{DataAdapter-DS-twowe.js → DataAdapter-D2tc6hx9.js} +15 -10
- package/lib/DataAdapter-D2tc6hx9.js.map +1 -0
- package/lib/{DebugRenderer-D1ryrRYx.js → DebugRenderer-CGUKy6Fy.js} +2 -2
- package/lib/{DebugRenderer-D1ryrRYx.js.map → DebugRenderer-CGUKy6Fy.js.map} +1 -1
- package/lib/{DevToolsPlugin-DH7zX7HU.js → DevToolsPlugin-B8GX3H9b.js} +2 -2
- package/lib/{DevToolsPlugin-DH7zX7HU.js.map → DevToolsPlugin-B8GX3H9b.js.map} +1 -1
- package/lib/{GSAPPlugin-BBtRjUqe.js → GSAPPlugin-Br9AjuTD.js} +6 -6
- package/lib/{GSAPPlugin-BBtRjUqe.js.map → GSAPPlugin-Br9AjuTD.js.map} +1 -1
- package/lib/{LayoutPlugin-Co6vcoV4.js → LayoutPlugin-BX3OP7pw.js} +4 -4
- package/lib/{LayoutPlugin-Co6vcoV4.js.map → LayoutPlugin-BX3OP7pw.js.map} +1 -1
- package/lib/{SpinePlugin-uN641Hmk.js → SpinePlugin-C-JDdXpf.js} +690 -793
- package/lib/SpinePlugin-C-JDdXpf.js.map +1 -0
- package/lib/{StatsPlugin-vZQRm6wh.js → StatsPlugin-yJlJtoQ2.js} +7 -7
- package/lib/StatsPlugin-yJlJtoQ2.js.map +1 -0
- package/lib/{VoiceOverPlugin-KlZdGj1G.js → VoiceOverPlugin-DjSZp4Ko.js} +7 -6
- package/lib/VoiceOverPlugin-DjSZp4Ko.js.map +1 -0
- package/lib/caper.mjs +159 -1917
- package/lib/caper.mjs.map +1 -1
- package/lib/const-C3sxeYhl.js +1746 -0
- package/lib/const-C3sxeYhl.js.map +1 -0
- package/lib/core/Application.d.ts +8 -3
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/Application.test.d.ts +2 -0
- package/lib/core/Application.test.d.ts.map +1 -0
- package/lib/core/config.d.ts +6 -1
- package/lib/core/config.d.ts.map +1 -1
- package/lib/core/config.test.d.ts +2 -0
- package/lib/core/config.test.d.ts.map +1 -0
- package/lib/core/create.d.ts.map +1 -1
- package/lib/core/interfaces/IApplication.d.ts +6 -6
- package/lib/core/interfaces/IApplication.d.ts.map +1 -1
- package/lib/core/interfaces/ICoreSignals.d.ts +2 -1
- package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
- package/lib/display/Camera.test.d.ts +2 -0
- package/lib/display/Camera.test.d.ts.map +1 -0
- package/lib/display/Entity.d.ts +2 -2
- package/lib/display/SceneTransition.test.d.ts +1 -0
- package/lib/display/SceneTransition.test.d.ts.map +1 -0
- package/lib/display/SpineAnimation.d.ts +3 -3
- package/lib/mixins/factory/methods.d.ts.map +1 -1
- package/lib/padding-1kHDpGlQ.js +41 -0
- package/lib/padding-1kHDpGlQ.js.map +1 -0
- package/lib/plugins/AssetsPlugin.d.ts.map +1 -1
- package/lib/plugins/AssetsPlugin.test.d.ts +2 -0
- package/lib/plugins/AssetsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/DataAdapter.d.ts.map +1 -1
- package/lib/plugins/DataAdapter.test.d.ts +2 -0
- package/lib/plugins/DataAdapter.test.d.ts.map +1 -0
- package/lib/plugins/FullScreenPlugin.d.ts +0 -5
- package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
- package/lib/plugins/FullScreenPlugin.test.d.ts +2 -0
- package/lib/plugins/FullScreenPlugin.test.d.ts.map +1 -0
- package/lib/plugins/GSAPPlugin.d.ts +0 -2
- package/lib/plugins/GSAPPlugin.d.ts.map +1 -1
- package/lib/plugins/GSAPPlugin.test.d.ts +2 -0
- package/lib/plugins/GSAPPlugin.test.d.ts.map +1 -0
- package/lib/plugins/KeyboardPlugin.d.ts +0 -1
- package/lib/plugins/KeyboardPlugin.d.ts.map +1 -1
- package/lib/plugins/LookupPlugin.d.ts +6 -1
- package/lib/plugins/LookupPlugin.d.ts.map +1 -1
- package/lib/plugins/LookupPlugin.test.d.ts +2 -0
- package/lib/plugins/LookupPlugin.test.d.ts.map +1 -0
- package/lib/plugins/Plugin.d.ts +101 -2
- package/lib/plugins/Plugin.d.ts.map +1 -1
- package/lib/plugins/PopupManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/PopupManagerPlugin.test.d.ts +1 -0
- package/lib/plugins/PopupManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/SceneManagerPlugin.d.ts +1 -1
- package/lib/plugins/SceneManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/SceneManagerPlugin.test.d.ts +2 -0
- package/lib/plugins/SceneManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/StatsPlugin.d.ts.map +1 -1
- package/lib/plugins/TimerPlugin.d.ts.map +1 -1
- package/lib/plugins/WebEventsPlugin.d.ts +0 -1
- package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
- package/lib/plugins/actions/ActionsPlugin.test.d.ts +2 -0
- package/lib/plugins/actions/ActionsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/audio/AudioChannel.d.ts.map +1 -1
- package/lib/plugins/audio/AudioManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/audio/AudioManagerPlugin.test.d.ts +2 -0
- package/lib/plugins/audio/AudioManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/audio/VoiceOverPlugin.d.ts.map +1 -1
- package/lib/plugins/breakpoints/BreakpointPlugin.d.ts.map +1 -1
- package/lib/plugins/captions/CaptionsPlugin.test.d.ts +2 -0
- package/lib/plugins/captions/CaptionsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/focus/FocusManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/focus/FocusManagerPlugin.test.d.ts +2 -0
- package/lib/plugins/focus/FocusManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/gesture/GesturePlugin.d.ts.map +1 -1
- package/lib/plugins/input/AbstractControls.d.ts +47 -3
- package/lib/plugins/input/AbstractControls.d.ts.map +1 -1
- package/lib/plugins/input/Controls.test.d.ts +2 -0
- package/lib/plugins/input/Controls.test.d.ts.map +1 -0
- package/lib/plugins/input/InputPlugin.d.ts.map +1 -1
- package/lib/plugins/input/controlsCore.d.ts +58 -0
- package/lib/plugins/input/controlsCore.d.ts.map +1 -0
- package/lib/plugins/input/controlsCore.test.d.ts +2 -0
- package/lib/plugins/input/controlsCore.test.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/KeyboardControls.d.ts +15 -0
- package/lib/plugins/input/keyboard/KeyboardControls.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/index.d.ts +1 -1
- package/lib/plugins/input/keyboard/index.d.ts.map +1 -1
- package/lib/plugins/input/touch/VirtualControls.d.ts +8 -14
- package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
- package/lib/plugins/input/touch/VirtualControls.test.d.ts +2 -0
- package/lib/plugins/input/touch/VirtualControls.test.d.ts.map +1 -0
- package/lib/{registries-BLrxALZ0.js → registries-BfV-VF7a.js} +665 -597
- package/lib/registries-BfV-VF7a.js.map +1 -0
- package/lib/store/Store.d.ts.map +1 -1
- package/lib/ui/Button.d.ts.map +1 -1
- package/lib/ui/UICanvas.d.ts +10 -1
- package/lib/ui/UICanvas.d.ts.map +1 -1
- package/lib/ui/UICanvas.test.d.ts +0 -1
- package/lib/ui/UICanvas.test.d.ts.map +1 -1
- package/lib/utils/color.d.ts.map +1 -1
- package/lib/utils/color.test.d.ts +2 -0
- package/lib/utils/color.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/Application.test.ts +168 -0
- package/src/core/Application.ts +37 -27
- package/src/core/config.test.ts +79 -0
- package/src/core/config.ts +28 -6
- package/src/core/create.ts +11 -0
- package/src/core/interfaces/IApplication.ts +6 -6
- package/src/core/interfaces/ICoreSignals.ts +2 -0
- package/src/display/Camera.test.ts +40 -0
- package/src/display/Camera.ts +3 -3
- package/src/display/Entity.ts +2 -2
- package/src/display/SceneTransition.test.ts +58 -0
- package/src/display/SceneTransition.ts +1 -1
- package/src/display/SpineAnimation.ts +4 -4
- package/src/mixins/factory/methods.ts +0 -1
- package/src/plugins/AssetsPlugin.test.ts +48 -0
- package/src/plugins/AssetsPlugin.ts +3 -1
- package/src/plugins/DataAdapter.test.ts +32 -0
- package/src/plugins/DataAdapter.ts +5 -1
- package/src/plugins/FullScreenPlugin.test.ts +48 -0
- package/src/plugins/FullScreenPlugin.ts +9 -18
- package/src/plugins/GSAPPlugin.test.ts +31 -0
- package/src/plugins/GSAPPlugin.ts +1 -4
- package/src/plugins/KeyboardPlugin.ts +3 -10
- package/src/plugins/LookupPlugin.test.ts +42 -0
- package/src/plugins/LookupPlugin.ts +15 -3
- package/src/plugins/Plugin.test.ts +130 -2
- package/src/plugins/Plugin.ts +159 -3
- package/src/plugins/PopupManagerPlugin.test.ts +53 -0
- package/src/plugins/PopupManagerPlugin.ts +5 -3
- package/src/plugins/SceneManagerPlugin.test.ts +76 -0
- package/src/plugins/SceneManagerPlugin.ts +18 -13
- package/src/plugins/StatsPlugin.ts +3 -2
- package/src/plugins/TimerPlugin.ts +1 -2
- package/src/plugins/WebEventsPlugin.test.ts +32 -0
- package/src/plugins/WebEventsPlugin.ts +9 -16
- package/src/plugins/actions/ActionsPlugin.test.ts +68 -0
- package/src/plugins/actions/ActionsPlugin.ts +5 -5
- package/src/plugins/audio/AudioChannel.test.ts +19 -0
- package/src/plugins/audio/AudioChannel.ts +8 -1
- package/src/plugins/audio/AudioManagerPlugin.test.ts +41 -0
- package/src/plugins/audio/AudioManagerPlugin.ts +3 -2
- package/src/plugins/audio/VoiceOverPlugin.ts +1 -0
- package/src/plugins/breakpoints/BreakpointPlugin.ts +1 -2
- package/src/plugins/captions/CaptionsPlugin.test.ts +40 -0
- package/src/plugins/captions/CaptionsPlugin.ts +2 -2
- package/src/plugins/focus/FocusManagerPlugin.test.ts +71 -0
- package/src/plugins/focus/FocusManagerPlugin.ts +7 -0
- package/src/plugins/gesture/GesturePlugin.ts +4 -8
- package/src/plugins/input/AbstractControls.ts +104 -3
- package/src/plugins/input/Controls.test.ts +205 -0
- package/src/plugins/input/InputPlugin.ts +6 -14
- package/src/plugins/input/controlsCore.test.ts +151 -0
- package/src/plugins/input/controlsCore.ts +162 -0
- package/src/plugins/input/keyboard/KeyboardControls.ts +84 -0
- package/src/plugins/input/keyboard/index.ts +1 -1
- package/src/plugins/input/touch/VirtualControls.test.ts +38 -0
- package/src/plugins/input/touch/VirtualControls.ts +53 -120
- package/src/plugins/spine/SpinePlugin.ts +2 -2
- package/src/store/Store.test.ts +15 -0
- package/src/store/Store.ts +4 -1
- package/src/ui/Button.test.ts +12 -0
- package/src/ui/Button.ts +1 -0
- package/src/ui/UICanvas.test.ts +109 -6
- package/src/ui/UICanvas.ts +41 -3
- package/src/utils/color.test.ts +16 -0
- package/src/utils/color.ts +2 -1
- package/src/utils/platform.ts +1 -1
- package/src/version.ts +1 -1
- package/lib/DataAdapter-DS-twowe.js.map +0 -1
- package/lib/SpinePlugin-uN641Hmk.js.map +0 -1
- package/lib/StatsPlugin-vZQRm6wh.js.map +0 -1
- package/lib/VoiceOverPlugin-KlZdGj1G.js.map +0 -1
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts +0 -28
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +0 -1
- package/lib/registries-BLrxALZ0.js.map +0 -1
- package/src/plugins/input/keyboard/KeyboardContols.ts +0 -182
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
// Plugin.ts transitively imports Application → Pixi display graph. Stub it.
|
|
4
4
|
vi.mock('../core', () => ({
|
|
5
5
|
coreFunctionRegistry: {},
|
|
6
6
|
coreSignalRegistry: {},
|
|
7
7
|
}));
|
|
8
|
+
|
|
9
|
+
const { mockTicker } = vi.hoisted(() => ({
|
|
10
|
+
mockTicker: { add: vi.fn(), remove: vi.fn() },
|
|
11
|
+
}));
|
|
12
|
+
|
|
8
13
|
vi.mock('../core/Application', () => ({
|
|
9
|
-
Application: { getInstance: () => ({}) },
|
|
14
|
+
Application: { getInstance: () => ({ ticker: mockTicker }) },
|
|
10
15
|
}));
|
|
11
16
|
|
|
12
17
|
import { Signal } from '../signals';
|
|
18
|
+
import { Logger } from '../utils';
|
|
13
19
|
import { Plugin } from './Plugin';
|
|
14
20
|
|
|
15
21
|
class TestPlugin extends Plugin<{ foo: string }> {
|
|
@@ -71,3 +77,125 @@ describe('Plugin', () => {
|
|
|
71
77
|
await expect(p.postInitialize({} as never)).resolves.toBeUndefined();
|
|
72
78
|
});
|
|
73
79
|
});
|
|
80
|
+
|
|
81
|
+
describe('Plugin lifecycle cleanup', () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
mockTicker.add.mockClear();
|
|
85
|
+
mockTicker.remove.mockClear();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('runs disposers LIFO, and only once', () => {
|
|
89
|
+
const p = new TestPlugin();
|
|
90
|
+
const order: string[] = [];
|
|
91
|
+
p.addDisposer(
|
|
92
|
+
() => order.push('a'),
|
|
93
|
+
() => order.push('b'),
|
|
94
|
+
);
|
|
95
|
+
p.addDisposer(() => order.push('c'));
|
|
96
|
+
|
|
97
|
+
p.destroy();
|
|
98
|
+
expect(order).toEqual(['c', 'b', 'a']);
|
|
99
|
+
|
|
100
|
+
p.destroy();
|
|
101
|
+
expect(order).toEqual(['c', 'b', 'a']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('destroy is idempotent for signal connections too', () => {
|
|
105
|
+
const p = new TestPlugin();
|
|
106
|
+
const s = new Signal<() => void>();
|
|
107
|
+
const handler = vi.fn();
|
|
108
|
+
p.addSignalConnection(s.connect(handler));
|
|
109
|
+
|
|
110
|
+
p.destroy();
|
|
111
|
+
expect(() => p.destroy()).not.toThrow();
|
|
112
|
+
s.emit();
|
|
113
|
+
expect(handler).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a throwing disposer does not stop the rest, and is reported', () => {
|
|
117
|
+
const errorSpy = vi.spyOn(Logger, 'error').mockImplementation(() => undefined);
|
|
118
|
+
const p = new TestPlugin();
|
|
119
|
+
const first = vi.fn();
|
|
120
|
+
const last = vi.fn();
|
|
121
|
+
|
|
122
|
+
p.addDisposer(first);
|
|
123
|
+
p.addDisposer(() => {
|
|
124
|
+
throw new Error('boom');
|
|
125
|
+
});
|
|
126
|
+
p.addDisposer(last);
|
|
127
|
+
|
|
128
|
+
expect(() => p.destroy()).not.toThrow();
|
|
129
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(last).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('listen() attaches now and detaches on destroy', () => {
|
|
135
|
+
const p = new TestPlugin();
|
|
136
|
+
const target = document.createElement('div');
|
|
137
|
+
const handler = vi.fn();
|
|
138
|
+
|
|
139
|
+
p.listen(target, 'click', handler);
|
|
140
|
+
target.dispatchEvent(new Event('click'));
|
|
141
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
142
|
+
|
|
143
|
+
p.destroy();
|
|
144
|
+
target.dispatchEvent(new Event('click'));
|
|
145
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('listen() detaches capture-phase listeners with matching options', () => {
|
|
149
|
+
const p = new TestPlugin();
|
|
150
|
+
const parent = document.createElement('div');
|
|
151
|
+
const child = document.createElement('div');
|
|
152
|
+
parent.appendChild(child);
|
|
153
|
+
document.body.appendChild(parent);
|
|
154
|
+
const handler = vi.fn();
|
|
155
|
+
|
|
156
|
+
p.listen(parent, 'click', handler, true);
|
|
157
|
+
child.dispatchEvent(new Event('click', { bubbles: true }));
|
|
158
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
p.destroy();
|
|
161
|
+
child.dispatchEvent(new Event('click', { bubbles: true }));
|
|
162
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
163
|
+
|
|
164
|
+
parent.remove();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('listen() returns an early-removal fn that is safe to call twice', () => {
|
|
168
|
+
const p = new TestPlugin();
|
|
169
|
+
const target = document.createElement('div');
|
|
170
|
+
const handler = vi.fn();
|
|
171
|
+
|
|
172
|
+
const remove = p.listen(target, 'click', handler);
|
|
173
|
+
remove();
|
|
174
|
+
remove();
|
|
175
|
+
target.dispatchEvent(new Event('click'));
|
|
176
|
+
expect(handler).not.toHaveBeenCalled();
|
|
177
|
+
expect(() => p.destroy()).not.toThrow();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('addTickerCallback() adds to the app ticker and removes on destroy', () => {
|
|
181
|
+
const p = new TestPlugin();
|
|
182
|
+
const fn = vi.fn();
|
|
183
|
+
|
|
184
|
+
p.addTickerCallback(fn, p, 5);
|
|
185
|
+
expect(mockTicker.add).toHaveBeenCalledWith(fn, p, 5);
|
|
186
|
+
|
|
187
|
+
p.destroy();
|
|
188
|
+
expect(mockTicker.remove).toHaveBeenCalledWith(fn, p);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('addTickerCallback() returns an early-removal fn that only removes once', () => {
|
|
192
|
+
const p = new TestPlugin();
|
|
193
|
+
const fn = vi.fn();
|
|
194
|
+
|
|
195
|
+
const remove = p.addTickerCallback(fn);
|
|
196
|
+
remove();
|
|
197
|
+
remove();
|
|
198
|
+
p.destroy();
|
|
199
|
+
expect(mockTicker.remove).toHaveBeenCalledTimes(1);
|
|
200
|
+
});
|
|
201
|
+
});
|
package/src/plugins/Plugin.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { Ticker } from 'pixi.js';
|
|
1
2
|
import type { IApplication, ICoreFunctions, ICoreSignals } from '../core';
|
|
2
3
|
import { coreFunctionRegistry, coreSignalRegistry } from '../core';
|
|
3
4
|
import { Application } from '../core/Application';
|
|
4
5
|
import { SignalConnection, SignalConnections } from '../signals';
|
|
5
|
-
import { type AppTypeOverrides, bindAllMethods, ImportListItemModule } from '../utils';
|
|
6
|
+
import { type AppTypeOverrides, bindAllMethods, ImportListItemModule, Logger } from '../utils';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Public contract for a Caper plugin. A plugin is a long-lived object
|
|
@@ -25,7 +26,40 @@ import { type AppTypeOverrides, bindAllMethods, ImportListItemModule } from '../
|
|
|
25
26
|
* methods are called from scenes / other plugins / the app.
|
|
26
27
|
* 5. `destroy()` — called when the app shuts down. Tear down
|
|
27
28
|
* connections, disconnect signals, free resources. The base class
|
|
28
|
-
* already disconnects everything added via `addSignalConnection
|
|
29
|
+
* already disconnects everything added via `addSignalConnection`
|
|
30
|
+
* and runs everything registered via the cleanup primitives below.
|
|
31
|
+
*
|
|
32
|
+
* **Cleanup primitives.** Don't hand-roll matching add/remove pairs —
|
|
33
|
+
* register the resource through the base class and `destroy()` releases
|
|
34
|
+
* it for you. Overriding `destroy()` is still fine; just call
|
|
35
|
+
* `super.destroy()`.
|
|
36
|
+
*
|
|
37
|
+
* - `addSignalConnection(...)` — signal connections.
|
|
38
|
+
* - `listen(target, type, handler, options?)` — DOM listeners. Adds
|
|
39
|
+
* now, removes on destroy with the same capture/options semantics.
|
|
40
|
+
* - `addTickerCallback(fn, context?, priority?)` — ticker callbacks on
|
|
41
|
+
* `app.ticker`. Added now, removed on destroy.
|
|
42
|
+
* - `addDisposer(...fns)` — anything else (DOM nodes, timers, third
|
|
43
|
+
* party handles). Disposers run last-in-first-out on destroy, each
|
|
44
|
+
* isolated so one failure can't skip the others.
|
|
45
|
+
*
|
|
46
|
+
* `listen` and `addTickerCallback` also return a removal function, for
|
|
47
|
+
* the rarer case where you need to detach before the plugin dies. It's
|
|
48
|
+
* safe to call more than once.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* public initialize(): void {
|
|
53
|
+
* this.listen(window, 'resize', this._onResize);
|
|
54
|
+
* this.addTickerCallback(this._update);
|
|
55
|
+
* this.addDisposer(() => this._el.remove());
|
|
56
|
+
* }
|
|
57
|
+
* // no destroy() override needed — the base class cleans all three up
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Listeners that get attached and detached repeatedly at runtime (an
|
|
61
|
+
* activate/deactivate cycle, say) should stay manual — the primitives
|
|
62
|
+
* are for resources whose lifetime matches the plugin's.
|
|
29
63
|
*
|
|
30
64
|
* Discovery: plugin classes under `src/plugins/` are auto-discovered
|
|
31
65
|
* by the Vite plugin if they default-export a class. Annotate with
|
|
@@ -79,7 +113,11 @@ export interface IPlugin<O = any> {
|
|
|
79
113
|
*/
|
|
80
114
|
postInitialize(_app: IApplication): Promise<void> | void;
|
|
81
115
|
|
|
82
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Tear down. Called on app shutdown. Runs every registered disposer
|
|
118
|
+
* (last-in-first-out) and disconnects every tracked signal connection.
|
|
119
|
+
* Safe to call more than once — the second call is a no-op.
|
|
120
|
+
*/
|
|
83
121
|
destroy(): void;
|
|
84
122
|
|
|
85
123
|
/**
|
|
@@ -91,6 +129,40 @@ export interface IPlugin<O = any> {
|
|
|
91
129
|
/** Disconnect every connection added via `addSignalConnection`. */
|
|
92
130
|
clearSignalConnections(): void;
|
|
93
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Register cleanup callbacks to run on `destroy`. Use for resources
|
|
134
|
+
* the other primitives don't cover — DOM nodes, timers, third-party
|
|
135
|
+
* handles.
|
|
136
|
+
*
|
|
137
|
+
* Disposers run last-in-first-out (so teardown mirrors setup), and each
|
|
138
|
+
* one is isolated: a throwing disposer is reported and the rest still
|
|
139
|
+
* run.
|
|
140
|
+
*/
|
|
141
|
+
addDisposer(...fns: Array<() => void>): void;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Add a DOM event listener now and remove it on `destroy`, with the
|
|
145
|
+
* same capture/options semantics it was added with.
|
|
146
|
+
*
|
|
147
|
+
* @returns A removal function, for the rarer case where the listener
|
|
148
|
+
* must come off before the plugin dies. Safe to call more than once.
|
|
149
|
+
*/
|
|
150
|
+
listen(
|
|
151
|
+
target: EventTarget,
|
|
152
|
+
type: string,
|
|
153
|
+
handler: EventListenerOrEventListenerObject,
|
|
154
|
+
options?: AddEventListenerOptions | boolean,
|
|
155
|
+
): () => void;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Add a callback to `app.ticker` now and remove it on `destroy`. The
|
|
159
|
+
* ticker is resolved when this is called, not at construction.
|
|
160
|
+
*
|
|
161
|
+
* @returns A removal function, for the rarer case where the callback
|
|
162
|
+
* must come off before the plugin dies. Safe to call more than once.
|
|
163
|
+
*/
|
|
164
|
+
addTickerCallback(fn: (ticker: Ticker) => void, context?: unknown, priority?: number): () => void;
|
|
165
|
+
|
|
94
166
|
/** Register methods this plugin exposes via the core function registry. */
|
|
95
167
|
registerCoreFunctions(): void;
|
|
96
168
|
|
|
@@ -117,6 +189,8 @@ export class Plugin<O = any> implements IPlugin<O> {
|
|
|
117
189
|
private static readonly __caper_method_binding_root = true;
|
|
118
190
|
// A collection of signal connections.
|
|
119
191
|
protected _signalConnections: SignalConnections = new SignalConnections();
|
|
192
|
+
// Cleanup callbacks registered via addDisposer / listen / addTickerCallback.
|
|
193
|
+
protected _disposers: Array<() => void> = [];
|
|
120
194
|
|
|
121
195
|
protected _options: O;
|
|
122
196
|
|
|
@@ -132,7 +206,25 @@ export class Plugin<O = any> implements IPlugin<O> {
|
|
|
132
206
|
return Application.getInstance();
|
|
133
207
|
}
|
|
134
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Tear down the plugin. Runs every registered disposer (last-in-first-out,
|
|
211
|
+
* each isolated so one failure can't skip the others), then disconnects
|
|
212
|
+
* every tracked signal connection. Idempotent.
|
|
213
|
+
*
|
|
214
|
+
* Subclasses may override this, but must call `super.destroy()`.
|
|
215
|
+
*/
|
|
135
216
|
public destroy(): void {
|
|
217
|
+
// swap the list out first, so a disposer that (indirectly) re-enters
|
|
218
|
+
// destroy can't run anything twice
|
|
219
|
+
const disposers = this._disposers;
|
|
220
|
+
this._disposers = [];
|
|
221
|
+
for (let i = disposers.length - 1; i >= 0; i--) {
|
|
222
|
+
try {
|
|
223
|
+
disposers[i]();
|
|
224
|
+
} catch (e) {
|
|
225
|
+
Logger.error(`Plugin "${this.id}" threw while running a disposer:`, e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
136
228
|
this._signalConnections.disconnectAll();
|
|
137
229
|
}
|
|
138
230
|
|
|
@@ -164,6 +256,70 @@ export class Plugin<O = any> implements IPlugin<O> {
|
|
|
164
256
|
this._signalConnections.disconnectAll();
|
|
165
257
|
}
|
|
166
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Register cleanup callbacks to run on `destroy`, last-in-first-out.
|
|
261
|
+
* Use for resources the other primitives don't cover — DOM nodes,
|
|
262
|
+
* timers, third-party handles.
|
|
263
|
+
* @param fns - The cleanup callbacks to register.
|
|
264
|
+
*/
|
|
265
|
+
public addDisposer(...fns: Array<() => void>): void {
|
|
266
|
+
for (const fn of fns) {
|
|
267
|
+
this._disposers.push(fn);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Add a DOM event listener now and remove it on `destroy`, with the same
|
|
273
|
+
* capture/options semantics it was added with.
|
|
274
|
+
* @param target - The event target to listen on.
|
|
275
|
+
* @param type - The event type.
|
|
276
|
+
* @param handler - The listener.
|
|
277
|
+
* @param options - Passed through to both add and remove, so capture-phase
|
|
278
|
+
* listeners detach correctly.
|
|
279
|
+
* @returns A removal function for early detachment. Safe to call more than once.
|
|
280
|
+
*/
|
|
281
|
+
public listen(
|
|
282
|
+
target: EventTarget,
|
|
283
|
+
type: string,
|
|
284
|
+
handler: EventListenerOrEventListenerObject,
|
|
285
|
+
options?: AddEventListenerOptions | boolean,
|
|
286
|
+
): () => void {
|
|
287
|
+
target.addEventListener(type, handler, options);
|
|
288
|
+
let removed = false;
|
|
289
|
+
const remove = () => {
|
|
290
|
+
if (removed) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
removed = true;
|
|
294
|
+
target.removeEventListener(type, handler, options);
|
|
295
|
+
};
|
|
296
|
+
this.addDisposer(remove);
|
|
297
|
+
return remove;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Add a callback to `app.ticker` now and remove it on `destroy`. The ticker
|
|
302
|
+
* is resolved when this is called, not at construction.
|
|
303
|
+
* @param fn - The ticker callback.
|
|
304
|
+
* @param context - The context to invoke the callback with.
|
|
305
|
+
* @param priority - Pixi `UPDATE_PRIORITY` for the callback.
|
|
306
|
+
* @returns A removal function for early detachment. Safe to call more than once.
|
|
307
|
+
*/
|
|
308
|
+
public addTickerCallback(fn: (ticker: Ticker) => void, context?: unknown, priority?: number): () => void {
|
|
309
|
+
const ticker = this.app.ticker;
|
|
310
|
+
ticker.add(fn, context, priority);
|
|
311
|
+
let removed = false;
|
|
312
|
+
const remove = () => {
|
|
313
|
+
if (removed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
removed = true;
|
|
317
|
+
ticker.remove(fn, context);
|
|
318
|
+
};
|
|
319
|
+
this.addDisposer(remove);
|
|
320
|
+
return remove;
|
|
321
|
+
}
|
|
322
|
+
|
|
167
323
|
/**
|
|
168
324
|
* @override
|
|
169
325
|
* @protected
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockApp = vi.hoisted(() => ({
|
|
4
|
+
ticker: { addOnce: (cb: () => void) => cb() },
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
// PopupManagerPlugin transitively imports Application → Pixi display graph. Stub it.
|
|
8
|
+
vi.mock('../core', () => ({
|
|
9
|
+
coreFunctionRegistry: {},
|
|
10
|
+
coreSignalRegistry: {},
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('../core/Application', () => ({
|
|
13
|
+
Application: { getInstance: () => mockApp },
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Evaluating the table registers it into mixins/factory/defaults, which the
|
|
17
|
+
// plugin's `view` Container reads lazily on construction.
|
|
18
|
+
import '../mixins/factory/const';
|
|
19
|
+
import { PopupManagerPlugin } from './PopupManagerPlugin';
|
|
20
|
+
|
|
21
|
+
function makeFakePopup() {
|
|
22
|
+
return {
|
|
23
|
+
beforeHide: vi.fn(),
|
|
24
|
+
hide: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
end: vi.fn(),
|
|
26
|
+
restoreActionContext: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('PopupManagerPlugin removeAllPopups', () => {
|
|
31
|
+
let plugin: PopupManagerPlugin;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
plugin = new PopupManagerPlugin();
|
|
35
|
+
vi.spyOn(plugin.view, 'removeChild').mockReturnValue(undefined as never);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('clears active popups and the current id when animating', async () => {
|
|
39
|
+
const first = makeFakePopup();
|
|
40
|
+
const second = makeFakePopup();
|
|
41
|
+
(plugin as any)._activePopups.set('first', first);
|
|
42
|
+
(plugin as any)._activePopups.set('second', second);
|
|
43
|
+
(plugin as any)._currentPopupId = 'second';
|
|
44
|
+
|
|
45
|
+
plugin.removeAllPopups(true);
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
47
|
+
|
|
48
|
+
expect(first.hide).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(second.hide).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(plugin.hasActivePopups).toBe(false);
|
|
51
|
+
expect(plugin.currentPopupId).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -179,9 +179,11 @@ export class PopupManagerPlugin extends Plugin implements IPopupManagerPlugin {
|
|
|
179
179
|
*/
|
|
180
180
|
removeAllPopups(animate: boolean = false): void {
|
|
181
181
|
if (animate) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
// reuse the single-popup hide path so the view, the active map and the
|
|
183
|
+
// current id get cleaned up once each hide animation resolves
|
|
184
|
+
for (const id of [...this._activePopups.keys()]) {
|
|
185
|
+
void this.hidePopup(id);
|
|
186
|
+
}
|
|
185
187
|
} else {
|
|
186
188
|
this._activePopups.clear();
|
|
187
189
|
this.view.removeChildren();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Signal } from '../signals';
|
|
4
|
+
|
|
5
|
+
const mockApp = vi.hoisted(() => ({
|
|
6
|
+
config: {} as Record<string, unknown>,
|
|
7
|
+
onPause: null as any,
|
|
8
|
+
onResume: null as any,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// SceneManagerPlugin transitively imports Application → Pixi display graph. Stub it.
|
|
12
|
+
vi.mock('../core', () => ({
|
|
13
|
+
coreFunctionRegistry: {},
|
|
14
|
+
coreSignalRegistry: {},
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('../core/Application', () => ({
|
|
17
|
+
Application: { getInstance: () => mockApp, containerElement: null },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { SceneManagerPlugin } from './SceneManagerPlugin';
|
|
21
|
+
|
|
22
|
+
describe('SceneManagerPlugin destroy', () => {
|
|
23
|
+
let plugin: SceneManagerPlugin;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockApp.config = { showSceneDebugMenu: false, useHash: true };
|
|
27
|
+
mockApp.onPause = new Signal<(config: any) => void>();
|
|
28
|
+
mockApp.onResume = new Signal<(config: any) => void>();
|
|
29
|
+
(globalThis as any).Caper = { get: () => [] };
|
|
30
|
+
plugin = new SceneManagerPlugin();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
delete (globalThis as any).Caper;
|
|
35
|
+
window.location.hash = '';
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('stops responding to hashchange after destroy', async () => {
|
|
39
|
+
await plugin.initialize({}, mockApp as never);
|
|
40
|
+
const spy = vi.spyOn(plugin, 'getSceneFromHash').mockReturnValue(null);
|
|
41
|
+
|
|
42
|
+
window.dispatchEvent(new Event('hashchange'));
|
|
43
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
44
|
+
|
|
45
|
+
plugin.destroy();
|
|
46
|
+
window.dispatchEvent(new Event('hashchange'));
|
|
47
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('stops forwarding pause/resume to the current scene after destroy', async () => {
|
|
51
|
+
await plugin.initialize({}, mockApp as never);
|
|
52
|
+
const onPause = vi.fn();
|
|
53
|
+
const onResume = vi.fn();
|
|
54
|
+
plugin.currentScene = { onPause, onResume } as never;
|
|
55
|
+
|
|
56
|
+
mockApp.onPause.emit({});
|
|
57
|
+
mockApp.onResume.emit({});
|
|
58
|
+
expect(onPause).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(onResume).toHaveBeenCalledTimes(1);
|
|
60
|
+
|
|
61
|
+
plugin.destroy();
|
|
62
|
+
mockApp.onPause.emit({});
|
|
63
|
+
mockApp.onResume.emit({});
|
|
64
|
+
expect(onPause).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(onResume).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('removes the debug menu from the DOM after destroy', async () => {
|
|
69
|
+
mockApp.config = { showSceneDebugMenu: true };
|
|
70
|
+
await plugin.initialize({}, mockApp as never);
|
|
71
|
+
expect(document.getElementById('scene-debug')).not.toBeNull();
|
|
72
|
+
|
|
73
|
+
plugin.destroy();
|
|
74
|
+
expect(document.getElementById('scene-debug')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -139,8 +139,6 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
139
139
|
this._defaultLoadMethod = method;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
public destroy(): void {}
|
|
143
|
-
|
|
144
142
|
public async initialize(_options: any, app: IApplication): Promise<void> {
|
|
145
143
|
this._debugVisible =
|
|
146
144
|
this.app.config?.showSceneDebugMenu === true || (isDev && this.app.config?.showSceneDebugMenu !== false);
|
|
@@ -179,8 +177,10 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
179
177
|
this._listenForHashChange();
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
this.
|
|
183
|
-
|
|
180
|
+
this.addSignalConnection(
|
|
181
|
+
this.app.onPause.connect(this._onPause, 'highest'),
|
|
182
|
+
this.app.onResume.connect(this._onResume, 'highest'),
|
|
183
|
+
);
|
|
184
184
|
|
|
185
185
|
return Promise.resolve(undefined);
|
|
186
186
|
}
|
|
@@ -368,12 +368,14 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
private _listenForHashChange() {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
371
|
+
this.listen(window, 'hashchange', this._onHashChange);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private _onHashChange() {
|
|
375
|
+
const sceneId = this.getSceneFromHash();
|
|
376
|
+
if (sceneId) {
|
|
377
|
+
void this.loadScene(sceneId);
|
|
378
|
+
}
|
|
377
379
|
}
|
|
378
380
|
|
|
379
381
|
private async _createCurrentScene() {
|
|
@@ -569,6 +571,7 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
569
571
|
this._debugMenu.appendChild(icon);
|
|
570
572
|
|
|
571
573
|
(Application.containerElement || document.body).appendChild(this._debugMenu);
|
|
574
|
+
this.addDisposer(() => this._debugMenu?.parentElement?.removeChild(this._debugMenu));
|
|
572
575
|
|
|
573
576
|
this._sceneSelect = document.createElement('select');
|
|
574
577
|
this._sceneSelect.style.cssText =
|
|
@@ -643,7 +646,7 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
643
646
|
|
|
644
647
|
this._debugMenu.appendChild(this._sceneSelect);
|
|
645
648
|
|
|
646
|
-
this._debugMenu
|
|
649
|
+
this.listen(this._debugMenu, 'change', (e: Event) => {
|
|
647
650
|
if (this._queue) {
|
|
648
651
|
e.preventDefault();
|
|
649
652
|
return;
|
|
@@ -655,8 +658,10 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
655
658
|
}
|
|
656
659
|
});
|
|
657
660
|
|
|
658
|
-
this.
|
|
659
|
-
|
|
661
|
+
this.addSignalConnection(
|
|
662
|
+
this.onSceneChangeStart.connect(this._disableDebugMenu),
|
|
663
|
+
this.onSceneChangeComplete.connect(this._enableDebugMenu),
|
|
664
|
+
);
|
|
660
665
|
}
|
|
661
666
|
|
|
662
667
|
private _enableDebugMenu() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UPDATE_PRIORITY } from 'pixi.js';
|
|
2
2
|
import Stats from 'stats.js';
|
|
3
3
|
import { Application } from '../core/Application';
|
|
4
4
|
import { Plugin } from './Plugin';
|
|
@@ -12,6 +12,7 @@ export class StatsPlugin extends Plugin {
|
|
|
12
12
|
this.stats.dom.id = 'stats';
|
|
13
13
|
|
|
14
14
|
Application.containerElement?.appendChild(this.stats.dom);
|
|
15
|
+
this.addDisposer(() => this.stats.dom.remove());
|
|
15
16
|
|
|
16
17
|
this.stats.dom.style.position = 'absolute';
|
|
17
18
|
this.stats.dom.style.top = 'auto';
|
|
@@ -19,6 +20,6 @@ export class StatsPlugin extends Plugin {
|
|
|
19
20
|
this.stats.dom.style.right = '0';
|
|
20
21
|
this.stats.dom.style.left = 'auto';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
this.addTickerCallback(this.stats.update, this.stats, UPDATE_PRIORITY.UTILITY);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -676,7 +676,7 @@ export class TimerPlugin extends Plugin implements ITimerPlugin {
|
|
|
676
676
|
|
|
677
677
|
public postInitialize() {
|
|
678
678
|
// Set up update loop for main thread timers only
|
|
679
|
-
this.
|
|
679
|
+
this.addTickerCallback(this.update);
|
|
680
680
|
|
|
681
681
|
// Initialize worker if needed
|
|
682
682
|
this.initWorker();
|
|
@@ -763,7 +763,6 @@ export class TimerPlugin extends Plugin implements ITimerPlugin {
|
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
public destroy(): void {
|
|
766
|
-
this.app.ticker.remove(this.update);
|
|
767
766
|
this.worker?.terminate();
|
|
768
767
|
this.worker = null;
|
|
769
768
|
this.timerCallbacks.clear();
|
|
@@ -9,6 +9,7 @@ vi.mock('../core/Application', () => ({
|
|
|
9
9
|
Application: { getInstance: () => ({ renderer: { canvas: null } }) },
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
+
import { Signal } from '../signals';
|
|
12
13
|
import { WebEventsPlugin } from './WebEventsPlugin';
|
|
13
14
|
|
|
14
15
|
type FakeVisualViewport = EventTarget & { scale: number };
|
|
@@ -73,3 +74,34 @@ describe('WebEventsPlugin visualViewport resize', () => {
|
|
|
73
74
|
expect(() => plugin.initialize()).not.toThrow();
|
|
74
75
|
});
|
|
75
76
|
});
|
|
77
|
+
|
|
78
|
+
describe('WebEventsPlugin destroy', () => {
|
|
79
|
+
let plugin: WebEventsPlugin;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
plugin = new WebEventsPlugin();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('stops handling orientationchange after destroy', () => {
|
|
86
|
+
const spy = vi.spyOn(plugin as any, '_onOrientationChanged');
|
|
87
|
+
plugin.initialize();
|
|
88
|
+
|
|
89
|
+
window.dispatchEvent(new Event('orientationchange'));
|
|
90
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
91
|
+
|
|
92
|
+
plugin.destroy();
|
|
93
|
+
window.dispatchEvent(new Event('orientationchange'));
|
|
94
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('disconnects tracked signal connections on destroy', () => {
|
|
98
|
+
const signal = new Signal<() => void>();
|
|
99
|
+
const handler = vi.fn();
|
|
100
|
+
plugin.addSignalConnection(signal.connect(handler));
|
|
101
|
+
|
|
102
|
+
plugin.destroy();
|
|
103
|
+
signal.emit();
|
|
104
|
+
|
|
105
|
+
expect(handler).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
});
|