ui_migration test(store) add tests for wot, and settings
This commit is contained in:
parent
674e52e224
commit
4f1938696a
2 changed files with 327 additions and 0 deletions
83
webapp/src/tests/unit/stores/settings.spec.js
Normal file
83
webapp/src/tests/unit/stores/settings.spec.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
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 basUri is arbitrary 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();
|
||||||
|
expect(store.ready).toBe(false);
|
||||||
|
expect(store.waiting).toBe(false);
|
||||||
|
expect(store.appTheme).toBe("system");
|
||||||
|
expect(store.activeStreams).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
244
webapp/src/tests/unit/stores/wot.spec.js
Normal file
244
webapp/src/tests/unit/stores/wot.spec.js
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
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)
|
||||||
|
// This will be reworked by using the HAR fixtures or the fixtures/common_definitions.js
|
||||||
|
// This test uses arbitrary values for simplicity but this could add confusion
|
||||||
|
// How-to-test documentation would require a glossary of defined mock values
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue