discourse/plugins/discourse-ai/test/javascripts/unit/services/ai-bot-docked-submit-test.js
Keegan George 9f6588ce59
FIX: Resolve broken uploads in AI bot docked composer (#39712)
Previously, uploads in the AI bot docked composer were completely
non-functional due to two issues: a Glimmer modifier ordering race
condition where the file input's `didInsert` fired before the parent
container's `didInsert` (so `UppyUpload` was never bound to the file
input), and the submit service rejected upload-only submissions because
it early-returned on empty `raw` before considering attached uploads.

This change fixes the initialization order by calling
`uppyUpload.setup()` in `setupContainer` when the file input is already
available, restructures the submit validation to allow upload-only posts
and skip the min-length check when uploads are present, and adds
vertical padding to the uploads container for better visual spacing.

<img width="791" height="330" alt="Screenshot 2026-05-04 at 10 14 43"
src="https://github.com/user-attachments/assets/7a0b8e38-f29e-40f7-bac7-361ab549474c"
/>
2026-05-04 11:01:32 -07:00

213 lines
6.3 KiB
JavaScript
Vendored

import { getOwner } from "@ember/owner";
import { setupTest } from "ember-qunit";
import { module, test } from "qunit";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
module("Unit | Service | ai-bot-docked-submit", function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
const siteSettings = getOwner(this).lookup("service:site-settings");
siteSettings.min_personal_message_post_length = 10;
});
test("returns null when topicId is missing", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
const result = await service.submitReply({
topicId: null,
raw: "Hello world, this is long enough",
});
assert.strictEqual(result, null);
});
test("returns null when raw is empty and no uploads", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
const result = await service.submitReply({
topicId: 42,
raw: "",
uploads: [],
inProgressUploadsCount: 0,
});
assert.strictEqual(result, null);
});
test("returns null and alerts when raw is shorter than min length and no uploads", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
let requestsCount = 0;
pretender.post("/posts.json", () => {
requestsCount += 1;
return response(200, {});
});
const result = await service.submitReply({
topicId: 42,
raw: "hi",
uploads: [],
inProgressUploadsCount: 0,
});
assert.strictEqual(result, null);
assert.strictEqual(requestsCount, 0, "no POST when too short");
});
test("submits with only uploads and no raw text", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
let rawSent;
pretender.post("/posts.json", (request) => {
const params = new URLSearchParams(request.requestBody);
rawSent = params.get("raw");
return response(200, { id: 1, topic_id: 42 });
});
const result = await service.submitReply({
topicId: 42,
raw: "",
uploads: [
{
short_url: "upload://abc123.pdf",
original_filename: "document.pdf",
extension: "pdf",
filesize: 1024,
},
],
inProgressUploadsCount: 0,
});
assert.notStrictEqual(
result,
null,
"submission succeeds with only uploads"
);
assert.true(
rawSent.includes("upload://abc123.pdf"),
"upload markdown is sent"
);
});
test("skips min length check when uploads are present", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
let rawSent;
pretender.post("/posts.json", (request) => {
const params = new URLSearchParams(request.requestBody);
rawSent = params.get("raw");
return response(200, { id: 1, topic_id: 42 });
});
const result = await service.submitReply({
topicId: 42,
raw: "hi",
uploads: [
{
short_url: "upload://abc123.png",
original_filename: "screenshot.png",
extension: "png",
width: 400,
height: 300,
},
],
inProgressUploadsCount: 0,
});
assert.notStrictEqual(
result,
null,
"submission succeeds with short text + uploads"
);
assert.true(rawSent.includes("hi"), "raw text preserved");
assert.true(
rawSent.includes("upload://abc123.png"),
"upload markdown appended"
);
});
test("returns null when uploads are still in progress", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
let requestsCount = 0;
pretender.post("/posts.json", () => {
requestsCount += 1;
return response(200, {});
});
const result = await service.submitReply({
topicId: 42,
raw: "Long enough message here",
uploads: [],
inProgressUploadsCount: 2,
});
assert.strictEqual(result, null);
assert.strictEqual(requestsCount, 0);
});
test("POSTs raw + topic_id + nested_post flag", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
let submittedBody;
pretender.post("/posts.json", (request) => {
submittedBody = request.requestBody;
return response(200, { id: 999, topic_id: 42 });
});
const result = await service.submitReply({
topicId: 42,
raw: "Long enough message body",
uploads: [],
inProgressUploadsCount: 0,
});
assert.strictEqual(result.id, 999);
assert.true(submittedBody.includes("topic_id=42"));
assert.true(submittedBody.includes("nested_post=true"));
});
test("appends upload markdown to raw content", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
const formDataFromBody = (body) => {
const params = new URLSearchParams(body);
return params.get("raw");
};
let rawSent;
pretender.post("/posts.json", (request) => {
rawSent = formDataFromBody(request.requestBody);
return response(200, { id: 1, topic_id: 42 });
});
await service.submitReply({
topicId: 42,
raw: "Here is a file",
uploads: [
{
short_url: "upload://abc123.png",
original_filename: "screenshot.png",
extension: "png",
width: 400,
height: 300,
},
],
inProgressUploadsCount: 0,
});
assert.true(rawSent.includes("Here is a file"), "raw text preserved");
assert.true(
rawSent.includes("upload://abc123.png"),
"upload markdown appended"
);
});
test("loading state flips around the request", async function (assert) {
const service = getOwner(this).lookup("service:ai-bot-docked-submit");
pretender.post("/posts.json", () => response(200, { id: 1, topic_id: 42 }));
assert.false(service.loading, "initially not loading");
const promise = service.submitReply({
topicId: 42,
raw: "Long enough message body",
uploads: [],
inProgressUploadsCount: 0,
});
assert.true(service.loading, "loading while in-flight");
await promise;
assert.false(service.loading, "cleared after response");
});
});