diff --git a/clients/tui/__tests__/uriTemplateToForm.test.ts b/clients/tui/__tests__/uriTemplateToForm.test.ts index 9dc1a32c..c44b6b37 100644 --- a/clients/tui/__tests__/uriTemplateToForm.test.ts +++ b/clients/tui/__tests__/uriTemplateToForm.test.ts @@ -64,3 +64,15 @@ describe("uriTemplateToForm", () => { expect(form.sections[0]!.fields).toEqual([]); }); }); + +describe("a name repeated inside one expression", () => { + it("is required, not treated as a shared group", () => { + // `{a,a}` is one requirement named twice. Before core deduplicated the + // group, `requiredGroups` returned ["a","a"], so this form's + // `length === 1` test left the field optional while ResourceTestModal's + // submit guard still refused a blank -- an un-submittable form. + const [field] = uriTemplateToForm("x://{a,a}", "T").sections[0].fields; + expect(field.name).toBe("a"); + expect(field.required).toBe(true); + }); +}); diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx index 87b2ba15..b7d6c934 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx @@ -266,6 +266,58 @@ describe("ResourceTemplatePanel", () => { expect(onReadResource).not.toHaveBeenCalled(); }); + it("names every unmet group, so a disabled button always has a reason", async () => { + // With `{a,b}{b,c}{a,c}`, filling `b` satisfies the first two groups. A + // per-field hint then looks satisfied while Read Resource stays disabled + // on the third -- so the outstanding requirement is stated form-level. + const user = userEvent.setup(); + renderWithMantine( + , + ); + expect(screen.getByText(/Still needed:/)).toHaveTextContent( + "Still needed: a or b; b or c; a or c", + ); + await user.type(screen.getByLabelText("b"), "1"); + expect(screen.getByText(/Still needed:/)).toHaveTextContent( + "Still needed: a or c", + ); + expect( + screen.getByRole("button", { name: "Read Resource" }), + ).toBeDisabled(); + await user.type(screen.getByLabelText("a"), "2"); + expect(screen.queryByText(/Still needed:/)).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Read Resource" }), + ).toBeEnabled(); + }); + + it("hints every shared group a variable sits in, not just the first", () => { + renderWithMantine( + , + ); + expect( + screen.getByText("Any one of each: a, b; a, c"), + ).toBeInTheDocument(); + }); + + it("treats a name repeated in one expression as a plain requirement", () => { + renderWithMantine( + , + ); + // Not "Any one of: a, a" -- it is one field, required. + expect(screen.queryByText(/Any one of/)).not.toBeInTheDocument(); + expect(screen.getByText("Still needed: a")).toBeInTheDocument(); + }); + it("shows the malformed template unexpanded in the preview", () => { renderWithMantine( names.length === 1 && names[0] === varName, ); - const sharedGroup = individuallyRequired - ? undefined - : groups.find( + // EVERY shared group this variable sits in, not the first. With + // `{a,b}{b,c}{a,c}`, showing only the first left `b` looking like it + // satisfied everything while Read Resource stayed disabled on the + // unmet `{a,c}` -- a hidden requirement is worse than a wordy hint. + // The form-level "Still needed" line below names what is actually + // outstanding, which is the part a per-field hint cannot express. + const sharedGroups = individuallyRequired + ? [] + : groups.filter( (names) => names.length > 1 && names.includes(varName), ); const description = !required ? "Optional" - : sharedGroup - ? `Any one of: ${sharedGroup.join(", ")}` + : sharedGroups.length > 0 + ? `Any one of${sharedGroups.length > 1 ? " each" : ""}: ${sharedGroups + .map((names) => names.join(", ")) + .join("; ")}` : undefined; return useAutocomplete ? ( {expansion.error} )} + {expansion.error === undefined && unmet.length > 0 && ( + + Still needed: {unmet.map((names) => names.join(" or ")).join("; ")} + + )}