diff --git a/package-lock.json b/package-lock.json index ea97f8f1..1c3f9e55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@open-wc/eslint-config": "^12.0.3", "@open-wc/testing": "^4.0.0", "@turbo/gen": "^1.9.8", + "@types/mocha": "^10.0.9", "@web/dev-server": "^0.4.1", "@web/dev-server-esbuild": "^1.0.2", "@web/test-runner-playwright": "^0.11.0", @@ -6224,6 +6225,13 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz", + "integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -26662,7 +26670,7 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@elixir-cloud/design": "^0.1.1", + "@elixir-cloud/design": "^1.0.0", "lit": "^3.2.0" }, "devDependencies": { @@ -27075,7 +27083,7 @@ }, "packages/ecc-client-ga4gh-tes": { "name": "@elixir-cloud/tes", - "version": "0.1.1", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { "@elixir-cloud/design": "*", @@ -28005,7 +28013,7 @@ }, "packages/ecc-client-ga4gh-wes": { "name": "@elixir-cloud/wes", - "version": "0.1.1", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { "@elixir-cloud/design": "*", @@ -28882,7 +28890,7 @@ }, "packages/ecc-utils-design": { "name": "@elixir-cloud/design", - "version": "0.1.1", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { "@anurag_gupta/tus-js-client": "^4.2.10", diff --git a/package.json b/package.json index 2594c06f..6fe05592 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@open-wc/eslint-config": "^12.0.3", "@open-wc/testing": "^4.0.0", "@turbo/gen": "^1.9.8", + "@types/mocha": "^10.0.9", "@web/dev-server": "^0.4.1", "@web/dev-server-esbuild": "^1.0.2", "@web/test-runner-playwright": "^0.11.0", diff --git a/packages/ecc-utils-design/src/components/form/form.ts b/packages/ecc-utils-design/src/components/form/form.ts index 15c3dfe7..922443b9 100644 --- a/packages/ecc-utils-design/src/components/form/form.ts +++ b/packages/ecc-utils-design/src/components/form/form.ts @@ -9,7 +9,7 @@ import "@shoelace-style/shoelace/dist/components/details/details.js"; import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; import "@shoelace-style/shoelace/dist/components/select/select.js"; import "@shoelace-style/shoelace/dist/components/option/option.js"; -import _ from "lodash-es"; +import * as _ from "lodash-es"; import { hostStyles } from "../../styles/host.styles.js"; import formStyles from "./form.styles.js"; import { primitiveStylesheet } from "../../styles/primitive.styles.js"; @@ -120,20 +120,20 @@ export default class EccUtilsDesignForm extends LitElement { } return html` -
+
${field.fieldOptions?.tooltip && field.fieldOptions.tooltip !== "" ? html` - ` : html` - `} @@ -141,7 +141,7 @@ export default class EccUtilsDesignForm extends LitElement { size="small" class="switch" data-label=${field.label} - data-testid="form-switch" + data-testid="switch" label=${field.label} ?required=${field.fieldOptions?.required} ?checked=${_.get(this.form, path)} @@ -162,12 +162,12 @@ export default class EccUtilsDesignForm extends LitElement { private handleTusFileUpload = async ( e: Event, field: Field - ): Promise => { + ): Promise | null> => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) { console.error("No file selected for upload."); - return; + return null; } try { @@ -192,11 +192,20 @@ export default class EccUtilsDesignForm extends LitElement { this.requestUpdate(); }, onSuccess: () => { + const data: any = { + url: upload.url, + file, + name: "", + }; + if ("name" in upload.file) { console.log("Download %s from %s", upload.file.name, upload.url); + data.name = upload.file.name; } else { console.log("Download file from %s", upload.url); } + + return data; }, }); @@ -209,6 +218,8 @@ export default class EccUtilsDesignForm extends LitElement { } catch (error) { console.error("An error occurred while initializing the upload:", error); } + + return null; }; renderInputTemplate(field: Field, path: string): TemplateResult { @@ -221,61 +232,82 @@ export default class EccUtilsDesignForm extends LitElement { if (field.type === "file") { return html` -
+
${field.fieldOptions?.tooltip && field.fieldOptions.tooltip !== "" ? html` - ` : html` -
`; } @@ -285,26 +317,27 @@ export default class EccUtilsDesignForm extends LitElement { if (field.fieldOptions?.default && !this.hasUpdated) { _.set(this.form, path, field.fieldOptions.default); } else if (field.fieldOptions?.returnIfEmpty) { - _.set(this.form, path, ""); + _.set(this.form, path, null); } } if (field.type === "select") { return html` -
+
${field.fieldOptions?.tooltip && field.fieldOptions.tooltip !== "" ? html` - ` : html` - ` - : html` `} + : html` `} `; } private renderArrayTemplate(field: Field, path: string): TemplateResult { + if (!field.children?.length) return html``; + const { arrayOptions } = field; if (!_.get(this.form, path)) { @@ -392,28 +435,33 @@ export default class EccUtilsDesignForm extends LitElement { }; return html` -
+
${field.fieldOptions?.tooltip && field.fieldOptions.tooltip !== "" ? html` - ` : html` -
${_.get(this.form, path)?.map( (_item: any, index: number) => html` -
+
{ if (resolveDeleteButtonIsActive()) { @@ -486,21 +539,22 @@ export default class EccUtilsDesignForm extends LitElement { } private renderGroupTemplate(field: Field, path: string): TemplateResult { - if (!field.children) return html``; + if (!field.children?.length) return html``; const renderChildren = () => html` -
+
${field.children?.map((child) => this.renderTemplate(child, `${path}`) )}
`; - return html`
+ return html`
${field.groupOptions?.collapsible ? html` ` : html` -
+
${field.fieldOptions?.tooltip && field.fieldOptions.tooltip !== "" ? html` - ` : html` -
@@ -539,19 +598,33 @@ export default class EccUtilsDesignForm extends LitElement { if (field.type === "array") { return this.renderArrayTemplate(field, newPath); } - - if (field.fieldOptions?.required && !_.get(this.form, newPath)) { - this.requiredButEmpty.push(field.key); - } if (field.type === "switch") { return this.renderSwitchTemplate(field, newPath); } + + if (field.fieldOptions?.required) { + if ( + !_.get(this.form, newPath) && + !this.requiredButEmpty.includes(field.key) + ) { + // add to requiredButEmpty + if (this.hasUpdated || !field.fieldOptions.default) { + this.requiredButEmpty.push(field.key); + } + } else if (_.get(this.form, newPath)) { + // remove from requiredButEmpty + this.requiredButEmpty = this.requiredButEmpty.filter( + (key) => key !== field.key + ); + } + } + return this.renderInputTemplate(field, newPath); } private renderErrorTemplate(): TemplateResult { if (this.formState !== "error") return html``; - return html` + return html` + { this.fields = fields; - this.component = await fixture( + const el = await fixture( html`` ); + this.setEl(el); + // need to make the component available as more specific EccUtilsDesign type so we can access its methods for stubbing and spying, while maintaining type safety - this.form = this.component as EccUtilsDesignForm; + this.form = el as EccUtilsDesignForm; }; - formElement = () => this.element("form", "root"); + formElement = () => this.getElement("", "form").el; - submitButton = () => this.buttonElement("form-submit", "root"); + submitButton = () => this.getButtonElement("", "submit-button"); - errorTemplate = () => this.element("form-error", "root"); + errorTemplate = () => this.getElement("", "error-alert").el; - successTemplate = () => this.element("form-success", "root"); + successTemplate = () => this.getElement("", "success-alert").el; // actions - public clickSubmitButton() { - this.clickButton(this.submitButton()); + public async clickSubmitButton() { + this.submitButton().click(); + await this.component?.updateComplete; } } export default async function createNewFormComponent(fields: Field[]) { - const formComponent = new FormComponent(new EccUtilsDesignForm()); + const formComponent = new FormComponent( + new EccUtilsDesignForm(), // this is just a placeholder + "litElement" + ); // any other call of initializeForm will reinitialize with new fields; await formComponent.initializeForm(fields); return formComponent; } export type FormComponentType = FormComponent; -export const testIds = { - formInput: "form-input", - formInputFile: "form-input-file", - formInputParent: "form-input-file-parent", - formSwitch: "form-switch", - formSwitchParent: "form-switch-parent", - formArrayAddButton: "form-array-add", - formArrayDeleteButton: "form-array-delete", - formArrayItem: "form-array-item", - formArray: "form-array", - formGroup: "form-group", - formGroupItem: "form-group-item", - formCollapsibleGroup: "form-group-collapsible", - formNonCollapsibleGroup: "form-group-non-collapsible", - formTooltip: "form-tooltip", -}; diff --git a/packages/ecc-utils-design/src/components/form/tests/form.test.ts b/packages/ecc-utils-design/src/components/form/tests/form.test.ts index f09073a5..79305f04 100644 --- a/packages/ecc-utils-design/src/components/form/tests/form.test.ts +++ b/packages/ecc-utils-design/src/components/form/tests/form.test.ts @@ -2,578 +2,1540 @@ /* eslint-disable no-unused-expressions */ import { expect } from "@open-wc/testing"; -import * as _ from "lodash-es"; import sinon from "sinon"; -import createNewFormComponent, { - FormComponentType, - testIds, -} from "./form.class.js"; +import createNewFormComponent, { FormComponentType } from "./form.class.js"; +import { Field } from "../form.js"; import { - simpleTestData, - complexArrayTestData, - simpleArrayTestData, - groupTestData, - submitTestData, - fieldOptionsTestData, - requiredFieldsTestData, - testDataForFileOptions, -} from "./testData.js"; - -const { - formInput, - formInputFile, - formInputParent, - formSwitch, - formSwitchParent, - formArrayAddButton, - formArrayDeleteButton, - formArrayItem, - formArray, - formGroup, - formCollapsibleGroup, - formNonCollapsibleGroup, - formTooltip, -} = testIds; + GenericElement, + InputField, + SelectField, +} from "../../../internal/TestComponent.js"; describe("renders correctly", () => { let formComponent: FormComponentType; beforeEach(async () => { - formComponent = await createNewFormComponent(simpleTestData); + formComponent = await createNewFormComponent([ + { + key: "name", + label: "Name", + }, + ]); }); - it("returns error when field is empty", async () => { + it("should return error when field is empty", async () => { try { await createNewFormComponent([]); } catch (formError: any & { message: string }) { expect(formError).to.be.an("error"); - expect(formError!.message).to.equal( + expect(formError!.message).equal( "Fields is required & should not be empty array" ); } }); - it("works correctly with minimum required fields", async () => { + it("should work correctly with minimum required fields", async () => { // renders correctly - expect(formComponent.form).to.be.visible; - expect(formComponent.inputField(formInput, "root")).to.be.visible; - - // throws error sumbit is attempted and no field is filled - const formError = sinon.stub(formComponent.form, "error"); - formComponent.clickSubmitButton(); - - sinon.assert.calledOnceWithExactly(formError, { message: "Form is empty" }); + expect(formComponent.form).is.visible; + expect(formComponent.getInputField("Name").el).is.visible; }); - it("renders error template correctly", async () => { + it("should render error template correctly", async () => { formComponent.form.error({ message: "test error" }); await formComponent.form.updateComplete; - expect(formComponent.errorTemplate()).to.be.visible.and.to.contain.text( - "test error" - ); + expect(formComponent.errorTemplate()).is.visible; + expect(formComponent.errorTemplate()).contain.text("test error"); }); - it("renders success template correctly", async () => { + it("should render success template correctly", async () => { formComponent.form.success({ message: "test success" }); await formComponent.form.updateComplete; - expect(formComponent.successTemplate()).to.be.visible.and.to.contain.text( - "test success" - ); + expect(formComponent.successTemplate()).is.visible; + expect(formComponent.successTemplate()).contain.text("test success"); }); }); -describe("when loading", () => { +describe("input fields", async () => { + // this tests text, date, number, email, password, tel, url, search, datetime-local and time types as well since they're identical based on the interactions our code performs with them let formComponent: FormComponentType; + let inputField: InputField; + beforeEach(async () => { - formComponent = await createNewFormComponent(simpleTestData); - formComponent.form.loading(); - await formComponent.form.updateComplete; + formComponent = await createNewFormComponent([ + { key: "name", label: "Name" }, + ]); + + inputField = formComponent.getInputField("Name"); }); - it("should disable the submit button", async () => { - expect(formComponent.submitButton()).has.attribute("disabled"); + it("should render correctly and in default state", async () => { + expect(inputField.el.type).equal("text"); // type text by default + expect(inputField.el).is.visible; // is visible + expect(inputField.el.value).equal(""); // no content by default + expect(inputField.el.getAttribute("required")).equal(null); // not required + + const label = inputField.getElement("", "label").el; + const tooltip = inputField.getElement("", "tooltip"); + expect(tooltip).to.be.null; // no tooltip + expect(label.textContent?.trim()).equal("Name"); }); -}); -describe("when array template is rendered", () => { - let formComponent: FormComponentType; - beforeEach(async () => { - formComponent = await createNewFormComponent(complexArrayTestData); + it("should render input:text when type is set to text", async () => { + formComponent = await createNewFormComponent([ + { key: "name", label: "Name", type: "text" }, + ]); + + inputField = formComponent.getInputField("Name"); + expect(inputField.el).is.visible; + expect(inputField.el.type).equal("text"); }); - it("should render children fields correctly", () => { - // 10 input fields should be rendered by default - expect(formComponent.inputField(formInput, "root", true)).to.have.lengthOf( - 10 - ); - // 2 switch field should be rendered by default - expect(formComponent.inputField(formSwitch, "root", true)).to.have.lengthOf( - 2 + // check if the rest render when set + + it("should set content in the form object correctly", async () => { + const submitSpy = sinon.spy(); + + formComponent.form.addEventListener("ecc-utils-submit", submitSpy); + await inputField.fill("David"); + formComponent.clickSubmitButton(); + + sinon.assert.calledOnceWithMatch( + submitSpy, + sinon.match.instanceOf(CustomEvent) ); - // 2 file fields should be rendered by default - expect( - formComponent.inputField(formInputFile, "root", true) - ).to.have.lengthOf(2); + const { detail } = submitSpy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + name: "David", + }, + }, + }); }); - it("delete button should work properly", async () => { - const arrayFields = formComponent.element(formArrayItem, "root", true); - expect(arrayFields).to.have.lengthOf(2); + it("should fire change event when change is text is entered", () => { + const changeSpy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-change", changeSpy); - const deleteButtons = formComponent.buttonElement( - formArrayDeleteButton, - arrayFields[0], - false + inputField.fill(); + sinon.assert.calledOnceWithExactly( + changeSpy, + sinon.match.instanceOf(CustomEvent) ); + const { detail } = changeSpy.getCall(0).args[0]; + expect(detail).deep.equal({ key: "name", value: "test value" }); + }); - await formComponent.clickButton(deleteButtons); - expect(formComponent.element(formArrayItem, "root", true)).to.have.lengthOf( - 1 - ); + describe("fieldOptions", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "name", + label: "Name", + type: "text", + fieldOptions: { + default: "David", + required: true, + tooltip: "enter name", + returnIfEmpty: true, + }, + }, + ]); + + inputField = formComponent.getInputField("Name"); + }); + + it("should set default content in the element and in the form object correctly", async () => { + const submitSpy = sinon.spy(); + + formComponent.form.addEventListener("ecc-utils-submit", submitSpy); + + expect(inputField.el.value).equal("David"); + + formComponent.clickSubmitButton(); + sinon.assert.calledOnceWithExactly( + submitSpy, + sinon.match.instanceOf(CustomEvent) + ); + const { detail } = submitSpy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + name: "David", + }, + }, + }); + }); + + it("should set required option correctly", async () => { + expect(inputField.el.getAttribute("required")).not.equal(null); + }); + + it("should set tooltip correctly", async () => { + const tooltip = inputField.getElement("", "tooltip").el; + + expect(tooltip).to.have.attribute("content", "enter name"); + }); + + describe("returnIfEmpty is true", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "name", + label: "Name", + type: "text", + fieldOptions: { + returnIfEmpty: true, + }, + }, + ]); + + inputField = formComponent.getInputField("Name"); + }); + + it("should set the value of the field to null in the form object", () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + formComponent.clickSubmitButton(); + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) + ); + + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + name: null, + }, + }, + }); + }); + + it("should set the value of the field to null when the field is set then cleared ", async () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + await inputField.fill(); // fill the input field + await inputField.fill(""); // empty the input field + formComponent.clickSubmitButton(); + + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + name: null, + }, + }, + }); + }); + }); + + describe("returnIfEmpty is not set or false", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { key: "name", label: "Name" }, + { + key: "comment", + label: "Comment", + fieldOptions: { default: "test value" }, + }, + ]); + }); + + it("should not add empty value to the form object", async () => { + const spy = sinon.spy(); + + formComponent.form.addEventListener("ecc-utils-submit", spy); + + formComponent.clickSubmitButton(); + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) + ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { data: { comment: "test value" } }, + }); + }); + + it("should remove value from form object if field is set and then cleared", async () => { + const spy = sinon.spy(); + + formComponent.form.addEventListener("ecc-utils-submit", spy); + + const nameInputField = formComponent.getInputField("Name"); + + // fill the name field and clear it + await nameInputField.fill(); + await nameInputField.fill(""); + formComponent.clickSubmitButton(); + + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) + ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { data: { comment: "test value" } }, + }); + }); + }); }); +}); - it("delete button should delete the correct instance", async () => { - await formComponent.initializeForm(simpleArrayTestData); +describe("switch fields", () => { + let formComponent: FormComponentType; + let switchField: InputField; - await formComponent.clickButton( - formComponent.buttonElement(formArrayAddButton, "root"), - 2 - ); + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "consent", + label: "Consent", + type: "switch", + }, + ]); + + switchField = formComponent.getInputField("Consent"); + }); - let arrayItems = formComponent.element(formArrayItem, "root", true); - expect(arrayItems).to.have.lengthOf(3); + it("should render correcly and in default state", () => { + const { el } = switchField; + expect(el).is.visible; // is visible + expect(el.getAttribute("required")).to.be.null; // not required + expect(el.getAttribute("checked")).to.be.null; // not checked by default - const firstInputField = formComponent.inputField(formInput, arrayItems[0]); - const secondInputField = formComponent.inputField(formInput, arrayItems[1]); - const thirdInputField = formComponent.inputField(formInput, arrayItems[2]); + const switchContainer = formComponent.getElement("", "switch-container"); + const tooltip = switchContainer.getElement("", "tooltip"); + const label = switchContainer.getElement("", "label").el; - formComponent.fillInputField(firstInputField, "test value 1"); - formComponent.fillInputField(secondInputField, "test value 2"); - formComponent.fillInputField(thirdInputField, "test value 3"); + expect(label.textContent?.trim()).equal("Consent"); + expect(tooltip).to.be.null; // no tooltip + }); - // delete the instance in the middle - await formComponent.clickButton( - formComponent.buttonElement(formArrayDeleteButton, arrayItems[1], false) + it("should set the content as false by default in the form object", () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + formComponent.clickSubmitButton(); + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + consent: false, + }, + }, + }); + }); + + it("should set content in the form object correctly", async () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); - arrayItems = formComponent.element(formArrayItem, "root", true); - expect(arrayItems).to.have.lengthOf(2); - expect( - formComponent.inputField(formInput, arrayItems[0], false).value - ).to.equal("test value 1"); - expect( - formComponent.inputField(formInput, arrayItems[1], false).value - ).to.equal("test value 3"); - }); - - it("add button should work properly", async () => { - const addButton = formComponent.buttonElement( - formArrayAddButton, - "root", - false + await switchField.toggle(); + await formComponent.clickSubmitButton(); + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + consent: true, + }, + }, + }); + }); - await formComponent.clickButton(addButton); + it("should fire change event when toggled", async () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-change", spy); - expect(formComponent.element(formArrayItem, "root", true)).to.have.lengthOf( - 3 + await switchField.toggle(); + + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ key: "consent", value: true }); }); - it("add Button should new instance at the bottom", async () => { - await formComponent.initializeForm(simpleArrayTestData); + describe("field options", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "consent", + label: "Consent", + type: "switch", + fieldOptions: { + default: true, + required: true, + tooltip: "Do you agree to our TOC", + }, + }, + ]); + + switchField = formComponent.getInputField("Consent"); + }); + + it("sets switch default content in the element and in the form object correctly", () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); - let arrayItems = formComponent.element(formArrayItem, "root", true); + expect(switchField.el.getAttribute("checked")).not.equal(null); + formComponent.clickSubmitButton(); - // check default instances - expect(arrayItems).to.have.lengthOf(1); + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) + ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + consent: true, + }, + }, + }); + }); - const firstInputField = formComponent.inputField(formInput, arrayItems[0]); - formComponent.fillInputField(firstInputField, "test value 1"); - await formComponent.clickButton( - formComponent.buttonElement(formArrayAddButton, "root") - ); + it("sets required option correctly", () => { + expect(switchField.el.getAttribute("required")).not.equal(null); + }); - arrayItems = formComponent.element(formArrayItem, "root", true); - const secondInputField = formComponent.inputField(formInput, arrayItems[1]); - formComponent.fillInputField(secondInputField, "test value 2"); + it("sets tooltip correctly", () => { + const tooltip = formComponent + .getElement("", "switch-container") + .getElement("", "tooltip").el; - expect( - formComponent.inputField(formInput, arrayItems[0], false).value - ).to.equal("test value 1"); - expect( - formComponent.inputField(formInput, arrayItems[1], false).value - ).to.equal("test value 2"); + expect(tooltip).to.have.attribute("content", "Do you agree to our TOC"); + }); }); }); -describe("when group template is rendered", () => { +describe("select fields", () => { let formComponent: FormComponentType; + let selectField: SelectField; + beforeEach(async () => { - formComponent = await createNewFormComponent(groupTestData); + formComponent = await createNewFormComponent([ + { + key: "format", + label: "Format", + type: "select", + selectOptions: [ + { + label: "PDF", + value: "pdf", + }, + { + label: "JPEG", + value: "jpeg", + }, + ], + }, + ]); + + selectField = formComponent.getSelectField("Format"); }); - it("should render children fields correctly", async () => { - const groupTemplate = formComponent.element(formGroup, "root"); - const collapsibleGroup = formComponent.element( - formCollapsibleGroup, - groupTemplate, - false - ); + it("should render correctly and in default state", async () => { + const { el } = selectField; + expect(el).to.be.visible; // is visible + expect(el.getAttribute("required")).to.be.null; // not required + expect(el.value).equal(""); // not default value + + const selectContainer = formComponent.getElement("", "select-container"); + + const label = selectContainer.getElement("", "label"); + const tooltip = selectContainer.getElement("", "tooltip"); + expect(tooltip).to.be.null; // no tooltip + expect(label.el.textContent?.trim()).equal("Format"); - expect(groupTemplate).to.be.visible; - expect(collapsibleGroup).to.be.visible; - expect(formComponent.element(formNonCollapsibleGroup, groupTemplate, false)) - .to.be.not.exist; - expect( - formComponent.inputField(formInput, collapsibleGroup, true) - ).to.have.lengthOf(3); - expect( - formComponent.inputField(formSwitch, collapsibleGroup, true) - ).to.have.lengthOf(1); - expect( - formComponent.inputField(formInputFile, collapsibleGroup, true) - ).to.have.lengthOf(1); + // renders correct number of options with correct content + expect(el.children).to.be.length(2); + + const option1 = selectField.getElement("PDF").el as HTMLOptionElement; + const option2 = selectField.getElement("JPEG").el as HTMLOptionElement; + + expect(option1.value).to.be.equal("pdf"); + expect(option2.value).to.be.equal("jpeg"); }); -}); -describe("when submit button is clicked", () => { - it("should verify that the form is not empty", async () => { - const formComponent = await createNewFormComponent(simpleTestData); - const formError = sinon.stub(formComponent.form, "error"); - formComponent.clickSubmitButton(); + it("should fire change event when new option is selected", async () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-change", spy); - sinon.assert.calledOnceWithExactly(formError, { message: "Form is empty" }); - }); - - it("should verify that all required fields are filled in simple scenarios", async () => { - const formComponent = await createNewFormComponent(requiredFieldsTestData); - - const inputFields = formComponent.inputField(formInput, "root", true); - - await formComponent.fillInputField(inputFields[0]); - expect(formComponent.submitButton()).to.have.attribute("disabled"); - - await formComponent.fillInputField(inputFields[1], "19"); - expect(formComponent.submitButton()).to.not.have.attribute("disabled"); - // note: didn't fill the email field since it is not required - }); - - // ... existing code ... - - it("should verify that all required fields are filled in array scenarios", async () => { - // revert this to new test ids - // const formComponent = await createNewFormComponent(complexArrayTestData); - // expect(formComponent.submitButton()).to.have.attribute("disabled"); - // const arrayTemplates = formComponent.element(formArrayItem, "root", true); - // // Fill all required fields in the first array - // const inputFieldsFirstArray = formComponent.inputField( - // formInput, - // arrayTemplates[0], - // true - // ); - // inputFieldsFirstArray.forEach((field) => - // formComponent.fillInputField(field) - // ); - // // Fill all required fields in the second array - // const inputFieldsSecondArray = formComponent.inputField( - // formInput, - // arrayTemplates[1], - // true - // ); - // const fileFieldsSecondArray = formComponent.inputField( - // formInputFile, - // arrayTemplates[1], - // true - // ); - // inputFieldsSecondArray.forEach((field) => - // formComponent.fillInputField(field) - // ); - // fileFieldsSecondArray.forEach((field) => - // formComponent.fillInputFileField(field) - // ); - // await formComponent.form.updateComplete; - // expect(formComponent.submitButton()).to.not.have.attribute("disabled"); - // // Add a new instance to the first array - // await formComponent.clickButton( - // formComponent.buttonElement(formArrayAddButton, arrayTemplates[0]) - // ); - // expect(formComponent.submitButton()).to.have.attribute("disabled"); - // // Fill required fields in the new instance - // const newArrayItem = formComponent.element( - // formArrayItem, - // arrayTemplates[0], - // true - // )[1]; - // const newInputFields = formComponent.inputField( - // formInput, - // newArrayItem, - // true - // ); - // newInputFields.forEach((field) => formComponent.fillInputField(field)); - // await formComponent.form.updateComplete; - // expect(formComponent.submitButton()).to.not.have.attribute("disabled"); - // // Add a new instance to the second array - // await formComponent.clickButton( - // formComponent.buttonElement(formArrayAddButton, arrayTemplates[1]) - // ); - // expect(formComponent.submitButton()).to.have.attribute("disabled"); - // // Fill required fields in the new instance of the second array - // const newSecondArrayItem = formComponent.element( - // formArrayItem, - // arrayTemplates[1], - // true - // )[1]; - // const newSecondInputFields = formComponent.inputField( - // formInput, - // newSecondArrayItem, - // true - // ); - // const newSecondFileFields = formComponent.inputField( - // formInputFile, - // newSecondArrayItem, - // true - // ); - // newSecondInputFields.forEach((field) => - // formComponent.fillInputField(field) - // ); - // newSecondFileFields.forEach((field) => - // formComponent.fillInputFileField(field) - // ); - // await formComponent.form.updateComplete; - // expect(formComponent.submitButton()).to.not.have.attribute("disabled"); - }); - - // ... rest of the existing code ... - - it("should verify that all required fields are filled in group scenarios", async () => { - const formComponent = await createNewFormComponent(groupTestData); - expect(formComponent.submitButton()).to.have.attribute("disabled"); - - const groupTemplate = formComponent.element(formGroup, "root"); - const collapsibleGroup = formComponent.element( - formCollapsibleGroup, - groupTemplate, - false + await selectField.select("JPEG"); + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ key: "format", value: "jpeg" }); + }); - const inputFields = formComponent.inputField( - formInput, - collapsibleGroup, - true - ); - const fileFields = formComponent.inputField( - formInputFile, - collapsibleGroup, - true + it("should set correct option in the element and form object correctly", async () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + await selectField.select("JPEG"); + expect(selectField.el.value).to.be.equal("jpeg"); + formComponent.clickSubmitButton(); + + sinon.assert.calledOnceWithExactly( + spy, + sinon.match.instanceOf(CustomEvent) ); + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ form: { data: { format: "jpeg" } } }); + }); - // fill all required fields - inputFields.forEach((field) => formComponent.fillInputField(field)); - fileFields.forEach((field) => formComponent.fillInputFileField(field)); + describe("fieldOptions", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "format", + label: "Format", + type: "select", + selectOptions: [ + { + label: "PDF", + value: "pdf", + }, + { + label: "JPEG", + value: "jpeg", + }, + ], + fieldOptions: { + required: true, + tooltip: "select file format", + }, + }, + ]); + + selectField = formComponent.getSelectField("Format"); + }); - await formComponent.form.updateComplete; - expect(formComponent.submitButton()).to.not.have.attribute("disabled"); + it("should set required correctly", async () => { + const label = formComponent + .getElement("", "select-container") + .getElement("", "label").el; + + expect(label.textContent?.trim()).equal("Format *"); + expect(selectField.el.getAttribute("required")).to.not.be.equal(null); + }); + + it("should set tooltip correctly", async () => { + const tooltip = formComponent + .getElement("", "select-container") + .getElement("", "tooltip").el; + + expect(tooltip).to.have.attribute("content", "select file format"); + }); }); }); -// use the submit data to test what happens when the form is submitted -// hint: use the demo to print the data to the console -// also test how the error works -describe("when form is submitted", () => { +describe("file fields", () => { let formComponent: FormComponentType; + let fileField: InputField; + let spy: sinon.SinonSpy; + beforeEach(async () => { - formComponent = await createNewFormComponent(submitTestData); + formComponent = await createNewFormComponent([ + { + key: "id", + label: "ID", + type: "file", + }, + ]); + + fileField = formComponent.getInputField("ID"); + spy = sinon.spy(); }); - it("should call the submit function with the correct data", async () => { - formComponent.form.addEventListener( - "ecc-utils-submit", - (e: CustomEvent) => { - expect( - _.isEqual(e.detail, { - form: { - data: { - isMarried: true, - address: { - street: "test value", - phoneNumbers: [ - { - phoneNumber: "test value", - }, - { - phoneNumber: "test value", - }, - ], - houseNumber: "test value", - city: "test value", - }, - bankAccounts: [ - { - accountBalance: "", - isPrimary: false, - bankName: "test value", - branchCode: "test value", - accountNumber: "test value", - beneficiaryName: "test value", - }, - { - accountBalance: "", - isPrimary: false, - bankName: "test value", - branchCode: "test value", - accountNumber: "test value", - beneficiaryName: "test value", - }, - ], - name: "test value", - age: "test value", - }, + it("should render correctly and in default state", () => { + const { el } = fileField; + const fileContainer = formComponent.getElement("", "input-file-container"); + + const label = fileContainer.getElement("", "label").el; + + expect(el).is.visible; // is visible + expect(el.getAttribute("accept")).equal("*"); // accepts all file types + expect(el.getAttribute("data-type")).equal("native"); + expect(el.getAttribute("multiple")).equal(null); + expect(el.getAttribute("required")).equal(null); + expect(label.textContent?.trim()).equal("ID"); + + const tooltip = formComponent.getElement("", "tooltip"); + expect(tooltip).to.be.null; + }); + + describe("when protocol is set to tus", () => { + let uploadStub: sinon.SinonStub; + + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "id", + label: "ID", + type: "file", + fileOptions: { + protocol: "tus", + tusOptions: { + endpoint: "#", }, - }) - ).to.be.true; - } - ); + }, + }, + ]); + + fileField = formComponent.getInputField("ID"); + uploadStub = sinon.stub(formComponent.form, "handleTusFileUpload"); + uploadStub.resolves({ + url: "mockURL", + file: "mockFile", + name: "mockName", + }); + }); + + afterEach(() => { + uploadStub.restore(); + }); + + it("should render tus field at default state", () => { + const { el } = fileField; + + const fileContainer = formComponent.getElement( + "", + "input-file-container" + ); + const label = fileContainer.getElement("", "label").el; + + expect(el).is.visible; // is visible + + expect(label.textContent?.trim()).equal("ID"); + expect(el.getAttribute("accept")).equal("*"); // accepts all file types + expect(el.getAttribute("data-type")).equal("tus"); // data-type is tus + expect(el.getAttribute("multiple")).equal(null); // no multiple attribute + expect(el.getAttribute("required")).equal(null); // not required + + const tooltip = fileContainer.getElement("root", "tooltip"); + expect(tooltip).to.be.null; // no tooltip - const inputFields = formComponent.inputField(formInput, "root", true); - const switchFields = formComponent.inputField(formSwitch, "root", true); - const fileFields = formComponent.inputField(formInputFile, "root", true); + const uploadBar = fileContainer.getElement("", "file-upload-bar").el; + const uploadPercentage = fileContainer.getElement( + "", + "file-upload-percentage" + ).el; - // fill all input fields - inputFields.forEach((field) => { - // skip the account balance field - // to test for test the returnIfEmpty flag - if (!(field.getAttribute("data-label") === "Account Balance")) { - formComponent.fillInputField(field); - } + expect(uploadBar).is.visible; // progress bar is empty + expect(uploadPercentage).is.visible; + expect(uploadPercentage.innerText).equal("0.00%"); // progress percentage is at 0 }); - // fill all file fields - fileFields.forEach((field) => { - formComponent.fillInputFileField(field); + + it("should not fire change event when tusOptions.endpoint is not added", async () => { + formComponent = await createNewFormComponent([ + { + key: "id", + label: "ID", + type: "file", + fileOptions: { + protocol: "tus", + }, + }, + ]); + + fileField = formComponent.getInputField("ID"); + formComponent.form.addEventListener("ecc-utils-change", spy); + await fileField.upload(); + + sinon.assert.notCalled(spy); }); - // toggle switch fields - switchFields.forEach((field) => { - // toggle the switch field for the married field - // to test if the switch field sets correctly - if (field.getAttribute("data-label") === "Married") { - formComponent.toggleSwitch(field); - } + + it("should call change field with correct data when a file is uploaded", async () => { + formComponent.form.addEventListener("ecc-utils-change", spy); + + await fileField.upload(); + sinon.assert.calledOnce(spy); + + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + key: "id", + value: { + url: "mockURL", + file: "mockFile", + name: "mockName", + }, + }); }); - await formComponent.form.updateComplete; + it("should set data correctly in the form object when a file is uploaded", async () => { + formComponent.form.addEventListener("ecc-utils-submit", spy); + + await fileField.upload(); + await formComponent.clickSubmitButton(); + sinon.assert.calledOnce(spy); + + const { detail } = spy.getCall(0).args[0]; + expect(detail).deep.equal({ + form: { + data: { + id: { + url: "mockURL", + file: "mockFile", + name: "mockName", + }, + }, + }, + }); + }); - formComponent.clickSubmitButton(); - // add test for the file field + describe("fieldOptions", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "id", + label: "ID", + type: "file", + fieldOptions: { + required: true, + multiple: true, + accept: "image/png", + tooltip: "your ID document", + }, + fileOptions: { + protocol: "tus", + }, + }, + ]); + + fileField = formComponent.getInputField("ID"); + }); + + it("should set multiple property correctly", () => { + expect(fileField.el.getAttribute("multiple")).to.not.be.null; + }); + + it("should set required property correctly", () => { + expect(fileField.el.getAttribute("required")).to.not.be.null; + }); + + it("should set tooltip correctly", () => { + const tooltip = formComponent.getElement("", "tooltip").el; + + expect(tooltip).is.visible; + expect(tooltip.getAttribute("content")).equal("your ID document"); + }); + }); + }); + + describe("when protocol is set to native", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "id", + label: "ID", + type: "file", + fileOptions: { + protocol: "native", + }, + }, + ]); + + fileField = formComponent.getInputField("ID"); + }); + + it("should render native field at default state", () => { + const { el } = fileField; + + const fileContainer = formComponent.getElement( + "", + "input-file-container" + ); + + const label = fileContainer.getElement("", "label").el; + + expect(el).is.visible; // is visible + expect(label.textContent?.trim()).to.equal("ID"); + expect(el.getAttribute("accept")).equal("*"); // accepts all file types + expect(el.getAttribute("data-type")).equal("native"); + expect(el.getAttribute("multiple")).equal(null); + expect(el.getAttribute("required")).equal(null); + + const tooltip = fileContainer.getElement("", "tooltip"); + expect(tooltip).to.be.null; + }); + + it("should call change field with correct data when a file is uploaded", async () => { + formComponent.form.addEventListener("ecc-utils-change", spy); + + await fileField.upload(); + sinon.assert.calledOnce(spy); + + const { detail } = spy.getCall(0).args[0]; + + expect(detail.key).equal("id"); + expect(detail.value).to.be.instanceOf(FileList); + expect(detail.value.length).equal(1); + expect(detail.value[0]).instanceOf(File); + expect(detail.value[0].name).equal("test-file.txt"); + }); + + it("should set data correctly in the form object when a file is uploaded", async () => { + formComponent.form.addEventListener("ecc-utils-submit", spy); + + await fileField.upload(); + await formComponent.clickSubmitButton(); + sinon.assert.calledOnce(spy); + + const { id } = spy.getCall(0).args[0].detail.form.data; + + expect(id).to.be.instanceOf(FileList); + expect(id.length).equal(1); + expect(id[0]).instanceOf(File); + expect(id[0].name).equal("test-file.txt"); + }); + }); + + describe("fieldOptions", () => { + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + key: "id", + label: "ID", + type: "file", + fieldOptions: { + required: true, + multiple: true, + accept: "image/png", + tooltip: "your ID document", + }, + fileOptions: { + protocol: "native", + }, + }, + ]); + + fileField = formComponent.getInputField("ID"); + }); + + it("should set multiple property correctly", () => { + expect(fileField.el.getAttribute("multiple")).to.not.be.null; + }); + + it("should set required property correctly", () => { + const fileContainer = formComponent.getElement( + "", + "input-file-container" + ); + + const label = fileContainer.getElement("", "label").el; + + expect(label.textContent?.trim()).equal("ID *"); + expect(fileField.el.getAttribute("required")).to.not.be.null; + }); + + it("should set tooltip correctly", () => { + const fileContainer = formComponent.getElement( + "", + "input-file-container" + ); + const tooltip = fileContainer.getElement("", "tooltip").el; + + expect(tooltip).is.visible; + expect(tooltip.getAttribute("content")).equal("your ID document"); + }); }); }); -describe("when fieldOptions are set", async () => { - // test untested field options - let formComponent: FormComponentType; +describe("array component", () => { + let defaultData: Field[]; + beforeEach(async () => { - formComponent = await createNewFormComponent(fieldOptionsTestData); - }); - - it("should render tooltip when set", async () => { - const inputFields = formComponent.inputField(formInput, "root", true); - const fileFields = formComponent.element(formInputParent, "root", true); - const arrayFields = formComponent.element(formArray, "root", true); - const groupFields = formComponent.element(formGroup, "root", true); - const switchFields = formComponent.element(formSwitchParent, "root", true); - - [ - ...Array.from(inputFields), - ...Array.from(fileFields), - ...Array.from(arrayFields), - ...Array.from(groupFields), - ...Array.from(switchFields), - ].forEach((field) => { + defaultData = [ + { + key: "dependents", + label: "Dependents", + type: "array", + children: [ + { + key: "name", + label: "Name", + }, + { + key: "18+", + label: "18+", + type: "switch", + }, + { + key: "gender", + label: "Gender", + type: "select", + selectOptions: [ + { + label: "Non-Binary", + value: "nonbinary", + }, + { + label: "Other", + value: "other", + }, + ], + }, + { + key: "passportPicture", + label: "Passport Picture", + type: "file", + }, + ], + }, + ]; + }); + + describe("renders correctly", () => { + it("should not render when children are not set", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], children: [] }, + ]); + const arrayComponent = formComponent.getElement("Dependents"); + + expect(arrayComponent).is.null; + }); + + it("should render correctly and in defualt state when children are set", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], arrayOptions: {} }, + ]); + const arrayComponent = formComponent.getElement("Dependents"); + const label = arrayComponent.getElement("", "label").el; + + expect(arrayComponent.el).is.visible; + expect(arrayComponent.getElement("", "tooltip")).is.null; + expect(arrayComponent.getElement("", "array-delete")).is.null; + expect(label.textContent?.trim()).to.equal("Dependents"); + + expect( + arrayComponent.getElement("", "array-add").el + ).is.visible.and.not.have.attribute("disabled"); + }); + + it("should render correctly and in default state when children and default instances are set", async () => { + const formComponent = await createNewFormComponent([ + { + ...defaultData[0], + arrayOptions: { + defaultInstances: 1, + }, + }, + ]); + const arrayComponent = formComponent.getElement("Dependents"); + const label = arrayComponent.getElement("", "label").el; + const arrayAddButton = arrayComponent.getElement("", "array-add").el; + const arrayDeleteButton = arrayComponent.getElement( + "", + "array-delete" + ).el; + + expect(label.textContent?.trim()).to.equal("Dependents"); + expect(arrayComponent.getInputField("Name").el).is.visible; + expect(arrayComponent.getInputField("18+").el).is.visible; + expect(arrayComponent.getSelectField("Gender").el).is.visible; + expect(arrayComponent.getInputField("Passport Picture").el).is.visible; + expect(arrayComponent.getElement("", "array-delete").el).is.visible; + + expect(arrayComponent.getElement("", "tooltip")).is.null; + expect(arrayAddButton).is.visible.and.not.have.attribute("disabled"); + expect(arrayDeleteButton).is.visible.and.not.have.attribute("disabled"); + }); + }); + + describe("array functionality", () => { + let formComponent: FormComponentType; + let arrayComponent: GenericElement; + + const getItem = (idx: number) => + arrayComponent?.getElement(`Dependents-${idx}`) || null; + + const getNameField = (idx: number) => + getItem(idx)?.getInputField("Name") || null; + + const getGenderField = (idx: number) => + getItem(idx)?.getSelectField("Gender") || null; + + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + ...defaultData[0], + arrayOptions: { + defaultInstances: 3, + }, + }, + ]); + + arrayComponent = formComponent.getElement("Dependents"); + + await getNameField(0).fill("test string 0"); + await getNameField(1).fill("test string 1"); + await getNameField(2).fill("test string 2"); + }); + + it("should add new array item at the botton when add array button is clicked", async () => { + const addButton = arrayComponent.getButtonElement("", "array-add"); + + expect(getNameField(3)).to.be.null; + + await addButton.click(); + const lastInput = getNameField(3); + + expect(lastInput.el.value).equal(""); + }); + + it("should remove first item when remove is clicked on the first item", async () => { + const deleteBtn1 = getItem(0).getButtonElement("", "array-delete"); + + await deleteBtn1.click(); + expect(getNameField(0).el.value).equal("test string 1"); + expect(getNameField(2)).to.be.null; + }); + + it("should remove middle item when remove is clicked on the middle item", async () => { + const deleteBtn2 = getItem(1).getButtonElement("", "array-delete"); + + await deleteBtn2.click(); + expect(getNameField(0).el.value).equal("test string 0"); + expect(getNameField(1).el.value).equal("test string 2"); + expect(getNameField(2)).to.be.null; + }); + + it("should remove last item when remove is clicked on the last item", async () => { + const deleteBtn3 = getItem(2).getButtonElement("", "array-delete"); + + await deleteBtn3.click(); + + expect(getNameField(0).el.value).equal("test string 0"); + expect(getNameField(1).el.value).equal("test string 1"); + expect(getNameField(2)).to.be.null; + }); + + it("should set the content correctly in the form object", async () => { + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + // Fill in the array items + await getNameField(0).fill("John Doe"); + await getNameField(1).fill("Jane Smith"); + + await getGenderField(0).select("Non-Binary"); + await getGenderField(1).select("Other"); + + // Submit the form + formComponent.clickSubmitButton(); + + // Assert that the spy was called with the correct data + sinon.assert.calledOnce(spy); + const { detail } = spy.getCall(0).args[0]; + expect(detail).to.deep.equal({ + form: { + data: { + dependents: [ + { name: "John Doe", gender: "nonbinary", "18+": false }, + { name: "Jane Smith", gender: "other", "18+": false }, + { name: "test string 2", "18+": false }, + ], + }, + }, + }); + }); + }); + + describe("array options", () => { + it("should render the correct number of default instances when set", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], arrayOptions: { defaultInstances: 2 } }, + ]); + const childInstances = formComponent + .getElement("Dependents") + .getElement("", "array-item", true); + + expect(childInstances.length).to.equal(2); + }); + + it("array add button should not be disabled when current instances is less than the max", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], arrayOptions: { defaultInstances: 1, max: 3 } }, + ]); + + let addButton = formComponent.getButtonElement("", "array-add"); + expect(addButton.el).to.not.have.attribute("disabled"); + + await addButton.click(); + addButton = formComponent.getButtonElement("", "array-add"); + expect(addButton.el).to.not.have.attribute("disabled"); + }); + + it("array add button should be disabled when current instances is the same as the max when set", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], arrayOptions: { defaultInstances: 1, max: 1 } }, + ]); + + const addButton = formComponent.getButtonElement("", "array-add"); + expect(addButton.el).to.have.attribute("disabled", ""); + }); + + it("array delete button should not be disabled when current instances is greater than the min", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], arrayOptions: { defaultInstances: 3, min: 1 } }, + ]); + + const deleteButton = formComponent.getButtonElement("", "array-delete"); + expect(deleteButton.el).not.to.have.attribute("disabled"); + + await deleteButton.click(); expect( - formComponent.element(formTooltip, field) - ).to.be.visible.and.to.have.attribute("content", "test tooltip"); + formComponent.getButtonElement("", "array-delete").el + ).not.to.have.attribute("disabled"); + }); + + it("array delete button should be disabled when current instances is the same as the min", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], arrayOptions: { defaultInstances: 1, min: 1 } }, + ]); + + const deleteButton = formComponent.getButtonElement("", "array-delete"); + expect(deleteButton.el).to.have.attribute("disabled", ""); + }); + }); + + describe("field options", () => { + let formComponent: FormComponentType; + + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + ...defaultData[0], + fieldOptions: { + required: true, + tooltip: "please list all your dependents", + }, + }, + ]); + }); + + it("should render tooltip correctly when set", () => { + const tooltip = formComponent + .getElement("Dependents") + .getElement("", "tooltip").el; + + expect(tooltip).attribute("content", "please list all your dependents"); + }); + + it("should show `*` when required is set", () => { + const label = formComponent + .getElement("Dependents") + .getElement("", "label").el; + + expect(label.textContent?.trim()).to.equal("Dependents *"); + }); + }); +}); + +describe("group component", () => { + let defaultData: Field[]; + + beforeEach(async () => { + defaultData = [ + { + key: "dependents", + label: "Dependents", + type: "group", + children: [ + { + key: "name", + label: "Name", + }, + { + key: "18+", + label: "18+", + type: "switch", + }, + { + key: "gender", + label: "Gender", + type: "select", + selectOptions: [ + { + label: "Non-Binary", + value: "nonBinary", + }, + { + label: "Other", + value: "other", + }, + ], + }, + { + key: "passportPicture", + label: "Passport Picture", + type: "file", + }, + ], + }, + ]; + }); + + describe("renders correclty and in default state", () => { + it("should not render when children are not set", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], children: [] }, + ]); + const groupComponent = formComponent.getElement("Dependents"); + + expect(groupComponent).is.null; + }); + + it("should render correctly and in default state when children are set", async () => { + const formComponent = await createNewFormComponent(defaultData); + const groupComponent = formComponent.getElement( + "", + "group-non-collapsible" + ); + const label = groupComponent.getElement("", "label").el; + + expect(groupComponent.el).is.visible; + expect(groupComponent.getElement("", "tooltip")).is.null; + expect(label.textContent?.trim()).to.equal("Dependents"); + + const groupContainer = formComponent.getElement("", "group-container"); + expect(groupContainer.getInputField("Name").el).is.visible; + expect(groupContainer.getInputField("18+").el).is.visible; + expect(groupContainer.getSelectField("Gender").el).is.visible; + }); + + it("should render correctly when collapsible is true", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], groupOptions: { collapsible: true } }, + ]); + + const groupComponent = formComponent.getElement("", "group-collapsible"); + + expect(groupComponent.el).is.visible; + expect(groupComponent.getElement("", "tooltip")).is.null; + expect(groupComponent.el.getAttribute("summary")?.trim()).equal( + "Dependents" + ); + }); + + it("should render correctly when collapsible is false", async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], groupOptions: { collapsible: false } }, + ]); + const groupComponent = formComponent.getElement( + "", + "group-non-collapsible" + ); + const label = groupComponent.getElement("", "label").el; + + expect(groupComponent.el).is.visible; + expect(groupComponent.getElement("", "tooltip")).is.null; + expect(label.textContent?.trim()).to.equal("Dependents"); + }); + }); + + describe("field options", () => { + it("should set tooltip correctly on non-collapsible groups", async () => { + const formComponent = await createNewFormComponent([ + { + ...defaultData[0], + fieldOptions: { required: true }, + groupOptions: { collapsible: false }, + }, + ]); + + const label = formComponent + .getElement("Dependents") + .getElement("", "label").el; + + expect(label.textContent?.trim()).equal("Dependents *"); + }); + + it("should set tooltip correctly on collapsible groups", async () => { + const formComponent = await createNewFormComponent([ + { + ...defaultData[0], + fieldOptions: { required: true }, + groupOptions: { collapsible: true }, + }, + ]); + + const label = formComponent + .getElement("Dependents") + .el.getAttribute("summary"); + + expect(label?.trim()).equal("Dependents *"); + }); + + it("should set required correctly", async () => { + const formComponent = await createNewFormComponent([ + { + ...defaultData[0], + fieldOptions: { tooltip: "fill dependents data" }, + }, + ]); + + const tooltip = formComponent + .getElement("Dependents") + .getElement("", "tooltip").el; + + expect(tooltip).to.have.attribute("content", "fill dependents data"); }); }); - it("should set default value correctly", async () => { - // this is pretty brittle but the test is simple enough - formComponent.form.addEventListener( - "ecc-utils-submit", - (e: CustomEvent) => { - expect( - _.isEqual(e.detail, { - form: { - data: { participants: [{ name: "John Doe", isMarried: true }] }, + describe("group functionality", () => { + it('should set the content correctly inthe form object when "collapsible" is true', async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], groupOptions: { collapsible: false } }, + ]); + + const groupContainer = formComponent.getElement("", "group-container"); + + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + // Fill in the group fields + await groupContainer.getInputField("Name").fill("John Doe"); + await groupContainer.getSelectField("Gender").select("Non-Binary"); + await groupContainer.getInputField("18+").toggle(); + + // Submit the form + formComponent.clickSubmitButton(); + + // Assert that the spy was called with the correct data + sinon.assert.calledOnce(spy); + const { detail } = spy.getCall(0).args[0]; + expect(detail).to.deep.equal({ + form: { + data: { + dependents: { + "18+": true, + name: "John Doe", + gender: "nonBinary", }, - }) - ).to.be.true; - } - ); - const inputField = formComponent.form.shadowRoot!.querySelector( - '[data-label="Name"]' - ); - const switchField = formComponent.form.shadowRoot!.querySelector( - '[data-label="Married"]' - ); + }, + }, + }); + }); - expect(inputField).to.have.attribute("value", "John Doe"); - expect(switchField).to.have.attribute("checked"); + it('should set the content correctly inthe form object when "collapsible" is true', async () => { + const formComponent = await createNewFormComponent([ + { ...defaultData[0], groupOptions: { collapsible: true } }, + ]); + + const groupContainer = formComponent.getElement("", "group-container"); + + const spy = sinon.spy(); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + // Fill in the group fields + await groupContainer.getInputField("Name").fill("John Doe"); + await groupContainer.getSelectField("Gender").select("Non-Binary"); + await groupContainer.getInputField("18+").toggle(); + + // Submit the form + formComponent.clickSubmitButton(); + + // Assert that the spy was called with the correct data + sinon.assert.calledOnce(spy); + const { detail } = spy.getCall(0).args[0]; + expect(detail).to.deep.equal({ + form: { + data: { + dependents: { + "18+": true, + name: "John Doe", + gender: "nonBinary", + }, + }, + }, + }); + }); + }); +}); - formComponent.clickSubmitButton(); +describe("when loading", () => { + let formComponent: FormComponentType; + beforeEach(async () => { + formComponent = await createNewFormComponent([ + { + label: "Name", + key: "name", + }, + ]); + + formComponent.form.loading(); + await formComponent.form.updateComplete; }); - it("should accept single or multiple files depending on the value set for multiple", async () => { - const localFormComponent = await createNewFormComponent( - testDataForFileOptions - ); + it("should disable the submit button", async () => { + expect(formComponent.submitButton().el).has.attribute("disabled"); + }); +}); - const idField = localFormComponent.inputField( - formInputFile, - "root", - true - )[0]; - const vactionPhotosField = localFormComponent.inputField( - formInputFile, - "root", - true - )[1]; - const passportField = localFormComponent.inputField( - formInputFile, - "root", - true - )[2]; - - expect(idField).to.not.have.attribute("multiple"); - expect(vactionPhotosField).to.have.attribute("multiple"); - expect(passportField).to.not.have.attribute("multiple"); - }); - - it("should set the accept attribute correctly", async () => { - const localFormComponent = await createNewFormComponent( - testDataForFileOptions - ); +describe("submit button", () => { + let defaultData: Field[]; + + beforeEach(() => { + defaultData = [ + { + label: "First Name", + key: "firstname", + fieldOptions: { + required: true, + }, + }, + { + label: "Last Name", + key: "lastname", + }, + { + label: "Age", + key: "age", + type: "number", + fieldOptions: { + required: true, + }, + }, + ]; + }); + + describe("when submit button is clicked", async () => { + it("should throw an error if the form is empty", async () => { + const formComponent = await createNewFormComponent([ + { + label: "Name", + key: "name", + }, + ]); + + const formError = sinon.stub(formComponent.form, "error"); + formComponent.clickSubmitButton(); + + sinon.assert.calledOnceWithExactly(formError, { + message: "Form is empty", + }); + }); + }); + + describe("when there are required fields", () => { + it("should not be disabled if required fields are filled", async () => { + const formComponent = await createNewFormComponent(defaultData); + + // only filling required fields + await formComponent.getInputField("First Name").fill(); + await formComponent.getInputField("Age").fill("45"); + + expect(formComponent.submitButton().el).to.not.have.attribute("disabled"); + }); + + it("should be disabled if even 1 required field is not filled", async () => { + const formComponent = await createNewFormComponent(defaultData); - const idField = localFormComponent.inputField( - formInputFile, - "root", - true - )[0]; - const vactionPhotosField = localFormComponent.inputField( - formInputFile, - "root", - true - )[1]; - const passportField = localFormComponent.inputField( - formInputFile, - "root", - true - )[2]; - - expect(idField).to.have.attribute("accept", ".pdf .doc .docx"); - expect(vactionPhotosField).to.have.attribute("accept", "image/*"); - expect(passportField).to.have.attribute("accept", "*"); + // only filling required fields + await formComponent.getInputField("Last Name").fill(); + await formComponent.getInputField("Age").fill("45"); + + expect(formComponent.submitButton().el).to.have.attribute("disabled"); + }); + + it("should be disabled if a required field is filled then cleared", async () => { + const formComponent = await createNewFormComponent(defaultData); + + // only filling required fields + await formComponent.getInputField("First Name").fill(); + await formComponent.getInputField("Age").fill("45"); + await formComponent.getInputField("First Name").fill(""); + + expect(formComponent.submitButton().el).to.have.attribute("disabled"); + }); + + describe("when there is an array", () => { + let formComponent: FormComponentType; + + beforeEach(async () => { + formComponent = await createNewFormComponent([ + ...defaultData, + { + label: "Other Sinatoriaries", + key: "signatories", + type: "array", + children: defaultData, + arrayOptions: { + defaultInstances: 1, + }, + }, + ]); + }); + + it("should be disabled if required fields are filled and then a new array is added", async () => { + const inputFields = formComponent.getInputField("", "input", true); + + inputFields.forEach((field) => { + if (field.el.required) field.fill(); + }); + + await formComponent.form.updateComplete; + expect(formComponent.submitButton().el).not.to.have.attribute( + "disabled" + ); + + // Adding new array instance + await formComponent.getButtonElement("", "array-add").click(); + expect(formComponent.submitButton().el).to.have.attribute("disabled"); + }); + }); + + // describe('when there is a coll') }); }); -// after this consider some error scenarios -// consider using the get fields directly in the tests +describe("when form is submitted", () => { + let defaultData: Field[]; + + beforeEach(async () => { + defaultData = [ + { + label: "Name", + key: "name", + }, + ]; + }); + + it("should call the submit function with the correct data", async () => { + const spy = sinon.spy(); + const formComponent = await createNewFormComponent(defaultData); + formComponent.form.addEventListener("ecc-utils-submit", spy); + + formComponent.getInputField("Name").fill("David"); + await formComponent.clickSubmitButton(); + + // Assert that the spy was called with the correct data + sinon.assert.calledOnce(spy); + const { detail } = spy.getCall(0).args[0]; + expect(detail).to.deep.equal({ + form: { + data: { + name: "David", + }, + }, + }); + }); +}); diff --git a/packages/ecc-utils-design/src/components/form/tests/testData.ts b/packages/ecc-utils-design/src/components/form/tests/testData.ts index bbb0b11c..daa4c1c0 100644 --- a/packages/ecc-utils-design/src/components/form/tests/testData.ts +++ b/packages/ecc-utils-design/src/components/form/tests/testData.ts @@ -7,120 +7,30 @@ export const simpleTestData: Field[] = [ }, ]; -export const complexArrayTestData: Field[] = [ +export const selectFieldTestData: Field[] = [ { - key: "addresses", - label: "Addresses", - type: "array", - fieldOptions: { tooltip: "Your Addresses" }, - arrayOptions: { defaultInstances: 0, max: 2 }, - children: [ - { - key: "houseNumber", - label: "House Number", - type: "text", - fieldOptions: { required: true, tooltip: "Your house number" }, - }, - { - key: "street", - label: "Street", - type: "text", - fieldOptions: { - default: "1601 Harrier Ln", - required: false, - tooltip: "Your street name", - }, - }, - { - key: "city", - label: "City", - type: "text", - fieldOptions: { required: true, tooltip: "Your city name" }, - }, - { - key: "isPrimary", - label: "Primary", - type: "switch", - fieldOptions: { - default: true, - tooltip: "Is this your primary residence?", - }, - }, - ], - }, - { - key: "bankAccounts", - label: "Bank Accounts", - type: "array", - arrayOptions: { defaultInstances: 2, max: 4, min: 1 }, - children: [ - { - key: "bankName", - label: "Bank Name", - type: "text", - fieldOptions: { required: true, tooltip: "your bank name" }, - }, - { - key: "branchCode", - label: "Branch Code", - type: "text", - fieldOptions: { - required: true, - }, - }, - { - key: "accountNumber", - label: "Account Number", - type: "text", - fieldOptions: { - required: true, - tooltip: "Your bank account number", - }, - }, - { - key: "beneficiaryName", - label: "Beneficiary Name", - fieldOptions: { - required: true, - tooltip: "the name on the account", - }, - }, - { - key: "accountBalance", - label: "Account Balance", - type: "number", - fieldOptions: { - tooltip: "The account Balance", - }, - }, - { - key: "isPrimary", - label: "Primary", - type: "switch", - fieldOptions: { - default: true, - tooltip: "Is this your primary bank account?", - }, - }, - { - key: "id", - label: "ID", - type: "file", - fieldOptions: { - required: true, - tooltip: "Upload a copy of your ID", - }, - }, + key: "gender", + label: "Gender", + type: "select", + fieldOptions: { + required: true, + }, + selectOptions: [ + { label: "Male", value: "male" }, + { label: "Female", value: "female" }, + { label: "Non-binary", value: "non-binary" }, + { label: "other", value: "other" }, + { label: "Prefer not to say", value: "none" }, ], }, ]; -export const simpleArrayTestData: Field[] = [ +export const arrayTestData: Field[] = [ { key: "otherNames", label: "Other Names", type: "array", - arrayOptions: { defaultInstances: 1, max: 3 }, + arrayOptions: { defaultInstances: 2, max: 3 }, children: [ { key: "name", @@ -128,6 +38,18 @@ export const simpleArrayTestData: Field[] = [ type: "text", fieldOptions: { required: true, tooltip: "Your other name" }, }, + { + key: "18+", + label: "18+", + type: "switch", + fieldOptions: { required: true, tooltip: "switch" }, + }, + { + key: "passportPicture", + label: "Passport Picture", + type: "file", + fieldOptions: { required: false, tooltip: "your passport" }, + }, ], }, ]; diff --git a/packages/ecc-utils-design/src/internal/TestComponent.ts b/packages/ecc-utils-design/src/internal/TestComponent.ts index bf5dccfc..e5b2b7db 100644 --- a/packages/ecc-utils-design/src/internal/TestComponent.ts +++ b/packages/ecc-utils-design/src/internal/TestComponent.ts @@ -1,67 +1,145 @@ +// eslint-disable-next-line max-classes-per-file import { LitElement } from "lit"; -type ParentElement = Document | Element | ShadowRoot | "root"; -type ComponentType = LitElement; +type ParentElement = Document | Element | ShadowRoot; +type InstanceType = "litElement" | "element"; +type ComponentType = LitElement | null; -export default class TestComponent { - component: ComponentType; - constructor(component: ComponentType) { - this.component = component; +export default class Field { + component: ComponentType = null; + instance: InstanceType; + el: ParentElement; + + constructor(el: Element | LitElement, instance: InstanceType = "element") { + this.instance = instance; + + if (instance === "litElement") { + this.component = el as LitElement; + this.el = el.shadowRoot!; + } else { + this.el = el; + } } - private getFields = ( - id: string, - parentElement: ParentElement, - retrieveAll = false - ) => { - if (parentElement === "root") { - // eslint-disable-next-line no-param-reassign - parentElement = this.component.shadowRoot!; + _getFields = (label?: string, id?: string, retrieveAll = false) => { + if (!id && !label) return null; + if (label && !id) { + if (retrieveAll) { + return this.el.querySelectorAll(`[data-label="${label}"]`); + } + return this.el.querySelector(`[data-label="${label}"]`); } + + if (!label && id) { + if (retrieveAll) { + return this.el.querySelectorAll(`[data-testid="${id}"]`); + } + return this.el.querySelector(`[data-testid="${id}"]`); + } + if (retrieveAll) { - return parentElement.querySelectorAll(`[data-testid="${id}"]`); + return this.el.querySelectorAll( + `[data-testid="${id}"][data-label="${label}"]` + ); } - return parentElement.querySelector(`[data-testid="${id}"]`); + return this.el.querySelector( + `[data-testid="${id}"][data-label="${label}"]` + ); }; + setEl(el: Element) { + if (this.instance === "litElement") { + this.component = el as LitElement; + this.el = el.shadowRoot!; + } else { + this.el = el; + } + } + + disable() { + if (this.instance === "element") { + (this.el as HTMLElement).setAttribute("disable", ""); + } + } + // locators - element(testId: string, p: ParentElement): HTMLElement; - element(testId: string, p: ParentElement, all: true): NodeListOf; - element(testId: string, p: ParentElement, all: false): HTMLElement; - element(testId: string, p: ParentElement, all = false) { - return this.getFields(testId, p, all); + getElement(label?: string, testId?: string): GenericElement; + getElement(label?: string, testId?: string, all?: false): GenericElement; + getElement(label?: string, testId?: string, all?: true): GenericElement[]; + getElement(label?: string, testId?: string, all = false) { + const fields = this._getFields(label, testId, all) as + | HTMLElement + | NodeListOf; + + if (!fields) return null; + if (fields instanceof NodeList) { + return Array.from(fields).map((f) => new GenericElement(f)); + } + + return new GenericElement(fields); + } + + getInputField(label?: string, testId?: string): InputField; + getInputField(label?: string, testId?: string, all?: false): InputField; + getInputField(label?: string, testId?: string, all?: true): InputField[]; + getInputField(label?: string, testId?: string, all = false) { + const fields = this._getFields(label, testId, all) as + | HTMLInputElement + | NodeListOf; + + if (!fields) return null; + if (fields instanceof NodeList) { + return Array.from(fields).map((f) => new InputField(f)); + } + + return new InputField(fields); } - inputField(testId: string, p: ParentElement): HTMLInputElement; - inputField( - testId: string, - p: ParentElement, - all: true - ): NodeListOf; + getSelectField(label?: string, testId?: string): SelectField; + getSelectField(label?: string, testId?: string, all?: false): SelectField; + getSelectField(label?: string, testId?: string, all?: true): SelectField[]; + getSelectField(label?: string, testId?: string, all = false) { + const fields = this._getFields(label, testId, all) as + | HTMLSelectElement + | NodeListOf; + + if (!fields) return null; + if (fields instanceof NodeList) { + return Array.from(fields).map((f) => new SelectField(f)); + } - inputField(testId: string, p: ParentElement, all: false): HTMLInputElement; - inputField(testId: string, p: ParentElement, all = false) { - return this.getFields(testId, p, all); + return new SelectField(fields); } - buttonElement(testId: string, p: ParentElement): HTMLButtonElement; - buttonElement( - testId: string, - P: ParentElement, - all: true - ): NodeListOf; - - buttonElement( - testId: string, - p: ParentElement, - all: false - ): HTMLButtonElement; - - buttonElement(testId: string, p: ParentElement, all = false) { - return this.getFields(testId, p, all); + getButtonElement(label?: string, testId?: string): ButtonElement; + getButtonElement(label?: string, testId?: string, all?: false): ButtonElement; + getButtonElement( + label?: string, + testId?: string, + all?: true + ): ButtonElement[]; + + getButtonElement(label?: string, testId?: string, all = false) { + const fields = this._getFields(label, testId, all) as + | HTMLButtonElement + | NodeListOf; + + if (!fields) return null; + if (fields instanceof NodeList) { + return Array.from(fields).map((f) => new ButtonElement(f)); + } + + return new ButtonElement(fields); } +} - // methods +export class InputField extends Field { + el: HTMLInputElement; + + constructor(el: HTMLInputElement) { + super(el); + this.el = el; + } /** * Fills an input file field with an optional specified file text. @@ -70,26 +148,25 @@ export default class TestComponent { * @param fileText - The text content of the file. * @note You do not need to await this method, you can await the form updateComplete as an alternative */ - public async fillInputField( - inputField: HTMLInputElement, - text = "test value" - ) { + public async fill(text = "test value") { + if (this.el.getAttribute("disable")) return; + // eslint-disable-next-line no-param-reassign - inputField.value = text; - inputField.dispatchEvent(new Event("sl-input")); - await this.component.updateComplete; + this.el.value = text; + this.el.dispatchEvent(new Event("sl-input")); + + await this.component?.updateComplete; } - public async fillInputFileField( - inputField: HTMLInputElement, - fileText = "test value" - ) { - if (inputField.type !== "file") { - throw new Error("inputField is not a valid file element"); + public async upload(dataText = "test value") { + if (this.el.getAttribute("disable")) return; + + if (this.el.type !== "file") { + throw new Error("this field is not a valid file element"); } const files = [ - new File([fileText], "test-file.txt", { type: "text/plain" }), + new File([dataText], "test-file.txt", { type: "text/plain" }), ]; const dataTransfer = new DataTransfer(); files.forEach((file) => { @@ -97,24 +174,80 @@ export default class TestComponent { }); // eslint-disable-next-line no-param-reassign - inputField.files = dataTransfer.files; - inputField.dispatchEvent(new Event("change")); + this.el.files = dataTransfer.files; + this.el.dispatchEvent(new Event("change")); - await this.component.updateComplete; + await this.component?.updateComplete; } - public async toggleSwitch(switchField: HTMLInputElement) { - switchField.click(); - await this.component.updateComplete; + public async toggle() { + if (this.el.getAttribute("disable")) return; + + this.el.click(); + await this.component?.updateComplete; + } +} + +export class SelectField extends Field { + el: HTMLSelectElement; + + constructor(el: HTMLSelectElement) { + super(el); + this.el = el; } - // actions - public clickButton = async ( - button: HTMLButtonElement, - numberOfClicks = 1 - ) => { + public select = async (label: number | string) => { + if (this.el.getAttribute("disable")) return; + let option: HTMLOptionElement | null = null; + + if (typeof label === "string") { + const children = Array.from(this.el.children); + + const el = children.find( + (opt) => opt.textContent?.trim() === label + ) as HTMLOptionElement; + + option = el || null; + } else if (typeof label === "number") { + option = this.el.children.item(label) as HTMLOptionElement; + } + + if (option?.value) { + this.el.value = option.value; + this.el.dispatchEvent(new Event("sl-change")); + this.el.dispatchEvent(new Event("sl-input")); + await this.component?.updateComplete; + } + }; +} + +export class ButtonElement extends Field { + el: HTMLButtonElement; + + constructor(el: HTMLButtonElement) { + super(el); + this.el = el; + } + + public click = async (numberOfClicks = 1) => { + if (this.el.getAttribute("disable")) return; + // eslint-disable-next-line no-plusplus - for (let i = 0; i < numberOfClicks; i++) button.click(); - await this.component.updateComplete; + for (let i = 0; i < numberOfClicks; i++) this.el.click(); + await this.component?.updateComplete; }; } + +export class GenericElement extends Field { + el: HTMLElement; + + constructor(el: HTMLElement) { + super(el); + this.el = el; + } +} + +export const elementIsVisible = (element: HTMLElement) => { + if (!element) return false; + return element.offsetHeight > 0 && element.offsetWidth > 0; +};