From 03e87aace1570e102182162d444de9ec70dcc699 Mon Sep 17 00:00:00 2001 From: Philipp Reinking Date: Sat, 17 Oct 2026 10:53:13 +0200 Subject: [PATCH 1/3] Fix the edges of the long text limit and height Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + .../Factory/Main/ConfigureTextarea.test.ts | 75 +++++++++++++++++-- .../Factory/Main/ConfigureTextarea.vue | 49 ++++++++++-- .../interactions/useTextareaAction.test.ts | 30 ++++++++ .../classic/interactions/useTextareaAction.ts | 2 +- resources/js/types/models.d.ts | 2 +- 6 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 resources/js/forms/classic/interactions/useTextareaAction.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 448a15af..7741c26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Lines marked **Upgrade step** need you to act after updating. ### Fixed +- A long text question with a limit of 100 characters now accepts exactly 100 characters and warns from 101. "Max characters" and "Height in lines" refuse 0, negative and fractional numbers and say why; leave them empty for no limit or the default height. An empty height stays empty when you open the question again. (#PR) - Browser Back now leaves a form's Settings page. Before, every tab you opened added a history step, so Back only went through the tabs. A link to a Settings tab still opens that tab. (#287) - A new long text question now shows in your form and in the editor preview, also before you set any options for it. Before, it failed to show. (#286) - A long text question with an empty "Max characters" now has no limit, also when you open it again. Before, the editor showed 500 and saved it back. New long text questions still start at 500. (#291) diff --git a/resources/js/components/Factory/Main/ConfigureTextarea.test.ts b/resources/js/components/Factory/Main/ConfigureTextarea.test.ts index e15f4743..441abe80 100644 --- a/resources/js/components/Factory/Main/ConfigureTextarea.test.ts +++ b/resources/js/components/Factory/Main/ConfigureTextarea.test.ts @@ -10,7 +10,11 @@ const saved = vi.hoisted(() => ({ vi.mock("../utils/useInteractionsUtils", () => ({ useInteractionsUtils: () => ({ - findOrCreate: async () => ({ id: 7, label: "", options: saved.options }), + findOrCreate: async () => ({ + id: 7, + label: "", + options: saved.options, + }), }), })); @@ -40,8 +44,10 @@ describe("long text max characters", () => { const { wrapper, save } = await reopen({ rows: 5, max_chars: null }); expect( - (wrapper.find("input#textarea-max-chars").element as HTMLInputElement) - .value, + ( + wrapper.find("input#textarea-max-chars") + .element as HTMLInputElement + ).value, ).toBe(""); expect(save).not.toHaveBeenCalled(); }); @@ -50,8 +56,10 @@ describe("long text max characters", () => { const { wrapper } = await reopen({ rows: 5, max_chars: 120 }); expect( - (wrapper.find("input#textarea-max-chars").element as HTMLInputElement) - .value, + ( + wrapper.find("input#textarea-max-chars") + .element as HTMLInputElement + ).value, ).toBe("120"); }); @@ -65,3 +73,60 @@ describe("long text max characters", () => { expect(save.mock.lastCall?.[0].options.max_chars).toBeNull(); }); }); + +describe("long text max characters, bad values", () => { + const maxInput = (wrapper: Awaited>["wrapper"]) => + wrapper.find("input#textarea-max-chars"); + + it.each(["0", "-5", "2.5"])( + "never saves %s and says why", + async (value) => { + const { wrapper, save } = await reopen({ rows: 5, max_chars: 500 }); + + await maxInput(wrapper).setValue(value); + await flushPromises(); + + expect(save).not.toHaveBeenCalled(); + expect(wrapper.find("#textarea-max-chars-error").exists()).toBe( + true, + ); + }, + ); + + it.each([0, -3])("shows a saved %s as empty", async (saved) => { + const { wrapper } = await reopen({ rows: 5, max_chars: saved }); + + expect((maxInput(wrapper).element as HTMLInputElement).value).toBe(""); + }); +}); + +describe("long text height", () => { + const rowsInput = ( + wrapper: Awaited>["wrapper"], + ) => wrapper.find("input#textarea-rows"); + + it("saves no height when the owner clears it, and keeps it empty on reopen", async () => { + const { wrapper, save } = await reopen({ rows: 5, max_chars: 500 }); + + await rowsInput(wrapper).setValue(""); + await flushPromises(); + + expect(save.mock.lastCall?.[0].options.rows).toBeNull(); + + const reopened = await reopen({ rows: null, max_chars: 500 }); + expect( + (rowsInput(reopened.wrapper).element as HTMLInputElement).value, + ).toBe(""); + expect(reopened.save).not.toHaveBeenCalled(); + }); + + it.each(["0", "-1", "2.5", "11"])("never saves a height of %s", async (value) => { + const { wrapper, save } = await reopen({ rows: 5, max_chars: 500 }); + + await rowsInput(wrapper).setValue(value); + await flushPromises(); + + expect(save).not.toHaveBeenCalled(); + expect(wrapper.find("#textarea-rows-error").exists()).toBe(true); + }); +}); diff --git a/resources/js/components/Factory/Main/ConfigureTextarea.vue b/resources/js/components/Factory/Main/ConfigureTextarea.vue index bf271507..def6d274 100644 --- a/resources/js/components/Factory/Main/ConfigureTextarea.vue +++ b/resources/js/components/Factory/Main/ConfigureTextarea.vue @@ -20,12 +20,20 @@ id="textarea-rows" placeholder="e.g. 4" type="number" - :min="1" - :max="10" step="1" block v-model="rows" + :aria-invalid="!rowsValid" + aria-describedby="textarea-rows-error" /> +
@@ -35,7 +43,17 @@ type="number" block v-model="maxChars" + :aria-invalid="!maxCharsValid" + aria-describedby="textarea-max-chars-error" /> +
@@ -43,7 +61,7 @@