1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import path from 'path';
18import expect from 'expect';
19import {
20  getTestState,
21  setupTestBrowserHooks,
22  setupTestPageAndContextHooks,
23  describeFailsFirefox,
24} from './mocha-utils'; // eslint-disable-line import/extensions
25
26const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt');
27
28describe('input tests', function () {
29  setupTestBrowserHooks();
30  setupTestPageAndContextHooks();
31
32  describeFailsFirefox('input', function () {
33    it('should upload the file', async () => {
34      const { page, server } = getTestState();
35
36      await page.goto(server.PREFIX + '/input/fileupload.html');
37      const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
38      const input = await page.$('input');
39      await page.evaluate((e: HTMLElement) => {
40        globalThis._inputEvents = [];
41        e.addEventListener('change', (ev) =>
42          globalThis._inputEvents.push(ev.type)
43        );
44        e.addEventListener('input', (ev) =>
45          globalThis._inputEvents.push(ev.type)
46        );
47      }, input);
48      await input.uploadFile(filePath);
49      expect(
50        await page.evaluate((e: HTMLInputElement) => e.files[0].name, input)
51      ).toBe('file-to-upload.txt');
52      expect(
53        await page.evaluate((e: HTMLInputElement) => e.files[0].type, input)
54      ).toBe('text/plain');
55      expect(await page.evaluate(() => globalThis._inputEvents)).toEqual([
56        'input',
57        'change',
58      ]);
59      expect(
60        await page.evaluate((e: HTMLInputElement) => {
61          const reader = new FileReader();
62          const promise = new Promise((fulfill) => (reader.onload = fulfill));
63          reader.readAsText(e.files[0]);
64          return promise.then(() => reader.result);
65        }, input)
66      ).toBe('contents of the file');
67    });
68  });
69
70  describeFailsFirefox('Page.waitForFileChooser', function () {
71    it('should work when file input is attached to DOM', async () => {
72      const { page } = getTestState();
73
74      await page.setContent(`<input type=file>`);
75      const [chooser] = await Promise.all([
76        page.waitForFileChooser(),
77        page.click('input'),
78      ]);
79      expect(chooser).toBeTruthy();
80    });
81    it('should work when file input is not attached to DOM', async () => {
82      const { page } = getTestState();
83
84      const [chooser] = await Promise.all([
85        page.waitForFileChooser(),
86        page.evaluate(() => {
87          const el = document.createElement('input');
88          el.type = 'file';
89          el.click();
90        }),
91      ]);
92      expect(chooser).toBeTruthy();
93    });
94    it('should respect timeout', async () => {
95      const { page, puppeteer } = getTestState();
96
97      let error = null;
98      await page
99        .waitForFileChooser({ timeout: 1 })
100        .catch((error_) => (error = error_));
101      expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
102    });
103    it('should respect default timeout when there is no custom timeout', async () => {
104      const { page, puppeteer } = getTestState();
105
106      page.setDefaultTimeout(1);
107      let error = null;
108      await page.waitForFileChooser().catch((error_) => (error = error_));
109      expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
110    });
111    it('should prioritize exact timeout over default timeout', async () => {
112      const { page, puppeteer } = getTestState();
113
114      page.setDefaultTimeout(0);
115      let error = null;
116      await page
117        .waitForFileChooser({ timeout: 1 })
118        .catch((error_) => (error = error_));
119      expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
120    });
121    it('should work with no timeout', async () => {
122      const { page } = getTestState();
123
124      const [chooser] = await Promise.all([
125        page.waitForFileChooser({ timeout: 0 }),
126        page.evaluate(() =>
127          setTimeout(() => {
128            const el = document.createElement('input');
129            el.type = 'file';
130            el.click();
131          }, 50)
132        ),
133      ]);
134      expect(chooser).toBeTruthy();
135    });
136    it('should return the same file chooser when there are many watchdogs simultaneously', async () => {
137      const { page } = getTestState();
138
139      await page.setContent(`<input type=file>`);
140      const [fileChooser1, fileChooser2] = await Promise.all([
141        page.waitForFileChooser(),
142        page.waitForFileChooser(),
143        page.$eval('input', (input: HTMLInputElement) => input.click()),
144      ]);
145      expect(fileChooser1 === fileChooser2).toBe(true);
146    });
147  });
148
149  describeFailsFirefox('FileChooser.accept', function () {
150    it('should accept single file', async () => {
151      const { page } = getTestState();
152
153      await page.setContent(
154        `<input type=file oninput='javascript:console.timeStamp()'>`
155      );
156      const [chooser] = await Promise.all([
157        page.waitForFileChooser(),
158        page.click('input'),
159      ]);
160      await Promise.all([
161        chooser.accept([FILE_TO_UPLOAD]),
162        new Promise((x) => page.once('metrics', x)),
163      ]);
164      expect(
165        await page.$eval(
166          'input',
167          (input: HTMLInputElement) => input.files.length
168        )
169      ).toBe(1);
170      expect(
171        await page.$eval(
172          'input',
173          (input: HTMLInputElement) => input.files[0].name
174        )
175      ).toBe('file-to-upload.txt');
176    });
177    it('should be able to read selected file', async () => {
178      const { page } = getTestState();
179
180      await page.setContent(`<input type=file>`);
181      page
182        .waitForFileChooser()
183        .then((chooser) => chooser.accept([FILE_TO_UPLOAD]));
184      expect(
185        await page.$eval('input', async (picker: HTMLInputElement) => {
186          picker.click();
187          await new Promise((x) => (picker.oninput = x));
188          const reader = new FileReader();
189          const promise = new Promise((fulfill) => (reader.onload = fulfill));
190          reader.readAsText(picker.files[0]);
191          return promise.then(() => reader.result);
192        })
193      ).toBe('contents of the file');
194    });
195    it('should be able to reset selected files with empty file list', async () => {
196      const { page } = getTestState();
197
198      await page.setContent(`<input type=file>`);
199      page
200        .waitForFileChooser()
201        .then((chooser) => chooser.accept([FILE_TO_UPLOAD]));
202      expect(
203        await page.$eval('input', async (picker: HTMLInputElement) => {
204          picker.click();
205          await new Promise((x) => (picker.oninput = x));
206          return picker.files.length;
207        })
208      ).toBe(1);
209      page.waitForFileChooser().then((chooser) => chooser.accept([]));
210      expect(
211        await page.$eval('input', async (picker: HTMLInputElement) => {
212          picker.click();
213          await new Promise((x) => (picker.oninput = x));
214          return picker.files.length;
215        })
216      ).toBe(0);
217    });
218    it('should not accept multiple files for single-file input', async () => {
219      const { page } = getTestState();
220
221      await page.setContent(`<input type=file>`);
222      const [chooser] = await Promise.all([
223        page.waitForFileChooser(),
224        page.click('input'),
225      ]);
226      let error = null;
227      await chooser
228        .accept([
229          path.relative(
230            process.cwd(),
231            __dirname + '/assets/file-to-upload.txt'
232          ),
233          path.relative(process.cwd(), __dirname + '/assets/pptr.png'),
234        ])
235        .catch((error_) => (error = error_));
236      expect(error).not.toBe(null);
237    });
238    it('should fail for non-existent files', async () => {
239      const { page } = getTestState();
240
241      await page.setContent(`<input type=file>`);
242      const [chooser] = await Promise.all([
243        page.waitForFileChooser(),
244        page.click('input'),
245      ]);
246      let error = null;
247      await chooser
248        .accept(['file-does-not-exist.txt'])
249        .catch((error_) => (error = error_));
250      expect(error).not.toBe(null);
251    });
252    it('should fail when accepting file chooser twice', async () => {
253      const { page } = getTestState();
254
255      await page.setContent(`<input type=file>`);
256      const [fileChooser] = await Promise.all([
257        page.waitForFileChooser(),
258        page.$eval('input', (input: HTMLInputElement) => input.click()),
259      ]);
260      await fileChooser.accept([]);
261      let error = null;
262      await fileChooser.accept([]).catch((error_) => (error = error_));
263      expect(error.message).toBe(
264        'Cannot accept FileChooser which is already handled!'
265      );
266    });
267  });
268
269  describeFailsFirefox('FileChooser.cancel', function () {
270    it('should cancel dialog', async () => {
271      const { page } = getTestState();
272
273      // Consider file chooser canceled if we can summon another one.
274      // There's no reliable way in WebPlatform to see that FileChooser was
275      // canceled.
276      await page.setContent(`<input type=file>`);
277      const [fileChooser1] = await Promise.all([
278        page.waitForFileChooser(),
279        page.$eval('input', (input: HTMLInputElement) => input.click()),
280      ]);
281      await fileChooser1.cancel();
282      // If this resolves, than we successfully canceled file chooser.
283      await Promise.all([
284        page.waitForFileChooser(),
285        page.$eval('input', (input: HTMLInputElement) => input.click()),
286      ]);
287    });
288    it('should fail when canceling file chooser twice', async () => {
289      const { page } = getTestState();
290
291      await page.setContent(`<input type=file>`);
292      const [fileChooser] = await Promise.all([
293        page.waitForFileChooser(),
294        page.$eval('input', (input: HTMLInputElement) => input.click()),
295      ]);
296      await fileChooser.cancel();
297      let error = null;
298
299      try {
300        fileChooser.cancel();
301      } catch (error_) {
302        error = error_;
303      }
304
305      expect(error.message).toBe(
306        'Cannot cancel FileChooser which is already handled!'
307      );
308    });
309  });
310
311  describeFailsFirefox('FileChooser.isMultiple', () => {
312    it('should work for single file pick', async () => {
313      const { page } = getTestState();
314
315      await page.setContent(`<input type=file>`);
316      const [chooser] = await Promise.all([
317        page.waitForFileChooser(),
318        page.click('input'),
319      ]);
320      expect(chooser.isMultiple()).toBe(false);
321    });
322    it('should work for "multiple"', async () => {
323      const { page } = getTestState();
324
325      await page.setContent(`<input multiple type=file>`);
326      const [chooser] = await Promise.all([
327        page.waitForFileChooser(),
328        page.click('input'),
329      ]);
330      expect(chooser.isMultiple()).toBe(true);
331    });
332    it('should work for "webkitdirectory"', async () => {
333      const { page } = getTestState();
334
335      await page.setContent(`<input multiple webkitdirectory type=file>`);
336      const [chooser] = await Promise.all([
337        page.waitForFileChooser(),
338        page.click('input'),
339      ]);
340      expect(chooser.isMultiple()).toBe(true);
341    });
342  });
343});
344