From 48166877e47bba0c3f1a707939f463ee33f7e343 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sat, 11 Mar 2023 19:15:21 +0800 Subject: [PATCH] Add support for multiple values Correctly set fields when they are arrays. --- cypress.json | 3 +- cypress/integration/checkbox.test.ts | 79 +++++++++++++- cypress/integration/text.test.ts | 152 ++++++++++++++++++++++++++- cypress/utils/index.ts | 18 +++- cypress/utils/jsdom-compat.ts | 3 +- src/index.ts | 79 ++++++++++++-- 6 files changed, 315 insertions(+), 19 deletions(-) diff --git a/cypress.json b/cypress.json index 60ed5aa..8513da0 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,4 @@ { - "video": false + "video": false, + "screenshotOnRunFailure": false } diff --git a/cypress/integration/checkbox.test.ts b/cypress/integration/checkbox.test.ts index d1fc7a4..62042e8 100644 --- a/cypress/integration/checkbox.test.ts +++ b/cypress/integration/checkbox.test.ts @@ -1,4 +1,4 @@ -import { getFormValues } from '../../src' +import { getFormValues, setFormValues } from '../../src'; import * as utils from '../utils' describe('checkbox', () => { @@ -91,5 +91,78 @@ describe('checkbox', () => { expectedStaticValue: 'enabled=on', }); }); - }) -}) + }); + + + describe('duplicate', () => { + beforeEach(utils.setup(` + + + + + Text/Basic + + +
+ + + + + +
+ + + `)); + + 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: { + enabled: ['hello 1', 'hello 2'], + }, + }); + }); + + it('should set both values', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { + enabled: ['hello 3', 'hello 4'], + }) + }, + 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: { + enabled: ['hello 3', 'hello 4'], + }, + }); + }); + }); +}); diff --git a/cypress/integration/text.test.ts b/cypress/integration/text.test.ts index 84e5df4..df1a626 100644 --- a/cypress/integration/text.test.ts +++ b/cypress/integration/text.test.ts @@ -1,4 +1,4 @@ -import { getFormValues, setFormValues } from '../../src'; +import { getFormValues, LineEnding, setFormValues } from '../../src'; import * as utils from '../utils' describe('text', () => { @@ -158,7 +158,7 @@ describe('text', () => { }); }); - describe('programmatical value setting', () => { + describe('programmatic value setting', () => { beforeEach(utils.setup(` @@ -199,5 +199,151 @@ describe('text', () => { }); }); - // TODO implement textarea tests + describe('textarea', () => { + beforeEach(utils.setup(` + + + + + Text/Basic + + +
+ + +
+ + + `)); + + it('should read LF line breaks', () => { + utils.test({ + action: (cy: any) => { + cy.get('[name="hello"]') + .type('Hi\nHello', { parseSpecialCharSequences: false }) + return cy.get('[type="submit"]') + }, + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter, lineEndings: LineEnding.LF })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); + }, + expectedStaticValue: { + hello: 'Hi\nHello', + }, + }); + }); + + it('should read CR line breaks', () => { + utils.test({ + action: (cy: any) => { + cy.get('[name="hello"]') + .type('Hi\rHello', { parseSpecialCharSequences: false }) + return cy.get('[type="submit"]') + }, + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter, lineEndings: LineEnding.CR })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); + }, + expectedStaticValue: { + hello: 'Hi\rHello', + }, + }); + }); + + it('should read CRLF line breaks', () => { + utils.test({ + action: (cy: any) => { + cy.get('[name="hello"]') + .type('Hi\r\nHello', { parseSpecialCharSequences: false }) + return cy.get('[type="submit"]') + }, + test: (form: HTMLFormElement, submitter: any, search: any) => { + const before = utils.makeSearchParams(getFormValues(form, { submitter, lineEndings: LineEnding.CRLF })) + .toString(); + const after = utils.makeSearchParams(search) + .toString(); + expect(before) + .toEqual(after); + }, + expectedStaticValue: { + hello: 'Hi\r\nHello', + }, + }); + }); + }); + + describe('duplicate', () => { + beforeEach(utils.setup(` + + + + + Text/Basic + + +
+ + + +
+ + + `)); + + 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: ['value', 'another value'], + }, + }); + }); + + it('should set both values', () => { + utils.test({ + preAction: (form: HTMLFormElement) => { + setFormValues(form, { + hello: ['new value 1', 'another value 2'], + }) + }, + 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: ['new value 1', 'another value 2'], + }, + }); + }); + }); }) diff --git a/cypress/utils/index.ts b/cypress/utils/index.ts index 04fcf6a..654437a 100644 --- a/cypress/utils/index.ts +++ b/cypress/utils/index.ts @@ -2,7 +2,7 @@ import JSDOMDummyCypress from './jsdom-compat' -type ExpectedSearchValue = Record | string +type ExpectedSearchValue = any type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any @@ -105,7 +105,21 @@ export const makeSearchParams = (beforeValues: Record | string) (beforeSearchParams, [key, value]) => { const theValue = !Array.isArray(value) ? [value] : value theValue.forEach(v => { - beforeSearchParams.append(key, v) + let processedLineBreaks = v + if (typeof cy !== 'undefined') { + let forceLineBreaks: string; + + // TODO make this foolproof + if (navigator.platform.indexOf("Mac") === 0 || + navigator.platform === "iPhone") { + forceLineBreaks = '\n'; + } else if (navigator.platform === 'Win32') { + forceLineBreaks = '\r\n'; + } + processedLineBreaks = processedLineBreaks + .replace(/(\r\n|\r|\n)/g, forceLineBreaks) + } + beforeSearchParams.append(key, processedLineBreaks) }) return beforeSearchParams }, diff --git a/cypress/utils/jsdom-compat.ts b/cypress/utils/jsdom-compat.ts index cee039b..fdf225d 100644 --- a/cypress/utils/jsdom-compat.ts +++ b/cypress/utils/jsdom-compat.ts @@ -9,7 +9,8 @@ class JSDOMJQuery { this.selectedElements = Array.from(elements) } - type(s: string) { + type(sRaw: string) { + const s = sRaw.replace(/\{enter}/g, '\n'); this.selectedElements.forEach((el: any) => { if (el.tagName === 'TEXTAREA') { el.innerText = s diff --git a/src/index.ts b/src/index.ts index 5b5e507..5926deb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ */ export enum LineEnding { /** - * Carriage return. Used for legacy Mac OS systems. + * Carriage return. Used for legacy macOS systems. */ CR = '\r', /** @@ -274,10 +274,15 @@ const setInputCheckboxFieldValue = ( inputEl: HTMLInputCheckboxElement, value: unknown, ) => { - const checkedValue = inputEl.getAttribute(ATTRIBUTE_VALUE); - if (checkedValue !== null) { + const valueWhenChecked = inputEl.getAttribute(ATTRIBUTE_VALUE); + + if (valueWhenChecked !== null) { // eslint-disable-next-line no-param-reassign - inputEl.checked = value === checkedValue; + inputEl.checked = ( + Array.isArray(value) + ? value.includes(valueWhenChecked) + : value === valueWhenChecked + ); return; } @@ -542,12 +547,16 @@ const getInputFieldValue = ( * Sets the value of an `` element. * @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? * @note This function is a noop for `` because by design, file inputs are not * assignable programmatically. */ const setInputFieldValue = ( inputEl: HTMLInputElement, value: unknown, + nthOfName: number, + totalOfName: number, ) => { switch (inputEl.type.toLowerCase()) { case INPUT_TYPE_CHECKBOX: @@ -570,6 +579,13 @@ const setInputFieldValue = ( default: break; } + + if (Array.isArray(value) && totalOfName > 1) { + // eslint-disable-next-line no-param-reassign + inputEl.value = value[nthOfName]; + return; + } + // eslint-disable-next-line no-param-reassign inputEl.value = value as string; }; @@ -614,8 +630,15 @@ export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOpti * Sets the value of a field element. * @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? */ -const setFieldValue = (el: HTMLElement, value: unknown) => { +const setFieldValue = ( + el: HTMLElement, + value: unknown, + nthOfName: number, + totalOfName: number, +) => { switch (el.tagName) { case TAG_NAME_TEXTAREA: setTextAreaFieldValue(el as HTMLTextAreaElement, value); @@ -624,7 +647,7 @@ const setFieldValue = (el: HTMLElement, value: unknown) => { setSelectFieldValue(el as HTMLSelectElement, value); return; case TAG_NAME_INPUT: - setInputFieldValue(el as HTMLInputElement, value); + setInputFieldValue(el as HTMLInputElement, value, nthOfName, totalOfName); return; default: break; @@ -780,6 +803,22 @@ export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValu return fieldValues; }; +const normalizeValues = (values: unknown): Record => { + if (typeof values === 'string') { + return Object.fromEntries(new URLSearchParams(values).entries()); + } + + if (values instanceof URLSearchParams) { + return Object.fromEntries(values.entries()); + } + + if (Array.isArray(values)) { + return Object.fromEntries(values); + } + + return values as Record; +}; + /** * Sets the values of all the fields within the form through accessing the DOM nodes. Partial values * may be passed to set values only to certain form fields. @@ -802,11 +841,33 @@ export const setFormValues = ( } const fieldElements = filterFieldElements(form); - const objectValues = new URLSearchParams(values as unknown as string | Record); + const objectValues = normalizeValues(values); + + const count = fieldElements + .filter(([, el]) => el.name in objectValues) + .reduce( + (currentCount, [, el]) => ({ + ...currentCount, + [el.name]: ( + el.tagName === TAG_NAME_INPUT && el.type === INPUT_TYPE_RADIO + ? 1 + : ( + typeof currentCount[el.name] === 'number' + ? currentCount[el.name] + 1 + : 1 + ) + ), + }), + {} as Record, + ); + + const counter = {} as Record; + fieldElements - .filter(([, el]) => objectValues.has(el.name)) + .filter(([, el]) => el.name in objectValues) .forEach(([, el]) => { - setFieldValue(el, objectValues.get(el.name)); + counter[el.name] = typeof counter[el.name] === 'number' ? counter[el.name] + 1 : 0; + setFieldValue(el, objectValues[el.name], counter[el.name], count[el.name]); }); };