test(stores): add state machine coverage
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { toView } from "@/stores/user/user-machine.helpers";
|
||||
|
||||
describe("toView", () => {
|
||||
it("fills optional user fields with stable defaults", () => {
|
||||
expect(toView({ id: "user-1", username: "Luna" })).toEqual({
|
||||
id: "user-1",
|
||||
username: "Luna",
|
||||
email: "",
|
||||
avatarUrl: "",
|
||||
intimacy: 0,
|
||||
dolBalance: 0,
|
||||
relationshipStage: "",
|
||||
currentMood: "",
|
||||
isGuest: false,
|
||||
isVip: false,
|
||||
voiceMinutesRemaining: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves provided user fields", () => {
|
||||
expect(
|
||||
toView({
|
||||
id: "user-2",
|
||||
username: "Elio",
|
||||
email: "elio@example.com",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
intimacy: 42,
|
||||
dolBalance: 12,
|
||||
relationshipStage: "close_friend",
|
||||
currentMood: "playful",
|
||||
isGuest: true,
|
||||
isVip: true,
|
||||
voiceMinutesRemaining: 30,
|
||||
}),
|
||||
).toEqual({
|
||||
id: "user-2",
|
||||
username: "Elio",
|
||||
email: "elio@example.com",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
intimacy: 42,
|
||||
dolBalance: 12,
|
||||
relationshipStage: "close_friend",
|
||||
currentMood: "playful",
|
||||
isGuest: true,
|
||||
isVip: true,
|
||||
voiceMinutesRemaining: 30,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createActor, fromPromise, waitFor } from "xstate";
|
||||
|
||||
import type { UserView } from "@/data/dto/user";
|
||||
import { userMachine } from "@/stores/user/user-machine";
|
||||
import type { InitData } from "@/stores/user/user-machine.helpers";
|
||||
|
||||
function makeUser(overrides: Partial<UserView> = {}): UserView {
|
||||
return {
|
||||
id: "user-1",
|
||||
username: "Elio Fan",
|
||||
email: "user@example.com",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
intimacy: 42,
|
||||
dolBalance: 7,
|
||||
relationshipStage: "close_friend",
|
||||
currentMood: "happy",
|
||||
isGuest: false,
|
||||
isVip: false,
|
||||
voiceMinutesRemaining: 12,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createTestUserMachine(
|
||||
overrides: Partial<{
|
||||
initData: InitData;
|
||||
fetchedUser: UserView | null;
|
||||
logoutSpy: () => void;
|
||||
}> = {},
|
||||
) {
|
||||
const logoutSpy = overrides.logoutSpy ?? vi.fn<() => void>();
|
||||
|
||||
return userMachine.provide({
|
||||
actors: {
|
||||
userInit: fromPromise<InitData>(async () => ({
|
||||
user: makeUser(),
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
...overrides.initData,
|
||||
})),
|
||||
userFetch: fromPromise<UserView | null>(
|
||||
async () =>
|
||||
"fetchedUser" in overrides
|
||||
? overrides.fetchedUser ?? null
|
||||
: makeUser({ username: "Fetched" }),
|
||||
),
|
||||
userLogout: fromPromise<void>(async () => {
|
||||
logoutSpy();
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("userMachine", () => {
|
||||
it("initializes user and avatar from local storage actor output", async () => {
|
||||
const actor = createActor(
|
||||
createTestUserMachine({
|
||||
initData: {
|
||||
user: makeUser({ username: "Local User" }),
|
||||
avatarUrl: "https://example.com/local-avatar.png",
|
||||
},
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "UserInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
|
||||
|
||||
expect(actor.getSnapshot().context.currentUser?.username).toBe("Local User");
|
||||
expect(actor.getSnapshot().context.avatarUrl).toBe(
|
||||
"https://example.com/local-avatar.png",
|
||||
);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("updates user profile fields synchronously", () => {
|
||||
const actor = createActor(createTestUserMachine()).start();
|
||||
|
||||
actor.send({ type: "UserUpdate", user: makeUser({ username: "Before" }) });
|
||||
actor.send({ type: "UserUpdateUsername", username: "After" });
|
||||
actor.send({ type: "UserUpdatePronouns", pronouns: "They" });
|
||||
|
||||
expect(actor.getSnapshot().context.currentUser?.username).toBe("After");
|
||||
expect(actor.getSnapshot().context.pronouns).toBe("They");
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("fetches the latest user and clears loading state when done", async () => {
|
||||
const actor = createActor(
|
||||
createTestUserMachine({
|
||||
fetchedUser: makeUser({ username: "Fresh User", isVip: true }),
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "UserFetch" });
|
||||
expect(actor.getSnapshot().context.isLoading).toBe(true);
|
||||
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
|
||||
|
||||
const context = actor.getSnapshot().context;
|
||||
expect(context.currentUser?.username).toBe("Fresh User");
|
||||
expect(context.currentUser?.isVip).toBe(true);
|
||||
expect(context.isLoading).toBe(false);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("keeps the current user when fetch returns null", async () => {
|
||||
const actor = createActor(
|
||||
createTestUserMachine({
|
||||
fetchedUser: null,
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "UserUpdate", user: makeUser({ username: "Cached" }) });
|
||||
actor.send({ type: "UserFetch" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
|
||||
|
||||
const context = actor.getSnapshot().context;
|
||||
expect(context.currentUser?.username).toBe("Cached");
|
||||
expect(context.isLoading).toBe(false);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("clears user data after logout", async () => {
|
||||
const logoutSpy = vi.fn<() => void>();
|
||||
const actor = createActor(createTestUserMachine({ logoutSpy })).start();
|
||||
|
||||
actor.send({ type: "UserUpdate", user: makeUser() });
|
||||
actor.send({ type: "UserLogout" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
|
||||
|
||||
const context = actor.getSnapshot().context;
|
||||
expect(logoutSpy).toHaveBeenCalledOnce();
|
||||
expect(context.currentUser).toBeNull();
|
||||
expect(context.avatarUrl).toBeNull();
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
});
|
||||
@@ -100,8 +100,9 @@ export const userMachine = setup({
|
||||
src: "userFetch",
|
||||
onDone: {
|
||||
target: "idle",
|
||||
actions: assign(({ event }) => ({
|
||||
currentUser: event.output,
|
||||
actions: assign(({ context, event }) => ({
|
||||
currentUser: event.output ?? context.currentUser,
|
||||
isLoading: false,
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
|
||||
Reference in New Issue
Block a user