From 4f1938696ab0287614e6003cf0cc3810dbcd4fa6 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Wed, 27 May 2026 19:52:33 -0600 Subject: [PATCH] ui_migration test(store) add tests for wot, and settings --- webapp/src/tests/unit/stores/settings.spec.js | 83 ++++++ webapp/src/tests/unit/stores/wot.spec.js | 244 ++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 webapp/src/tests/unit/stores/settings.spec.js create mode 100644 webapp/src/tests/unit/stores/wot.spec.js diff --git a/webapp/src/tests/unit/stores/settings.spec.js b/webapp/src/tests/unit/stores/settings.spec.js new file mode 100644 index 00000000..cabb8244 --- /dev/null +++ b/webapp/src/tests/unit/stores/settings.spec.js @@ -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); + }); + }); +}); diff --git a/webapp/src/tests/unit/stores/wot.spec.js b/webapp/src/tests/unit/stores/wot.spec.js new file mode 100644 index 00000000..4d7a8fd6 --- /dev/null +++ b/webapp/src/tests/unit/stores/wot.spec.js @@ -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", + ); + }); + }); +});