Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -16779,9 +16779,20 @@ function setupFormValidation() {

forms.forEach((form) => {
// Add validation to name fields
const nameFields = form.querySelectorAll(
'input[name*="name"], input[name*="Name"]',
);
// Target only the actual technical name inputs (avoid matching displayName)
const nameFields = Array.from(
form.querySelectorAll(
'input[name="name"], input[name="customName"], input[name="custom_name"]',
),
).filter((f) => {
// Exclude hidden inputs and any display-name-like fields so
// display names remain optional and aren't validated here.
if (!f) return false;
if (f.type && f.type.toLowerCase() === "hidden") return false;
if (/display/i.test(f.name || "")) return false;
return true;
});

nameFields.forEach((field) => {
field.addEventListener("blur", function () {
const parentNode = this.parentNode;
Expand Down
164 changes: 164 additions & 0 deletions tests/js/admin-form-validation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* Unit tests for setupFormValidation() name-field selection logic.
*
* Verifies that only technical name inputs receive blur validation,
* and that displayName / hidden name fields are correctly excluded.
*/

import {
describe,
test,
expect,
beforeAll,
beforeEach,
afterAll,
} from "vitest";
import { loadAdminJs, cleanupAdminJs } from "./helpers/admin-env.js";

let win;
let doc;

beforeAll(() => {
win = loadAdminJs();
win.MAX_NAME_LENGTH = 200;
doc = win.document;
});

afterAll(() => {
cleanupAdminJs();
});

beforeEach(() => {
doc.body.textContent = "";
});

/**
* Build a minimal form with the given input elements,
* call setupFormValidation(), and return the form.
*/
function buildForm(inputs) {
const form = doc.createElement("form");
inputs.forEach(({ name, type, value, id }) => {
const wrapper = doc.createElement("div");
const input = doc.createElement("input");
input.name = name;
if (type) input.type = type;
if (value !== undefined) input.value = value;
if (id) input.id = id;
wrapper.appendChild(input);
form.appendChild(wrapper);
});
doc.body.appendChild(form);
win.setupFormValidation();
return form;
}

/** Dispatch a blur event on the given element. */
function blur(el) {
el.dispatchEvent(new win.Event("blur"));
}

// ---------------------------------------------------------------------------
// setupFormValidation — name field selection
// ---------------------------------------------------------------------------
describe("setupFormValidation name-field selection", () => {
test("validates visible input[name='name']", () => {
const form = buildForm([{ name: "name", type: "text", value: "" }]);
const input = form.querySelector('input[name="name"]');
blur(input);
// Empty name triggers validation error
expect(input.validity.customError).toBe(true);
});

test("validates input[name='customName']", () => {
const form = buildForm([
{ name: "customName", type: "text", value: "" },
]);
const input = form.querySelector('input[name="customName"]');
blur(input);
expect(input.validity.customError).toBe(true);
});

test("does NOT validate input[name='displayName']", () => {
const form = buildForm([
{ name: "displayName", type: "text", value: "" },
]);
const input = form.querySelector('input[name="displayName"]');
blur(input);
// displayName is excluded — no custom validity set
expect(input.validity.customError).toBe(false);
});

test("does NOT validate input[name='display_name']", () => {
const form = buildForm([
{ name: "display_name", type: "text", value: "" },
]);
const input = form.querySelector('input[name="display_name"]');
blur(input);
expect(input.validity.customError).toBe(false);
});

test("does NOT validate hidden input[name='name']", () => {
const form = buildForm([{ name: "name", type: "hidden", value: "" }]);
const input = form.querySelector('input[name="name"]');
blur(input);
expect(input.validity.customError).toBe(false);
});

test("validates valid name and clears error styling", () => {
const form = buildForm([
{
name: "name",
type: "text",
value: "my-tool",
id: "tool-name",
},
]);
const input = form.querySelector('input[name="name"]');
blur(input);
expect(input.validity.customError).toBe(false);
expect(input.classList.contains("border-red-500")).toBe(false);
});

test("shows error styling on invalid name", () => {
const form = buildForm([{ name: "name", type: "text", value: "" }]);
const input = form.querySelector('input[name="name"]');
blur(input);
expect(input.classList.contains("border-red-500")).toBe(true);
});

test("form with both name and displayName only validates name", () => {
const form = buildForm([
{ name: "name", type: "text", value: "" },
{ name: "displayName", type: "text", value: "" },
]);
const nameInput = form.querySelector('input[name="name"]');
const displayInput = form.querySelector('input[name="displayName"]');

blur(nameInput);
blur(displayInput);

// Only the technical name field gets validation
expect(nameInput.validity.customError).toBe(true);
expect(displayInput.validity.customError).toBe(false);
});

test("edit form: hidden name excluded, customName validated", () => {
const form = buildForm([
{ name: "name", type: "hidden", value: "original-name" },
{ name: "customName", type: "text", value: "" },
{ name: "displayName", type: "text", value: "" },
]);
const hiddenName = form.querySelector('input[name="name"]');
const customName = form.querySelector('input[name="customName"]');
const displayName = form.querySelector('input[name="displayName"]');

blur(hiddenName);
blur(customName);
blur(displayName);

expect(hiddenName.validity.customError).toBe(false);
expect(customName.validity.customError).toBe(true);
expect(displayName.validity.customError).toBe(false);
});
});
Loading