From 8f20e1211e6037367a71791f7e378bfe30d1d262 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Tue, 9 Jun 2026 04:54:49 -0600 Subject: [PATCH] ui_migration feature(vitest) Add setup for skipped experimental components --- .gitlab-ci.yml | 12 -- webapp/.gitignore | 8 +- webapp/package.json | 2 +- webapp/src/tests/unit/allComponents.spec.js | 177 +++++++++++++------- webapp/src/tests/unit/setup.js | 30 +++- webapp/vitest.config.js | 5 +- 6 files changed, 158 insertions(+), 76 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f22c066a..6dfd7d6e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -161,18 +161,6 @@ javascript-lint-and-test: - npm run lint:style # The test runs only if a test spec file has changed - npm run test:unit - artifacts: - name: "vitest-coverage-$CI_COMMIT_REF_SLUG" - when: always - expire_in: 1 week - paths: - - webapp/coverage/ - reports: - # Tells GitLab where to find the Cobertura file for MR integration - coverage_report: - coverage_format: cobertura - path: webapp/coverage/cobertura-coverage.xml - # Build JS app build: diff --git a/webapp/.gitignore b/webapp/.gitignore index 2f59b8bf..2384ab27 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -27,4 +27,10 @@ yarn-error.log* # NativeScript application hooks platforms -./webpack.config.js \ No newline at end of file +./webpack.config.js + +# Vue test skipped files +**/*experimental*.vue + +# Experimental Vue components +src/components/experimental/ \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index c9e95b23..076b66f2 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -16,7 +16,7 @@ "lint:fix": "eslint . --fix --ignore-pattern .gitignore", "lint:style": "stylelint \"src/**/*.{vue,less,css}\" --max-warnings=0", "lint:style:fix": "stylelint \"src/**/*.{vue,less,css}\" --fix", - "test:unit": "NODE_OPTIONS=--expose-gc vitest run src/tests/unit/ --no-cache --mode test --detectAsyncLeaks --coverage --isolate=true --pool=forks --silent --logHeapUsage" + "test:unit": "NODE_OPTIONS=--expose-gc vitest run src/tests/unit/ --no-cache --mode test --detectAsyncLeaks --isolate=true --pool=forks --logHeapUsage --reporter=verbose" }, "dependencies": { "@vueuse/core": "^14.3.0", diff --git a/webapp/src/tests/unit/allComponents.spec.js b/webapp/src/tests/unit/allComponents.spec.js index 606b4a92..8843bdae 100644 --- a/webapp/src/tests/unit/allComponents.spec.js +++ b/webapp/src/tests/unit/allComponents.spec.js @@ -1,48 +1,98 @@ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest"; 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 -const componentModules = import.meta.glob("../../components/**/*.vue", { eager: true }); - -// Skip list, for components that have their own test spec.js file +// Except the ones inside experimental +const componentModules = Object.fromEntries( + Object.entries(import.meta.glob("../../components/**/*.vue", { eager: true })), +); +// 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 const componentOverrides = { - "paginateLinks.vue": { + "serverSpecifiedPropertyControl.vue": { props: { - totalPages: 5, - currentPage: 1, + propertyData: {}, + propertyName: "", + thingName: "", }, + global: { + mocks: { + wotStore: { + thingDescriptions: { + test_thing: { + properties: { + property_1: {}, + fancy_property_2: {}, + }, + }, + }, + }, + }, + }, + }, + + "slideScanContent.vue": { + global: { + mocks: { + wotStore: { + thingDescriptions: { + test_thing: { + properties: { + fancy_property_1: {}, + fancy_property_2: {}, + }, + }, + }, + }, + }, + }, + }, + + "slideScanControls.vue": { + global: { + mocks: { + wotStore: { + thingDescriptions: { + test_thing: { + properties: { + fancy_property_1: {}, + fancy_property_2: {}, + }, + }, + }, + }, + }, + }, + }, + + "miniStreamDisplay.vue": { + props: { streamId: "testProp" }, + }, + "streamContent.vue": { + props: { streamId: "testProp" }, + }, + "paginateLinks.vue": { + props: { totalPages: 5, currentPage: 1 }, }, "simpleAccordion.vue": { props: { title: "Test Accordion Title" }, }, "tabContent.vue": { - props: { - tabID: "tab-1", - currentTab: "tab-1", - }, + props: { tabID: "tab-1", currentTab: "tab-1" }, }, "tabIcon.vue": { - props: { - tabID: "tab-1", - currentTab: "tab-1", - }, + props: { tabID: "tab-1", currentTab: "tab-1" }, }, "actionButton.vue": { - props: { - action: "test-action", - thing: "test-thing", - }, + props: { action: "test-action", thing: "test-thing" }, }, "actionLogDisplay.vue": { - props: { - taskStatus: "", - log: [], - }, + props: { taskStatus: "", log: [] }, }, "endActionButton.vue": { props: { url: "http://microscope.local/api/v3" }, @@ -67,20 +117,23 @@ const componentOverrides = { }, }, "inputFromSchema.vue": { - props: { - dataSchema: {}, - }, + props: { dataSchema: {} }, }, + "propertyControl.vue": { props: { propertyName: "", - thingName: "", + thingName: "test_thing", dataSchema: {}, }, + mocks: { wotStore: { thingDescriptions: { - actions: {}, + test_thing: { + actions: {}, + properties: {}, + }, }, }, }, @@ -95,25 +148,14 @@ const componentOverrides = { }, }, "serverSpecifiedInterface.vue": { - props: { - elements: [], - }, - }, - "serverSpecifiedPropertyControl.vue": { - props: { - propertyData: {}, - propertyName: "", - thingName: "", - }, + props: { elements: [] }, }, "calibrationWizardTask.vue": { - props: { - steps: [], - }, + props: { steps: [] }, }, "singleStepTask.vue": { props: { - stepComponent: {}, + stepComponent: markRaw({ template: '
' }), }, }, "actionTab.vue": { @@ -125,9 +167,7 @@ const componentOverrides = { }, }, "galleryCard.vue": { - props: { - itemData: {}, - }, + props: { itemData: {} }, }, "openSeadragonViewer.vue": { props: { @@ -138,20 +178,24 @@ const componentOverrides = { }, }, "cameraCalibrationSettings.vue": { - props: { - cameraUri: "", - }, + props: { cameraUri: "" }, }, + "galleryContent.vue": { - mocks: { - readThingProperty: vi.fn(() => []), + global: { + mocks: { + readThingProperty: vi.fn(() => []), + }, }, }, + "CSMCalibrationSettings.vue": { - mocks: { - thingDescriptions: { - csm: { - actions: {}, + global: { + mocks: { + thingDescriptions: { + csm: { + actions: {}, + }, }, }, }, @@ -194,15 +238,30 @@ describe("Automated Component Smoke Tests", () => { for (const path in componentModules) { const component = componentModules[path].default; const fileName = path.split("/").pop(); - const isSkipped = skipList.includes(fileName); + const isSkipped = skipList.includes(fileName) || path.toLowerCase().includes("experimental"); - if (!component) return; + // 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) { + 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.`, + ); + }); + + // 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) 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] || {}; @@ -222,7 +281,7 @@ describe("Automated Component Smoke Tests", () => { ], stubs: {}, mocks: { - // These values come from MIXINS + // These mocks come from MIXINS thingActionAvailable: vi.fn(() => true), readThingProperty: vi.fn(() => ({})), thingAvailable: vi.fn(() => ({})), @@ -230,17 +289,17 @@ describe("Automated Component Smoke Tests", () => { actions: {}, properties: {}, })), - thingDescriptions: vi.fn(() => ({})), getOngoingAction: vi.fn(() => Promise.resolve()), getThingEndpoint: vi.fn(() => Promise.resolve([])), modalError: "", + ...(override.global?.mocks || {}), ...(override.mocks || {}), }, }, }); await flushPromises(); - vi.advanceTimersByTime(1000); + vi.advanceTimersByTime(200); expect(wrapper.exists()).toBe(true); }); } diff --git a/webapp/src/tests/unit/setup.js b/webapp/src/tests/unit/setup.js index 9fbbf9f1..8abd3ae1 100644 --- a/webapp/src/tests/unit/setup.js +++ b/webapp/src/tests/unit/setup.js @@ -1,6 +1,9 @@ -import { vi, beforeEach } from "vitest"; +import { vi, beforeEach, afterEach } from "vitest"; + +let consoleWatchdog; // Create an isolated, in-memory mock for localStorage + const localStorageMock = (() => { let store = {}; return { @@ -23,4 +26,29 @@ vi.stubGlobal("localStorage", localStorageMock); // Clean up beforeEach(() => { localStorage.clear(); + consoleWatchdog = vi.spyOn(console, "warn"); +}); + +afterEach(({ task }) => { + if (!consoleWatchdog) return; + + // Grab all warnings + const warnings = consoleWatchdog.mock.calls; + + // Clean up the spy so it doesn't leak + consoleWatchdog.mockRestore(); + + // 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 + 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 `[Vue warn] Failure in "${task.name}":\n\n${warningMessages}`; + } }); diff --git a/webapp/vitest.config.js b/webapp/vitest.config.js index 7ad5f011..1e181a01 100644 --- a/webapp/vitest.config.js +++ b/webapp/vitest.config.js @@ -41,11 +41,12 @@ export default defineConfig({ clean: true, cleanOnStart: true, // text gives output on STOUT and html creates an artifact - reporter: ["text", "html", "cobertura"], + reporter: ["text"], all: true, // Files to include or exclude in coverage include: ["src/**/*.vue", "src/**/*.js"], - exclude: [], + // Exclude from coverage all files with keyword in path and name "Experimental" + exclude: ["*/experimental/*", "experimental"], // These thresholds raise ERROR if the coverage it's under the provided percentages. // As the coverage is low we set the limit low, as testing spec files increase, // these thresholds need to increase accordingly.