Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/dropzone/e2e/upload-error.spec.js
Original file line number Diff line number Diff line change
@@ -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",
);
});
});
9 changes: 9 additions & 0 deletions packages/dropzone/test/test-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}"`;
}
Expand Down
12 changes: 12 additions & 0 deletions packages/dropzone/test/test-sites/1-basic/upload_error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dropzone Test — upload error</title>

<script src="../dist/dropzone-min.js"></script>
<link rel="stylesheet" href="../dist/dropzone.css" type="text/css" />

<form class="dropzone" action="/fail"></form>

<script>
new Dropzone(".dropzone");
</script>
22 changes: 11 additions & 11 deletions packages/dropzone/test/unit-tests/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -348,7 +348,7 @@ describe("Dropzone", function () {
}));
});

return describe("file specific", function () {
describe("file specific", function () {
let file = null;
beforeEach(function () {
file = {
Expand Down Expand Up @@ -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");
Expand All @@ -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],
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
172 changes: 172 additions & 0 deletions packages/dropzone/test/unit-tests/drag-and-drop.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
46 changes: 45 additions & 1 deletion packages/dropzone/test/unit-tests/emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {};
Expand Down Expand Up @@ -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;
}
});
});
});
Loading
Loading