@rpg-engine/long-bow 0.8.219 → 0.8.220

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 (35) hide show
  1. package/dist/components/DraggableContainer.d.ts +0 -6
  2. package/dist/components/Store/MetadataCollector.d.ts +2 -2
  3. package/dist/components/Store/Store.d.ts +10 -27
  4. package/dist/components/Store/StoreHeader.d.ts +14 -0
  5. package/dist/components/Store/hooks/useStoreCart.d.ts +2 -0
  6. package/dist/components/Store/hooks/useStoreMetadata.d.ts +4 -11
  7. package/dist/components/Store/hooks/useStoreTabs.d.ts +20 -0
  8. package/dist/components/Store/internal/packToBlueprint.d.ts +2 -0
  9. package/dist/components/Store/sections/StoreItemsSection.d.ts +5 -3
  10. package/dist/hooks/useStoreFiltering.d.ts +7 -4
  11. package/dist/long-bow.cjs.development.js +349 -396
  12. package/dist/long-bow.cjs.development.js.map +1 -1
  13. package/dist/long-bow.cjs.production.min.js +1 -1
  14. package/dist/long-bow.cjs.production.min.js.map +1 -1
  15. package/dist/long-bow.esm.js +351 -398
  16. package/dist/long-bow.esm.js.map +1 -1
  17. package/package.json +1 -1
  18. package/src/components/DraggableContainer.tsx +0 -24
  19. package/src/components/Store/CartView.tsx +7 -2
  20. package/src/components/Store/MetadataCollector.tsx +60 -40
  21. package/src/components/Store/Store.tsx +75 -282
  22. package/src/components/Store/StoreHeader.tsx +74 -0
  23. package/src/components/Store/__test__/MetadataCollector.spec.tsx +94 -164
  24. package/src/components/Store/__test__/Store.spec.tsx +4 -0
  25. package/src/components/Store/__test__/useStoreMetadata.spec.tsx +58 -156
  26. package/src/components/Store/__test__/useStoreTabs.spec.tsx +69 -0
  27. package/src/components/Store/hooks/useStoreCart.ts +5 -2
  28. package/src/components/Store/hooks/useStoreMetadata.ts +30 -48
  29. package/src/components/Store/hooks/useStoreTabs.ts +104 -0
  30. package/src/components/Store/internal/packToBlueprint.ts +21 -0
  31. package/src/components/Store/sections/StoreItemsSection.tsx +19 -60
  32. package/src/components/Store/sections/StorePacksSection.tsx +0 -1
  33. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -6
  34. package/src/hooks/useStoreFiltering.spec.tsx +79 -0
  35. package/src/hooks/useStoreFiltering.ts +27 -9
@@ -1,95 +1,47 @@
1
- // @ts-nocheck - Disable type checking for this file
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ // @ts-nocheck
2
5
  import { MetadataType } from '@rpg-engine/shared';
3
6
  import React from 'react';
7
+ import ReactDOM from 'react-dom';
8
+ import { act } from 'react-dom/test-utils';
4
9
  import { CharacterSkinSelectionModal } from '../../Character/CharacterSkinSelectionModal';
5
10
  import { MetadataCollector } from '../MetadataCollector';
6
11
 
7
- // If MetadataType is not properly loaded, mock it with the same values
8
12
  jest.mock('@rpg-engine/shared', () => ({
9
- // Keep any real values from the shared module
10
13
  ...jest.requireActual('@rpg-engine/shared'),
11
- // Override MetadataType with our mock values
12
14
  MetadataType: {
13
15
  None: 'None',
14
- CharacterSkin: 'CharacterSkin'
15
- }
16
+ CharacterSkin: 'CharacterSkin',
17
+ },
16
18
  }));
17
19
 
18
- // Mock the CharacterSkinSelectionModal component
19
20
  jest.mock('../../Character/CharacterSkinSelectionModal', () => ({
20
21
  CharacterSkinSelectionModal: jest.fn(() => <div>Skin Selection Modal</div>),
21
22
  }));
22
23
 
