mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
adds more tests
This commit is contained in:

committed by
Geoff Seemueller

parent
33baf588b6
commit
ebbfd4d31a
@@ -19,9 +19,9 @@
|
|||||||
"tail:analytics-service": "wrangler tail -c workers/analytics/wrangler-analytics.toml",
|
"tail:analytics-service": "wrangler tail -c workers/analytics/wrangler-analytics.toml",
|
||||||
"tail:session-proxy": "wrangler tail -c workers/session-proxy/wrangler-session-proxy.toml --env production",
|
"tail:session-proxy": "wrangler tail -c workers/session-proxy/wrangler-session-proxy.toml --env production",
|
||||||
"openai:local": "./scripts/start_inference_server.sh",
|
"openai:local": "./scripts/start_inference_server.sh",
|
||||||
"test": "vitest run",
|
"test": "NODE_OPTIONS=--no-experimental-fetch vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "NODE_OPTIONS=--no-experimental-fetch vitest",
|
||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "NODE_OPTIONS=--no-experimental-fetch vitest run --coverage"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.32.1",
|
"@anthropic-ai/sdk": "^0.32.1",
|
||||||
|
@@ -1,272 +1,229 @@
|
|||||||
import { applySnapshot, flow, Instance, types } from "mobx-state-tree";
|
// ---------------------------
|
||||||
|
// stores/MessagesStore.ts
|
||||||
|
// ---------------------------
|
||||||
|
import { Instance, types } from "mobx-state-tree";
|
||||||
import Message from "../models/Message";
|
import Message from "../models/Message";
|
||||||
|
|
||||||
|
export const MessagesStore = types
|
||||||
|
.model("MessagesStore", {
|
||||||
|
items: types.optional(types.array(Message), []),
|
||||||
|
})
|
||||||
|
.actions((self) => ({
|
||||||
|
add(message: Instance<typeof Message>) {
|
||||||
|
self.items.push(message);
|
||||||
|
},
|
||||||
|
updateLast(content: string) {
|
||||||
|
if (self.items.length) {
|
||||||
|
self.items[self.items.length - 1].content = content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appendLast(content: string) {
|
||||||
|
if (self.items.length) {
|
||||||
|
self.items[self.items.length - 1].content += content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAfter(index: number) {
|
||||||
|
if (index >= 0 && index < self.items.length) {
|
||||||
|
self.items.splice(index + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
self.items.clear();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface IMessagesStore extends Instance<typeof MessagesStore> {}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// stores/UIStore.ts
|
||||||
|
// ---------------------------
|
||||||
|
import { Instance, types } from "mobx-state-tree";
|
||||||
|
|
||||||
|
export const UIStore = types
|
||||||
|
.model("UIStore", {
|
||||||
|
input: types.optional(types.string, ""),
|
||||||
|
isLoading: types.optional(types.boolean, false),
|
||||||
|
})
|
||||||
|
.actions((self) => ({
|
||||||
|
setInput(value: string) {
|
||||||
|
self.input = value;
|
||||||
|
},
|
||||||
|
setIsLoading(value: boolean) {
|
||||||
|
self.isLoading = value;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface IUIStore extends Instance<typeof UIStore> {}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// stores/ModelStore.ts
|
||||||
|
// ---------------------------
|
||||||
|
import { Instance, types } from "mobx-state-tree";
|
||||||
|
|
||||||
|
export const ModelStore = types
|
||||||
|
.model("ModelStore", {
|
||||||
|
model: types.optional(
|
||||||
|
types.string,
|
||||||
|
"meta-llama/llama-4-scout-17b-16e-instruct",
|
||||||
|
),
|
||||||
|
imageModel: types.optional(types.string, "black-forest-labs/flux-1.1-pro"),
|
||||||
|
supportedModels: types.optional(types.array(types.string), []),
|
||||||
|
})
|
||||||
|
.actions((self) => ({
|
||||||
|
setModel(value: string) {
|
||||||
|
self.model = value;
|
||||||
|
try {
|
||||||
|
localStorage.setItem("recentModel", value);
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
|
setImageModel(value: string) {
|
||||||
|
self.imageModel = value;
|
||||||
|
},
|
||||||
|
setSupportedModels(list: string[]) {
|
||||||
|
self.supportedModels = list;
|
||||||
|
if (!list.includes(self.model)) {
|
||||||
|
// fall back to last entry (arbitrary but predictable)
|
||||||
|
self.model = list[list.length - 1] ?? self.model;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface IModelStore extends Instance<typeof ModelStore> {}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// stores/StreamStore.ts
|
||||||
|
// Handles networking + SSE lifecycle.
|
||||||
|
// Depends on MessagesStore, UIStore, and ModelStore via composition.
|
||||||
|
// ---------------------------
|
||||||
|
import {
|
||||||
|
getParent,
|
||||||
|
Instance,
|
||||||
|
flow,
|
||||||
|
types,
|
||||||
|
} from "mobx-state-tree";
|
||||||
|
import type { IMessagesStore } from "./MessagesStore";
|
||||||
|
import type { IUIStore } from "./UIStore";
|
||||||
|
import type { IModelStore } from "./ModelStore";
|
||||||
import UserOptionsStore from "./UserOptionsStore";
|
import UserOptionsStore from "./UserOptionsStore";
|
||||||
|
import Message from "../models/Message";
|
||||||
|
|
||||||
const ClientChatStore = types
|
interface RootDeps extends IMessagesStore, IUIStore, IModelStore {}
|
||||||
.model("ClientChatStore", {
|
|
||||||
messages: types.optional(types.array(Message), []),
|
export const StreamStore = types
|
||||||
input: types.optional(types.string, ""),
|
.model("StreamStore", {})
|
||||||
isLoading: types.optional(types.boolean, false),
|
.volatile(() => ({
|
||||||
model: types.optional(types.string, "meta-llama/llama-4-scout-17b-16e-instruct"),
|
eventSource: null as EventSource | null,
|
||||||
imageModel: types.optional(types.string, "black-forest-labs/flux-1.1-pro"),
|
}))
|
||||||
supportedModels: types.optional(types.array(types.string), [])
|
.actions((self) => {
|
||||||
})
|
// helpers
|
||||||
.actions((self) => ({
|
const root = getParent<RootDeps>(self);
|
||||||
cleanup() {
|
|
||||||
if (self.eventSource) {
|
function cleanup() {
|
||||||
self.eventSource.close();
|
if (self.eventSource) {
|
||||||
self.eventSource = null;
|
self.eventSource.close();
|
||||||
|
self.eventSource = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
setSupportedModels(modelsList: string[]) {
|
|
||||||
self.supportedModels = modelsList;
|
|
||||||
if(!modelsList.includes(self.model)) {
|
|
||||||
self.model = modelsList.pop()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendMessage: flow(function* () {
|
|
||||||
if (!self.input.trim() || self.isLoading) return;
|
|
||||||
|
|
||||||
self.cleanup();
|
const sendMessage = flow(function* () {
|
||||||
|
if (!root.input.trim() || root.isLoading) return;
|
||||||
|
cleanup();
|
||||||
|
yield UserOptionsStore.setFollowModeEnabled(true);
|
||||||
|
root.setIsLoading(true);
|
||||||
|
|
||||||
yield self.setFollowModeEnabled(true);
|
const userMessage = Message.create({
|
||||||
self.setIsLoading(true);
|
content: root.input,
|
||||||
|
role: "user" as const,
|
||||||
const userMessage = Message.create({
|
|
||||||
content: self.input,
|
|
||||||
role: "user" as const,
|
|
||||||
});
|
|
||||||
self.addMessage(userMessage);
|
|
||||||
self.setInput("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
messages: self.messages.slice(),
|
|
||||||
model: self.model,
|
|
||||||
};
|
|
||||||
|
|
||||||
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
self.addMessage(Message.create({ content: "", role: "assistant" }));
|
|
||||||
|
|
||||||
const response = yield fetch("/api/chat", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
});
|
||||||
if (response.status === 429) {
|
root.add(userMessage);
|
||||||
self.updateLastMessage(
|
root.setInput("");
|
||||||
`Too many requests in the given time. Please wait a few moments and try again.`,
|
|
||||||
);
|
|
||||||
self.cleanup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.status > 200) {
|
|
||||||
self.updateLastMessage(`Error! Something went wrong, try again.`);
|
|
||||||
self.cleanup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { streamUrl } = yield response.json();
|
|
||||||
self.eventSource = new EventSource(streamUrl);
|
|
||||||
|
|
||||||
self.eventSource.onmessage = async (event) => {
|
|
||||||
try {
|
|
||||||
const dataString = event.data;
|
|
||||||
const parsedData = JSON.parse(dataString);
|
|
||||||
|
|
||||||
if (parsedData.type === "error") {
|
|
||||||
self.updateLastMessage(`${parsedData.error}`);
|
|
||||||
self.cleanup();
|
|
||||||
self.setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
parsedData.type === "chat" &&
|
|
||||||
parsedData.data.choices[0]?.finish_reason === "stop"
|
|
||||||
) {
|
|
||||||
self.appendToLastMessage(
|
|
||||||
parsedData.data.choices[0]?.delta?.content || "",
|
|
||||||
);
|
|
||||||
self.cleanup();
|
|
||||||
self.setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedData.type === "chat") {
|
|
||||||
self.appendToLastMessage(
|
|
||||||
parsedData.data.choices[0]?.delta?.content || "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing stream:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.eventSource.onerror = (e) => {
|
|
||||||
self.cleanup();
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in sendMessage:", error);
|
|
||||||
if (
|
|
||||||
!self.messages.length ||
|
|
||||||
self.messages[self.messages.length - 1].role !== "assistant"
|
|
||||||
) {
|
|
||||||
self.addMessage({
|
|
||||||
content: "Sorry, there was an error.",
|
|
||||||
role: "assistant",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.updateLastMessage("Sorry, there was an error.");
|
|
||||||
}
|
|
||||||
self.cleanup();
|
|
||||||
self.setIsLoading(false);
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
setFollowModeEnabled: flow(function* (isEnabled: boolean) {
|
|
||||||
yield UserOptionsStore.setFollowModeEnabled(isEnabled);
|
|
||||||
}),
|
|
||||||
setInput(value: string) {
|
|
||||||
self.input = value;
|
|
||||||
},
|
|
||||||
setModel(value: string) {
|
|
||||||
self.model = value;
|
|
||||||
try {
|
|
||||||
localStorage.setItem("recentModel", value);
|
|
||||||
} catch (error) {}
|
|
||||||
},
|
|
||||||
setImageModel(value: string) {
|
|
||||||
self.imageModel = value;
|
|
||||||
},
|
|
||||||
addMessage(message: Instance<typeof Message>) {
|
|
||||||
self.messages.push(message);
|
|
||||||
},
|
|
||||||
editMessage: flow(function* (index: number, content: string) {
|
|
||||||
yield self.setFollowModeEnabled(true);
|
|
||||||
if (index >= 0 && index < self.messages.length) {
|
|
||||||
self.messages[index].setContent(content);
|
|
||||||
|
|
||||||
self.messages.splice(index + 1);
|
|
||||||
|
|
||||||
self.setIsLoading(true);
|
|
||||||
|
|
||||||
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
self.addMessage(Message.create({ content: "", role: "assistant" }));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = { messages: root.items.slice(), model: root.model };
|
||||||
messages: self.messages.slice(),
|
// optimistic UI delay (demo‑purpose)
|
||||||
model: self.model,
|
yield new Promise((r) => setTimeout(r, 500));
|
||||||
};
|
root.add(Message.create({ content: "", role: "assistant" }));
|
||||||
|
|
||||||
const response = yield fetch("/api/chat", {
|
const response: Response = yield fetch("/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
self.updateLastMessage(
|
root.updateLast("Too many requests • please slow down.");
|
||||||
`Too many requests in the given time. Please wait a few moments and try again.`,
|
cleanup();
|
||||||
);
|
|
||||||
self.cleanup();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (response.status > 200) {
|
if (response.status > 200) {
|
||||||
self.updateLastMessage(`Error! Something went wrong, try again.`);
|
root.updateLast("Error • something went wrong.");
|
||||||
self.cleanup();
|
cleanup();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { streamUrl } = yield response.json();
|
const { streamUrl } = (yield response.json()) as { streamUrl: string };
|
||||||
self.eventSource = new EventSource(streamUrl);
|
self.eventSource = new EventSource(streamUrl);
|
||||||
|
|
||||||
self.eventSource.onmessage = (event) => {
|
self.eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const dataString = event.data;
|
const parsed = JSON.parse(event.data);
|
||||||
const parsedData = JSON.parse(dataString);
|
if (parsed.type === "error") {
|
||||||
|
root.updateLast(parsed.error);
|
||||||
if (parsedData.type === "error") {
|
cleanup();
|
||||||
self.updateLastMessage(`${parsedData.error}`);
|
root.setIsLoading(false);
|
||||||
self.cleanup();
|
|
||||||
self.setIsLoading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parsedData.type === "chat" &&
|
parsed.type === "chat" &&
|
||||||
parsedData.data.choices[0]?.finish_reason === "stop"
|
parsed.data.choices[0]?.finish_reason === "stop"
|
||||||
) {
|
) {
|
||||||
self.cleanup();
|
root.appendLast(parsed.data.choices[0]?.delta?.content ?? "");
|
||||||
self.setIsLoading(false);
|
cleanup();
|
||||||
|
root.setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedData.type === "chat") {
|
if (parsed.type === "chat") {
|
||||||
self.appendToLastMessage(
|
root.appendLast(parsed.data.choices[0]?.delta?.content ?? "");
|
||||||
parsedData.data.choices[0]?.delta?.content || "",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error processing stream:", error);
|
console.error("stream parse error", err);
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.eventSource.onerror = (e) => {
|
self.eventSource.onerror = () => cleanup();
|
||||||
console.log("EventSource encountered an error", JSON.stringify(e));
|
} catch (err) {
|
||||||
};
|
console.error("sendMessage", err);
|
||||||
} catch (error) {
|
root.updateLast("Sorry • network error.");
|
||||||
console.error("Error in editMessage:", error);
|
cleanup();
|
||||||
self.addMessage({
|
root.setIsLoading(false);
|
||||||
content: "Sorry, there was an error.",
|
|
||||||
role: "assistant",
|
|
||||||
});
|
|
||||||
self.cleanup();
|
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}),
|
|
||||||
getIsLoading() {
|
|
||||||
return self.isLoading;
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
applySnapshot(self, {});
|
|
||||||
},
|
|
||||||
removeMessagesAfter(index: number) {
|
|
||||||
if (index >= 0 && index < self.messages.length) {
|
|
||||||
self.messages.splice(index + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateLastMessage(content: string) {
|
|
||||||
if (self.messages.length > 0) {
|
|
||||||
self.messages[self.messages.length - 1].content = content;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
appendToLastMessage(content: string) {
|
|
||||||
if (self.messages.length > 0) {
|
|
||||||
self.messages[self.messages.length - 1].content += content;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setIsLoading(value: boolean) {
|
|
||||||
self.isLoading = value;
|
|
||||||
},
|
|
||||||
stopIncomingMessage() {
|
|
||||||
if (self.eventSource) {
|
|
||||||
self.eventSource.close();
|
|
||||||
self.eventSource = null;
|
|
||||||
}
|
|
||||||
self.setIsLoading(false);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export type IMessage = Instance<typeof Message>;
|
const stopIncomingMessage = () => {
|
||||||
|
cleanup();
|
||||||
|
root.setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
export type IClientChatStore = Instance<typeof this>;
|
return { sendMessage, stopIncomingMessage, cleanup };
|
||||||
|
});
|
||||||
|
|
||||||
export default ClientChatStore.create();
|
export interface IStreamStore extends Instance<typeof StreamStore> {}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// stores/ClientChatStore.ts (root)
|
||||||
|
// ---------------------------
|
||||||
|
import { types } from "mobx-state-tree";
|
||||||
|
// import { MessagesStore } from "./MessagesStore";
|
||||||
|
// import { UIStore } from "./UIStore";
|
||||||
|
// import { ModelStore } from "./ModelStore";
|
||||||
|
// import { StreamStore } from "./StreamStore";
|
||||||
|
|
||||||
|
export const ClientChatStore = types
|
||||||
|
.compose(MessagesStore, UIStore, ModelStore, StreamStore)
|
||||||
|
.named("ClientChatStore");
|
||||||
|
|
||||||
|
export const clientChatStore = ClientChatStore.create();
|
||||||
|
|
||||||
|
export type IClientChatStore = Instance<typeof ClientChatStore>;
|
@@ -72,5 +72,5 @@ export default ClientTransactionStore.create({
|
|||||||
amount: "",
|
amount: "",
|
||||||
donerId: "",
|
donerId: "",
|
||||||
userConfirmed: false,
|
userConfirmed: false,
|
||||||
transactionId: "",
|
txId: "",
|
||||||
});
|
});
|
||||||
|
@@ -3,16 +3,21 @@ import ClientChatStore from "./ClientChatStore";
|
|||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
const UserOptionsStore = types
|
export const UserOptionsStoreModel = types
|
||||||
.model("UserOptionsStore", {
|
.model("UserOptionsStore", {
|
||||||
followModeEnabled: types.optional(types.boolean, false),
|
followModeEnabled: types.optional(types.boolean, false),
|
||||||
theme: types.optional(types.string, "darknight"),
|
theme: types.optional(types.string, "darknight"),
|
||||||
text_model: types.optional(types.string, "llama-3.3-70b-versatile"),
|
text_model: types.optional(types.string, "llama-3.3-70b-versatile"),
|
||||||
})
|
})
|
||||||
.actions((self) => ({
|
.actions((self) => ({
|
||||||
getFollowModeEnabled: flow(function* () {
|
getFollowModeEnabled() {
|
||||||
return self.followModeEnabled;
|
return self.followModeEnabled;
|
||||||
}),
|
},
|
||||||
|
resetStore() {
|
||||||
|
self.followModeEnabled = false;
|
||||||
|
self.theme = "darknight";
|
||||||
|
self.text_model = "llama-3.3-70b-versatile";
|
||||||
|
},
|
||||||
storeUserOptions() {
|
storeUserOptions() {
|
||||||
const userOptionsCookie = document.cookie
|
const userOptionsCookie = document.cookie
|
||||||
.split(";")
|
.split(";")
|
||||||
@@ -35,7 +40,7 @@ const UserOptionsStore = types
|
|||||||
Cookies.set("user_preferences", encodedUserPreferences);
|
Cookies.set("user_preferences", encodedUserPreferences);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initialize: flow(function* () {
|
initialize() {
|
||||||
const userPreferencesCoookie = document.cookie
|
const userPreferencesCoookie = document.cookie
|
||||||
.split(";")
|
.split(";")
|
||||||
.find((row) => row.startsWith("user_preferences"));
|
.find((row) => row.startsWith("user_preferences"));
|
||||||
@@ -53,48 +58,54 @@ const UserOptionsStore = types
|
|||||||
self.text_model = userPreferences.text_model;
|
self.text_model = userPreferences.text_model;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("scroll", async () => {
|
window.addEventListener("scroll", () => {
|
||||||
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
||||||
console.log("scrolling");
|
console.log("scrolling");
|
||||||
await self.setFollowModeEnabled(false);
|
self.setFollowModeEnabled(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("wheel", async () => {
|
window.addEventListener("wheel", () => {
|
||||||
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
||||||
console.log("wheel");
|
console.log("wheel");
|
||||||
await self.setFollowModeEnabled(false);
|
self.setFollowModeEnabled(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("touchmove", async () => {
|
window.addEventListener("touchmove", () => {
|
||||||
console.log("touchmove");
|
console.log("touchmove");
|
||||||
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
||||||
await self.setFollowModeEnabled(false);
|
self.setFollowModeEnabled(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("mousedown", async () => {
|
window.addEventListener("mousedown", () => {
|
||||||
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
if (ClientChatStore.isLoading && self.followModeEnabled) {
|
||||||
await self.setFollowModeEnabled(false);
|
self.setFollowModeEnabled(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
},
|
||||||
deleteCookie() {
|
deleteCookie() {
|
||||||
document.cookie = "user_preferences=; max-age=; path=/;";
|
document.cookie = "user_preferences=; max-age=; path=/;";
|
||||||
},
|
},
|
||||||
setFollowModeEnabled: flow(function* (followMode: boolean) {
|
setFollowModeEnabled(followMode: boolean) {
|
||||||
self.followModeEnabled = followMode;
|
self.followModeEnabled = followMode;
|
||||||
}),
|
},
|
||||||
toggleFollowMode: flow(function* () {
|
toggleFollowMode() {
|
||||||
self.followModeEnabled = !self.followModeEnabled;
|
self.followModeEnabled = !self.followModeEnabled;
|
||||||
}),
|
},
|
||||||
selectTheme: flow(function* (theme: string) {
|
selectTheme(theme: string) {
|
||||||
self.theme = theme;
|
self.theme = theme;
|
||||||
self.storeUserOptions();
|
self.storeUserOptions();
|
||||||
}),
|
},
|
||||||
|
setTheme(theme: string) {
|
||||||
|
self.theme = theme;
|
||||||
|
},
|
||||||
|
setTextModel(model: string) {
|
||||||
|
self.text_model = model;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const userOptionsStore = UserOptionsStore.create();
|
const userOptionsStore = UserOptionsStoreModel.create();
|
||||||
|
|
||||||
export default userOptionsStore;
|
export default userOptionsStore;
|
||||||
|
43
src/stores/__tests__/AppMenuStore.test.ts
Normal file
43
src/stores/__tests__/AppMenuStore.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import menuState from '../AppMenuStore';
|
||||||
|
|
||||||
|
describe('AppMenuStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset the menu state before each test
|
||||||
|
menuState.closeMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have isOpen set to false initially', () => {
|
||||||
|
// Reset to initial state
|
||||||
|
menuState.closeMenu();
|
||||||
|
expect(menuState.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set isOpen to true when openMenu is called', () => {
|
||||||
|
menuState.openMenu();
|
||||||
|
expect(menuState.isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set isOpen to false when closeMenu is called', () => {
|
||||||
|
// First open the menu
|
||||||
|
menuState.openMenu();
|
||||||
|
expect(menuState.isOpen).toBe(true);
|
||||||
|
|
||||||
|
// Then close it
|
||||||
|
menuState.closeMenu();
|
||||||
|
expect(menuState.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle isOpen when toggleMenu is called', () => {
|
||||||
|
// Initially isOpen should be false (from beforeEach)
|
||||||
|
expect(menuState.isOpen).toBe(false);
|
||||||
|
|
||||||
|
// First toggle - should set to true
|
||||||
|
menuState.toggleMenu();
|
||||||
|
expect(menuState.isOpen).toBe(true);
|
||||||
|
|
||||||
|
// Second toggle - should set back to false
|
||||||
|
menuState.toggleMenu();
|
||||||
|
expect(menuState.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
190
src/stores/__tests__/ClientTransactionStore.test.ts
Normal file
190
src/stores/__tests__/ClientTransactionStore.test.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import clientTransactionStore from '../ClientTransactionStore';
|
||||||
|
|
||||||
|
// Mock global fetch
|
||||||
|
const mockFetch = vi.fn();
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
|
||||||
|
describe('ClientTransactionStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset the store to its initial state before each test
|
||||||
|
clientTransactionStore.resetTransaction();
|
||||||
|
clientTransactionStore.setSelectedMethod('Ethereum');
|
||||||
|
clientTransactionStore.setAmount('');
|
||||||
|
clientTransactionStore.setDonerId('');
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// No need to fix inconsistency anymore as we've updated the model
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSelectedMethod', () => {
|
||||||
|
it('should set the selected method and reset userConfirmed', () => {
|
||||||
|
// First set userConfirmed to true to verify it gets reset
|
||||||
|
clientTransactionStore.confirmUser();
|
||||||
|
expect(clientTransactionStore.userConfirmed).toBe(true);
|
||||||
|
|
||||||
|
clientTransactionStore.setSelectedMethod('Bitcoin');
|
||||||
|
|
||||||
|
expect(clientTransactionStore.selectedMethod).toBe('Bitcoin');
|
||||||
|
expect(clientTransactionStore.userConfirmed).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAmount', () => {
|
||||||
|
it('should set the amount', () => {
|
||||||
|
clientTransactionStore.setAmount('100');
|
||||||
|
expect(clientTransactionStore.amount).toBe('100');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDonerId', () => {
|
||||||
|
it('should set the donerId', () => {
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
expect(clientTransactionStore.donerId).toBe('donor123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('confirmUser', () => {
|
||||||
|
it('should set userConfirmed to true', () => {
|
||||||
|
clientTransactionStore.confirmUser();
|
||||||
|
expect(clientTransactionStore.userConfirmed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setTransactionId', () => {
|
||||||
|
it('should set the transaction ID', () => {
|
||||||
|
clientTransactionStore.setTransactionId('tx123');
|
||||||
|
expect(clientTransactionStore.txId).toBe('tx123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDepositAddress', () => {
|
||||||
|
it('should set the deposit address', () => {
|
||||||
|
clientTransactionStore.setDepositAddress('0xabc123');
|
||||||
|
expect(clientTransactionStore.depositAddress).toBe('0xabc123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetTransaction', () => {
|
||||||
|
it('should reset transaction-related properties', () => {
|
||||||
|
// Set up some values first
|
||||||
|
clientTransactionStore.setTransactionId('tx123');
|
||||||
|
clientTransactionStore.setDepositAddress('0xabc123');
|
||||||
|
clientTransactionStore.confirmUser();
|
||||||
|
|
||||||
|
// Reset the transaction
|
||||||
|
clientTransactionStore.resetTransaction();
|
||||||
|
|
||||||
|
// Verify reset values
|
||||||
|
expect(clientTransactionStore.txId).toBe('');
|
||||||
|
expect(clientTransactionStore.depositAddress).toBe(null);
|
||||||
|
expect(clientTransactionStore.userConfirmed).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prepareTransaction', () => {
|
||||||
|
it('should throw an error if amount is empty', async () => {
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
|
||||||
|
await expect(clientTransactionStore.prepareTransaction()).rejects.toThrow('Invalid donation data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if donerId is empty', async () => {
|
||||||
|
clientTransactionStore.setAmount('100');
|
||||||
|
|
||||||
|
await expect(clientTransactionStore.prepareTransaction()).rejects.toThrow('Invalid donation data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if amount is less than or equal to 0', async () => {
|
||||||
|
clientTransactionStore.setAmount('0');
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
|
||||||
|
await expect(clientTransactionStore.prepareTransaction()).rejects.toThrow('Invalid donation data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
// Set up valid transaction data
|
||||||
|
clientTransactionStore.setAmount('100');
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
|
||||||
|
// Mock a failed API response
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(clientTransactionStore.prepareTransaction()).rejects.toThrow('Failed to prepare transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully prepare an Ethereum transaction', async () => {
|
||||||
|
// Set up valid transaction data
|
||||||
|
clientTransactionStore.setAmount('100');
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
clientTransactionStore.setSelectedMethod('Ethereum');
|
||||||
|
|
||||||
|
// Mock a successful API response
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
txKey: 'tx123',
|
||||||
|
depositAddress: 'abc123', // Without 0x prefix to test the Ethereum-specific logic
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await clientTransactionStore.prepareTransaction();
|
||||||
|
|
||||||
|
// Verify API call
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/tx', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'PREPARE_TX,donor123,ethereum,100',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify store updates
|
||||||
|
expect(clientTransactionStore.txId).toBe('tx123');
|
||||||
|
expect(clientTransactionStore.depositAddress).toBe('0xabc123'); // Should have 0x prefix added
|
||||||
|
expect(clientTransactionStore.userConfirmed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully prepare a non-Ethereum transaction', async () => {
|
||||||
|
// Set up valid transaction data
|
||||||
|
clientTransactionStore.setAmount('100');
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
clientTransactionStore.setSelectedMethod('Bitcoin');
|
||||||
|
|
||||||
|
// Mock a successful API response
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
txKey: 'tx123',
|
||||||
|
depositAddress: 'btc123', // Bitcoin address doesn't need prefix
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await clientTransactionStore.prepareTransaction();
|
||||||
|
|
||||||
|
// Verify API call
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/tx', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'PREPARE_TX,donor123,bitcoin,100',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify store updates
|
||||||
|
expect(clientTransactionStore.txId).toBe('tx123');
|
||||||
|
expect(clientTransactionStore.depositAddress).toBe('btc123'); // Should not have prefix added
|
||||||
|
expect(clientTransactionStore.userConfirmed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API errors and rethrow them', async () => {
|
||||||
|
// Set up valid transaction data
|
||||||
|
clientTransactionStore.setAmount('100');
|
||||||
|
clientTransactionStore.setDonerId('donor123');
|
||||||
|
|
||||||
|
// Mock an API error
|
||||||
|
const mockError = new Error('Network error');
|
||||||
|
mockFetch.mockRejectedValueOnce(mockError);
|
||||||
|
|
||||||
|
await expect(clientTransactionStore.prepareTransaction()).rejects.toThrow(mockError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
242
src/stores/__tests__/UserOptionsStore.test.ts
Normal file
242
src/stores/__tests__/UserOptionsStore.test.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import userOptionsStore, { UserOptionsStoreModel } from '../UserOptionsStore';
|
||||||
|
import ClientChatStore from '../ClientChatStore';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('js-cookie', () => ({
|
||||||
|
default: {
|
||||||
|
set: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../ClientChatStore', () => ({
|
||||||
|
default: {
|
||||||
|
isLoading: false,
|
||||||
|
setIsLoading: vi.fn((value) => {
|
||||||
|
(ClientChatStore as any).isLoading = value;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('UserOptionsStore', () => {
|
||||||
|
// Mock document.cookie
|
||||||
|
let originalDocumentCookie: PropertyDescriptor | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Save original document.cookie property descriptor
|
||||||
|
originalDocumentCookie = Object.getOwnPropertyDescriptor(document, 'cookie');
|
||||||
|
|
||||||
|
// Mock document.cookie
|
||||||
|
Object.defineProperty(document, 'cookie', {
|
||||||
|
writable: true,
|
||||||
|
value: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Reset store to default values
|
||||||
|
userOptionsStore.resetStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original document.cookie property descriptor
|
||||||
|
if (originalDocumentCookie) {
|
||||||
|
Object.defineProperty(document, 'cookie', originalDocumentCookie);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFollowModeEnabled', () => {
|
||||||
|
it('should return the current followModeEnabled value', () => {
|
||||||
|
userOptionsStore.setFollowModeEnabled(false);
|
||||||
|
expect(userOptionsStore.getFollowModeEnabled()).toBe(false);
|
||||||
|
|
||||||
|
userOptionsStore.setFollowModeEnabled(true);
|
||||||
|
expect(userOptionsStore.getFollowModeEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('storeUserOptions', () => {
|
||||||
|
it('should store user options in a cookie', () => {
|
||||||
|
// Set up document.cookie to simulate an existing cookie
|
||||||
|
document.cookie = 'user_preferences=abc123';
|
||||||
|
|
||||||
|
userOptionsStore.setTheme('light');
|
||||||
|
userOptionsStore.setTextModel('test-model');
|
||||||
|
|
||||||
|
userOptionsStore.storeUserOptions();
|
||||||
|
|
||||||
|
// Check that Cookies.set was called with the correct arguments
|
||||||
|
const expectedOptions = JSON.stringify({
|
||||||
|
theme: 'light',
|
||||||
|
text_model: 'test-model',
|
||||||
|
});
|
||||||
|
const encodedOptions = btoa(expectedOptions);
|
||||||
|
|
||||||
|
expect(Cookies.set).toHaveBeenCalledWith('user_preferences', encodedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialize', () => {
|
||||||
|
it('should create a cookie if none exists', () => {
|
||||||
|
// Ensure no cookie exists
|
||||||
|
document.cookie = '';
|
||||||
|
|
||||||
|
// Mock storeUserOptions to avoid actual implementation
|
||||||
|
const storeUserOptionsMock = vi.fn();
|
||||||
|
const originalStoreUserOptions = userOptionsStore.storeUserOptions;
|
||||||
|
userOptionsStore.storeUserOptions = storeUserOptionsMock;
|
||||||
|
|
||||||
|
try {
|
||||||
|
userOptionsStore.initialize();
|
||||||
|
expect(storeUserOptionsMock).toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
// Restore original method
|
||||||
|
userOptionsStore.storeUserOptions = originalStoreUserOptions;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load preferences from existing cookie', () => {
|
||||||
|
// Create a mock cookie with preferences
|
||||||
|
const mockPreferences = {
|
||||||
|
theme: 'light',
|
||||||
|
text_model: 'test-model',
|
||||||
|
};
|
||||||
|
const encodedPreferences = btoa(JSON.stringify(mockPreferences));
|
||||||
|
document.cookie = `user_preferences=${encodedPreferences}`;
|
||||||
|
|
||||||
|
userOptionsStore.initialize();
|
||||||
|
|
||||||
|
expect(userOptionsStore.theme).toBe('light');
|
||||||
|
expect(userOptionsStore.text_model).toBe('test-model');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set up event listeners', () => {
|
||||||
|
// Spy on window.addEventListener
|
||||||
|
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
||||||
|
|
||||||
|
userOptionsStore.initialize();
|
||||||
|
|
||||||
|
// Check that event listeners were added for scroll, wheel, touchmove, and mousedown
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('wheel', expect.any(Function));
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('touchmove', expect.any(Function));
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteCookie', () => {
|
||||||
|
it('should delete the user_preferences cookie', () => {
|
||||||
|
userOptionsStore.deleteCookie();
|
||||||
|
|
||||||
|
expect(document.cookie).toContain('user_preferences=; max-age=; path=/;');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setFollowModeEnabled', () => {
|
||||||
|
it('should set the followModeEnabled value', () => {
|
||||||
|
userOptionsStore.setFollowModeEnabled(true);
|
||||||
|
expect(userOptionsStore.followModeEnabled).toBe(true);
|
||||||
|
|
||||||
|
userOptionsStore.setFollowModeEnabled(false);
|
||||||
|
expect(userOptionsStore.followModeEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleFollowMode', () => {
|
||||||
|
it('should toggle the followModeEnabled value', () => {
|
||||||
|
userOptionsStore.setFollowModeEnabled(false);
|
||||||
|
|
||||||
|
userOptionsStore.toggleFollowMode();
|
||||||
|
expect(userOptionsStore.followModeEnabled).toBe(true);
|
||||||
|
|
||||||
|
userOptionsStore.toggleFollowMode();
|
||||||
|
expect(userOptionsStore.followModeEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectTheme', () => {
|
||||||
|
it('should set the theme and store user options', () => {
|
||||||
|
// Mock storeUserOptions to avoid actual implementation
|
||||||
|
const storeUserOptionsMock = vi.fn();
|
||||||
|
const originalStoreUserOptions = userOptionsStore.storeUserOptions;
|
||||||
|
userOptionsStore.storeUserOptions = storeUserOptionsMock;
|
||||||
|
|
||||||
|
try {
|
||||||
|
userOptionsStore.selectTheme('light');
|
||||||
|
expect(userOptionsStore.theme).toBe('light');
|
||||||
|
expect(storeUserOptionsMock).toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
// Restore original method
|
||||||
|
userOptionsStore.storeUserOptions = originalStoreUserOptions;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event listeners', () => {
|
||||||
|
it('should disable follow mode when scrolling if loading', () => {
|
||||||
|
// Create a new instance of the store for this test
|
||||||
|
const testStore = UserOptionsStoreModel.create({
|
||||||
|
followModeEnabled: true,
|
||||||
|
theme: "darknight",
|
||||||
|
text_model: "llama-3.3-70b-versatile"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock ClientChatStore.isLoading
|
||||||
|
const originalIsLoading = ClientChatStore.isLoading;
|
||||||
|
(ClientChatStore as any).isLoading = true;
|
||||||
|
|
||||||
|
// Mock setFollowModeEnabled
|
||||||
|
const setFollowModeEnabledMock = vi.fn();
|
||||||
|
testStore.setFollowModeEnabled = setFollowModeEnabledMock;
|
||||||
|
|
||||||
|
// Add the event listener manually (similar to initialize)
|
||||||
|
const scrollHandler = () => {
|
||||||
|
if (ClientChatStore.isLoading && testStore.followModeEnabled) {
|
||||||
|
testStore.setFollowModeEnabled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger the handler directly
|
||||||
|
scrollHandler();
|
||||||
|
|
||||||
|
// Restore original value
|
||||||
|
(ClientChatStore as any).isLoading = originalIsLoading;
|
||||||
|
|
||||||
|
expect(setFollowModeEnabledMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not disable follow mode when scrolling if not loading', () => {
|
||||||
|
// Create a new instance of the store for this test
|
||||||
|
const testStore = UserOptionsStoreModel.create({
|
||||||
|
followModeEnabled: true,
|
||||||
|
theme: "darknight",
|
||||||
|
text_model: "llama-3.3-70b-versatile"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock ClientChatStore.isLoading
|
||||||
|
const originalIsLoading = ClientChatStore.isLoading;
|
||||||
|
(ClientChatStore as any).isLoading = false;
|
||||||
|
|
||||||
|
// Mock setFollowModeEnabled
|
||||||
|
const setFollowModeEnabledMock = vi.fn();
|
||||||
|
testStore.setFollowModeEnabled = setFollowModeEnabledMock;
|
||||||
|
|
||||||
|
// Add the event listener manually (similar to initialize)
|
||||||
|
const scrollHandler = () => {
|
||||||
|
if (ClientChatStore.isLoading && testStore.followModeEnabled) {
|
||||||
|
testStore.setFollowModeEnabled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger the handler directly
|
||||||
|
scrollHandler();
|
||||||
|
|
||||||
|
// Restore original value
|
||||||
|
(ClientChatStore as any).isLoading = originalIsLoading;
|
||||||
|
|
||||||
|
expect(setFollowModeEnabledMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user