diff --git a/src/pages/Facility/settings/activityDefinition/ActivityDefinitionForm.tsx b/src/pages/Facility/settings/activityDefinition/ActivityDefinitionForm.tsx index 53f0781ce97..c8b0611402c 100644 --- a/src/pages/Facility/settings/activityDefinition/ActivityDefinitionForm.tsx +++ b/src/pages/Facility/settings/activityDefinition/ActivityDefinitionForm.tsx @@ -129,13 +129,14 @@ function ActivityDefinitionFormContent({ const { t } = useTranslation(); const formSchema = z.object({ - title: z.string().min(1, t("field_required")), + title: z.string().trim().min(1, t("field_required")), slug_value: z .string() + .trim() .min(5, t("character_count_validation", { min: 5, max: 25 })) .max(25, t("character_count_validation", { min: 5, max: 25 })), - description: z.string().min(1, t("field_required")), - usage: z.string().min(1, t("field_required")), + description: z.string().trim().min(1, t("field_required")), + usage: z.string().trim().min(1, t("field_required")), derived_from_uri: z.string().nullable(), status: z.nativeEnum(Status), classification: z.nativeEnum(Classification), diff --git a/src/pages/Facility/settings/observationDefinition/ObservationDefinitionForm.tsx b/src/pages/Facility/settings/observationDefinition/ObservationDefinitionForm.tsx index 4298316299e..2c37e49f34b 100644 --- a/src/pages/Facility/settings/observationDefinition/ObservationDefinitionForm.tsx +++ b/src/pages/Facility/settings/observationDefinition/ObservationDefinitionForm.tsx @@ -140,12 +140,13 @@ function ObservationDefinitionFormContent({ const formSchema = z .object({ - title: z.string().min(1, t("field_required")), + title: z.string().trim().min(1, t("field_required")), slug_value: z .string() + .trim() .min(5, t("character_count_validation", { min: 5, max: 25 })) .max(25, t("character_count_validation", { min: 5, max: 25 })), - description: z.string().min(1, t("field_required")), + description: z.string().trim().min(1, t("field_required")), status: z.nativeEnum(ObservationDefinitionStatus), category: z.enum( OBSERVATION_DEFINITION_CATEGORY as [string, ...string[]], diff --git a/src/pages/Facility/settings/specimen-definitions/SpecimenDefinitionForm.tsx b/src/pages/Facility/settings/specimen-definitions/SpecimenDefinitionForm.tsx index 95bf1131687..2d4afe15810 100644 --- a/src/pages/Facility/settings/specimen-definitions/SpecimenDefinitionForm.tsx +++ b/src/pages/Facility/settings/specimen-definitions/SpecimenDefinitionForm.tsx @@ -179,13 +179,14 @@ function SpecimenDefinitionFormContent({ const isEditMode = Boolean(specimenSlug); const formSchema = z.object({ - title: z.string().min(1, t("field_required")), + title: z.string().trim().min(1, t("field_required")), slug_value: z .string() + .trim() .min(5, t("character_count_validation", { min: 5, max: 25 })) .max(25, t("character_count_validation", { min: 5, max: 25 })), status: z.nativeEnum(SpecimenDefinitionStatus), - description: z.string().min(1, t("field_required")), + description: z.string().trim().min(1, t("field_required")), derived_from_uri: z .string() .url({ message: t("field_required") }) diff --git a/tests/facility/settings/activityDefinition/activityDefinitionCreate.spec.ts b/tests/facility/settings/activityDefinition/activityDefinitionCreate.spec.ts index 3fc0a13b660..ea278376ecf 100644 --- a/tests/facility/settings/activityDefinition/activityDefinitionCreate.spec.ts +++ b/tests/facility/settings/activityDefinition/activityDefinitionCreate.spec.ts @@ -4,7 +4,10 @@ import { createActivityDefinition, RESOURCE_CATEGORY_SLUG, } from "tests/facility/settings/activityDefinition/activityDefinition"; -import { getFieldErrorMessage } from "tests/helper/error"; +import { + expectWhitespaceRejected, + getFieldErrorMessage, +} from "tests/helper/error"; import { getCardByTitle } from "tests/helper/ui"; import { getFacilityId } from "tests/support/facilityId"; @@ -60,6 +63,24 @@ test.describe("activity definition form", () => { ); }); + test("should reject whitespace-only required text fields", async ({ + page, + }) => { + await page.goto( + `/facility/${facilityId}/settings/activity_definitions/categories/f-${facilityId}-${RESOURCE_CATEGORY_SLUG}/new`, + ); + + // title, description and usage are all trimmed before the required check. + await expectWhitespaceRejected( + [ + page.getByRole("textbox", { name: "Title *" }), + page.getByRole("textbox", { name: "Description *" }), + page.getByRole("textbox", { name: "Usage *" }), + ], + () => page.getByRole("button", { name: "Create" }).click(), + ); + }); + test("should create activity definition with required fields", async ({ page, }) => { diff --git a/tests/facility/settings/observationDefinition/observationDefinition.spec.ts b/tests/facility/settings/observationDefinition/observationDefinition.spec.ts index d22d3c9cb3a..846ee03adca 100644 --- a/tests/facility/settings/observationDefinition/observationDefinition.spec.ts +++ b/tests/facility/settings/observationDefinition/observationDefinition.spec.ts @@ -1,6 +1,7 @@ import { faker } from "@faker-js/faker"; import { expect, test } from "@playwright/test"; +import { expectWhitespaceRejected } from "tests/helper/error"; import { getFacilityId } from "tests/support/facilityId"; const UNITS = [ @@ -201,6 +202,19 @@ test.describe("Observation Definition Form with Interpretation", () => { await page.goto(targetUrl); }); + test("should reject whitespace-only required text fields", async ({ + page, + }) => { + // title and description are both trimmed before the required check. + await expectWhitespaceRejected( + [ + page.getByRole("textbox", { name: "Title" }), + page.getByRole("textbox", { name: "Description" }), + ], + () => page.getByRole("button", { name: "Create" }).click(), + ); + }); + test("should create observation definition with root-level interpretation", async ({ page, }) => { diff --git a/tests/facility/settings/specimenDefinitions/specimenDefinitionCreate.spec.ts b/tests/facility/settings/specimenDefinitions/specimenDefinitionCreate.spec.ts index 898fa6032b6..05346e785d2 100644 --- a/tests/facility/settings/specimenDefinitions/specimenDefinitionCreate.spec.ts +++ b/tests/facility/settings/specimenDefinitions/specimenDefinitionCreate.spec.ts @@ -11,7 +11,10 @@ import { STATUS_OPTIONS, typeCollectedOptions, } from "tests/facility/settings/specimenDefinitions/specimenDefinitionConstants"; -import { getFieldErrorMessage } from "tests/helper/error"; +import { + expectWhitespaceRejected, + getFieldErrorMessage, +} from "tests/helper/error"; import { getFacilityId } from "tests/support/facilityId"; // Use the authenticated state @@ -43,6 +46,23 @@ test.describe("Specimen Definitions Create", () => { await page.goto(targetUrl); }); + test("should reject whitespace-only required text fields", async ({ + page, + }) => { + await page.getByRole("button", { name: "Add Definition" }).click(); + + // title and description are both trimmed before the required check. + await expectWhitespaceRejected( + [ + // "Title *" / "Description *" target the required fields specifically; + // the form also has a nested optional "Description" (container) field. + page.getByRole("textbox", { name: "Title *" }), + page.getByRole("textbox", { name: "Description *" }), + ], + () => page.getByRole("button", { name: /save/i }).click(), + ); + }); + test("should create specimen definition with all fields", async ({ page, }) => { diff --git a/tests/helper/error.ts b/tests/helper/error.ts index d3aa01d2aad..beee403ce5f 100644 --- a/tests/helper/error.ts +++ b/tests/helper/error.ts @@ -1,4 +1,4 @@ -import type { Locator } from "@playwright/test"; +import { expect, type Locator } from "@playwright/test"; /** * Gets the error message element for a form field. @@ -16,3 +16,35 @@ import type { Locator } from "@playwright/test"; export function getFieldErrorMessage(fieldLocator: Locator): Locator { return fieldLocator.locator("..").locator('[data-slot="form-message"]'); } + +/** + * Fills the given required text fields with whitespace-only input, submits the + * form, and asserts each field shows its required-field error message — i.e. + * that the schema trims the value before the `.min(1)` check. + * + * @param fields - Required free-text field locators (textboxes/textareas) + * @param submit - Action that submits the form (e.g. clicking Create/Save) + * @param expectedError - Text/regex the field error must contain + * (default: `/required/i`, matching "This field is required") + * + * @example + * await expectWhitespaceRejected( + * [page.getByRole("textbox", { name: /^Title\b/ })], + * () => page.getByRole("button", { name: "Create" }).click(), + * ); + */ +export async function expectWhitespaceRejected( + fields: Locator[], + submit: () => Promise, + expectedError: string | RegExp = /required/i, +): Promise { + for (const field of fields) { + await field.fill(" "); + } + await submit(); + for (const field of fields) { + const error = getFieldErrorMessage(field); + await expect(error).toBeVisible(); + await expect(error).toContainText(expectedError); + } +}