File size: 3,267 Bytes
3201ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { processFiles, processImages } from "#/utils/file-processing";

const reads = vi.hoisted(() => ({
  arrayBuffers: [] as string[],
  dataUrls: [] as string[],
}));

class ControlledFileReader {
  onload: (() => void) | null = null;
  onerror: (() => void) | null = null;
  onabort: (() => void) | null = null;

  private finish(file: File) {
    queueMicrotask(() => {
      if (file.name.startsWith("error")) {
        this.onerror?.();
      } else if (file.name.startsWith("abort")) {
        this.onabort?.();
      } else {
        this.onload?.();
      }
    });
  }

  readAsArrayBuffer(file: File) {
    reads.arrayBuffers.push(file.name);
    this.finish(file);
  }

  readAsDataURL(file: File) {
    reads.dataUrls.push(file.name);
    this.finish(file);
  }
}

function file(name: string, type = "text/plain") {
  return new File(["content"], name, { type });
}

describe("file processing", () => {
  beforeEach(() => {
    reads.arrayBuffers = [];
    reads.dataUrls = [];
    vi.stubGlobal("FileReader", ControlledFileReader);
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("returns successful files in input order while retaining read failures", async () => {
    const first = file("first.txt");
    const failed = file("error-report.txt");
    const last = file("last.txt");

    const result = await processFiles([first, failed, last]);

    expect(reads.arrayBuffers).toEqual([
      "first.txt",
      "error-report.txt",
      "last.txt",
    ]);
    expect(result.successful).toEqual([first, last]);
    expect(result.failed).toEqual([
      {
        file: failed,
        error: new Error("Failed to read file: error-report.txt"),
      },
    ]);
  });

  it("reports aborted regular-file reads", async () => {
    const aborted = file("abort-upload.txt");

    await expect(processFiles([aborted])).resolves.toEqual({
      successful: [],
      failed: [
        {
          file: aborted,
          error: new Error("File reading was aborted: abort-upload.txt"),
        },
      ],
    });
  });

  it("reads images as data URLs and isolates errors and aborts", async () => {
    const successful = file("photo.png", "image/png");
    const failed = file("error-photo.png", "image/png");
    const aborted = file("abort-photo.png", "image/png");

    const result = await processImages([successful, failed, aborted]);

    expect(reads.dataUrls).toEqual([
      "photo.png",
      "error-photo.png",
      "abort-photo.png",
    ]);
    expect(result.successful).toEqual([successful]);
    expect(result.failed).toEqual([
      {
        file: failed,
        error: new Error("Failed to read image: error-photo.png"),
      },
      {
        file: aborted,
        error: new Error("Image reading was aborted: abort-photo.png"),
      },
    ]);
  });

  it("handles empty file and image batches without creating readers", async () => {
    await expect(processFiles([])).resolves.toEqual({
      successful: [],
      failed: [],
    });
    await expect(processImages([])).resolves.toEqual({
      successful: [],
      failed: [],
    });
    expect(reads.arrayBuffers).toEqual([]);
    expect(reads.dataUrls).toEqual([]);
  });
});