@@ -1,3 +1,4 @@ | |||||
{ | { | ||||
"video": false | |||||
"video": false, | |||||
"screenshotOnRunFailure": false | |||||
} | } |
@@ -1,4 +1,4 @@ | |||||
import { getFormValues } from '../../src' | |||||
import { getFormValues, setFormValues } from '../../src'; | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('checkbox', () => { | describe('checkbox', () => { | ||||
@@ -91,5 +91,78 @@ describe('checkbox', () => { | |||||
expectedStaticValue: 'enabled=on', | expectedStaticValue: 'enabled=on', | ||||
}); | }); | ||||
}); | }); | ||||
}) | |||||
}) | |||||
}); | |||||
describe('duplicate', () => { | |||||
beforeEach(utils.setup(` | |||||
<!DOCTYPE html> | |||||
<html lang="en-PH"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<title>Text/Basic</title> | |||||
</head> | |||||
<body> | |||||
<form> | |||||
<label> | |||||
<span>Hello 1</span> | |||||
<input type="checkbox" name="enabled" value="hello 1" checked /> | |||||
</label> | |||||
<label> | |||||
<span>Hello 2</span> | |||||
<input type="checkbox" name="enabled" value="hello 2" checked /> | |||||
</label> | |||||
<label> | |||||
<span>Hello 3</span> | |||||
<input type="checkbox" name="enabled" value="hello 3" /> | |||||
</label> | |||||
<label> | |||||
<span>Hello 4</span> | |||||
<input type="checkbox" name="enabled" value="hello 4" /> | |||||
</label> | |||||
<button type="submit">Submit</button> | |||||
</form> | |||||
</body> | |||||
</html> | |||||
`)); | |||||
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'], | |||||
}, | |||||
}); | |||||
}); | |||||
}); | |||||
}); |
@@ -1,4 +1,4 @@ | |||||
import { getFormValues, setFormValues } from '../../src'; | |||||
import { getFormValues, LineEnding, setFormValues } from '../../src'; | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('text', () => { | describe('text', () => { | ||||
@@ -158,7 +158,7 @@ describe('text', () => { | |||||
}); | }); | ||||
}); | }); | ||||
describe('programmatical value setting', () => { | |||||
describe('programmatic value setting', () => { | |||||
beforeEach(utils.setup(` | beforeEach(utils.setup(` | ||||
<!DOCTYPE html> | <!DOCTYPE html> | ||||
<html lang="en-PH"> | <html lang="en-PH"> | ||||
@@ -199,5 +199,151 @@ describe('text', () => { | |||||
}); | }); | ||||
}); | }); | ||||
// TODO implement textarea tests | |||||
describe('textarea', () => { | |||||
beforeEach(utils.setup(` | |||||
<!DOCTYPE html> | |||||
<html lang="en-PH"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<title>Text/Basic</title> | |||||
</head> | |||||
<body> | |||||
<form> | |||||
<label> | |||||
<span>Hello</span> | |||||
<textarea name="hello"></textarea> | |||||
</label> | |||||
<button type="submit">Submit</button> | |||||
</form> | |||||
</body> | |||||
</html> | |||||
`)); | |||||
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(` | |||||
<!DOCTYPE html> | |||||
<html lang="en-PH"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<title>Text/Basic</title> | |||||
</head> | |||||
<body> | |||||
<form> | |||||
<label> | |||||
<span>Hello 1</span> | |||||
<input id="hello1" type="text" value="value" name="hello"/> | |||||
</label> | |||||
<label> | |||||
<span>Hello 2</span> | |||||
<input id="hello2" type="text" value="another value" name="hello"/> | |||||
</label> | |||||
<button type="submit">Submit</button> | |||||
</form> | |||||
</body> | |||||
</html> | |||||
`)); | |||||
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'], | |||||
}, | |||||
}); | |||||
}); | |||||
}); | |||||
}) | }) |
@@ -2,7 +2,7 @@ | |||||
import JSDOMDummyCypress from './jsdom-compat' | import JSDOMDummyCypress from './jsdom-compat' | ||||
type ExpectedSearchValue = Record<string, string> | string | |||||
type ExpectedSearchValue = any | |||||
type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any | type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any | ||||
@@ -105,7 +105,21 @@ export const makeSearchParams = (beforeValues: Record<string, unknown> | string) | |||||
(beforeSearchParams, [key, value]) => { | (beforeSearchParams, [key, value]) => { | ||||
const theValue = !Array.isArray(value) ? [value] : value | const theValue = !Array.isArray(value) ? [value] : value | ||||
theValue.forEach(v => { | 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 | return beforeSearchParams | ||||
}, | }, | ||||
@@ -9,7 +9,8 @@ class JSDOMJQuery { | |||||
this.selectedElements = Array.from(elements) | this.selectedElements = Array.from(elements) | ||||
} | } | ||||
type(s: string) { | |||||
type(sRaw: string) { | |||||
const s = sRaw.replace(/\{enter}/g, '\n'); | |||||
this.selectedElements.forEach((el: any) => { | this.selectedElements.forEach((el: any) => { | ||||
if (el.tagName === 'TEXTAREA') { | if (el.tagName === 'TEXTAREA') { | ||||
el.innerText = s | el.innerText = s | ||||
@@ -3,7 +3,7 @@ | |||||
*/ | */ | ||||
export enum LineEnding { | export enum LineEnding { | ||||
/** | /** | ||||
* Carriage return. Used for legacy Mac OS systems. | |||||
* Carriage return. Used for legacy macOS systems. | |||||
*/ | */ | ||||
CR = '\r', | CR = '\r', | ||||
/** | /** | ||||
@@ -274,10 +274,15 @@ const setInputCheckboxFieldValue = ( | |||||
inputEl: HTMLInputCheckboxElement, | inputEl: HTMLInputCheckboxElement, | ||||
value: unknown, | 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 | // eslint-disable-next-line no-param-reassign | ||||
inputEl.checked = value === checkedValue; | |||||
inputEl.checked = ( | |||||
Array.isArray(value) | |||||
? value.includes(valueWhenChecked) | |||||
: value === valueWhenChecked | |||||
); | |||||
return; | return; | ||||
} | } | ||||
@@ -542,12 +547,16 @@ const getInputFieldValue = ( | |||||
* Sets the value of an `<input>` element. | * Sets the value of an `<input>` element. | ||||
* @param inputEl - The element. | * @param inputEl - The element. | ||||
* @param value - Value of the input 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 `<input type="file">` because by design, file inputs are not | * @note This function is a noop for `<input type="file">` because by design, file inputs are not | ||||
* assignable programmatically. | * assignable programmatically. | ||||
*/ | */ | ||||
const setInputFieldValue = ( | const setInputFieldValue = ( | ||||
inputEl: HTMLInputElement, | inputEl: HTMLInputElement, | ||||
value: unknown, | value: unknown, | ||||
nthOfName: number, | |||||
totalOfName: number, | |||||
) => { | ) => { | ||||
switch (inputEl.type.toLowerCase()) { | switch (inputEl.type.toLowerCase()) { | ||||
case INPUT_TYPE_CHECKBOX: | case INPUT_TYPE_CHECKBOX: | ||||
@@ -570,6 +579,13 @@ const setInputFieldValue = ( | |||||
default: | default: | ||||
break; | 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 | // eslint-disable-next-line no-param-reassign | ||||
inputEl.value = value as string; | inputEl.value = value as string; | ||||
}; | }; | ||||
@@ -614,8 +630,15 @@ export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOpti | |||||
* Sets the value of a field element. | * Sets the value of a field element. | ||||
* @param el - The field element. | * @param el - The field element. | ||||
* @param value - Value of 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) { | switch (el.tagName) { | ||||
case TAG_NAME_TEXTAREA: | case TAG_NAME_TEXTAREA: | ||||
setTextAreaFieldValue(el as HTMLTextAreaElement, value); | setTextAreaFieldValue(el as HTMLTextAreaElement, value); | ||||
@@ -624,7 +647,7 @@ const setFieldValue = (el: HTMLElement, value: unknown) => { | |||||
setSelectFieldValue(el as HTMLSelectElement, value); | setSelectFieldValue(el as HTMLSelectElement, value); | ||||
return; | return; | ||||
case TAG_NAME_INPUT: | case TAG_NAME_INPUT: | ||||
setInputFieldValue(el as HTMLInputElement, value); | |||||
setInputFieldValue(el as HTMLInputElement, value, nthOfName, totalOfName); | |||||
return; | return; | ||||
default: | default: | ||||
break; | break; | ||||
@@ -780,6 +803,22 @@ export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValu | |||||
return fieldValues; | return fieldValues; | ||||
}; | }; | ||||
const normalizeValues = (values: unknown): Record<string, unknown | unknown[]> => { | |||||
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<string, unknown | unknown[]>; | |||||
}; | |||||
/** | /** | ||||
* Sets the values of all the fields within the form through accessing the DOM nodes. Partial values | * 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. | * may be passed to set values only to certain form fields. | ||||
@@ -802,11 +841,33 @@ export const setFormValues = ( | |||||
} | } | ||||
const fieldElements = filterFieldElements(form); | const fieldElements = filterFieldElements(form); | ||||
const objectValues = new URLSearchParams(values as unknown as string | Record<string, string>); | |||||
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<string, number>, | |||||
); | |||||
const counter = {} as Record<string, number>; | |||||
fieldElements | fieldElements | ||||
.filter(([, el]) => objectValues.has(el.name)) | |||||
.filter(([, el]) => el.name in objectValues) | |||||
.forEach(([, el]) => { | .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]); | |||||
}); | }); | ||||
}; | }; | ||||