Files
plp/apps/web/src/stores/chat.spec.ts
T
root 7ca855f19c feat(web): 构建漂流瓶移动端 PWA
- 演示登录、内存令牌、单 Promise 刷新与路由守卫\n- 投瓶、捞瓶、首次回复、会话与实时聊天\n- 独立管理员登录与审核/举报工作台\n- Socket 认证续期、断线补齐与账号切换清理\n- PWA 离线壳、深海主题与移动端无障碍
2026-09-17 22:19:24 +08:00

175 lines
5.1 KiB
TypeScript

import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { flushPromises } from "@vue/test-utils";
import { api, useAuthStore } from "./auth";
import { useChatStore } from "./chat";
import type { Conversation } from "./chat";
import type { ChatMessage } from "./messages";
const socket = vi.hoisted(() => ({
connected: false,
on: vi.fn(),
connect: vi.fn(),
disconnect: vi.fn(),
}));
vi.mock("../api/socket", () => ({
createChatSocket: () => socket,
sendSocketMessage: vi.fn(),
}));
const me = {
accountId: "a",
publicId: "p",
nickname: null,
avatarColor: null,
bio: null,
profileReviewStatus: null,
};
function message(seq: number): ChatMessage {
return {
id: `m-${seq}`,
conversationId: "c-1",
sender: { publicId: "p" },
clientMsgId: `client-${seq}`,
seq: String(seq),
text: String(seq),
status: "sent",
sentAt: new Date(seq).toISOString(),
};
}
const conversation = (): Conversation => ({
id: "c-1",
status: "ACTIVE",
lastMessageAt: new Date(0).toISOString(),
lastMessage: null,
unread: "0",
lastReadSeq: "0",
});
beforeEach(() => {
setActivePinia(createPinia());
vi.restoreAllMocks();
socket.connected = false;
socket.disconnect.mockClear();
});
describe("chat history synchronization", () => {
it("loads every page beyond 100 messages and reliably deduplicates overlap", async () => {
const pages = [
Array.from({ length: 100 }, (_, index) => message(index + 1)),
Array.from({ length: 100 }, (_, index) => message(index + 100)),
Array.from({ length: 51 }, (_, index) => message(index + 199)),
[],
];
const get = vi
.spyOn(api, "get")
.mockImplementation(() =>
Promise.resolve({ items: pages.shift() ?? [] }),
);
const chat = useChatStore();
const result = await chat.loadMessages("c-1");
expect(result).toBeDefined();
expect(result ?? []).toHaveLength(249);
expect(result?.at(-1)?.seq).toBe("249");
expect(get).toHaveBeenCalledTimes(3);
expect(get.mock.calls.map(([path]) => path)).toEqual([
"/conversations/c-1/messages?afterSeq=0&limit=100",
"/conversations/c-1/messages?afterSeq=100&limit=100",
"/conversations/c-1/messages?afterSeq=199&limit=100",
]);
});
it("stops paging after the page cap to bound waterfall requests", async () => {
const pages = [
Array.from({ length: 100 }, (_, index) => message(index + 1)),
Array.from({ length: 100 }, (_, index) => message(index + 101)),
Array.from({ length: 100 }, (_, index) => message(index + 201)),
Array.from({ length: 100 }, (_, index) => message(index + 301)),
Array.from({ length: 100 }, (_, index) => message(index + 401)),
[],
];
const get = vi
.spyOn(api, "get")
.mockImplementation(() =>
Promise.resolve({ items: pages.shift() ?? [] }),
);
const chat = useChatStore();
const result = await chat.loadMessages("c-1");
expect(result ?? []).toHaveLength(500);
expect(get.mock.calls.length).toBeLessThanOrEqual(5);
expect(get.mock.calls.at(-1)?.[0]).toBe(
"/conversations/c-1/messages?afterSeq=400&limit=100",
);
});
it("syncs only the most recent active conversations when reconnecting", async () => {
const get = vi.spyOn(api, "get").mockResolvedValue({ items: [] });
const chat = useChatStore();
chat.conversations = Array.from({ length: 30 }, (_, index) => {
const id = `c-${index}`;
return {
id,
status: "ACTIVE",
lastMessageAt: new Date(1700000000000 + index).toISOString(),
lastMessage: null,
unread: "0",
lastReadSeq: "0",
};
});
await chat.syncAll();
expect(
get.mock.calls.filter(([path]) => path.includes("/messages")),
).toHaveLength(5);
expect(
get.mock.calls.filter(([path]) => path.includes("/messages"))[0]![0],
).toContain("c-29");
});
});
describe("chat session lifecycle", () => {
it("disconnects and clears account state when authentication ends", async () => {
vi.spyOn(api, "post").mockResolvedValue({ accessToken: "fresh" });
vi.spyOn(api, "get").mockResolvedValue(me);
const auth = useAuthStore();
await auth.login("13800000000", "123456", "device");
expect(auth.isAuthenticated).toBe(true);
const chat = useChatStore();
chat.conversations = [conversation()];
chat.messages = { "c-1": [message(1)] };
expect(chat.conversations).toHaveLength(1);
auth.me = null;
await flushPromises();
expect(auth.isAuthenticated).toBe(false);
expect(socket.disconnect).toHaveBeenCalled();
expect(chat.conversations).toEqual([]);
expect(chat.messages).toEqual({});
expect(chat.connected).toBe(false);
});
it("reset clears conversations and disconnects even when called directly", () => {
const auth = useAuthStore();
auth.me = me;
const chat = useChatStore();
chat.conversations = [conversation()];
chat.messages = { "c-1": [message(1)] };
chat.reset();
expect(socket.disconnect).toHaveBeenCalled();
expect(chat.conversations).toEqual([]);
expect(chat.messages).toEqual({});
});
});