From 5fa727bf45fff4fee408df76add1e04c16428aa9 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sun, 12 Mar 2023 15:20:52 +0800 Subject: [PATCH] Make coverage 100% Include all tests for the primary functions as well as the utilities. --- cypress/integration/checkbox.test.ts | 220 ++++++++++++++++- cypress/integration/date.test.ts | 13 + cypress/integration/datetime-local.test.ts | 13 + cypress/integration/misc.test.ts | 216 ++++++++++++++++- cypress/integration/month.test.ts | 13 + cypress/integration/select.test.ts | 187 +++++++++++++- cypress/integration/time.test.ts | 265 ++++++++++++++++++++ src/index.ts | 268 ++++++++++++++------- 8 files changed, 1109 insertions(+), 86 deletions(-) create mode 100644 cypress/integration/time.test.ts diff --git a/cypress/integration/checkbox.test.ts b/cypress/integration/checkbox.test.ts index ca291ac..a360caa 100644 --- a/cypress/integration/checkbox.test.ts +++ b/cypress/integration/checkbox.test.ts @@ -55,7 +55,7 @@ describe('checkbox', () => { } }); }); - }) + }); describe('checked', () => { beforeEach(utils.setup(` @@ -93,7 +93,6 @@ describe('checkbox', () => { }); }); - describe('duplicate', () => { beforeEach(utils.setup(` @@ -165,4 +164,221 @@ describe('checkbox', () => { }); }); }); + + describe('setting values', () => { + beforeEach(utils.setup(` + + + + + Checkbox/Setting Values + + +
+ + +
+ + + `)) + + it('should check for boolean "true"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: true, }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).toBe('on'); + }, + expectedStaticValue: 'enabled=on', + }); + }); + + it('should check for string "true"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'true', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).toBe('on'); + }, + expectedStaticValue: 'enabled=on', + }); + }); + + it('should check for string "yes"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'yes', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).toBe('on'); + }, + expectedStaticValue: 'enabled=on', + }); + }); + + it('should check for string "on"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'on', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).toBe('on'); + }, + expectedStaticValue: 'enabled=on', + }); + }); + + it('should uncheck for boolean "false"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: false, }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should uncheck for string "false"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'false', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should uncheck for string "no"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'no', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should uncheck for string "off"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'off', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should check for number "1"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 1, }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).toBe('on'); + }, + expectedStaticValue: 'enabled=on', + }); + }); + + it('should check for string "1"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: '1', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).toBe('on'); + }, + expectedStaticValue: 'enabled=on', + }); + }); + + it('should uncheck for number "0"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 0, }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should uncheck for string "0"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: '0', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should uncheck for object "null"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: null, }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + + it('should uncheck for string "null"', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { enabled: 'null', }) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter }) + expect(values['enabled']).not.toBe('on'); + }, + expectedStaticValue: '', + }); + }); + }); }); diff --git a/cypress/integration/date.test.ts b/cypress/integration/date.test.ts index 43b91d4..54d1c66 100644 --- a/cypress/integration/date.test.ts +++ b/cypress/integration/date.test.ts @@ -38,6 +38,19 @@ describe('date', () => { }, }); }); + + it('should enable Date representation', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter, forceDateValues: true }); + // somehow, checking instanceof Date fails here, because we're using an artificial date + // object? + const testDate = new Date(values.hello as Date); + expect((values.hello as Date).getTime()).toBe(testDate.getTime()); + }, + }); + }); }) describe('disabled', () => { diff --git a/cypress/integration/datetime-local.test.ts b/cypress/integration/datetime-local.test.ts index c920605..6642731 100644 --- a/cypress/integration/datetime-local.test.ts +++ b/cypress/integration/datetime-local.test.ts @@ -38,6 +38,19 @@ describe('date', () => { }, }); }); + + it('should enable Date representation', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter, forceDateValues: true }); + // somehow, checking instanceof Date fails here, because we're using an artificial date + // object? + const testDate = new Date(values.hello as Date); + expect((values.hello as Date).getTime()).toBe(testDate.getTime()); + }, + }); + }); }) describe('disabled', () => { diff --git a/cypress/integration/misc.test.ts b/cypress/integration/misc.test.ts index 24f969f..e13ac04 100644 --- a/cypress/integration/misc.test.ts +++ b/cypress/integration/misc.test.ts @@ -1,4 +1,10 @@ -import getFormValuesDeprecated, { getFormValues, setFormValues } from '../../src'; +import getFormValuesDeprecated, { + getFormValues, + setFormValues, + isFieldElement, + isElementValueIncludedInFormSubmit, + getValue, +} from '../../src'; import * as utils from '../utils' describe('misc', () => { @@ -177,7 +183,213 @@ describe('misc', () => { }, }); }); - }) + }); + + describe('utilities', () => { + beforeEach(utils.setup(` + + + + + Misc/Utilities + + +
+ + + + + + + + + `)); + + it('should check for valid field elements value', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const meter = document.getElementById('meter'); + expect(getValue(meter)).toBe(5); + }, + }); + }); + + it('should check for invalid field elements value', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + expect(getValue(document.body)).toBe(null); + }, + }); + }); + + it('should check for elements as included fields', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const input = document.getElementById('input'); + expect(isElementValueIncludedInFormSubmit(input)).toBe(true); + }, + }); + }); + + it('should check for elements as excluded fields', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const notField = document.getElementById('notField'); + expect(isElementValueIncludedInFormSubmit(notField)).toBe(false); + const disabled = document.getElementById('disabled'); + expect(isElementValueIncludedInFormSubmit(disabled)).toBe(false); + const meter = document.getElementById('meter'); + expect(isElementValueIncludedInFormSubmit(meter)).toBe(false); + }, + }); + }); + + it('should check for elements as valid for fields', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const input = document.getElementById('input'); + expect(isFieldElement(input)).toBe(true); + const disabled = document.getElementById('disabled'); + expect(isFieldElement(disabled)).toBe(true); + }, + }); + }); + + it('should check for elements as invalid for fields', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const meter = document.getElementById('meter'); + expect(isFieldElement(meter)).toBe(false); + const notField = document.getElementById('notField'); + expect(isFieldElement(notField)).toBe(false); + }, + }); + }); + }); + + describe('setting values', () => { + beforeEach(utils.setup(` + + + + + Misc/Blank + + +
+ + +
+ + + `)) + + it('should parse string values for setFormValues', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + let isThrown = false; + try { + setFormValues(form, 'foobar=baz'); + } catch (e) { + isThrown = true; + } + + expect(isThrown).toBe(false); + expect(getFormValues(form)).toEqual({ foobar: 'baz', }); + }, + }) + }); + + it('should parse entries values for setFormValues', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + let isThrown = false; + try { + setFormValues(form, [['foobar', 'baz']]); + } catch (e) { + isThrown = true; + } + + expect(isThrown).toBe(false); + expect(getFormValues(form)).toEqual({ foobar: 'baz', }); + }, + }) + }); + + it('should parse URLSearchParams values for setFormValues', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + let isThrown = false; + try { + setFormValues(form, new URLSearchParams('foobar=baz')); + } catch (e) { + isThrown = true; + } + + expect(isThrown).toBe(false); + expect(getFormValues(form)).toEqual({ foobar: 'baz', }); + }, + }) + }); + + it('should parse object values for setFormValues', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + let isThrown = false; + try { + setFormValues(form, { foobar: 'baz', }); + } catch (e) { + isThrown = true; + } + + expect(isThrown).toBe(false); + expect(getFormValues(form)).toEqual({ foobar: 'baz', }); + }, + }) + }); + }); + + describe('duplicates', () => { + beforeEach(utils.setup(` + + + + + Misc/Blank + + +
+ + + + +
+ + + `)); + + it('should parse duplicates correctly', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { foobar: ['foo', 'bar', 'baz']}) + }, + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + expect(getFormValues(form)).toEqual({ foobar: ['foo', 'bar', 'baz'], }); + }, + }) + }); + }); describe('blank', () => { beforeEach(utils.setup(` diff --git a/cypress/integration/month.test.ts b/cypress/integration/month.test.ts index 765cfea..1a5096a 100644 --- a/cypress/integration/month.test.ts +++ b/cypress/integration/month.test.ts @@ -38,6 +38,19 @@ describe('month', () => { }, }); }); + + it('should enable Date representation', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + test: (form: HTMLFormElement, submitter: any, search: any) => { + const values = getFormValues(form, { submitter, forceDateValues: true }); + // somehow, checking instanceof Date fails here, because we're using an artificial date + // object? + const testDate = new Date(values.hello as Date); + expect((values.hello as Date).getTime()).toBe(testDate.getTime()); + }, + }); + }); }) describe('disabled', () => { diff --git a/cypress/integration/select.test.ts b/cypress/integration/select.test.ts index 1c893ec..453cce4 100644 --- a/cypress/integration/select.test.ts +++ b/cypress/integration/select.test.ts @@ -1,4 +1,4 @@ -import { getFormValues } from '../../src' +import { getFormValues, setFormValues } from '../../src'; import * as utils from '../utils' describe('select', () => { @@ -41,6 +41,107 @@ describe('select', () => { expectedStaticValue: 'hello=Bar&hello=Quux' }); }); + + it('should set values correctly', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: ['Foo', 'Baz'] }); + }, + 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=Foo&hello=Baz' + }); + }); + }) + + describe('multiple duplicate', () => { + beforeEach(utils.setup(` + + + + + Select/Multiple Duplicate + + +
+ + +
+ + + `)) + + it('should have multiple form values on a single field', () => { + 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=Bar&hello=Quux&hello=Mango&hello=Ube' + }); + }); + + it('should set multiple form values across all selects', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: ['Foo', 'Baz', 'Chocolate', 'Vanilla'] }) + }, + 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=Foo&hello=Baz&hello=Chocolate&hello=Vanilla' + }); + }); + + it('should set multiple form values on each corresponding select element', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: [['Foo', 'Baz', 'Chocolate'], ['Vanilla']] }) + }, + 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=Foo&hello=Baz&hello=Vanilla' + }); + }); }) describe('single', () => { @@ -84,4 +185,88 @@ describe('select', () => { }); }); }) + + describe('single duplicate', () => { + beforeEach(utils.setup(` + + + + + Select/Single Duplicate + + +
+ + +
+ + + `)) + + it('should have multiple form values on a single field', () => { + 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=Bar&hello=Ube' + }); + }); + + it('should set multiple form values across all selects', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: ['Foo', 'Chocolate'] }) + }, + 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=Foo&hello=Chocolate' + }); + }); + + it('should set multiple form values on each corresponding select element', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: ['Foo', 'Ube'] }) + }, + 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=Foo&hello=Ube' + }); + }); + }) }) diff --git a/cypress/integration/time.test.ts b/cypress/integration/time.test.ts new file mode 100644 index 0000000..2eeb884 --- /dev/null +++ b/cypress/integration/time.test.ts @@ -0,0 +1,265 @@ +import { getFormValues, setFormValues } from '../../src'; +import * as utils from '../utils' + +describe('time', () => { + describe('basic', () => { + beforeEach(utils.setup(` + + + + + Time/Basic + + +
+ + +
+ + + `)) + + it('should have single form value', () => { + 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: '13:37', + }, + }); + }); + }) + + describe('disabled', () => { + beforeEach(utils.setup(` + + + + + Time/Disabled + + +
+ + +
+ + + `)) + + it('should have blank form value', () => { + 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: {}, + }); + }); + }) + + describe('outside', () => { + beforeEach(utils.setup(` + + + + + Time/Outside + + +
+ +
+ + + + `)) + + it('should have single form value', () => { + 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: '13:37', + }, + }); + }); + }); + + describe('readonly', () => { + beforeEach(utils.setup(` + + + + + Time/Readonly + + +
+ + +
+ + + `)) + + it('should have single form value', () => { + 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: '13:37', + }, + }); + }); + }); + + describe('programmatic value setting', () => { + beforeEach(utils.setup(` + + + + + Time/Programmatic Value Setting + + +
+ + +
+ + + `)); + + it('should have form values set', () => { + utils.test({ + action: (cy: any) => cy.get('[type="submit"]'), + preAction: (form: HTMLFormElement) => { + setFormValues(form, { hello: '13:37', }) + }, + 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: '13:37', + }, + }); + }); + }); + + describe('duplicate', () => { + beforeEach(utils.setup(` + + + + + Time/Duplicate + + +
+ + + +
+ + + `)); + + it('should get both values', () => { + 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: ['13:37', '06:09'], + }, + }); + }); + + it('should set both values', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { + hello: ['04:20', '05:30'], + }) + }, + 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: ['04:20', '05:30'], + }, + }); + }); + }); +}) diff --git a/src/index.ts b/src/index.ts index a4c9d23..0376d76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,8 +44,9 @@ const FORM_FIELD_INPUT_EXCLUDED_TYPES = ['submit', 'reset'] as const; /** * Checks if an element can hold a custom (user-inputted) field value. * @param el - The element. + * @returns Value determining if an element can hold a custom (user-inputted) field value. */ -export const isFormFieldElement = (el: HTMLElement) => { +export const isFieldElement = (el: HTMLElement) => { const { tagName } = el; if (FORM_FIELD_ELEMENT_TAG_NAMES.includes(tagName as typeof FORM_FIELD_ELEMENT_TAG_NAMES[0])) { return true; @@ -92,15 +93,15 @@ const getTextAreaFieldValue = ( * @param textareaEl - The element. * @param value - Value of the textarea element. * @param nthOfName - What order is this field in with respect to fields of the same name? - * @param totalOfName - How many fields with the same name are in the form? + * @param elementsOfSameName - How many fields with the same name are in the form? */ const setTextAreaFieldValue = ( textareaEl: HTMLTextAreaElement, value: unknown, nthOfName: number, - totalOfName: number, + elementsOfSameName: HTMLTextAreaElement[], ) => { - if (Array.isArray(value) && totalOfName > 1) { + if (Array.isArray(value) && elementsOfSameName.length > 1) { // eslint-disable-next-line no-param-reassign textareaEl.value = value[nthOfName]; return; @@ -128,19 +129,54 @@ const getSelectFieldValue = ( * Sets the value of a ` element. + const currentValue = valueArray[nthOfName] as string[]; + Array.from(selectEl.options).forEach((el) => { + // eslint-disable-next-line no-param-reassign + el.selected = currentValue.includes(el.value); + }); + return; + } + + // Else we're just checking if these values are in the value array provided. + // They will apply across all select elements. + + if (elementsOfSameName.some((el) => el.multiple)) { + Array.from(selectEl.options).forEach((el) => { + // eslint-disable-next-line no-param-reassign + el.selected = (value as string[]).includes(el.value); + }); + return; + } + + Array.from(selectEl.options).forEach((el) => { // eslint-disable-next-line no-param-reassign - el.selected = true; + el.selected = el.value === (value as string[])[nthOfName]; }); + + return; + } + + Array.from(selectEl.options).forEach((el) => { + // eslint-disable-next-line no-param-reassign + el.selected = Array.isArray(value) + ? (value as string[]).includes(el.value) + : el.value === value; + }); }; /** @@ -248,6 +284,45 @@ const getInputCheckboxFieldValue = ( return null; }; +const parseBooleanValues = (value: unknown) => { + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + const normalizedValue = value.toLowerCase(); + if (INPUT_CHECKBOX_FALSY_VALUES.includes( + normalizedValue as typeof INPUT_CHECKBOX_FALSY_VALUES[0], + )) { + return false; + } + + if (INPUT_CHECKBOX_TRUTHY_VALUES.includes( + normalizedValue as typeof INPUT_CHECKBOX_TRUTHY_VALUES[0], + )) { + return true; + } + } + + if (typeof value === 'number') { + if (value === 0) { + return false; + } + + if (value === 1) { + return true; + } + } + + if (typeof value === 'object') { + if (value === null) { + return false; + } + } + + return undefined; +}; + /** * Sets the value of an `` element. * @param inputEl - The element. @@ -269,26 +344,10 @@ const setInputCheckboxFieldValue = ( return; } - if ( - INPUT_CHECKBOX_FALSY_VALUES.includes( - (value as string).toLowerCase() as typeof INPUT_CHECKBOX_FALSY_VALUES[0], - ) - || !value - ) { - // eslint-disable-next-line no-param-reassign - inputEl.checked = false; - return; - } - - if ( - INPUT_CHECKBOX_TRUTHY_VALUES.includes( - (value as string).toLowerCase() as typeof INPUT_CHECKBOX_TRUTHY_VALUES[0], - ) - || value === true - || value === 1 - ) { + const newValue = parseBooleanValues(value); + if (typeof newValue === 'boolean') { // eslint-disable-next-line no-param-reassign - inputEl.checked = true; + inputEl.checked = newValue; } }; @@ -324,9 +383,6 @@ const getInputFileFieldValue = ( options = {} as GetInputFileFieldValueOptions, ) => { const { files } = inputEl; - if ((files as unknown) === null) { - return null; - } if (options.getFileObjects) { return files; } @@ -395,17 +451,17 @@ const getInputNumericFieldValue = ( * @param inputEl - The element. * @param value - Value of the input element. * @param nthOfName - What order is this field in with respect to fields of the same name? - * @param totalOfName - How many fields with the same name are in the form? + * @param elementsWithSameName - How many fields with the same name are in the form? */ const setInputNumericFieldValue = ( inputEl: HTMLInputNumericElement, value: unknown, nthOfName: number, - totalOfName: number, + elementsWithSameName: HTMLInputNumericElement[], ) => { const valueArray = Array.isArray(value) ? value : [value]; // eslint-disable-next-line no-param-reassign - inputEl.valueAsNumber = Number(valueArray[totalOfName > 1 ? nthOfName : 0]); + inputEl.valueAsNumber = Number(valueArray[elementsWithSameName.length > 1 ? nthOfName : 0]); }; /** @@ -472,7 +528,11 @@ const getInputDateLikeFieldValue = ( options = {} as GetInputDateFieldValueOptions, ) => { if (options.forceDateValues) { - return inputEl.valueAsDate; + return ( + // somehow datetime-local does not return us the current `valueAsDate` when the string + // representation in `value` is incomplete. + inputEl.type === INPUT_TYPE_DATETIME_LOCAL ? new Date(inputEl.value) : inputEl.valueAsDate + ); } return inputEl.value; }; @@ -492,20 +552,22 @@ const DATE_FORMAT_ISO_MONTH = 'yyyy-MM' as const; * @param inputEl - The element. * @param value - Value of the input element. * @param nthOfName - What order is this field in with respect to fields of the same name? - * @param totalOfName - How many fields with the same name are in the form? + * @param elementsOfSameName - How many fields with the same name are in the form? */ const setInputDateLikeFieldValue = ( inputEl: HTMLInputDateLikeElement, value: unknown, nthOfName: number, - totalOfName: number, + elementsOfSameName: HTMLInputDateLikeElement[], ) => { const valueArray = Array.isArray(value) ? value : [value]; + const hasMultipleElementsOfSameName = elementsOfSameName.length > 1; + const elementIndex = hasMultipleElementsOfSameName ? nthOfName : 0; if (inputEl.type.toLowerCase() === INPUT_TYPE_DATE) { // eslint-disable-next-line no-param-reassign inputEl.value = new Date( - valueArray[totalOfName > 1 ? nthOfName : 0] as ConstructorParameters[0], + valueArray[elementIndex] as ConstructorParameters[0], ) .toISOString() .slice(0, DATE_FORMAT_ISO_DATE.length); @@ -515,7 +577,7 @@ const setInputDateLikeFieldValue = ( if (inputEl.type.toLowerCase() === INPUT_TYPE_DATETIME_LOCAL) { // eslint-disable-next-line no-param-reassign inputEl.value = new Date( - valueArray[totalOfName > 1 ? nthOfName : 0] as ConstructorParameters[0], + valueArray[elementIndex] as ConstructorParameters[0], ) .toISOString() .slice(0, -1); // remove extra 'Z' suffix @@ -524,7 +586,7 @@ const setInputDateLikeFieldValue = ( if (inputEl.type.toLowerCase() === INPUT_TYPE_MONTH) { // eslint-disable-next-line no-param-reassign inputEl.value = new Date( - valueArray[totalOfName > 1 ? nthOfName : 0] as ConstructorParameters[0], + valueArray[elementIndex] as ConstructorParameters[0], ) .toISOString() .slice(0, DATE_FORMAT_ISO_MONTH.length); // remove extra 'Z' suffix @@ -580,6 +642,11 @@ const INPUT_TYPE_HIDDEN = 'hidden' as const; */ const INPUT_TYPE_COLOR = 'color' as const; +/** + * Value of the `type` attribute for `` elements considered as time pickers. + */ +const INPUT_TYPE_TIME = 'time' as const; + /** * Gets the value of an `` element. * @param inputEl - The element. @@ -612,12 +679,13 @@ const getInputFieldValue = ( case INPUT_TYPE_PASSWORD: case INPUT_TYPE_HIDDEN: case INPUT_TYPE_COLOR: + case INPUT_TYPE_TIME: default: break; } - // force return `null` for custom elements supporting setting values. - return inputEl.value ?? null; + // don't force returning `null` for custom elements supporting setting values. + return inputEl.value; }; /** @@ -625,7 +693,7 @@ const getInputFieldValue = ( * @param inputEl - The element. * @param value - Value of the input element. * @param nthOfName - What order is this field in with respect to fields of the same name? - * @param totalOfName - How many fields with the same name are in the form? + * @param elementsWithSameName - How many fields with the same name are in the form? * @note This function is a noop for `` because by design, file inputs are not * assignable programmatically. */ @@ -633,7 +701,7 @@ const setInputFieldValue = ( inputEl: HTMLInputElement, value: unknown, nthOfName: number, - totalOfName: number, + elementsWithSameName: HTMLInputElement[], ) => { switch (inputEl.type.toLowerCase()) { case INPUT_TYPE_CHECKBOX: @@ -651,7 +719,7 @@ const setInputFieldValue = ( inputEl as HTMLInputNumericElement, value, nthOfName, - totalOfName, + elementsWithSameName as HTMLInputNumericElement[], ); return; case INPUT_TYPE_DATE: @@ -661,7 +729,7 @@ const setInputFieldValue = ( inputEl as HTMLInputDateLikeElement, value, nthOfName, - totalOfName, + elementsWithSameName as HTMLInputDateLikeElement[], ); return; case INPUT_TYPE_TEXT: @@ -672,11 +740,12 @@ const setInputFieldValue = ( case INPUT_TYPE_PASSWORD: case INPUT_TYPE_HIDDEN: case INPUT_TYPE_COLOR: + case INPUT_TYPE_TIME: default: break; } - if (Array.isArray(value) && totalOfName > 1) { + if (Array.isArray(value) && elementsWithSameName.length > 1) { // eslint-disable-next-line no-param-reassign inputEl.value = value[nthOfName]; return; @@ -700,23 +769,24 @@ type HTMLElementWithName = (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement); /** - * Gets the value of a field element. + * Gets the value of an element regardless if it's a field element or not. * @param el - The field element. * @param options - The options. - * @returns Value of the field element. + * @returns Value of the element. */ -export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => { +export const getValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => { switch (el.tagName) { case TAG_NAME_TEXTAREA: return getTextAreaFieldValue(el as HTMLTextAreaElement, options); case TAG_NAME_SELECT: return getSelectFieldValue(el as HTMLSelectElement); case TAG_NAME_INPUT: + return getInputFieldValue(el as HTMLInputElement, options); default: break; } - return getInputFieldValue(el as HTMLInputElement, options); + return 'value' in el ? el.value : null; }; /** @@ -724,27 +794,42 @@ export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOpti * @param el - The field element. * @param value - Value of the field element. * @param nthOfName - What order is this field in with respect to fields of the same name? - * @param totalOfName - How many fields with the same name are in the form? + * @param elementsWithSameName - How many fields with the same name are in the form? */ const setFieldValue = ( el: HTMLElement, value: unknown, nthOfName: number, - totalOfName: number, + elementsWithSameName: HTMLElement[], ) => { switch (el.tagName) { case TAG_NAME_TEXTAREA: - setTextAreaFieldValue(el as HTMLTextAreaElement, value, nthOfName, totalOfName); + setTextAreaFieldValue( + el as HTMLTextAreaElement, + value, + nthOfName, + elementsWithSameName as HTMLTextAreaElement[], + ); return; case TAG_NAME_SELECT: - setSelectFieldValue(el as HTMLSelectElement, value); + setSelectFieldValue( + el as HTMLSelectElement, + value, + nthOfName, + elementsWithSameName as HTMLSelectElement[], + ); return; case TAG_NAME_INPUT: default: break; } - setInputFieldValue(el as HTMLInputElement, value, nthOfName, totalOfName); + setInputFieldValue( + el as HTMLInputElement, + value, + nthOfName, + elementsWithSameName as HTMLInputElement[], + ); }; /** @@ -758,17 +843,17 @@ const ATTRIBUTE_NAME = 'name' as const; const ATTRIBUTE_DISABLED = 'disabled' as const; /** - * Determines if an element is a named and enabled form field. + * Determines if an element's value is included when its form is submitted. * @param el - The element. - * @returns Value determining if the element is a named and enabled form field. + * @returns Value determining if the element's value is included when its form is submitted. */ -export const isNamedEnabledFormFieldElement = (el: HTMLElement) => { +export const isElementValueIncludedInFormSubmit = (el: HTMLElement) => { const namedEl = el as unknown as Record; return ( typeof namedEl[ATTRIBUTE_NAME] === 'string' && namedEl[ATTRIBUTE_NAME].length > 0 && !(ATTRIBUTE_DISABLED in namedEl && Boolean(namedEl[ATTRIBUTE_DISABLED])) - && isFormFieldElement(namedEl as unknown as HTMLElement) + && isFieldElement(namedEl as unknown as HTMLElement) ); }; @@ -829,7 +914,7 @@ const filterFieldElements = (form: HTMLFormElement) => { !Number.isNaN(Number(k)) // Only the enabled/read-only elements can be enumerated. - && isNamedEnabledFormFieldElement(el) + && isElementValueIncludedInFormSubmit(el) )) as [string, HTMLElementWithName][]; }; @@ -845,7 +930,7 @@ export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValu const fieldElements = filterFieldElements(form); const fieldValues = fieldElements.reduce( (theFormValues, [, el]) => { - const fieldValue = getFieldValue(el, options); + const fieldValue = getValue(el, options); if (fieldValue === null) { return theFormValues; } @@ -853,24 +938,35 @@ export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValu const { name: fieldName } = el; const { [fieldName]: oldFormValue = null } = theFormValues; - if (oldFormValue === null) { + if (oldFormValue !== null && !Array.isArray(oldFormValue)) { return { ...theFormValues, - [fieldName]: fieldValue, + [fieldName]: [oldFormValue, fieldValue], }; } - if (!Array.isArray(oldFormValue)) { + if (Array.isArray(oldFormValue)) { + if (Array.isArray(fieldValue)) { + return { + ...theFormValues, + [fieldName]: [...oldFormValue, ...fieldValue], + }; + } return { ...theFormValues, - [fieldName]: [oldFormValue, fieldValue], + [fieldName]: [...oldFormValue, fieldValue], }; } return { ...theFormValues, - [fieldName]: [...oldFormValue, fieldValue], + [fieldName]: fieldValue, }; + + // return { + // ...theFormValues, + // [fieldName]: [...oldFormValue, fieldValue], + // }; }, {} as Record, ); @@ -928,36 +1024,46 @@ export const setFormValues = ( const fieldElements = filterFieldElements(form); const objectValues = normalizeValues(values); - const count = fieldElements + const elementsWithSameName = fieldElements .filter(([, el]) => el.name in objectValues) .reduce( (currentCount, [, el]) => { if (el.tagName === TAG_NAME_INPUT && el.type === INPUT_TYPE_RADIO) { return { ...currentCount, - [el.name]: 1, + [el.name]: [el], }; } return { ...currentCount, [el.name]: ( - typeof currentCount[el.name] === 'number' - ? currentCount[el.name] + 1 - : 1 + Array.isArray(currentCount[el.name]) + ? [...currentCount[el.name], el] + : [el] ), }; }, - {} as Record, + {} as Record, ); - const counter = {} as Record; + const nthElementOfName = {} as Record; fieldElements .filter(([, el]) => el.name in objectValues) .forEach(([, el]) => { - counter[el.name] = typeof counter[el.name] === 'number' ? counter[el.name] + 1 : 0; - setFieldValue(el, objectValues[el.name], counter[el.name], count[el.name]); + nthElementOfName[el.name] = ( + typeof nthElementOfName[el.name] === 'number' + ? nthElementOfName[el.name] + 1 + : 0 + ); + + setFieldValue( + el, + objectValues[el.name], + nthElementOfName[el.name], + elementsWithSameName[el.name], + ); }); };