@rpg-engine/long-bow 0.8.199 → 0.8.202
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/NPCDialog/NPCDialog.d.ts +1 -0
- package/dist/components/NPCDialog/NPCDialogText.d.ts +2 -1
- package/dist/long-bow.cjs.development.js +45 -22
- 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 +45 -22
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Marketplace/BuyPanel.tsx +4 -3
- package/src/components/Marketplace/__test__/BuyPanel.spec.tsx +45 -3
- package/src/components/NPCDialog/NPCDialog.tsx +3 -0
- package/src/components/NPCDialog/NPCDialogText.tsx +43 -15
- package/src/components/NPCDialog/__test__/NPCDialogText.spec.tsx +68 -0
- package/src/components/Store/Store.tsx +4 -3
- package/src/components/Store/__test__/Store.spec.tsx +191 -0
- package/src/stories/Features/store/Store.stories.tsx +62 -0
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
|
|
|
23
23
|
type MarketplaceBrowseMode = 'sell' | 'buy';
|
|
24
24
|
|
|
25
25
|
const BUY_REQUESTS_PER_PAGE = 5;
|
|
26
|
+
const MARKETPLACE_PANEL_HEIGHT = '390px';
|
|
27
|
+
const MARKETPLACE_PANEL_MOBILE_HEIGHT = '250px';
|
|
26
28
|
|
|
27
29
|
const formatBlueprintKey = (key: string): string => {
|
|
28
30
|
const name = key.includes('/') ? key.split('/').pop()! : key;
|
|
@@ -646,8 +648,7 @@ const ItemComponentScrollWrapper = styled.div`
|
|
|
646
648
|
flex-direction: column;
|
|
647
649
|
overflow-y: auto;
|
|
648
650
|
overflow-x: hidden;
|
|
649
|
-
|
|
650
|
-
min-height: 120px;
|
|
651
|
+
height: ${MARKETPLACE_PANEL_HEIGHT};
|
|
651
652
|
width: 95%;
|
|
652
653
|
margin: 1rem auto 0 auto;
|
|
653
654
|
background: rgba(0, 0, 0, 0.2);
|
|
@@ -655,7 +656,7 @@ const ItemComponentScrollWrapper = styled.div`
|
|
|
655
656
|
border-radius: 4px;
|
|
656
657
|
|
|
657
658
|
@media (max-width: 950px) {
|
|
658
|
-
|
|
659
|
+
height: ${MARKETPLACE_PANEL_MOBILE_HEIGHT};
|
|
659
660
|
}
|
|
660
661
|
`;
|
|
661
662
|
|
|
@@ -8,6 +8,10 @@ import ReactDOM from 'react-dom';
|
|
|
8
8
|
import { act } from 'react-dom/test-utils';
|
|
9
9
|
import { BuyPanel } from '../BuyPanel';
|
|
10
10
|
|
|
11
|
+
jest.mock('pixelarticons/react/SortVertical', () => ({
|
|
12
|
+
SortVertical: () => <svg data-testid="sort-icon" />,
|
|
13
|
+
}));
|
|
14
|
+
|
|
11
15
|
jest.mock('../../ConfirmModal', () => ({
|
|
12
16
|
ConfirmModal: ({ message, onConfirm, onClose }) => (
|
|
13
17
|
<div data-testid="confirm-modal">
|
|
@@ -22,6 +26,18 @@ jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
|
22
26
|
CTAButton: ({ label, onClick }) => <button onClick={onClick}>{label}</button>,
|
|
23
27
|
}));
|
|
24
28
|
|
|
29
|
+
jest.mock('../MarketplaceRows', () => ({
|
|
30
|
+
GroupedMarketplaceRow: () => <div data-testid="grouped-marketplace-row" />,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
jest.mock('../BuyOrderRows', () => ({
|
|
34
|
+
GroupedBuyOrderRow: ({ bestOrder, onFulfill }) => (
|
|
35
|
+
<div data-testid="grouped-buy-order-row">
|
|
36
|
+
<button onClick={() => onFulfill(bestOrder._id)}>Fulfill</button>
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
}));
|
|
40
|
+
|
|
25
41
|
describe('BuyPanel fulfill flow', () => {
|
|
26
42
|
let container;
|
|
27
43
|
|
|
@@ -35,7 +51,7 @@ describe('BuyPanel fulfill flow', () => {
|
|
|
35
51
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
36
52
|
};
|
|
37
53
|
|
|
38
|
-
const renderBuyPanel = (onFulfillBuyOrder = jest.fn()) => {
|
|
54
|
+
const renderBuyPanel = (onFulfillBuyOrder = jest.fn(), extraProps = {}) => {
|
|
39
55
|
act(() => {
|
|
40
56
|
ReactDOM.render(
|
|
41
57
|
<BuyPanel
|
|
@@ -60,6 +76,7 @@ describe('BuyPanel fulfill flow', () => {
|
|
|
60
76
|
openBuyOrdersPage={1}
|
|
61
77
|
onOpenBuyOrdersPageChange={jest.fn()}
|
|
62
78
|
onFulfillBuyOrder={onFulfillBuyOrder}
|
|
79
|
+
{...extraProps}
|
|
63
80
|
/>,
|
|
64
81
|
container
|
|
65
82
|
);
|
|
@@ -83,8 +100,16 @@ describe('BuyPanel fulfill flow', () => {
|
|
|
83
100
|
it('waits for confirmation before calling onFulfillBuyOrder', () => {
|
|
84
101
|
const onFulfillBuyOrder = renderBuyPanel();
|
|
85
102
|
|
|
103
|
+
const buyRequestsButton = Array.from(container.querySelectorAll('button')).find(
|
|
104
|
+
button => button.textContent === 'Buy Requests'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
act(() => {
|
|
108
|
+
buyRequestsButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
109
|
+
});
|
|
110
|
+
|
|
86
111
|
const fulfillButton = Array.from(container.querySelectorAll('button')).find(
|
|
87
|
-
|
|
112
|
+
button => button.textContent === 'Fulfill'
|
|
88
113
|
);
|
|
89
114
|
|
|
90
115
|
act(() => {
|
|
@@ -108,8 +133,16 @@ describe('BuyPanel fulfill flow', () => {
|
|
|
108
133
|
it('does not call onFulfillBuyOrder when the confirm modal is cancelled', () => {
|
|
109
134
|
const onFulfillBuyOrder = renderBuyPanel();
|
|
110
135
|
|
|
136
|
+
const buyRequestsButton = Array.from(container.querySelectorAll('button')).find(
|
|
137
|
+
button => button.textContent === 'Buy Requests'
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
buyRequestsButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
142
|
+
});
|
|
143
|
+
|
|
111
144
|
const fulfillButton = Array.from(container.querySelectorAll('button')).find(
|
|
112
|
-
|
|
145
|
+
button => button.textContent === 'Fulfill'
|
|
113
146
|
);
|
|
114
147
|
|
|
115
148
|
act(() => {
|
|
@@ -126,4 +159,13 @@ describe('BuyPanel fulfill flow', () => {
|
|
|
126
159
|
|
|
127
160
|
expect(onFulfillBuyOrder).not.toHaveBeenCalled();
|
|
128
161
|
});
|
|
162
|
+
|
|
163
|
+
it('keeps the marketplace viewport height fixed while loading', () => {
|
|
164
|
+
renderBuyPanel(jest.fn(), { isLoading: true });
|
|
165
|
+
|
|
166
|
+
const marketContainer = container.querySelector('#MarketContainer');
|
|
167
|
+
|
|
168
|
+
expect(marketContainer).not.toBeNull();
|
|
169
|
+
expect(getComputedStyle(marketContainer).height).toBe('390px');
|
|
170
|
+
});
|
|
129
171
|
});
|
|
@@ -18,6 +18,7 @@ export interface INPCDialogProps {
|
|
|
18
18
|
text?: string;
|
|
19
19
|
type: NPCDialogType;
|
|
20
20
|
imagePath?: string;
|
|
21
|
+
isTranslated?: boolean;
|
|
21
22
|
onClose?: () => void;
|
|
22
23
|
isQuestionDialog?: boolean;
|
|
23
24
|
answers?: IQuestionDialogAnswer[];
|
|
@@ -29,6 +30,7 @@ export const NPCDialog: React.FC<INPCDialogProps> = ({
|
|
|
29
30
|
type,
|
|
30
31
|
onClose,
|
|
31
32
|
imagePath,
|
|
33
|
+
isTranslated = false,
|
|
32
34
|
isQuestionDialog = false,
|
|
33
35
|
questions,
|
|
34
36
|
answers,
|
|
@@ -70,6 +72,7 @@ export const NPCDialog: React.FC<INPCDialogProps> = ({
|
|
|
70
72
|
<NPCDialogText
|
|
71
73
|
type={type}
|
|
72
74
|
text={text || 'No text provided.'}
|
|
75
|
+
isTranslated={isTranslated}
|
|
73
76
|
onClose={() => {
|
|
74
77
|
if (onClose) {
|
|
75
78
|
onClose();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
|
-
import { NPCDialogType } from '
|
|
3
|
+
import type { NPCDialogType } from './NPCDialog';
|
|
4
4
|
import { IS_MOBILE_OR_TABLET } from '../../constants/uiDevices';
|
|
5
5
|
import { chunkString } from '../../libs/StringHelpers';
|
|
6
6
|
import { DynamicText } from '../typography/DynamicText';
|
|
@@ -13,6 +13,7 @@ interface IProps {
|
|
|
13
13
|
onEndStep?: () => void;
|
|
14
14
|
onStartStep?: () => void;
|
|
15
15
|
type?: NPCDialogType;
|
|
16
|
+
isTranslated?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const NPCDialogText: React.FC<IProps> = ({
|
|
@@ -21,6 +22,7 @@ export const NPCDialogText: React.FC<IProps> = ({
|
|
|
21
22
|
onEndStep,
|
|
22
23
|
onStartStep,
|
|
23
24
|
type,
|
|
25
|
+
isTranslated = false,
|
|
24
26
|
}) => {
|
|
25
27
|
const windowSize = useRef([window.innerWidth, window.innerHeight]);
|
|
26
28
|
function maxCharacters(width: number) {
|
|
@@ -43,6 +45,11 @@ export const NPCDialogText: React.FC<IProps> = ({
|
|
|
43
45
|
const textChunks = chunkString(text, maxCharacters(windowSize.current[0]));
|
|
44
46
|
|
|
45
47
|
const [chunkIndex, setChunkIndex] = useState<number>(0);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setChunkIndex(0);
|
|
51
|
+
}, [text]);
|
|
52
|
+
|
|
46
53
|
const onHandleSpacePress = (event: KeyboardEvent) => {
|
|
47
54
|
if (event.code === 'Space') {
|
|
48
55
|
goToNextStep();
|
|
@@ -70,24 +77,37 @@ export const NPCDialogText: React.FC<IProps> = ({
|
|
|
70
77
|
false
|
|
71
78
|
);
|
|
72
79
|
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setShowGoNextIndicator(isTranslated);
|
|
82
|
+
|
|
83
|
+
if (isTranslated) {
|
|
84
|
+
onStartStep && onStartStep();
|
|
85
|
+
onEndStep && onEndStep();
|
|
86
|
+
}
|
|
87
|
+
}, [chunkIndex, isTranslated, onEndStep, onStartStep, text]);
|
|
88
|
+
|
|
73
89
|
return (
|
|
74
90
|
<Container>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
{isTranslated ? (
|
|
92
|
+
<TextContainer>{textChunks?.[chunkIndex] || ''}</TextContainer>
|
|
93
|
+
) : (
|
|
94
|
+
<DynamicText
|
|
95
|
+
text={textChunks?.[chunkIndex] || ''}
|
|
96
|
+
onFinish={() => {
|
|
97
|
+
setShowGoNextIndicator(true);
|
|
98
|
+
|
|
99
|
+
onEndStep && onEndStep();
|
|
100
|
+
}}
|
|
101
|
+
onStart={() => {
|
|
102
|
+
setShowGoNextIndicator(false);
|
|
103
|
+
|
|
104
|
+
onStartStep && onStartStep();
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
88
108
|
{showGoNextIndicator && (
|
|
89
109
|
<PressSpaceIndicator
|
|
90
|
-
right={type ===
|
|
110
|
+
right={type === 'TextOnly' ? '1rem' : '10.5rem'}
|
|
91
111
|
src={IS_MOBILE_OR_TABLET ? pressButtonGif : pressSpaceGif}
|
|
92
112
|
onPointerDown={() => {
|
|
93
113
|
goToNextStep();
|
|
@@ -100,6 +120,14 @@ export const NPCDialogText: React.FC<IProps> = ({
|
|
|
100
120
|
|
|
101
121
|
const Container = styled.div``;
|
|
102
122
|
|
|
123
|
+
const TextContainer = styled.p`
|
|
124
|
+
font-size: 0.7rem !important;
|
|
125
|
+
color: white;
|
|
126
|
+
text-shadow: 1px 1px 0px #000000;
|
|
127
|
+
letter-spacing: 1.2px;
|
|
128
|
+
word-break: normal;
|
|
129
|
+
`;
|
|
130
|
+
|
|
103
131
|
interface IPressSpaceIndicatorProps {
|
|
104
132
|
right: string;
|
|
105
133
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { NPCDialogText } from '../NPCDialogText';
|
|
9
|
+
|
|
10
|
+
jest.mock('../img/press-button.gif', () => 'press-button.gif');
|
|
11
|
+
jest.mock('../img/space.gif', () => 'space.gif');
|
|
12
|
+
|
|
13
|
+
describe('NPCDialogText', () => {
|
|
14
|
+
let container;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
container = document.createElement('div');
|
|
18
|
+
document.body.appendChild(container);
|
|
19
|
+
jest.useFakeTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
24
|
+
document.body.removeChild(container);
|
|
25
|
+
jest.useRealTimers();
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders the full translated text immediately and skips the typewriter delay', () => {
|
|
30
|
+
act(() => {
|
|
31
|
+
ReactDOM.render(
|
|
32
|
+
<NPCDialogText
|
|
33
|
+
text="Translated NPC dialog"
|
|
34
|
+
type={'TextOnly'}
|
|
35
|
+
isTranslated
|
|
36
|
+
onClose={jest.fn()}
|
|
37
|
+
/>,
|
|
38
|
+
container
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(container.textContent).toContain('Translated NPC dialog');
|
|
43
|
+
expect(container.querySelectorAll('img')).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps the typewriter effect for untranslated text until the animation finishes', () => {
|
|
47
|
+
act(() => {
|
|
48
|
+
ReactDOM.render(
|
|
49
|
+
<NPCDialogText
|
|
50
|
+
text="Hi"
|
|
51
|
+
type={'TextOnly'}
|
|
52
|
+
onClose={jest.fn()}
|
|
53
|
+
/>,
|
|
54
|
+
container
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(container.textContent).toBe('');
|
|
59
|
+
expect(container.querySelectorAll('img')).toHaveLength(0);
|
|
60
|
+
|
|
61
|
+
act(() => {
|
|
62
|
+
jest.advanceTimersByTime(100);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(container.textContent).toContain('Hi');
|
|
66
|
+
expect(container.querySelectorAll('img')).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -127,13 +127,14 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
127
127
|
onRedeemInputFocus,
|
|
128
128
|
onRedeemInputBlur,
|
|
129
129
|
}) => {
|
|
130
|
+
const defaultTabOrder: TabId[] = ['premium', 'packs', 'items'];
|
|
130
131
|
const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
|
|
131
132
|
const [activeTab, setActiveTab] = useState<TabId>(() => {
|
|
132
|
-
const initialTabs = (tabOrder ??
|
|
133
|
+
const initialTabs = (tabOrder ?? defaultTabOrder).filter(id => !(hidePremiumTab && id === 'premium'));
|
|
133
134
|
if (defaultActiveTab && initialTabs.includes(defaultActiveTab)) {
|
|
134
135
|
return defaultActiveTab;
|
|
135
136
|
}
|
|
136
|
-
return hidePremiumTab ? 'items' : 'premium';
|
|
137
|
+
return initialTabs[0] ?? (hidePremiumTab ? 'items' : 'premium');
|
|
137
138
|
});
|
|
138
139
|
const {
|
|
139
140
|
cartItems,
|
|
@@ -239,7 +240,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
239
240
|
|
|
240
241
|
// Build tabs dynamically based on props
|
|
241
242
|
const tabIds: TabId[] = [
|
|
242
|
-
...(tabOrder ??
|
|
243
|
+
...(tabOrder ?? defaultTabOrder),
|
|
243
244
|
...(onRedeem ? ['redeem' as TabId] : []),
|
|
244
245
|
...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
|
|
245
246
|
...((onShowHistory || customHistoryContent) ? ['history' as TabId] : [])
|
|
@@ -0,0 +1,191 @@
|
|
|
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 { Store } from '../Store';
|
|
9
|
+
|
|
10
|
+
const mockTabs = jest.fn(({ options, activeTabId }) => (
|
|
11
|
+
<div data-testid="tabs" data-active-tab-id={activeTabId}>
|
|
12
|
+
{options.map(option => (
|
|
13
|
+
<button key={option.id} type="button">
|
|
14
|
+
{option.id}
|
|
15
|
+
</button>
|
|
16
|
+
))}
|
|
17
|
+
</div>
|
|
18
|
+
));
|
|
19
|
+
|
|
20
|
+
jest.mock('styled-components', () => {
|
|
21
|
+
const createTag = (tag) => () => {
|
|
22
|
+
if (tag === 'button') return ({ children, ...props }) => <button {...props}>{children}</button>;
|
|
23
|
+
return ({ children, ...props }) => <div {...props}>{children}</div>;
|
|
24
|
+
};
|
|
25
|
+
const styled = new Proxy(() => {}, {
|
|
26
|
+
get: (_target, prop) => {
|
|
27
|
+
if (prop === '__esModule') return true;
|
|
28
|
+
return createTag(prop);
|
|
29
|
+
},
|
|
30
|
+
apply: () => createTag('div'),
|
|
31
|
+
});
|
|
32
|
+
return { __esModule: true, default: styled, keyframes: () => '', css: () => '' };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
jest.mock('../../../constants/uiColors', () => ({
|
|
36
|
+
uiColors: {
|
|
37
|
+
black: '#000',
|
|
38
|
+
white: '#fff',
|
|
39
|
+
red: '#f00',
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.mock('../../DraggableContainer', () => ({
|
|
44
|
+
DraggableContainer: ({ children }) => <div data-testid="draggable-container">{children}</div>,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
jest.mock('../../RPGUI/RPGUIContainer', () => ({
|
|
48
|
+
RPGUIContainerTypes: {
|
|
49
|
+
Framed: 'Framed',
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
jest.mock('../../shared/Tabs', () => ({
|
|
54
|
+
Tabs: (props) => mockTabs(props),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
jest.mock('../../shared/LabelPill/LabelPill', () => ({
|
|
58
|
+
LabelPill: ({ children }) => <span>{children}</span>,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
62
|
+
CTAButton: ({ children, label, onClick }) => (
|
|
63
|
+
<button type="button" onClick={onClick}>
|
|
64
|
+
{children ?? label}
|
|
65
|
+
</button>
|
|
66
|
+
),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
jest.mock('../CartView', () => ({
|
|
70
|
+
CartView: () => <div>Cart View</div>,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
jest.mock('../FeaturedBanner', () => ({
|
|
74
|
+
FeaturedBanner: () => <div>Featured Banner</div>,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
jest.mock('../MetadataCollector', () => ({
|
|
78
|
+
MetadataCollector: () => <div>Metadata Collector</div>,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
jest.mock('../sections/StoreItemsSection', () => ({
|
|
82
|
+
StoreItemsSection: () => <div>Items Section</div>,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
jest.mock('../sections/StorePacksSection', () => ({
|
|
86
|
+
StorePacksSection: () => <div>Packs Section</div>,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
jest.mock('../sections/StoreRedeemSection', () => ({
|
|
90
|
+
StoreRedeemSection: () => <div>Redeem Section</div>,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
jest.mock('../StoreItemDetails', () => ({
|
|
94
|
+
StoreItemDetails: () => <div>Store Item Details</div>,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
jest.mock('../hooks/useStoreCart', () => ({
|
|
98
|
+
useStoreCart: () => ({
|
|
99
|
+
cartItems: [],
|
|
100
|
+
handleAddToCart: jest.fn(),
|
|
101
|
+
handleRemoveFromCart: jest.fn(),
|
|
102
|
+
handlePurchase: jest.fn(),
|
|
103
|
+
openCart: jest.fn(),
|
|
104
|
+
closeCart: jest.fn(),
|
|
105
|
+
getTotalItems: () => 0,
|
|
106
|
+
getTotalPrice: () => 0,
|
|
107
|
+
isCartOpen: false,
|
|
108
|
+
}),
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
jest.mock('pixelarticons/react/Crown', () => ({
|
|
112
|
+
Crown: () => <span>crown</span>,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
jest.mock('pixelarticons/react/Gift', () => ({
|
|
116
|
+
Gift: () => <span>gift</span>,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
jest.mock('pixelarticons/react/Package', () => ({
|
|
120
|
+
Package: () => <span>package</span>,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
jest.mock('pixelarticons/react/Wallet', () => ({
|
|
124
|
+
Wallet: () => <span>wallet</span>,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
jest.mock('react-icons/fa', () => ({
|
|
128
|
+
FaHistory: () => <span>history</span>,
|
|
129
|
+
FaShoppingCart: () => <span>cart</span>,
|
|
130
|
+
FaTicketAlt: () => <span>ticket</span>,
|
|
131
|
+
FaWallet: () => <span>wallet</span>,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
describe('Store', () => {
|
|
135
|
+
let container;
|
|
136
|
+
|
|
137
|
+
const baseProps = {
|
|
138
|
+
items: [],
|
|
139
|
+
packs: [],
|
|
140
|
+
atlasJSON: {},
|
|
141
|
+
atlasIMG: '',
|
|
142
|
+
onPurchase: jest.fn().mockResolvedValue(true),
|
|
143
|
+
userAccountType: 'Free',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
container = document.createElement('div');
|
|
148
|
+
document.body.appendChild(container);
|
|
149
|
+
mockTabs.mockClear();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
afterEach(() => {
|
|
153
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
154
|
+
document.body.removeChild(container);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('activates the first tab from tabOrder when no defaultActiveTab is provided', () => {
|
|
158
|
+
act(() => {
|
|
159
|
+
ReactDOM.render(
|
|
160
|
+
<Store
|
|
161
|
+
{...baseProps}
|
|
162
|
+
hidePremiumTab
|
|
163
|
+
tabOrder={['packs', 'items', 'wallet']}
|
|
164
|
+
customWalletContent={<div>Wallet</div>}
|
|
165
|
+
/>,
|
|
166
|
+
container
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const tabsProps = mockTabs.mock.calls[mockTabs.mock.calls.length - 1][0];
|
|
171
|
+
expect(tabsProps.options.map(option => option.id)).toEqual(['packs', 'items', 'wallet']);
|
|
172
|
+
expect(tabsProps.activeTabId).toBe('packs');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('skips hidden premium when picking the initial tab from tabOrder', () => {
|
|
176
|
+
act(() => {
|
|
177
|
+
ReactDOM.render(
|
|
178
|
+
<Store
|
|
179
|
+
{...baseProps}
|
|
180
|
+
hidePremiumTab
|
|
181
|
+
tabOrder={['premium', 'packs', 'items']}
|
|
182
|
+
/>,
|
|
183
|
+
container
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const tabsProps = mockTabs.mock.calls[mockTabs.mock.calls.length - 1][0];
|
|
188
|
+
expect(tabsProps.options.map(option => option.id)).toEqual(['packs', 'items']);
|
|
189
|
+
expect(tabsProps.activeTabId).toBe('packs');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -564,3 +564,65 @@ export const INR: Story = {
|
|
|
564
564
|
/>
|
|
565
565
|
),
|
|
566
566
|
};
|
|
567
|
+
|
|
568
|
+
export const WithRedeemTab: Story = {
|
|
569
|
+
render: () => (
|
|
570
|
+
<Store
|
|
571
|
+
items={duplicatedItems}
|
|
572
|
+
packs={mockPacks}
|
|
573
|
+
userAccountType={UserAccountTypes.Free}
|
|
574
|
+
onPurchase={(purchase: Partial<IPurchase>) => {
|
|
575
|
+
console.log('Purchase details:', purchase);
|
|
576
|
+
return Promise.resolve(true);
|
|
577
|
+
}}
|
|
578
|
+
onRedeem={async (code: string) => {
|
|
579
|
+
if (code === 'CHB-550-ABCDEF1234') {
|
|
580
|
+
return { success: true, dcAmount: 550 };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return { success: false, error: 'Invalid voucher code.' };
|
|
584
|
+
}}
|
|
585
|
+
customWalletContent={<DCWalletContent
|
|
586
|
+
dcBalance={1200}
|
|
587
|
+
historyData={{ transactions: [], totalPages: 1, currentPage: 1 }}
|
|
588
|
+
historyLoading={false}
|
|
589
|
+
onRequestHistory={() => {}}
|
|
590
|
+
transferLoading={false}
|
|
591
|
+
transferResult={null}
|
|
592
|
+
onSendTransfer={() => {}}
|
|
593
|
+
onClearTransferResult={() => {}}
|
|
594
|
+
onBuyDC={() => console.log('Buy DC')}
|
|
595
|
+
/>}
|
|
596
|
+
customHistoryContent={<DCHistoryPanel
|
|
597
|
+
transactions={[]}
|
|
598
|
+
totalPages={1}
|
|
599
|
+
currentPage={1}
|
|
600
|
+
loading={false}
|
|
601
|
+
onRequestHistory={() => {}}
|
|
602
|
+
/>}
|
|
603
|
+
onClose={() => console.log('Store closed')}
|
|
604
|
+
atlasJSON={itemsAtlasJSON}
|
|
605
|
+
atlasIMG={itemsAtlasIMG}
|
|
606
|
+
hidePremiumTab={true}
|
|
607
|
+
tabOrder={['items', 'packs', 'redeem']}
|
|
608
|
+
defaultActiveTab="redeem"
|
|
609
|
+
textInputItemKeys={['original-greater-life-potion-2', 'original-angelic-sword-1', 'character-name-change']}
|
|
610
|
+
onTabChange={(tab, count) => console.log('[tracking] tab_change', { tab, count })}
|
|
611
|
+
onCategoryChange={(category, count) => console.log('[tracking] category_change', { category, count })}
|
|
612
|
+
onItemView={(item, position) => console.log('[tracking] item_viewed', { key: item.key, position })}
|
|
613
|
+
onPackView={(pack, position) => console.log('[tracking] pack_viewed', { key: pack.key, position })}
|
|
614
|
+
onCartOpen={() => console.log('[tracking] cart_opened')}
|
|
615
|
+
onAddToCart={(item, qty) => console.log('[tracking] add_to_cart', { key: item.key, qty })}
|
|
616
|
+
onRemoveFromCart={(key) => console.log('[tracking] remove_from_cart', { key })}
|
|
617
|
+
onCheckoutStart={(items, total) => console.log('[tracking] checkout_start', { items, total })}
|
|
618
|
+
onPurchaseSuccess={(items, total) => console.log('[tracking] purchase_success', { items, total })}
|
|
619
|
+
onPurchaseError={(error) => console.log('[tracking] purchase_error', { error })}
|
|
620
|
+
itemBadges={{
|
|
621
|
+
'original-greater-life-potion-0': { originalPrice: 15.00 },
|
|
622
|
+
'character-name-change': { originalPrice: 15.00 },
|
|
623
|
+
'skin-character-customization': { originalPrice: 20.00 },
|
|
624
|
+
'starter-pack': { originalPrice: 9.99 }
|
|
625
|
+
}}
|
|
626
|
+
/>
|
|
627
|
+
),
|
|
628
|
+
};
|