ui_migration feature(vitest) Revisit allComponents.spec.js add jsdocstrings

This commit is contained in:
Antonio Anaya 2026-06-09 15:36:38 -06:00 committed by Antonio Anaya
parent 8f20e1211e
commit 5ec088e07a
2 changed files with 81 additions and 37 deletions

View file

@ -3,16 +3,33 @@ import { shallowMount, flushPromises } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing"; import { createTestingPinia } from "@pinia/testing";
import { markRaw } from "vue"; import { markRaw } from "vue";
// Eagerly import ALL .vue files at the components directory /**
// Except the ones inside experimental * @file Automated component smoke testing suite.
const componentModules = Object.fromEntries( * @description Iterates through all project components and performs a
Object.entries(import.meta.glob("../../components/**/*.vue", { eager: true })), * 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<string, { default: any }>} 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"]; 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 = { const componentOverrides = {
"serverSpecifiedPropertyControl.vue": { "serverSpecifiedPropertyControl.vue": {
props: { props: {
@ -26,8 +43,8 @@ const componentOverrides = {
thingDescriptions: { thingDescriptions: {
test_thing: { test_thing: {
properties: { properties: {
property_1: {}, test_property_1: {},
fancy_property_2: {}, test_property_2: {},
}, },
}, },
}, },
@ -43,8 +60,8 @@ const componentOverrides = {
thingDescriptions: { thingDescriptions: {
test_thing: { test_thing: {
properties: { properties: {
fancy_property_1: {}, test_property_1: {},
fancy_property_2: {}, test_property_2: {},
}, },
}, },
}, },
@ -60,8 +77,8 @@ const componentOverrides = {
thingDescriptions: { thingDescriptions: {
test_thing: { test_thing: {
properties: { properties: {
fancy_property_1: {}, test_property_1: {},
fancy_property_2: {}, 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; let wrapper;
/**
* Environment setup
* set Fake timers and Mocks
*/
beforeEach(() => { beforeEach(() => {
// Environment setup
vi.useFakeTimers(); vi.useFakeTimers();
vi.resetAllMocks(); 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(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
wrapper.unmount(); wrapper.unmount();
wrapper = null; wrapper = null;
} }
// Clean up virtual DOM and memory bindings
document.body.innerHTML = ""; document.body.innerHTML = "";
// Forces V8 engine to immediately run Garbage Collection
if (global.gc) { if (global.gc) {
global.gc(); global.gc();
} }
@ -234,54 +264,68 @@ describe("Automated Component Smoke Tests", () => {
await new Promise((resolve) => setTimeout(resolve, 15)); 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) { for (const path in componentModules) {
const component = componentModules[path].default; const component = componentModules[path].default;
const fileName = path.split("/").pop(); const fileName = path.split("/").pop();
const isSkipped = skipList.includes(fileName) || path.toLowerCase().includes("experimental"); 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 skips vue files that are corrupted or have no meta content
// This check serves the purpose to avoid a file halting a CI pipeline * return is preferred as a wrong path in file could lead to halt on CI pipeline or over verbose output
if (!component) { * 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); console.error(component);
// Generate a test specifically to report this failure to Vitest
it(`${fileName} has a default export`, () => { const errorName = fileName + "Wrong Path";
expect.fail(
`Test Generation Failed: No content in "${fileName}". ` + it(`${errorName} has no correct Path`, () => {
`Please fix this file or add it to the skipList.`, expect.fail(`Incorrect Path parsing for "${fileName}"`);
);
}); });
// Skip generating the REST of the test suite for this broken file
return; 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; const testRunner = isSkipped ? it.skip : it;
// Run the test
testRunner(`Shallow mounts ${fileName} without crashing`, async () => { testRunner(`Shallow mounts ${fileName} without crashing`, async () => {
// Look up the specific config for this file, default to an empty object
const override = componentOverrides[fileName] || {}; const override = componentOverrides[fileName] || {};
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
attachTo: document.body, attachTo: document.body,
// Inject the specific props here!
props: override.props || {}, props: override.props || {},
global: { global: {
plugins: [ plugins: [
createTestingPinia({ createTestingPinia({
createSpy: vi.fn, createSpy: vi.fn,
// Inject the specific state if it exists, otherwise use empty object
initialState: override.initialState || {}, initialState: override.initialState || {},
}), }),
], ],
stubs: {}, stubs: {},
mocks: { mocks: {
// These mocks come from MIXINS
thingActionAvailable: vi.fn(() => true), thingActionAvailable: vi.fn(() => true),
readThingProperty: vi.fn(() => ({})), readThingProperty: vi.fn(() => ({})),
thingAvailable: vi.fn(() => ({})), thingAvailable: vi.fn(() => ({})),

View file

@ -43,7 +43,7 @@ afterEach(({ task }) => {
(args) => typeof args[0] === "string" && args[0].includes("[Vue warn]"), (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) { if (vueWarnings.length > 0) {
const warningMessages = vueWarnings.map((args) => args.join(" ")).join("\n\n"); const warningMessages = vueWarnings.map((args) => args.join(" ")).join("\n\n");