@rpg-engine/long-bow 0.8.197 → 0.8.199
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/Marketplace/GroupedRowContainer.d.ts +1 -0
- package/dist/components/Store/Store.d.ts +11 -1
- package/dist/components/Store/sections/StoreRedeemSection.d.ts +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +331 -95
- 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 +332 -97
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Marketplace/BuyOrderRows.tsx +1 -0
- package/src/components/Marketplace/BuyPanel.tsx +6 -3
- package/src/components/Marketplace/CharacterMarketplaceRows.tsx +2 -2
- package/src/components/Marketplace/GroupedRowContainer.tsx +9 -4
- package/src/components/Marketplace/ManagmentPanel.tsx +2 -2
- package/src/components/Marketplace/Marketplace.tsx +2 -2
- package/src/components/Marketplace/MarketplaceRows.tsx +3 -2
- package/src/components/Store/Store.tsx +27 -4
- package/src/components/Store/__test__/StoreRedeemSection.spec.tsx +232 -0
- package/src/components/Store/sections/StoreRedeemSection.tsx +258 -0
- package/src/constants/skillInfoData.ts +124 -60
- package/src/index.tsx +1 -0
- package/src/utils/__test__/atlasUtils.spec.ts +15 -0
- package/src/utils/atlasUtils.ts +59 -1
package/package.json
CHANGED
|
@@ -642,9 +642,12 @@ const WrapperContainer = styled.div<{ $sell: boolean }>`
|
|
|
642
642
|
`;
|
|
643
643
|
|
|
644
644
|
const ItemComponentScrollWrapper = styled.div`
|
|
645
|
-
|
|
645
|
+
display: flex;
|
|
646
|
+
flex-direction: column;
|
|
647
|
+
overflow-y: auto;
|
|
646
648
|
overflow-x: hidden;
|
|
647
|
-
height: 390px;
|
|
649
|
+
max-height: 390px;
|
|
650
|
+
min-height: 120px;
|
|
648
651
|
width: 95%;
|
|
649
652
|
margin: 1rem auto 0 auto;
|
|
650
653
|
background: rgba(0, 0, 0, 0.2);
|
|
@@ -652,7 +655,7 @@ const ItemComponentScrollWrapper = styled.div`
|
|
|
652
655
|
border-radius: 4px;
|
|
653
656
|
|
|
654
657
|
@media (max-width: 950px) {
|
|
655
|
-
height: 250px;
|
|
658
|
+
max-height: 250px;
|
|
656
659
|
}
|
|
657
660
|
`;
|
|
658
661
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
|
|
2
|
-
import {
|
|
2
|
+
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { ItemRowWrapper } from '../shared/ItemRowWrapper';
|
|
@@ -77,7 +77,7 @@ export const CharacterMarketplaceRows: React.FC<ICharacterMarketplaceRowsProps>
|
|
|
77
77
|
|
|
78
78
|
<ActionSection>
|
|
79
79
|
<CTAButton
|
|
80
|
-
icon={onCharacterBuy ? <
|
|
80
|
+
icon={onCharacterBuy ? <Wallet width={18} height={18} /> : undefined}
|
|
81
81
|
label={onCharacterBuy ? 'Buy' : 'Delist'}
|
|
82
82
|
disabled={disabled || isBeingBought}
|
|
83
83
|
onClick={() => {
|
|
@@ -5,12 +5,14 @@ export interface IGroupedRowContainerProps {
|
|
|
5
5
|
mainRow: React.ReactNode;
|
|
6
6
|
subRows: React.ReactNode[];
|
|
7
7
|
badgeLabel?: string;
|
|
8
|
+
metaRightInset?: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const GroupedRowContainer: React.FC<IGroupedRowContainerProps> = ({
|
|
11
12
|
mainRow,
|
|
12
13
|
subRows,
|
|
13
14
|
badgeLabel = 'offers',
|
|
15
|
+
metaRightInset = 120,
|
|
14
16
|
}) => {
|
|
15
17
|
const [expanded, setExpanded] = useState(false);
|
|
16
18
|
const hasMultiple = subRows.length > 0;
|
|
@@ -21,7 +23,7 @@ export const GroupedRowContainer: React.FC<IGroupedRowContainerProps> = ({
|
|
|
21
23
|
<GroupHeader $clickable={hasMultiple} onClick={hasMultiple ? () => setExpanded(e => !e) : undefined}>
|
|
22
24
|
{mainRow}
|
|
23
25
|
{hasMultiple && (
|
|
24
|
-
<GroupMeta>
|
|
26
|
+
<GroupMeta $rightInset={metaRightInset}>
|
|
25
27
|
<OfferBadge>{totalCount} {badgeLabel}</OfferBadge>
|
|
26
28
|
<Chevron $expanded={expanded}>▸</Chevron>
|
|
27
29
|
</GroupMeta>
|
|
@@ -48,15 +50,18 @@ const GroupHeader = styled.div<{ $clickable: boolean }>`
|
|
|
48
50
|
overflow-x: hidden;
|
|
49
51
|
`;
|
|
50
52
|
|
|
51
|
-
const GroupMeta = styled.div
|
|
53
|
+
const GroupMeta = styled.div<{ $rightInset: number }>`
|
|
52
54
|
position: absolute;
|
|
53
|
-
right:
|
|
55
|
+
right: ${({ $rightInset }) => `${$rightInset}px`};
|
|
54
56
|
top: 50%;
|
|
55
57
|
transform: translateY(-50%);
|
|
56
58
|
display: flex;
|
|
57
59
|
align-items: center;
|
|
58
|
-
gap:
|
|
60
|
+
gap: 4px;
|
|
61
|
+
padding-left: 10px;
|
|
62
|
+
background: linear-gradient(to left, rgba(25, 23, 23, 0.85), rgba(25, 23, 23, 0));
|
|
59
63
|
pointer-events: none;
|
|
64
|
+
z-index: 1;
|
|
60
65
|
`;
|
|
61
66
|
|
|
62
67
|
const OfferBadge = styled.span`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDCAmount, goldToDC, IEquipmentSet, IItem, IMarketplaceItem, MarketplaceAcceptedCurrency } from '@rpg-engine/shared';
|
|
2
|
-
import {
|
|
2
|
+
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
3
3
|
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
4
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import styled from 'styled-components';
|
|
@@ -185,7 +185,7 @@ export const ManagmentPanel: React.FC<IManagmentPanelProps> = ({
|
|
|
185
185
|
</BalanceRow>
|
|
186
186
|
)}
|
|
187
187
|
<SmallCTAButton
|
|
188
|
-
icon={<
|
|
188
|
+
icon={<Wallet width={18} height={18} />}
|
|
189
189
|
label="Withdraw Gold"
|
|
190
190
|
disabled={availableGold === 0}
|
|
191
191
|
onClick={() => availableGold > 0 && onMoneyWithdraw()}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
IMarketplaceItem,
|
|
10
10
|
IMarketplaceTransaction,
|
|
11
11
|
} from '@rpg-engine/shared';
|
|
12
|
-
import {
|
|
12
|
+
import { DateTime } from 'pixelarticons/react/DateTime';
|
|
13
13
|
import { Settings2 } from 'pixelarticons/react/Settings2';
|
|
14
14
|
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
15
15
|
import { ShoppingCart } from 'pixelarticons/react/ShoppingCart';
|
|
@@ -262,7 +262,7 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
262
262
|
{
|
|
263
263
|
id: 'history',
|
|
264
264
|
label: 'History',
|
|
265
|
-
icon: <
|
|
265
|
+
icon: <DateTime width={18} height={18} />,
|
|
266
266
|
},
|
|
267
267
|
...(showWalletTab && walletProps
|
|
268
268
|
? [
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
IMarketplaceItem,
|
|
7
7
|
MarketplaceAcceptedCurrency,
|
|
8
8
|
} from '@rpg-engine/shared';
|
|
9
|
-
import {
|
|
9
|
+
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
10
10
|
import { Delete } from 'pixelarticons/react/Delete';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import styled from 'styled-components';
|
|
@@ -134,7 +134,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
|
|
|
134
134
|
|
|
135
135
|
<ActionSection>
|
|
136
136
|
<CTAButton
|
|
137
|
-
icon={onMarketPlaceItemBuy ? <
|
|
137
|
+
icon={onMarketPlaceItemBuy ? <Wallet width={18} height={18} /> : <Delete width={18} height={18} />}
|
|
138
138
|
label={onMarketPlaceItemBuy ? 'Buy' : 'Remove'}
|
|
139
139
|
disabled={disabled}
|
|
140
140
|
onClick={() => {
|
|
@@ -201,6 +201,7 @@ export const GroupedMarketplaceRow: React.FC<IGroupedMarketplaceRowProps> = ({
|
|
|
201
201
|
mainRow={makeRow(bestListing)}
|
|
202
202
|
subRows={otherListings.map(makeRow)}
|
|
203
203
|
badgeLabel="offers"
|
|
204
|
+
metaRightInset={132}
|
|
204
205
|
/>
|
|
205
206
|
);
|
|
206
207
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IItemPack, IPurchase, IProductBlueprint, ItemRarities, ItemSubType, ItemType, UserAccountTypes, PaymentCurrency, PurchaseType } from '@rpg-engine/shared';
|
|
2
|
-
import { Box } from 'pixelarticons/react/Box';
|
|
3
2
|
import { Crown } from 'pixelarticons/react/Crown';
|
|
4
3
|
import { Gift } from 'pixelarticons/react/Gift';
|
|
4
|
+
import { Package } from 'pixelarticons/react/Package';
|
|
5
5
|
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
6
6
|
import React, { ReactNode, useMemo, useState } from 'react';
|
|
7
|
-
import { FaHistory, FaShoppingCart, FaWallet } from 'react-icons/fa';
|
|
7
|
+
import { FaHistory, FaShoppingCart, FaTicketAlt, FaWallet } from 'react-icons/fa';
|
|
8
8
|
import styled from 'styled-components';
|
|
9
9
|
import { uiColors } from '../../constants/uiColors';
|
|
10
10
|
import { DraggableContainer } from '../DraggableContainer';
|
|
@@ -18,10 +18,11 @@ import { useStoreCart } from './hooks/useStoreCart';
|
|
|
18
18
|
import { MetadataCollector } from './MetadataCollector';
|
|
19
19
|
import { StoreItemsSection } from './sections/StoreItemsSection';
|
|
20
20
|
import { StorePacksSection } from './sections/StorePacksSection';
|
|
21
|
+
import { StoreRedeemSection } from './sections/StoreRedeemSection';
|
|
21
22
|
import { StoreItemDetails } from './StoreItemDetails';
|
|
22
23
|
|
|
23
24
|
// Define TabId union type for tab identifiers
|
|
24
|
-
type TabId = 'premium' | 'packs' | 'items' | 'wallet' | 'history';
|
|
25
|
+
type TabId = 'premium' | 'packs' | 'items' | 'wallet' | 'history' | 'redeem';
|
|
25
26
|
|
|
26
27
|
// Define IStoreProps locally as a workaround
|
|
27
28
|
export interface IStoreProps {
|
|
@@ -74,6 +75,12 @@ export interface IStoreProps {
|
|
|
74
75
|
onBuyDC?: () => void;
|
|
75
76
|
/** Currency symbol to display (e.g. "$" for USD, "R$" for BRL). Defaults to "$". */
|
|
76
77
|
currencySymbol?: string;
|
|
78
|
+
/** Callback to redeem a voucher code. When provided, the Redeem tab is shown. */
|
|
79
|
+
onRedeem?: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
|
|
80
|
+
/** Called when the voucher code input gains focus. */
|
|
81
|
+
onRedeemInputFocus?: () => void;
|
|
82
|
+
/** Called when the voucher code input loses focus. */
|
|
83
|
+
onRedeemInputBlur?: () => void;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export type { IFeaturedItem };
|
|
@@ -116,6 +123,9 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
116
123
|
onPurchaseError,
|
|
117
124
|
onBuyDC,
|
|
118
125
|
currencySymbol = '$',
|
|
126
|
+
onRedeem,
|
|
127
|
+
onRedeemInputFocus,
|
|
128
|
+
onRedeemInputBlur,
|
|
119
129
|
}) => {
|
|
120
130
|
const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
|
|
121
131
|
const [activeTab, setActiveTab] = useState<TabId>(() => {
|
|
@@ -230,6 +240,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
230
240
|
// Build tabs dynamically based on props
|
|
231
241
|
const tabIds: TabId[] = [
|
|
232
242
|
...(tabOrder ?? ['premium', 'packs', 'items']),
|
|
243
|
+
...(onRedeem ? ['redeem' as TabId] : []),
|
|
233
244
|
...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
|
|
234
245
|
...((onShowHistory || customHistoryContent) ? ['history' as TabId] : [])
|
|
235
246
|
];
|
|
@@ -318,7 +329,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
318
329
|
items: {
|
|
319
330
|
id: 'items',
|
|
320
331
|
title: 'Items',
|
|
321
|
-
icon: <
|
|
332
|
+
icon: <Package width={18} height={18} />,
|
|
322
333
|
content: (
|
|
323
334
|
<StoreItemsSection
|
|
324
335
|
items={filteredItems.items}
|
|
@@ -335,6 +346,18 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
335
346
|
/>
|
|
336
347
|
),
|
|
337
348
|
},
|
|
349
|
+
redeem: {
|
|
350
|
+
id: 'redeem',
|
|
351
|
+
title: 'Redeem',
|
|
352
|
+
icon: <FaTicketAlt size={16} />,
|
|
353
|
+
content: onRedeem ? (
|
|
354
|
+
<StoreRedeemSection
|
|
355
|
+
onRedeem={onRedeem}
|
|
356
|
+
onInputFocus={onRedeemInputFocus}
|
|
357
|
+
onInputBlur={onRedeemInputBlur}
|
|
358
|
+
/>
|
|
359
|
+
) : null,
|
|
360
|
+
},
|
|
338
361
|
wallet: {
|
|
339
362
|
id: 'wallet',
|
|
340
363
|
title: walletLabel ?? 'Wallet',
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import ReactDOM from 'react-dom';
|
|
7
|
+
import { act, Simulate } from 'react-dom/test-utils';
|
|
8
|
+
import { StoreRedeemSection } from '../sections/StoreRedeemSection';
|
|
9
|
+
|
|
10
|
+
jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
11
|
+
CTAButton: ({ label, onClick, disabled }) => (
|
|
12
|
+
<button onClick={onClick} disabled={disabled} data-testid="cta-button">
|
|
13
|
+
{label}
|
|
14
|
+
</button>
|
|
15
|
+
),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock('styled-components', () => {
|
|
19
|
+
const createTag = (tag) => () => {
|
|
20
|
+
if (tag === 'input') return (props) => <input {...props} />;
|
|
21
|
+
if (tag === 'h3') return ({ children }) => <h3>{children}</h3>;
|
|
22
|
+
if (tag === 'p') return ({ children }) => <p>{children}</p>;
|
|
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('react-icons/fa', () => ({
|
|
36
|
+
FaCheckCircle: () => <span>check</span>,
|
|
37
|
+
FaExclamationCircle: () => <span>error</span>,
|
|
38
|
+
FaTicketAlt: () => <span>ticket</span>,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
jest.mock('../../../constants/uiColors', () => ({
|
|
42
|
+
uiColors: {
|
|
43
|
+
white: '#fff',
|
|
44
|
+
lightGray: '#888',
|
|
45
|
+
green: '#4CAF50',
|
|
46
|
+
red: '#D04648',
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
51
|
+
|
|
52
|
+
describe('StoreRedeemSection', () => {
|
|
53
|
+
let container;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
container = document.createElement('div');
|
|
57
|
+
document.body.appendChild(container);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
62
|
+
document.body.removeChild(container);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should render input and redeem button', () => {
|
|
66
|
+
act(() => {
|
|
67
|
+
ReactDOM.render(
|
|
68
|
+
<StoreRedeemSection onRedeem={jest.fn()} />,
|
|
69
|
+
container
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
expect(container.querySelector('input')).toBeTruthy();
|
|
73
|
+
expect(container.textContent).toContain('Redeem Code');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should disable button when input is empty', () => {
|
|
77
|
+
act(() => {
|
|
78
|
+
ReactDOM.render(
|
|
79
|
+
<StoreRedeemSection onRedeem={jest.fn()} />,
|
|
80
|
+
container
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
84
|
+
expect(button.disabled).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should enable button when input has text', () => {
|
|
88
|
+
act(() => {
|
|
89
|
+
ReactDOM.render(
|
|
90
|
+
<StoreRedeemSection onRedeem={jest.fn()} />,
|
|
91
|
+
container
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
const input = container.querySelector('input');
|
|
95
|
+
act(() => {
|
|
96
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
97
|
+
window.HTMLInputElement.prototype,
|
|
98
|
+
'value'
|
|
99
|
+
).set;
|
|
100
|
+
nativeInputValueSetter.call(input, 'ABC-123');
|
|
101
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
102
|
+
});
|
|
103
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
104
|
+
expect(button.disabled).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should show success with DC amount on valid redeem', async () => {
|
|
108
|
+
const onRedeem = jest.fn().mockResolvedValue({ success: true, dcAmount: 550 });
|
|
109
|
+
|
|
110
|
+
act(() => {
|
|
111
|
+
ReactDOM.render(
|
|
112
|
+
<StoreRedeemSection onRedeem={onRedeem} />,
|
|
113
|
+
container
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const input = container.querySelector('input');
|
|
118
|
+
act(() => {
|
|
119
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
120
|
+
window.HTMLInputElement.prototype,
|
|
121
|
+
'value'
|
|
122
|
+
).set;
|
|
123
|
+
nativeInputValueSetter.call(input, 'CHB-550-ABCDEF1234');
|
|
124
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
128
|
+
await act(async () => {
|
|
129
|
+
button.click();
|
|
130
|
+
await flushPromises();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(onRedeem).toHaveBeenCalledWith('CHB-550-ABCDEF1234');
|
|
134
|
+
expect(container.textContent).toContain('Code Redeemed');
|
|
135
|
+
expect(container.textContent).toContain('+550 DC');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should show error on failed redeem', async () => {
|
|
139
|
+
const onRedeem = jest.fn().mockResolvedValue({ success: false, error: 'Invalid voucher code.' });
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
ReactDOM.render(
|
|
143
|
+
<StoreRedeemSection onRedeem={onRedeem} />,
|
|
144
|
+
container
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const input = container.querySelector('input');
|
|
149
|
+
act(() => {
|
|
150
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
151
|
+
window.HTMLInputElement.prototype,
|
|
152
|
+
'value'
|
|
153
|
+
).set;
|
|
154
|
+
nativeInputValueSetter.call(input, 'INVALID-CODE');
|
|
155
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
159
|
+
await act(async () => {
|
|
160
|
+
button.click();
|
|
161
|
+
await flushPromises();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(container.textContent).toContain('Invalid voucher code.');
|
|
165
|
+
expect(container.textContent).toContain('Try Again');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should reset to idle when "Redeem Another" is clicked after success', async () => {
|
|
169
|
+
const onRedeem = jest.fn().mockResolvedValue({ success: true, dcAmount: 100 });
|
|
170
|
+
|
|
171
|
+
act(() => {
|
|
172
|
+
ReactDOM.render(
|
|
173
|
+
<StoreRedeemSection onRedeem={onRedeem} />,
|
|
174
|
+
container
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const input = container.querySelector('input');
|
|
179
|
+
act(() => {
|
|
180
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
181
|
+
window.HTMLInputElement.prototype,
|
|
182
|
+
'value'
|
|
183
|
+
).set;
|
|
184
|
+
nativeInputValueSetter.call(input, 'CODE123');
|
|
185
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const submitBtn = container.querySelector('[data-testid="cta-button"]');
|
|
189
|
+
await act(async () => {
|
|
190
|
+
submitBtn.click();
|
|
191
|
+
await flushPromises();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(container.textContent).toContain('Redeem Another');
|
|
195
|
+
|
|
196
|
+
const redeemAnotherBtn = Array.from(container.querySelectorAll('[data-testid="cta-button"]'))
|
|
197
|
+
.find(b => b.textContent.includes('Redeem Another'));
|
|
198
|
+
|
|
199
|
+
act(() => { redeemAnotherBtn.click(); });
|
|
200
|
+
|
|
201
|
+
expect(container.querySelector('input')).toBeTruthy();
|
|
202
|
+
expect(container.textContent).toContain('Redeem Code');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should call onInputFocus when input is focused', () => {
|
|
206
|
+
const onInputFocus = jest.fn();
|
|
207
|
+
act(() => {
|
|
208
|
+
ReactDOM.render(
|
|
209
|
+
<StoreRedeemSection onRedeem={jest.fn()} onInputFocus={onInputFocus} />,
|
|
210
|
+
container
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const input = container.querySelector('input');
|
|
215
|
+
act(() => { Simulate.focus(input); });
|
|
216
|
+
expect(onInputFocus).toHaveBeenCalledTimes(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should call onInputBlur when input loses focus', () => {
|
|
220
|
+
const onInputBlur = jest.fn();
|
|
221
|
+
act(() => {
|
|
222
|
+
ReactDOM.render(
|
|
223
|
+
<StoreRedeemSection onRedeem={jest.fn()} onInputBlur={onInputBlur} />,
|
|
224
|
+
container
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const input = container.querySelector('input');
|
|
229
|
+
act(() => { Simulate.blur(input); });
|
|
230
|
+
expect(onInputBlur).toHaveBeenCalledTimes(1);
|
|
231
|
+
});
|
|
232
|
+
});
|