diff --git a/cypress.config.ts b/cypress.config.ts
index b7bba5408c..42aa4d7a81 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -1,4 +1,5 @@
import { defineConfig } from "cypress";
+import { devServer } from "cypress-rspack-dev-server";
export default defineConfig({
fixturesFolder: "t/cypress/fixtures",
@@ -21,4 +22,16 @@ export default defineConfig({
apiPassword: "koha",
},
},
+
+ component: {
+ devServer(devServerConfig) {
+ return devServer({
+ ...devServerConfig,
+ rspackConfig: require("./rspack.config.js")[0],
+ });
+ },
+ indexHtmlFile: "t/cypress/support/component-index.html",
+ specPattern: "t/cypress/component/**/*.*",
+ supportFile: "t/cypress/support/component.ts",
+ },
});
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Cypress/ShowElementComponentType.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Cypress/ShowElementComponentType.vue
new file mode 100644
index 0000000000..71c1c66d38
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Cypress/ShowElementComponentType.vue
@@ -0,0 +1,15 @@
+
+ {{ testString }}
+ {{ resourceProp }}
+
+
+
+
+
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js b/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js
index b196078d0f..163f474bf5 100644
--- a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js
@@ -1,5 +1,11 @@
const methods = ["__", "__x", "__n", "__nx", "__p", "__px", "__np", "__npx"];
+if (window.Cypress.testingType === "component") {
+ methods.forEach(method => {
+ window[method] = string => string;
+ });
+}
+
const translators = Object.fromEntries(
methods.map(method => [method, (...args) => window[method](...args)])
);
diff --git a/package.json b/package.json
index 66748e8699..b229568646 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@koha-community/prettier-plugin-template-toolkit": "^2.0.2",
"@popperjs/core": "^2.11.6",
"@redocly/cli": "^1.25.11",
+ "@rspack/binding-darwin-arm64": "^1.4.10",
"bootstrap": "^5.3.3",
"css-loader": "^6.6.0",
"cypress": "^12.17.2",
@@ -74,6 +75,7 @@
"@vue/compiler-sfc": "^3.2.31",
"browserify": "^17.0.0",
"clean-webpack-plugin": "^4.0.0",
+ "cypress-rspack-dev-server": "^1.1.0",
"eslint": "^9.12.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
diff --git a/t/cypress/component/ShowElement_spec.ts b/t/cypress/component/ShowElement_spec.ts
new file mode 100644
index 0000000000..bcb5d9ebdd
--- /dev/null
+++ b/t/cypress/component/ShowElement_spec.ts
@@ -0,0 +1,246 @@
+import ShowElement from "../../../koha-tmpl/intranet-tmpl/prog/js/vue/components/ShowElement.vue";
+
+const resource = {
+ id: 1,
+ name: "test",
+ showName: "For show",
+ nestedObject: {
+ nestedProperty: "nested",
+ },
+ radioCheck: true,
+ tableTest: [
+ { columnValueOne: "Col 1", columnValueTwo: "Value1" },
+ { columnValueOne: "Col 2", columnValueTwo: "Value2" },
+ { columnValueOne: "Col 3", columnValueTwo: "Value3" },
+ ],
+};
+const instancedResource = {
+ get_lib_from_av: (avCat, value) => {
+ return "avValue";
+ },
+};
+
+describe("ShowElement", () => {
+ it("should render a string based on the type and name property of the attribute", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ },
+ },
+ });
+ cy.get("label").contains("Name");
+ cy.get("span").contains(resource.name);
+ });
+ it("should hide that property if the hidden attribute is set and falsy", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ hidden: res => false,
+ },
+ },
+ });
+ cy.get("label").should("not.exist");
+ });
+ it("should wrap that property in a link if the link attribute is set", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ link: {
+ href: "blah",
+ },
+ },
+ },
+ });
+ cy.get("label").contains("Name");
+ cy.get("a").should("have.attr", "href", "blah");
+ cy.get("span").contains(resource.name);
+ });
+ it("should detect a showElement property and use the data from that", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ showElement: {
+ type: "text",
+ name: "showName",
+ },
+ },
+ },
+ });
+ cy.get("label").contains("Name");
+ cy.get("span").contains(resource.showName);
+ });
+ it("should retrieve a value from a nested object if nesting is detected", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ showElement: {
+ type: "text",
+ name: "showName",
+ value: "nestedObject.nestedProperty",
+ },
+ },
+ },
+ });
+ cy.get("label").contains("Name");
+ cy.get("span").contains(resource.nestedObject.nestedProperty);
+ });
+ it("should format the value if the formatValue attribute is set", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ showElement: {
+ type: "text",
+ name: "showName",
+ format: (attr, resource) => "This has been formatted",
+ },
+ },
+ },
+ });
+ cy.get("label").contains("Name");
+ cy.get("span").contains("This has been formatted");
+ });
+ it("should correctly assign the value to a radio type attr", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ showElement: {
+ type: "radio",
+ name: "radioCheck",
+ value: "radioCheck",
+ options: [
+ { value: true, description: "Correct" },
+ { value: false, description: "Incorrect" },
+ ],
+ },
+ },
+ },
+ });
+ cy.get("span").contains("Correct");
+ });
+ it("should correctly assign the value to a select/av type attr", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ avCat: "testAv",
+ showElement: {
+ type: "select",
+ },
+ },
+ },
+ });
+ cy.get("span").contains("avValue");
+ });
+ it("should correctly assign the value to boolean based attrs", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ showElement: {
+ type: "boolean",
+ },
+ },
+ },
+ });
+ cy.get("span").contains("Yes");
+ });
+ it("should correctly render a table when required", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ label: "Name",
+ showElement: {
+ type: "table",
+ columnData: "tableTest",
+ hidden: () => true,
+ columns: [
+ { name: "Column 1", value: "columnValueOne" },
+ { name: "Column 2", value: "columnValueTwo" },
+ ],
+ },
+ },
+ },
+ });
+ cy.get("table thead th:first").contains("Column 1");
+ cy.get("table thead th").eq(1).contains("Column 2");
+ cy.get("table tbody tr:first td:first").contains("Col 1");
+ cy.get("table tbody tr:first td").eq(1).contains("Value1");
+ cy.get("table tbody tr:nth-child(2) td:first").contains("Col 2");
+ cy.get("table tbody tr:nth-child(2) td").eq(1).contains("Value2");
+ });
+ it("should render a component if passed a path and assign props", () => {
+ cy.mount(ShowElement, {
+ props: {
+ resource,
+ instancedResource,
+ attr: {
+ type: "text",
+ name: "name",
+ showElement: {
+ type: "component",
+ hidden: () => true,
+ componentPath: "./Cypress/ShowElementComponentType.vue",
+ componentProps: {
+ testString: {
+ type: "string",
+ value: "A test string",
+ },
+ resourceProp: {
+ resourceProperty: "showName",
+ },
+ },
+ },
+ },
+ },
+ });
+ cy.get("h1").contains("A test string");
+ cy.get("h2").contains(resource.showName);
+ });
+});
diff --git a/t/cypress/support/component-index.html b/t/cypress/support/component-index.html
new file mode 100644
index 0000000000..ac6e79fd83
--- /dev/null
+++ b/t/cypress/support/component-index.html
@@ -0,0 +1,12 @@
+
+
+