Skip to content

Commit 5da6c37

Browse files
maxmilcapricorn86
andauthored
fix: [#1859] Only add buttons to FormData if they are the submitter (#1860)
* fix: [#1859] Only add the value of buttons to the form data if they are the submitter * fix: [#1859] Fix failing test Not sure why npm test did not detect that this test needed to be rerun after my changes. * chore: [#1859] Adds review fixes --------- Co-authored-by: David Ortner <[email protected]>
1 parent 45d6948 commit 5da6c37

File tree

4 files changed

+170
-16
lines changed

4 files changed

+170
-16
lines changed

packages/happy-dom/src/form-data/FormData.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import File from '../file/File.js';
44
import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js';
55
import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js';
66
import BrowserWindow from '../window/BrowserWindow.js';
7+
import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js';
8+
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
79

810
type FormDataEntry = {
911
name: string;
@@ -25,12 +27,32 @@ export default class FormData implements Iterable<[string, string | File]> {
2527
* Constructor.
2628
*
2729
* @param [form] Form.
30+
* @param [submitter] The element that triggered the submission if this came from a form submit.
2831
*/
29-
constructor(form?: HTMLFormElement) {
32+
constructor(form?: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement) {
3033
if (!form) {
3134
return;
3235
}
3336

37+
if (submitter) {
38+
const formProxy = form[PropertySymbol.proxy] ? form[PropertySymbol.proxy] : form;
39+
if (submitter.form !== formProxy) {
40+
throw new this[PropertySymbol.window].DOMException(
41+
'The specified element is not owned by this form element',
42+
DOMExceptionNameEnum.notFoundError
43+
);
44+
}
45+
const isSubmitButton =
46+
(submitter[PropertySymbol.tagName] === 'INPUT' &&
47+
(submitter.type === 'submit' || submitter.type === 'image')) ||
48+
(submitter[PropertySymbol.tagName] === 'BUTTON' && submitter.type === 'submit');
49+
if (!isSubmitButton) {
50+
throw new this[PropertySymbol.window].TypeError(
51+
'The specified element is not a submit button'
52+
);
53+
}
54+
}
55+
3456
const items = form[PropertySymbol.getFormControlItems]();
3557

3658
for (const item of items) {
@@ -62,7 +84,7 @@ export default class FormData implements Iterable<[string, string | File]> {
6284
case 'submit':
6385
case 'reset':
6486
case 'button':
65-
if ((<HTMLInputElement>item).value) {
87+
if (item === submitter && (<HTMLInputElement>item).value) {
6688
this.append(name, (<HTMLInputElement>item).value);
6789
}
6890
break;
@@ -72,8 +94,8 @@ export default class FormData implements Iterable<[string, string | File]> {
7294
}
7395
break;
7496
case 'BUTTON':
75-
if ((<HTMLInputElement>item).value) {
76-
this.append(name, (<HTMLInputElement>item).value);
97+
if (item === submitter && (<HTMLButtonElement>item).value) {
98+
this.append(name, (<HTMLButtonElement>item).value);
7799
}
78100
break;
79101
case 'TEXTAREA':

packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ export default class HTMLFormElement extends HTMLElement {
614614
return;
615615
}
616616

617-
const formData = new this[PropertySymbol.window].FormData(this);
617+
const formData = new this[PropertySymbol.window].FormData(this, submitter);
618618
let targetFrame: IBrowserFrame;
619619

620620
switch (submitter?.formTarget || this.target) {

packages/happy-dom/test/form-data/FormData.test.ts

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ describe('FormData', () => {
1717
vi.restoreAllMocks();
1818
});
1919

20+
beforeEach(() => {
21+
window = new Window();
22+
document = window.document;
23+
});
24+
afterEach(() => {
25+
vi.restoreAllMocks();
26+
});
2027
describe('constructor', () => {
21-
it('Supports sending in an HTMLFormElement to the contructor.', () => {
28+
it('Supports sending in an HTMLFormElement to the constructor.', () => {
2229
const form = document.createElement('form');
2330
const file = new File([Buffer.from('fileContent')], 'file.txt', { type: 'text/plain' });
2431
const textInput = document.createElement('input');
@@ -37,33 +44,26 @@ describe('FormData', () => {
3744
textInput.type = 'text';
3845
textInput.name = 'textInput';
3946
textInput.value = 'text value';
40-
4147
hiddenInput.type = 'hidden';
4248
hiddenInput.name = 'hiddenInput';
4349
hiddenInput.value = 'hidden value 1';
44-
4550
hiddenInput2.type = 'hidden';
4651
hiddenInput2.name = 'hiddenInput';
4752
hiddenInput2.value = 'hidden value 2';
48-
4953
fileInput.type = 'file';
5054
fileInput.name = 'fileInput';
5155
fileInput.files.push(file);
52-
5356
radioInput1.type = 'radio';
5457
radioInput1.name = 'radioInput';
5558
radioInput1.value = 'radio value 1';
5659
radioInput1.checked = false;
57-
5860
radioInput2.type = 'radio';
5961
radioInput2.name = 'radioInput';
6062
radioInput2.value = 'radio value 2';
6163
radioInput2.checked = true;
62-
6364
checkboxInput1.type = 'checkbox';
6465
checkboxInput1.name = 'checkboxInput';
6566
checkboxInput1.value = 'checkbox value 1';
66-
6767
checkboxInput2.type = 'checkbox';
6868
checkboxInput2.name = 'checkboxInput';
6969
checkboxInput2.value = 'checkbox value 2';
@@ -106,8 +106,106 @@ describe('FormData', () => {
106106
expect(formData.getAll('checkboxInput')).toEqual(['checkbox value 2']);
107107
expect(formData.get('button1')).toBe(null);
108108
expect(formData.get('button2')).toBe(null);
109-
expect(formData.get('button3')).toBe('button3');
110-
expect(formData.get('button4')).toBe('button4');
109+
expect(formData.get('button3')).toBe(null);
110+
expect(formData.get('button4')).toBe(null);
111+
});
112+
113+
it('Supports sending in an HTMLFormElement and a submitter to the constructor.', () => {
114+
const form = document.createElement('form');
115+
const file = new File([Buffer.from('fileContent')], 'file.txt', { type: 'text/plain' });
116+
const textInput = document.createElement('input');
117+
const hiddenInput = document.createElement('input');
118+
const hiddenInput2 = document.createElement('input');
119+
const fileInput = document.createElement('input');
120+
const radioInput1 = document.createElement('input');
121+
const radioInput2 = document.createElement('input');
122+
const checkboxInput1 = document.createElement('input');
123+
const checkboxInput2 = document.createElement('input');
124+
const button = document.createElement('button');
125+
126+
textInput.type = 'text';
127+
textInput.name = 'textInput';
128+
textInput.value = 'text value';
129+
hiddenInput.type = 'hidden';
130+
hiddenInput.name = 'hiddenInput';
131+
hiddenInput.value = 'hidden value 1';
132+
hiddenInput2.type = 'hidden';
133+
hiddenInput2.name = 'hiddenInput';
134+
hiddenInput2.value = 'hidden value 2';
135+
fileInput.type = 'file';
136+
fileInput.name = 'fileInput';
137+
fileInput.files.push(file);
138+
radioInput1.type = 'radio';
139+
radioInput1.name = 'radioInput';
140+
radioInput1.value = 'radio value 1';
141+
radioInput1.checked = false;
142+
radioInput2.type = 'radio';
143+
radioInput2.name = 'radioInput';
144+
radioInput2.value = 'radio value 2';
145+
radioInput2.checked = true;
146+
checkboxInput1.type = 'checkbox';
147+
checkboxInput1.name = 'checkboxInput';
148+
checkboxInput1.value = 'checkbox value 1';
149+
checkboxInput2.type = 'checkbox';
150+
checkboxInput2.name = 'checkboxInput';
151+
checkboxInput2.value = 'checkbox value 2';
152+
checkboxInput2.checked = true;
153+
154+
button.type = 'submit';
155+
button.name = 'button1';
156+
button.value = 'button';
157+
158+
form.appendChild(textInput);
159+
form.appendChild(hiddenInput);
160+
form.appendChild(hiddenInput2);
161+
form.appendChild(fileInput);
162+
form.appendChild(radioInput1);
163+
form.appendChild(radioInput2);
164+
form.appendChild(checkboxInput1);
165+
form.appendChild(checkboxInput2);
166+
form.appendChild(button);
167+
168+
const formData = new window.FormData(form, button);
169+
170+
expect(formData.get('textInput')).toBe('text value');
171+
expect(formData.get('hiddenInput')).toBe('hidden value 1');
172+
expect(formData.get('fileInput')).toBe(file);
173+
expect(formData.get('radioInput')).toBe('radio value 2');
174+
expect(formData.get('checkboxInput')).toBe('checkbox value 2');
175+
expect(formData.getAll('hiddenInput')).toEqual(['hidden value 1', 'hidden value 2']);
176+
expect(formData.getAll('radioInput')).toEqual(['radio value 2']);
177+
expect(formData.getAll('checkboxInput')).toEqual(['checkbox value 2']);
178+
expect(formData.get('button1')).toBe('button');
179+
});
180+
181+
it('Only sends button value if the button has a name and value.', () => {
182+
const form = document.createElement('form');
183+
const button1 = document.createElement('button');
184+
const button2 = document.createElement('input');
185+
const button3 = document.createElement('button');
186+
const button4 = document.createElement('input');
187+
188+
button1.name = 'button1';
189+
190+
button2.type = 'submit';
191+
button2.name = 'button2';
192+
193+
button3.name = 'button3';
194+
button3.value = 'button3';
195+
196+
button4.type = 'submit';
197+
button4.name = 'button4';
198+
button4.value = 'button4';
199+
200+
form.appendChild(button1);
201+
form.appendChild(button2);
202+
form.appendChild(button3);
203+
form.appendChild(button4);
204+
205+
expect(new window.FormData(form, button1).get('button1')).toBe(null);
206+
expect(new window.FormData(form, button2).get('button2')).toBe(null);
207+
expect(new window.FormData(form, button3).get('button3')).toBe('button3');
208+
expect(new window.FormData(form, button4).get('button4')).toBe('button4');
111209
});
112210

113211
it('Supports input elements with empty values.', () => {

packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,7 @@ describe('HTMLFormElement', () => {
10671067
<input type="radio" name="radio1" value="value1" required>
10681068
<input type="radio" name="radio1" value="value2" required>
10691069
<input type="radio" name="radio1" value="value3" required>
1070-
<input type="submit" name="button1"></input>
1070+
<input type="submit" name="button1">
10711071
</div>
10721072
`;
10731073

@@ -1188,6 +1188,40 @@ describe('HTMLFormElement', () => {
11881188

11891189
expect(page.mainFrame.url).toBe('about:blank');
11901190
});
1191+
1192+
it('Only includes the button value of the submitting button', async () => {
1193+
let request: Request | null = null;
1194+
1195+
vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> {
1196+
request = this.request;
1197+
return Promise.resolve(<Response>{
1198+
url: request?.url,
1199+
text: () =>
1200+
new Promise((resolve) => setTimeout(() => resolve('<html><body>Test</body></html>'), 2))
1201+
});
1202+
});
1203+
1204+
const browser = new Browser();
1205+
const page = browser.newPage();
1206+
const document = page.mainFrame.window.document;
1207+
1208+
document.write(`
1209+
<form method="post" action="http://example.com">
1210+
<input type="submit" name="action" value="submitted">
1211+
<input type="submit" name="action" value="notSubmitted">
1212+
<input type="reset" name="action" value="reset">
1213+
<input type="button" name="action" value="inputButton">
1214+
<button name="action" value="vanillaButton"></button>
1215+
</form>
1216+
`);
1217+
1218+
const submitButton = <HTMLInputElement>document.querySelector('input[value="submitted"]');
1219+
submitButton.click();
1220+
await page.mainFrame.waitForNavigation();
1221+
1222+
const formData = await request!.formData();
1223+
expect(formData.getAll('action')).toEqual(['submitted']);
1224+
});
11911225
});
11921226

11931227
describe('reset()', () => {

0 commit comments

Comments
 (0)