Browse Source

Add support for multiple values

Correctly set fields when they are arrays.
master
TheoryOfNekomata 1 year ago
parent
commit
48166877e4
6 changed files with 315 additions and 19 deletions
  1. +2
    -1
      cypress.json
  2. +76
    -3
      cypress/integration/checkbox.test.ts
  3. +149
    -3
      cypress/integration/text.test.ts
  4. +16
    -2
      cypress/utils/index.ts
  5. +2
    -1
      cypress/utils/jsdom-compat.ts
  6. +70
    -9
      src/index.ts

+ 2
- 1
cypress.json View File

@@ -1,3 +1,4 @@
{ {
"video": false
"video": false,
"screenshotOnRunFailure": false
} }

+ 76
- 3
cypress/integration/checkbox.test.ts View File

@@ -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'],
},
});
});
});
});

+ 149
- 3
cypress/integration/text.test.ts View File

@@ -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'],
},
});
});
});
}) })

+ 16
- 2
cypress/utils/index.ts View File

@@ -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
}, },


+ 2
- 1
cypress/utils/jsdom-compat.ts View File

@@ -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


+ 70
- 9
src/index.ts View File

@@ -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]);
}); });
}; };




Loading…
Cancel
Save