xref: /expo/apps/test-suite/tests/FileSystem.js (revision 59c27971)
1'use strict';
2
3import { Asset } from 'expo-asset';
4import Constants from 'expo-constants';
5import * as FS from 'expo-file-system';
6import { Platform } from 'expo-modules-core';
7
8export const name = 'FileSystem';
9
10export async function test({ describe, expect, it, ...t }) {
11  describe('FileSystem', () => {
12    const throws = async (run) => {
13      let error = null;
14      try {
15        await run();
16      } catch (e) {
17        error = e;
18      }
19      expect(error).toBeTruthy();
20    };
21
22    if (Constants.appOwnership === 'expo') {
23      describe('managed workflow', () => {
24        it('throws out-of-scope exceptions', async () => {
25          const p = FS.documentDirectory;
26
27          await throws(() => FS.readAsStringAsync(p + '../hello/world'));
28          await throws(() => FS.writeAsStringAsync(p + '../hello/world', ''));
29          await throws(() => FS.deleteAsync(p + '../hello/world'));
30          await throws(() => FS.deleteAsync(p));
31          await throws(() => FS.deleteAsync(FS.cacheDirectory));
32          await throws(() => FS.moveAsync({ from: p + '../a/b', to: 'c' }));
33          await throws(() => FS.moveAsync({ from: 'c', to: p + '../a/b' }));
34          await throws(() => FS.copyAsync({ from: p + '../a/b', to: 'c' }));
35          await throws(() => FS.copyAsync({ from: 'c', to: p + '../a/b' }));
36          await throws(() => FS.makeDirectoryAsync(p + '../hello/world'));
37          await throws(() => FS.readDirectoryAsync(p + '../hello/world'));
38          await throws(() => FS.downloadAsync('http://www.google.com', p + '../hello/world'));
39          await throws(() => FS.readDirectoryAsync(p + '../'));
40          await throws(() => FS.downloadAsync('http://www.google.com', p + '../hello/world'));
41        });
42      });
43    }
44
45    if (Platform.OS === 'web') {
46      // Web doesn't support FileSystem
47      return;
48    }
49
50    it('delete(idempotent) -> !exists -> download(md5, uri) -> exists -> delete -> !exists', async () => {
51      const localUri = FS.documentDirectory + 'download1.png';
52
53      const assertExists = async (expectedToExist) => {
54        const { exists } = await FS.getInfoAsync(localUri);
55        if (expectedToExist) {
56          expect(exists).toBeTruthy();
57        } else {
58          expect(exists).not.toBeTruthy();
59        }
60      };
61
62      await FS.deleteAsync(localUri, { idempotent: true });
63      await assertExists(false);
64
65      const { md5, headers } = await FS.downloadAsync(
66        'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png',
67        localUri,
68        { md5: true }
69      );
70      expect(md5).toBe('1e02045c10b8f1145edc7c8375998f87');
71      await assertExists(true);
72      expect(headers['Content-Type']).toBe('image/png');
73
74      await FS.deleteAsync(localUri);
75      await assertExists(false);
76    }, 9000);
77
78    it('Can read/write Base64', async () => {
79      const asset = await Asset.fromModule(require('../assets/icons/app.png'));
80      await asset.downloadAsync();
81
82      for (let startingPosition = 0; startingPosition < 3; startingPosition++) {
83        const options = {
84          encoding: FS.EncodingType.Base64,
85          position: startingPosition,
86          length: startingPosition + 1,
87        };
88
89        const b64 = await FS.readAsStringAsync(asset.localUri, options);
90        expect(b64).toBeDefined();
91        expect(typeof b64).toBe('string');
92        expect(b64.length % 4).toBe(0);
93
94        const localUri = FS.documentDirectory + 'b64.png';
95
96        await FS.writeAsStringAsync(localUri, b64, { encoding: FS.EncodingType.Base64 });
97
98        expect(await FS.readAsStringAsync(localUri, { encoding: FS.EncodingType.Base64 })).toBe(
99          b64
100        );
101      }
102    });
103
104    it('delete(idempotent) -> delete[error]', async () => {
105      const localUri = FS.documentDirectory + 'willDelete.png';
106
107      await FS.deleteAsync(localUri, { idempotent: true });
108
109      let error;
110      try {
111        await FS.deleteAsync(localUri);
112      } catch (e) {
113        error = e;
114      }
115      expect(error.message).toMatch(/not exist/);
116    });
117
118    it('download(md5, uri) -> read -> delete -> !exists -> read[error]', async () => {
119      const localUri = FS.documentDirectory + 'download1.txt';
120
121      const { md5 } = await FS.downloadAsync(
122        'https://s3-us-west-1.amazonaws.com/test-suite-data/text-file.txt',
123        localUri,
124        { md5: true }
125      );
126      expect(md5).toBe('86d73d2f11e507365f7ea8e7ec3cc4cb');
127
128      const string = await FS.readAsStringAsync(localUri);
129      expect(string).toBe('hello, world\nthis is a test file\n');
130
131      await FS.deleteAsync(localUri, { idempotent: true });
132
133      let error;
134      try {
135        await FS.readAsStringAsync(localUri);
136      } catch (e) {
137        error = e;
138      }
139      expect(error).toBeTruthy();
140    }, 9000);
141
142    it('delete(idempotent) -> !exists -> write -> read -> write -> read', async () => {
143      const localUri = FS.documentDirectory + 'write1.txt';
144
145      await FS.deleteAsync(localUri, { idempotent: true });
146
147      const { exists } = await FS.getInfoAsync(localUri);
148      expect(exists).not.toBeTruthy();
149
150      const writeAndVerify = async (expected) => {
151        await FS.writeAsStringAsync(localUri, expected);
152        const string = await FS.readAsStringAsync(localUri);
153        expect(string).toBe(expected);
154      };
155
156      await writeAndVerify('hello, world');
157      await writeAndVerify('hello, world!!!!!!');
158    });
159
160    it('delete(new) -> 2 * [write -> move -> !exists(orig) -> read(new)]', async () => {
161      const from = FS.documentDirectory + 'from.txt';
162      const to = FS.documentDirectory + 'to.txt';
163      const contents = ['contents 1', 'contents 2'];
164
165      await FS.deleteAsync(to, { idempotent: true });
166
167      // Move twice to make sure we can overwrite
168      for (let i = 0; i < 2; ++i) {
169        await FS.writeAsStringAsync(from, contents[i]);
170
171        await FS.moveAsync({ from, to });
172
173        const { exists } = await FS.getInfoAsync(from);
174        expect(exists).not.toBeTruthy();
175
176        expect(await FS.readAsStringAsync(to)).toBe(contents[i]);
177      }
178    });
179
180    it('delete(new) -> 2 * [write -> copy -> exists(orig) -> read(new)]', async () => {
181      const from = FS.documentDirectory + 'from.txt';
182      const to = FS.documentDirectory + 'to.txt';
183      const contents = ['contents 1', 'contents 2'];
184
185      await FS.deleteAsync(to, { idempotent: true });
186
187      // Copy twice to make sure we can overwrite
188      for (let i = 0; i < 2; ++i) {
189        await FS.writeAsStringAsync(from, contents[i]);
190
191        await FS.copyAsync({ from, to });
192
193        const { exists } = await FS.getInfoAsync(from);
194        expect(exists).toBeTruthy();
195
196        expect(await FS.readAsStringAsync(to)).toBe(contents[i]);
197      }
198    });
199
200    it(
201      'delete(dir) -> write(dir/file)[error] -> mkdir(dir) ->' +
202        'mkdir(dir)[error] -> write(dir/file) -> read',
203      async () => {
204        let error;
205        const path = FS.documentDirectory + 'dir/file';
206        const dir = FS.documentDirectory + 'dir';
207        const contents = 'hello, world';
208
209        await FS.deleteAsync(dir, { idempotent: true });
210
211        error = null;
212        try {
213          await FS.writeAsStringAsync(path, contents);
214        } catch (e) {
215          error = e;
216        }
217        expect(error).toBeTruthy();
218
219        await FS.makeDirectoryAsync(dir);
220
221        error = null;
222        try {
223          await FS.makeDirectoryAsync(dir);
224        } catch (e) {
225          error = e;
226        }
227        expect(error).toBeTruthy();
228
229        await FS.writeAsStringAsync(path, contents);
230
231        expect(await FS.readAsStringAsync(path)).toBe(contents);
232      }
233    );
234
235    it(
236      'delete(dir) -> write(dir/dir2/file)[error] -> ' +
237        'mkdir(dir/dir2, intermediates) -> ' +
238        'mkdir(dir/dir2, intermediates) -> write(dir/dir2/file) -> read',
239      async () => {
240        let error;
241        const path = FS.documentDirectory + 'dir/dir2/file';
242        const dir = FS.documentDirectory + 'dir/dir2';
243        const contents = 'hello, world';
244
245        await FS.deleteAsync(dir, { idempotent: true });
246
247        error = null;
248        try {
249          await FS.writeAsStringAsync(path, contents);
250        } catch (e) {
251          error = e;
252        }
253        expect(error).toBeTruthy();
254
255        await FS.makeDirectoryAsync(dir, {
256          intermediates: true,
257        });
258
259        error = null;
260        try {
261          await FS.makeDirectoryAsync(dir);
262        } catch (e) {
263          error = e;
264        }
265        expect(error).toBeTruthy();
266
267        error = null;
268        try {
269          await FS.makeDirectoryAsync(dir, {
270            intermediates: true,
271          });
272        } catch (e) {
273          error = e;
274        }
275        expect(error).toBe(null);
276
277        await FS.writeAsStringAsync(path, contents);
278
279        expect(await FS.readAsStringAsync(path)).toBe(contents);
280      }
281    );
282
283    it('getInfo(dirPath)', async () => {
284      const dir = FS.documentDirectory + 'dir';
285      const path = FS.documentDirectory + 'dir/file.txt';
286
287      await FS.deleteAsync(dir, { idempotent: true });
288      await FS.makeDirectoryAsync(dir, {
289        intermediates: true,
290      });
291      await FS.writeAsStringAsync(path, 'Expo is awesome ������');
292      const info = await FS.getInfoAsync(dir);
293
294      expect(info).toBeDefined();
295      expect(info.exists).toBe(true);
296      expect(info.isDirectory).toBe(true);
297      expect(info.size).toBe(28);
298    });
299
300    /*
301    This test fails in CI because of an exception being thrown by deleteAsync in the nativeModule.
302    I traced it down to the FileUtils.forceDelete call here:
303    https://github.com/expo/expo/blob/bcd136b096df84e0b0f72a15acbda45491de8201/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.java#L294
304    it(
305      'delete(dir, idempotent) -> make tree -> check contents ' +
306        '-> check directory listings' +
307        '-> move -> check directory listings' +
308        '-> copy -> check directory listings',
309      async () => {
310        let error;
311        const dir = FS.documentDirectory + 'dir';
312
313        await FS.deleteAsync(dir, { idempotent: true });
314
315        await FS.makeDirectoryAsync(dir + '/child1', {
316          intermediates: true,
317        });
318        await FS.makeDirectoryAsync(dir + '/child2', {
319          intermediates: true,
320        });
321
322        await FS.writeAsStringAsync(dir + '/file1', 'contents1');
323        await FS.writeAsStringAsync(dir + '/file2', 'contents2');
324
325        await FS.writeAsStringAsync(dir + '/child1/file3', 'contents3');
326
327        await FS.writeAsStringAsync(dir + '/child2/file4', 'contents4');
328        await FS.writeAsStringAsync(dir + '/child2/file5', 'contents5');
329
330        const checkContents = async (path, contents) =>
331          expect(await FS.readAsStringAsync(path)).toBe(contents);
332
333        await checkContents(dir + '/file1', 'contents1');
334        await checkContents(dir + '/file2', 'contents2');
335        await checkContents(dir + '/child1/file3', 'contents3');
336        await checkContents(dir + '/child2/file4', 'contents4');
337        await checkContents(dir + '/child2/file5', 'contents5');
338
339        const checkDirectory = async (path, expected) => {
340          const list = await FS.readDirectoryAsync(path);
341          expect(list.sort()).toEqual(expected.sort());
342        };
343
344        const checkRoot = async root => {
345          await checkDirectory(root, ['file1', 'file2', 'child1', 'child2']);
346          await checkDirectory(root + '/child1', ['file3']);
347          await checkDirectory(root + '/child2', ['file4', 'file5']);
348
349          error = null;
350          try {
351            await checkDirectory(root + '/file1', ['nope']);
352          } catch (e) {
353            error = e;
354          }
355          expect(error).toBeTruthy();
356        };
357
358        await checkRoot(dir);
359
360        await FS.deleteAsync(FS.documentDirectory + 'moved', {
361          idempotent: true,
362        });
363        await FS.moveAsync({ from: dir, to: FS.documentDirectory + 'moved' });
364        await checkRoot(FS.documentDirectory + 'moved');
365        await FS.copyAsync({
366          from: FS.documentDirectory + 'moved',
367          to: FS.documentDirectory + 'copied',
368        });
369        await checkRoot(FS.documentDirectory + 'copied');
370      }
371    );
372    */
373
374    it('delete(idempotent) -> download(md5) -> getInfo(size)', async () => {
375      const localUri = FS.documentDirectory + 'download1.png';
376
377      await FS.deleteAsync(localUri, { idempotent: true });
378
379      const { md5 } = await FS.downloadAsync(
380        'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png',
381        localUri,
382        { md5: true }
383      );
384      expect(md5).toBe('1e02045c10b8f1145edc7c8375998f87');
385
386      const { size, modificationTime } = await FS.getInfoAsync(localUri);
387      expect(size).toBe(3230);
388      const nowTime = 0.001 * new Date().getTime();
389      expect(nowTime - modificationTime).toBeLessThan(3600);
390
391      await FS.deleteAsync(localUri);
392    }, 30000);
393
394    it('missing parameters', async () => {
395      const p = FS.documentDirectory + 'test';
396
397      await throws(() => FS.moveAsync({ from: p }));
398      await throws(() => FS.moveAsync({ to: p }));
399      await throws(() => FS.copyAsync({ from: p }));
400      await throws(() => FS.copyAsync({ to: p }));
401    });
402
403    it('can read root directories', async () => {
404      await FS.readDirectoryAsync(FS.documentDirectory);
405      await FS.readDirectoryAsync(FS.cacheDirectory);
406    });
407
408    it('download(network failure)', async () => {
409      const localUri = FS.documentDirectory + 'download1.png';
410
411      const assertExists = async (expectedToExist) => {
412        const { exists } = await FS.getInfoAsync(localUri);
413        if (expectedToExist) {
414          expect(exists).toBeTruthy();
415        } else {
416          expect(exists).not.toBeTruthy();
417        }
418      };
419
420      await FS.deleteAsync(localUri, { idempotent: true });
421      await assertExists(false);
422
423      let error;
424      try {
425        await FS.downloadAsync('https://nonexistent-subdomain.expo.io', localUri, {
426          md5: true,
427          sessionType: FS.FileSystemSessionType.FOREGROUND,
428        });
429      } catch (e) {
430        error = e;
431      }
432      expect(error).toBeTruthy();
433      await assertExists(false);
434      await FS.deleteAsync(localUri, { idempotent: true });
435    }, 30000);
436
437    it('download(404)', async () => {
438      const localUri = FS.documentDirectory + 'download1.png';
439
440      const assertExists = async (expectedToExist) => {
441        const { exists } = await FS.getInfoAsync(localUri);
442        if (expectedToExist) {
443          expect(exists).toBeTruthy();
444        } else {
445          expect(exists).not.toBeTruthy();
446        }
447      };
448
449      await FS.deleteAsync(localUri, { idempotent: true });
450      await assertExists(false);
451
452      const { status } = await FS.downloadAsync('https://github.com/omg1231sdfaljs', localUri, {
453        md5: true,
454      });
455      await assertExists(true);
456      expect(status).toBe(404);
457
458      await FS.deleteAsync(localUri);
459      await assertExists(false);
460    }, 30000);
461
462    it('download(nonexistent local path)', async () => {
463      try {
464        const remoteUrl = 'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png';
465        const localUri = FS.documentDirectory + 'doesnt/exists/download1.png';
466        await FS.downloadAsync(remoteUrl, localUri);
467      } catch (err) {
468        expect(err.message).toMatch(/Directory '.*' does not exist/);
469      }
470    }, 30000);
471
472    it('mkdir(multi-level) + download(multi-level local path)', async () => {
473      const remoteUrl = 'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png';
474      const localDirUri = FS.documentDirectory + 'foo/bar/baz';
475      const localFileUri = localDirUri + 'download1.png';
476
477      await FS.makeDirectoryAsync(localDirUri, { intermediates: true });
478
479      await FS.downloadAsync(remoteUrl, localFileUri);
480    }, 30000);
481
482    it('create UTF-8 folder and get info', async () => {
483      const folderName = '中文';
484      const folderUri = FS.documentDirectory + folderName;
485
486      const dirInfo = await FS.getInfoAsync(folderUri);
487      if (dirInfo.exists) {
488        await FS.deleteAsync(folderUri);
489      }
490
491      await FS.makeDirectoryAsync(folderUri);
492      const newDirInfo = await FS.getInfoAsync(folderUri);
493
494      expect(newDirInfo.exists).toBeTruthy();
495      expect(newDirInfo.isDirectory).toBeTruthy();
496    }, 30000);
497  });
498}
499