Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ 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")),
Comment thread
Valyrian-Code marked this conversation as resolved.
slug_value: z
.string()
.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")),
Comment thread
Valyrian-Code marked this conversation as resolved.
Comment thread
Valyrian-Code marked this conversation as resolved.
derived_from_uri: z.string().nullable(),
status: z.nativeEnum(Status),
classification: z.nativeEnum(Classification),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ function ObservationDefinitionFormContent({

const formSchema = z
.object({
title: z.string().min(1, t("field_required")),
title: z.string().trim().min(1, t("field_required")),
Comment thread
Valyrian-Code marked this conversation as resolved.
slug_value: z
.string()
.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")),
Comment thread
Valyrian-Code marked this conversation as resolved.
Comment thread
Valyrian-Code marked this conversation as resolved.
status: z.nativeEnum(ObservationDefinitionStatus),
category: z.enum(
OBSERVATION_DEFINITION_CATEGORY as [string, ...string[]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ 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")),
Comment thread
Valyrian-Code marked this conversation as resolved.
slug_value: z
.string()
.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")),
Comment thread
Valyrian-Code marked this conversation as resolved.
derived_from_uri: z
.string()
.url({ message: t("field_required") })
Comment thread
Valyrian-Code marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
}) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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(),
);
Comment thread
Valyrian-Code marked this conversation as resolved.
});

test("should create observation definition with root-level interpretation", async ({
page,
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,6 +46,21 @@ 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(
[
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,
}) => {
Expand Down
29 changes: 28 additions & 1 deletion tests/helper/error.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,3 +16,30 @@ 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 — 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)
*
* @example
* await expectWhitespaceRejected(
* [page.getByRole("textbox", { name: "Title *" })],
* () => page.getByRole("button", { name: "Create" }).click(),
* );
*/
export async function expectWhitespaceRejected(
fields: Locator[],
submit: () => Promise<void>,
): Promise<void> {
Comment thread
Valyrian-Code marked this conversation as resolved.
for (const field of fields) {
await field.fill(" ");
}
await submit();
for (const field of fields) {
await expect(getFieldErrorMessage(field)).toBeVisible();
}
}
Comment thread
Valyrian-Code marked this conversation as resolved.
Comment thread
Valyrian-Code marked this conversation as resolved.
Loading