test(stores): add state machine coverage

This commit is contained in:
2026-06-25 13:28:08 +08:00
parent 0bff65dd32
commit 6081f2896a
8 changed files with 1081 additions and 2 deletions
@@ -0,0 +1,146 @@
import { describe, expect, it, vi } from "vitest";
import { createActor, fromPromise, waitFor } from "xstate";
import type { LoginStatus } from "@/data/dto/auth";
import type { AuthProvider } from "@/lib/auth/auth_platform";
import { authMachine } from "@/stores/auth/auth-machine";
function createTestAuthMachine(
overrides: Partial<{
checkAuthStatus: LoginStatus;
guestLogin: LoginStatus;
emailLogin: LoginStatus;
logoutSpy: () => void;
}> = {},
) {
const logoutSpy = overrides.logoutSpy ?? vi.fn<() => void>();
return authMachine.provide({
actors: {
checkAuthStatus: fromPromise<LoginStatus, void>(
async () => overrides.checkAuthStatus ?? "notLoggedIn",
),
guestLogin: fromPromise<LoginStatus, void>(
async () => overrides.guestLogin ?? "guest",
),
emailLogin: fromPromise<
LoginStatus,
{ email: string; password: string }
>(async () => overrides.emailLogin ?? "email"),
emailRegisterThenLogin: fromPromise<
LoginStatus,
{
email: string;
password: string;
username: string;
confirmPassword: string;
}
>(async () => "email"),
oauthSignIn: fromPromise<void, AuthProvider>(async () => {}),
syncGoogleBackend: fromPromise<LoginStatus, { idToken: string }>(
async () => "google",
),
syncFacebookBackend: fromPromise<
LoginStatus,
{ accessToken: string }
>(async () => "facebook"),
logout: fromPromise<void>(async () => {
logoutSpy();
}),
},
});
}
describe("authMachine", () => {
it("updates panel mode, auth mode, and unsupported Apple login errors", () => {
const actor = createActor(createTestAuthMachine()).start();
actor.send({ type: "AuthPanelModeChanged", mode: "email" });
actor.send({ type: "AuthModeChanged", mode: "register" });
expect(actor.getSnapshot().context.authPanelMode).toBe("email");
expect(actor.getSnapshot().context.authMode).toBe("register");
actor.send({ type: "AuthAppleLoginSubmitted" });
expect(actor.getSnapshot().context.errorMessage).toBe(
"Apple login not implemented",
);
actor.send({ type: "AuthFormCleared" });
expect(actor.getSnapshot().context.errorMessage).toBeNull();
actor.stop();
});
it("initializes from storage without triggering a pending redirect", async () => {
const actor = createActor(
createTestAuthMachine({ checkAuthStatus: "facebook" }),
).start();
actor.send({ type: "AuthInit" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
const context = actor.getSnapshot().context;
expect(context.loginStatus).toBe("facebook");
expect(context.hasInitialized).toBe(true);
expect(context.pendingRedirect).toBe(false);
actor.stop();
});
it("guest login marks initialized but does not set pending redirect", async () => {
const actor = createActor(createTestAuthMachine()).start();
actor.send({ type: "AuthGuestLoginSubmitted" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
const context = actor.getSnapshot().context;
expect(context.loginStatus).toBe("guest");
expect(context.hasInitialized).toBe(true);
expect(context.pendingRedirect).toBe(false);
actor.stop();
});
it("email login sets pending redirect until it is cleared", async () => {
const actor = createActor(createTestAuthMachine()).start();
actor.send({
type: "AuthEmailLoginSubmitted",
email: "user@example.com",
password: "12345678",
});
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
expect(actor.getSnapshot().context.loginStatus).toBe("email");
expect(actor.getSnapshot().context.pendingRedirect).toBe(true);
actor.send({ type: "AuthClearPendingRedirect" });
expect(actor.getSnapshot().context.pendingRedirect).toBe(false);
actor.stop();
});
it("logout resets auth state and keeps initialization completed", async () => {
const logoutSpy = vi.fn<() => void>();
const actor = createActor(createTestAuthMachine({ logoutSpy })).start();
actor.send({
type: "AuthEmailLoginSubmitted",
email: "user@example.com",
password: "12345678",
});
await waitFor(actor, (snapshot) => snapshot.context.loginStatus === "email");
actor.send({ type: "AuthLogoutSubmitted" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
const context = actor.getSnapshot().context;
expect(logoutSpy).toHaveBeenCalledOnce();
expect(context.loginStatus).toBe("notLoggedIn");
expect(context.pendingRedirect).toBe(false);
expect(context.hasInitialized).toBe(true);
actor.stop();
});
});