nitro-v36-with-renderer-v36
/
src /
components /
catalog /
views /
page /
widgets /
CatalogPurchaseWidgetView.test.tsx
CatalogPurchaseWidgetView.test.tsx 15.9 Ko
Télécharger le fichier
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { GetClubMemberLevel } from '../../../../../api';
import {
useCatalogActions,
useCatalogBundleDiscountRuleset,
useCatalogData,
useCatalogSkipPurchaseConfirmation,
useCatalogUiState,
useNotification,
usePurse
} from '../../../../../hooks';
import { CatalogPurchaseWidgetView } from './CatalogPurchaseWidgetView';
vi.mock('@octane/renderer', () => ({
CreateLinkEvent: vi.fn(),
LtdRaffleEnteredMessageEvent: class LtdRaffleEnteredMessageEvent {},
LtdRaffleResultMessageEvent: class LtdRaffleResultMessageEvent {},
NotEnoughBalanceMessageEvent: class NotEnoughBalanceMessageEvent {},
PurchaseFromCatalogComposer: class {}
}));
vi.mock('../../../../../api', () => ({
BuilderFurniPlaceableStatus: {
OKAY: 0,
MISSING_OFFER: 1,
FURNI_LIMIT_REACHED: 2,
NOT_IN_ROOM: 3,
NOT_ROOM_OWNER: 4,
GUILD_ROOM: 5,
VISITORS_IN_ROOM: 6,
NOT_GROUP_ADMIN: 7
},
CatalogPurchaseState: { CONFIRM: 1, FAILED: 3, NONE: 0, PURCHASE: 2, SOLD_OUT: 4 },
CatalogType: { BUILDER: 'BUILDERS_CLUB', NORMAL: 'NORMAL' },
DispatchUiEvent: vi.fn(),
GetClubMemberLevel: vi.fn(() => 0),
GetConfigurationValue: vi.fn((_key: string, fallback: unknown) => fallback),
LocalizeText: (key: string) => key,
NotificationBubbleType: { LTD: 'ltd' },
Offer: { PRICING_MODEL_SINGLE: 'single' },
OpenUrl: vi.fn(),
ProductTypeEnum: { FLOOR: 's', HABBICON: 'h', WALL: 'i' },
SendMessageComposer: vi.fn()
}));
vi.mock('../../../../../api/catalog/CatalogBundleDiscount', () => ({ getCatalogBundlePrice: (price: number) => ({ price }) }));
const habbiconCatalog = vi.hoisted(() => ({ entries: [] as { id: number; owned?: boolean; claimable?: boolean }[] }));
vi.mock('../../../../../api/habbicons', () => ({ useHabbiconCatalog: () => habbiconCatalog }));
vi.mock('../../../../../api/utils/localizeWithFallback', () => ({ localizeWithFallback: (_key: string, fallback: string) => fallback }));
vi.mock('../../../../../common', () => ({
LayoutLoadingSpinnerView: () => null,
Text: ({ children }: any) => <span>{children}</span>
}));
vi.mock('../../../../../events', () => ({
CatalogEvent: class {},
CatalogInitGiftEvent: class {},
CatalogPurchasedEvent: { PURCHASE_SUCCESS: 'purchase-success' },
CatalogPurchaseFailureEvent: { PURCHASE_FAILED: 'purchase-failed' },
CatalogPurchaseNotAllowedEvent: { NOT_ALLOWED: 'purchase-not-allowed' },
CatalogPurchaseSoldOutEvent: { SOLD_OUT: 'purchase-sold-out' }
}));
const messageHandlers = vi.hoisted(() => new Map<string, (event: unknown) => void>());
const confirmProps = vi.hoisted(() => ({ current: null as any }));
vi.mock('../../../../../hooks', () => ({
useMessageEvent: (eventType: { name: string }, handler: (event: unknown) => void) => {
messageHandlers.set(eventType.name, handler);
},
useCatalogActions: vi.fn(),
useCatalogBundleDiscountRuleset: vi.fn(),
useCatalogData: vi.fn(),
useCatalogSkipPurchaseConfirmation: vi.fn(),
useCatalogUiState: vi.fn(),
useNotification: vi.fn(),
usePurse: vi.fn(),
useUiEvent: vi.fn()
}));
vi.mock('../../CatalogPurchaseConfirmView', () => ({
CatalogPurchaseConfirmView: (props: any) => {
confirmProps.current = props;
return null;
}
}));
const makeOffer = (overrides: Record<string, unknown> = {}) => ({
activityPointType: 0,
bundlePurchaseAllowed: false,
clubLevel: 0,
giftable: true,
haveOffer: true,
isLazy: false,
isRentOffer: false,
offerId: 1,
page: { pageId: 5 },
priceInActivityPoints: 0,
priceInCredits: 3,
pricingModel: 'single',
product: { isUniqueLimitedItem: false, productClassId: 10, productType: 's' },
...overrides
});
const setOffer = (offer: Record<string, unknown>) => {
vi.mocked(useCatalogData).mockReturnValue({ currentOffer: offer, currentPage: { pageId: 5 } } as any);
};
afterEach(cleanup);
beforeEach(() => {
vi.clearAllMocks();
messageHandlers.clear();
confirmProps.current = null;
habbiconCatalog.entries = [];
vi.mocked(GetClubMemberLevel).mockReturnValue(0);
vi.mocked(useCatalogActions).mockReturnValue({
getBuilderFurniPlaceableStatus: () => 0,
getNodesByOfferId: () => [],
requestOfferToMover: vi.fn(),
resetPlacedOfferData: vi.fn()
} as any);
vi.mocked(useCatalogBundleDiscountRuleset).mockReturnValue({ data: null } as any);
vi.mocked(useCatalogSkipPurchaseConfirmation).mockReturnValue([false] as any);
vi.mocked(useCatalogUiState).mockReturnValue({
currentType: 'NORMAL',
giftReceiver: null,
purchaseOptions: { extraData: null, extraParamRequired: false, previewStuffData: null, quantity: 1 },
setCatalogPlaceMultipleObjects: vi.fn(),
setPurchaseOptions: vi.fn()
} as any);
vi.mocked(useNotification).mockReturnValue({ showConfirm: vi.fn(), showSingleBubble: vi.fn(), simpleAlert: vi.fn() } as any);
vi.mocked(usePurse).mockReturnValue({ getCurrencyAmount: () => 100 } as any);
setOffer(makeOffer());
});
describe('catalog purchase widget', () => {
it('offers the gift and buy buttons for an offer the player can buy', () => {
render(<CatalogPurchaseWidgetView />);
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.gift' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.buy' })).toBeEnabled();
});
it('replaces both buttons with the club invitation below the required club level', () => {
setOffer(makeOffer({ clubLevel: 1 }));
render(<CatalogPurchaseWidgetView />);
expect(screen.getByText('catalog.buy.widget.get.vip.to.unlock.this.product')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'catalog.buy.widget.get.vip.button' })).toBeEnabled();
expect(screen.queryByRole('button', { name: 'catalog.purchase_confirmation.gift' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'catalog.purchase_confirmation.buy' })).not.toBeInTheDocument();
});
it('keeps the purchase buttons for a member of the required club level', () => {
vi.mocked(GetClubMemberLevel).mockReturnValue(1);
setOffer(makeOffer({ clubLevel: 1 }));
render(<CatalogPurchaseWidgetView />);
expect(screen.queryByText('catalog.buy.widget.get.vip.to.unlock.this.product')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.buy' })).toBeEnabled();
});
it('shows the owned state instead of the club invitation for an owned habbicon', () => {
habbiconCatalog.entries = [{ id: 10, owned: true }];
setOffer(makeOffer({ clubLevel: 1, product: { isUniqueLimitedItem: false, productClassId: 10, productType: 'h' } }));
render(<CatalogPurchaseWidgetView />);
expect(screen.queryByText('catalog.buy.widget.get.vip.to.unlock.this.product')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Owned' })).toBeDisabled();
});
it('keeps the buy label on the disabled button while the offer is unavailable', () => {
setOffer(makeOffer({ haveOffer: false }));
render(<CatalogPurchaseWidgetView />);
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.buy' })).toBeDisabled();
expect(screen.queryByText('catalog.alert.not_available')).not.toBeInTheDocument();
});
it('keeps the rent label on the disabled button while a rent offer is unavailable', () => {
setOffer(makeOffer({ haveOffer: false, isRentOffer: true }));
render(<CatalogPurchaseWidgetView />);
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.rent' })).toBeDisabled();
});
it('shows the loading label while a lazy offer resolves', () => {
setOffer(makeOffer({ haveOffer: false, isLazy: true }));
render(<CatalogPurchaseWidgetView />);
expect(screen.getByRole('button', { name: 'generic.loading' })).toBeDisabled();
});
it('lets a layout block the purchase without hiding the buttons', () => {
render(<CatalogPurchaseWidgetView disabled />);
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.gift' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'catalog.purchase_confirmation.buy' })).toBeDisabled();
});
describe('a balance the server finds short (AIR NotEnoughBalanceMessageEvent)', () => {
const notEnough = (notEnoughCredits: boolean, notEnoughActivityPoints: boolean, activityPointType: number) => ({
getParser: () => ({ activityPointType, notEnoughActivityPoints, notEnoughCredits })
});
const startPurchase = () => {
render(<CatalogPurchaseWidgetView />);
fireEvent.click(screen.getByRole('button', { name: 'catalog.purchase_confirmation.buy' }));
act(() => confirmProps.current.onConfirm());
confirmProps.current = null;
};
it('ends the purchase and offers the shop when credits are short', () => {
const { showConfirm } = vi.mocked(useNotification)();
startPurchase();
act(() => messageHandlers.get('NotEnoughBalanceMessageEvent')(notEnough(true, false, 0)));
expect(confirmProps.current).toBeNull();
expect(showConfirm).toHaveBeenCalledWith(
'catalog.alert.notenough.credits.description',
expect.any(Function),
expect.any(Function),
null,
null,
'catalog.alert.notenough.title'
);
});
it('names the short currency when activity points are short', () => {
const { simpleAlert } = vi.mocked(useNotification)();
startPurchase();
act(() => messageHandlers.get('NotEnoughBalanceMessageEvent')(notEnough(false, true, 5)));
expect(simpleAlert).toHaveBeenCalledWith(
'catalog.alert.notenough.activitypoints.description',
null,
null,
null,
'catalog.alert.notenough.activitypoints.title.5'
);
});
it('ignores a short balance for a purchase this widget did not send', () => {
const { showConfirm, simpleAlert } = vi.mocked(useNotification)();
render(<CatalogPurchaseWidgetView />);
act(() => messageHandlers.get('NotEnoughBalanceMessageEvent')(notEnough(true, false, 0)));
expect(showConfirm).not.toHaveBeenCalled();
expect(simpleAlert).not.toHaveBeenCalled();
});
});
describe('LTD raffle (AIR PurchaseConfirmationDialog raffle_container)', () => {
const startLimitedPurchase = () => {
setOffer(makeOffer({ product: { isUniqueLimitedItem: true, uniqueLimitedItemsLeft: 2, productClassId: 10, productType: 's' } }));
const view = render(<CatalogPurchaseWidgetView />);
fireEvent.click(screen.getByRole('button', { name: 'catalog.purchase_confirmation.buy' }));
act(() => confirmProps.current.onConfirm());
act(() => messageHandlers.get('LtdRaffleEnteredMessageEvent')({}));
return view;
};
it('keeps the confirmation open with the raffling line once the purchase enters the raffle', () => {
startLimitedPurchase();
expect(confirmProps.current.isRaffling).toBe(true);
expect(confirmProps.current.isSubmitting).toBe(true);
});
it('closes the dialog without a note when the draw result arrives', () => {
const { showSingleBubble } = vi.mocked(useNotification)();
startLimitedPurchase();
confirmProps.current = null;
act(() => messageHandlers.get('LtdRaffleResultMessageEvent')({}));
expect(confirmProps.current).toBeNull();
expect(showSingleBubble).not.toHaveBeenCalled();
});
it('leaves the ongoing note when the dialog is closed while the raffle runs', () => {
const { showSingleBubble } = vi.mocked(useNotification)();
startLimitedPurchase();
act(() => confirmProps.current.onCancel());
expect(showSingleBubble).toHaveBeenCalledWith('This LTD purchase has not been processed yet!', 'ltd');
});
it('leaves the ongoing note when the catalog closes while the raffle runs', () => {
const { showSingleBubble } = vi.mocked(useNotification)();
const { unmount } = startLimitedPurchase();
unmount();
expect(showSingleBubble).toHaveBeenCalledWith('This LTD purchase has not been processed yet!', 'ltd');
});
it('ignores a raffle entry for a purchase this widget did not send', () => {
render(<CatalogPurchaseWidgetView />);
act(() => messageHandlers.get('LtdRaffleEnteredMessageEvent')({}));
expect(confirmProps.current).toBeNull();
});
});
describe('Builders Club builder widget (AIR builderWidget)', () => {
const builderSetup = (status: number) => {
const requestOfferToMover = vi.fn();
vi.mocked(useCatalogActions).mockReturnValue({
getBuilderFurniPlaceableStatus: () => status,
getNodesByOfferId: () => [],
requestOfferToMover,
resetPlacedOfferData: vi.fn()
} as any);
vi.mocked(useCatalogUiState).mockReturnValue({
currentType: 'BUILDERS_CLUB',
giftReceiver: null,
purchaseOptions: { extraData: null, extraParamRequired: false, previewStuffData: null, quantity: 1 },
setCatalogPlaceMultipleObjects: vi.fn(),
setPurchaseOptions: vi.fn()
} as any);
setOffer(makeOffer({ product: { productType: 's' } }));
return requestOfferToMover;
};
it('shows the hint and places the furni while it can be placed', () => {
const requestOfferToMover = builderSetup(0);
render(<CatalogPurchaseWidgetView />);
expect(screen.getByText('builder.placement_widget.hint')).toBeInTheDocument();
expect(screen.queryByRole('status')).toBeNull();
fireEvent.click(screen.getByText('builder.placement_widget.place_one'));
expect(requestOfferToMover).toHaveBeenCalledTimes(1);
});
it('shows the not-in-room error with its icon and disables both buttons outside a room', () => {
const requestOfferToMover = builderSetup(3);
render(<CatalogPurchaseWidgetView />);
expect(screen.getByRole('status')).toHaveAttribute('data-error', 'notroom');
expect(screen.getByText('builder.placement_widget.error.not_in_room')).toBeInTheDocument();
expect(screen.getByText('builder.placement_widget.place_one')).toBeDisabled();
expect(screen.getByText('builder.placement_widget.place_many')).toBeDisabled();
fireEvent.click(screen.getByText('builder.placement_widget.place_many'));
expect(requestOfferToMover).not.toHaveBeenCalled();
});
it('disables the buttons at the furni limit, with the limit error, as AIR does', () => {
builderSetup(2);
render(<CatalogPurchaseWidgetView />);
expect(screen.getByRole('status')).toHaveAttribute('data-error', 'furnilimit');
expect(screen.getByText('builder.placement_widget.place_one')).toBeDisabled();
});
it('disables the buttons without an error strip when the offer is missing', () => {
builderSetup(1);
render(<CatalogPurchaseWidgetView />);
expect(screen.queryByRole('status')).toBeNull();
expect(screen.getByText('builder.placement_widget.place_one')).toBeDisabled();
});
});
});
