| <!DOCTYPE html> |
| <meta charset="utf-8"> |
| <title>WebMCP: ToolCancelEvent interface and ontoolcancel handler</title> |
| <link rel="help" href="https://webmachinelearning.github.io/webmcp/#tool-cancel-event"> |
| <link rel="author" href="mailto:beaufort.francois@gmail.com"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| |
| <script> |
| test(() => { |
| const event = new ToolCancelEvent('toolcancel'); |
| assert_equals(event.type, 'toolcancel'); |
| assert_equals(event.toolName, '', 'toolName defaults to the empty string'); |
| assert_false(event.bubbles); |
| assert_false(event.cancelable); |
| assert_false(event.composed); |
| assert_false(event.isTrusted); |
| }, 'ToolCancelEvent constructor with default event init dictionary'); |
| |
| test(() => { |
| const event = new ToolCancelEvent('toolcancel', { |
| toolName: 'my_tool', |
| bubbles: true, |
| cancelable: true, |
| composed: true, |
| }); |
| assert_equals(event.toolName, 'my_tool'); |
| assert_true(event.bubbles); |
| assert_true(event.cancelable); |
| assert_true(event.composed); |
| }, 'ToolCancelEvent constructor with an event init dictionary'); |
| |
| test(t => { |
| const events = []; |
| const handler = e => events.push(e); |
| document.modelContext.ontoolcancel = handler; |
| t.add_cleanup(() => { document.modelContext.ontoolcancel = null; }); |
| |
| const event = new ToolCancelEvent('toolcancel', { toolName: 'my_tool' }); |
| document.modelContext.dispatchEvent(event); |
| |
| assert_array_equals(events, [event], |
| 'synthetic toolcancel events must reach ontoolcancel'); |
| assert_equals(events[0].target, document.modelContext); |
| assert_equals(events[0].toolName, 'my_tool'); |
| }, 'ontoolcancel is invoked for synthetic toolcancel events dispatched ' + |
| 'at ModelContext'); |
| |
| promise_test(async t => { |
| assert_equals(document.modelContext.ontoolcancel, null, |
| 'ontoolcancel must initially be null'); |
| |
| const events = []; |
| const handler = e => events.push(e); |
| document.modelContext.ontoolcancel = handler; |
| t.add_cleanup(() => { document.modelContext.ontoolcancel = null; }); |
| assert_equals(document.modelContext.ontoolcancel, handler, |
| 'ontoolcancel must reflect the assigned handler'); |
| |
| const regController = new AbortController(); |
| t.add_cleanup(() => regController.abort()); |
| await document.modelContext.registerTool({ |
| name: 'onevent_tool', |
| description: 'Tool used to trigger ontoolcancel', |
| execute: () => new Promise(() => {}), |
| }, { signal: regController.signal }); |
| |
| const tools = await document.modelContext.getTools(); |
| const tool = tools.find(tool => tool.name === 'onevent_tool'); |
| |
| // Executes the tool, aborts it, and resolves once the `toolcancel` event has |
| // been dispatched at the ModelContext. The signal's abort steps reject the |
| // executeTool() promise first, and only then cancel the pending tool |
| // execution in parallel, which queues the task that fires `toolcancel`. |
| // Awaiting the rejection alone is therefore not enough to observe the event. |
| // https://webmachinelearning.github.io/webmcp/#cancel-a-pending-tool-execution |
| const executeAndCancelTool = async () => { |
| const cancelPromise = new Promise(resolve => { |
| document.modelContext.addEventListener('toolcancel', resolve, { once: true }); |
| }); |
| const controller = new AbortController(); |
| const execPromise = |
| document.modelContext.executeTool(tool, {}, { signal: controller.signal }); |
| const rejectPromise = promise_rejects_dom(t, 'AbortError', execPromise); |
| controller.abort(); |
| await rejectPromise; |
| await cancelPromise; |
| }; |
| |
| await executeAndCancelTool(); |
| |
| assert_equals(events.length, 1, 'ontoolcancel must be invoked once'); |
| assert_true(events[0] instanceof ToolCancelEvent); |
| assert_equals(events[0].toolName, 'onevent_tool'); |
| |
| document.modelContext.ontoolcancel = null; |
| assert_equals(document.modelContext.ontoolcancel, null, |
| 'ontoolcancel must be nullable'); |
| |
| await executeAndCancelTool(); |
| |
| assert_equals(events.length, 1, |
| 'the removed handler must not be invoked again'); |
| }, 'ontoolcancel is invoked when a tool invocation is canceled'); |
| </script> |