fix(e2e): stabilize authenticated unlock flows
This commit is contained in:
@@ -55,6 +55,7 @@ function makeChatState(overrides: Partial<ChatState> = {}): ChatState {
|
||||
requiredCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
historyLoaded: true,
|
||||
networkHistoryLoaded: true,
|
||||
historyTotal: 0,
|
||||
historyLimit: 50,
|
||||
nextHistoryOffset: 0,
|
||||
@@ -267,6 +268,8 @@ describe("chat history pagination helpers", () => {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
expect(nextState.networkHistoryLoaded).toBe(true);
|
||||
|
||||
expect(nextState.historyLimit).toBe(50);
|
||||
expect(nextState.nextHistoryOffset).toBe(50);
|
||||
expect(nextState.historyTotal).toBe(120);
|
||||
|
||||
@@ -22,6 +22,8 @@ interface ChatState {
|
||||
upgradeReason: MachineContext["upgradeReason"];
|
||||
/** True as soon as a local snapshot or the first network result is renderable. */
|
||||
historyLoaded: boolean;
|
||||
/** True after the network history request has completed or failed. */
|
||||
networkHistoryLoaded: boolean;
|
||||
hasMoreHistory: boolean;
|
||||
isLoadingMoreHistory: boolean;
|
||||
unlockHistoryPromptVisible: boolean;
|
||||
@@ -93,6 +95,7 @@ function selectChatState(state: ChatSnapshot): SelectedChatState {
|
||||
upgradePromptVisible: state.context.upgradePromptVisible,
|
||||
upgradeReason: state.context.upgradeReason,
|
||||
historyLoaded: state.context.historyLoaded,
|
||||
networkHistoryLoaded: state.context.networkHistoryLoaded,
|
||||
hasMoreHistory:
|
||||
state.context.historyLoaded &&
|
||||
state.context.nextHistoryOffset < state.context.historyTotal,
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface ChatState {
|
||||
shortfallCredits: number;
|
||||
/** history 首屏可展示标志(本地快照加载完成即可进入 ready)。 */
|
||||
historyLoaded: boolean;
|
||||
/** 网络历史已返回或已明确失败;用于避免过早判定深链消息不存在。 */
|
||||
networkHistoryLoaded: boolean;
|
||||
historyTotal: number;
|
||||
historyLimit: number;
|
||||
nextHistoryOffset: number;
|
||||
@@ -80,6 +82,7 @@ export function createInitialChatState(
|
||||
requiredCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
historyLoaded: false,
|
||||
networkHistoryLoaded: false,
|
||||
historyTotal: 0,
|
||||
historyLimit: 50,
|
||||
nextHistoryOffset: 0,
|
||||
|
||||
@@ -29,6 +29,7 @@ export function applyNetworkHistoryLoadedOutput(
|
||||
ChatState,
|
||||
| "messages"
|
||||
| "historyLoaded"
|
||||
| "networkHistoryLoaded"
|
||||
| "historyTotal"
|
||||
| "historyLimit"
|
||||
| "nextHistoryOffset"
|
||||
@@ -47,6 +48,7 @@ export function applyNetworkHistoryLoadedOutput(
|
||||
return {
|
||||
messages: [...output.messages, ...optimisticTail],
|
||||
historyLoaded: true,
|
||||
networkHistoryLoaded: true,
|
||||
historyTotal: Math.max(0, output.total),
|
||||
historyLimit,
|
||||
nextHistoryOffset: historyLimit,
|
||||
|
||||
@@ -55,6 +55,7 @@ const markHistoryLoadFailedAction = baseChatMachineSetup.assign(
|
||||
const characterErrorCode = getCharacterErrorCode(event.error);
|
||||
return {
|
||||
historyLoaded: true,
|
||||
networkHistoryLoaded: true,
|
||||
characterErrorCode,
|
||||
canSendMessage:
|
||||
characterErrorCode === "CHARACTER_DISABLED" ||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { StrictMode } from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
chatDispatch: vi.fn(),
|
||||
getLoginToken: vi.fn(async () => ({
|
||||
success: true as const,
|
||||
data: "email-token",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/data/storage/auth", () => ({
|
||||
AuthStorage: {
|
||||
getInstance: () => ({ getLoginToken: mocks.getLoginToken }),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/auth/auth-context", () => ({
|
||||
selectAuthIsLoading: () => false,
|
||||
useAuthSelector: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
context: {
|
||||
hasInitialized: true,
|
||||
loginStatus: "email",
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/chat/chat-context", () => ({
|
||||
useChatDispatch: () => mocks.chatDispatch,
|
||||
}));
|
||||
|
||||
import { ChatAuthSync } from "../chat-auth-sync";
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
let root: ReturnType<typeof createRoot> | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (root) act(() => root?.unmount());
|
||||
container?.remove();
|
||||
container = null;
|
||||
root = null;
|
||||
mocks.chatDispatch.mockReset();
|
||||
mocks.getLoginToken.mockClear();
|
||||
});
|
||||
|
||||
describe("ChatAuthSync", () => {
|
||||
it("dispatches the authenticated session after StrictMode restarts the effect", async () => {
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<StrictMode>
|
||||
<ChatAuthSync />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(mocks.chatDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.chatDispatch).toHaveBeenCalledWith({
|
||||
type: "ChatUserLogin",
|
||||
token: "email-token",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,14 +34,15 @@ export function ChatAuthSync() {
|
||||
|
||||
const sessionKey = authState.loginStatus;
|
||||
if (prevSessionKeyRef.current === sessionKey) return;
|
||||
prevSessionKeyRef.current = sessionKey;
|
||||
|
||||
if (authState.loginStatus === "notLoggedIn") {
|
||||
prevSessionKeyRef.current = sessionKey;
|
||||
chatDispatch({ type: "ChatLogout" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (authState.loginStatus === "guest") {
|
||||
prevSessionKeyRef.current = sessionKey;
|
||||
chatDispatch({ type: "ChatGuestLogin" });
|
||||
return;
|
||||
}
|
||||
@@ -50,6 +51,7 @@ export function ChatAuthSync() {
|
||||
void (async () => {
|
||||
const tokenR = await AuthStorage.getInstance().getLoginToken();
|
||||
if (!cancelled && tokenR.success && tokenR.data) {
|
||||
prevSessionKeyRef.current = sessionKey;
|
||||
chatDispatch({
|
||||
type: "ChatUserLogin",
|
||||
token: tokenR.data,
|
||||
|
||||
@@ -29,12 +29,13 @@ export function ChatPaymentSuccessSync() {
|
||||
|
||||
const paidKey = `${paymentState.currentOrderId}:${paymentState.orderStatus}`;
|
||||
if (lastPaidKeyRef.current === paidKey) return;
|
||||
lastPaidKeyRef.current = paidKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
if (await hasPendingChatUnlock(characterId)) return;
|
||||
if (!cancelled) chatDispatch({ type: "ChatPaymentSucceeded" });
|
||||
const hasPendingUnlock = await hasPendingChatUnlock(characterId);
|
||||
if (cancelled) return;
|
||||
lastPaidKeyRef.current = paidKey;
|
||||
if (!hasPendingUnlock) chatDispatch({ type: "ChatPaymentSucceeded" });
|
||||
})();
|
||||
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user