@caperjs/core 0.2.3 → 0.3.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.
Files changed (175) hide show
  1. package/build/index.mjs +3 -1
  2. package/build/internal/discovery.mjs +1 -1
  3. package/build/internal/manifest.mjs +8 -3
  4. package/build/internal/validate.mjs +3 -1
  5. package/build/plugins/assetTypes.mjs +4 -4
  6. package/build/plugins/caperConfig.mjs +12 -5
  7. package/build/plugins/lists.mjs +29 -9
  8. package/cli/create.mjs +57 -6
  9. package/cli/create.test.mjs +36 -0
  10. package/cli/update.mjs +1 -1
  11. package/lib/{CaptionsPlugin-WPAcfAkx.js → CaptionsPlugin-BLbLl3WB.js} +3 -3
  12. package/lib/{CaptionsPlugin-WPAcfAkx.js.map → CaptionsPlugin-BLbLl3WB.js.map} +1 -1
  13. package/lib/{DataAdapter-DS-twowe.js → DataAdapter-aHSNM_26.js} +12 -7
  14. package/lib/DataAdapter-aHSNM_26.js.map +1 -0
  15. package/lib/{DebugRenderer-D1ryrRYx.js → DebugRenderer-8LjHtBzA.js} +2 -2
  16. package/lib/{DebugRenderer-D1ryrRYx.js.map → DebugRenderer-8LjHtBzA.js.map} +1 -1
  17. package/lib/{DevToolsPlugin-DH7zX7HU.js → DevToolsPlugin-Vrk6aOtS.js} +2 -2
  18. package/lib/{DevToolsPlugin-DH7zX7HU.js.map → DevToolsPlugin-Vrk6aOtS.js.map} +1 -1
  19. package/lib/{GSAPPlugin-BBtRjUqe.js → GSAPPlugin-Hm3F4zdg.js} +3 -3
  20. package/lib/{GSAPPlugin-BBtRjUqe.js.map → GSAPPlugin-Hm3F4zdg.js.map} +1 -1
  21. package/lib/{LayoutPlugin-Co6vcoV4.js → LayoutPlugin-Dpgeawfw.js} +4 -4
  22. package/lib/{LayoutPlugin-Co6vcoV4.js.map → LayoutPlugin-Dpgeawfw.js.map} +1 -1
  23. package/lib/{SpinePlugin-uN641Hmk.js → SpinePlugin-B08yAw1S.js} +690 -793
  24. package/lib/SpinePlugin-B08yAw1S.js.map +1 -0
  25. package/lib/{StatsPlugin-vZQRm6wh.js → StatsPlugin-D2Xa5j97.js} +5 -2
  26. package/lib/StatsPlugin-D2Xa5j97.js.map +1 -0
  27. package/lib/{VoiceOverPlugin-KlZdGj1G.js → VoiceOverPlugin-qtFlqpRn.js} +6 -5
  28. package/lib/VoiceOverPlugin-qtFlqpRn.js.map +1 -0
  29. package/lib/caper.mjs +37 -19
  30. package/lib/caper.mjs.map +1 -1
  31. package/lib/core/Application.d.ts +8 -3
  32. package/lib/core/Application.d.ts.map +1 -1
  33. package/lib/core/Application.test.d.ts +2 -0
  34. package/lib/core/Application.test.d.ts.map +1 -0
  35. package/lib/core/config.d.ts +6 -1
  36. package/lib/core/config.d.ts.map +1 -1
  37. package/lib/core/config.test.d.ts +2 -0
  38. package/lib/core/config.test.d.ts.map +1 -0
  39. package/lib/core/interfaces/IApplication.d.ts +6 -6
  40. package/lib/core/interfaces/IApplication.d.ts.map +1 -1
  41. package/lib/core/interfaces/ICoreSignals.d.ts +2 -1
  42. package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
  43. package/lib/display/Camera.test.d.ts +2 -0
  44. package/lib/display/Camera.test.d.ts.map +1 -0
  45. package/lib/display/Entity.d.ts +2 -2
  46. package/lib/display/SceneTransition.test.d.ts +1 -0
  47. package/lib/display/SceneTransition.test.d.ts.map +1 -0
  48. package/lib/display/SpineAnimation.d.ts +3 -3
  49. package/lib/mixins/factory/methods.d.ts.map +1 -1
  50. package/lib/plugins/AssetsPlugin.d.ts.map +1 -1
  51. package/lib/plugins/AssetsPlugin.test.d.ts +2 -0
  52. package/lib/plugins/AssetsPlugin.test.d.ts.map +1 -0
  53. package/lib/plugins/DataAdapter.d.ts.map +1 -1
  54. package/lib/plugins/DataAdapter.test.d.ts +2 -0
  55. package/lib/plugins/DataAdapter.test.d.ts.map +1 -0
  56. package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
  57. package/lib/plugins/FullScreenPlugin.test.d.ts +2 -0
  58. package/lib/plugins/FullScreenPlugin.test.d.ts.map +1 -0
  59. package/lib/plugins/GSAPPlugin.d.ts +0 -2
  60. package/lib/plugins/GSAPPlugin.d.ts.map +1 -1
  61. package/lib/plugins/GSAPPlugin.test.d.ts +2 -0
  62. package/lib/plugins/GSAPPlugin.test.d.ts.map +1 -0
  63. package/lib/plugins/LookupPlugin.d.ts +6 -1
  64. package/lib/plugins/LookupPlugin.d.ts.map +1 -1
  65. package/lib/plugins/LookupPlugin.test.d.ts +2 -0
  66. package/lib/plugins/LookupPlugin.test.d.ts.map +1 -0
  67. package/lib/plugins/PopupManagerPlugin.d.ts.map +1 -1
  68. package/lib/plugins/PopupManagerPlugin.test.d.ts +1 -0
  69. package/lib/plugins/PopupManagerPlugin.test.d.ts.map +1 -0
  70. package/lib/plugins/SceneManagerPlugin.d.ts +1 -0
  71. package/lib/plugins/SceneManagerPlugin.d.ts.map +1 -1
  72. package/lib/plugins/SceneManagerPlugin.test.d.ts +2 -0
  73. package/lib/plugins/SceneManagerPlugin.test.d.ts.map +1 -0
  74. package/lib/plugins/StatsPlugin.d.ts +1 -0
  75. package/lib/plugins/StatsPlugin.d.ts.map +1 -1
  76. package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
  77. package/lib/plugins/actions/ActionsPlugin.test.d.ts +2 -0
  78. package/lib/plugins/actions/ActionsPlugin.test.d.ts.map +1 -0
  79. package/lib/plugins/audio/AudioChannel.d.ts.map +1 -1
  80. package/lib/plugins/audio/AudioManagerPlugin.d.ts.map +1 -1
  81. package/lib/plugins/audio/AudioManagerPlugin.test.d.ts +2 -0
  82. package/lib/plugins/audio/AudioManagerPlugin.test.d.ts.map +1 -0
  83. package/lib/plugins/audio/VoiceOverPlugin.d.ts.map +1 -1
  84. package/lib/plugins/captions/CaptionsPlugin.test.d.ts +2 -0
  85. package/lib/plugins/captions/CaptionsPlugin.test.d.ts.map +1 -0
  86. package/lib/plugins/focus/FocusManagerPlugin.d.ts.map +1 -1
  87. package/lib/plugins/focus/FocusManagerPlugin.test.d.ts +2 -0
  88. package/lib/plugins/focus/FocusManagerPlugin.test.d.ts.map +1 -0
  89. package/lib/plugins/input/AbstractControls.d.ts +5 -0
  90. package/lib/plugins/input/AbstractControls.d.ts.map +1 -1
  91. package/lib/plugins/input/Controls.test.d.ts +2 -0
  92. package/lib/plugins/input/Controls.test.d.ts.map +1 -0
  93. package/lib/plugins/input/keyboard/KeyboardContols.d.ts +5 -0
  94. package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +1 -1
  95. package/lib/plugins/input/touch/VirtualControls.d.ts +6 -0
  96. package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
  97. package/lib/plugins/input/touch/VirtualControls.test.d.ts +2 -0
  98. package/lib/plugins/input/touch/VirtualControls.test.d.ts.map +1 -0
  99. package/lib/{registries-BLrxALZ0.js → registries-m2vKTIih.js} +156 -101
  100. package/lib/registries-m2vKTIih.js.map +1 -0
  101. package/lib/store/Store.d.ts.map +1 -1
  102. package/lib/ui/Button.d.ts +8 -1
  103. package/lib/ui/Button.d.ts.map +1 -1
  104. package/lib/ui/Button.test.d.ts +2 -0
  105. package/lib/ui/Button.test.d.ts.map +1 -0
  106. package/lib/ui/UICanvas.d.ts +10 -1
  107. package/lib/ui/UICanvas.d.ts.map +1 -1
  108. package/lib/ui/UICanvas.test.d.ts +0 -1
  109. package/lib/ui/UICanvas.test.d.ts.map +1 -1
  110. package/lib/utils/color.d.ts.map +1 -1
  111. package/lib/utils/color.test.d.ts +2 -0
  112. package/lib/utils/color.test.d.ts.map +1 -0
  113. package/package.json +1 -1
  114. package/src/core/Application.test.ts +168 -0
  115. package/src/core/Application.ts +37 -27
  116. package/src/core/config.test.ts +79 -0
  117. package/src/core/config.ts +28 -6
  118. package/src/core/interfaces/IApplication.ts +6 -6
  119. package/src/core/interfaces/ICoreSignals.ts +2 -0
  120. package/src/display/Camera.test.ts +40 -0
  121. package/src/display/Camera.ts +3 -3
  122. package/src/display/Entity.ts +2 -2
  123. package/src/display/SceneTransition.test.ts +58 -0
  124. package/src/display/SceneTransition.ts +1 -1
  125. package/src/display/SpineAnimation.ts +4 -4
  126. package/src/mixins/factory/methods.ts +0 -1
  127. package/src/plugins/AssetsPlugin.test.ts +48 -0
  128. package/src/plugins/AssetsPlugin.ts +3 -1
  129. package/src/plugins/DataAdapter.test.ts +32 -0
  130. package/src/plugins/DataAdapter.ts +5 -1
  131. package/src/plugins/FullScreenPlugin.test.ts +48 -0
  132. package/src/plugins/FullScreenPlugin.ts +5 -1
  133. package/src/plugins/GSAPPlugin.test.ts +31 -0
  134. package/src/plugins/GSAPPlugin.ts +1 -4
  135. package/src/plugins/LookupPlugin.test.ts +42 -0
  136. package/src/plugins/LookupPlugin.ts +15 -3
  137. package/src/plugins/PopupManagerPlugin.test.ts +53 -0
  138. package/src/plugins/PopupManagerPlugin.ts +5 -3
  139. package/src/plugins/SceneManagerPlugin.test.ts +76 -0
  140. package/src/plugins/SceneManagerPlugin.ts +21 -11
  141. package/src/plugins/StatsPlugin.ts +8 -0
  142. package/src/plugins/WebEventsPlugin.test.ts +32 -0
  143. package/src/plugins/WebEventsPlugin.ts +2 -0
  144. package/src/plugins/actions/ActionsPlugin.test.ts +68 -0
  145. package/src/plugins/actions/ActionsPlugin.ts +5 -5
  146. package/src/plugins/audio/AudioChannel.test.ts +19 -0
  147. package/src/plugins/audio/AudioChannel.ts +8 -1
  148. package/src/plugins/audio/AudioManagerPlugin.test.ts +41 -0
  149. package/src/plugins/audio/AudioManagerPlugin.ts +3 -2
  150. package/src/plugins/audio/VoiceOverPlugin.ts +1 -0
  151. package/src/plugins/captions/CaptionsPlugin.test.ts +40 -0
  152. package/src/plugins/captions/CaptionsPlugin.ts +1 -1
  153. package/src/plugins/focus/FocusManagerPlugin.test.ts +71 -0
  154. package/src/plugins/focus/FocusManagerPlugin.ts +7 -0
  155. package/src/plugins/input/AbstractControls.ts +6 -0
  156. package/src/plugins/input/Controls.test.ts +149 -0
  157. package/src/plugins/input/keyboard/KeyboardContols.ts +29 -5
  158. package/src/plugins/input/touch/VirtualControls.test.ts +38 -0
  159. package/src/plugins/input/touch/VirtualControls.ts +44 -10
  160. package/src/plugins/spine/SpinePlugin.ts +2 -2
  161. package/src/store/Store.test.ts +15 -0
  162. package/src/store/Store.ts +4 -1
  163. package/src/ui/Button.test.ts +248 -0
  164. package/src/ui/Button.ts +42 -10
  165. package/src/ui/UICanvas.test.ts +109 -6
  166. package/src/ui/UICanvas.ts +41 -3
  167. package/src/utils/color.test.ts +16 -0
  168. package/src/utils/color.ts +2 -1
  169. package/src/utils/platform.ts +1 -1
  170. package/src/version.ts +1 -1
  171. package/lib/DataAdapter-DS-twowe.js.map +0 -1
  172. package/lib/SpinePlugin-uN641Hmk.js.map +0 -1
  173. package/lib/StatsPlugin-vZQRm6wh.js.map +0 -1
  174. package/lib/VoiceOverPlugin-KlZdGj1G.js.map +0 -1
  175. package/lib/registries-BLrxALZ0.js.map +0 -1
