fix(characters): reset route actors on character change
This commit is contained in:
@@ -1,11 +1,23 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { getCharacterBySlug } from "@/data/constants/character";
|
||||||
import { PaymentRouteProvider } from "@/providers/payment-route-provider";
|
import { PaymentRouteProvider } from "@/providers/payment-route-provider";
|
||||||
|
|
||||||
export default function CharacterTipLayout({
|
export default async function CharacterTipLayout({
|
||||||
children,
|
children,
|
||||||
|
params,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
params: Promise<{ characterSlug: string }>;
|
||||||
}) {
|
}) {
|
||||||
return <PaymentRouteProvider>{children}</PaymentRouteProvider>;
|
const { characterSlug } = await params;
|
||||||
|
const character = getCharacterBySlug(characterSlug);
|
||||||
|
if (!character) notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentRouteProvider scopeKey={character.id}>
|
||||||
|
{children}
|
||||||
|
</PaymentRouteProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { act, type ReactNode } from "react";
|
||||||
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const instances = vi.hoisted(() => ({
|
||||||
|
chat: 0,
|
||||||
|
payment: 0,
|
||||||
|
privateRoom: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/stores/chat/chat-context", async () => {
|
||||||
|
const { createElement, useState } = await import("react");
|
||||||
|
return {
|
||||||
|
ChatProvider({
|
||||||
|
characterId,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
characterId: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const [instanceId] = useState(() => ++instances.chat);
|
||||||
|
return createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
"data-character-id": characterId,
|
||||||
|
"data-instance-id": instanceId,
|
||||||
|
"data-testid": "chat-provider",
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/stores/payment/payment-context", async () => {
|
||||||
|
const { createElement, useState } = await import("react");
|
||||||
|
return {
|
||||||
|
PaymentProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [instanceId] = useState(() => ++instances.payment);
|
||||||
|
return createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
"data-instance-id": instanceId,
|
||||||
|
"data-testid": "payment-provider",
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/stores/private-room", async () => {
|
||||||
|
const { createElement, useState } = await import("react");
|
||||||
|
return {
|
||||||
|
PrivateRoomProvider({
|
||||||
|
characterId,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
characterId: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const [instanceId] = useState(() => ++instances.privateRoom);
|
||||||
|
return createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
"data-character-id": characterId,
|
||||||
|
"data-instance-id": instanceId,
|
||||||
|
"data-testid": "private-room-provider",
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/stores/sync/chat-auth-sync", () => ({ ChatAuthSync: () => null }));
|
||||||
|
vi.mock("@/stores/sync/chat-payment-success-sync", () => ({
|
||||||
|
ChatPaymentSuccessSync: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/stores/sync/payment-success-sync", () => ({
|
||||||
|
PaymentSuccessSync: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { ChatRouteProviders } from "@/providers/chat-route-providers";
|
||||||
|
import { PaymentRouteProvider } from "@/providers/payment-route-provider";
|
||||||
|
import { PrivateRoomRouteProvider } from "@/providers/private-room-route-provider";
|
||||||
|
|
||||||
|
describe("character actor route providers", () => {
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
instances.chat = 0;
|
||||||
|
instances.payment = 0;
|
||||||
|
instances.privateRoom = 0;
|
||||||
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
||||||
|
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.append(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => root.unmount());
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recreates chat and chat payment actors when the character changes", () => {
|
||||||
|
renderChat("character_elio");
|
||||||
|
const firstChatInstance = instanceId("chat-provider");
|
||||||
|
const firstPaymentInstance = instanceId("payment-provider");
|
||||||
|
|
||||||
|
renderChat("character_maya");
|
||||||
|
|
||||||
|
expect(instanceId("chat-provider")).not.toBe(firstChatInstance);
|
||||||
|
expect(instanceId("payment-provider")).not.toBe(firstPaymentInstance);
|
||||||
|
expect(testNode("chat-provider").dataset.characterId).toBe(
|
||||||
|
"character_maya",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recreates the private-room actor when the character changes", () => {
|
||||||
|
renderPrivateRoom("character_elio");
|
||||||
|
const firstInstance = instanceId("private-room-provider");
|
||||||
|
|
||||||
|
renderPrivateRoom("character_maya");
|
||||||
|
|
||||||
|
expect(instanceId("private-room-provider")).not.toBe(firstInstance);
|
||||||
|
expect(testNode("private-room-provider").dataset.characterId).toBe(
|
||||||
|
"character_maya",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recreates the route payment actor when its character scope changes", () => {
|
||||||
|
renderPayment("character_elio");
|
||||||
|
const firstInstance = instanceId("payment-provider");
|
||||||
|
|
||||||
|
renderPayment("character_maya");
|
||||||
|
|
||||||
|
expect(instanceId("payment-provider")).not.toBe(firstInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderChat(characterId: string): void {
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<ChatRouteProviders characterId={characterId}>
|
||||||
|
<span>chat</span>
|
||||||
|
</ChatRouteProviders>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPrivateRoom(characterId: string): void {
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<PrivateRoomRouteProvider characterId={characterId}>
|
||||||
|
<span>private room</span>
|
||||||
|
</PrivateRoomRouteProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPayment(scopeKey: string): void {
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<PaymentRouteProvider scopeKey={scopeKey}>
|
||||||
|
<span>payment</span>
|
||||||
|
</PaymentRouteProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNode(testId: string): HTMLElement {
|
||||||
|
const node = container.querySelector<HTMLElement>(
|
||||||
|
`[data-testid="${testId}"]`,
|
||||||
|
);
|
||||||
|
if (!node) throw new Error(`Missing ${testId}`);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function instanceId(testId: string): string | undefined {
|
||||||
|
return testNode(testId).dataset.instanceId;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -19,9 +19,9 @@ export function ChatRouteProviders({
|
|||||||
characterId = DEFAULT_CHARACTER_ID,
|
characterId = DEFAULT_CHARACTER_ID,
|
||||||
}: ChatRouteProvidersProps) {
|
}: ChatRouteProvidersProps) {
|
||||||
return (
|
return (
|
||||||
<PaymentProvider>
|
<PaymentProvider key={characterId}>
|
||||||
<PaymentSuccessSync />
|
<PaymentSuccessSync />
|
||||||
<ChatProvider characterId={characterId}>
|
<ChatProvider key={characterId} characterId={characterId}>
|
||||||
<ChatAuthSync />
|
<ChatAuthSync />
|
||||||
<ChatPaymentSuccessSync />
|
<ChatPaymentSuccessSync />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -5,9 +5,17 @@ import type { ReactNode } from "react";
|
|||||||
import { PaymentProvider } from "@/stores/payment/payment-context";
|
import { PaymentProvider } from "@/stores/payment/payment-context";
|
||||||
import { PaymentSuccessSync } from "@/stores/sync/payment-success-sync";
|
import { PaymentSuccessSync } from "@/stores/sync/payment-success-sync";
|
||||||
|
|
||||||
export function PaymentRouteProvider({ children }: { children: ReactNode }) {
|
export interface PaymentRouteProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
scopeKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PaymentRouteProvider({
|
||||||
|
children,
|
||||||
|
scopeKey,
|
||||||
|
}: PaymentRouteProviderProps) {
|
||||||
return (
|
return (
|
||||||
<PaymentProvider>
|
<PaymentProvider key={scopeKey}>
|
||||||
<PaymentSuccessSync />
|
<PaymentSuccessSync />
|
||||||
{children}
|
{children}
|
||||||
</PaymentProvider>
|
</PaymentProvider>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function PrivateRoomRouteProvider({
|
|||||||
characterId = DEFAULT_CHARACTER_ID,
|
characterId = DEFAULT_CHARACTER_ID,
|
||||||
}: PrivateRoomRouteProviderProps) {
|
}: PrivateRoomRouteProviderProps) {
|
||||||
return (
|
return (
|
||||||
<PrivateRoomProvider characterId={characterId}>
|
<PrivateRoomProvider key={characterId} characterId={characterId}>
|
||||||
{children}
|
{children}
|
||||||
</PrivateRoomProvider>
|
</PrivateRoomProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user