@rpg-engine/long-bow 0.8.170 → 0.8.172

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 (53) hide show
  1. package/dist/components/Store/CartView.d.ts +21 -1
  2. package/dist/components/Store/CountdownTimer.d.ts +7 -0
  3. package/dist/components/Store/FeaturedBanner.d.ts +23 -0
  4. package/dist/components/Store/PurchaseSuccess.d.ts +18 -0
  5. package/dist/components/Store/Store.d.ts +50 -2
  6. package/dist/components/Store/StoreBadges.d.ts +13 -0
  7. package/dist/components/Store/StoreCharacterSkinRow.d.ts +1 -0
  8. package/dist/components/Store/StoreItemRow.d.ts +10 -0
  9. package/dist/components/Store/TrustBar.d.ts +9 -0
  10. package/dist/components/Store/sections/StoreItemsSection.d.ts +13 -0
  11. package/dist/components/Store/sections/StorePacksSection.d.ts +11 -0
  12. package/dist/components/shared/CTAButton/CTAButton.d.ts +1 -0
  13. package/dist/components/shared/CustomScrollbar.d.ts +9 -0
  14. package/dist/index.d.ts +6 -1
  15. package/dist/long-bow.cjs.development.js +1284 -302
  16. package/dist/long-bow.cjs.development.js.map +1 -1
  17. package/dist/long-bow.cjs.production.min.js +1 -1
  18. package/dist/long-bow.cjs.production.min.js.map +1 -1
  19. package/dist/long-bow.esm.js +1281 -304
  20. package/dist/long-bow.esm.js.map +1 -1
  21. package/dist/stories/Features/store/FeaturedBanner.stories.d.ts +1 -0
  22. package/dist/stories/Features/store/PurchaseSuccess.stories.d.ts +1 -0
  23. package/dist/stories/Features/store/StoreBadges.stories.d.ts +1 -0
  24. package/dist/stories/Features/store/TrustBar.stories.d.ts +1 -0
  25. package/package.json +2 -2
  26. package/src/components/Marketplace/BuyPanel.tsx +1 -1
  27. package/src/components/Store/CartView.tsx +143 -13
  28. package/src/components/Store/CountdownTimer.tsx +86 -0
  29. package/src/components/Store/FeaturedBanner.tsx +273 -0
  30. package/src/components/Store/PurchaseSuccess.tsx +258 -0
  31. package/src/components/Store/Store.tsx +236 -50
  32. package/src/components/Store/StoreBadges.tsx +94 -0
  33. package/src/components/Store/StoreCharacterSkinRow.tsx +113 -22
  34. package/src/components/Store/StoreItemRow.tsx +135 -17
  35. package/src/components/Store/TrustBar.tsx +69 -0
  36. package/src/components/Store/__test__/CountdownTimer.spec.tsx +100 -0
  37. package/src/components/Store/__test__/FeaturedBanner.spec.tsx +207 -0
  38. package/src/components/Store/__test__/PurchaseSuccess.spec.tsx +174 -0
  39. package/src/components/Store/__test__/StoreBadges.spec.tsx +133 -0
  40. package/src/components/Store/__test__/TrustBar.spec.tsx +85 -0
  41. package/src/components/Store/sections/StoreItemsSection.tsx +27 -1
  42. package/src/components/Store/sections/StorePacksSection.tsx +92 -28
  43. package/src/components/shared/CTAButton/CTAButton.tsx +25 -1
  44. package/src/components/shared/CustomScrollbar.ts +41 -0
  45. package/src/components/shared/ItemRowWrapper.tsx +26 -12
  46. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -0
  47. package/src/components/shared/SpriteFromAtlas.tsx +4 -1
  48. package/src/index.tsx +6 -1
  49. package/src/stories/Features/store/FeaturedBanner.stories.tsx +121 -0
  50. package/src/stories/Features/store/PurchaseSuccess.stories.tsx +74 -0
  51. package/src/stories/Features/store/Store.stories.tsx +39 -3
  52. package/src/stories/Features/store/StoreBadges.stories.tsx +83 -0
  53. package/src/stories/Features/store/TrustBar.stories.tsx +51 -0