@@ -0,0 +1,40 @@
1
+ import { Container as PixiContainer } from 'pixi.js';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ // Camera transitively imports Application → Pixi display graph. Stub it.
5
+ vi.mock('../core', () => ({
6
+ coreFunctionRegistry: {},
7
+ coreSignalRegistry: {},
8
+ }));
9
+
10
+ const mocks = vi.hoisted(() => ({
11
+ app: {
12
+ size: { width: 800, height: 400 },
13
+ },
14
+ }));
15
+
16
+ vi.mock('../core/Application', () => ({
17
+ Application: { getInstance: () => mocks.app },
18
+ }));
19
+
20
+ import { Camera } from './Camera';
21
+
22
+ describe('Camera targetScale', () => {
23
+ it('reflects the target scale, not the target pivot', () => {
24
+ const camera = new Camera({ container: new PixiContainer(), viewportWidth: 200, viewportHeight: 100 });
25
+
26
+ camera.zoom(3);
27
+
28
+ expect(camera.targetScale.x).toBe(3);
29
+ expect(camera.targetScale.y).toBe(3);
30
+ expect(camera.targetScale).not.toBe(camera.targetPivot);
31
+ });
32
+ });
33
+
34
+ describe('Camera viewportHeight default', () => {
35
+ it('falls back to app.size.height, not app.size.width', () => {
36
+ const camera = new Camera({ container: new PixiContainer() });
37
+
38
+ expect(camera.viewportHeight).toBe(mocks.app.size.height);
39
+ });
40
+ });
@@ -85,7 +85,7 @@ export class Camera extends Container implements ICamera {
85
85
  this.minY = config.minY;
86
86
  }
