From 5ec088e07ab38c1a00052b05a0e778976c4a0e30 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Tue, 9 Jun 2026 15:36:38 -0600 Subject: [PATCH] ui_migration feature(vitest) Revisit allComponents.spec.js add jsdocstrings --- webapp/src/tests/unit/allComponents.spec.js | 116 ++++++++++++++------ webapp/src/tests/unit/setup.js | 2 +- 2 files changed, 81 insertions(+), 37 deletions(-) diff --git a/webapp/src/tests/unit/allComponents.spec.js b/webapp/src/tests/unit/allComponents.spec.js index 8843bdae..83c3aded 100644 --- a/webapp/src/tests/unit/allComponents.spec.js +++ b/webapp/src/tests/unit/allComponents.spec.js @@ -3,16 +3,33 @@ import { shallowMount, flushPromises } from "@vue/test-utils"; import { createTestingPinia } from "@pinia/testing"; import { markRaw } from "vue"; -// Eagerly import ALL .vue files at the components directory -// Except the ones inside experimental -const componentModules = Object.fromEntries( - Object.entries(import.meta.glob("../../components/**/*.vue", { eager: true })), +/** + * @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 + * before they reach production. + */ + +/** * 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"], + { eager: true }, ); -// Skip list, add here components that have their own test spec.js file + +/** + * IMPORTANT + * Skip list, add here components that have their own test .spec.js file + * */ const skipList = ["loggingContent.vue"]; -// Map filenames to the specific props or mocks required -// Not all components ask for props +/** + * Map filenames to the specific props or mocks required + * Not all components ask for props + * If a component has unmet props or global mocks setup.js is set to fail the test + **/ const componentOverrides = { "serverSpecifiedPropertyControl.vue": { props: { @@ -26,8 +43,8 @@ const componentOverrides = { thingDescriptions: { test_thing: { properties: { - property_1: {}, - fancy_property_2: {}, + test_property_1: {}, + test_property_2: {}, }, }, }, @@ -43,8 +60,8 @@ const componentOverrides = { thingDescriptions: { test_thing: { properties: { - fancy_property_1: {}, - fancy_property_2: {}, + test_property_1: {}, + test_property_2: {}, }, }, }, @@ -60,8 +77,8 @@ const componentOverrides = { thingDescriptions: { test_thing: { properties: { - fancy_property_1: {}, - fancy_property_2: {}, + test_property_1: {}, + test_property_2: {}, }, }, }, @@ -202,26 +219,39 @@ const componentOverrides = { }, }; -describe("Automated Component Smoke Tests", () => { - // Declare wrapper at this scope so afterEach can clean it up +/** + * Test description this is a fast unit test + * The objective of this testing is find early + * component loading issues like bad component setups + */ + +describe("Unit Test", () => { let wrapper; + /** + * Environment setup + * set Fake timers and Mocks + */ beforeEach(() => { - // Environment setup vi.useFakeTimers(); vi.resetAllMocks(); }); + /** + * Clean up after every single test + * Unmounts any present component wrapper= + * Clean up virtual DOM and memory bindings + * Forces V8 engine to immediately run Garbage Collection + * clears out timers to avoid leaks + */ afterEach(() => { if (wrapper) { wrapper.unmount(); wrapper = null; } - // Clean up virtual DOM and memory bindings document.body.innerHTML = ""; - // Forces V8 engine to immediately run Garbage Collection if (global.gc) { global.gc(); } @@ -234,54 +264,68 @@ describe("Automated Component Smoke Tests", () => { await new Promise((resolve) => setTimeout(resolve, 15)); }); - // Loop through the file paths + /** + * Loop through the file paths + * Unwraps the component and exposes its definition using ".default" + * Includes meta information like name, props, components, methods. + * Do not confuse with export default from script component section + * Skips components at the skipList from files that are explicitly skipped + * Also skips components which name starts with the word "experimental" + * allowing to work with edge case components if needed + * is discouraged to push components that won't pass a smoke test + * */ for (const path in componentModules) { const component = componentModules[path].default; const fileName = path.split("/").pop(); const isSkipped = skipList.includes(fileName) || path.toLowerCase().includes("experimental"); - // This check skips vue files that are corrupted or have no meta content - // .default has meta content even without explicit default export - // This check serves the purpose to avoid a file halting a CI pipeline - if (!component) { + /** + * This check skips vue files that are corrupted or have no meta content + * return is preferred as a wrong path in file could lead to halt on CI pipeline or over verbose output + * An error here means a wrong path or file access error + * This was needed during the writing of this script + * Leaving for other future edge cases usage + * This fails the overall test + * */ + if (!component || undefined) { console.error(component); - // Generate a test specifically to report this failure to Vitest - it(`${fileName} has a default export`, () => { - expect.fail( - `Test Generation Failed: No content in "${fileName}". ` + - `Please fix this file or add it to the skipList.`, - ); + + const errorName = fileName + "Wrong Path"; + + it(`${errorName} has no correct Path`, () => { + expect.fail(`Incorrect Path parsing for "${fileName}"`); }); - // Skip generating the REST of the test suite for this broken file return; } - // Dynamically assign the test runner (it.skip if in the list, otherwise standard it) + /** + * Dynamically assign the test runner (it.skip if in the list, otherwise standard it) + * Run the test + * Look up the specific config for this file, default to an empty object + * Override specific props + * Inject the specific state if it exists, otherwise use empty object + * Mocks values come from MIXINS + */ const testRunner = isSkipped ? it.skip : it; - // Run the test testRunner(`Shallow mounts ${fileName} without crashing`, async () => { - // Look up the specific config for this file, default to an empty object const override = componentOverrides[fileName] || {}; wrapper = shallowMount(component, { attachTo: document.body, - // Inject the specific props here! props: override.props || {}, global: { plugins: [ createTestingPinia({ createSpy: vi.fn, - // Inject the specific state if it exists, otherwise use empty object initialState: override.initialState || {}, }), ], stubs: {}, mocks: { - // These mocks come from MIXINS thingActionAvailable: vi.fn(() => true), readThingProperty: vi.fn(() => ({})), thingAvailable: vi.fn(() => ({})), diff --git a/webapp/src/tests/unit/setup.js b/webapp/src/tests/unit/setup.js index 8abd3ae1..b2870900 100644 --- a/webapp/src/tests/unit/setup.js +++ b/webapp/src/tests/unit/setup.js @@ -43,7 +43,7 @@ afterEach(({ task }) => { (args) => typeof args[0] === "string" && args[0].includes("[Vue warn]"), ); - // If Vue has warns forcefully fail this test block + // 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");