diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d103c0..1914ef17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Lines marked **Upgrade step** need you to act after updating. ### Fixed - 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) - Ticking "Remember me" on the login page now keeps you logged in after your browser session ends. Before, it was ignored. (#227) - The counter under the text editor shows the length of your text as soon as a text opens. Before, it showed a larger number until your first edit. (#228) - A question or completion page text that holds only an image keeps the image when you save it. Before, it was saved as empty and the image was lost. (#229) diff --git a/resources/js/components/CharCount.vue b/resources/js/components/CharCount.vue index bf528e5c..0a05ce7b 100644 --- a/resources/js/components/CharCount.vue +++ b/resources/js/components/CharCount.vue @@ -19,7 +19,7 @@ import { computed, ref } from "vue"; const props = defineProps<{ text?: string; - maxChars?: number; + maxChars?: number | null; }>(); const charCount = ref(props.text?.length ?? 0); diff --git a/resources/js/components/Factory/Main/ConfigureTextarea.test.ts b/resources/js/components/Factory/Main/ConfigureTextarea.test.ts new file mode 100644 index 00000000..e15f4743 --- /dev/null +++ b/resources/js/components/Factory/Main/ConfigureTextarea.test.ts @@ -0,0 +1,67 @@ +import { flushPromises, mount } from "@vue/test-utils"; +import { createPinia, setActivePinia } from "pinia"; +import { describe, expect, it, vi } from "vitest"; +import ConfigureTextarea from "./ConfigureTextarea.vue"; +import { useWorkbench } from "@/stores"; + +const saved = vi.hoisted(() => ({ + options: {} as Record, +})); + +vi.mock("../utils/useInteractionsUtils", () => ({ + useInteractionsUtils: () => ({ + findOrCreate: async () => ({ id: 7, label: "", options: saved.options }), + }), +})); + +// a long text answer that was saved before, with the given options +const reopen = async (options: Record) => { + saved.options = options; + const pinia = createPinia(); + setActivePinia(pinia); + const workbench = useWorkbench(); + workbench.block = { + interactions: [{ type: "textarea" }], + } as unknown as FormBlockModel; + const save = vi + .spyOn(workbench, "updateInteraction") + .mockImplementation(() => {}); + + const wrapper = mount(ConfigureTextarea, { + global: { plugins: [pinia], provide: { block: { type: "input" } } }, + }); + await flushPromises(); + + return { wrapper, save }; +}; + +describe("long text max characters", () => { + it("stays empty and saves nothing when a question with no limit reopens", async () => { + const { wrapper, save } = await reopen({ rows: 5, max_chars: null }); + + expect( + (wrapper.find("input#textarea-max-chars").element as HTMLInputElement) + .value, + ).toBe(""); + expect(save).not.toHaveBeenCalled(); + }); + + it("shows the saved limit when a question with a limit reopens", async () => { + const { wrapper } = await reopen({ rows: 5, max_chars: 120 }); + + expect( + (wrapper.find("input#textarea-max-chars").element as HTMLInputElement) + .value, + ).toBe("120"); + }); + + it("saves no limit when the owner empties the field", async () => { + const { wrapper, save } = await reopen({ rows: 5, max_chars: 500 }); + + await wrapper.find("input#textarea-max-chars").setValue(""); + await flushPromises(); + + expect(save).toHaveBeenCalledTimes(1); + expect(save.mock.lastCall?.[0].options.max_chars).toBeNull(); + }); +}); diff --git a/resources/js/components/Factory/Main/ConfigureTextarea.vue b/resources/js/components/Factory/Main/ConfigureTextarea.vue index 8791ebeb..bf271507 100644 --- a/resources/js/components/Factory/Main/ConfigureTextarea.vue +++ b/resources/js/components/Factory/Main/ConfigureTextarea.vue @@ -31,7 +31,7 @@ (""); const rows = ref(5); -const maxChars = ref(500); +// empty means no limit; a new question starts at 500 +const maxChars = ref(500); const interaction = ref(null) as unknown as Ref; onMounted(async () => { @@ -65,7 +66,7 @@ onMounted(async () => { label.value = interaction.value.label; rows.value = interaction.value.options?.rows ?? 5; - maxChars.value = interaction.value.options?.max_chars ?? 500; + maxChars.value = isNew ? 500 : (interaction.value.options?.max_chars ?? ""); await settled(isNew); } }); @@ -76,7 +77,7 @@ const settled = useSettingsWatch([label, rows, maxChars], (newValues) => { label: newValues[0], options: { rows: Math.max(1, Math.min(10, parseInt(newValues[1]))), - max_chars: parseInt(newValues[2]), + max_chars: parseInt(newValues[2]) || null, }, }; diff --git a/resources/js/types/models.d.ts b/resources/js/types/models.d.ts index 359fcffb..b6d4232d 100644 --- a/resources/js/types/models.d.ts +++ b/resources/js/types/models.d.ts @@ -167,7 +167,7 @@ type FormBlockInteractionType = type FormBlockInteractionSettings = { rows?: number; - max_chars?: number; + max_chars?: number | null; minDate?: string; maxDate?: string; noPastDates?: boolean;