87
87
  this.viewportWidth = config.viewportWidth ?? this.app.size.width;
88
- this.viewportHeight = config.viewportHeight ?? this.app.size.width;
88
+ this.viewportHeight = config.viewportHeight ?? this.app.size.height;
89
89
  this.worldWidth = config.worldWidth ?? this.viewportWidth;
90
90
  this.worldHeight = config.worldHeight ?? this.viewportHeight;
91
91
  this.maxX = config.maxX ?? this.worldWidth - this.viewportWidth;
@@ -125,7 +125,7 @@ export class Camera extends Container implements ICamera {
125
125
  protected _targetScale: Point = new Point(1, 1);
126
126
 
127
127
  get targetScale(): Point {
128
- return this._targetPivot;
128
+ return this._targetScale;
129
129
  }
130
130
 
131
131
  private _lerp: number = 0;
@@ -135,7 +135,7 @@ export class Camera extends Container implements ICamera {
135
135
  }
136
136
 
137
137
  set lerp(value: number) {
138
- // if the value is less than 0 or greater than 1, clamp it to the range [0, 1], and log an error
138
+ // if the value is less than 0 or greater than 1, throw an error
139
139
  if (value < 0 || value > 1) {
140
140
  throw new Error('Camera lerp value must be in the range [0, 1]');
141
141
  }
@@ -9,8 +9,8 @@ import { Container } from './Container';
9
9
  *
10
10
  * **Lifecycle** (inherited from Container):
11
11
  *
12
- * 1. `constructor(props)` — props are stashed on `this.props` before
13
- * Container's constructor runs. Don't reference `this.app` yet.
12
+ * 1. `constructor(props)` — Container's constructor runs first, then props
13
+ * are stashed on `this.props`. Don't reference `this.app` yet.
14
14
  * 2. **addChild** — the factory auto-adds the instance to the calling
15
15
  * Container; Pixi emits an `added` event.
16
16
  * 3. `added()` — override this to build the display tree using
@@ -0,0 +1,58 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ // Registers the factory method table `Factory()` (part of Container's mixin
3
+ // chain) reads from — must be imported before any display/ui class is
4
+ // constructed. See importOrder.containerFirst.test.ts.
5
+ import '../mixins/factory/const';
6
+ import { Signal } from '../signals';
7
+ import { SceneTransition } from './SceneTransition';
8
+
9
+ describe('SceneTransition asset signal wiring', () => {
10
+ let app: {
11
+ assets: {
12
+ onLoadStart: Signal<() => void>;
13
+ onLoadProgress: Signal<(progress: number) => void>;
14
+ onLoadComplete: Signal<() => void>;
15
+ };
16
+ };
17
+ let originalAppDescriptor: PropertyDescriptor | undefined;
18
+
19
+ beforeEach(() => {
20
+ app = {
21
+ assets: {
22
+ onLoadStart: new Signal<() => void>(),
23
+ onLoadProgress: new Signal<(progress: number) => void>(),
24
+ onLoadComplete: new Signal<() => void>(),
25
+ },
26
+ };
27
+ // Container.app is a getter; override it on the prototype so the
28
+ // constructor picks up the fake app before the instance even exists.
29
+ originalAppDescriptor = Object.getOwnPropertyDescriptor(SceneTransition.prototype, 'app');
30
+ Object.defineProperty(SceneTransition.prototype, 'app', { get: () => app, configurable: true });
31
+ });
32
+
33
+ afterEach(() => {
34
+ if (originalAppDescriptor) {
35
+ Object.defineProperty(SceneTransition.prototype, 'app', originalAppDescriptor);
36
+ } else {
37
+ delete (SceneTransition.prototype as any).app;
38
+ }
39
+ });
40
+
41
+ it('routes progress ticks to handleLoadProgress and completion to handleLoadComplete', () => {
42
+ const progressSpy = vi.spyOn(SceneTransition.prototype as any, 'handleLoadProgress');
43
+ const completeSpy = vi.spyOn(SceneTransition.prototype as any, 'handleLoadComplete');
44
+
45
+ const transition = new SceneTransition();
46
+ void transition;
47
+
48
+ app.assets.onLoadProgress.emit(0.5);
49
+
50
+ expect(progressSpy).toHaveBeenCalledTimes(1);
51
+ expect(completeSpy).not.toHaveBeenCalled();
52
+
53
+ app.assets.onLoadComplete.emit();
54
+
55
+ expect(completeSpy).toHaveBeenCalledTimes(1);
56
+ expect(progressSpy).toHaveBeenCalledTimes(1);
57
+ });
58
+ });
@@ -46,7 +46,7 @@ export class SceneTransition extends Container {
46
46
  this.addSignalConnection(
47
47
  this.app.assets.onLoadStart.connect(this.handleLoadStart),
48
48
  this.app.assets.onLoadProgress.connect(this.handleLoadProgress),
49
- this.app.assets.onLoadProgress.connect(this.handleLoadComplete),
49
+ this.app.assets.onLoadComplete.connect(this.handleLoadComplete),
50
50
  );
51
51
  }
52
52
 
@@ -20,8 +20,8 @@ export interface ISpineAnimation<ANames extends string = string> extends Instanc
20
20
  readonly onAnimationEvent: Signal<(entry: TrackEntry, event: Event) => void>;
21
21
  readonly onPaused: Signal<(entry: TrackEntry | null) => void>;
22
22
  readonly onResumed: Signal<(entry: TrackEntry | null) => void>;
23
- setAnimation(name: ANames, loop?: boolean, tracklndex?: number): void;
24
- getCurrentAnimation(tracklndex?: number): ANames;
23
+ setAnimation(name: ANames, loop?: boolean, trackIndex?: number): void;
24
+ getCurrentAnimation(trackIndex?: number): ANames;
25
25
  pause(): void;
26
26
  resume(): void;
27
27
  togglePause(): void;
@@ -113,8 +113,8 @@ export class SpineAnimation<ANames extends string = string> extends _SpineAnimat
113
113
  return (this.spine.state.getCurrent(trackIndex)?.animation?.name as ANames) || ('' as ANames);
114
114
  }
115
115
 
116
- setAnimation(name: ANames, loop = false, tracklndex: number = 0) {
117
- this.spine.state.setAnimation(tracklndex, name, loop);
116
+ setAnimation(name: ANames, loop = false, trackIndex: number = 0) {
117
+ this.spine.state.setAnimation(trackIndex, name, loop);
118
118
  }
119
119
 
120
120
  pause() {
@@ -12,7 +12,6 @@ export function createFactoryMethods<T extends typeof defaultFactoryMethods = ty
12
12
  const obj = methods[key](...args);
13
13
  if (addToStage) {
14
14
  instance.addChild(obj);
15
- // instance.addChild(obj);
16
15
  }
17
16
  return obj;
18
17
  };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // AssetsPlugin extends Plugin, which transitively imports Application → Pixi
4
+ // display graph. Stub it the same way Plugin.test.ts does.
5
+ vi.mock('../core', () => ({
6
+ coreFunctionRegistry: {},
7
+ coreSignalRegistry: {},
8
+ }));
9
+ vi.mock('../core/Application', () => ({
10
+ Application: { getInstance: () => ({ canvas: { dispatchEvent: vi.fn() } }) },
11
+ }));
12
+
13
+ vi.mock('pixi.js', async (importOriginal) => {
14
+ const actual = await importOriginal<typeof import('pixi.js')>();
15
+ return {
16
+ ...actual,
17
+ Assets: {
18
+ load: vi.fn(async () => undefined),
19
+ loadBundle: vi.fn(async () => undefined),
20
+ setPreferences: vi.fn(),
21
+ backgroundLoad: vi.fn(async () => undefined),
22
+ backgroundLoadBundle: vi.fn(async () => undefined),
23
+ unload: vi.fn(async () => undefined),
24
+ unloadBundle: vi.fn(async () => undefined),
25
+ },
26
+ };
27
+ });
28
+
29
+ import { Assets } from 'pixi.js';
30
+ import { AssetsPlugin } from './AssetsPlugin';
31
+
32
+ const loadBundle = vi.mocked(Assets.loadBundle);
33
+
34
+ describe('AssetsPlugin', () => {
35
+ it('loadRequired only requests bundles that are not already loaded', async () => {
36
+ const plugin = new AssetsPlugin();
37
+
38
+ // Mark bundle 'a' as already loaded.
39
+ await plugin.loadBundles('a');
40
+ loadBundle.mockClear();
41
+
42
+ plugin.initialize({ preload: { bundles: ['a', 'b'] } });
43
+ await plugin.loadRequired();
44
+
45
+ expect(loadBundle).toHaveBeenCalledTimes(1);
46
+ expect(loadBundle).toHaveBeenCalledWith(['b'], expect.anything());
47
+ });
48
+ });
@@ -187,7 +187,9 @@ export class AssetsPlugin extends Plugin<AssetLoadingOptions> implements IAssets
187
187
  bundles = [bundles];
188
188
  }
189
189
  bundles = bundles.filter((bundle) => !this._isBundleLoaded(bundle));
190
- await Assets.loadBundle(this._required.bundles, this._handleLoadProgress);
190
+ if (bundles.length) {
191
+ await Assets.loadBundle(bundles, this._handleLoadProgress);
192
+ }
191
193
  }
