diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..cbfc895 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,16 @@ +{ + "root": true, + "extends": [ + "lxsmnsyc/typescript" + ], + "rules": { + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-call": "off" + }, + "parserOptions": { + "project": "./tsconfig.eslint.json" + } +} diff --git a/.npmignore b/.npmignore index 3a7a719..d01c9a6 100644 --- a/.npmignore +++ b/.npmignore @@ -4,7 +4,6 @@ src/ .editorconfig .prettierrc cypress.json -jest.config.js publish.sh tsconfig.json yarn.lock diff --git a/LICENSE b/LICENSE index c736f61..f24dd21 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,7 @@ -MIT License +MIT License Copyright (c) 2023 TheoryOfNekomata -Copyright (c) 2021 TheoryOfNekomata +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 777bede..e691b21 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ _(read "form extra")_ -Extract form values through the DOM. +Extract and set form values through the DOM. ## Motivation @@ -16,7 +16,9 @@ values to each field in the form. Libraries made for extracting form values query field elements in the DOM, which is inefficient since they need to traverse the DOM tree in some way, using methods such as `document.getElementsByTagName()` and -`document.querySelector()`. +`document.querySelector()`. This is the same case with setting each form values for, say, prefilling values to save +time. It might be a simple improvement to the user experience, but the logic behind can be unwieldy as there may be +inconsistencies in setting up each field value depending on the form library being used. Upon retrieving the field values somehow, some libraries attempt to duplicate the values of the fields as they change, for instance by attaching event listeners and storing the new values into some internal object or map. This is then @@ -25,15 +27,15 @@ where changes to the document are essential to establish functionality and impro --- -With `formxtra`, there is no need to traverse the DOM for individual fields to get their values, provided they are: +With `formxtra`, there is no need to traverse elements for individual fields to get and set their values, provided they are: * Associated to the form (either as a descendant of the `
` element or [associated through the `form=""` attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form)) * Has a valid `name` -The values of these fields can be easily extracted, using the `form.elements` attribute built-in to the DOM. With this, -only the reference to the form is needed. The current state of the field elements is already stored in the DOM, waiting -to be accessed. +The values of these fields can be easily extracted and set, using the `form.elements` attribute built-in to the DOM. +With this, only the reference to the form is needed. The current state of the field elements is already stored in the +DOM, waiting to be accessed. ## Installation @@ -68,23 +70,31 @@ For an example form: Use the library as follows (code is in TypeScript, but can work with JavaScript as well): ```typescript -import getFormValues from '@theoryofnekomata/formxtra'; +// The default export is same with `getFormValues`, but it is recommended to use the named import for future-proofing! +import { getFormValues, setFormValues } from '@theoryofnekomata/formxtra'; +// This is the only query we need. On libraries like React, we can easily get form elements when we attach submit event +// listeners. const form: HTMLFormElement = document.getElementById('form'); -// optional, but just in case there are multiple submit buttons in the form, +// Optional, but just in case there are multiple submit buttons in the form, // individual submitters can be considered const submitter = form.querySelector('[type="submit"][name="type"][value="client"]'); const values = getFormValues(form, { submitter }); const processResult = (result: Record) => { + setFormValues(form, { + username: 'Username', + password: 'verylongsecret', + }); + throw new Error('Not yet implemented.'); }; // Best use case is with event handlers form.addEventListener('submit', async e => { - const { target: form, submitter } = e; + const { currentTarget: form, submitter } = e; e.preventDefault(); const values = getFormValues(form, { submitter }); diff --git a/cypress.json b/cypress.json index 0967ef4..60ed5aa 100644 --- a/cypress.json +++ b/cypress.json @@ -1 +1,3 @@ -{} +{ + "video": false +} diff --git a/cypress/integration/checkbox.test.ts b/cypress/integration/checkbox.test.ts index 67fd9bb..d1fc7a4 100644 --- a/cypress/integration/checkbox.test.ts +++ b/cypress/integration/checkbox.test.ts @@ -1,4 +1,4 @@ -import getFormValues from '../../src' +import { getFormValues } from '../../src' import * as utils from '../utils' describe('checkbox', () => { @@ -23,27 +23,37 @@ describe('checkbox', () => { `)) it('should have no form values', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const values = getFormValues(form, {submitter}) - const before = utils.makeSearchParams(values).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(values['enabled']).toBeUndefined(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + const before = utils.makeSearchParams(values) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(values['enabled']) + .toBeUndefined(); + expect(before) + .toEqual(after); }, - '' - ); + expectedStaticValue: '', + }); }); it('should have false checked value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const values = getFormValues(form, {submitter, booleanValuelessCheckbox: true }) - expect(values['enabled']).toBe(false); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, + { + submitter, + booleanValuelessCheckbox: true + } + ) + expect(values['enabled']) + .toBe(false); } - ); + }); }); }) @@ -68,15 +78,18 @@ describe('checkbox', () => { `)) it('should have single form value on a single field', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - 'enabled=on' - ); + expectedStaticValue: 'enabled=on', + }); }); }) }) diff --git a/cypress/integration/file.test.ts b/cypress/integration/file.test.ts index 56274af..03781ea 100644 --- a/cypress/integration/file.test.ts +++ b/cypress/integration/file.test.ts @@ -1,4 +1,4 @@ -import getFormValues from '../../src' +import { getFormValues } from '../../src' import * as utils from '../utils' describe('file', () => { @@ -23,52 +23,65 @@ describe('file', () => { `)) it('should have no form values when no file is selected', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - {} - ); + expectedStaticValue: { + hello: '' + }, + }); }) it('should have single form value when a file is selected', () => { - utils.test( - (cy: any) => { + utils.test({ + action: (cy: any) => { cy .get('[name="hello"]') .attachFile('uploads/data.json') return cy.get('[type="submit"]') }, - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'data.json', - } - ); + }, + }); }) it('should retrieve the file list upon setting appropriate option', () => { - utils.test( - (cy: any) => { + utils.test({ + action: (cy: any) => { cy .get('[name="hello"]') .attachFile('uploads/data.json') return cy.get('[type="submit"]') }, - (form: HTMLFormElement, submitter: any) => { - const formValues = getFormValues(form, {submitter, getFileObjects: true}) - expect(formValues.hello[0].name).toBe('data.json') - //expect(before).toEqual(after); + test: (form: HTMLFormElement, submitter: any) => { + const formValues = getFormValues(form, + { + submitter, + getFileObjects: true + } + ) + expect(formValues.hello[0].name) + .toBe('data.json') }, - ); + }); }) }) @@ -93,50 +106,65 @@ describe('file', () => { `)) it('should have no form values when no file is selected', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); + }, + expectedStaticValue: { + hello: '', }, - {} - ); + }); }) it('should have single form value when a file is selected', () => { - utils.test( - (cy: any) => { + utils.test({ + action: (cy: any) => { cy .get('[name="hello"]') .attachFile(['uploads/data.json', 'uploads/data2.json']) return cy.get('[type="submit"]') }, - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - 'hello=data.json&hello=data2.json', - ); + expectedStaticValue: 'hello=data.json&hello=data2.json', + }); }) it('should retrieve the file list upon setting appropriate option', () => { - utils.test( - (cy: any) => { + utils.test({ + action: (cy: any) => { cy .get('[name="hello"]') .attachFile(['uploads/data.json', 'uploads/data2.json']) return cy.get('[type="submit"]') }, - (form: HTMLFormElement, submitter: any) => { - const formValues = getFormValues(form, {submitter, getFileObjects: true}) - expect(formValues.hello[0].name).toBe('data.json') - expect(formValues.hello[1].name).toBe('data2.json') + test: (form: HTMLFormElement, submitter: any) => { + const formValues = getFormValues(form, + { + submitter, + getFileObjects: true + } + ) + expect(formValues.hello[0].name) + .toBe('data.json') + expect(formValues.hello[1].name) + .toBe('data2.json') }, - ); + }); }) }) }) diff --git a/cypress/integration/misc.test.ts b/cypress/integration/misc.test.ts index 2425e55..3eb839e 100644 --- a/cypress/integration/misc.test.ts +++ b/cypress/integration/misc.test.ts @@ -1,4 +1,4 @@ -import getFormValues from '../../src' +import { getFormValues, setFormValues } from '../../src'; import * as utils from '../utils' describe('misc', () => { @@ -19,15 +19,18 @@ describe('misc', () => { `)) it('should have blank form value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - {} - ); + expectedStaticValue: {}, + }); }); }) @@ -65,6 +68,9 @@ describe('misc', () => { +
+ +
+
+
+ +
Default Dependents @@ -109,6 +121,12 @@ describe('misc', () => {
+
+ +
@@ -134,35 +152,100 @@ describe('misc', () => { `)) it('should have correct form values', () => { - utils.test( - (cy) => { - cy.get('[name="first_name"]').type('John') - cy.get('[name="middle_name"]').type('Marcelo') - cy.get('[name="last_name"]').type('Dela Cruz') - cy.get('[name="gender"][value="m"]').check() - cy.get('[name="civil_status"]').select('Married') - cy.get('[name="new_registration"]').check() - cy.get('[name="nationality"][value="filipino"]').check() - cy.get('[name="dependent"][value="Jun"]').check() - - // Note: JSDOM is static for now - cy.get('button.dependents').click() - cy.get('.additional-dependent [name="dependent"][type="text"]').last().type('Juana') - cy.get('button.dependents').click() - cy.get('.additional-dependent [name="dependent"][type="text"]').last().type('Jane') - cy.get('button.dependents').click() - cy.get('.additional-dependent [name="dependent"][type="text"]').last().type('Josh') + utils.test({ + action: (cy) => { + cy.get('[name="first_name"]') + .type('John') + cy.get('[name="middle_name"]') + .type('Marcelo') + cy.get('[name="last_name"]') + .type('Dela Cruz') + cy.get('[name="gender"][value="m"]') + .check() + cy.get('[name="birthday"]') + .type('1989-06-04') + cy.get('[name="civil_status"]') + .select('Married') + cy.get('[name="new_registration"]') + .check() + cy.get('[name="appointment_datetime"]') + .type('2001-09-11T06:09') + cy.get('[name="nationality"][value="filipino"]') + .check() + cy.get('[name="gross"]') + .type('131072') + cy.get('[name="dependent"][value="Jun"]') + .check() - cy.get('[name="notes"]').type('Test content\n\nNew line\n\nAnother line') + cy.get('button.dependents') + .click() + cy.get('.additional-dependent [name="dependent"][type="text"]') + .last() + .type('Juana') + cy.get('button.dependents') + .click() + cy.get('.additional-dependent [name="dependent"][type="text"]') + .last() + .type('Jane') + cy.get('button.dependents') + .click() + cy.get('.additional-dependent [name="dependent"][type="text"]') + .last() + .type('Josh') + cy.get('[name="qos"]') + .invoke('val', 10) + .trigger('change') + cy.get('[name="notes"]') + .type('Test content\n\nNew line\n\nAnother line') return cy.get('[name="submit"][value="Hi"]') }, - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + + expect(before) + .toEqual(after); + }, + expectedStaticValue: 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&birthday=1989-06-04&civil_status=married&new_registration=on&appointment_datetime=2001-09-11T06%3A09&nationality=filipino&gross=131072&dependent=Jun¬es=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=10&submit=Hi', + }); + }); + + it('should have filled form values', () => { + utils.test({ + action: (cy) => cy.wait(3000).get('[name="submit"][value="Hi"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); + }, + preAction: (form: HTMLFormElement) => { + setFormValues(form, { + first_name: 'John', + middle_name: 'Marcelo', + last_name: 'Dela Cruz', + gender: 'm', + birthday: new Date('1989-06-04'), + civil_status: 'married', + new_registration: 'on', + appointment_datetime: new Date('2001-09-11T06:09:00'), + nationality: 'filipino', + gross: 131072, + dependent: 'Jun', + notes: `Test content + +New line + +Another line`, + qos: 10, + }); }, - 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&civil_status=married&new_registration=on&nationality=filipino&dependent=Jun¬es=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&submit=Hi', - ); + expectedStaticValue: 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&birthday=1989-06-04&civil_status=married&new_registration=on&appointment_datetime=2001-09-11T06%3A09&nationality=filipino&gross=131072&dependent=Jun¬es=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=10&submit=Hi', + }); }); }) }) diff --git a/cypress/integration/select.test.ts b/cypress/integration/select.test.ts index 226df96..1c893ec 100644 --- a/cypress/integration/select.test.ts +++ b/cypress/integration/select.test.ts @@ -1,4 +1,4 @@ -import getFormValues from '../../src' +import { getFormValues } from '../../src' import * as utils from '../utils' describe('select', () => { @@ -28,15 +28,18 @@ describe('select', () => { `)) it('should have multiple form values on a single field', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - 'hello=Bar&hello=Quux' - ); + expectedStaticValue: 'hello=Bar&hello=Quux' + }); }); }) @@ -65,17 +68,20 @@ describe('select', () => { `)) it('should have single form value on a single field', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Baz', } - ); + }); }); }) }) diff --git a/cypress/integration/submitter.test.ts b/cypress/integration/submitter.test.ts index 433598b..6a38e9f 100644 --- a/cypress/integration/submitter.test.ts +++ b/cypress/integration/submitter.test.ts @@ -1,4 +1,4 @@ -import getFormValues from '../../src' +import { getFormValues } from '../../src' import * as utils from '../utils' describe('submitter', () => { @@ -24,18 +24,21 @@ describe('submitter', () => { `)) it('should have double form values', () => { - utils.test( - (cy: any) => cy.get('[name="action"][value="Foo"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[name="action"][value="Foo"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Hi', action: 'Foo', - } - ); + }, + }); }); }) @@ -61,18 +64,21 @@ describe('submitter', () => { `)) it('should have double form values', () => { - utils.test( - (cy: any) => cy.get('[name="action"][value="Bar"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[name="action"][value="Bar"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Hi', action: 'Bar', - } - ); + }, + }); }); }) @@ -96,17 +102,20 @@ describe('submitter', () => { `)) it('should have single form value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Hi', - } - ); + }, + }); }); }) }) diff --git a/cypress/integration/text.test.ts b/cypress/integration/text.test.ts index a5f5776..166ea93 100644 --- a/cypress/integration/text.test.ts +++ b/cypress/integration/text.test.ts @@ -1,4 +1,4 @@ -import getFormValues from '../../src' +import { getFormValues, setFormValues } from '../../src'; import * as utils from '../utils' describe('text', () => { @@ -23,17 +23,20 @@ describe('text', () => { `)) it('should have single form value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Hi', - } - ); + }, + }); }); }) @@ -61,15 +64,18 @@ describe('text', () => { `)) it('should have blank form value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - {} - ); + expectedStaticValue: {}, + }); }); }) @@ -94,19 +100,22 @@ describe('text', () => { `)) it('should have single form value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Hi', - } - ); + }, + }); }); - }) + }); describe('readonly', () => { beforeEach(utils.setup(` @@ -132,17 +141,61 @@ describe('text', () => { `)) it('should have single form value', () => { - utils.test( - (cy: any) => cy.get('[type="submit"]'), - (form: HTMLFormElement, submitter: any, search: any) => { - const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); - const after = utils.makeSearchParams(search).toString(); - expect(before).toEqual(after); + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); }, - { + expectedStaticValue: { hello: 'Hi', - } - ); + }, + }); }); - }) + }); + + describe('programmatical value setting', () => { + beforeEach(utils.setup(` + + + + + Text/Basic + + + + + + + + + `)); + + it('should have form values set', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: 'Hi', }) + }, + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); + }, + expectedStaticValue: { + hello: 'Hi', + }, + }); + }); + }); }) diff --git a/cypress/utils/index.ts b/cypress/utils/index.ts index 7204d66..04fcf6a 100644 --- a/cypress/utils/index.ts +++ b/cypress/utils/index.ts @@ -4,7 +4,7 @@ import JSDOMDummyCypress from './jsdom-compat' type ExpectedSearchValue = Record | string -type RetrieveSubmitterFn = (wrapper: any) => any +type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any type HTMLSubmitterElement = HTMLButtonElement | HTMLInputElement @@ -18,13 +18,27 @@ export const setup = (template: string) => { } } return () => { + // @ts-ignore window.document.open(undefined, undefined, undefined, true) window.document.write(template) window.document.close() } } -export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, expectedValue?: ExpectedSearchValue) => { +type TestOptions = { + action: RetrieveSubmitterFn, + test: TestFn, + expectedStaticValue?: ExpectedSearchValue, + preAction?: Function, +} + +export const test = (options: TestOptions) => { + const { + action: retrieveSubmitterFn, + test: testFn, + expectedStaticValue, + preAction, + } = options; let form: HTMLFormElement let submitter: HTMLButtonElement | HTMLInputElement let r: any @@ -34,6 +48,10 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e .get('form') .then((formResult: any) => { [form] = Array.from(formResult); + + if (typeof preAction === 'function') { + preAction(form); + } }) r = retrieveSubmitterFn(cy) @@ -41,7 +59,7 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e [submitter] = Array.from(submitterQueryEl as any[]) }) - if (typeof expectedValue !== 'undefined') { + if (typeof expectedStaticValue !== 'undefined') { r.click() cy .wait('@submitted') @@ -61,10 +79,15 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e .then((submitterQueryEl: any) => { [submitter] = Array.from(submitterQueryEl as any[]); [form] = Array.from(window.document.getElementsByTagName('form')) - testFn(form, submitter, expectedValue) + + if (typeof preAction === 'function') { + preAction(form); + } + + testFn(form, submitter, expectedStaticValue) }) - if (typeof expectedValue !== 'undefined') { + if (typeof expectedStaticValue !== 'undefined') { r.click() } } diff --git a/cypress/utils/jsdom-compat.ts b/cypress/utils/jsdom-compat.ts index 1df5a20..cee039b 100644 --- a/cypress/utils/jsdom-compat.ts +++ b/cypress/utils/jsdom-compat.ts @@ -1,7 +1,7 @@ /// -import { readFileSync } from 'fs' -import { join } from 'path' +import { readFileSync, statSync } from 'fs' +import { join, basename } from 'path' class JSDOMJQuery { private selectedElements: Node[] @@ -16,6 +16,11 @@ class JSDOMJQuery { el.value = s return } + if (el.type === 'datetime-local') { + el.value = new Date(`${s}:00.000Z`).toISOString().slice(0, 'yyyy-MM-DDTHH:mm'.length) + return + } + el.setAttribute('value', s) el.value = s }) @@ -57,17 +62,52 @@ class JSDOMJQuery { return this } - attachFile(filename: string) { - const contents = readFileSync(join('cypress', 'fixtures', filename)) - // TODO - contents.toString('binary') + invoke(key: string, value: unknown) { + if (key === 'val') { + this.selectedElements.forEach((el) => (el as unknown as Record).valueAsNumber = value); + } + return this } + + trigger(which: string) { + return this + } + + attachFile(filename: string | string[]) { + const { File, FileList } = window; + const theFilenames = Array.isArray(filename) ? filename : [filename]; + const theFiles = theFilenames.map((f) => { + const filePath = join('cypress', 'fixtures', f); + const { mtimeMs: lastModified } = statSync(filePath); + const contents = readFileSync(filePath); + return new File( + [contents], + basename(filePath), + { + lastModified, + type: '', + }, + ); + }); + (theFiles as unknown as Record).__proto__ = Object.create(FileList.prototype); + this.selectedElements.forEach((el) => { + Object.defineProperty(el, 'files', { + value: theFiles, + writable: false, + }) + }); + return this; + } } export default class JSDOMDummyCypress { private currentElement = window.document; + wait(time: number) { + return this; + } + get(q: string) { return new JSDOMJQuery(this.currentElement.querySelectorAll(q)); } diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 4931934..0000000 --- a/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - testEnvironment: 'jsdom', -}; diff --git a/package.json b/package.json index 116561b..17fecab 100644 --- a/package.json +++ b/package.json @@ -1,80 +1,78 @@ { - "version": "0.2.3", - "license": "MIT", - "main": "dist/index.js", - "typings": "dist/index.d.ts", - "files": [ - "dist" - ], - "publishing": { - "github": { - "repository": "https://github.com/TheoryOfNekomata/formxtra.git", - "publishConfig": { - "registry": "https://npm.pkg.github.com" - } - }, - "master": { - "repository": "https://code.modal.sh/TheoryOfNekomata/formxtra.git", - "publishConfig": { - "registry": "https://js.pack.modal.sh" - } - }, - "npm": { - "publishConfig": { - "registry": "https://registry.npmjs.com" - } - } - }, - "engines": { - "node": ">=10" - }, - "scripts": { - "start": "tsdx watch", - "build": "tsdx build", - "test:jsdom": "tsdx test", - "test:dom": "cypress open", - "lint": "tsdx lint", - "prepare": "tsdx build", - "size": "size-limit", - "analyze": "size-limit --why" - }, - "peerDependencies": {}, - "husky": { - "hooks": { - "pre-commit": "tsdx lint" - } - }, - "name": "@theoryofnekomata/formxtra", - "author": "TheoryOfNekomata ", - "description": "Extract form values through the DOM.", - "module": "dist/get-form-values.esm.js", - "keywords": [ - "form", - "value", - "utility" - ], - "homepage": "https://code.modal.sh/TheoryOfNekomata/formxtra", - "size-limit": [ - { - "path": "dist/get-form-values.cjs.production.min.js", - "limit": "10 KB" - }, - { - "path": "dist/get-form-values.esm.js", - "limit": "10 KB" - } - ], - "devDependencies": { - "@size-limit/preset-small-lib": "^4.10.2", - "@types/jsdom": "^16.2.10", - "cypress": "^7.2.0", - "cypress-file-upload": "^5.0.7", - "cypress-jest-adapter": "^0.1.1", - "husky": "^6.0.0", - "jsdom": "^16.5.3", - "size-limit": "^4.10.2", - "tsdx": "^0.14.1", - "tslib": "^2.2.0", - "typescript": "^4.2.4" - } + "name": "@theoryofnekomata/formxtra", + "version": "1.0.0", + "files": [ + "dist", + "src" + ], + "publishing": { + "github": { + "repository": "https://github.com/TheoryOfNekomata/formxtra.git", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } + }, + "master": { + "repository": "https://code.modal.sh/TheoryOfNekomata/formxtra.git", + "publishConfig": { + "registry": "https://js.pack.modal.sh" + } + }, + "npm": { + "publishConfig": { + "registry": "https://registry.npmjs.com" + } + } + }, + "engines": { + "node": ">=10" + }, + "license": "MIT", + "keywords": [ + "form", + "value", + "utility" + ], + "devDependencies": { + "@types/jsdom": "^21.1.0", + "@types/node": "^18.14.1", + "cypress": "^7.2.0", + "cypress-file-upload": "^5.0.7", + "cypress-jest-adapter": "^0.1.1", + "eslint": "^8.35.0", + "eslint-config-lxsmnsyc": "^0.4.8", + "husky": "^6.0.0", + "jsdom": "^21.1.0", + "pridepack": "2.4.1", + "tslib": "^2.5.0", + "typescript": "^4.9.5", + "vitest": "^0.29.2" + }, + "scripts": { + "prepublishOnly": "pridepack clean && pridepack build", + "build": "pridepack build", + "type-check": "pridepack check", + "lint": "pridepack lint", + "clean": "pridepack clean", + "watch": "pridepack watch", + "start": "pridepack start", + "dev": "pridepack dev", + "test:jsdom": "vitest", + "test:cypress": "cypress run", + "test:cpanel": "cypress open" + }, + "private": false, + "description": "Extract and set form values through the DOM.", + "repository": { + "url": "https://code.modal.sh/TheoryOfNekomata/formxtra.git", + "type": "git" + }, + "homepage": "https://code.modal.sh/TheoryOfNekomata/formxtra", + "bugs": { + "url": "https://code.modal.sh/TheoryOfNekomata/formxtra/issues" + }, + "author": "TheoryOfNekomata ", + "publishConfig": { + "access": "public" + } } diff --git a/pridepack.json b/pridepack.json new file mode 100644 index 0000000..841fb58 --- /dev/null +++ b/pridepack.json @@ -0,0 +1,3 @@ +{ + "target": "es2018" +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d622070..5f80805 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,48 +2,50 @@ * Line ending. */ export enum LineEnding { - /** - * Carriage return. - */ - CR = '\r', - /** - * Line feed. - */ - LF = '\n', - /** - * Carriage return/line feed combination. - */ - CRLF = '\r\n', + /** + * Carriage return. Used for legacy Mac OS systems. + */ + CR = '\r', + /** + * Line feed. Used for Linux/*NIX systems as well as newer macOS systems. + */ + LF = '\n', + /** + * Carriage return/line feed combination. Used for Windows systems. + */ + CRLF = '\r\n', } +type PlaceholderObject = Record + /** * Checks if an element can hold a field value. * @param el - The element. */ export const isFormFieldElement = (el: HTMLElement) => { - const { tagName } = el - if (['SELECT', 'TEXTAREA'].includes(tagName)) { - return true - } - if (tagName !== 'INPUT') { - return false - } - const inputEl = el as HTMLInputElement - const { type } = inputEl - if (type === 'submit' || type === 'reset') { - return false - } - return Boolean(inputEl.name) -} + const { tagName } = el; + if (['SELECT', 'TEXTAREA'].includes(tagName)) { + return true; + } + if (tagName !== 'INPUT') { + return false; + } + const inputEl = el as HTMLInputElement; + const { type } = inputEl; + if (type === 'submit' || type === 'reset') { + return false; + } + return Boolean(inputEl.name); +}; /** * Options for getting a `