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 @@ + + + + + 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 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/t/cypress/support/component.ts b/t/cypress/support/component.ts new file mode 100644 index 0000000000..10b46ffdf7 --- /dev/null +++ b/t/cypress/support/component.ts @@ -0,0 +1,39 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +// import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from "cypress/vue"; + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +// declare global { +// namespace Cypress { +// interface Chainable { +// mount: typeof mount +// } +// } +// }; + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount(MyComponent)