diff --git a/packages/dropzone/e2e/upload-error.spec.js b/packages/dropzone/e2e/upload-error.spec.js
new file mode 100644
index 000000000..72ee86689
--- /dev/null
+++ b/packages/dropzone/e2e/upload-error.spec.js
@@ -0,0 +1,29 @@
+import { test, expect } from "@playwright/test";
+import { dropFile } from "./support/drop-file.js";
+
+// The unit suite drives failures through a fake XMLHttpRequest. This is the
+// same path against the built bundle, a real request and a real 500, so the
+// error reaches the preview the way a user would see it.
+test.describe("Dropzone against a server that rejects the upload", () => {
+ test("marks the file as errored and shows the message", async ({ page }) => {
+ await page.goto("/1-basic/upload_error.html");
+
+ const upload = page.waitForResponse(
+ (response) =>
+ response.request().method() === "POST" && new URL(response.url()).pathname === "/fail",
+ );
+
+ await dropFile(page, ".dropzone", "image.jpg", "image/jpeg");
+ await upload;
+
+ const preview = page.locator(".dz-preview");
+ await expect(preview).toHaveClass(/dz-error/);
+ await expect(preview).not.toHaveClass(/dz-success/);
+ // The server answers with {"error": "..."} as application/json, and the
+ // default error handler unwraps that `error` key, so what the user sees is
+ // the server's own message rather than dictResponseError.
+ await expect(preview.locator("[data-dz-errormessage]")).toContainText(
+ "Upload rejected by the server",
+ );
+ });
+});
diff --git a/packages/dropzone/test/test-server.js b/packages/dropzone/test/test-server.js
index b67a65a2b..9542bf1f1 100644
--- a/packages/dropzone/test/test-server.js
+++ b/packages/dropzone/test/test-server.js
@@ -56,6 +56,15 @@ const httpServer = http.createServer((req, res) => {
.on("data", () => {})
.on("end", () => {
const headers = { "Content-Type": "application/json" };
+
+ // Lets the end-to-end tests drive a rejected upload. Everything else
+ // here succeeds, so the failure path had no way of being reached.
+ if (req.url.startsWith("/fail")) {
+ res.writeHead(500, headers);
+ res.end('{"error": "Upload rejected by the server"}');
+ return;
+ }
+
if (req.url.startsWith("/amazon-multipart-upload")) {
headers.ETag = `"${Math.round(Math.random() * 10000)}"`;
}
diff --git a/packages/dropzone/test/test-sites/1-basic/upload_error.html b/packages/dropzone/test/test-sites/1-basic/upload_error.html
new file mode 100644
index 000000000..a3d140e0f
--- /dev/null
+++ b/packages/dropzone/test/test-sites/1-basic/upload_error.html
@@ -0,0 +1,12 @@
+
+
+
Dropzone Test — upload error
+
+
+
+
+
+
+
diff --git a/packages/dropzone/test/unit-tests/all.js b/packages/dropzone/test/unit-tests/all.js
index c5a214f1c..78de3f045 100644
--- a/packages/dropzone/test/unit-tests/all.js
+++ b/packages/dropzone/test/unit-tests/all.js
@@ -75,7 +75,7 @@ describe("Dropzone", function () {
return expect(dropzone.options.url).toBe("real-action");
});
- return describe("options", function () {
+ describe("options", function () {
let element = null;
let element2 = null;
beforeEach(function () {
@@ -125,7 +125,7 @@ describe("Dropzone", function () {
return expect(dropzone.options.acceptedFiles).toBe("my/type");
});
- return describe("options.clickable", function () {
+ describe("options.clickable", function () {
let clickableElement = null;
dropzone = null;
beforeEach(function () {
@@ -348,7 +348,7 @@ describe("Dropzone", function () {
}));
});
- return describe("file specific", function () {
+ describe("file specific", function () {
let file = null;
beforeEach(function () {
file = {
@@ -425,7 +425,7 @@ describe("Dropzone", function () {
).toEqual("100%");
}));
- return describe(".resize()", function () {
+ describe(".resize()", function () {
describe("with default thumbnail settings", function () {
it("should properly return target dimensions for 'contain'", function () {
let info = dropzone.options.resize.call(dropzone, file, 120, 120, "crop");
@@ -446,7 +446,7 @@ describe("Dropzone", function () {
});
});
- return describe("with null thumbnail settings", function () {
+ describe("with null thumbnail settings", function () {
it("should properly return target dimensions for crop", function () {
let testSettings = [
[null, null],
@@ -870,7 +870,7 @@ describe("Dropzone", function () {
});
});
- return describe("events", () => {
+ describe("events", () => {
describe("progress updates", () =>
it("should properly emit a totaluploadprogress event", () =>
new Promise((done) => {
@@ -1121,7 +1121,7 @@ describe("Dropzone", function () {
}, 10);
})));
- return describe("getFilesWithStatus()", () =>
+ describe("getFilesWithStatus()", () =>
it("should return all files with provided status", function () {
let mock1 = getMockFile();
let mock2 = getMockFile();
@@ -1290,7 +1290,7 @@ describe("Dropzone", function () {
return expect(dropzone.removeFile.mock.calls.length).toEqual(2);
});
- return describe("thumbnails", function () {
+ describe("thumbnails", function () {
it("should properly queue the thumbnail creation", () =>
new Promise((done) => {
let ct_callback;
@@ -1377,7 +1377,7 @@ describe("Dropzone", function () {
expect(thumbnail.draggable).toBe(false);
});
- return describe("when file is SVG", () =>
+ describe("when file is SVG", () =>
it("should use the SVG image itself", () =>
new Promise((done) => {
let createBlob = function (data, type) {
@@ -2224,7 +2224,7 @@ describe("Dropzone", function () {
}));
});
- return describe("should properly set status of file", () =>
+ describe("should properly set status of file", () =>
it("should correctly set `withCredentials` on the xhr object", () =>
new Promise((done) => {
dropzone.addFile(mockFile);
@@ -2328,7 +2328,7 @@ describe("Dropzone", function () {
}));
});
- return describe("complete file", () =>
+ describe("complete file", () =>
it("should properly emit the queuecomplete event when the complete queue is finished", () =>
new Promise((done) => {
let mock1 = getMockFile("text/html", "mock1");
diff --git a/packages/dropzone/test/unit-tests/drag-and-drop.js b/packages/dropzone/test/unit-tests/drag-and-drop.js
new file mode 100644
index 000000000..db75ce2bf
--- /dev/null
+++ b/packages/dropzone/test/unit-tests/drag-and-drop.js
@@ -0,0 +1,172 @@
+import { vi } from "vitest";
+import { Dropzone } from "../../src/dropzone.js";
+
+// The listeners the constructor binds to the element. The existing suite calls
+// dropzone.drop() directly, which skips this wiring entirely -- so every one of
+// these handlers, the whole point of the library, was previously unreached.
+describe("drag and drop", function () {
+ let element = null;
+ let dropzone = null;
+
+ beforeEach(function () {
+ element = document.createElement("div");
+ // The class matters: the default message, and so the .dz-message element
+ // the click handler looks for, is only injected when it is present.
+ element.className = "dropzone";
+ document.body.appendChild(element);
+ dropzone = new Dropzone(element, { url: "/upload", autoProcessQueue: false });
+ });
+
+ afterEach(function () {
+ dropzone.destroy();
+ element.remove();
+ });
+
+ // A drag event carrying whatever dataTransfer the test needs. `types` is what
+ // the library inspects to decide whether a drag is worth intercepting at all.
+ let dragEvent = function (type, { types = ["Files"], effectAllowed } = {}) {
+ let event = new Event(type, { bubbles: true, cancelable: true });
+ event.dataTransfer = { types, effectAllowed, dropEffect: null, files: [], items: [] };
+ return event;
+ };
+
+ describe("events", function () {
+ for (let type of ["dragstart", "dragenter", "dragover", "dragleave", "dragend"]) {
+ it(`should emit ${type}`, function () {
+ let received = null;
+ dropzone.on(type, (e) => (received = e));
+
+ let event = dragEvent(type);
+ element.dispatchEvent(event);
+
+ expect(received).toBe(event);
+ });
+ }
+
+ it("should hand a drop to drop()", function () {
+ let drop = vi.spyOn(dropzone, "drop").mockImplementation(() => {});
+
+ let event = dragEvent("drop");
+ element.dispatchEvent(event);
+
+ expect(drop).toHaveBeenCalledTimes(1);
+ expect(drop.mock.calls[0][0]).toBe(event);
+ });
+ });
+
+ // A drag carrying anything other than files belongs to whatever else is on
+ // the page, so the library deliberately keeps its hands off it.
+ describe("propagation", function () {
+ for (let type of ["dragenter", "dragover", "drop"]) {
+ it(`should swallow ${type} when the drag carries files`, function () {
+ vi.spyOn(dropzone, "drop").mockImplementation(() => {});
+ let event = dragEvent(type);
+ let stop = vi.spyOn(event, "stopPropagation");
+ let prevent = vi.spyOn(event, "preventDefault");
+
+ element.dispatchEvent(event);
+
+ expect(stop).toHaveBeenCalled();
+ expect(prevent).toHaveBeenCalled();
+ });
+
+ it(`should leave ${type} alone when the drag carries no files`, function () {
+ vi.spyOn(dropzone, "drop").mockImplementation(() => {});
+ let event = dragEvent(type, { types: ["text/plain"] });
+ let stop = vi.spyOn(event, "stopPropagation");
+ let prevent = vi.spyOn(event, "preventDefault");
+
+ element.dispatchEvent(event);
+
+ expect(stop).not.toHaveBeenCalled();
+ expect(prevent).not.toHaveBeenCalled();
+ });
+ }
+
+ it("should not be confused by a dataTransfer with no types at all", function () {
+ let event = new Event("dragenter", { bubbles: true, cancelable: true });
+ event.dataTransfer = { files: [], items: [] };
+ let stop = vi.spyOn(event, "stopPropagation");
+
+ expect(() => element.dispatchEvent(event)).not.toThrow();
+ expect(stop).not.toHaveBeenCalled();
+ });
+ });
+
+ // Without this, dragging a file out of Chrome's download bar drops nothing:
+ // the browser needs to be told the drag is a copy.
+ describe("dropEffect", function () {
+ it("should be copy by default", function () {
+ let event = dragEvent("dragover");
+ element.dispatchEvent(event);
+ expect(event.dataTransfer.dropEffect).toBe("copy");
+ });
+
+ for (let effectAllowed of ["move", "linkMove"]) {
+ it(`should be move when effectAllowed is ${effectAllowed}`, function () {
+ let event = dragEvent("dragover", { effectAllowed });
+ element.dispatchEvent(event);
+ expect(event.dataTransfer.dropEffect).toBe("move");
+ });
+ }
+
+ it("should survive a dataTransfer that throws when read", function () {
+ // Internet Explorer 11 threw SCRIPT65535 here, which is why the read is
+ // wrapped. The guard is still load-bearing for any exotic dataTransfer.
+ let event = new Event("dragover", { bubbles: true, cancelable: true });
+ event.dataTransfer = {
+ types: ["Files"],
+ get effectAllowed() {
+ throw new Error("nope");
+ },
+ set dropEffect(value) {
+ this._dropEffect = value;
+ },
+ get dropEffect() {
+ return this._dropEffect;
+ },
+ };
+
+ expect(() => element.dispatchEvent(event)).not.toThrow();
+ expect(event.dataTransfer.dropEffect).toBe("copy");
+ });
+ });
+
+ describe("clicking", function () {
+ it("should forward a click on the element to the hidden input", function () {
+ let click = vi.spyOn(dropzone.hiddenFileInput, "click").mockImplementation(() => {});
+
+ element.dispatchEvent(new Event("click", { bubbles: true }));
+
+ expect(click).toHaveBeenCalledTimes(1);
+ });
+
+ it("should forward a click on the message element", function () {
+ let message = element.querySelector(".dz-message");
+ let click = vi.spyOn(dropzone.hiddenFileInput, "click").mockImplementation(() => {});
+
+ message.dispatchEvent(new Event("click", { bubbles: true }));
+
+ expect(click).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not forward a click on some other child", function () {
+ let other = document.createElement("span");
+ element.appendChild(other);
+ let click = vi.spyOn(dropzone.hiddenFileInput, "click").mockImplementation(() => {});
+
+ other.dispatchEvent(new Event("click", { bubbles: true }));
+
+ expect(click).not.toHaveBeenCalled();
+ });
+
+ it("should not forward anything once disabled", function () {
+ let click = vi.spyOn(dropzone.hiddenFileInput, "click").mockImplementation(() => {});
+ dropzone.disable();
+
+ element.dispatchEvent(new Event("click", { bubbles: true }));
+
+ expect(click).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/dropzone/test/unit-tests/emitter.js b/packages/dropzone/test/unit-tests/emitter.js
index fbc6db428..e0993ac93 100644
--- a/packages/dropzone/test/unit-tests/emitter.js
+++ b/packages/dropzone/test/unit-tests/emitter.js
@@ -70,7 +70,7 @@ describe("Emitter", function () {
return expect(callCount2).toBe(1);
});
- return describe(".off()", function () {
+ describe(".off()", function () {
let callback1 = function () {};
let callback2 = function () {};
let callback3 = function () {};
@@ -120,4 +120,48 @@ describe("Emitter", function () {
return expect(emt).toBe(emitter);
});
});
+
+ describe("off() with nothing to remove", function () {
+ it("should be a no-op for an event that has no listeners", function () {
+ emitter.on("test", function () {});
+ let returned = emitter.off("unknown");
+
+ expect(returned).toBe(emitter);
+ expect(emitter._callbacks["test"].length).toBe(1);
+ });
+
+ it("should be a no-op before any listener was registered", function () {
+ expect(emitter.off("unknown")).toBe(emitter);
+ });
+ });
+
+ describe("makeEvent()", function () {
+ it("should build a CustomEvent carrying the detail", function () {
+ let event = emitter.makeEvent("dropzone:test", { args: [1, 2] });
+
+ expect(event).toBeInstanceOf(CustomEvent);
+ expect(event.type).toBe("dropzone:test");
+ expect(event.bubbles).toBe(true);
+ expect(event.cancelable).toBe(true);
+ expect(event.detail).toEqual({ args: [1, 2] });
+ });
+
+ // The branch behind this exists for IE 11, which cannot be reached from a
+ // modern browser without taking the constructor away. It is on the 7.0
+ // list for deletion; until then this at least proves it still works.
+ it("should fall back to initCustomEvent when the constructor is missing", function () {
+ let CustomEventConstructor = window.CustomEvent;
+ window.CustomEvent = undefined;
+
+ try {
+ let event = emitter.makeEvent("dropzone:test", { args: [3] });
+
+ expect(event.type).toBe("dropzone:test");
+ expect(event.bubbles).toBe(true);
+ expect(event.detail).toEqual({ args: [3] });
+ } finally {
+ window.CustomEvent = CustomEventConstructor;
+ }
+ });
+ });
});
diff --git a/packages/dropzone/test/unit-tests/paste.js b/packages/dropzone/test/unit-tests/paste.js
new file mode 100644
index 000000000..5c1ac8cd1
--- /dev/null
+++ b/packages/dropzone/test/unit-tests/paste.js
@@ -0,0 +1,97 @@
+import { vi } from "vitest";
+import { Dropzone } from "../../src/dropzone.js";
+
+describe("paste", function () {
+ let element = null;
+ let dropzone = null;
+
+ beforeEach(function () {
+ element = document.createElement("div");
+ document.body.appendChild(element);
+ dropzone = new Dropzone(element, { url: "/upload", autoProcessQueue: false });
+ });
+
+ afterEach(function () {
+ dropzone.destroy();
+ element.remove();
+ });
+
+ let clipboardEvent = (items) => ({ clipboardData: items == null ? undefined : { items } });
+ let fileItem = (name) => ({
+ kind: "file",
+ webkitGetAsEntry: () => null,
+ getAsFile: () => new File(["x"], name),
+ });
+
+ describe("guards", function () {
+ it("should ignore an event with no clipboardData", function () {
+ let addFiles = vi.spyOn(dropzone, "_addFilesFromItems");
+ dropzone.paste({});
+ expect(addFiles).not.toHaveBeenCalled();
+ });
+
+ it("should ignore clipboardData with no items", function () {
+ let addFiles = vi.spyOn(dropzone, "_addFilesFromItems");
+ dropzone.paste(clipboardEvent(null));
+ expect(addFiles).not.toHaveBeenCalled();
+ });
+
+ it("should ignore being called with nothing at all", function () {
+ expect(() => dropzone.paste()).not.toThrow();
+ });
+
+ it("should not reach the items when the clipboard is empty", function () {
+ let addFiles = vi.spyOn(dropzone, "_addFilesFromItems");
+ dropzone.paste(clipboardEvent([]));
+ expect(addFiles).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("with files on the clipboard", function () {
+ it("should emit paste with the event", function () {
+ let received = null;
+ dropzone.on("paste", (e) => (received = e));
+
+ let event = clipboardEvent([fileItem("pasted.png")]);
+ dropzone.paste(event);
+
+ expect(received).toBe(event);
+ });
+
+ it("should emit paste before adding anything", function () {
+ let order = [];
+ dropzone.on("paste", () => order.push("paste"));
+ dropzone.on("addedfile", () => order.push("addedfile"));
+
+ dropzone.paste(clipboardEvent([fileItem("pasted.png")]));
+
+ expect(order[0]).toBe("paste");
+ });
+
+ it("should add the pasted file", function () {
+ dropzone.paste(clipboardEvent([fileItem("pasted.png")]));
+
+ expect(dropzone.files.map((f) => f.name)).toEqual(["pasted.png"]);
+ });
+
+ it("should add every pasted file", function () {
+ dropzone.paste(clipboardEvent([fileItem("one.png"), fileItem("two.png")]));
+
+ expect(dropzone.files.map((f) => f.name)).toEqual(["one.png", "two.png"]);
+ });
+ });
+
+ // Documenting current behaviour rather than endorsing it. drop() emits
+ // addedfiles as of 6.2 and the hidden input's change handler always has;
+ // paste is the one entry point that does not, which the 7.0 roadmap lists as
+ // an inconsistency to settle. If this test starts failing because paste
+ // learned to emit it, that is the fix landing -- update the test.
+ it("should not emit addedfiles, unlike the other two entry points", function () {
+ let addedfiles = vi.fn();
+ dropzone.on("addedfiles", addedfiles);
+
+ dropzone.paste(clipboardEvent([fileItem("pasted.png")]));
+
+ expect(addedfiles).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/dropzone/test/unit-tests/public-api.js b/packages/dropzone/test/unit-tests/public-api.js
new file mode 100644
index 000000000..aaa3bc9e6
--- /dev/null
+++ b/packages/dropzone/test/unit-tests/public-api.js
@@ -0,0 +1,242 @@
+import { vi } from "vitest";
+import { Dropzone } from "../../src/dropzone.js";
+import defaultOptions from "../../src/options.js";
+
+// Documented, publicly reachable behaviour that nothing exercised: the
+// fallback form, files added by the server, the removal confirmations, and
+// Dropzone.discover.
+describe("public API", function () {
+ let element = null;
+ let dropzone = null;
+
+ let mockFile = (name = "server.png") => ({ name, size: 123 });
+
+ afterEach(function () {
+ if (dropzone != null && typeof dropzone.destroy === "function") dropzone.destroy();
+ if (element != null) element.remove();
+ dropzone = element = null;
+ });
+
+ let create = (options = {}, html = '') => {
+ element = Dropzone.createElement(html);
+ document.body.appendChild(element);
+ dropzone = new Dropzone(element, { url: "/upload", autoProcessQueue: false, ...options });
+ return dropzone;
+ };
+
+ describe("displayExistingFile()", function () {
+ it("should emit addedfile and complete", function () {
+ create();
+ let events = [];
+ dropzone.on("addedfile", () => events.push("addedfile"));
+ dropzone.on("complete", () => events.push("complete"));
+
+ dropzone.displayExistingFile(mockFile(), "/thumb.png", null, null, false);
+
+ expect(events).toEqual(["addedfile", "complete"]);
+ });
+
+ it("should emit the thumbnail url unchanged when not resizing", function () {
+ create();
+ let thumbnail = null;
+ dropzone.on("thumbnail", (file, url) => (thumbnail = url));
+
+ dropzone.displayExistingFile(mockFile(), "/thumb.png", null, null, false);
+
+ expect(thumbnail).toBe("/thumb.png");
+ });
+
+ it("should run the callback when not resizing", function () {
+ create();
+ let callback = vi.fn();
+
+ dropzone.displayExistingFile(mockFile(), "/thumb.png", callback, null, false);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it("should keep the url on the file when resizing", function () {
+ create();
+ vi.spyOn(dropzone, "createThumbnailFromUrl").mockImplementation(() => {});
+ let file = mockFile();
+
+ dropzone.displayExistingFile(file, "/thumb.png");
+
+ expect(file.dataURL).toBe("/thumb.png");
+ expect(dropzone.createThumbnailFromUrl).toHaveBeenCalledTimes(1);
+ });
+
+ // #2003 and the 7.0 roadmap: files added this way never reach this.files,
+ // so maxFiles cannot see them. The documented workaround is to push them
+ // by hand, which is why fixing it is a breaking change rather than a
+ // patch. This pins the behaviour so the fix is a deliberate act.
+ it("should not count towards maxFiles", function () {
+ create({ maxFiles: 1 });
+
+ dropzone.displayExistingFile(mockFile(), "/thumb.png", null, null, false);
+
+ expect(dropzone.files).toHaveLength(0);
+ });
+ });
+
+ describe("fallback()", function () {
+ // Worth pinning: fallback() ends with `return this.element.appendChild(...)`,
+ // and a constructor returning an object overrides `this`, so this call
+ // hands back the fallback input element rather than a Dropzone.
+ let fallbackOn = () => {
+ element = Dropzone.createElement('');
+ document.body.appendChild(element);
+ return new Dropzone(element, { url: "/upload", forceFallback: true });
+ };
+
+ it("should return the fallback element, not a Dropzone", function () {
+ let returned = fallbackOn();
+
+ expect(returned).toBeInstanceOf(HTMLElement);
+ expect(returned).not.toBeInstanceOf(Dropzone);
+ });
+
+ it("should mark the element as unsupported", function () {
+ fallbackOn();
+
+ expect(element.className).toContain("dz-browser-not-supported");
+ });
+
+ it("should show the fallback message", function () {
+ fallbackOn();
+
+ expect(element.textContent).toContain(defaultOptions.dictFallbackMessage);
+ });
+
+ it("should add a file input that posts to the url", function () {
+ fallbackOn();
+
+ expect(element.querySelector("input[type=file]")).not.toBe(null);
+ expect(element.querySelector("form").getAttribute("action")).toBe("/upload");
+ });
+ });
+
+ describe("removal confirmation", function () {
+ let removeLink = () => element.querySelector("a[data-dz-remove]");
+
+ let addFile = () => {
+ let file = new File(["contents"], "file.txt", { type: "text/plain" });
+ dropzone.addFile(file);
+ return dropzone.files[0];
+ };
+
+ it("should ask before removing when dictRemoveFileConfirmation is set", function () {
+ create({ addRemoveLinks: true, dictRemoveFileConfirmation: "Really?" });
+ let confirm = vi.spyOn(Dropzone, "confirm").mockImplementation(() => {});
+ addFile();
+
+ removeLink().dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
+
+ expect(confirm).toHaveBeenCalledTimes(1);
+ expect(confirm.mock.calls[0][0]).toBe("Really?");
+ confirm.mockRestore();
+ });
+
+ it("should remove the file when the confirmation is accepted", function () {
+ create({ addRemoveLinks: true, dictRemoveFileConfirmation: "Really?" });
+ vi.spyOn(Dropzone, "confirm").mockImplementation((message, accepted) => accepted());
+ let file = addFile();
+
+ removeLink().dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
+
+ expect(dropzone.files).not.toContain(file);
+ Dropzone.confirm.mockRestore();
+ });
+
+ it("should not ask when there is no confirmation message", function () {
+ create({ addRemoveLinks: true });
+ let confirm = vi.spyOn(Dropzone, "confirm").mockImplementation(() => {});
+ let file = addFile();
+
+ removeLink().dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
+
+ expect(confirm).not.toHaveBeenCalled();
+ expect(dropzone.files).not.toContain(file);
+ confirm.mockRestore();
+ });
+
+ it("should ask with the cancel message while the file is uploading", function () {
+ create({ addRemoveLinks: true });
+ let confirm = vi.spyOn(Dropzone, "confirm").mockImplementation(() => {});
+ let file = addFile();
+ file.status = Dropzone.UPLOADING;
+
+ removeLink().dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
+
+ expect(confirm.mock.calls[0][0]).toBe(dropzone.options.dictCancelUploadConfirmation);
+ confirm.mockRestore();
+ });
+ });
+
+ describe("renameFilename", function () {
+ it("should be wrapped into renameFile for backwards compatibility", function () {
+ create({ renameFilename: (name) => `prefix-${name}` });
+
+ expect(dropzone.options.renameFile({ name: "photo.png" })).toBe("prefix-photo.png");
+ });
+
+ it("should receive the name and the file", function () {
+ let seen = null;
+ create({
+ renameFilename(name, file) {
+ seen = { name, file };
+ return name;
+ },
+ });
+ let file = { name: "photo.png" };
+
+ dropzone.options.renameFile(file);
+
+ expect(seen.name).toBe("photo.png");
+ expect(seen.file).toBe(file);
+ });
+ });
+
+ describe("handleFiles()", function () {
+ it("should add every file it is given", function () {
+ create();
+
+ dropzone.handleFiles([
+ new File(["a"], "a.txt", { type: "text/plain" }),
+ new File(["b"], "b.txt", { type: "text/plain" }),
+ ]);
+
+ expect(dropzone.files.map((f) => f.name)).toEqual(["a.txt", "b.txt"]);
+ });
+ });
+
+ describe("Dropzone.discover()", function () {
+ it("should attach to elements carrying the dropzone class", function () {
+ element = Dropzone.createElement('');
+ element.setAttribute("action", "/upload");
+ document.body.appendChild(element);
+
+ Dropzone.discover();
+
+ expect(element.dropzone).toBeInstanceOf(Dropzone);
+ dropzone = element.dropzone;
+ });
+
+ // Opting out is done by id, not by a class: Dropzone.optionsForElement
+ // looks up Dropzone.options[camelizedId], and discover skips the element
+ // when that is exactly false.
+ it("should skip an element whose options are false", function () {
+ element = Dropzone.createElement('');
+ element.setAttribute("action", "/upload");
+ document.body.appendChild(element);
+ Dropzone.options.optedOut = false;
+
+ try {
+ Dropzone.discover();
+ expect(element.dropzone).toBeUndefined();
+ } finally {
+ delete Dropzone.options.optedOut;
+ }
+ });
+ });
+});
diff --git a/packages/dropzone/test/unit-tests/upload-errors.js b/packages/dropzone/test/unit-tests/upload-errors.js
new file mode 100644
index 000000000..b4f4d14f1
--- /dev/null
+++ b/packages/dropzone/test/unit-tests/upload-errors.js
@@ -0,0 +1,153 @@
+import { vi } from "vitest";
+import { Dropzone } from "../../src/dropzone.js";
+import { useFakeXMLHttpRequest } from "../fake-xhr.js";
+import { sleep } from "./utils";
+
+// The transport-level failure paths. onload had tests; ontimeout, onerror and
+// the progress forwarding did not, which is the half of uploading that only
+// runs when something has already gone wrong.
+describe("upload failures", function () {
+ let getMockFile = (filename = "test file name") => {
+ let file = new File(["file contents"], filename, { type: "text/html" });
+ file.status = Dropzone.ADDED;
+ file.accepted = true;
+ file.upload = { filename };
+ return file;
+ };
+
+ let xhr = null;
+ let element = null;
+ let dropzone = null;
+ let requests = null;
+
+ beforeEach(function () {
+ xhr = useFakeXMLHttpRequest();
+ requests = [];
+ xhr.onCreate = (request) => requests.push(request);
+
+ element = Dropzone.createElement("");
+ document.body.appendChild(element);
+ });
+
+ afterEach(function () {
+ dropzone.destroy();
+ element.remove();
+ xhr.restore();
+ });
+
+ let start = (options = {}) => {
+ dropzone = new Dropzone(element, { url: "/upload", timeout: 30000, ...options });
+ return dropzone;
+ };
+
+ describe("a request that times out", function () {
+ it("should report the timeout in seconds", async function () {
+ start();
+ let errors = [];
+ dropzone.on("error", (file, message) => errors.push(message));
+
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+ requests[0].ontimeout();
+
+ expect(errors).toEqual(["Request timedout after 30 seconds"]);
+ });
+
+ it("should mark the file as errored", async function () {
+ start();
+ let file = getMockFile();
+
+ dropzone.addFile(file);
+ await sleep(10);
+ requests[0].ontimeout();
+
+ expect(file.status).toBe(Dropzone.ERROR);
+ });
+
+ it("should still emit complete", async function () {
+ start();
+ let complete = vi.fn();
+ dropzone.on("complete", complete);
+
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+ requests[0].ontimeout();
+
+ expect(complete).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("a request that errors", function () {
+ it("should emit error with the default message", async function () {
+ start();
+ let errors = [];
+ dropzone.on("error", (file, message) => errors.push(message));
+
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+ requests[0].onerror();
+
+ expect(errors).toEqual([dropzone.options.dictResponseError.replace("{{statusCode}}", "0")]);
+ });
+
+ it("should leave a cancelled file alone", async function () {
+ start();
+ let error = vi.fn();
+ dropzone.on("error", error);
+ let file = getMockFile();
+
+ dropzone.addFile(file);
+ await sleep(10);
+ file.status = Dropzone.CANCELED;
+ requests[0].onerror();
+
+ expect(error).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("progress", function () {
+ it("should forward upload progress to the file", async function () {
+ start();
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+
+ requests[0].upload.onprogress({ lengthComputable: true, loaded: 50, total: 100 });
+
+ expect(dropzone.files[0].upload.progress).toBe(50);
+ });
+
+ it("should emit uploadprogress", async function () {
+ start();
+ let progress = [];
+ dropzone.on("uploadprogress", (file, percent) => progress.push(percent));
+
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+ requests[0].upload.onprogress({ lengthComputable: true, loaded: 25, total: 100 });
+
+ expect(progress).toContain(25);
+ });
+ });
+
+ describe("_getChunk", function () {
+ it("should find the chunk belonging to an xhr", async function () {
+ start({ forceChunking: true, chunking: true, chunkSize: 4 });
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+
+ let file = dropzone.files[0];
+ let chunk = dropzone._getChunk(file, requests[0]);
+
+ expect(chunk).toBeDefined();
+ expect(chunk.xhr).toBe(requests[0]);
+ });
+
+ it("should return undefined for an xhr that belongs to no chunk", async function () {
+ start({ forceChunking: true, chunking: true, chunkSize: 4 });
+ dropzone.addFile(getMockFile());
+ await sleep(10);
+
+ expect(dropzone._getChunk(dropzone.files[0], {})).toBeUndefined();
+ });
+ });
+});