fix tests

This commit is contained in:
jaydenfyi 2026-01-25 11:21:25 +08:00
parent fecc028709
commit d9093572ac
3 changed files with 106 additions and 26 deletions

View File

@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(pnpm build:*)",
"Bash(pnpm test:*)",
"Bash(pnpm -w run test:*)"
]
}
}

View File

@ -3,13 +3,50 @@ import { probeTwitch } from "./probe.js";
import type { TwitchAccountConfig } from "./types.js";
// Mock Twurple modules - Vitest v4 compatible mocking
const mockConnect = vi.fn().mockResolvedValue(undefined);
const mockUnbind = vi.fn();
// Event handler storage
let connectHandler: (() => void) | null = null;
let disconnectHandler: ((manually: boolean, reason?: Error) => void) | null = null;
let authFailHandler: (() => void) | null = null;
// Event listener mocks that store handlers and return unbind function
const mockOnConnect = vi.fn((handler: () => void) => {
connectHandler = handler;
return { unbind: mockUnbind };
});
const mockOnDisconnect = vi.fn((handler: (manually: boolean, reason?: Error) => void) => {
disconnectHandler = handler;
return { unbind: mockUnbind };
});
const mockOnAuthenticationFailure = vi.fn((handler: () => void) => {
authFailHandler = handler;
return { unbind: mockUnbind };
});
// Connect mock that triggers the registered handler
const defaultConnectImpl = async () => {
// Simulate successful connection by calling the handler after a delay
if (connectHandler) {
// Use setTimeout to simulate async connection
await new Promise((resolve) => setTimeout(resolve, 0));
connectHandler();
}
};
const mockConnect = vi.fn().mockImplementation(defaultConnectImpl);
const mockQuit = vi.fn().mockResolvedValue(undefined);
vi.mock("@twurple/chat", () => ({
ChatClient: class {
connect = mockConnect;
quit = mockQuit;
onConnect = mockOnConnect;
onDisconnect = mockOnDisconnect;
onAuthenticationFailure = mockOnAuthenticationFailure;
},
}));
@ -25,6 +62,10 @@ describe("probeTwitch", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset handlers
connectHandler = null;
disconnectHandler = null;
authFailHandler = null;
});
it("returns error when username is missing", async () => {
@ -85,11 +126,18 @@ describe("probeTwitch", () => {
expect(result.error).toContain("timeout");
// Reset mock
mockConnect.mockResolvedValue(undefined);
mockConnect.mockImplementation(defaultConnectImpl);
});
it("cleans up client even on failure", async () => {
mockConnect.mockRejectedValueOnce(new Error("Connection failed"));
mockConnect.mockImplementationOnce(async () => {
// Simulate connection failure by calling disconnect handler
// onDisconnect signature: (manually: boolean, reason?: Error) => void
if (disconnectHandler) {
await new Promise((resolve) => setTimeout(resolve, 0));
disconnectHandler(false, new Error("Connection failed"));
}
});
const result = await probeTwitch(mockAccount, 5000);
@ -98,7 +146,7 @@ describe("probeTwitch", () => {
expect(mockQuit).toHaveBeenCalled();
// Reset mocks
mockConnect.mockResolvedValue(undefined);
mockConnect.mockImplementation(defaultConnectImpl);
});
it("measures elapsed time", async () => {
@ -109,7 +157,14 @@ describe("probeTwitch", () => {
});
it("handles connection errors gracefully", async () => {
mockConnect.mockRejectedValueOnce(new Error("Network error"));
mockConnect.mockImplementationOnce(async () => {
// Simulate connection failure by calling disconnect handler
// onDisconnect signature: (manually: boolean, reason?: Error) => void
if (disconnectHandler) {
await new Promise((resolve) => setTimeout(resolve, 0));
disconnectHandler(false, new Error("Network error"));
}
});
const result = await probeTwitch(mockAccount, 5000);
@ -117,7 +172,7 @@ describe("probeTwitch", () => {
expect(result.error).toContain("Network error");
// Reset mock
mockConnect.mockResolvedValue(undefined);
mockConnect.mockImplementation(defaultConnectImpl);
});
it("trims token before validation", async () => {
@ -132,7 +187,14 @@ describe("probeTwitch", () => {
});
it("handles non-Error objects in catch block", async () => {
mockConnect.mockRejectedValueOnce("String error");
mockConnect.mockImplementationOnce(async () => {
// Simulate connection failure by calling disconnect handler
// onDisconnect signature: (manually: boolean, reason?: Error) => void
if (disconnectHandler) {
await new Promise((resolve) => setTimeout(resolve, 0));
disconnectHandler(false, "String error" as unknown as Error);
}
});
const result = await probeTwitch(mockAccount, 5000);
@ -140,6 +202,6 @@ describe("probeTwitch", () => {
expect(result.error).toBe("String error");
// Reset mock
mockConnect.mockResolvedValue(undefined);
mockConnect.mockImplementation(defaultConnectImpl);
});
});

View File

@ -18,8 +18,23 @@ const mockConnect = vi.fn().mockResolvedValue(undefined);
const mockJoin = vi.fn().mockResolvedValue(undefined);
const mockSay = vi.fn().mockResolvedValue({ messageId: "test-msg-123" });
const mockQuit = vi.fn();
const mockOnMessage = vi.fn();
const mockOnWhisper = vi.fn();
const mockUnbind = vi.fn();
// Event handler storage for testing
const messageHandlers: Array<(channel: string, user: string, message: string, msg: any) => void> = [];
const whisperHandlers: Array<(user: string, message: string, msg: any) => void> = [];
// Mock functions that track handlers and return unbind objects
const mockOnMessage = vi.fn((handler: any) => {
messageHandlers.push(handler);
return { unbind: mockUnbind };
});
const mockOnWhisper = vi.fn((handler: any) => {
whisperHandlers.push(handler);
return { unbind: mockUnbind };
});
const mockAddUserForToken = vi.fn().mockResolvedValue("123456");
const mockOnRefresh = vi.fn();
const mockOnRefreshFailure = vi.fn();
@ -33,6 +48,14 @@ vi.mock("@twurple/chat", () => ({
say = mockSay;
quit = mockQuit;
},
LogLevel: {
CRITICAL: "CRITICAL",
ERROR: "ERROR",
WARNING: "WARNING",
INFO: "INFO",
DEBUG: "DEBUG",
TRACE: "TRACE",
},
}));
const mockAuthProvider = {
@ -85,6 +108,10 @@ describe("TwitchClientManager", () => {
// Clear all mocks first
vi.clearAllMocks();
// Clear handler arrays
messageHandlers.length = 0;
whisperHandlers.length = 0;
// Re-set up the default token mock implementation after clearing
const { resolveTwitchToken } = await import("./token.js");
vi.mocked(resolveTwitchToken).mockReturnValue({
@ -396,7 +423,7 @@ describe("TwitchClientManager", () => {
await manager.getClient(testAccount);
// Get the onMessage callback
const onMessageCallback = mockOnMessage.mock.calls[0]?.[0];
const onMessageCallback = messageHandlers[0];
if (!onMessageCallback) throw new Error("onMessageCallback not found");
// Simulate Twitch message
@ -426,7 +453,7 @@ describe("TwitchClientManager", () => {
await manager.getClient(testAccount);
// Get the onWhisper callback
const onWhisperCallback = mockOnWhisper.mock.calls[0]?.[0];
const onWhisperCallback = whisperHandlers[0];
if (!onWhisperCallback) throw new Error("onWhisperCallback not found");
// Simulate Twitch whisper
@ -452,7 +479,7 @@ describe("TwitchClientManager", () => {
it("should normalize channel names without # prefix", async () => {
await manager.getClient(testAccount);
const onMessageCallback = mockOnMessage.mock.calls[0]?.[0];
const onMessageCallback = messageHandlers[0];
onMessageCallback("testchannel", "testuser", "Test", {
userInfo: {
@ -473,7 +500,7 @@ describe("TwitchClientManager", () => {
it("should include user role flags in message", async () => {
await manager.getClient(testAccount);
const onMessageCallback = mockOnMessage.mock.calls[0]?.[0];
const onMessageCallback = messageHandlers[0];
onMessageCallback("#testchannel", "moduser", "Test", {
userInfo: {
@ -497,7 +524,7 @@ describe("TwitchClientManager", () => {
it("should handle broadcaster messages", async () => {
await manager.getClient(testAccount);
const onMessageCallback = mockOnMessage.mock.calls[0]?.[0];
const onMessageCallback = messageHandlers[0];
onMessageCallback("#testchannel", "broadcaster", "Test", {
userInfo: {
@ -528,7 +555,7 @@ describe("TwitchClientManager", () => {
await manager.getClient(testAccount2);
// Simulate message for first account
const onMessage1 = mockOnMessage.mock.calls[0]?.[0];
const onMessage1 = messageHandlers[0];
if (!onMessage1) throw new Error("onMessage1 not found");
onMessage1("#testchannel", "user1", "msg1", {
userInfo: {
@ -544,7 +571,7 @@ describe("TwitchClientManager", () => {
});
// Simulate message for second account
const onMessage2 = mockOnMessage.mock.calls[1]?.[0];
const onMessage2 = messageHandlers[1];
if (!onMessage2) throw new Error("onMessage2 not found");
onMessage2("#testchannel2", "user2", "msg2", {
userInfo: {