Merge pull request #2025 from modelcontextprotocol/v2/fix/1853-json-editor-escaping
test(web): pin the JSON editor's reported escaping reproductions (#1853, #1856, #1885)
This commit is contained in:
commit
fcf0a6309b
1 changed files with 171 additions and 0 deletions
|
|
@ -1477,3 +1477,174 @@ describe("SchemaForm draft validity (#2020)", () => {
|
|||
expect(screen.getByText(/Not valid JSON/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Three separate reports of one defect: the JSON editor re-escaping the text
|
||||
// being typed. #1853 saw it while typing an array, #1856 on the Backspace that
|
||||
// first makes a valid array invalid, and #1885 on a nullable parameter whose
|
||||
// default `null` had to be edited in place. All three come from the same
|
||||
// mechanism — unparseable draft text stored back as the field's *value*, which
|
||||
// the next controlled render re-`JSON.stringify`d, adding a layer of quotes and
|
||||
// backslashes per keystroke.
|
||||
//
|
||||
// The mechanism was removed by the draft/value split in `SchemaJsonField`
|
||||
// (#1928/#2007) and the nullable-union collapse that keeps a `T | null` field
|
||||
// off this editor entirely (#1928). These lock each report's own reproduction
|
||||
// to it, since the fixes were made for differently-framed issues and nothing
|
||||
// otherwise pins the reported flows.
|
||||
describe("JSON editor escaping (#1853, #1856, #1885)", () => {
|
||||
function EscapingHarness({
|
||||
schema,
|
||||
initial = {},
|
||||
}: {
|
||||
schema: InspectorFormSchema;
|
||||
initial?: Record<string, unknown>;
|
||||
}) {
|
||||
const [values, setValues] = useState<Record<string, unknown>>(initial);
|
||||
return <SchemaForm schema={schema} values={values} onChange={setValues} />;
|
||||
}
|
||||
|
||||
// #1853: `batch_process_items`, typed character by character.
|
||||
it("types a string array through without escaping it (#1853)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const schema: InspectorFormSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
itemIds: {
|
||||
type: "array",
|
||||
title: "Item Ids",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
required: ["itemIds"],
|
||||
};
|
||||
renderWithMantine(<EscapingHarness schema={schema} />);
|
||||
|
||||
const jsonInput = screen.getByLabelText(/Item Ids/) as HTMLTextAreaElement;
|
||||
// `[` opens a userEvent keyboard descriptor, so it is escaped as `[[`.
|
||||
await user.type(jsonInput, '[["item-1","item-2"]');
|
||||
|
||||
expect(jsonInput.value).toBe('["item-1","item-2"]');
|
||||
expect(jsonInput.value).not.toContain("\\");
|
||||
});
|
||||
|
||||
// #1853 again, from the comment thread: the caret sitting *outside* the
|
||||
// quotes of a `""` value. One keystroke there made the draft invalid, which
|
||||
// is all it took — `""` + `a` rendered as `"\"\"a"`.
|
||||
it("keeps a keystroke typed outside a string's quotes literal (#1853)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const schema: InspectorFormSchema = {
|
||||
type: "object",
|
||||
// No `type`, so the field lands on the JSON editor holding a string.
|
||||
properties: { note: { title: "Note" } },
|
||||
};
|
||||
renderWithMantine(
|
||||
<EscapingHarness schema={schema} initial={{ note: "" }} />,
|
||||
);
|
||||
|
||||
const jsonInput = screen.getByLabelText(/Note/) as HTMLTextAreaElement;
|
||||
await user.click(jsonInput);
|
||||
await user.keyboard("{End}a");
|
||||
|
||||
expect(jsonInput.value).toBe('""a');
|
||||
});
|
||||
|
||||
// #1856: `sum_numbers`, Backspace with the caret after the closing `]`.
|
||||
it("keeps the draft raw when Backspace invalidates an array (#1856)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const schema: InspectorFormSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
numbers: {
|
||||
type: "array",
|
||||
title: "Numbers",
|
||||
items: { type: "number" },
|
||||
},
|
||||
},
|
||||
required: ["numbers"],
|
||||
};
|
||||
renderWithMantine(
|
||||
<EscapingHarness schema={schema} initial={{ numbers: [1, 2] }} />,
|
||||
);
|
||||
|
||||
const jsonInput = screen.getByLabelText(/Numbers/) as HTMLTextAreaElement;
|
||||
await user.click(jsonInput);
|
||||
await user.keyboard("{End}{Backspace}");
|
||||
|
||||
// Exactly the seeded text minus its last character — the draft the reporter
|
||||
// expected, where the field instead showed a quoted, escaped string.
|
||||
expect(jsonInput.value).toBe("[\n 1,\n 2\n");
|
||||
|
||||
// Each further edit compounded the escaping, so keep going.
|
||||
await user.keyboard("{Backspace}{Backspace}");
|
||||
expect(jsonInput.value).not.toContain("\\");
|
||||
expect(jsonInput.value.startsWith('"')).toBe(false);
|
||||
|
||||
// And the draft is still live: closing it back up produces a real array.
|
||||
await user.keyboard("2]");
|
||||
expect(jsonInput.value).toBe("[\n 1,\n 2]");
|
||||
});
|
||||
|
||||
// #1885: FastMCP's `b: int | None = None`, which compiles to an `anyOf` with
|
||||
// a null branch and `default: null`. The reporter was editing the literal
|
||||
// `null` token in a raw JSON box; collapsing the union routes the field to a
|
||||
// real number input, so there is no JSON token to edit in the first place.
|
||||
it("edits a nullable integer as a number input, not a null token (#1885)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const schema: InspectorFormSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
a: { type: "integer", title: "A" },
|
||||
b: {
|
||||
title: "B",
|
||||
anyOf: [{ type: "integer" }, { type: "null" }],
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
required: ["a"],
|
||||
};
|
||||
// No seeded value: the `null` has to come from the schema's `default`,
|
||||
// which is where the reporter's did.
|
||||
renderWithMantine(<EscapingHarness schema={schema} />);
|
||||
|
||||
const b = screen.getByLabelText(/^B$/) as HTMLInputElement;
|
||||
expect(b.tagName).toBe("INPUT");
|
||||
// `null` is rendered as "no value", not as four editable characters.
|
||||
expect(b.value).toBe("");
|
||||
|
||||
await user.click(b);
|
||||
await user.keyboard("42{Backspace}");
|
||||
expect(b.value).toBe("4");
|
||||
expect(b.value).not.toContain("\\");
|
||||
});
|
||||
|
||||
// The same nullable-with-`default: null` shape on a field the collapse still
|
||||
// sends to the JSON editor (an array union). Backspacing the `null` token
|
||||
// there is the exact keystroke sequence from #1885's recording.
|
||||
it("backspaces a null default in the JSON editor without escaping (#1885)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const schema: InspectorFormSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
cfg: {
|
||||
title: "Cfg",
|
||||
anyOf: [{ type: "array" }, { type: "null" }],
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
// Again unseeded, so the editor's opening `null` is the schema default
|
||||
// being resolved — the state the reporter's recording starts from.
|
||||
renderWithMantine(<EscapingHarness schema={schema} />);
|
||||
|
||||
const jsonInput = screen.getByLabelText(/Cfg/) as HTMLTextAreaElement;
|
||||
expect(jsonInput.value).toBe("null");
|
||||
|
||||
await user.click(jsonInput);
|
||||
await user.keyboard("{End}{Backspace}");
|
||||
expect(jsonInput.value).toBe("nul");
|
||||
|
||||
await user.keyboard("{Backspace}{Backspace}{Backspace}");
|
||||
expect(jsonInput.value).toBe("");
|
||||
expect(jsonInput.value).not.toContain("\\");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue