Merge branch 'store_unit_testing' into 'v3'

ui_migration test(store) add tests for wot, and settings

Closes #694

See merge request openflexure/openflexure-microscope-server!602
This commit is contained in:
Julian Stirling 2026-06-05 16:10:10 +00:00
commit 98b7f23148
2 changed files with 342 additions and 0 deletions

View file

@ -0,0 +1,101 @@
import { setActivePinia, createPinia } from "pinia";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { useSettingsStore } from "../../../stores/settings";
describe("Settings Store", () => {
beforeEach(() => {
// Mock the browser's window.location BEFORE the store initializes
// This prevents the getOriginFromLocation() function from crashing.
vi.stubGlobal("location", {
// The following baseUri is arbitrarily set
href: "http://microscope-test.local:5000/",
});
// Create a fresh Pinia instance for every single test
// This guarantees state doesn't leak between the 'it' blocks.
setActivePinia(createPinia());
});
describe("Initialization", () => {
it("parses the baseUri correctly from window.location", () => {
const store = useSettingsStore();
// It should extract the origin and append /api/v3
expect(store.baseUri).toBe("http://microscope-test.local:5000/api/v3");
});
it("initializes with correct default state", () => {
const store = useSettingsStore();
// Checks whole state items
expect(store.ready).toBe(false);
expect(store.waiting).toBe(false);
expect(store.appTheme).toBe("system");
expect(store.activeStreams).toEqual({});
expect(store.error).toBe("");
expect(store.trackWindow).toBe(true);
expect(store.microscopeHostname).toBe("");
expect(store.disableStream).toBe(false);
expect(store.overrideOrigin).toBe("http://microscope.local:5000/api/v3");
expect(store.navigationStepSize).toEqual({
x: 200,
y: 200,
z: 50,
});
expect(store.navigationInvert).toEqual({
x: false,
y: false,
z: false,
});
});
});
describe("Actions", () => {
it("setConnected() updates waiting and ready states", () => {
const store = useSettingsStore();
// Mutate state to ensure setConnected overrides it
store.waiting = true;
store.ready = false;
store.setConnected();
expect(store.waiting).toBe(false);
expect(store.ready).toBe(true);
});
it("resetState() clears connection status and sets error", () => {
const store = useSettingsStore();
// Mutate state
store.waiting = true;
store.ready = true;
store.error = "";
store.resetState();
expect(store.waiting).toBe(false);
expect(store.ready).toBe(false);
expect(store.error).toBe("Microscope is not connected.");
});
it("addStream() adds a stream ID to activeStreams", () => {
const store = useSettingsStore();
store.addStream("mjpeg_stream");
expect(store.activeStreams["mjpeg_stream"]).toBe(true);
});
it("removeStream() sets a stream ID to false in activeStreams", () => {
const store = useSettingsStore();
// Setup initial state
store.activeStreams = { mjpeg_stream: true };
store.removeStream("mjpeg_stream");
expect(store.activeStreams["mjpeg_stream"]).toBe(false);
});
});
});

View file

@ -0,0 +1,241 @@
import { setActivePinia, createPinia } from "pinia";
import { describe, it, expect, beforeEach, vi } from "vitest";
import axios from "axios";
import { useWotStore, findFormHref } from "../../../stores/wot";
// Mock Axios
vi.mock("axios");
describe("WoT Store", () => {
// A reusable dummy Thing Description (TD)
const mockTD = {
base: "http://microscope.local/api/v3/",
properties: {
status: {
forms: [{ op: ["readproperty", "writeproperty"], href: "status" }],
},
},
actions: {
toggle: {
forms: [{ op: "invokeaction", href: "http://external.com/api/v3/toggle" }],
},
},
};
beforeEach(() => {
// New Pinia for every test to avoid leaks and inter-playing states
// Clear axios call history between tests
setActivePinia(createPinia());
vi.clearAllMocks();
});
describe("Standalone Helper: findFormHref", () => {
it("returns undefined if affordance is missing", () => {
expect(findFormHref(undefined, "read")).toBeUndefined();
});
it("finds href when op is an array", () => {
const affordance = mockTD.properties.status;
expect(findFormHref(affordance, "readproperty")).toBe("status");
});
it("finds href when op is a string", () => {
const affordance = mockTD.actions.toggle;
expect(findFormHref(affordance, "invokeaction")).toBe("http://external.com/api/v3/toggle");
});
it("returns undefined if op does not match", () => {
const affordance = mockTD.properties.status;
expect(findFormHref(affordance, "fakeoperation")).toBeUndefined();
});
});
describe("State & Basic Actions", () => {
it("adds, removes, and clears thing descriptions", () => {
const store = useWotStore();
store.addThingDescription("camera", mockTD);
expect(store.thingDescriptions["camera"]).toEqual(mockTD);
expect(store.thingList).toContain("camera");
store.removeThingDescription("camera");
expect(store.thingDescriptions["camera"]).toBeUndefined();
expect(store.thingList.length).toBe(0);
// Test clear all
store.addThingDescription("cam1", mockTD);
store.addThingDescription("cam2", mockTD);
store.removeAllThingDescriptions();
expect(store.thingList.length).toBe(0);
});
it("correctly identifies if a thing is available", () => {
const store = useWotStore();
store.addThingDescription("camera", mockTD);
expect(store.thingAvailable("camera")).toBe(true);
expect(store.thingAvailable("stage")).toBe(false);
});
});
describe("Network Actions (Axios)", () => {
it("fetches a single Thing Description WITH a provided name", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({ data: mockTD });
await store.fetchThingDescription("http://microscope.local/api/v3/camera", "myCamera");
expect(axios.get).toHaveBeenCalledWith("http://microscope.local/api/v3/camera");
expect(store.thingDescriptions["myCamera"]).toEqual(mockTD);
});
it("fetches a single Thing Description and extracts name from URI if missing", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({ data: mockTD });
// The logic should split by "/" and grab "stage"
await store.fetchThingDescription("http://microscope.local/api/v3/stage/");
expect(store.thingDescriptions["stage"]).toEqual(mockTD);
});
it("fetches multiple Thing Descriptions", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({
status: 200,
data: {
"/sensor/": { title: "Sensor TD" },
motor: { title: "Motor TD" },
},
});
await store.fetchThingDescriptions("http://microscope.local/api/v3/things");
// The logic strips slashes: "/sensor/" -> "sensor"
expect(store.thingDescriptions["sensor"].title).toBe("Sensor TD");
expect(store.thingDescriptions["motor"].title).toBe("Motor TD");
});
it("throws an error if fetchThingDescriptions fails", async () => {
const store = useWotStore();
axios.get.mockResolvedValueOnce({ status: 404 });
await expect(store.fetchThingDescriptions("http://bad-url")).rejects.toMatch(
"Could not retrieve",
);
});
});
describe("TD Parsing & URL Generation", () => {
beforeEach(() => {
const store = useWotStore();
const mockTD = {
base: "http://microscope.local/api/v3/",
properties: {
status: {
forms: [{ op: ["readproperty", "writeproperty"], href: "status" }],
},
},
actions: {
toggle: {
forms: [{ op: "invokeaction", href: "http://external.com/api/v3/toggle" }],
},
},
};
store.addThingDescription("microscope", mockTD);
});
it("checks affordance availability (and covers Line 55)", () => {
const store = useWotStore();
// Success case
expect(store.thingAffordanceAvailable("microscope", "properties", "status")).toBe(true);
// Missing Thing (!td)
expect(store.thingAffordanceAvailable("fakeThing", "properties", "status")).toBe(false);
// Missing Property inside an existing Category
expect(store.thingAffordanceAvailable("microscope", "properties", "fakeProp")).toBe(false);
// Thing exists, but the Category/AffordanceType ('events') does not exist at all!
expect(store.thingAffordanceAvailable("microscope", "events", "someEvent")).toBe(false);
});
it("handles completely missing forms, affordances, and operations", () => {
const store = useWotStore();
// Missing Thing entirely
expect(
store.thingFormUrl("fakeThing", "properties", "missing", "read", true),
).toBeUndefined();
expect(() => {
store.thingFormUrl("fakeThing", "properties", "missing", "read", false);
}).toThrowError(/Could not find form/);
// The 'microscope' exists, but 'fakeProp' does not.
expect(
store.thingFormUrl("microscope", "properties", "fakeProp", "read", true),
).toBeUndefined();
expect(() => {
store.thingFormUrl("microscope", "properties", "fakeProp", "read", false);
}).toThrowError(/Could not find form/);
// 'microscope' and 'status' exist, but 'fakeOp' is missing.
expect(
store.thingFormUrl("microscope", "properties", "status", "fakeOp", true),
).toBeUndefined();
expect(() => {
store.thingFormUrl("microscope", "properties", "status", "fakeOp", false);
}).toThrowError(/Could not find form/);
});
it("thingFormUrl concats base url with relative href", () => {
const store = useWotStore();
const url = store.thingFormUrl("microscope", "properties", "status", "readproperty");
expect(url).toBe("http://microscope.local/api/v3/status");
});
it("thingFormUrl uses absolute href directly if it starts with http", () => {
const store = useWotStore();
const url = store.thingFormUrl("microscope", "actions", "toggle", "invokeaction");
expect(url).toBe("http://external.com/api/v3/toggle");
});
it("returns relative href if the Thing Description has no base property", () => {
const store = useWotStore();
store.addThingDescription("noBaseThing", {
properties: {
status: { forms: [{ op: "readproperty", href: "relative-path-only" }] },
},
});
const url = store.thingFormUrl("noBaseThing", "properties", "status", "readproperty");
expect(url).toBe("relative-path-only");
});
it("normalizes slashes when base lacks trailing slash and href has leading slash", () => {
const store = useWotStore();
store.addThingDescription("messyThing", {
// No trailing slash
base: "http://microscope.local/api/v3",
properties: {
// Extra leading slash
status: { forms: [{ op: "readproperty", href: "/status" }] },
},
});
const url = store.thingFormUrl("messyThing", "properties", "status", "readproperty");
expect(url).toBe("http://microscope.local/api/v3/status");
});
it("thingPropertyUrl and thingActionUrl proxy correctly", () => {
const store = useWotStore();
expect(store.thingPropertyUrl("microscope", "status", "readproperty")).toBe(
"http://microscope.local/api/v3/status",
);
expect(store.thingActionUrl("microscope", "toggle", "invokeaction")).toBe(
"http://external.com/api/v3/toggle",
);
});
});
});