@@ -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' | |||
describe('checkbox', () => { | |||
@@ -91,5 +91,78 @@ describe('checkbox', () => { | |||
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' | |||
describe('text', () => { | |||
@@ -158,7 +158,7 @@ describe('text', () => { | |||
}); | |||
}); | |||
describe('programmatical value setting', () => { | |||
describe('programmatic value setting', () => { | |||
beforeEach(utils.setup(` | |||
<!DOCTYPE html> | |||
<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' | |||
type ExpectedSearchValue = Record<string, string> | string | |||
type ExpectedSearchValue = any | |||
type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any | |||
@@ -105,7 +105,21 @@ export const makeSearchParams = (beforeValues: Record<string, unknown> | 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 | |||
}, | |||
@@ -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 | |||
@@ -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 `<input>` 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 `<input type="file">` 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<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 | |||
* 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<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 | |||
.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]); | |||
}); | |||
}; | |||