nitro-v36-with-renderer-v36
/
src /
components /
catalog /
views /
page /
layout /
marketplace /
MarketplacePostOfferView.test.tsx
MarketplacePostOfferView.test.tsx 12.8 Ko
Télécharger le fichier
/* @vitest-environment jsdom */
import { GetSessionDataManager } from '@octane/renderer';
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { PropsWithChildren } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
send: vi.fn(),
showConfirm: vi.fn(),
simpleAlert: vi.fn(),
uiHandler: null as (event: any) => void,
messageHandler: null as (event: any) => void,
balanceHandler: null as (event: any) => void,
openUrl: vi.fn(),
stats: null as any,
statsArgs: [] as number[][]
}));
vi.mock('@octane/renderer', async (importOriginal) => {
const actual = await importOriginal<typeof import('@octane/renderer')>();
const composer = (name: string) =>
class {
public readonly name = name;
public readonly args: unknown[];
constructor(...args: unknown[]) {
this.args = args;
}
};
return {
...actual,
GetMarketplaceCanMakeOfferComposer: composer('canMakeOffer'),
MakeOfferMessageComposer: composer('makeOffer'),
MakeMultipleOffersMessageComposer: composer('makeMultipleOffers'),
BuyMarketplaceTokensMessageComposer: composer('buyTokens'),
MarketplaceCanMakeOfferResult: class MarketplaceCanMakeOfferResult {},
NotEnoughBalanceMessageEvent: class NotEnoughBalanceMessageEvent {}
};
});
vi.mock('../../../../../../api', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../../../../api')>();
return {
...actual,
LocalizeText: (key: string) => key,
GetConfigurationValue: (key: string, fallback: unknown) => (key === 'web.shop.relativeUrl' ? '/shop' : fallback),
OpenUrl: mocks.openUrl,
SendMessageComposer: mocks.send
};
});
vi.mock('../../../../../../common', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../../../../common')>();
return {
...actual,
LayoutFurniImageView: () => <div data-testid="furni-image" />,
OctaneCardView: ({ children }: PropsWithChildren) => <div>{children}</div>,
OctaneCardHeaderView: ({ headerText, onCloseClick }: { headerText: string; onCloseClick: () => void }) => (
<div>
<span>{headerText}</span>
<button type="button" onClick={onCloseClick}>
close
</button>
</div>
),
OctaneCardContentView: ({ children }: PropsWithChildren) => <div>{children}</div>
};
});
vi.mock('../../../../../../hooks', () => ({
useMarketplaceConfiguration: () => ({
data: { minimumPrice: 5, maximumPrice: 1000, offerTime: 48, displayTime: 7, commission: 1, credits: 1, advertisements: 5 }
}),
useMarketplaceItemStats: (category: number, furniId: number) => {
mocks.statsArgs.push([category, furniId]);
return category ? mocks.stats : null;
},
useMessageEvent: (type: { name?: string }, handler: (event: any) => void) => {
if (type?.name === 'NotEnoughBalanceMessageEvent') mocks.balanceHandler = handler;
else mocks.messageHandler = handler;
},
useUiEvent: (_type: unknown, handler: (event: any) => void) => {
mocks.uiHandler = handler;
},
useNotification: () => ({ showConfirm: mocks.showConfirm, simpleAlert: mocks.simpleAlert })
}));
import { MarketplacePostOfferView } from './MarketplacePostOfferView';
const furni = (id: number, type = 2270) =>
({
id,
type,
isWallItem: false,
category: 1,
extra: 0,
stuffData: { uniqueNumber: 0, uniqueSeries: 0, rarityLevel: -1, getLegacyString: () => '' }
}) as any;
const open = (items = [furni(11)]) => {
act(() => mocks.uiHandler({ item: items[0], items }));
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 1 }) }));
};
describe('MarketplacePostOfferView', () => {
beforeEach(() => {
mocks.send.mockClear();
mocks.showConfirm.mockClear();
mocks.simpleAlert.mockClear();
mocks.openUrl.mockClear();
mocks.stats = null;
mocks.statsArgs.length = 0;
vi.mocked(GetSessionDataManager).mockReturnValue({
getFloorItemData: (id: number) => (id === 2270 ? { name: 'Troon', className: 'throne' } : null),
getWallItemData: () => null
} as any);
});
afterEach(() => {
cleanup();
vi.mocked(GetSessionDataManager).mockReset();
});
it('asks whether selling is allowed, then opens with the furnidata name and the stats request', () => {
render(<MarketplacePostOfferView />);
act(() => mocks.uiHandler({ item: furni(11), items: [furni(11)] }));
expect(mocks.send.mock.calls[0][0].name).toBe('canMakeOffer');
expect(screen.queryByTestId('marketplace-post-offer-name')).toBeNull();
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 1 }) }));
expect(screen.getByTestId('marketplace-post-offer-name').textContent).toBe('Troon');
expect(mocks.statsArgs.at(-1)).toEqual([1, 2270]);
expect(screen.getByText('Your offer will be visible to other users for 2 day(s).')).toBeTruthy();
});
it('keeps Post disabled below the minimum and shows the revenue from the minimum up', () => {
render(<MarketplacePostOfferView />);
open();
const post = screen.getByRole('button', { name: 'Post offer' });
const final = screen.getByTestId('marketplace-post-offer-final');
expect(post).toHaveProperty('disabled', true);
expect(final.textContent).toBe('You can offer items for between 5 and 1000 credits.');
fireEvent.change(screen.getByLabelText('Asking price:'), { target: { value: '4' } });
expect(post).toHaveProperty('disabled', true);
fireEvent.change(screen.getByLabelText('Asking price:'), { target: { value: '25x' } });
expect(post).toHaveProperty('disabled', false);
expect(final.textContent).toBe('Expected revenue per item: 25');
fireEvent.change(screen.getByLabelText('Asking price:'), { target: { value: '5000' } });
expect((screen.getByLabelText('Asking price:') as HTMLInputElement).value).toBe('1000');
});
it('shows only the stat lines above zero and copies the suggested price', () => {
mocks.stats = { averagePrice: 101, lowestCurrentPrice: 0, suggestedPrice: 100 };
render(<MarketplacePostOfferView />);
open();
expect(screen.getByTestId('marketplace-post-offer-average').textContent).toBe('Average price (7 days): 101');
expect(screen.queryByTestId('marketplace-post-offer-lowest')).toBeNull();
expect(screen.getByTestId('marketplace-post-offer-suggested').textContent).toBe('Suggested price: 100');
fireEvent.click(screen.getByRole('button', { name: 'Copy suggested price' }));
expect((screen.getByLabelText('Asking price:') as HTMLInputElement).value).toBe('100');
});
it('hides the copy button once the stats carry no suggestion', () => {
mocks.stats = { averagePrice: 0, lowestCurrentPrice: 0, suggestedPrice: 0 };
render(<MarketplacePostOfferView />);
open();
expect(screen.queryByRole('button', { name: 'Copy suggested price' })).toBeNull();
});
it('closes into the confirmation and sends one multi-offer for the chosen amount', () => {
render(<MarketplacePostOfferView />);
open([furni(11), furni(12), furni(13)]);
fireEvent.change(screen.getByLabelText('Asking price:'), { target: { value: '30' } });
fireEvent.change(screen.getByLabelText('Amount (max 3)'), { target: { value: '9' } });
expect((screen.getByLabelText('Amount (max 3)') as HTMLInputElement).value).toBe('3');
fireEvent.click(screen.getByRole('button', { name: 'Post offer' }));
expect(screen.queryByTestId('marketplace-post-offer-name')).toBeNull();
expect(mocks.showConfirm).toHaveBeenCalledTimes(1);
expect(mocks.showConfirm.mock.calls[0][0]).toBe('You are about to sell 3 x Troon for 30 credit(s) each. When all are sold you will earn 90 credit(s).');
mocks.send.mockClear();
act(() => mocks.showConfirm.mock.calls[0][1]());
const sent = mocks.send.mock.calls[0][0];
expect(sent.name).toBe('makeMultipleOffers');
expect(sent.args).toEqual([30, 1, [11, 12, 13]]);
});
it('Cancel closes without a confirmation', () => {
render(<MarketplacePostOfferView />);
open();
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(screen.queryByTestId('marketplace-post-offer-name')).toBeNull();
expect(mocks.showConfirm).not.toHaveBeenCalled();
});
it('offers the token batch when no selling tokens are left and opens the offer once they are bought', () => {
render(<MarketplacePostOfferView />);
act(() => mocks.uiHandler({ item: furni(11), items: [furni(11)] }));
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 4, tokenCount: 0 }) }));
expect(screen.queryByTestId('marketplace-post-offer-name')).toBeNull();
expect(screen.getByTestId('marketplace-buy-tokens-info').textContent).toBe(
'Selling furni on the marketplace costs 1 credit for 5 advertisements. You have none left, so your next advertisement costs 1 credit. The 4 after it are free.'
);
fireEvent.click(screen.getByRole('button', { name: 'Buy (1 credit)' }));
expect(mocks.send.mock.calls.at(-1)[0].name).toBe('buyTokens');
expect(screen.queryByTestId('marketplace-buy-tokens-info')).toBeNull();
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 1, tokenCount: 5 }) }));
expect(screen.getByTestId('marketplace-post-offer-name').textContent).toBe('Troon');
});
it('shows the no-credits window when the token purchase is refused and links to the shop', () => {
render(<MarketplacePostOfferView />);
act(() => mocks.uiHandler({ item: furni(11), items: [furni(11)] }));
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 4, tokenCount: 0 }) }));
fireEvent.click(screen.getByRole('button', { name: 'Buy (1 credit)' }));
act(() => mocks.balanceHandler({ getParser: () => ({ notEnoughCredits: true, notEnoughActivityPoints: false }) }));
expect(screen.getByTestId('marketplace-no-credits-info').textContent).toBe('You need 1 credit for five advertisements.');
fireEvent.click(screen.getByRole('button', { name: 'Get credits' }));
expect(mocks.openUrl).toHaveBeenCalledWith('/shop');
expect(screen.queryByTestId('marketplace-no-credits-info')).toBeNull();
// The items were released: a late answer opens nothing.
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 1, tokenCount: 5 }) }));
expect(screen.queryByTestId('marketplace-post-offer-name')).toBeNull();
});
it('keeps the items while the user reads the buy-tokens window', () => {
vi.useFakeTimers();
try {
render(<MarketplacePostOfferView />);
act(() => mocks.uiHandler({ item: furni(11), items: [furni(11)] }));
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 4, tokenCount: 0 }) }));
act(() => vi.advanceTimersByTime(3000));
fireEvent.click(screen.getByRole('button', { name: 'Buy (1 credit)' }));
// The timer of the first check must not release the items of the purchase.
act(() => vi.advanceTimersByTime(4000));
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 1, tokenCount: 5 }) }));
expect(screen.getByTestId('marketplace-post-offer-name').textContent).toBe('Troon');
} finally {
vi.useRealTimers();
}
});
it('ignores a balance refusal that is not about the token purchase', () => {
render(<MarketplacePostOfferView />);
act(() => mocks.balanceHandler({ getParser: () => ({ notEnoughCredits: true }) }));
expect(screen.queryByTestId('marketplace-no-credits-info')).toBeNull();
});
it('cancelling the buy-tokens window releases the items', () => {
render(<MarketplacePostOfferView />);
act(() => mocks.uiHandler({ item: furni(11), items: [furni(11)] }));
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 4, tokenCount: 0 }) }));
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(screen.queryByTestId('marketplace-buy-tokens-info')).toBeNull();
act(() => mocks.messageHandler({ getParser: () => ({ resultCode: 1, tokenCount: 5 }) }));
expect(screen.queryByTestId('marketplace-post-offer-name')).toBeNull();
expect(mocks.send.mock.calls.map((call) => call[0].name)).toEqual(['canMakeOffer']);
});
});
