import { acceptance, chromeTest, createFile, loggedInUser, paste, query, } from "discourse/tests/helpers/qunit-helpers"; import { withPluginApi } from "discourse/lib/plugin-api"; import { authorizedExtensions, dialog } from "discourse/lib/uploads"; import { click, fillIn, settled, visit } from "@ember/test-helpers"; import I18n from "I18n"; import { skip, test } from "qunit"; import { Promise } from "rsvp"; import sinon from "sinon"; let uploadNumber = 1; function pretender(server, helper) { server.post("/uploads/lookup-urls", () => { return helper.response([ { url: "/images/discourse-logo-sketch-small.png", short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", }, { url: "/images/discourse-logo-sketch-small.png", short_path: "/uploads/short-url/sdfljsdfgjlkwg4328.jpeg", short_url: "upload://sdfljsdfgjlkwg4328.jpeg", }, ]); }); server.post( "/uploads.json", () => { let response = null; if (uploadNumber === 1) { response = { extension: "jpeg", filesize: 126177, height: 800, human_filesize: "123 KB", id: 202, original_filename: "avatar.PNG.jpg", retain_hours: null, short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", thumbnail_height: 320, thumbnail_width: 690, url: "/images/discourse-logo-sketch-small.png", width: 1920, }; uploadNumber += 1; } else { response = { extension: "jpeg", filesize: 4322, height: 800, human_filesize: "566 KB", id: 202, original_filename: "avatar2.PNG.jpg", retain_hours: null, short_path: "/uploads/short-url/sdfljsdfgjlkwg4328.jpeg", short_url: "upload://sdfljsdfgjlkwg4328.jpeg", thumbnail_height: 320, thumbnail_width: 690, url: "/images/discourse-logo-sketch-small.png", width: 1920, }; } return helper.response(response); }, 500 // this delay is important to slow down the uploads a bit so we can click elements in the UI like the cancel button ); } acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { needs.user(); needs.pretender(pretender); needs.settings({ simultaneous_uploads: 2, enable_rich_text_paste: true, allow_uncategorized_topics: true, }); needs.hooks.afterEach(() => { uploadNumber = 1; }); test("should insert the Uploading placeholder then the complete image placeholder", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" ); done(); }); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n" ); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); // TODO: On Firefox Evergreen this often fails, because the order of uploads // in markdown is reversed chromeTest( "handles adding one file for upload then adding another when the first is still in progress", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n![avatar2.PNG|690x320](upload://sdfljsdfgjlkwg4328.jpeg)\n" ); done(); }); let image2Added = false; appEvents.on("composer:upload-started", () => { if (!image2Added) { appEvents.trigger("composer:add-files", image2); image2Added = true; } }); const image1 = createFile("avatar.png"); const image2 = createFile("avatar2.png"); appEvents.trigger("composer:add-files", image1); } ); test("should handle placeholders correctly even if the OS rewrites ellipses", async function (assert) { const execCommand = document.execCommand; sinon.stub(document, "execCommand").callsFake(function (...args) { if (args[0] === "insertText") { args[2] = args[2].replace("...", "…"); } return execCommand.call(document, ...args); }); await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" ); done(); }); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png…]()\n" ); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); test("should error if too many files are added at once", async function (assert) { await visit("/"); await click("#create-topic"); const appEvents = loggedInUser().appEvents; const image = createFile("avatar.png"); const image1 = createFile("avatar1.png"); const image2 = createFile("avatar2.png"); const done = assert.async(); appEvents.on("composer:uploads-aborted", async () => { await settled(); assert.strictEqual( query(".dialog-body").textContent.trim(), I18n.t("post.errors.too_many_dragged_and_dropped_files", { count: 2, }), "it should warn about too many files added" ); await click(".dialog-footer .btn-primary"); done(); }); appEvents.trigger("composer:add-files", [image, image1, image2]); }); test("should error if an unauthorized extension file is added", async function (assert) { await visit("/"); await click("#create-topic"); const appEvents = loggedInUser().appEvents; const jsonFile = createFile("something.json", "application/json"); const done = assert.async(); appEvents.on("composer:uploads-aborted", async () => { await settled(); assert.strictEqual( query(".dialog-body").textContent.trim(), I18n.t("post.errors.upload_not_authorized", { authorized_extensions: authorizedExtensions( false, this.siteSettings ).join(", "), }), "it should warn about unauthorized extensions" ); await click(".dialog-footer .btn-primary"); done(); }); appEvents.trigger("composer:add-files", [jsonFile]); }); test("cancelling uploads clears the placeholders out", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); const image = createFile("avatar.png"); const image2 = createFile("avatar2.png"); const appEvents = loggedInUser().appEvents; let uploadStarted = 0; appEvents.on("composer:upload-started", () => { uploadStarted++; if (uploadStarted === 2) { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n[Uploading: avatar2.png...]()\n", "it should show the upload placeholders when the upload starts" ); } }); appEvents.on("composer:uploads-cancelled", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n", "it should clear the cancelled placeholders" ); }); await new Promise(function (resolve) { appEvents.on("composer:uploads-preprocessing-complete", function () { resolve(); }); appEvents.trigger("composer:add-files", [image, image2]); }); await click("#cancel-file-upload"); }); test("should insert a newline before and after an image when pasting in the end of the line", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n" ); }); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" ); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); test("should insert a newline before and after an image when pasting in the middle of the line", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image: Text after the image."); const textArea = query(".d-editor-input"); textArea.selectionStart = 10; textArea.selectionEnd = 10; const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n Text after the image." ); }); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n Text after the image." ); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); test("should insert a newline before and after an image when pasting with text selected", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn( ".d-editor-input", "The image: [paste here] Text after the image." ); const textArea = query(".d-editor-input"); textArea.selectionStart = 10; textArea.selectionEnd = 23; const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n Text after the image." ); }); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n Text after the image." ); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); test("should insert a newline only after an image when pasting into an empty composer", async function (assert) { await visit("/"); await click("#create-topic"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "[Uploading: avatar.png...]()\n" ); }); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" ); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); test("should insert a newline only after an image when pasting into a blank line", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:upload-started", () => { assert.strictEqual( query(".d-editor-input").value, "The image:\n[Uploading: avatar.png...]()\n" ); }); appEvents.on("composer:all-uploads-complete", async () => { await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" ); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); skip("should place cursor properly after inserting a placeholder", async function (assert) { const appEvents = loggedInUser().appEvents; const done = assert.async(); await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\ntext after image"); const input = query(".d-editor-input"); input.selectionStart = 10; input.selectionEnd = 10; appEvents.on("composer:all-uploads-complete", () => { // after uploading we have this in the textarea: // "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\ntext after image" // cursor should be just before "text after image": assert.equal(input.selectionStart, 76); assert.equal(input.selectionEnd, 76); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); test("should be able to paste a table with files and not upload the files", async function (assert) { await visit("/"); await click("#create-topic"); const appEvents = loggedInUser().appEvents; const done = assert.async(); let uppyEventFired = false; appEvents.on("composer:upload-started", () => { uppyEventFired = true; }); let element = query(".d-editor"); let inputElement = query(".d-editor-input"); inputElement.focus(); await paste(element, "\ta\tb\n1\t2\t3", { types: ["text/plain", "Files"], files: [createFile("avatar.png")], }); await settled(); assert.strictEqual( inputElement.value, "||a|b|\n|---|---|---|\n|1|2|3|\n", "only the plain text table is pasted" ); assert.strictEqual( uppyEventFired, false, "uppy does not start uploading the file" ); done(); }); }); acceptance("Uppy Composer Attachment - Upload Error", function (needs) { needs.user(); needs.pretender((server, helper) => { server.post("/uploads.json", () => { return helper.response(422, { success: false, errors: [ "There was an error uploading the file, the gif was way too cool.", ], }); }); }); needs.settings({ simultaneous_uploads: 2, allow_uncategorized_topics: true, }); test("should show an error message for the failed upload", async function (assert) { // Don't log the upload error const stub = sinon .stub(console, "error") .withArgs( sinon.match(/\[Uppy\]/), sinon.match(/Failed to upload avatar\.png/) ); await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:upload-error", async () => { sinon.assert.calledOnce(stub); await settled(); assert.strictEqual( query(".dialog-body").textContent.trim(), "There was an error uploading the file, the gif was way too cool.", "it should show the error message from the server" ); await click(".dialog-footer .btn-primary"); done(); }); const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); }); acceptance("Uppy Composer Attachment - Upload Handler", function (needs) { needs.user(); needs.pretender(pretender); needs.settings({ simultaneous_uploads: 2, allow_uncategorized_topics: true, }); needs.hooks.beforeEach(() => { withPluginApi("0.8.14", (api) => { api.addComposerUploadHandler(["png"], (files) => { const file = files[0]; const isNativeFile = file instanceof File ? "WAS" : "WAS NOT"; dialog.alert( `This is an upload handler test for ${file.name}. The file ${isNativeFile} a native file object.` ); }); }); }); test("should use upload handler if the matching extension is used and a single file is uploaded", async function (assert) { await visit("/"); await click("#create-topic"); const image = createFile("handler-test.png"); const appEvents = loggedInUser().appEvents; const done = assert.async(); appEvents.on("composer:uploads-aborted", async () => { await settled(); assert.strictEqual( query(".dialog-body").textContent.trim(), "This is an upload handler test for handler-test.png. The file WAS a native file object.", "it should show the dialog triggered by the upload handler" ); await click(".dialog-footer .btn-primary"); done(); }); appEvents.trigger("composer:add-files", [image]); }); });