Skip to content
File

Blob: src/workerd/api/tests/form-data-test.js

javascript949 lines
1// Copyright (c) 2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import {
5 strictEqual,
6 deepStrictEqual,
7 notStrictEqual,
8 rejects,
9 throws,
10} from 'node:assert';
11 
12export const apiFormDataParse = {
13 async test(ctrl, env) {
14 const INPUT = `---
15Content-Disposition: form-data; name="field0"
16
17part0
18---
19Content-Disposition: form-data; name="field1"
20
21part1
22---
23Content-Disposition: form-data; name="field0"
24
25part2
26---
27Content-Disposition: form-data; name="field1"
28
29part3
30-----`;
31 
32 const req = new Request('https://example.com', {
33 method: 'POST',
34 body: INPUT,
35 headers: {
36 'content-type': 'multipart/form-data; Boundary="-"',
37 },
38 });
39 
40 const formData = await req.formData();
41 
42 deepStrictEqual(formData.getAll('field0'), ['part0', 'part2']);
43 deepStrictEqual(formData.getAll('field1'), ['part1', 'part3']);
44 },
45};
46 
47export const invalidFormdataContentDisposition = {
48 async test() {
49 const INPUT = `--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
50Content-Disposition: foobar
51Content-Type: application/octet-stream
52
53foo-content
54--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a--
55`;
56 
57 const req = new Request('https://example.org', {
58 method: 'POST',
59 body: INPUT,
60 headers: {
61 'content-type':
62 'multipart/form-data;boundary=2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a',
63 },
64 });
65 
66 try {
67 await req.formData();
68 throw new Error('Parsing the form data should have thrown');
69 } catch (err) {
70 strictEqual(
71 err.message,
72 'Content-Disposition header for FormData part must ' +
73 'have the value "form-data", possibly followed by ' +
74 'parameters. Got: "foobar"'
75 );
76 }
77 },
78};
79 
80export const invalidFormData = {
81 async test() {
82 const form = new FormData();
83 form.set('foo', new File(['foo-content'], 'foo.txt\\'));
84 try {
85 new Request('http://example.org', {
86 method: 'POST',
87 body: form,
88 });
89 throw new Error('should have thrown');
90 } catch (err) {
91 strictEqual(err.message, "Name or filename can't end with backslash");
92 }
93 },
94};
95 
96export const formDataWithFilesBlobs = {
97 async test() {
98 const INPUT = `--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
99Content-Disposition: form-data; name="foo"; filename="foo.txt"
100Content-Type: application/octet-stream
101
102foo-content
103--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
104Content-Disposition: form-data; name="bar"; filename="bar-renamed.txt"
105Content-Type: application/octet-stream
106
107bar1-content
108--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
109Content-Disposition: form-data; name="bar"; filename="bar2.txt"
110Content-Type: text/bary
111
112bar2-content
113--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
114Content-Disposition: form-data; name="baz"; filename="baz"
115Content-Type: text/bazzy
116
117baz-content
118--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
119Content-Disposition: form-data; name="qux"; filename="qux%0A%22\\\\.txt"
120Content-Type: application/octet-stream
121
122qux-content
123--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a--
124`;
125 
126 const req = new Request('https://example.org', {
127 method: 'POST',
128 body: INPUT,
129 headers: {
130 'content-type':
131 'multipart/form-data;boundary=2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a',
132 },
133 });
134 
135 async function assertFile(file, name, type, content) {
136 if (!(file instanceof File)) {
137 throw new Error('not a File: ' + file);
138 }
139 
140 strictEqual(name, file.name);
141 strictEqual(type, file.type);
142 strictEqual(content, await file.text());
143 }
144 
145 const form = await req.formData();
146 await assertFile(
147 form.get('foo'),
148 'foo.txt',
149 'application/octet-stream',
150 'foo-content'
151 );
152 await assertFile(
153 form.getAll('bar')[0],
154 'bar-renamed.txt',
155 'application/octet-stream',
156 'bar1-content'
157 );
158 await assertFile(
159 form.getAll('bar')[1],
160 'bar2.txt',
161 'text/bary',
162 'bar2-content'
163 );
164 await assertFile(form.get('baz'), 'baz', 'text/bazzy', 'baz-content');
165 await assertFile(
166 form.get('qux'),
167 'qux%0A%22\\.txt',
168 'application/octet-stream',
169 'qux-content'
170 );
171 },
172};
173 
174export const sendFilesInFormdata = {
175 async test() {
176 const INPUT = `--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
177Content-Disposition: form-data; name="foo"; filename="foo.txt"
178Content-Type: application/octet-stream
179
180foo-content
181--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
182Content-Disposition: form-data; name="bar"; filename="bar-renamed.txt"
183Content-Type: application/octet-stream
184
185bar1-content
186--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
187Content-Disposition: form-data; name="bar"; filename="bar2.txt"
188Content-Type: text/bary
189
190bar2-content
191--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
192Content-Disposition: form-data; name="baz"; filename="baz"
193Content-Type: text/bazzy
194
195baz-content
196--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a
197Content-Disposition: form-data; name="qux"; filename="qux%0A%22\\.txt"
198Content-Type: application/octet-stream
199
200qux-content
201--2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a--
202`;
203 
204 const req = new Request('https://example.org', {
205 method: 'POST',
206 body: INPUT,
207 headers: {
208 'content-type':
209 'multipart/form-data;boundary=2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a',
210 },
211 });
212 
213 const form = await req.formData();
214 
215 form.set('foo', new File(['foo-content'], 'foo.txt'));
216 form.append(
217 'bar',
218 new File(['bar1-content'], 'bar1.txt'),
219 'bar-renamed.txt'
220 );
221 form.append(
222 'bar',
223 new File(['bar2-content'], 'bar2.txt', { type: 'text/bary' })
224 );
225 form.append('baz', new Blob(['baz-content'], { type: 'text/bazzy' }));
226 form.set('qux', new Blob(['qux-content']), 'qux\n"\\.txt');
227 
228 if (!(form.get('foo') instanceof File)) {
229 throw new Error('expected file');
230 }
231 if (form.get('foo').name != 'foo.txt') {
232 throw new Error('expected file name foo.txt');
233 }
234 if (!(form.getAll('bar')[1] instanceof File)) {
235 throw new Error('expected files');
236 }
237 },
238};
239 
240async function parseFormData(contentType, text) {
241 const req = new Request('http://example.org', {
242 method: 'POST',
243 body: text,
244 headers: {
245 'content-type': contentType,
246 },
247 });
248 return await req.formData();
249}
250 
251export const testFormDataParser = {
252 async test() {
253 const successCases = [
254 {
255 // No parts. Note that Chrome throws a TypeError on this input, but it'll generate output that
256 // looks like this if you ask it to serialize an empty form.
257 contentType: 'multipart/form-data; boundary="+"',
258 body: '--+--',
259 expected: '',
260 comment: 'Empty form is okay',
261 },
262 {
263 contentType: 'multipart/form-data; boundary="+"',
264 body: [
265 // CRLF after boundary, CRLFCRLF after header, CRLF after message
266 '--+\r\n',
267 'Content-Disposition: form-data; name="field0"\r\n',
268 '\r\n',
269 'part0\r\n',
270 
271 // LF after boundary, CRLFLF after header, tabs in header, LF after message
272 '--+\n',
273 'CONTENT-DISPOSITION:\tform-data\t;\tname="field1"\r\n',
274 '\n',
275 'part1\n',
276 
277 // LFCRLF after header, no OWS in header, Content-Type header above disposition
278 '--+\r\n',
279 'content-disposition:form-data;name="field0"\n',
280 '\r\n',
281 'part2\r\n',
282 
283 // LFCRLF after header
284 '--+\r\n',
285 'CoNTent-dIsposiTIOn: form-data; name="field1"\n',
286 '\r\n',
287 'part3\r\n',
288 
289 '--+--',
290 ].join(''),
291 expected: 'field0=part0,field1=part1,field0=part2,field1=part3',
292 comment: 'Mixed CRLF and LF, case-insensitivity of header name',
293 },
294 {
295 contentType: 'multipart/form-data; boundary="+"',
296 body: [
297 // CRLFCRLF after header, empty message with CRLF
298 '--+\r\n',
299 'Content-Disposition: form-data; name="empties"\r\n',
300 '\r\n',
301 '\r\n',
302 
303 // CRLFCRLF after header, empty message with LF
304 '--+\r\n',
305 'Content-Disposition: form-data; name="empties"\r\n',
306 '\r\n',
307 '\n',
308 
309 // CRLFLF after header, empty message with CRLF
310 '--+\r\n',
311 'Content-Disposition: form-data; name="empties"\r\n',
312 '\n',
313 '\r\n',
314 
315 // CRLFLF after header, empty message with LF
316 '--+\r\n',
317 'Content-Disposition: form-data; name="empties"\r\n',
318 '\n',
319 '\n',
320 
321 // LFCRLF after header, empty message with CRLF
322 '--+\r\n',
323 'Content-Disposition: form-data; name="empties"\n',
324 '\r\n',
325 '\r\n',
326 
327 // LFCRLF after header, empty message with LF
328 '--+\r\n',
329 'Content-Disposition: form-data; name="empties"\n',
330 '\r\n',
331 '\n',
332 
333 // LFLF after header, empty message with CRLF
334 '--+\r\n',
335 'Content-Disposition: form-data; name="empties"\n',
336 '\n',
337 '\r\n',
338 
339 // LFLF after header, empty message with LF
340 '--+\r\n',
341 'Content-Disposition: form-data; name="empties"\n',
342 '\n',
343 '\n',
344 
345 '--+--',
346 ].join(''),
347 expected:
348 'empties=,empties=,empties=,empties=,empties=,empties=,empties=,empties=',
349 comment: 'Mixed CRLF and LF with empty messages',
350 },
351 {
352 contentType: 'multipart/form-data; boundary="+"',
353 body: [
354 '--+\r\n',
355 'Content-Type: text/plain; charset=utf-8\r\n',
356 'Content-Disposition: form-data; name="field0"\r\n',
357 '\r\n',
358 'part0\r\n',
359 
360 '--+\r\n',
361 'Content-Disposition: form-data; name="field1"\r\n',
362 'Content-Type: text/plain; charset=utf-8\r\n',
363 '\r\n',
364 'part1\r\n',
365 
366 '--+--',
367 ].join(''),
368 expected: 'field0=part0,field1=part1',
369 comment: 'Content-Type header should be okay',
370 },
371 {
372 contentType: 'application/x-www-form-urlencoded',
373 body: [
374 'field0=part0',
375 'field1=part1',
376 'field0=part2',
377 'field1=part3',
378 ].join('&'),
379 expected: 'field0=part0,field1=part1,field0=part2,field1=part3',
380 comment: 'Basic application/x-www-form-urlencoded parse works',
381 },
382 {
383 contentType: 'application/x-www-form-urlencoded',
384 body: ['field0=data+with+an+%26+in+it'].join('&'),
385 expected: 'field0=data with an & in it',
386 comment:
387 'application/x-www-form-urlencoded data gets percent-and-plus-decoded',
388 },
389 {
390 contentType: 'application/x-www-form-urlencoded',
391 body: ['', '=', 'field1', '=part2', 'field1='].join('&'),
392 expected: '=,field1=,=part2,field1=',
393 comment:
394 'application/x-www-form-urlencoded data with awkward &, = placement',
395 },
396 ];
397 
398 for (let i = 0; i < successCases.length; ++i) {
399 const c = successCases[i];
400 const fd = await parseFormData(c.contentType, c.body);
401 const actual = [];
402 for (let [k, v] of fd) {
403 actual.push(`${k}=${v}`);
404 }
405 strictEqual(actual.join(','), c.expected);
406 }
407 
408 let failureCases = [
409 {
410 contentType: 'multipart/form-data; boundary="+"',
411 body: '',
412 comment: 'Empty body throws',
413 },
414 {
415 contentType: 'multipart/form-data; boundary="+"',
416 body: '--asdf--',
417 comment: 'Bad boundary throws',
418 },
419 {
420 contentType: 'multipart/form-data; boundary="+"',
421 body: '--+',
422 comment: 'Non-terminal boundary at end throws',
423 },
424 {
425 contentType: 'multipart/form-data; boundary="+"',
426 body: [
427 '--+\r\n',
428 'Content-Disposition: form-data; name="field0"\r\n',
429 '\r\n',
430 'part0\r\n',
431 '-+--',
432 ].join(''),
433 comment: 'Bad terminal delimiter',
434 },
435 {
436 contentType: 'multipart/form-data; boundary="+"',
437 body: [
438 '--+\r\n',
439 'Bad-Content-Disposition: form-data; name="field0"\r\n',
440 '\r\n',
441 'part0\r\n',
442 '--+--',
443 ].join(''),
444 comment: 'Bad Content-Disposition header',
445 },
446 {
447 contentType: 'multipart/form-data; boundary="+"',
448 body: [
449 '--+\r\n',
450 'Content-Disposition-Bad: form-data; name="field0"\r\n',
451 '\r\n',
452 'part0\r\n',
453 '--+--',
454 ].join(''),
455 comment: 'Bad Content-Disposition header',
456 },
457 {
458 contentType: 'multipart/form-data; boundary="+"',
459 body: [
460 '--+\r\n',
461 'Content-Disposition: form-data; name="field0"\r\n',
462 '\r',
463 'part0\r\n',
464 '--+--',
465 ].join(''),
466 comment: 'No header termination CRLFCRLF',
467 },
468 {
469 contentType: 'multipart/form-data; boundary="+"',
470 body: [
471 '--+\r\n',
472 'Content-Disposition: form-data; name="field0"\r\n',
473 '\r\n',
474 'part0\r\n',
475 ].join(''),
476 comment: 'No subsequent boundary string',
477 },
478 {
479 contentType: 'multipart/form-data; boundary="+"',
480 body: [
481 '--+\r\n',
482 'Content-Disposition: form-data; name="field0"\r\n',
483 '\r\n',
484 'part0\r\n',
485 '--+\r--',
486 ].join(''),
487 comment: "Boundary was not succeeded by CRLF, LF, or '--'",
488 },
489 {
490 contentType: 'multipart/form-data; boundary=',
491 body: '----',
492 comment: 'Empty boundary parameter in content-type',
493 },
494 {
495 contentType: 'application/x-www-form-urlencoded; charset=big5',
496 body: '--+--',
497 comment: 'Unsupported charset',
498 },
499 ];
500 
501 for (let i = 0; i < failureCases.length; ++i) {
502 const c = failureCases[i];
503 await rejects(() => parseFormData(c.contentType, c.body));
504 }
505 },
506};
507 
508export const testFormDataSerializer = {
509 async test() {
510 // Test the serializer by making a round trip through our serializer and parser.
511 
512 const expected = new FormData();
513 expected.append('field0', 'part0');
514 expected.append('field1', 'part1');
515 expected.append('field0', 'part2');
516 expected.append('field1', 'part3');
517 expected.append('field-with-a-"-in-it', 'part4');
518 
519 // Serialize the FormData.
520 const response = new Response(expected);
521 
522 // Parse it back.
523 // This regex assumes an unquoted boundary, which is true for our serializer.
524 const boundary = /boundary=(.+)$/.exec(
525 response.headers.get('Content-Type')
526 )[1];
527 const actual = await parseFormData(
528 `multipart/form-data; boundary="${boundary}"`,
529 await response.text()
530 );
531 
532 const expectedData = [
533 'field0=part0',
534 'field1=part1',
535 'field0=part2',
536 'field1=part3',
537 'field-with-a-%22-in-it=part4',
538 ];
539 const actualData = [];
540 for (let [k, v] of actual) {
541 actualData.push(`${k}=${v}`);
542 }
543 
544 strictEqual('' + actualData.join(','), '' + expectedData.join(','));
545 },
546};
547 
548export const testFormDataSet = {
549 test() {
550 const fd = new FormData();
551 fd.append('foo', '0');
552 fd.append('foo', '1');
553 fd.append('foo', '2');
554 fd.append('foo', '3');
555 fd.append('foo', '4');
556 fd.append('foo', '5');
557 fd.set('foo', '6');
558 
559 strictEqual('' + fd.getAll('foo'), '6');
560 },
561};
562 
563export const testFormDataIterators = {
564 test() {
565 const fd = new FormData();
566 const entry = fd.entries();
567 const key = fd.keys();
568 const value = fd.values();
569 strictEqual(entry.next().value, undefined);
570 strictEqual(key.next().value, undefined);
571 strictEqual(value.next().value, undefined);
572 
573 fd.append('key', '0');
574 fd.append('key', '1');
575 strictEqual('' + entry.next().value, 'key,0');
576 strictEqual('' + key.next().value, 'key');
577 strictEqual('' + value.next().value, '0');
578 
579 fd.delete('key');
580 strictEqual(entry.next().value, undefined);
581 strictEqual(key.next().value, undefined);
582 strictEqual(value.next().value, undefined);
583 },
584};
585 
586export const testFormDataForeach = {
587 test() {
588 const fd = new FormData();
589 
590 fd.forEach(function (v, k, t) {
591 throw new Error('should not be called on empty array');
592 });
593 
594 let foreachOutput = [];
595 fd.append('key1', 'value1');
596 fd.append('key2', 'value2');
597 
598 let i = 0;
599 fd.forEach(function (value, key, captureFd) {
600 notStrictEqual(value, '3'); // if this is true, then the test is useless
601 // updating the headers should affect them immediately when not called through forEach
602 captureFd.set(key, '3');
603 strictEqual(captureFd.get(key), '3');
604 // updating the headers should not affect `value`
605 notStrictEqual(value, '3');
606 foreachOutput.push(`${key}=${value}`);
607 
608 captureFd.append('some-key', '4');
609 // console.log("appended");
610 i += 1;
611 });
612 
613 // appending keys within the loop should call the callback on the new items
614 strictEqual(i, 4);
615 strictEqual(
616 '' + foreachOutput.join('&'),
617 'key1=value1&key2=value2&some-key=4&some-key=4'
618 );
619 // `capture_headers.set` should affect the outer headers object
620 strictEqual(fd.get('key1'), '3');
621 strictEqual(fd.get('key2'), '3');
622 // `capture_headers.append` should affect the outer object
623 deepStrictEqual(fd.getAll('some-key'), ['3', '4']);
624 
625 throws(() => fd.forEach());
626 throws(() => fd.forEach(1));
627 
628 // `this` can be overridden by setting the second argument
629 fd.forEach(function () {
630 // NOTE: can't use `assert_equals` because `this` has type `object` which apparently it doesn't like
631 strictEqual(this, 1);
632 }, 1);
633 
634 throws(() => {
635 fd.forEach(function () {
636 throw new Error('boo');
637 });
638 });
639 
640 // forEach should not move the value
641 fd.set('key1', 'a');
642 fd.forEach(() => {});
643 strictEqual(fd.get('key1'), 'a');
644 },
645};
646 
647export const w3cTestFormDataAppend = {
648 test() {
649 function test_formdata(creator, verifier, description) {
650 let result = [];
651 for (let [k, v] of creator()) {
652 result.push(`${k}=${v}`);
653 }
654 verifier(result.join(','));
655 }
656 
657 test_formdata(
658 function () {
659 var fd = new FormData();
660 fd.append('name', new String('value'));
661 return fd;
662 },
663 function (data) {
664 strictEqual(data, 'name=value');
665 },
666 'Passing a String object to FormData.append should work.'
667 );
668 
669 strictEqual(create_formdata(['key', 'value1']).get('key'), 'value1');
670 strictEqual(
671 create_formdata(['key', 'value2'], ['key', 'value1']).get('key'),
672 'value2'
673 );
674 strictEqual(create_formdata(['key', undefined]).get('key'), 'undefined');
675 strictEqual(
676 create_formdata(['key', undefined], ['key', 'value1']).get('key'),
677 'undefined'
678 );
679 strictEqual(create_formdata(['key', null]).get('key'), 'null');
680 strictEqual(
681 create_formdata(['key', null], ['key', 'value1']).get('key'),
682 'null'
683 );
684 
685 function create_formdata() {
686 var fd = new FormData();
687 for (var i = 0; i < arguments.length; i++) {
688 fd.append.apply(fd, arguments[i]);
689 }
690 return fd;
691 }
692 },
693};
694 
695export const w3cTestFormDataBlob = {
696 test() {
697 function create_formdata() {
698 var fd = new FormData();
699 for (var i = 0; i < arguments.length; i++) {
700 fd.append.apply(fd, arguments[i]);
701 }
702 return fd;
703 }
704 
705 throws(() => create_formdata('a', 'b', 'c'));
706 },
707};
708 
709export const w3cTestFormDataDelete = {
710 test() {
711 {
712 let fd = create_formdata(['key', 'value1'], ['key', 'value2']);
713 fd.delete('key');
714 strictEqual(fd.get('key'), null);
715 }
716 
717 {
718 let fd = create_formdata(['key', 'value1'], ['key', 'value2']);
719 fd.delete('nil');
720 strictEqual(fd.get('key'), 'value1');
721 }
722 
723 {
724 let fd = create_formdata(['key1', 'value1'], ['key2', 'value2']);
725 fd.delete('key1');
726 strictEqual(fd.get('key1'), null);
727 strictEqual(fd.get('key2'), 'value2');
728 }
729 
730 function create_formdata() {
731 let fd = new FormData();
732 for (let i = 0; i < arguments.length; i++) {
733 fd.append.apply(fd, arguments[i]);
734 }
735 return fd;
736 }
737 },
738};
739 
740export const w3cTestFormDataForeach = {
741 test() {
742 let fd = new FormData();
743 fd.append('n1', 'v1');
744 fd.append('n2', 'v2');
745 fd.append('n3', 'v3');
746 fd.append('n1', 'v4');
747 fd.append('n2', 'v5');
748 fd.append('n3', 'v6');
749 fd.delete('n2');
750 let expected_keys = ['n1', 'n3', 'n1', 'n3'];
751 let expected_values = ['v1', 'v3', 'v4', 'v6'];
752 // TODO(soon): Test with this File object.
753 //var file = new File(['hello'], "hello.txt");
754 //fd.append('f1', file);
755 //var expected_keys = ['n1', 'n3', 'n1', 'n3', 'f1'];
756 //var expected_values = ['v1', 'v3', 'v4', 'v6', file];
757 
758 {
759 let mykeys = [],
760 myvalues = [];
761 for (let entry of fd) {
762 strictEqual(entry.length, 2);
763 mykeys.push(entry[0]);
764 myvalues.push(entry[1]);
765 }
766 deepStrictEqual(mykeys, expected_keys);
767 deepStrictEqual(myvalues, expected_values);
768 }
769 
770 {
771 let mykeys = [],
772 myvalues = [];
773 for (let entry of fd.entries()) {
774 strictEqual(
775 entry.length,
776 2,
777 'entries() iterator should yield key/value pairs'
778 );
779 mykeys.push(entry[0]);
780 myvalues.push(entry[1]);
781 }
782 deepStrictEqual(
783 mykeys,
784 expected_keys,
785 'entries() iterator should see duplicate keys'
786 );
787 deepStrictEqual(
788 myvalues,
789 expected_values,
790 'entries() iterator should see non-deleted values'
791 );
792 }
793 
794 {
795 let mykeys = [];
796 for (let entry of fd.keys()) mykeys.push(entry);
797 deepStrictEqual(mykeys, expected_keys);
798 }
799 
800 {
801 let myvalues = [];
802 for (let entry of fd.values()) myvalues.push(entry);
803 deepStrictEqual(
804 myvalues,
805 expected_values,
806 'values() iterator should see non-deleted values'
807 );
808 }
809 },
810};
811 
812export const w3cTestFormDataGet = {
813 test() {
814 strictEqual(
815 create_formdata(['key', 'value1'], ['key', 'value2']).get('key'),
816 'value1'
817 );
818 strictEqual(
819 create_formdata(['key', 'value1'], ['key', 'value2']).get('nil'),
820 null
821 );
822 strictEqual(create_formdata().get('key'), null);
823 deepStrictEqual(
824 create_formdata(['key', 'value1'], ['key', 'value2']).getAll('key'),
825 ['value1', 'value2']
826 );
827 deepStrictEqual(
828 create_formdata(['key', 'value1'], ['key', 'value2']).getAll('nil'),
829 []
830 );
831 deepStrictEqual(create_formdata().getAll('key'), []);
832 
833 function create_formdata() {
834 var fd = new FormData();
835 for (var i = 0; i < arguments.length; i++) {
836 fd.append.apply(fd, arguments[i]);
837 }
838 return fd;
839 }
840 },
841};
842 
843export const w3cTestFormDataHas = {
844 test() {
845 strictEqual(
846 create_formdata(['key', 'value1'], ['key', 'value2']).has('key'),
847 true
848 );
849 strictEqual(
850 create_formdata(['key', 'value1'], ['key', 'value2']).has('nil'),
851 false
852 );
853 strictEqual(create_formdata().has('key'), false);
854 
855 function create_formdata() {
856 var fd = new FormData();
857 for (var i = 0; i < arguments.length; i++) {
858 fd.append.apply(fd, arguments[i]);
859 }
860 return fd;
861 }
862 },
863};
864 
865export const w3cTestFormDataSet = {
866 test() {
867 function test_formdata(creator, verifier, description) {
868 let result = [];
869 for (let [k, v] of creator()) {
870 result.push(`${k}=${v}`);
871 }
872 verifier(result.join(','));
873 }
874 
875 test_formdata(
876 function () {
877 var fd = new FormData();
878 fd.set('name', new String('value'));
879 return fd;
880 },
881 function (data) {
882 strictEqual(data, 'name=value');
883 },
884 'Passing a String object to FormData.set should work'
885 );
886 
887 strictEqual(create_formdata(['key', 'value1']).get('key'), 'value1');
888 strictEqual(
889 create_formdata(['key', 'value2'], ['key', 'value1']).get('key'),
890 'value1'
891 );
892 strictEqual(create_formdata(['key', undefined]).get('key'), 'undefined');
893 strictEqual(
894 create_formdata(['key', undefined], ['key', 'value1']).get('key'),
895 'value1'
896 );
897 strictEqual(create_formdata(['key', null]).get('key'), 'null');
898 strictEqual(
899 create_formdata(['key', null], ['key', 'value1']).get('key'),
900 'value1'
901 );
902 
903 // TODO(conform): Support File/Blob.
904 //test(function () {
905 // var fd = new FormData();
906 // fd.set('key', new Blob([]), 'blank.txt');
907 // var file = fd.get('key');
908 // assert_true(file instanceof File);
909 // assert_equals(file.name, 'blank.txt');
910 //}, 'testFormDataSetEmptyBlob');
911 
912 function create_formdata() {
913 var fd = new FormData();
914 for (var i = 0; i < arguments.length; i++) {
915 fd.set.apply(fd, arguments[i]);
916 }
917 return fd;
918 }
919 },
920};
921 
922export const w3cTestFormData = {
923 test() {
924 function do_test(name, fd, expected) {
925 let result = [];
926 for (let [k, v] of fd) {
927 result.push(`${k}=${v}`);
928 }
929 strictEqual(result.join(','), expected, name);
930 }
931 
932 function create_formdata() {
933 var fd = new FormData();
934 for (var i = 0; i < arguments.length; i++) {
935 fd.append.apply(fd, arguments[i]);
936 }
937 return fd;
938 }
939 
940 do_test('empty formdata', new FormData(), '');
941 do_test(
942 'formdata with string',
943 create_formdata(['key', 'value']),
944 'key=value'
945 );
946 //do_test("formdata with named string", create_formdata(['key', new Blob(['value'], {type: 'text/plain'}), 'kv.txt']), '\nkey=kv.txt:text/plain:5,');
947 },
948};