@rpg-engine/long-bow 0.8.171 → 0.8.173
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/dist/components/Store/CartView.d.ts +21 -1
- package/dist/components/Store/CountdownTimer.d.ts +7 -0
- package/dist/components/Store/FeaturedBanner.d.ts +23 -0
- package/dist/components/Store/PurchaseSuccess.d.ts +18 -0
- package/dist/components/Store/Store.d.ts +50 -2
- package/dist/components/Store/StoreBadges.d.ts +13 -0
- package/dist/components/Store/StoreCharacterSkinRow.d.ts +1 -0
- package/dist/components/Store/StoreItemRow.d.ts +10 -0
- package/dist/components/Store/TrustBar.d.ts +9 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +13 -0
- package/dist/components/Store/sections/StorePacksSection.d.ts +11 -0
- package/dist/components/shared/CTAButton/CTAButton.d.ts +1 -0
- package/dist/components/shared/CustomScrollbar.d.ts +9 -0
- package/dist/index.d.ts +6 -1
- package/dist/long-bow.cjs.development.js +1279 -303
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +1276 -305
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/store/FeaturedBanner.stories.d.ts +1 -0
- package/dist/stories/Features/store/PurchaseSuccess.stories.d.ts +1 -0
- package/dist/stories/Features/store/StoreBadges.stories.d.ts +1 -0
- package/dist/stories/Features/store/TrustBar.stories.d.ts +1 -0
- package/package.json +2 -2
- package/src/components/Marketplace/BuyPanel.tsx +1 -1
- package/src/components/RPGUI/RPGUIScrollbar.tsx +2 -2
- package/src/components/Store/CartView.tsx +143 -33
- package/src/components/Store/CountdownTimer.tsx +86 -0
- package/src/components/Store/FeaturedBanner.tsx +270 -0
- package/src/components/Store/PurchaseSuccess.tsx +255 -0
- package/src/components/Store/Store.tsx +236 -51
- package/src/components/Store/StoreBadges.tsx +94 -0
- package/src/components/Store/StoreCharacterSkinRow.tsx +113 -22
- package/src/components/Store/StoreItemRow.tsx +135 -17
- package/src/components/Store/TrustBar.tsx +69 -0
- package/src/components/Store/__test__/CountdownTimer.spec.tsx +100 -0
- package/src/components/Store/__test__/FeaturedBanner.spec.tsx +207 -0
- package/src/components/Store/__test__/PurchaseSuccess.spec.tsx +174 -0
- package/src/components/Store/__test__/StoreBadges.spec.tsx +133 -0
- package/src/components/Store/__test__/TrustBar.spec.tsx +85 -0
- package/src/components/Store/sections/StoreItemsSection.tsx +27 -1
- package/src/components/Store/sections/StorePacksSection.tsx +92 -28
- package/src/components/shared/CTAButton/CTAButton.tsx +25 -1
- package/src/components/shared/CustomScrollbar.ts +41 -0
- package/src/components/shared/ItemRowWrapper.tsx +26 -12
- package/src/components/shared/ScrollableContent/ScrollableContent.tsx +1 -0
- package/src/components/shared/SpriteFromAtlas.tsx +4 -1
- package/src/index.tsx +6 -1
- package/src/stories/Features/store/FeaturedBanner.stories.tsx +121 -0
- package/src/stories/Features/store/PurchaseSuccess.stories.tsx +74 -0
- package/src/stories/Features/store/Store.stories.tsx +39 -3
- package/src/stories/Features/store/StoreBadges.stories.tsx +83 -0
- 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
|
+
});
|