@@ -0,0 +1,100 @@
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 { CountdownTimer } from '../CountdownTimer';
9
+
10
+ jest.mock('styled-components', () => {
11
+ const styled = new Proxy({}, {
12
+ get: () => () => (props) => props.children ?? null,
13
+ });
14
+ styled.span = (strings) => ({ children, ...props }) => <span {...props}>{children}</span>;
15
+ return { default: styled, keyframes: () => '' };
16
+ });
17
+
18
+ describe('CountdownTimer', () => {
19
+ let container;
20
+
21
+ beforeEach(() => {
22
+ container = document.createElement('div');
23
+ document.body.appendChild(container);
24
+ jest.useFakeTimers();
25
+ });
26
+
27
+ afterEach(() => {
28
+ ReactDOM.unmountComponentAtNode(container);
29
+ document.body.removeChild(container);
30
+ jest.useRealTimers();
31
+ });
32
+
33
+ it('should show EXPIRED for past dates', () => {
34
+ const past = new Date(Date.now() - 10000).toISOString();
35
+ act(() => {
36
+ ReactDOM.render(<CountdownTimer endsAt={past} />, container);
37
+ });
38
+ expect(container.textContent).toContain('EXPIRED');
39
+ });
40
+
41
+ it('should call onExpired immediately for past dates', () => {
42
+ const onExpired = jest.fn();
43
+ const past = new Date(Date.now() - 1000).toISOString();
44
+ act(() => {
45
+ ReactDOM.render(<CountdownTimer endsAt={past} onExpired={onExpired} />, container);
46
+ });
47
+ expect(onExpired).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ it('should display remaining time for future dates', () => {
51
+ const future = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(); // 2 hours
52
+ act(() => {
53
+ ReactDOM.render(<CountdownTimer endsAt={future} />, container);
54
+ });
55
+ expect(container.textContent).not.toContain('EXPIRED');
56
+ expect(container.textContent).toContain('h');
57
+ expect(container.textContent).toContain('m');
58
+ });
59
+
60
+ it('should include seconds when less than a day remains', () => {
61
+ const future = new Date(Date.now() + 5 * 60 * 1000).toISOString(); // 5 minutes
62
+ act(() => {
63
+ ReactDOM.render(<CountdownTimer endsAt={future} />, container);
64
+ });
65
+ expect(container.textContent).toContain('s');
66
+ });
67
+
68
+ it('should not show seconds when more than a day remains', () => {
69
+ const future = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(); // 2 days
70
+ act(() => {
71
+ ReactDOM.render(<CountdownTimer endsAt={future} />, container);
72
+ });
73
+ expect(container.textContent).toContain('d');
74
+ expect(container.textContent).not.toContain('s');
75
+ });
76
+
77
+ it('should call onExpired when timer reaches zero', () => {
78
+ const onExpired = jest.fn();
79
+ const future = new Date(Date.now() + 1500).toISOString(); // 1.5 seconds
80
+ act(() => {
81
+ ReactDOM.render(<CountdownTimer endsAt={future} onExpired={onExpired} />, container);
82
+ });
83
+ expect(onExpired).not.toHaveBeenCalled();
84
+ act(() => {
85
+ jest.advanceTimersByTime(2000);
86
+ });
87
+ expect(onExpired).toHaveBeenCalledTimes(1);
88
+ });
89
+
90
+ it('should render EXPIRED after timer runs out', () => {
91
+ const future = new Date(Date.now() + 1500).toISOString();
92
+ act(() => {
93
+ ReactDOM.render(<CountdownTimer endsAt={future} />, container);
94
+ });
95
+ act(() => {
96
+ jest.advanceTimersByTime(2000);
97
+ });
98
+ expect(container.textContent).toContain('EXPIRED');
99
+ });
100
+ });
@@ -0,0 +1,207 @@
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 { FeaturedBanner } from '../FeaturedBanner';
9
+
10
+ jest.mock('../CountdownTimer', () => ({
11
+ CountdownTimer: ({ endsAt }) => <span data-testid="countdown">{endsAt}</span>,
12
+ }));
13
+
14
+ jest.mock('../../shared/SpriteFromAtlas', () => ({
15
+ SpriteFromAtlas: ({ spriteKey }) => <div data-testid="sprite">{spriteKey}</div>,
16
+ }));
17
+
18
+ jest.mock('../../shared/CTAButton/CTAButton', () => ({
19
+ CTAButton: ({ label, onClick }) => <button onClick={onClick}>{label}</button>,
20
+ }));
21
+
22
+ jest.mock('../../shared/LabelPill/LabelPill', () => ({
23
+ LabelPill: ({ children }) => <span data-testid="badge">{children}</span>,
24
+ }));
25
+
26
+ jest.mock('styled-components', () => {
27
+ const makeEl = (tag) => () => ({ children, onClick, ...props }) =>
28
+ React.createElement(tag, { onClick, ...props }, children);
29
+ const styled = new Proxy({}, { get: (_, tag) => makeEl(tag) });
30
+ return { default: styled, keyframes: () => '' };
31
+ });
32
+
33
+ jest.mock('react-icons/fa', () => ({ FaBolt: () => <span>bolt</span> }));
34
+
35
+ const featuredItems = [
36
+ { key: 'item-a', name: 'Item A', price: 4.99, originalPrice: 9.99, badge: 'SALE' },
37
+ { key: 'item-b', name: 'Item B', price: 14.99, badge: 'NEW' },
38
+ ];
39
+
40
+ describe('FeaturedBanner', () => {
41
+ let container;
42
+
43
+ beforeEach(() => {
44
+ container = document.createElement('div');
45
+ document.body.appendChild(container);
46
+ });
47
+
48
+ afterEach(() => {
49
+ ReactDOM.unmountComponentAtNode(container);
50
+ document.body.removeChild(container);
51
+ });
52
+
53
+ it('should render nothing when items array is empty', () => {
54
+ act(() => {
55
+ ReactDOM.render(
56
+ <FeaturedBanner items={[]} atlasJSON={{}} atlasIMG="" onSelectItem={jest.fn()} />,
57
+ container
58
+ );
59
+ });
60
+ expect(container.innerHTML).toBe('');
61
+ });
62
+
63
+ it('should render featured item names', () => {
64
+ act(() => {
65
+ ReactDOM.render(
66
+ <FeaturedBanner items={featuredItems} atlasJSON={{}} atlasIMG="" onSelectItem={jest.fn()} />,
67
+ container
68
+ );
69
+ });
70
+ expect(container.textContent).toContain('Item A');
71
+ expect(container.textContent).toContain('Item B');
72
+ });
73
+
74
+ it('should render item prices', () => {
75
+ act(() => {
76
+ ReactDOM.render(
77
+ <FeaturedBanner items={featuredItems} atlasJSON={{}} atlasIMG="" onSelectItem={jest.fn()} />,
78
+ container
79
+ );
80
+ });
81
+ expect(container.textContent).toContain('4.99');
82
+ expect(container.textContent).toContain('14.99');
83
+ });
84
+
85
+ it('should render originalPrice with strikethrough when provided', () => {
86
+ act(() => {
87
+ ReactDOM.render(
88
+ <FeaturedBanner items={featuredItems} atlasJSON={{}} atlasIMG="" onSelectItem={jest.fn()} />,
89
+ container
90
+ );
91
+ });
92
+ expect(container.textContent).toContain('9.99');
93
+ });
94
+
95
+ it('should render badge when provided', () => {
96
+ act(() => {
97
+ ReactDOM.render(
98
+ <FeaturedBanner items={featuredItems} atlasJSON={{}} atlasIMG="" onSelectItem={jest.fn()} />,
99
+ container
100
+ );
101
+ });
102
+ const badges = container.querySelectorAll('[data-testid="badge"]');
103
+ expect(badges.length).toBeGreaterThanOrEqual(1);
104
+ expect(container.textContent).toContain('SALE');
105
+ });
106
+
107
+ it('should call onSelectItem when card is clicked', () => {
108
+ const onSelectItem = jest.fn();
109
+ act(() => {
110
+ ReactDOM.render(
111
+ <FeaturedBanner items={featuredItems} atlasJSON={{}} atlasIMG="" onSelectItem={onSelectItem} />,
112
+ container
113
+ );
114
+ });
115
+ // Click first card (div with onClick)
116
+ const cards = container.querySelectorAll('div[onclick], div');
117
+ act(() => {
118
+ const card = Array.from(container.querySelectorAll('*')).find(el =>
119
+ el.onclick && el.textContent.includes('Item A')
120
+ );
121
+ if (card) card.click();
122
+ });
123
+ expect(onSelectItem).toHaveBeenCalledWith(expect.objectContaining({ key: 'item-a' }));
124
+ });
125
+
126
+ it('should render Buy Now button when onQuickBuy provided', () => {
127
+ act(() => {
128
+ ReactDOM.render(
129
+ <FeaturedBanner
130
+ items={featuredItems}
131
+ atlasJSON={{}}
132
+ atlasIMG=""
133
+ onSelectItem={jest.fn()}
134
+ onQuickBuy={jest.fn()}
135
+ />,
136
+ container
137
+ );
138
+ });
139
+ const buttons = container.querySelectorAll('button');
140
+ const buyNow = Array.from(buttons).find(b => b.textContent.includes('Buy Now'));
141
+ expect(buyNow).not.toBeUndefined();
142
+ });
143
+
144
+ it('should not render Buy Now button when onQuickBuy absent', () => {
145
+ act(() => {
146
+ ReactDOM.render(
147
+ <FeaturedBanner items={featuredItems} atlasJSON={{}} atlasIMG="" onSelectItem={jest.fn()} />,
148
+ container
149
+ );
150
+ });
151
+ const buttons = container.querySelectorAll('button');
152
+ const buyNow = Array.from(buttons).find(b => b.textContent.includes('Buy Now'));
153
+ expect(buyNow).toBeUndefined();
154
+ });
155
+
156
+ it('should call onQuickBuy with correct item when Buy Now clicked', () => {
157
+ const onQuickBuy = jest.fn();
158
+ act(() => {
159
+ ReactDOM.render(
160
+ <FeaturedBanner
161
+ items={[featuredItems[0]]}
162
+ atlasJSON={{}}
163
+ atlasIMG=""
164
+ onSelectItem={jest.fn()}
165
+ onQuickBuy={onQuickBuy}
166
+ />,
167
+ container
168
+ );
169
+ });
170
+ const btn = Array.from(container.querySelectorAll('button')).find(b =>
171
+ b.textContent.includes('Buy Now')
172
+ );
173
+ act(() => { btn.click(); });
174
+ expect(onQuickBuy).toHaveBeenCalledWith(expect.objectContaining({ key: 'item-a' }));
175
+ });
176
+
177
+ it('should render CountdownTimer when endsAt provided', () => {
178
+ const future = new Date(Date.now() + 3600000).toISOString();
179
+ act(() => {
180
+ ReactDOM.render(
181
+ <FeaturedBanner
182
+ items={[{ key: 'x', name: 'X', price: 1, endsAt: future }]}
183
+ atlasJSON={{}}
184
+ atlasIMG=""
185
+ onSelectItem={jest.fn()}
186
+ />,
187
+ container
188
+ );
189
+ });
190
+ expect(container.querySelector('[data-testid="countdown"]')).not.toBeNull();
191
+ });
192
+
193
+ it('should not render CountdownTimer when endsAt absent', () => {
194
+ act(() => {
195
+ ReactDOM.render(
196
+ <FeaturedBanner
197
+ items={[{ key: 'x', name: 'X', price: 1 }]}
198
+ atlasJSON={{}}
199
+ atlasIMG=""
200
+ onSelectItem={jest.fn()}
201
+ />,
202
+ container
203
+ );
204
+ });
205
+ expect(container.querySelector('[data-testid="countdown"]')).toBeNull();
206
+ });
207
+ });
@@ -0,0 +1,174 @@
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 { PurchaseSuccess } from '../PurchaseSuccess';
9
+
10
+ jest.mock('../../../mocks/atlas/entities/entities.json', () => ({ frames: {} }), { virtual: true });
11
+ jest.mock('../../../mocks/atlas/entities/entities.png', () => 'entities.png', { virtual: true });
12
+
13
+ jest.mock('../../shared/SpriteFromAtlas', () => ({
14
+ SpriteFromAtlas: ({ spriteKey }) => <div data-testid="sprite">{spriteKey}</div>,
15
+ }));
16
+
17
+ jest.mock('../../shared/CTAButton/CTAButton', () => ({
18
+ CTAButton: ({ label, onClick }) => <button onClick={onClick}>{label}</button>,
19
+ }));
20
+
21
+ jest.mock('styled-components', () => {
22
+ const styled = new Proxy({}, {
23
+ get: () => () => ({ children, ...props }) => <div {...props}>{children}</div>,
24
+ });
25
+ styled.div = () => ({ children, ...props }) => <div {...props}>{children}</div>;
26
+ styled.h2 = () => ({ children }) => <h2>{children}</h2>;
27
+ styled.p = () => ({ children }) => <p>{children}</p>;
28
+ styled.span = () => ({ children }) => <span>{children}</span>;
29
+ return { default: styled, keyframes: () => '' };
30
+ });
31
+
32
+ jest.mock('react-icons/fa', () => ({
33
+ FaStar: () => <span>star</span>,
34
+ FaShoppingBag: () => <span>bag</span>,
35
+ FaTimes: () => <span>x</span>,
36
+ }));
37
+
38
+ const mockItems = [
39
+ { name: 'Life Potion', texturePath: 'items/life_potion.png', quantity: 2 },
40
+ { name: 'Sword', texturePath: 'items/sword.png', quantity: 1 },
41
+ ];
42
+
43
+ describe('PurchaseSuccess', () => {
44
+ let container;
45
+
46
+ beforeEach(() => {
47
+ container = document.createElement('div');
48
+ document.body.appendChild(container);
49
+ });
50
+
51
+ afterEach(() => {
52
+ ReactDOM.unmountComponentAtNode(container);
53
+ document.body.removeChild(container);
54
+ });
55
+
56
+ it('should render purchased item names', () => {
57
+ act(() => {
58
+ ReactDOM.render(
59
+ <PurchaseSuccess
60
+ items={mockItems}
61
+ totalPrice={9.99}
62
+ atlasJSON={{}}
63
+ atlasIMG="items.png"
64
+ onContinueShopping={jest.fn()}
65
+ onClose={jest.fn()}
66
+ />,
67
+ container
68
+ );
69
+ });
70
+ expect(container.textContent).toContain('Life Potion');
71
+ expect(container.textContent).toContain('Sword');
72
+ });
73
+
74
+ it('should show total price', () => {
75
+ act(() => {
76
+ ReactDOM.render(
77
+ <PurchaseSuccess
78
+ items={mockItems}
79
+ totalPrice={19.98}
80
+ atlasJSON={{}}
81
+ atlasIMG="items.png"
82
+ onContinueShopping={jest.fn()}
83
+ onClose={jest.fn()}
84
+ />,
85
+ container
86
+ );
87
+ });
88
+ expect(container.textContent).toContain('19.98');
89
+ });
90
+
91
+ it('should show quantity for items with qty > 1', () => {
92
+ act(() => {
93
+ ReactDOM.render(
94
+ <PurchaseSuccess
95
+ items={[{ name: 'Potion', texturePath: 'potion.png', quantity: 5 }]}
96
+ totalPrice={4.99}
97
+ atlasJSON={{}}
98
+ atlasIMG="items.png"
99
+ onContinueShopping={jest.fn()}
100
+ onClose={jest.fn()}
101
+ />,
102
+ container
103
+ );
104
+ });
105
+ expect(container.textContent).toContain('×5');
106
+ });
107
+
108
+ it('should not show quantity indicator for single items', () => {
109
+ act(() => {
110
+ ReactDOM.render(
111
+ <PurchaseSuccess
112
+ items={[{ name: 'Sword', texturePath: 'sword.png', quantity: 1 }]}
113
+ totalPrice={9.99}
114
+ atlasJSON={{}}
115
+ atlasIMG="items.png"
116
+ onContinueShopping={jest.fn()}
117
+ onClose={jest.fn()}
118
+ />,
119
+ container
120
+ );
121
+ });
122
+ expect(container.textContent).not.toContain('×1');
123
+ });
124
+
125
+ it('should call onContinueShopping when Continue Shopping clicked', () => {
126
+ const onContinueShopping = jest.fn();
127
+ act(() => {
128
+ ReactDOM.render(
129
+ <PurchaseSuccess
130
+ items={mockItems}
131
+ totalPrice={9.99}
132
+ atlasJSON={{}}
133
+ atlasIMG="items.png"
134
+ onContinueShopping={onContinueShopping}
135
+ onClose={jest.fn()}
136
+ />,
137
+ container
138
+ );
139
+ });
140
+ const btn = Array.from(container.querySelectorAll('button')).find(b =>
141
+ b.textContent.includes('Continue Shopping')
142
+ );
143
+ act(() => { btn.click(); });
144
+ expect(onContinueShopping).toHaveBeenCalledTimes(1);
145
+ });
146
+
147
+ it('should call onClose when Close Store clicked', () => {
148
+ const onClose = jest.fn();
149
+ act(() => {
150
+ ReactDOM.render(
151
+ <PurchaseSuccess
152
+ items={mockItems}
153
+ totalPrice={9.99}
154
+ atlasJSON={{}}
155
+ atlasIMG="items.png"
156
+ onContinueShopping={jest.fn()}
157
+ onClose={onClose}
158
+ />,
159
+ container
160
+ );
161
+ });
162
+ const closeLink = container.querySelector('[style]') ?? container.lastElementChild?.lastElementChild;
163
+ // Fire pointer down on the close link
164
+ const allDivs = container.querySelectorAll('div');
165
+ let found = false;
166
+ allDivs.forEach(div => {
167
+ if (div.textContent.includes('Close Store') && !found) {
168
+ act(() => { div.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); });
169
+ found = true;
170
+ }
171
+ });
172
+ expect(onClose).toHaveBeenCalledTimes(1);
173
+ });
174
+ });
@@ -0,0 +1,133 @@
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 { StoreBadges } from '../StoreBadges';
9
+
10
+ jest.mock('../CountdownTimer', () => ({
11
+ CountdownTimer: ({ endsAt }) => <span data-testid="countdown">{endsAt}</span>,
12
+ }));
13
+
14
+ jest.mock('styled-components', () => {
15
+ const styled = new Proxy({}, {
16
+ get: () => () => ({ children, ...props }) => <div {...props}>{children}</div>,
17
+ });
18
+ return { default: styled, keyframes: () => '' };
19
+ });
20
+
21
+ jest.mock('../../shared/LabelPill/LabelPill', () => ({
22
+ LabelPill: ({ children }) => <span data-testid="label-pill">{children}</span>,
23
+ }));
24
+
25
+ describe('StoreBadges', () => {
26
+ let container;
27
+
28
+ beforeEach(() => {
29
+ container = document.createElement('div');
30
+ document.body.appendChild(container);
31
+ });
32
+
33
+ afterEach(() => {
34
+ ReactDOM.unmountComponentAtNode(container);
35
+ document.body.removeChild(container);
36
+ });
37
+
38
+ it('should render nothing when no props provided', () => {
39
+ act(() => {
40
+ ReactDOM.render(<StoreBadges />, container);
41
+ });
42
+ expect(container.querySelectorAll('[data-testid="label-pill"]').length).toBe(0);
43
+ });
44
+
45
+ it('should render popular badge', () => {
46
+ act(() => {
47
+ ReactDOM.render(<StoreBadges badges={[{ type: 'popular' }]} />, container);
48
+ });
49
+ expect(container.textContent).toContain('Popular');
50
+ });
51
+
52
+ it('should render bestSeller badge', () => {
53
+ act(() => {
54
+ ReactDOM.render(<StoreBadges badges={[{ type: 'bestSeller' }]} />, container);
55
+ });
56
+ expect(container.textContent).toContain('Best Seller');
57
+ });
58
+
59
+ it('should render limited badge', () => {
60
+ act(() => {
61
+ ReactDOM.render(<StoreBadges badges={[{ type: 'limited' }]} />, container);
62
+ });
63
+ expect(container.textContent).toContain('Limited');
64
+ });
65
+
66
+ it('should render new badge', () => {
67
+ act(() => {
68
+ ReactDOM.render(<StoreBadges badges={[{ type: 'new' }]} />, container);
69
+ });
70
+ expect(container.textContent).toContain('New');
71
+ });
72
+
73
+ it('should render sale badge', () => {
74
+ act(() => {
75
+ ReactDOM.render(<StoreBadges badges={[{ type: 'sale' }]} />, container);
76
+ });
77
+ expect(container.textContent).toContain('Sale');
78
+ });
79
+
80
+ it('should use custom label when provided', () => {
81
+ act(() => {
82
+ ReactDOM.render(<StoreBadges badges={[{ type: 'limited', label: 'Only 2 left!' }]} />, container);
83
+ });
84
+ expect(container.textContent).toContain('Only 2 left!');
85
+ });
86
+
87
+ it('should render buy count when buyCount > 0', () => {
88
+ act(() => {
89
+ ReactDOM.render(<StoreBadges buyCount={42} />, container);
90
+ });
91
+ expect(container.textContent).toContain('42 bought');
92
+ });
93
+
94
+ it('should not render buy count when buyCount is 0', () => {
95
+ act(() => {
96
+ ReactDOM.render(<StoreBadges buyCount={0} />, container);
97
+ });
98
+ expect(container.textContent).not.toContain('bought');
99
+ });
100
+
101
+ it('should render viewers count when viewersCount > 1', () => {
102
+ act(() => {
103
+ ReactDOM.render(<StoreBadges viewersCount={5} />, container);
104
+ });
105
+ expect(container.textContent).toContain('5 viewing');
106
+ });
107
+
108
+ it('should not render viewers count when viewersCount is 1', () => {
109
+ act(() => {
110
+ ReactDOM.render(<StoreBadges viewersCount={1} />, container);
111
+ });
112
+ expect(container.textContent).not.toContain('viewing');
113
+ });
114
+
115
+ it('should render CountdownTimer when saleEndsAt provided', () => {
116
+ const endsAt = new Date(Date.now() + 3600000).toISOString();
117
+ act(() => {
118
+ ReactDOM.render(<StoreBadges saleEndsAt={endsAt} />, container);
119
+ });
120
+ expect(container.querySelector('[data-testid="countdown"]')).not.toBeNull();
121
+ });
122
+
123
+ it('should render multiple badges', () => {
124
+ act(() => {
125
+ ReactDOM.render(
126
+ <StoreBadges badges={[{ type: 'popular' }, { type: 'sale' }]} buyCount={10} />,
127
+ container
128
+ );
129
+ });
130
+ const pills = container.querySelectorAll('[data-testid="label-pill"]');
131
+ expect(pills.length).toBeGreaterThanOrEqual(3); // 2 badges + buy count
132
+ });
133
+ });
@@ -0,0 +1,85 @@
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 { TrustBar } from '../TrustBar';
9
+
10
+ jest.mock('styled-components', () => {
11
+ const styled = new Proxy({}, {
12
+ get: () => () => ({ children, ...props }) => <div {...props}>{children}</div>,
13
+ });
14
+ return { default: styled, keyframes: () => '' };
15
+ });
16
+
17
+ jest.mock('react-icons/fa', () => ({
18
+ FaLock: () => <span>lock</span>,
19
+ FaRocket: () => <span>rocket</span>,
20
+ FaHeadset: () => <span>headset</span>,
21
+ }));
22
+
23
+ describe('TrustBar', () => {
24
+ let container;
25
+
26
+ beforeEach(() => {
27
+ container = document.createElement('div');
28
+ document.body.appendChild(container);
29
+ });
30
+
31
+ afterEach(() => {
32
+ ReactDOM.unmountComponentAtNode(container);
33
+ document.body.removeChild(container);
34
+ });
35
+
36
+ it('should render default trust signals when no signals prop provided', () => {
37
+ act(() => {
38
+ ReactDOM.render(<TrustBar />, container);
39
+ });
40
+ expect(container.textContent).toContain('Secure Payment');
41
+ expect(container.textContent).toContain('Instant Delivery');
42
+ expect(container.textContent).toContain('24/7 Support');
43
+ });
44
+
45
+ it('should render custom signals when provided', () => {
46
+ act(() => {
47
+ ReactDOM.render(
48
+ <TrustBar signals={[{ label: 'SSL Encrypted' }, { label: 'No subscriptions' }]} />,
49
+ container
50
+ );
51
+ });
52
+ expect(container.textContent).toContain('SSL Encrypted');
53
+ expect(container.textContent).toContain('No subscriptions');
54
+ });
55
+
56
+ it('should not render default signals when custom signals provided', () => {
57
+ act(() => {
58
+ ReactDOM.render(
59
+ <TrustBar signals={[{ label: 'Custom' }]} />,
60
+ container
61
+ );
62
+ });
63
+ expect(container.textContent).not.toContain('Secure Payment');
64
+ });
65
+
66
+ it('should render signal with custom icon', () => {
67
+ act(() => {
68
+ ReactDOM.render(
69
+ <TrustBar signals={[{ icon: <span data-testid="custom-icon" />, label: 'Custom' }]} />,
70
+ container
71
+ );
72
+ });
73
+ expect(container.querySelector('[data-testid="custom-icon"]')).not.toBeNull();
74
+ });
75
+
76
+ it('should render signal without icon when icon not provided', () => {
77
+ act(() => {
78
+ ReactDOM.render(
79
+ <TrustBar signals={[{ label: 'No Icon Signal' }]} />,
80
+ container
81
+ );
82
+ });
83
+ expect(container.textContent).toContain('No Icon Signal');
84
+ });
85
+ });