diff --git a/webapp/src/tests/unit/allComponents.spec.js b/webapp/src/tests/unit/allComponents.spec.js index ca98e99d..00c47c30 100644 --- a/webapp/src/tests/unit/allComponents.spec.js +++ b/webapp/src/tests/unit/allComponents.spec.js @@ -7,13 +7,23 @@ import { componentOverrides } from "./overrides"; * @file Automated component smoke testing suite. * @description Iterates through all project components and performs a * shallow mount on each. This acts as a first line of defense to catch - * fatal DOM runtime errors, missing dependencies, or script crashes + * fatal DOM runtime errors, missing dependencies, memory leaks, lifecycle or script crashes * before they reach production. + * @note CI Execution: + * When running this script in a CI/CD pipeline, it must be executed with strict memory + * safety and process isolation to handle the heavy footprint of mounting the whole set of Vue components (). + * The `test:unit` script implements the following safety nets: + * - `NODE_OPTIONS=--expose-gc` & `--logHeapUsage`: Unlocks the V8 garbage collector to aggressively clear RAM. + * - `--pool=forks` & `--isolate=true`: Boots a dedicated Node.js child process per file to prevent cross-DOM pollution. + * - `--detectAsyncLeaks`: Actively catches hanging `setIntervals` or unresolved API promises. + * - `--no-cache`: Bypasses Vite caching to guarantee a pristine execution environment. + * - `--reporter=verbose`: Forces cloud logs to print exact component names for easier debugging. * @requires overrides.js for component-specific prop and mock configurations. */ -/** * Vite eagerly grabs all .vue components, explicitly excluding the experimental folder. - * * @type {Record} A dictionary mapping file paths to their Vue component modules. +/** + * Vite eagerly grabs all .vue components, explicitly excluding the experimental folder. + * @type {Record} A dictionary mapping file paths to their Vue component modules. */ const componentModules = import.meta.glob( ["../../components/**/*.vue", "!../../components/experimental/**/*.vue"], @@ -131,25 +141,24 @@ describe("Unit Test", () => { initialState: override.initialState || {}, }), ], - stubs: {}, mocks: { - thingActionAvailable: vi.fn(() => true), - readThingProperty: vi.fn(() => ({})), - thingAvailable: vi.fn(() => ({})), - thingDescription: vi.fn(() => ({ + readThingProperty: () => ({}), + thingActionAvailable: () => true, + thingAvailable: () => ({}), + thingDescription: () => ({ actions: {}, properties: {}, - })), - thingDescriptions: vi.fn(() => ({ + }), + thingDescriptions: () => ({ test_thing: { properties: { test_property_1: {}, test_property_2: {}, }, }, - })), - getOngoingAction: vi.fn(() => Promise.resolve()), - getThingEndpoint: vi.fn(() => Promise.resolve([])), + }), + getOngoingAction: () => Promise.resolve(), + getThingEndpoint: () => Promise.resolve([]), modalError: "", ...(override.global?.mocks || {}), ...(override.mocks || {}), diff --git a/webapp/src/tests/unit/overrides.js b/webapp/src/tests/unit/overrides.js index 39fc3dd0..bce3a3c7 100644 --- a/webapp/src/tests/unit/overrides.js +++ b/webapp/src/tests/unit/overrides.js @@ -7,6 +7,13 @@ import { markRaw } from "vue"; * If a component has unmet props or global mocks setup.js is set to fail the test **/ export const componentOverrides = { + "galleryContent.vue": { + global: { + mocks: { + readThingProperty: () => [], + }, + }, + }, "serverSpecifiedPropertyControl.vue": { props: { propertyData: {}, diff --git a/webapp/src/tests/unit/setup.js b/webapp/src/tests/unit/setup.js index b2870900..fff8ba25 100644 --- a/webapp/src/tests/unit/setup.js +++ b/webapp/src/tests/unit/setup.js @@ -1,9 +1,20 @@ import { vi, beforeEach, afterEach } from "vitest"; -let consoleWatchdog; +/** + * @file: Global setup for unit tests. + * @description This file sets up the testing environment for all unit tests, including: + * - Mocking localStorage to prevent side effects and ensure test isolation. + * - Spying on console.warn and console.error to catch any Vue warnings or uncaught exceptions during component mounting. + * - Cleaning up the DOM and memory after each test to prevent leaks and ensure a fresh state for subsequent tests. + * - Enforcing strict failure on any Vue warnings or console errors to maintain high code quality and catch issues early. + */ -// Create an isolated, in-memory mock for localStorage +let consoleWarnWatchdog; +let consoleErrorWatchdog; +/** + * Mock implementation of localStorage for testing purposes. + */ const localStorageMock = (() => { let store = {}; return { @@ -20,35 +31,55 @@ const localStorageMock = (() => { }; })(); -// Override Node's native localStorage +/** + * Override Node's native localStorage + */ vi.stubGlobal("localStorage", localStorageMock); -// Clean up +/** + * clean initial state before each test and set up console spies to catch warnings and errors. + */ beforeEach(() => { localStorage.clear(); - consoleWatchdog = vi.spyOn(console, "warn"); + consoleWarnWatchdog = vi.spyOn(console, "warn"); + consoleErrorWatchdog = vi.spyOn(console, "error"); }); +/** + * After each test, check for any Vue warnings or console errors that occurred during component mounting. + * If any warnings or errors are detected, fail the test and print the relevant messages for debugging. + */ afterEach(({ task }) => { - if (!consoleWatchdog) return; + if (!consoleWarnWatchdog) return; - // Grab all warnings - const warnings = consoleWatchdog.mock.calls; + /** Grab all warnings */ + const warnings = consoleWarnWatchdog.mock.calls; - // Clean up the spy so it doesn't leak - consoleWatchdog.mockRestore(); + /** Clean up the spy so it doesn't leak */ + consoleWarnWatchdog.mockRestore(); - // Filter specifically for Vue warnings + /** Filter specifically for Vue warnings */ const vueWarnings = warnings.filter( (args) => typeof args[0] === "string" && args[0].includes("[Vue warn]"), ); - // If Vue has warns forcefully fail this test block --max-warnings=0 + /** If Vue has warns forcefully fail this test block --max-warnings=0 */ if (vueWarnings.length > 0) { const warningMessages = vueWarnings.map((args) => args.join(" ")).join("\n\n"); - // Throw a plain string instead of using expect.fail() or new Error()! - // Without an Error object, there is no stack trace, so Vitest CANNOT show a code snippet. + /** Throw a plain string instead of using expect.fail() or new Error()! + * Without an Error object, there is no stack trace, so Vitest CANNOT show a code snippet. + */ throw `[Vue warn] Failure in "${task.name}":\n\n${warningMessages}`; } + /** Add watchdog for console.error to catch any uncaught exceptions during component mounting */ + if (consoleErrorWatchdog) { + const errors = consoleErrorWatchdog.mock.calls; + consoleErrorWatchdog.mockRestore(); + + if (errors.length > 0) { + const errorMessages = errors.map((args) => args.join(" ")).join("\n\n"); + throw `[Console Error] Hard crash in "${task.name}":\n\n${errorMessages}`; + } + } });