ui_migration feature(vitest) Add setup for skipped experimental components
This commit is contained in:
parent
3cc5875fdf
commit
8f20e1211e
6 changed files with 158 additions and 76 deletions
|
|
@ -161,18 +161,6 @@ javascript-lint-and-test:
|
||||||
- npm run lint:style
|
- npm run lint:style
|
||||||
# The test runs only if a test spec file has changed
|
# The test runs only if a test spec file has changed
|
||||||
- npm run test:unit
|
- 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 JS app
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
8
webapp/.gitignore
vendored
8
webapp/.gitignore
vendored
|
|
@ -27,4 +27,10 @@ yarn-error.log*
|
||||||
# NativeScript application
|
# NativeScript application
|
||||||
hooks
|
hooks
|
||||||
platforms
|
platforms
|
||||||
./webpack.config.js
|
./webpack.config.js
|
||||||
|
|
||||||
|
# Vue test skipped files
|
||||||
|
**/*experimental*.vue
|
||||||
|
|
||||||
|
# Experimental Vue components
|
||||||
|
src/components/experimental/
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"lint:fix": "eslint . --fix --ignore-pattern .gitignore",
|
"lint:fix": "eslint . --fix --ignore-pattern .gitignore",
|
||||||
"lint:style": "stylelint \"src/**/*.{vue,less,css}\" --max-warnings=0",
|
"lint:style": "stylelint \"src/**/*.{vue,less,css}\" --max-warnings=0",
|
||||||
"lint:style:fix": "stylelint \"src/**/*.{vue,less,css}\" --fix",
|
"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": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,98 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
|
||||||
import { shallowMount, flushPromises } from "@vue/test-utils";
|
import { shallowMount, flushPromises } from "@vue/test-utils";
|
||||||
import { createTestingPinia } from "@pinia/testing";
|
import { createTestingPinia } from "@pinia/testing";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
// Eagerly import ALL .vue files at the components directory
|
// Eagerly import ALL .vue files at the components directory
|
||||||
const componentModules = import.meta.glob("../../components/**/*.vue", { eager: true });
|
// Except the ones inside experimental
|
||||||
|
const componentModules = Object.fromEntries(
|
||||||
// Skip list, for components that have their own test spec.js file
|
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"];
|
const skipList = ["loggingContent.vue"];
|
||||||
|
|
||||||
// Map filenames to the specific props or mocks required
|
// Map filenames to the specific props or mocks required
|
||||||
// Not all components ask for props
|
// Not all components ask for props
|
||||||
const componentOverrides = {
|
const componentOverrides = {
|
||||||
"paginateLinks.vue": {
|
"serverSpecifiedPropertyControl.vue": {
|
||||||
props: {
|
props: {
|
||||||
totalPages: 5,
|
propertyData: {},
|
||||||
currentPage: 1,
|
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": {
|
"simpleAccordion.vue": {
|
||||||
props: { title: "Test Accordion Title" },
|
props: { title: "Test Accordion Title" },
|
||||||
},
|
},
|
||||||
"tabContent.vue": {
|
"tabContent.vue": {
|
||||||
props: {
|
props: { tabID: "tab-1", currentTab: "tab-1" },
|
||||||
tabID: "tab-1",
|
|
||||||
currentTab: "tab-1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"tabIcon.vue": {
|
"tabIcon.vue": {
|
||||||
props: {
|
props: { tabID: "tab-1", currentTab: "tab-1" },
|
||||||
tabID: "tab-1",
|
|
||||||
currentTab: "tab-1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"actionButton.vue": {
|
"actionButton.vue": {
|
||||||
props: {
|
props: { action: "test-action", thing: "test-thing" },
|
||||||
action: "test-action",
|
|
||||||
thing: "test-thing",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"actionLogDisplay.vue": {
|
"actionLogDisplay.vue": {
|
||||||
props: {
|
props: { taskStatus: "", log: [] },
|
||||||
taskStatus: "",
|
|
||||||
log: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"endActionButton.vue": {
|
"endActionButton.vue": {
|
||||||
props: { url: "http://microscope.local/api/v3" },
|
props: { url: "http://microscope.local/api/v3" },
|
||||||
|
|
@ -67,20 +117,23 @@ const componentOverrides = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"inputFromSchema.vue": {
|
"inputFromSchema.vue": {
|
||||||
props: {
|
props: { dataSchema: {} },
|
||||||
dataSchema: {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"propertyControl.vue": {
|
"propertyControl.vue": {
|
||||||
props: {
|
props: {
|
||||||
propertyName: "",
|
propertyName: "",
|
||||||
thingName: "",
|
thingName: "test_thing",
|
||||||
dataSchema: {},
|
dataSchema: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
mocks: {
|
mocks: {
|
||||||
wotStore: {
|
wotStore: {
|
||||||
thingDescriptions: {
|
thingDescriptions: {
|
||||||
actions: {},
|
test_thing: {
|
||||||
|
actions: {},
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -95,25 +148,14 @@ const componentOverrides = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"serverSpecifiedInterface.vue": {
|
"serverSpecifiedInterface.vue": {
|
||||||
props: {
|
props: { elements: [] },
|
||||||
elements: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"serverSpecifiedPropertyControl.vue": {
|
|
||||||
props: {
|
|
||||||
propertyData: {},
|
|
||||||
propertyName: "",
|
|
||||||
thingName: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"calibrationWizardTask.vue": {
|
"calibrationWizardTask.vue": {
|
||||||
props: {
|
props: { steps: [] },
|
||||||
steps: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"singleStepTask.vue": {
|
"singleStepTask.vue": {
|
||||||
props: {
|
props: {
|
||||||
stepComponent: {},
|
stepComponent: markRaw({ template: '<div class="dummy"></div>' }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"actionTab.vue": {
|
"actionTab.vue": {
|
||||||
|
|
@ -125,9 +167,7 @@ const componentOverrides = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"galleryCard.vue": {
|
"galleryCard.vue": {
|
||||||
props: {
|
props: { itemData: {} },
|
||||||
itemData: {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"openSeadragonViewer.vue": {
|
"openSeadragonViewer.vue": {
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -138,20 +178,24 @@ const componentOverrides = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"cameraCalibrationSettings.vue": {
|
"cameraCalibrationSettings.vue": {
|
||||||
props: {
|
props: { cameraUri: "" },
|
||||||
cameraUri: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"galleryContent.vue": {
|
"galleryContent.vue": {
|
||||||
mocks: {
|
global: {
|
||||||
readThingProperty: vi.fn(() => []),
|
mocks: {
|
||||||
|
readThingProperty: vi.fn(() => []),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
"CSMCalibrationSettings.vue": {
|
"CSMCalibrationSettings.vue": {
|
||||||
mocks: {
|
global: {
|
||||||
thingDescriptions: {
|
mocks: {
|
||||||
csm: {
|
thingDescriptions: {
|
||||||
actions: {},
|
csm: {
|
||||||
|
actions: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -194,15 +238,30 @@ describe("Automated Component Smoke Tests", () => {
|
||||||
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);
|
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)
|
// Dynamically assign the test runner (it.skip if in the list, otherwise standard it)
|
||||||
const testRunner = isSkipped ? it.skip : it;
|
const testRunner = isSkipped ? it.skip : it;
|
||||||
|
|
||||||
// Run the test
|
// 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
|
// Look up the specific config for this file, default to an empty object
|
||||||
const override = componentOverrides[fileName] || {};
|
const override = componentOverrides[fileName] || {};
|
||||||
|
|
||||||
|
|
@ -222,7 +281,7 @@ describe("Automated Component Smoke Tests", () => {
|
||||||
],
|
],
|
||||||
stubs: {},
|
stubs: {},
|
||||||
mocks: {
|
mocks: {
|
||||||
// These values come from MIXINS
|
// 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(() => ({})),
|
||||||
|
|
@ -230,17 +289,17 @@ describe("Automated Component Smoke Tests", () => {
|
||||||
actions: {},
|
actions: {},
|
||||||
properties: {},
|
properties: {},
|
||||||
})),
|
})),
|
||||||
thingDescriptions: vi.fn(() => ({})),
|
|
||||||
getOngoingAction: vi.fn(() => Promise.resolve()),
|
getOngoingAction: vi.fn(() => Promise.resolve()),
|
||||||
getThingEndpoint: vi.fn(() => Promise.resolve([])),
|
getThingEndpoint: vi.fn(() => Promise.resolve([])),
|
||||||
modalError: "",
|
modalError: "",
|
||||||
|
...(override.global?.mocks || {}),
|
||||||
...(override.mocks || {}),
|
...(override.mocks || {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
vi.advanceTimersByTime(1000);
|
vi.advanceTimersByTime(200);
|
||||||
expect(wrapper.exists()).toBe(true);
|
expect(wrapper.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Create an isolated, in-memory mock for localStorage
|
||||||
|
|
||||||
const localStorageMock = (() => {
|
const localStorageMock = (() => {
|
||||||
let store = {};
|
let store = {};
|
||||||
return {
|
return {
|
||||||
|
|
@ -23,4 +26,29 @@ vi.stubGlobal("localStorage", localStorageMock);
|
||||||
// Clean up
|
// Clean up
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
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}`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,12 @@ export default defineConfig({
|
||||||
clean: true,
|
clean: true,
|
||||||
cleanOnStart: true,
|
cleanOnStart: true,
|
||||||
// text gives output on STOUT and html creates an artifact
|
// text gives output on STOUT and html creates an artifact
|
||||||
reporter: ["text", "html", "cobertura"],
|
reporter: ["text"],
|
||||||
all: true,
|
all: true,
|
||||||
// Files to include or exclude in coverage
|
// Files to include or exclude in coverage
|
||||||
include: ["src/**/*.vue", "src/**/*.js"],
|
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.
|
// 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,
|
// As the coverage is low we set the limit low, as testing spec files increase,
|
||||||
// these thresholds need to increase accordingly.
|
// these thresholds need to increase accordingly.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue