diff --git a/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/chat_based_content_editor_controller.ts b/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/chat_based_content_editor_controller.ts index 8d78dc65..80acdfb7 100644 --- a/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/chat_based_content_editor_controller.ts +++ b/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/chat_based_content_editor_controller.ts @@ -326,19 +326,24 @@ export default class extends Controller { } try { - const csrfInput = document.querySelector('input[name="_csrf_token"]') as HTMLInputElement | null; + const form = this.cancelButtonTarget.closest("form"); + const csrfInput = form?.querySelector('input[name="_csrf_token"]') ?? null; const formData = new FormData(); if (csrfInput) { formData.append("_csrf_token", csrfInput.value); } - await fetch(cancelUrl, { + const cancelResponse = await fetch(cancelUrl, { method: "POST", headers: { "X-Requested-With": "XMLHttpRequest" }, body: formData, }); + if (!cancelResponse.ok) { + throw new Error(`Cancel request failed with status ${cancelResponse.status}`); + } + // Don't stop polling — let the polling loop detect the cancelled status // and done chunk naturally, so all pre-cancellation output is displayed. } catch { diff --git a/tests/frontend/integration/ChatBasedContentEditor/chat_editor_controller.test.ts b/tests/frontend/integration/ChatBasedContentEditor/chat_editor_controller.test.ts index 4b21a3eb..6e6b404d 100644 --- a/tests/frontend/integration/ChatBasedContentEditor/chat_editor_controller.test.ts +++ b/tests/frontend/integration/ChatBasedContentEditor/chat_editor_controller.test.ts @@ -99,7 +99,7 @@ describe("ChatBasedContentEditorController", () => { - + `; @@ -277,5 +277,95 @@ describe("ChatBasedContentEditorController", () => { }), ); }); + + it("stop button sends correct CSRF token from AI form, not global", async () => { + createChatEditorFixture(); + await waitForController(); + + const controller = application.getControllerForElementAndIdentifier( + document.querySelector('[data-controller="chat-based-content-editor"]') as HTMLElement, + "chat-based-content-editor", + ) as InstanceType; + + // Set up a mock polling state + const mockContainer = document.createElement("div"); + (controller as unknown as Record).currentPollingState = { + sessionId: "test-sess-123", + container: mockContainer, + lastId: 0, + pollUrl: "/api/poll/test-sess-123", + }; + + const fetchMock = vi.fn(async () => { + return { + ok: true, + status: 200, + json: async () => ({}), + }; + }); + + vi.stubGlobal("fetch", fetchMock); + + // Call handleCancel + await controller.handleCancel(); + + // Verify cancel request was made + expect(fetchMock).toHaveBeenCalledWith( + "/api/cancel/test-sess-123", + expect.objectContaining({ + method: "POST", + headers: { "X-Requested-With": "XMLHttpRequest" }, + }), + ); + + // Verify the CSRF token from the form was used + const mockCalls = fetchMock.mock.calls as unknown as Array<[unknown, unknown]>; + const cancelCall = mockCalls.find((call) => String(call[0]) === "/api/cancel/test-sess-123"); + if (cancelCall) { + const formData = (cancelCall[1] as Record)?.body as FormData | undefined; + expect(formData?.get("_csrf_token")).toBe("csrf-123"); + } + }); + + it("stop button re-enables on cancel failure and shows error state", async () => { + createChatEditorFixture(); + await waitForController(); + + const controller = application.getControllerForElementAndIdentifier( + document.querySelector('[data-controller="chat-based-content-editor"]') as HTMLElement, + "chat-based-content-editor", + ) as InstanceType; + + // Set up a mock polling state + const mockContainer = document.createElement("div"); + (controller as unknown as Record).currentPollingState = { + sessionId: "test-sess-fail", + container: mockContainer, + lastId: 0, + pollUrl: "/api/poll/test-sess-fail", + }; + + const fetchMock = vi.fn(async () => { + return { + ok: false, + status: 403, + json: async () => ({ error: "Forbidden" }), + }; + }); + + vi.stubGlobal("fetch", fetchMock); + + // Get the cancel button and verify it's disabled + const cancelButton = document.querySelector( + "[data-chat-based-content-editor-target='cancelButton']", + ) as HTMLButtonElement; + + // Call handleCancel + await controller.handleCancel(); + + // The catch block should re-enable the button + expect(cancelButton.disabled).toBe(false); + expect(cancelButton.textContent).toBe(translations.stop); + }); }); });