Bug 38201: Add component tests for useBaseResource

Signed-off-by: Michaela Sieber <michaela.sieber@kit.edu>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Matt Blenkinsop 2025-09-12 13:10:44 +01:00 committed by Lucas Gass
parent 37076455ff
commit d93e01850a
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
5 changed files with 692 additions and 22 deletions

View File

@ -0,0 +1,19 @@
<template></template>
<script>
import { useBaseResource } from "../../composables/base-resource.js";
export default {
props: {
resourceConfig: Object,
},
setup(props) {
const result = useBaseResource(props.resourceConfig);
return {
result,
};
},
name: "UseBaseResource",
};
</script>
<style></style>

View File

@ -9,6 +9,7 @@
"dependencies": {
"@cypress/vue": "^3.1.1",
"@cypress/webpack-dev-server": "^1.8.3",
"@cypress/webpack-preprocessor": "^6.0.4",
"@faker-js/faker": "^9.2.0",
"@fortawesome/fontawesome-svg-core": "^6.1.0",
"@fortawesome/free-solid-svg-icons": "^6.0.0",
@ -35,7 +36,7 @@
"minimatch": "^3.0.2",
"minimist": "^1.2.5",
"mysql": "^2.18.1",
"pinia": "^2.0.13",
"pinia": "^3.0.3",
"sass": "^1.58.1",
"style-loader": "^3.3.1",
"vue": "^3.5.4",
@ -69,6 +70,7 @@
"author": "",
"license": "GPL-3.0",
"devDependencies": {
"@pinia/testing": "^1.0.2",
"@rspack/cli": "^1.0.1",
"@rspack/core": "^1.0.1",
"@vue/compiler-sfc": "^3.2.31",

View File

@ -0,0 +1,488 @@
import UseBaseResource from "@koha-vue/components/Cypress/UseBaseResource.vue";
const globalConfig = {
global: {
stores: ["mainStore", "navigationStore"],
testingStore: {
initialState: {
authorisedValues: {
av_test: [
{
description: "Test value",
value: "test_value",
},
],
},
},
},
},
props: {
resourceConfig: cy.getBaseResourceConfig(),
},
};
describe("useBaseResource - toolbar methods", () => {
beforeEach(() => {
globalConfig.props.resourceConfig = cy.getBaseResourceConfig();
});
it("toolbarButtons - default buttons", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const toolbarButtons = baseResource.toolbarButtons.value;
const defaultButtonsShowComponent = toolbarButtons({}, "show", {});
expect(defaultButtonsShowComponent).to.have.length(2);
expect(defaultButtonsShowComponent[0].action).to.be.equal("edit");
expect(defaultButtonsShowComponent[1].action).to.be.equal("delete");
const defaultButtonsListComponent = toolbarButtons({}, "list", {});
expect(defaultButtonsListComponent).to.have.length(1);
expect(defaultButtonsListComponent[0].action).to.be.equal("add");
});
});
it("toolbarButtons - additional buttons", () => {
const additionalToolbarButtons = () => {
return {
list: [{ action: "testList", label: "Test" }],
show: [{ action: "testShow", label: "Test" }],
};
};
globalConfig.props.resourceConfig.additionalToolbarButtons =
additionalToolbarButtons;
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const toolbarButtons = baseResource.toolbarButtons.value;
const defaultButtonsShowComponent = toolbarButtons({}, "show", {});
expect(defaultButtonsShowComponent).to.have.length(3);
expect(defaultButtonsShowComponent[2].action).to.be.equal(
"testShow"
);
const defaultButtonsListComponent = toolbarButtons({}, "list", {});
expect(defaultButtonsListComponent).to.have.length(2);
expect(defaultButtonsListComponent[1].action).to.be.equal(
"testList"
);
});
});
it("toolbarButtons - additional buttons with the order set by the index property", () => {
const additionalToolbarButtons = () => {
return {
list: [{ action: "testList", label: "Test" }],
show: [{ action: "testShow", label: "Test", index: -1 }],
};
};
globalConfig.props.resourceConfig.additionalToolbarButtons =
additionalToolbarButtons;
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const toolbarButtons = baseResource.toolbarButtons.value;
const defaultButtonsShowComponent = toolbarButtons({}, "show", {});
expect(defaultButtonsShowComponent).to.have.length(3);
expect(defaultButtonsShowComponent[0].action).to.be.equal(
"testShow"
);
const defaultButtonsListComponent = toolbarButtons({}, "list", {});
expect(defaultButtonsListComponent).to.have.length(2);
expect(defaultButtonsListComponent[1].action).to.be.equal(
"testList"
);
});
});
it("goToResourceAdd", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const goToResourceAdd = baseResource.goToResourceAdd;
const router = baseResource.router;
cy.stub(router, "push");
goToResourceAdd();
expect(router.push).to.have.been.calledWith({ name: "testAdd" });
});
});
it("goToResourceEdit", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const goToResourceEdit = baseResource.goToResourceEdit;
const router = baseResource.router;
cy.stub(router, "push");
goToResourceEdit({ id: 1 });
expect(router.push).to.have.been.calledWith({
name: "testEdit",
params: {
id: 1,
},
});
});
});
it("goToResourceShow", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const goToResourceShow = baseResource.goToResourceShow;
const router = baseResource.router;
cy.stub(router, "push");
goToResourceShow({ id: 1 });
expect(router.push).to.have.been.calledWith({
name: "testShow",
params: {
id: 1,
},
});
});
});
it("goToResourceList", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const goToResourceList = baseResource.goToResourceList;
const router = baseResource.router;
cy.stub(router, "push");
goToResourceList({ id: 1 });
expect(router.push).to.have.been.calledWith({ name: "testList" });
});
});
// it("doResourceDelete", () => {
// cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
// cy.get_mainStore().then((mainStore) => {
// const baseResource = component.result;
// const doResourceDelete = baseResource.doResourceDelete;
// // const router = baseResource.router;
// // cy.stub(router, "push");
// cy.stub(mainStore, "setConfirmationDialog")
// // const confDialogSpy = cy.spy(mainStore, "setConfirmationDialog");
// doResourceDelete({id: 1, name: "Test"});
// expect(mainStore.setConfirmationDialog).to.have.been.called();
// })
// });
// });
});
describe("useBaseResource - datatables methods", () => {
beforeEach(() => {
globalConfig.props.resourceConfig = cy.getBaseResourceConfig();
});
it("getResourceTableUrl", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getResourceTableUrl = baseResource.getResourceTableUrl;
expect(getResourceTableUrl()).to.be.equal("testTableUrl");
});
});
it("getResourceShowURL", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getResourceShowURL = baseResource.getResourceShowURL;
const router = baseResource.router;
cy.stub(router, "resolve").callsFake(() => {
return {
href: "testShow",
};
});
getResourceShowURL(10);
expect(router.resolve).to.have.been.calledWith({
name: "testShow",
params: { id: 10 },
});
});
});
it("getFilterValues - no additionalFilters", () => {
const filters = {
filterValue: true,
anotherFilterValue: "filter",
andAnother: "another",
};
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFilterValues = baseResource.getFilterValues;
const filterResult = getFilterValues(filters);
expect(Object.keys(filterResult)).to.have.length(3);
expect(filterResult.filterValue).to.equal(true);
expect(filterResult.anotherFilterValue).to.equal("filter");
expect(filterResult.andAnother).to.equal("another");
});
});
it("getFilterValues - with additionalFilters", () => {
const filters = {
filterValue: true,
};
globalConfig.props.resourceConfig.table.additionalFilters = [
{ name: "additionalFilter", value: "test" },
{ name: "filterValue", value: false },
];
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFilterValues = baseResource.getFilterValues;
const filterResult = getFilterValues(filters);
expect(Object.keys(filterResult)).to.have.length(2);
expect(filterResult.filterValue).to.equal(true);
expect(filterResult.additionalFilter).to.equal("test");
});
});
});
describe("useBaseResource - resource display methods", () => {
beforeEach(() => {
globalConfig.props.resourceConfig = cy.getBaseResourceConfig();
});
it("getFieldGroupings - form with no field groups", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithNoGroups();
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFieldGroupings = baseResource.getFieldGroupings;
const fieldGroupings = getFieldGroupings("Form");
expect(fieldGroupings[0].name).to.equal(null);
expect(fieldGroupings[0].fields).to.have.length(3);
expect(fieldGroupings[0].fields[2].name).to.equal("formField");
expect(
fieldGroupings[0].fields[1].relationshipFields[0]
.relationshipName
).to.equal("relationships");
expect(fieldGroupings[0].hasDataToDisplay).to.equal(false);
});
});
it("getFieldGroupings - show with no field groups", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithNoGroups();
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFieldGroupings = baseResource.getFieldGroupings;
const fieldGroupings = getFieldGroupings("Show", {
name: "A name value",
});
expect(fieldGroupings[0].name).to.equal(null);
expect(fieldGroupings[0].fields).to.have.length(4);
expect(fieldGroupings[0].fields[2].name).to.equal("showField");
expect(fieldGroupings[0].hasDataToDisplay).to.equal(true);
});
});
it("getFieldGroupings - show with no field group and additional fields", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithNoGroups();
globalConfig.props.resourceConfig.extendedAttributesResourceType =
"test";
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFieldGroupings = baseResource.getFieldGroupings;
const fieldGroupings = getFieldGroupings("Show", {
name: "A name value",
});
expect(fieldGroupings[0].fields[4].name).to.equal(
"additional_fields"
);
});
});
it("getFieldGroupings - form with field groups", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithGroups();
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFieldGroupings = baseResource.getFieldGroupings;
const fieldGroupings = getFieldGroupings("Form");
expect(fieldGroupings).to.have.length(2);
const firstGroup = fieldGroupings[0];
expect(firstGroup.name).to.equal("Group 2");
expect(firstGroup.fields).to.have.length(2);
expect(firstGroup.fields[0].name).to.equal("name");
expect(firstGroup.fields[1].name).to.equal("formField");
const secondGroup = fieldGroupings[1];
expect(secondGroup.name).to.equal("Group 3");
expect(secondGroup.fields).to.have.length(1);
expect(secondGroup.fields[0].name).to.equal("relationships");
});
});
it("getFieldGroupings - show with field groups and limited data in the resource", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithGroups();
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFieldGroupings = baseResource.getFieldGroupings;
const fieldGroupings = getFieldGroupings("Show", {
name: "A name value",
});
expect(fieldGroupings).to.have.length(1);
const firstGroup = fieldGroupings[0];
expect(firstGroup.name).to.equal("Group 2");
expect(firstGroup.fields).to.have.length(1);
expect(firstGroup.fields[0].name).to.equal("name");
});
});
it("getFieldGroupings - show with field groups and fully populated data in the resource", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithGroups();
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getFieldGroupings = baseResource.getFieldGroupings;
const fieldGroupings = getFieldGroupings("Show", {
name: "A name value",
showField: "test",
displayName: "test",
});
expect(fieldGroupings).to.have.length(3);
const firstGroup = fieldGroupings[0];
expect(firstGroup.name).to.equal("Group 2");
expect(firstGroup.fields).to.have.length(1);
expect(firstGroup.fields[0].name).to.equal("name");
const secondGroup = fieldGroupings[1];
expect(secondGroup.name).to.equal("Group 3");
expect(secondGroup.fields).to.have.length(2);
expect(secondGroup.fields[0].name).to.equal("relationships");
expect(secondGroup.fields[1].name).to.equal("displayName");
const thirdGroup = fieldGroupings[2];
expect(thirdGroup.name).to.equal("Group 1");
expect(thirdGroup.fields).to.have.length(1);
expect(thirdGroup.fields[0].name).to.equal("showField");
});
});
it("getResource", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const getResource = baseResource.getResource;
const apiClient = globalConfig.props.resourceConfig.apiClient;
const apiSpy = cy.spy(apiClient, "get");
const componentData = {
instancedResource: {
afterResourceFetch: () => {},
},
resource: {
value: {},
},
initialized: {
value: false,
},
};
const resourceFetchSpy = cy.spy(
componentData.instancedResource,
"afterResourceFetch"
);
getResource(1, componentData, "Form")
.then(() => {
expect(apiSpy).to.have.been.called;
})
.then(() => {
expect(resourceFetchSpy).to.be.calledWith(
componentData,
{ id: 1, name: "Object 1" },
"Form"
);
});
});
});
it("resourceNamePlural", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const resourceNamePlural = baseResource.resourceNamePlural;
expect(resourceNamePlural.value).to.equal("testObjects");
});
});
});
describe("useBaseResource - resource creation methods", () => {
beforeEach(() => {
globalConfig.props.resourceConfig = cy.getBaseResourceConfig();
});
it("additionalFieldsChanged", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const additionalFieldsChanged =
baseResource.additionalFieldsChanged;
const resource = {
extended_attributes: null,
};
const additionalFieldValues = "A new value";
additionalFieldsChanged(additionalFieldValues, resource);
expect(resource.extended_attributes).to.equal(
additionalFieldValues
);
});
});
it("newResource", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithNoGroups();
globalConfig.props.resourceConfig.extendedAttributesResourceType =
"test";
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const newResource = baseResource.newResource;
expect(newResource.value).to.deep.equal({
id: null,
name: "",
relationships: [],
showField: "",
formField: "",
displayName: "",
select: "",
textarea: "",
boolean: false,
checkbox: false,
dummyType: null,
defaultValue: "This is a default value",
extended_attributes: [],
});
});
});
});
describe("useBaseResource - resource utility methods", () => {
beforeEach(() => {
globalConfig.props.resourceConfig = cy.getBaseResourceConfig();
});
it("hasAdditionalFields", () => {
globalConfig.props.resourceConfig.extendedAttributesResourceType =
"test";
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const hasAdditionalFields = baseResource.hasAdditionalFields;
expect(hasAdditionalFields.value).to.equal(true);
});
});
it("refreshTemplateState", () => {
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const baseResource = component.result;
const refreshTemplateState = baseResource.refreshTemplateState;
const refreshTemplate = baseResource.refreshTemplate;
expect(refreshTemplate.value).to.equal(0);
refreshTemplateState();
expect(refreshTemplate.value).to.equal(1);
refreshTemplateState();
expect(refreshTemplate.value).to.equal(2);
});
});
it("populateAttributesWithAuthorisedValues", () => {
globalConfig.props.resourceConfig.resourceAttrs =
cy.getResourceAttrsWithNoGroups();
cy.mount(UseBaseResource, globalConfig).then(({ component }) => {
const avCatResourceAttr =
globalConfig.props.resourceConfig.resourceAttrs.find(
ra => !!ra.avCat
);
expect(avCatResourceAttr.options).to.exist;
expect(Object.keys(avCatResourceAttr.options)).to.have.length(1);
expect(avCatResourceAttr.options[0].description).to.equal(
"Test value"
);
expect(avCatResourceAttr.options[0].value).to.equal("test_value");
const avRelationshipField =
globalConfig.props.resourceConfig.resourceAttrs
.find(ra => ra.name === "relationships")
.relationshipFields.find(rf => !!rf.avCat);
expect(avRelationshipField.options).to.exist;
expect(Object.keys(avRelationshipField.options)).to.have.length(1);
expect(avRelationshipField.options[0].description).to.equal(
"Test value"
);
expect(avRelationshipField.options[0].value).to.equal("test_value");
});
});
});

