| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/html/forms/html_form_control_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_form_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/core/html_names.h" |
| #include "third_party/blink/renderer/core/testing/page_test_base.h" |
| #include "third_party/blink/renderer/platform/json/json_parser.h" |
| #include "third_party/blink/renderer/platform/json/json_values.h" |
| #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" |
| |
| namespace blink { |
| |
| class HTMLFormMcpToolTest : public PageTestBase { |
| public: |
| HTMLInputElement* GetInputElement(const char* id) { |
| return DynamicTo<HTMLInputElement>( |
| GetDocument().getElementById(AtomicString(id))); |
| } |
| |
| HTMLFormElement* GetFormElement(const char* id) { |
| return DynamicTo<HTMLFormElement>( |
| GetDocument().getElementById(AtomicString(id))); |
| } |
| |
| // Private functions exposed via class friendship: |
| |
| static bool IsValidWebMCPForm(HTMLFormElement& form_element) { |
| return form_element.IsValidWebMCPForm(); |
| } |
| |
| static bool FillFormControls(HTMLFormElement& form_element, |
| const String& input_arguments) { |
| CHECK(IsValidWebMCPForm(form_element)); |
| CHECK(form_element.active_webmcp_tool_); |
| bool require_submit_button = false; |
| HTMLFormControlElement* submit_button; |
| return form_element.active_webmcp_tool_->FillFormControls( |
| input_arguments, require_submit_button, &submit_button); |
| } |
| |
| static String ComputeInputSchema(HTMLFormElement& form_element) { |
| CHECK(IsValidWebMCPForm(form_element)); |
| CHECK(form_element.active_webmcp_tool_); |
| return form_element.active_webmcp_tool_->ComputeInputSchema(); |
| } |
| |
| private: |
| ScopedWebMCPForTest scoped_feature{true}; |
| }; |
| |
| // Note that both toolname *and* tooldescription must be present |
| // for a <form> element to become a valid declarative WebMCP tool. |
| |
| TEST_F(HTMLFormMcpToolTest, NoTool) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, NoTool_NameOnly) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, NoTool_DescriptionOnly) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form tooldescription="perform task"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ToolPresent_Basic) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_TRUE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ToolRemovedWithAttribute_Name) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| form_element->removeAttribute(html_names::kToolnameAttr); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ToolRemovedWithAttribute_Description) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| form_element->removeAttribute(html_names::kTooldescriptionAttr); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ToolAppearsWhenAttributeSet_NameFirst) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| |
| form_element->setAttribute(html_names::kToolnameAttr, AtomicString("mytool")); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); // Still need a description. |
| form_element->setAttribute(html_names::kTooldescriptionAttr, |
| AtomicString("description")); |
| EXPECT_TRUE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ToolAppearsWhenAttributeSet_DescriptionFirst) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); |
| |
| form_element->setAttribute(html_names::kTooldescriptionAttr, |
| AtomicString("description")); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); // Still need a name. |
| form_element->setAttribute(html_names::kToolnameAttr, AtomicString("mytool")); |
| EXPECT_TRUE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, Tool_AppendFormElement) { |
| UpdateAllLifecyclePhasesForTest(); |
| |
| HTMLFormElement* form_element = |
| MakeGarbageCollected<HTMLFormElement>(GetDocument()); |
| form_element->setAttribute(html_names::kToolnameAttr, AtomicString("mytool")); |
| form_element->setAttribute(html_names::kTooldescriptionAttr, |
| AtomicString("description")); |
| EXPECT_FALSE(IsValidWebMCPForm(*form_element)); // Not connected. |
| |
| GetDocument().body()->AppendChild(form_element); |
| EXPECT_TRUE(IsValidWebMCPForm(*form_element)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, FillFormControls_Basic) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| <input id=text1 name=text1 type=text> |
| <input id=text2 name=text2 type=text> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| String json_string = |
| R"JSON( |
| { |
| "text1": "foo", |
| "text2": "bar" |
| } |
| )JSON"; |
| |
| EXPECT_TRUE(FillFormControls(*form_element, json_string)); |
| |
| HTMLInputElement* text1 = GetInputElement("text1"); |
| HTMLInputElement* text2 = GetInputElement("text2"); |
| ASSERT_TRUE(text1); |
| ASSERT_TRUE(text2); |
| |
| EXPECT_EQ("foo", text1->Value()); |
| EXPECT_EQ("bar", text2->Value()); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, FillFormControls_Partial) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| <input id=text1 name=text1 type=text value="initial1"> |
| <input id=text2 name=text2 type=text value="initial2"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| String json_string = |
| R"JSON( |
| { |
| "text2": "bar" |
| } |
| )JSON"; |
| |
| EXPECT_TRUE(FillFormControls(*form_element, json_string)); |
| |
| HTMLInputElement* text1 = GetInputElement("text1"); |
| HTMLInputElement* text2 = GetInputElement("text2"); |
| ASSERT_TRUE(text1); |
| ASSERT_TRUE(text2); |
| |
| EXPECT_EQ("initial1", text1->Value()); |
| EXPECT_EQ("bar", text2->Value()); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, FillFormControls_InvalidJsonFailure) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| EXPECT_FALSE(FillFormControls(*form_element, R"JSON({"x":"y",})JSON")); |
| EXPECT_FALSE(FillFormControls(*form_element, R"JSON(["unknown"])JSON")); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, FillFormControls_UnknownParamFailure) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| <input id=text1 name=text1 type=text> |
| <input id=text2 name=text2 type=text> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| String json_string = |
| R"JSON( |
| { |
| "unknown": "UNKNOWN" |
| } |
| )JSON"; |
| |
| EXPECT_FALSE(FillFormControls(*form_element, json_string)); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, FillFormControls_Transactional) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id=form toolname="mytool" tooldescription="perform task"> |
| <input id=text1 name=text1 type=text value="initial1"> |
| <input id=text2 name=text2 type=text value="initial2"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| |
| String json_string = |
| R"JSON( |
| { |
| "text1": "foo", |
| "unknown": "bar", |
| "text2": "bar" |
| } |
| )JSON"; |
| |
| EXPECT_FALSE(FillFormControls(*form_element, json_string)); |
| |
| HTMLInputElement* text1 = GetInputElement("text1"); |
| HTMLInputElement* text2 = GetInputElement("text2"); |
| ASSERT_TRUE(text1); |
| ASSERT_TRUE(text2); |
| |
| // A failure means no form control values were changed. |
| EXPECT_EQ("initial1", text1->Value()); |
| EXPECT_EQ("initial2", text2->Value()); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <input name="text1" type="text"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Required) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <input name="text1" type="text"> |
| <input name="text2" type="text" required> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string" |
| }, |
| "text2": { |
| "type": "string" |
| } |
| }, |
| "required": ["text2"] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Title) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <input name="text1" type="text" toolparamtitle="Surname"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "title": "Surname" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Description) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <input name="text1" type="text" toolparamdescription="Some nice text"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "description": "Some nice text" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Label) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <label for="text">Some text</label> |
| <input id="text" name="text1" type="text"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "description": "Some text" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Label_Multiple) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <label for="text">Label one</label> |
| <label for="text">Label two</label> |
| <input id="text" name="text1" type="text"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "description": "Label one; Label two" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_AriaDescription) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <input name="text1" type="text" aria-description="ARIA"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "description": "ARIA" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_PreferLabelOverAria) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <label for="text">Label</label> |
| <input id="text" name="text1" type="text" aria-description="ARIA"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "description": "Label" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_PreferAttrOverLabel) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <label for="text">Label</label> |
| <input |
| id="text" |
| name="text1" |
| type="text" |
| toolparamdescription="ATTR" |
| aria-description="ARIA"> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "text1": { |
| "type": "string", |
| "description": "ATTR" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_Select) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <select name="select" required> |
| <option value="Option 1">This is option 1</option> |
| <option value="Option 2">This is option 2</option> |
| <option value="Option 3">This is option 3</option> |
| </select> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "select": { |
| "type": "string", |
| "oneOf": [ |
| { "const": "Option 1", "title": "This is option 1" }, |
| { "const": "Option 2", "title": "This is option 2" }, |
| { "const": "Option 3", "title": "This is option 3" } |
| ] |
| } |
| }, |
| "required": ["select"] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| TEST_F(HTMLFormMcpToolTest, ParameterSchema_Select_Title) { |
| SetBodyInnerHTML( |
| R"HTML( |
| <form id="form" toolname="mytool" tooldescription="perform task"> |
| <select name="select" toolparamtitle="Possible Options"> |
| <option value="Option 1">This is option 1</option> |
| </select> |
| </form> |
| )HTML"); |
| |
| HTMLFormElement* form_element = GetFormElement("form"); |
| ASSERT_TRUE(form_element); |
| ASSERT_TRUE(IsValidWebMCPForm(*form_element)); |
| String actual = ComputeInputSchema(*form_element); |
| std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( |
| { |
| "type": "object", |
| "properties": { |
| "select": { |
| "type": "string", |
| "oneOf": [ |
| { "const": "Option 1", "title": "This is option 1" } |
| ], |
| "title": "Possible Options" |
| } |
| }, |
| "required": [] |
| } |
| )JSON"); |
| ASSERT_TRUE(expected_json); |
| EXPECT_EQ(expected_json->ToJSONString(), actual); |
| } |
| |
| } // namespace blink |