192
194
  }
193
195
  this._handleLoadComplete();
@@ -0,0 +1,32 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ // DataAdapter extends Plugin, which transitively imports Application → Pixi display graph. Stub it.
4
+ vi.mock('../core', () => ({
5
+ coreFunctionRegistry: {},
6
+ coreSignalRegistry: {},
7
+ }));
8
+ vi.mock('../core/Application', () => ({
9
+ Application: { getInstance: () => ({}) },
10
+ }));
11
+
12
+ import { vi } from 'vitest';
13
+ import { DataAdapter } from './DataAdapter';
14
+
15
+ describe('DataAdapter', () => {
16
+ beforeEach(() => {
17
+ localStorage.clear();
18
+ });
19
+
20
+ it('clear() with no key removes only this namespace\'s keys, leaving unrelated keys intact', () => {
21
+ localStorage.setItem('myns-foo', '"bar"');
22
+ localStorage.setItem('other-bar', '"baz"');
23
+
24
+ const adapter = new DataAdapter();
25
+ adapter.initialize({ namespace: 'myns', initial: { foo: 'bar' } });
26
+
27
+ adapter.clear();
28
+
29
+ expect(localStorage.getItem('myns-foo')).toBeNull();
30
+ expect(localStorage.getItem('other-bar')).toBe('"baz"');
31
+ });
32
+ });
@@ -103,6 +103,7 @@ export class DataAdapter extends Plugin<Partial<IDataAdapterOptions>> implements
103
103
 
104
104
  destroy() {
105
105
  this.data = {} as DataAdapterType;
106
+ super.destroy();
106
107
  }
107
108
 
108
109
  public initialize(options?: Partial<IDataAdapterOptions>): void {
@@ -205,7 +206,10 @@ export class DataAdapter extends Plugin<Partial<IDataAdapterOptions>> implements
205
206
  clear<K extends keyof DataAdapterType>(key?: K): void {
206
207
  if (key === undefined) {
207
208
  this.data = {} as DataAdapterType;
208
- localStorage.clear();
209
+ const prefix = `${this.namespace}-`;
210
+ for (const storageKey of Object.keys(localStorage).filter((k) => k.startsWith(prefix))) {
211
+ localStorage.removeItem(storageKey);
212
+ }
209
213
  this.onDataChange.emit({ key, clear: true });
210
214
  } else {
211
215
  delete this.data[key];
@@ -0,0 +1,48 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ // FullScreenPlugin transitively imports Application → Pixi display graph. Stub it.
4
+ vi.mock('../core', () => ({
5
+ coreFunctionRegistry: {},
6
+ coreSignalRegistry: {},
7
+ }));
8
+ vi.mock('../core/Application', () => ({
9
+ Application: { getInstance: () => ({}), containerElement: null },
10
+ }));
11
+
12
+ import { FullScreenPlugin } from './FullScreenPlugin';
13
+
14
+ describe('FullScreenPlugin fullscreenchange handling', () => {
15
+ let plugin: FullScreenPlugin;
16
+
17
+ beforeEach(() => {
18
+ plugin = new FullScreenPlugin();
19
+ plugin.initialize();
20
+ });
21
+
22
+ afterEach(() => {
23
+ plugin.destroy();
24
+ delete (document as any).webkitFullscreenElement;
25
+ });
26
+
27
+ it('clears the cached state when the browser leaves fullscreen', () => {
28
+ (plugin as any)._isFullScreen = true;
29
+ const spy = vi.fn();
30
+ plugin.onFullScreenChange.connect(spy);
31
+
32
+ document.dispatchEvent(new Event('fullscreenchange'));
33
+
34
+ expect(plugin.isFullScreen).toBe(false);
35
+ expect(spy).toHaveBeenCalledWith(false);
36
+ });
37
+
38
+ it('sets the cached state from vendor-prefixed fullscreen elements', () => {
39
+ (document as any).webkitFullscreenElement = document.body;
40
+ const spy = vi.fn();
41
+ plugin.onFullScreenChange.connect(spy);
42
+
43
+ document.dispatchEvent(new Event('webkitfullscreenchange'));
44
+
45
+ expect(plugin.isFullScreen).toBe(true);
46
+ expect(spy).toHaveBeenCalledWith(true);
47
+ });
48
+ });
@@ -160,6 +160,7 @@ export class FullScreenPlugin extends Plugin implements IFullScreenPlugin {
160
160
  document.removeEventListener('mozfullscreenchange', this._onFullScreenChange);
161
161
  document.removeEventListener('msfullscreenchange', this._onFullScreenChange);
162
162
  document.removeEventListener('fullscreenchange', this._onFullScreenChange);
163
+ super.destroy();
163
164
  }
164
165
 
165
166
  /**
@@ -354,6 +355,9 @@ export class FullScreenPlugin extends Plugin implements IFullScreenPlugin {
354
355
  * @private
355
356
  */
356
357
  private _onFullScreenChange(): void {
357
- this.onFullScreenChange.emit(document.fullscreenElement !== null);
358
+ // the user can leave fullscreen without us (Esc / browser chrome), so re-read
359
+ // the real state instead of trusting the cached flag
360
+ this._isFullScreen = this.isFullscreen;
361
+ this.onFullScreenChange.emit(this._isFullScreen);
358
362
  }
359
363
  }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // GSAPPlugin.ts extends Plugin.ts, which transitively imports Application → Pixi display graph. Stub it.
4
+ vi.mock('../core', () => ({
5
+ coreFunctionRegistry: {},
6
+ coreSignalRegistry: {},
7
+ }));
8
+ vi.mock('../core/Application', () => ({
9
+ Application: { getInstance: () => ({}) },
10
+ }));
11
+
12
+ import { GSAPPlugin } from './GSAPPlugin';
13
+
14
+ class TestGSAPPlugin extends GSAPPlugin {
15
+ public createGlobalContextPublic(): void {
16
+ this.createGlobalContext();
17
+ }
18
+ }
19
+
20
+ describe('GSAPPlugin', () => {
21
+ it('clearGlobal() does not throw and empties the global context', () => {
22
+ const plugin = new TestGSAPPlugin();
23
+ plugin.createGlobalContextPublic();
24
+
25
+ plugin.addAnimation({} as never);
26
+ expect(plugin.getContext('__caper_global')?.size).toBe(1);
27
+
28
+ expect(() => plugin.clearGlobal()).not.toThrow();
29
+ expect(plugin.getContext('__caper_global')?.size).toBe(0);
30
+ });
31
+ });
@@ -348,9 +348,6 @@ export class GSAPPlugin extends Plugin implements IGSAPPlugin {
348
348
  (detail: { animation: gsap.core.Tween | gsap.core.Timeline; contextId: string }) => void
349
349
  >();
350
350
 
351
- /** Global animation context reference for quick access */
352
- private _globalContext: AnimationContext;
353
-
354
351
  /** Map of all custom animation contexts by their string identifiers */
355
352
  private _animationContexts: Map<string, AnimationContext> = new Map();
356
353
 
@@ -804,7 +801,7 @@ export class GSAPPlugin extends Plugin implements IGSAPPlugin {
804
801
  if (kill) {
805
802
  this.killGlobal();
806
803
  }
807
- this._globalContext.clear();
804
+ this._animationContexts.get(GSAPPlugin.GLOBAL_CONTEXT_ID)?.clear();
808
805
 
809
806
  this.onClearGlobal.emit(kill);
810
807
  }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ // LookupPlugin -> Plugin.ts transitively imports Application → Pixi display graph. Stub it.
4
+ vi.mock('../core', () => ({
5
+ coreFunctionRegistry: {} as Record<string, unknown>,
6
+ coreSignalRegistry: {} as Record<string, unknown>,
7
+ }));
8
+ vi.mock('../core/Application', () => ({
9
+ Application: { getInstance: () => ({}) },
10
+ }));
11
+
12
+ import { coreFunctionRegistry } from '../core';
13
+ import { Container } from '../display/Container';
14
+ import { LookupPlugin } from './LookupPlugin';
15
+
16
+ describe('LookupPlugin', () => {
17
+ it('registers getChildAtPath (and its siblings) in the core function registry', () => {
18
+ const plugin = new LookupPlugin();
19
+ plugin.registerCoreFunctions();
20
+
21
+ expect((coreFunctionRegistry as Record<string, unknown>).getChildAtPath).toBe(plugin.getChildAtPath);
22
+ });
23
+
24
+ it('disconnects its global container subscriptions on destroy', async () => {
25
+ const plugin = new LookupPlugin();
26
+ const added = vi.spyOn(plugin as any, 'onChildAdded');
27
+ const removed = vi.spyOn(plugin as any, 'onChildRemoved');
28
+ await plugin.initialize();
29
+
30
+ const child = { label: 'Thing', children: [] } as any;
31
+ Container.onGlobalChildAdded.emit(child);
32
+ Container.onGlobalChildRemoved.emit(child);
33
+ expect(added).toHaveBeenCalledTimes(1);
34
+ expect(removed).toHaveBeenCalledTimes(1);
35
+
36
+ plugin.destroy();
37
+ Container.onGlobalChildAdded.emit(child);
38
+ Container.onGlobalChildRemoved.emit(child);
39
+ expect(added).toHaveBeenCalledTimes(1);
40
+ expect(removed).toHaveBeenCalledTimes(1);
41
+ });
42
+ });
@@ -97,7 +97,7 @@ export class LookupPlugin extends Plugin implements ILookupPlugin {
97
97
  /**
98
98
  * Core function names exposed by this plugin for framework integration.
99
99
  */
100
- get coreFunctions(): string[] {
100
+ protected getCoreFunctions(): string[] {
101
101
  return ['getChildAtPath', 'getPathForChild', 'getChildrenAtPaths', 'getPathsForChildren', 'getAllPaths'];
102
102
  }
103
103
 
@@ -106,8 +106,20 @@ export class LookupPlugin extends Plugin implements ILookupPlugin {
106
106
  * Sets up automatic tracking of container hierarchy changes.
107
107
  */
108
108
  public async initialize() {
109
- Container.onGlobalChildAdded.connect(this.onChildAdded);
110
- Container.onGlobalChildRemoved.connect(this.onChildRemoved);
109
+ this.addSignalConnection(
110
+ Container.onGlobalChildAdded.connect(this.onChildAdded),
111
+ Container.onGlobalChildRemoved.connect(this.onChildRemoved),
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Disconnects the global container subscriptions and drops the lookup maps,
117
+ * so a destroyed plugin stops tracking (and holding on to) containers.
118
+ */
119
+ public destroy(): void {
120
+ this.pathToContainer.clear();
121
+ this.containerToPath.clear();
122
+ super.destroy();
111
123
  }
112
124
 
113
125
  /**
@@ -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
- this._activePopups.forEach((popup) => {
183
- popup.hide();
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
+ });