View File

@ -6,8 +6,10 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<script>const flatpickr_defaults = {}</script>
<body>
<script>
const flatpickr_defaults = {}
</script>
<div data-cy-root></div>
</body>
</html>

View File

@ -23,6 +23,9 @@ import { mount } from "cypress/vue";
import i18n from "@koha-vue/i18n";
import { createWebHistory, createRouter } from "vue-router";
import vSelect from "vue-select";
import { createPinia, defineStore } from "pinia";
import { useMainStore } from "@koha-vue/stores/main";
import { useNavigationStore } from "@koha-vue/stores/navigation";
// Augment the Cypress namespace to include type definitions for
// your custom command.
@ -40,29 +43,185 @@ Cypress.Commands.add("mount", (component, options = {}) => {
options.global = options.global || {};
options.global.plugins = options.global.plugins || [];
if (!options.router) {
options.router = createRouter({
routes: [
{
path: "/:pathMatch(.*)*",
name: "home",
children: [],
},
],
history: createWebHistory(),
});
options.router.beforeEach((to, from, next) => {
if (!to.matched.length) next();
});
options.global.plugins.push({
install(app) {
app.use(options.router).component("v-select", vSelect);
// Create a default testing store for use where stores may be dynamic
const testingStore = defineStore("testing", {
state: () => options?.global?.testingStore?.initialState || {},
});
const pinia = createPinia();
const storesMatrix = {
mainStore: useMainStore(pinia),
navigationStore: useNavigationStore(pinia),
};
options.router = createRouter({
routes: [
{
path: "/:pathMatch(.*)*",
name: "home",
children: [],
},
});
}
options.global.plugins.push(i18n);
],
history: createWebHistory(),
});
options.router.beforeEach((to, from, next) => {
if (!to.matched.length) next();
});
options.global.plugins.push({
install(app) {
if (!app.config.globalProperties.$router) {
app.use(options.router);
}
if (!app._context.components["v-select"]) {
app.component("v-select", vSelect);
}
if (!app.config.globalProperties.$pinia) {
app.use(pinia);
if (options.global.stores?.length > 0) {
options.global.stores.forEach(store => {
Cypress.Commands.add(
`get_${store}`,
() => storesMatrix[store]
);
app.provide(store, storesMatrix[store]);
});
}
app.provide("testingStore", testingStore());
}
if (!app.config.globalProperties.$__) {
app.use(i18n);
}
},
});
return mount(component, options);
});
// Example use:
// cy.mount(MyComponent)
cy.getDummyAPIClient = () => {
return {
get: async id => {
return await new Promise(resolve =>
resolve({ id: id, name: "Object 1" })
);
},
getAll: async (query, params) =>
await new Promise(resolve =>
resolve([{ id: 1, name: "Object 1" }])
),
create: async object => {
return await new Promise(resolve => resolve({ id: 1, ...object }));
},
update: async (object, id) => {
return await new Promise(resolve => resolve({ id: id, ...object }));
},
delete: async id => {
return await new Promise(resolve => resolve(null));
},
count: async query => {
return await new Promise(resolve => resolve(1));
},
};
};
cy.getBaseResourceConfig = () => {
return {
moduleStore: "testingStore",
i18n: {
deleteConfirmationMessage: "Delete this object?",
},
apiClient: cy.getDummyAPIClient(),
components: {
show: "testShow",
add: "testAdd",
list: "testList",
edit: "testEdit",
},
idAttr: "id",
nameAttr: "name",
resourceName: "testObject",
table: {
resourceTableUrl: "testTableUrl",
},
};
};
cy.getResourceAttrsWithNoGroups = () => {
return [
{
name: "id",
type: "text",
hideIn: ["Form", "Show"],
},
{
name: "name",
type: "text",
},
{
name: "relationships",
type: "relationshipWidget",
relationshipFields: [
{ name: "relField", type: "text" },
{ name: "avField", type: "select", avCat: "av_test" },
],
},
{
name: "showField",
type: "text",
hideIn: ["Form"],
},
{
name: "formField",
type: "text",
hideIn: ["Show"],
},
{
name: "displayName",
type: "text",
hideIn: ["Form"],
},
{
name: "select",
type: "select",
avCat: "av_test",
hideIn: ["Form", "Show"],
},
{
name: "textarea",
type: "textarea",
hideIn: ["Form", "Show"],
},
{
name: "boolean",
type: "boolean",
hideIn: ["Form", "Show"],
},
{
name: "checkbox",
type: "checkbox",
hideIn: ["Form", "Show"],
},
{
name: "dummyType",
type: "dummyType",
hideIn: ["Form", "Show"],
},
{
name: "defaultValue",
type: "text",
defaultValue: "This is a default value",
hideIn: ["Form", "Show"],
},
];
};
cy.getResourceAttrsWithGroups = () => {
const resourceAttrs = cy.getResourceAttrsWithNoGroups();
const groups = ["Group 1", "Group 2", "Group 3"];
return resourceAttrs.map((attr, i) => {
return {
...attr,
group: groups[i % groups.length],
};
});
};