23
- // We need to mock the actual MetadataCollector component
24
- jest.mock('../MetadataCollector', () => ({
25
- MetadataCollector: jest.fn(({ metadataType, config, onCollect, onCancel }) => {
26
- // Set up cleanup function for the useEffect tests
27
- const React = require('react');
28
- React.useEffect(() => {
29
- return () => {
30
- if (globalThis.__metadataResolvers) {
31
- onCancel();
32
- }
33
- };
34
- }, [onCancel]);
35
-
36
- // This mock simulates the switch statement in the original component
37
- if (metadataType === 'CharacterSkin') {
38
- // Call the CharacterSkinSelectionModal
39
- const CharacterSkinMock = require('../../Character/CharacterSkinSelectionModal').CharacterSkinSelectionModal;
40
- CharacterSkinMock({
41
- isOpen: true,
42
- onClose: onCancel,
43
- onConfirm: (selectedSkin) => onCollect({ selectedSkin }),
44
- availableCharacters: config.availableCharacters || [],
45
- atlasJSON: config.atlasJSON,
46
- atlasIMG: config.atlasIMG,
47
- initialSelectedSkin: config.initialSelectedSkin
48
- });
49
- return null;
50
- } else {
51
- // Warn for unhandled types and auto-cancel
52
- console.warn(`No collector implemented for metadata type: ${metadataType}`);
53
- setTimeout(onCancel, 0);
54
- return null;
55
- }
56
- })
57
- }));
58
-
59
- // Mock globals and setup
60
- let cleanupFunction;
61
- let mockOnCollect;
62
- let mockOnCancel;
63
-
64
- // Mock window.__metadataResolvers
65
- Object.defineProperty(window, '__metadataResolvers', {
66
- writable: true,
67
- value: undefined
68
- });
69
-
70
24
  describe('MetadataCollector', () => {
25
+ let container;
26
+ let mockOnCollect;
27
+ let mockOnCancel;
28
+
71
29
  beforeEach(() => {
72
- // Reset mocks
73
30
  jest.clearAllMocks();
74
31
  mockOnCollect = jest.fn();
75
32
  mockOnCancel = jest.fn();
76
- cleanupFunction = undefined;
77
-
78
- // Clear window.__metadataResolvers
79
- window.__metadataResolvers = undefined;
80
-
81
- // Mock React's useEffect to capture cleanup function
82
- jest.spyOn(React, 'useEffect').mockImplementation(cb => {
83
- cleanupFunction = cb();
84
- });
33
+ jest.useFakeTimers();
34
+ container = document.createElement('div');
35
+ document.body.appendChild(container);
85
36
  });
86
37
 
87
38
  afterEach(() => {
88
- // Restore original implementation
89
- React.useEffect.mockRestore();
39
+ ReactDOM.unmountComponentAtNode(container);
40
+ document.body.removeChild(container);
41
+ jest.useRealTimers();
90
42
  });
91
43
 
92
- it('should render CharacterSkinSelectionModal for CharacterSkin metadata type', () => {
44
+ it('renders CharacterSkinSelectionModal for CharacterSkin metadata type', () => {
93
45
  const mockConfig = {
94
46
  availableCharacters: ['char1', 'char2'],
95
47
  atlasJSON: { frames: {} },
@@ -97,133 +49,111 @@ describe('MetadataCollector', () => {
97
49
  initialSelectedSkin: 'char1',
98
50
  };
99
51
 
100
- // Render the component (this is simple in our mock setup)
101
- MetadataCollector({
102
- metadataType: MetadataType.CharacterSkin,
103
- config: mockConfig,
104
- onCollect: mockOnCollect,
105
- onCancel: mockOnCancel,
52
+ act(() => {
53
+ ReactDOM.render(
54
+ <MetadataCollector
55
+ metadataType={MetadataType.CharacterSkin}
56
+ config={mockConfig}
57
+ onCollect={mockOnCollect}
58
+ onCancel={mockOnCancel}
59
+ />,
60
+ container
61
+ );
106
62
  });
107
63
 
108
- // Verify CharacterSkinSelectionModal was called
109
64
  expect(CharacterSkinSelectionModal).toHaveBeenCalled();
110
-
111
- // Get the first call arguments
112
65
  const callArgs = CharacterSkinSelectionModal.mock.calls[0][0];
113
-
114
- // Verify individual properties
66
+
115
67
  expect(callArgs.isOpen).toBe(true);
116
- expect(callArgs.onClose).toBe(mockOnCancel);
117
68
  expect(callArgs.availableCharacters).toEqual(mockConfig.availableCharacters);
118
69
  expect(callArgs.atlasJSON).toEqual(mockConfig.atlasJSON);
119
70
  expect(callArgs.atlasIMG).toEqual(mockConfig.atlasIMG);
120
71
  expect(callArgs.initialSelectedSkin).toEqual(mockConfig.initialSelectedSkin);
121
72
  });
122
73
 
123
- it('should call onCollect when CharacterSkinSelectionModal confirms selection', () => {
124
- const mockConfig = {
125
- availableCharacters: ['char1', 'char2'],
126
- };
127
-
128
- // Mock CharacterSkinSelectionModal to simulate calling onConfirm
129
- const { CharacterSkinSelectionModal } = require('../../Character/CharacterSkinSelectionModal');
130
- CharacterSkinSelectionModal.mockImplementationOnce(props => {
131
- // Immediately call onConfirm with a mock skin
132
- const mockSelectedSkin = 'char1';
133
- props.onConfirm(mockSelectedSkin);
134
- return <div>Skin Selection Modal</div>;
74
+ it('collects once without cancelling again when the modal closes after confirm', () => {
75
+ act(() => {
76
+ ReactDOM.render(
77
+ <MetadataCollector
78
+ metadataType={MetadataType.CharacterSkin}
79
+ config={{ availableCharacters: ['char1', 'char2'] }}
80
+ onCollect={mockOnCollect}
81
+ onCancel={mockOnCancel}
82
+ />,
83
+ container
84
+ );
135
85
  });
136
86
 
137
- // Render the component
138
- MetadataCollector({
139
- metadataType: MetadataType.CharacterSkin,
140
- config: mockConfig,
141
- onCollect: mockOnCollect,
142
- onCancel: mockOnCancel,
143
- });
144
-
145
- // Verify onCollect was called with the right parameters
146
- expect(mockOnCollect).toHaveBeenCalledWith({ selectedSkin: 'char1' });
147
- });
148
-
149
- it('should log warning and auto-cancel for unhandled metadata types', () => {
150
- const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
151
-
152
- // Mock setTimeout to execute callback immediately
153
- jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
154
- callback();
155
- return 0;
156
- });
87
+ const callArgs = CharacterSkinSelectionModal.mock.calls[0][0];
157
88
 
158
- // Render the component with unknown metadata type
159
- MetadataCollector({
160
- metadataType: 'UnknownType',
161
- config: {},
162
- onCollect: mockOnCollect,
163
- onCancel: mockOnCancel,
89
+ act(() => {
90
+ callArgs.onConfirm('char1');
91
+ callArgs.onClose();
164
92
  });
165
93
 
166
- // Verify warning was logged
167
- expect(warnSpy).toHaveBeenCalledWith(
168
- expect.stringContaining('No collector implemented for metadata type: UnknownType')
169
- );
94
+ expect(mockOnCollect).toHaveBeenCalledWith({ selectedSkin: 'char1' });
95
+ expect(mockOnCollect).toHaveBeenCalledTimes(1);
96
+ expect(mockOnCancel).not.toHaveBeenCalled();
170
97
 
171
- // Verify onCancel was called
172
- expect(mockOnCancel).toHaveBeenCalled();
98
+ act(() => {
99
+ ReactDOM.unmountComponentAtNode(container);
100
+ });
173
101
 
174
- // Cleanup
175
- warnSpy.mockRestore();
176
- jest.spyOn(global, 'setTimeout').mockRestore();
102
+ expect(mockOnCancel).not.toHaveBeenCalled();
177
103
  });
178
104
 
179
- it('should call onCancel when unmounted if collection is in progress', () => {
180
- // Set up window.__metadataResolvers to simulate active collection
181
- window.__metadataResolvers = {
182
- resolve: jest.fn(),
183
- item: { key: 'test-item' },
184
- };
105
+ it('cancels once when closed and does not cancel again on unmount', () => {
106
+ act(() => {
107
+ ReactDOM.render(
108
+ <MetadataCollector
109
+ metadataType={MetadataType.CharacterSkin}
110
+ config={{ availableCharacters: ['char1', 'char2'] }}
111
+ onCollect={mockOnCollect}
112
+ onCancel={mockOnCancel}
113
+ />,
114
+ container
115
+ );
116
+ });
117
+
118
+ const callArgs = CharacterSkinSelectionModal.mock.calls[0][0];
185
119
 
186
- // Render the component
187
- MetadataCollector({
188
- metadataType: MetadataType.CharacterSkin,
189
- config: {},
190
- onCollect: mockOnCollect,
191
- onCancel: mockOnCancel,
120
+ act(() => {
121
+ callArgs.onClose();
192
122
  });
193
123
 
194
- // Reset the mock before calling cleanup to see if it gets called
195
- mockOnCancel.mockReset();
124
+ expect(mockOnCancel).toHaveBeenCalledTimes(1);
196
125
 
197
- // Simulate unmount by calling the cleanup function
198
- if (cleanupFunction) {
199
- cleanupFunction();
200
- }
126
+ act(() => {
127
+ ReactDOM.unmountComponentAtNode(container);
128
+ });
201
129
 
202
- // Verify onCancel was called
203
- expect(mockOnCancel).toHaveBeenCalled();
130
+ expect(mockOnCancel).toHaveBeenCalledTimes(1);
204
131
  });
205
132
 
206
- it('should not call onCancel when unmounted if no collection is in progress', () => {
207
- // Ensure window.__metadataResolvers is undefined
208
- window.__metadataResolvers = undefined;
133
+ it('logs a warning and auto-cancels for unhandled metadata types', () => {
134
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
209
135
 
210
- // Render the component
211
- MetadataCollector({
212
- metadataType: MetadataType.CharacterSkin,
213
- config: {},
214
- onCollect: mockOnCollect,
215
- onCancel: mockOnCancel,
136
+ act(() => {
137
+ ReactDOM.render(
138
+ <MetadataCollector
139
+ metadataType={'UnknownType' as MetadataType}
140
+ config={{}}
141
+ onCollect={mockOnCollect}
142
+ onCancel={mockOnCancel}
143
+ />,
144
+ container
145
+ );
216
146
  });
217
147
 
218
- // Reset the mock before calling cleanup to see if it gets called
219
- mockOnCancel.mockReset();
148
+ expect(warnSpy).toHaveBeenCalledWith(
149
+ expect.stringContaining('No collector implemented for metadata type: UnknownType')
150
+ );
220
151
 
221
- // Simulate unmount by calling the cleanup function
222
- if (cleanupFunction) {
223
- cleanupFunction();
224
- }
152
+ act(() => {
153
+ jest.runAllTimers();
154
+ });
225
155
 
226
- // Verify onCancel was not called
227
- expect(mockOnCancel).not.toHaveBeenCalled();
156
+ expect(mockOnCancel).toHaveBeenCalledTimes(1);
157
+ warnSpy.mockRestore();
228
158
  });
229
- });
159
+ });
@@ -105,6 +105,9 @@ jest.mock('../hooks/useStoreCart', () => ({
105
105
  getTotalItems: () => 0,
106
106
  getTotalPrice: () => 0,
107
107
  isCartOpen: false,
108
+ isCollectingMetadata: false,
109
+ currentMetadataItem: null,
110
+ resolveMetadata: jest.fn(),
108
111
  }),
109
112
  }));
110
113
 
@@ -128,6 +131,7 @@ jest.mock('react-icons/fa', () => ({
128
131
  FaHistory: () => <span>history</span>,
129
132
  FaShoppingCart: () => <span>cart</span>,
130
133
  FaTicketAlt: () => <span>ticket</span>,
134
+ FaUsers: () => <span>users</span>,
131
135
  FaWallet: () => <span>wallet</span>,
132
136
  }));
133
137
 
@@ -1,181 +1,83 @@
1
- // @ts-nocheck - Disable type checking for this file
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ // @ts-nocheck
2
5
  import { MetadataType } from '@rpg-engine/shared';
6
+ import React from 'react';
7
+ import ReactDOM from 'react-dom';
8
+ import { act } from 'react-dom/test-utils';
3
9
  import { useStoreMetadata } from '../hooks/useStoreMetadata';
4
10
 
5
- // If MetadataType is not properly loaded, mock it with the same values
6
11
  jest.mock('@rpg-engine/shared', () => ({
7
- // Keep any real values from the shared module
8
12
  ...jest.requireActual('@rpg-engine/shared'),
9
- // Override MetadataType with our mock values
10
- MetadataType: {
11
- None: 'None',
12
- CharacterSkin: 'CharacterSkin'
13
- }
13
+ MetadataType: { None: 'None', CharacterSkin: 'CharacterSkin' },
14
14
  }));
15
15
 
16
- // Mock the useStoreMetadata hook
17
- jest.mock('../hooks/useStoreMetadata', () => {
18
- const useSM = jest.fn().mockImplementation(() => {
19
- const isCollectingMetadata = false;
20
-
21
- const collectMetadata = async (item) => {
22
- // If no metadata type or None, return null immediately
23
- if (!item.metadataType || item.metadataType === 'None') {
24
- return null;
25
- }
26
-
27
- // Setup for valid metadata types
28
- globalThis.__metadataResolvers = {
29
- resolve: (metadata) => {
30
- // Clean up
31
- globalThis.__metadataResolvers = undefined;
32
- return metadata;
33
- },
34
- item
35
- };
36
-
37
- // Handle the last test case specifically
38
- if (item.key === 'item-character-skin-metadata-cancel') {
39
- globalThis.__metadataResolvers = undefined; // Make sure it's cleaned up
40
- return Promise.resolve(null);
41
- }
42
-
43
- // Normal flow
44
- return Promise.resolve({ selectedSkin: 'test-skin' });
45
- };
46
-
47
- return {
48
- isCollectingMetadata,
49
- collectMetadata
50
- };
51
- });
52
-
53
- return { useStoreMetadata: useSM };
54
- });
16
+ const itemNoMetadata = { key: 'plain', name: 'Plain', price: 100 };
17
+ const itemCharSkin = { key: 'skin', name: 'Skin', price: 300, metadataType: 'CharacterSkin' };
55
18
 
56
- // Mock window.__metadataResolvers
57
- Object.defineProperty(window, '__metadataResolvers', {
58
- writable: true,
59
- value: undefined
60
- });
19
+ // Render the hook in a real React component to satisfy Hook rules
20
+ let hookResult;
21
+ const TestComponent = () => {
22
+ hookResult = useStoreMetadata();
23
+ return null;
24
+ };
61
25
 
62
26
  describe('useStoreMetadata', () => {
63
- // Create test items
64
- const mockItemNoMetadata = {
65
- key: 'item-no-metadata',
66
- name: 'Test Item 1',
67
- price: 100,
68
- textureAtlas: 'atlas',
69
- textureKey: 'texture',
70
- rarity: 'common',
71
- allowedEquipSlotType: 'hand'
72
- };
73
-
74
- const mockItemWithNoneMetadata = {
75
- key: 'item-none-metadata',
76
- name: 'Test Item 2',
77
- price: 200,
78
- metadataType: MetadataType.None,
79
- textureAtlas: 'atlas',
80
- textureKey: 'texture',
81
- rarity: 'common',
82
- allowedEquipSlotType: 'hand'
83
- };
84
-
85
- const mockItemWithCharacterSkinMetadata = {
86
- key: 'item-character-skin-metadata',
87
- name: 'Test Item 3',
88
- price: 300,
89
- metadataType: MetadataType.CharacterSkin,
90
- textureAtlas: 'atlas',
91
- textureKey: 'texture',
92
- rarity: 'common',
93
- allowedEquipSlotType: 'hand'
94
- };
95
-
96
- const mockItemWithCharacterSkinMetadataCancel = {
97
- key: 'item-character-skin-metadata-cancel',
98
- name: 'Test Item 4',
99
- price: 300,
100
- metadataType: MetadataType.CharacterSkin,
101
- textureAtlas: 'atlas',
102
- textureKey: 'texture',
103
- rarity: 'common',
104
- allowedEquipSlotType: 'hand'
105
- };
27
+ let container;
106
28
 
107
29
  beforeEach(() => {
108
- // Clear window.__metadataResolvers
109
- window.__metadataResolvers = undefined;
30
+ container = document.createElement('div');
31
+ document.body.appendChild(container);
32
+ act(() => { ReactDOM.render(<TestComponent />, container); });
33
+ });
110
34
 
111
- // Reset mocks
112
- jest.clearAllMocks();
35
+ afterEach(() => {
36
+ ReactDOM.unmountComponentAtNode(container);
37
+ document.body.removeChild(container);
113
38
  });
114
39
 
115
- it('should initialize with isCollectingMetadata set to false', () => {
116
- const hook = useStoreMetadata();
117
- expect(hook.isCollectingMetadata).toBe(false);
40
+ it('initializes with isCollectingMetadata false and no currentMetadataItem', () => {
41
+ expect(hookResult.isCollectingMetadata).toBe(false);
42
+ expect(hookResult.currentMetadataItem).toBeNull();
118
43
  });
119
44
 
120
- it('should return null for items with no metadataType', async () => {
121
- const hook = useStoreMetadata();
122
- const result = await hook.collectMetadata(mockItemNoMetadata);
45
+ it('returns null immediately for items without metadataType', async () => {
46
+ const result = await hookResult.collectMetadata(itemNoMetadata);
123
47
  expect(result).toBeNull();
48
+ expect(hookResult.isCollectingMetadata).toBe(false);
124
49
  });
125
50
 
126
- it('should return null for items with MetadataType.None', async () => {
127
- const hook = useStoreMetadata();
128
- const result = await hook.collectMetadata(mockItemWithNoneMetadata);
129
- expect(result).toBeNull();
51
+ it('sets isCollectingMetadata and currentMetadataItem when collecting starts', () => {
52
+ act(() => { hookResult.collectMetadata(itemCharSkin); });
53
+ expect(hookResult.isCollectingMetadata).toBe(true);
54
+ expect(hookResult.currentMetadataItem).toBe(itemCharSkin);
55
+ // clean up
56
+ act(() => { hookResult.resolveMetadata(null); });
130
57
  });
131
58
 
132
- it('should set up window.__metadataResolvers with the item for valid metadata types', async () => {
133
- const hook = useStoreMetadata();
134
-
135
- // Create a spy for window.__metadataResolvers.resolve
136
- const resolveSpy = jest.fn().mockReturnValue({ selectedSkin: 'test-skin' });
137
-
138
- // Start metadata collection
139
- const metadataPromise = hook.collectMetadata(mockItemWithCharacterSkinMetadata);
140
-
141
- // Setup our mock resolver
142
- if (globalThis.__metadataResolvers) {
143
- const originalResolve = globalThis.__metadataResolvers.resolve;
144
- globalThis.__metadataResolvers.resolve = function(metadata) {
145
- resolveSpy(metadata);
146
- return originalResolve(metadata);
147
- };
148
- }
149
-
150
- // Resolve the metadata
151
- const mockMetadata = { selectedSkin: 'test-skin' };
152
- if (globalThis.__metadataResolvers) {
153
- globalThis.__metadataResolvers.resolve(mockMetadata);
154
- }
155
-
156
- // Wait for the promise to resolve
157
- const result = await metadataPromise;
158
-
159
- // Assertions
160
- expect(result).toEqual(mockMetadata);
161
- expect(resolveSpy).toHaveBeenCalledWith(mockMetadata);
162
- expect(globalThis.__metadataResolvers).toBeUndefined(); // Should be cleaned up
59
+ it('resolves with metadata and resets state on resolveMetadata', async () => {
60
+ let promise;
61
+ act(() => { promise = hookResult.collectMetadata(itemCharSkin); });
62
+
63
+ const mockMetadata = { selectedSkin: 'warrior' };
64
+ act(() => { hookResult.resolveMetadata(mockMetadata); });
65
+
66
+ const resolved = await promise;
67
+ expect(resolved).toEqual(mockMetadata);
68
+ expect(hookResult.isCollectingMetadata).toBe(false);
69
+ expect(hookResult.currentMetadataItem).toBeNull();
163
70
  });
164
71
 
165
- it('should clean up window.__metadataResolvers when collection is cancelled', async () => {
166
- const hook = useStoreMetadata();
167
-
168
- // Make sure window.__metadataResolvers is undefined before starting
169
- window.__metadataResolvers = undefined;
170
-
171
- // Start metadata collection with the special cancel item
172
- const metadataPromise = hook.collectMetadata(mockItemWithCharacterSkinMetadataCancel);
173
-
174
- // Wait for the promise to resolve
175
- const result = await metadataPromise;
176
-
177
- // Assertions
178
- expect(result).toBeNull();
179
- expect(window.__metadataResolvers).toBeUndefined(); // Should be cleaned up
72
+ it('resolves with null and resets state when cancelled', async () => {
73
+ let promise;
74
+ act(() => { promise = hookResult.collectMetadata(itemCharSkin); });
75
+
76
+ act(() => { hookResult.resolveMetadata(null); });
77
+
78
+ const resolved = await promise;
79
+ expect(resolved).toBeNull();
80
+ expect(hookResult.isCollectingMetadata).toBe(false);
81
+ expect(hookResult.currentMetadataItem).toBeNull();
180
82
  });
181
- });
83
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ // @ts-nocheck
5
+ import React from 'react';
6
+ import ReactDOM from 'react-dom';
7
+ import { act } from 'react-dom/test-utils';
8
+ import { useStoreTabs } from '../hooks/useStoreTabs';
9
+
10
+ let hookResult;
11
+
12
+ const TestComponent = (props) => {
13
+ hookResult = useStoreTabs(props);
14
+ return null;
15
+ };
16
+
17
+ describe('useStoreTabs', () => {
18
+ let container;
19
+
20
+ beforeEach(() => {
21
+ container = document.createElement('div');
22
+ document.body.appendChild(container);
23
+ });
24
+
25
+ afterEach(() => {
26
+ ReactDOM.unmountComponentAtNode(container);
27
+ document.body.removeChild(container);
28
+ });
29
+
30
+ it('falls back to the next valid tab when the active tab disappears', () => {
31
+ act(() => {
32
+ ReactDOM.render(
33
+ <TestComponent tabOrder={['wallet', 'items']} hasWallet defaultActiveTab="wallet" />,
34
+ container
35
+ );
36
+ });
37
+
38
+ expect(hookResult.activeTab).toBe('wallet');
39
+ expect(hookResult.availableTabIds).toEqual(['wallet', 'items']);
40
+
41
+ act(() => {
42
+ ReactDOM.render(
43
+ <TestComponent tabOrder={['wallet', 'items']} defaultActiveTab="wallet" />,
44
+ container
45
+ );
46
+ });
47
+
48
+ expect(hookResult.activeTab).toBe('items');
49
+ expect(hookResult.availableTabIds).toEqual(['items']);
50
+ });
51
+
52
+ it('ignores tab changes for unavailable tabs', () => {
53
+ const onTabChange = jest.fn();
54
+
55
+ act(() => {
56
+ ReactDOM.render(
57
+ <TestComponent tabOrder={['items']} onTabChange={onTabChange} getItemCount={() => 3} />,
58
+ container
59
+ );
60
+ });
61
+
62
+ act(() => {
63
+ hookResult.handleTabChange('wallet');
64
+ });
65
+
66
+ expect(hookResult.activeTab).toBe('items');
67
+ expect(onTabChange).not.toHaveBeenCalled();
68
+ });
69
+ });