From f228a7b2a0fc49175847e791919f02ac92a2fb91 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Tue, 14 Apr 2026 13:24:55 +0300 Subject: [PATCH 01/11] add full driller-bosun update --- CLAUDE.md | 14 +- bin/explorbot-cli.ts | 11 +- experience/Button_AI.md | 147 ++++ experience/Button_Dropdown.md | 84 ++ experience/Button_Extra.md | 147 ++++ experience/Button_Green.md | 210 +++++ experience/Button_Merge.md | 30 + experience/Button_Primary.md | 210 +++++ experience/Button_Red.md | 210 +++++ experience/Button_Secondary.md | 219 +++++ experience/Button_Third.md | 219 +++++ experience/Code_Input.md | 15 + experience/Form_Elements.md | 32 + experience/General_Inputs.md | 111 +++ experience/Input_Empty_Handler.md | 12 + experience/Input_With_Tags.md | 14 + experience/Legacy.md | 30 + experience/Link.md | 113 +++ experience/New_Counter.md | 183 ++++ experience/Other_Buttons.md | 66 ++ experience/PowerSelect.md | 212 +++++ experience/PowerSelect_Filters.md | 244 ++++++ experience/PowerSelect_Input.md | 175 ++++ experience/PowerSelect_Multiple.md | 31 + experience/PowerSelect_Typeahead.md | 27 + experience/Search_Input.md | 13 + experience/Tabs.md | 111 +++ src/action-result.ts | 3 +- src/ai/bosun.ts | 557 ------------- src/ai/driller.ts | 1160 ++++++++++++++++++++++++++ src/commands/drill-command.ts | 6 +- src/components/AddRule.tsx | 2 +- src/config.ts | 1 + src/explorbot.ts | 10 +- src/explorer.ts | 26 + src/state-manager.ts | 7 +- src/utils/hooks-runner.ts | 8 +- src/utils/url-matcher.ts | 2 +- src/utils/web-element.ts | 187 ++++- src/utils/xpath.ts | 2 +- tests/unit/annotate-elements.test.ts | 43 + tests/unit/web-element.test.ts | 71 ++ 42 files changed, 4385 insertions(+), 590 deletions(-) create mode 100644 experience/Button_AI.md create mode 100644 experience/Button_Dropdown.md create mode 100644 experience/Button_Extra.md create mode 100644 experience/Button_Green.md create mode 100644 experience/Button_Merge.md create mode 100644 experience/Button_Primary.md create mode 100644 experience/Button_Red.md create mode 100644 experience/Button_Secondary.md create mode 100644 experience/Button_Third.md create mode 100644 experience/Code_Input.md create mode 100644 experience/Form_Elements.md create mode 100644 experience/General_Inputs.md create mode 100644 experience/Input_Empty_Handler.md create mode 100644 experience/Input_With_Tags.md create mode 100644 experience/Legacy.md create mode 100644 experience/Link.md create mode 100644 experience/New_Counter.md create mode 100644 experience/Other_Buttons.md create mode 100644 experience/PowerSelect.md create mode 100644 experience/PowerSelect_Filters.md create mode 100644 experience/PowerSelect_Input.md create mode 100644 experience/PowerSelect_Multiple.md create mode 100644 experience/PowerSelect_Typeahead.md create mode 100644 experience/Search_Input.md create mode 100644 experience/Tabs.md delete mode 100644 src/ai/bosun.ts create mode 100644 src/ai/driller.ts create mode 100644 tests/unit/web-element.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index d01dc3a..30f9d3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,7 +95,7 @@ ExplorBot (DI Container) ├── Planner ├── Pilot ←──────────┐ ├── Tester ──────────┘ (Pilot supervises Tester) - ├── Bosun → Researcher, Navigator — drill components to learn interactions + ├── Driller -> Navigator - drill components to learn interactions ├── Captain ├── Historian ├── ExperienceCompactor @@ -244,7 +244,7 @@ All agents implement the `Agent` interface. Task-executing agents (Tester, Capta - Planner — generate test scenarios - Pilot — supervise test execution, detect stuck patterns, request user help - Tester → Researcher, Navigator, Pilot, Historian*, Quartermaster* — execute tests with AI tools -- Bosun → Researcher, Navigator — drill page components to learn interactions +- Driller -> Navigator - drill page components to learn interactions - Captain → Historian*, Quartermaster* — handle user commands in TUI - Historian — save test sessions, generate code, report to Testomatio - ExperienceCompactor — compress experience files @@ -401,10 +401,10 @@ import React from 'react'; There are application commands available in TUI -- /research [uri] - performs research on a current page or navigate to [uri] if uri is provided -- /plan - plan testing feature starting from current page -- /navigate - move to other page. Use AI to complete navigation -- /drill [--knowledge ] [--max ] - drill all components on page to learn interactions +* /research [uri] - performs research on a current page or navigate to [uri] if uri is provided +* /plan - plan testing feature starting from current page +* /navigate - move to other page. Use AI to complete navigation +* /drill [--knowledge ] [--max-components ] - drill all components on page to learn interactions There are also CodeceptJS commands available: @@ -441,7 +441,7 @@ explorbot plan /login authentication # plan with focus on authentication ```bash explorbot drill # drill all components on page -explorbot drill /components --max 10 # limit to 10 components +explorbot drill /components --max-components 10 # limit to 10 components explorbot drill /login --knowledge /login # save to knowledge file ``` diff --git a/bin/explorbot-cli.ts b/bin/explorbot-cli.ts index aa033af..8d9984c 100755 --- a/bin/explorbot-cli.ts +++ b/bin/explorbot-cli.ts @@ -485,7 +485,12 @@ addCommonOptions(program.command('research ').description('Research a page ); addCommonOptions( - program.command('drill ').alias('bosun').description('Drill all components on a page to learn interactions').option('--knowledge ', 'Save learned interactions to knowledge file at this URL path').option('--max ', 'Maximum number of components to drill', '20') + program + .command('drill ') + .alias('driller') + .description('Drill all components on a page to learn interactions') + .option('--knowledge ', 'Save learned interactions to knowledge file at this URL path') + .option('--max-components ', 'Maximum number of components to drill') ).action(async (url, options) => { try { const explorBot = new ExplorBot(buildExplorBotOptions(url, options)); @@ -493,9 +498,9 @@ addCommonOptions( await explorBot.visit(url); - const plan = await explorBot.agentBosun().drill({ + const plan = await explorBot.agentDriller().drill({ knowledgePath: options.knowledge, - maxComponents: Number.parseInt(options.max, 10), + maxComponents: Number.parseInt(options.maxComponents || '30', 10), interactive: false, }); diff --git a/experience/Button_AI.md b/experience/Button_AI.md new file mode 100644 index 0000000..20469a7 --- /dev/null +++ b/experience/Button_AI.md @@ -0,0 +1,147 @@ +--- +url: /projects/test-d6178/components/?m=false&s=AI%3A%3AButton +title: Testomat.io +summary: Curated AI button interactions only. +--- +### SUCCEEDED: Drill click: AI button size mini icon only + +Solution: Clicks the mini icon-only AI button and opens the AI modal. + +```javascript +I.click("button.ai-btn.btn-only-icon.btn-mini:has(svg)") +``` + + +### SUCCEEDED: Drill click: AI button size mini icon only selected + +Solution: Clicks the selected mini icon-only AI button, opens the AI modal, and toggles selected state. + +```javascript +I.click("button.ai-btn.btn-only-icon.btn-mini.btn-selected:has(svg)") +``` + + +### SUCCEEDED: Drill click: AI button size small icon only + +Solution: Clicks the small icon-only AI button and opens the AI modal. + +```javascript +I.click("button.ai-btn.btn-only-icon.btn-sm:has(svg)") +``` + + +### SUCCEEDED: Drill click: AI button size small Default AI leading icon + +Solution: Clicks the small Default AI button with a leading icon and opens the AI modal. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Default AI\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: AI button size small Default AI leading and trailing icons + +Solution: Clicks the small Default AI button with leading and trailing icons and opens the AI modal/dropdown action. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: AI button size small Default AI selected + +Solution: Clicks the selected small Default AI dropdown-style button, opens the AI modal, and toggles selected state. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: AI button size medium icon only + +Solution: Clicks the medium icon-only AI button and opens the AI modal. + +```javascript +I.click("button.ai-btn.btn-only-icon.btn-md:has(svg)") +``` + + +### SUCCEEDED: Drill click: AI button size medium Default AI leading icon + +Solution: Clicks the medium Default AI button with a leading icon and opens the AI modal. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Default AI\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: AI button size medium Default AI leading and trailing icons + +Solution: Clicks the medium Default AI button with leading and trailing icons and opens the AI modal/dropdown menu. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: AI button size medium Default AI selected + +Solution: Clicks the selected medium Default AI dropdown-style button, opens the AI modal, and toggles selected state. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: AI button size large icon only + +Solution: Clicks the large icon-only AI button and opens the AI feature modal. + +```javascript +I.click("button.ai-btn.btn-only-icon.btn-lg:has(svg)") +``` + + +### SUCCEEDED: Drill click: AI button size large embedded text leading icon + +Solution: Clicks the large embedded-text AI button and opens the AI feature modal. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"embedded text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: AI button size large Default AI leading icon + +Solution: Clicks the large Default AI button with a leading icon and opens the AI modal. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Default AI\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: AI button size large Default AI leading and trailing icons + +Solution: Clicks the large Default AI button with leading and trailing icons and opens the AI modal/dropdown menu. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: AI button size large Default AI selected + +Solution: Clicks the selected large Default AI dropdown-style button, opens the AI modal, and toggles selected state. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: AI button with select dropdown + +Solution: Clicks the left side of the AI split button to open the AI modal; clicking the right side opens the dropdown. + +```javascript +I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-dropdown\") and contains(normalize-space(.),\"AI btn + select\")]") +``` diff --git a/experience/Button_Dropdown.md b/experience/Button_Dropdown.md new file mode 100644 index 0000000..a8cf45e --- /dev/null +++ b/experience/Button_Dropdown.md @@ -0,0 +1,84 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3ADropdown +title: Testomat.io +summary: Curated dropdown button interactions only. +--- +### SUCCEEDED: Drill click: Dropdown button small icon trigger + +Solution: Clicks the small dropdown button trigger and opens dropdown menu. + +```javascript +I.click("div.primary-btn.btn-icon-after.btn-sm") +``` + + +### SUCCEEDED: Drill click: Dropdown button small Default + +Solution: Clicks the small Default dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Default\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Dropdown button small Without icon + +Solution: Clicks the small Without icon dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Without icon\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Dropdown button medium icon trigger + +Solution: Clicks the medium dropdown button trigger and opens dropdown menu. + +```javascript +I.click("div.primary-btn.btn-icon-after.btn-md") +``` + + +### SUCCEEDED: Drill click: Dropdown button medium Default + +Solution: Clicks the medium Default dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Default\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Dropdown button medium Without icon + +Solution: Clicks the medium Without icon dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Without icon\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Dropdown button large icon trigger + +Solution: Clicks the large dropdown button trigger and opens dropdown menu. + +```javascript +I.click("div.primary-btn.btn-icon-after.btn-lg") +``` + + +### SUCCEEDED: Drill click: Dropdown button large Default + +Solution: Clicks the large Default dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Default\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Dropdown button large Without icon + +Solution: Clicks the large Without icon dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Without icon\" and not(.//svg)]") +``` diff --git a/experience/Button_Extra.md b/experience/Button_Extra.md new file mode 100644 index 0000000..5b34ec3 --- /dev/null +++ b/experience/Button_Extra.md @@ -0,0 +1,147 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3AExtra +title: Testomat.io +summary: Curated extra button interactions only. +--- +### SUCCEEDED: Drill click: Extra button mini default + +Solution: Clicks the mini extra button and opens its dropdown. + +```javascript +I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-xs\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") +``` + + +### SUCCEEDED: Drill click: Extra button mini above + +Solution: Clicks the mini extra button with cog icon and opens its dropdown above. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-xs:has(svg.md-icon-cog)") +``` + + +### SUCCEEDED: Drill click: Extra button mini render in body + +Solution: Clicks the mini extra button and opens its dropdown. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-xs:has(svg.md-icon-timer)") +``` + + +### SUCCEEDED: Drill click: Extra button mini beta + +Solution: Clicks the mini beta extra button and opens its dropdown. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-xs:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") +``` + + +### SUCCEEDED: Drill click: Extra button small default + +Solution: Clicks the small extra button and opens its dropdown. + +```javascript +I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-sm\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") +``` + + +### SUCCEEDED: Drill click: Extra button small above + +Solution: Clicks the small extra button with cog icon and opens its dropdown above. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-sm:has(svg.md-icon-cog)") +``` + + +### SUCCEEDED: Drill click: Extra button small render in body + +Solution: Clicks the small extra button and opens its dropdown. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-sm:has(svg.md-icon-timer)") +``` + + +### SUCCEEDED: Drill click: Extra button small beta + +Solution: Clicks the small beta extra button and opens its dropdown. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-sm:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") +``` + + +### SUCCEEDED: Drill click: Extra button medium default + +Solution: Clicks the medium extra button and opens its dropdown. + +```javascript +I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-md\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") +``` + + +### SUCCEEDED: Drill click: Extra button medium above + +Solution: Clicks the medium extra button with cog icon and opens its dropdown above. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-md:has(svg.md-icon-cog)") +``` + + +### SUCCEEDED: Drill click: Extra button medium render in body + +Solution: Clicks the medium extra button and opens its dropdown. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-md:has(svg.md-icon-timer)") +``` + + +### SUCCEEDED: Drill click: Extra button medium beta + +Solution: Clicks the medium beta extra button and opens its dropdown. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-md:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") +``` + + +### SUCCEEDED: Drill click: Extra button large default + +Solution: Clicks the large extra button and opens its dropdown. + +```javascript +I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-lg\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") +``` + + +### SUCCEEDED: Drill click: Extra button large above + +Solution: Clicks the large extra button with cog icon and opens its dropdown above. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-lg:has(svg.md-icon-cog)") +``` + + +### SUCCEEDED: Drill click: Extra button large render in body + +Solution: Clicks the large extra button and opens its dropdown. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-lg:has(svg.md-icon-timer)") +``` + + +### SUCCEEDED: Drill click: Extra button large beta + +Solution: Clicks the large beta extra button and opens its dropdown. + +```javascript +I.click("div.secondary-btn.btn-only-icon.btn-lg:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") +``` diff --git a/experience/Button_Green.md b/experience/Button_Green.md new file mode 100644 index 0000000..8323625 --- /dev/null +++ b/experience/Button_Green.md @@ -0,0 +1,210 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3AGreen +title: Testomat.io +summary: Curated green button interactions only. +--- +### SUCCEEDED: Drill click: Green button size small plain text + +Solution: Clicks the small green text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size small leading icon + +Solution: Clicks the small green text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Green button size small trailing icon + +Solution: Clicks the small green text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Green button size small leading and trailing icons + +Solution: Clicks the small green text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Green button size small icon only + +Solution: Clicks the small green icon-only button. + +```javascript +I.click("button.green-btn.btn-only-icon.btn-sm:has(svg)") +``` + + +### SUCCEEDED: Drill click: Green button size small selected + +Solution: Toggles the selected state of the small green selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size medium plain text + +Solution: Clicks the medium green text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size medium leading icon + +Solution: Clicks the medium green text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Green button size medium trailing icon + +Solution: Clicks the medium green text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Green button size medium leading and trailing icons + +Solution: Clicks the medium green text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Green button size medium icon only + +Solution: Clicks the medium green icon-only button. + +```javascript +I.click("button.green-btn.btn-only-icon.btn-md:has(svg)") +``` + + +### SUCCEEDED: Drill click: Green button size medium selected + +Solution: Toggles the selected state of the medium green selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size large plain text + +Solution: Clicks the large green text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size large leading icon + +Solution: Clicks the large green text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Green button size large trailing icon + +Solution: Clicks the large green text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Green button size large leading and trailing icons + +Solution: Clicks the large green text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Green button size large icon only + +Solution: Clicks the large green icon-only button. + +```javascript +I.click("button.green-btn.btn-only-icon.btn-lg:has(svg)") +``` + + +### SUCCEEDED: Drill click: Green button size large two icons only + +Solution: Clicks the large green button that contains two icons and no text. + +```javascript +I.click("button.green-btn.btn-only-two-icons.btn-lg.btn-icon-after") +``` + + +### SUCCEEDED: Drill click: Green button size large selected + +Solution: Toggles the selected state of the large green selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size extra large plain text + +Solution: Clicks the extra large green text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Green button size extra large leading icon + +Solution: Clicks the extra large green text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Green button size extra large icon only + +Solution: Clicks the extra large green icon-only button. + +```javascript +I.click("button.green-btn.btn-only-icon.btn-xl:has(svg)") +``` + + +### SUCCEEDED: Drill click: Green button size extra large selected + +Solution: Toggles the selected state of the extra large green selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` diff --git a/experience/Button_Merge.md b/experience/Button_Merge.md new file mode 100644 index 0000000..1936678 --- /dev/null +++ b/experience/Button_Merge.md @@ -0,0 +1,30 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3AMerge +title: Testomat.io +summary: Curated merge button interactions only. +--- +### SUCCEEDED: Drill click: Merge button small + +Solution: Clicks the small Merge dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"merge-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Merge\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Merge button medium + +Solution: Clicks the medium Merge dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"merge-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Merge\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Merge button large + +Solution: Clicks the large Merge dropdown button and opens dropdown menu. + +```javascript +I.click("//*[self::div and contains(@class,\"merge-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Merge\" and not(.//svg)]") +``` diff --git a/experience/Button_Primary.md b/experience/Button_Primary.md new file mode 100644 index 0000000..b5f3037 --- /dev/null +++ b/experience/Button_Primary.md @@ -0,0 +1,210 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3APrimary +title: Testomat.io +summary: Curated primary button interactions only. +--- +### SUCCEEDED: Drill click: Primary button size small plain text + +Solution: Clicks the small primary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size small leading icon + +Solution: Clicks the small primary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary button size small trailing icon + +Solution: Clicks the small primary text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Primary button size small leading and trailing icons + +Solution: Clicks the small primary text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Primary button size small icon only + +Solution: Clicks the small primary icon-only button. + +```javascript +I.click("button.primary-btn.btn-only-icon.btn-sm:has(svg)") +``` + + +### SUCCEEDED: Drill click: Primary button size small selected + +Solution: Toggles the selected state of the small primary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size medium plain text + +Solution: Clicks the medium primary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size medium leading icon + +Solution: Clicks the medium primary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary button size medium trailing icon + +Solution: Clicks the medium primary text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Primary button size medium leading and trailing icons + +Solution: Clicks the medium primary text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Primary button size medium icon only + +Solution: Clicks the medium primary icon-only button. + +```javascript +I.click("button.primary-btn.btn-only-icon.btn-md:has(svg)") +``` + + +### SUCCEEDED: Drill click: Primary button size medium selected + +Solution: Toggles the selected state of the medium primary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size large plain text + +Solution: Clicks the large primary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size large leading icon + +Solution: Clicks the large primary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary button size large trailing icon + +Solution: Clicks the large primary text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Primary button size large leading and trailing icons + +Solution: Clicks the large primary text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Primary button size large icon only + +Solution: Clicks the large primary icon-only button. + +```javascript +I.click("button.primary-btn.btn-only-icon.btn-lg:has(svg)") +``` + + +### SUCCEEDED: Drill click: Primary button size large two icons only + +Solution: Clicks the large primary button that contains two icons and no text. + +```javascript +I.click("button.primary-btn.btn-only-two-icons.btn-lg.btn-icon-after") +``` + + +### SUCCEEDED: Drill click: Primary button size large selected + +Solution: Toggles the selected state of the large primary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size extra large plain text + +Solution: Clicks the extra large primary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary button size extra large leading icon + +Solution: Clicks the extra large primary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary button size extra large icon only + +Solution: Clicks the extra large primary icon-only button. + +```javascript +I.click("button.primary-btn.btn-only-icon.btn-xl:has(svg)") +``` + + +### SUCCEEDED: Drill click: Primary button size extra large selected + +Solution: Toggles the selected state of the extra large primary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` diff --git a/experience/Button_Red.md b/experience/Button_Red.md new file mode 100644 index 0000000..2cb86cf --- /dev/null +++ b/experience/Button_Red.md @@ -0,0 +1,210 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3ARed +title: Testomat.io +summary: Curated red button interactions only. +--- +### SUCCEEDED: Drill click: Red button size small plain text + +Solution: Clicks the small red text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size small leading icon + +Solution: Clicks the small red text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Red button size small trailing icon + +Solution: Clicks the small red text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Red button size small leading and trailing icons + +Solution: Clicks the small red text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Red button size small icon only + +Solution: Clicks the small red icon-only button. + +```javascript +I.click("button.red-btn.btn-only-icon.btn-sm:has(svg)") +``` + + +### SUCCEEDED: Drill click: Red button size small selected + +Solution: Toggles the selected state of the small red selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size medium plain text + +Solution: Clicks the medium red text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size medium leading icon + +Solution: Clicks the medium red text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Red button size medium trailing icon + +Solution: Clicks the medium red text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Red button size medium leading and trailing icons + +Solution: Clicks the medium red text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Red button size medium icon only + +Solution: Clicks the medium red icon-only button. + +```javascript +I.click("button.red-btn.btn-only-icon.btn-md:has(svg)") +``` + + +### SUCCEEDED: Drill click: Red button size medium selected + +Solution: Toggles the selected state of the medium red selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size large plain text + +Solution: Clicks the large red text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size large leading icon + +Solution: Clicks the large red text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Red button size large trailing icon + +Solution: Clicks the large red text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Red button size large leading and trailing icons + +Solution: Clicks the large red text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Red button size large icon only + +Solution: Clicks the large red icon-only button. + +```javascript +I.click("button.red-btn.btn-only-icon.btn-lg:has(svg)") +``` + + +### SUCCEEDED: Drill click: Red button size large two icons only + +Solution: Clicks the large red button that contains two icons and no text. + +```javascript +I.click("button.red-btn.btn-only-two-icons.btn-lg.btn-icon-after") +``` + + +### SUCCEEDED: Drill click: Red button size large selected + +Solution: Toggles the selected state of the large red selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size extra large plain text + +Solution: Clicks the extra large red text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Red button size extra large leading icon + +Solution: Clicks the extra large red text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Red button size extra large icon only + +Solution: Clicks the extra large red icon-only button. + +```javascript +I.click("button.red-btn.btn-only-icon.btn-xl:has(svg)") +``` + + +### SUCCEEDED: Drill click: Red button size extra large selected + +Solution: Toggles the selected state of the extra large red selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` diff --git a/experience/Button_Secondary.md b/experience/Button_Secondary.md new file mode 100644 index 0000000..828ca17 --- /dev/null +++ b/experience/Button_Secondary.md @@ -0,0 +1,219 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3ASecondary +title: Testomat.io +summary: Curated secondary button interactions only. +--- +### SUCCEEDED: Drill click: Secondary button size mini icon only + +Solution: Clicks the mini secondary icon-only button. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-mini") +``` + + +### SUCCEEDED: Drill click: Secondary button size small plain text + +Solution: Clicks the small secondary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size small leading icon + +Solution: Clicks the small secondary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary button size small trailing icon + +Solution: Clicks the small secondary text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Secondary button size small leading and trailing icons + +Solution: Clicks the small secondary text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Secondary button size small icon only + +Solution: Clicks the small secondary icon-only button. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-sm") +``` + + +### SUCCEEDED: Drill click: Secondary button size small selected + +Solution: Toggles the selected state of the small secondary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size medium plain text + +Solution: Clicks the medium secondary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size medium leading icon + +Solution: Clicks the medium secondary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary button size medium trailing icon + +Solution: Clicks the medium secondary text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Secondary button size medium leading and trailing icons + +Solution: Clicks the medium secondary text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Secondary button size medium icon only + +Solution: Clicks the medium secondary icon-only button. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-md") +``` + + +### SUCCEEDED: Drill click: Secondary button size medium selected + +Solution: Toggles the selected state of the medium secondary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size large plain text + +Solution: Clicks the large secondary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size large leading icon + +Solution: Clicks the large secondary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary button size large trailing icon + +Solution: Clicks the large secondary text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Secondary button size large leading and trailing icons + +Solution: Clicks the large secondary text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Secondary button size large icon only + +Solution: Clicks the large secondary icon-only button. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-lg") +``` + + +### SUCCEEDED: Drill click: Secondary button size large two icons only + +Solution: Clicks the large secondary button that contains two icons and no text. + +```javascript +I.click("button.secondary-btn.btn-only-two-icons.btn-lg.btn-icon-after") +``` + + +### SUCCEEDED: Drill click: Secondary button size large selected + +Solution: Toggles the selected state of the large secondary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size extra large plain text + +Solution: Clicks the extra large secondary text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary button size extra large leading icon + +Solution: Clicks the extra large secondary text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary button size extra large icon only + +Solution: Clicks the extra large secondary icon-only button. + +```javascript +I.click("button.secondary-btn.btn-only-icon.btn-xl") +``` + + +### SUCCEEDED: Drill click: Secondary button size extra large selected + +Solution: Toggles the selected state of the extra large secondary selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` diff --git a/experience/Button_Third.md b/experience/Button_Third.md new file mode 100644 index 0000000..93b5002 --- /dev/null +++ b/experience/Button_Third.md @@ -0,0 +1,219 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Button%3A%3AThird +title: Testomat.io +summary: Curated third button interactions only. +--- +### SUCCEEDED: Drill click: Third button size mini icon only + +Solution: Clicks the mini third icon-only button. + +```javascript +I.click("button.third-btn.btn-only-icon.btn-mini") +``` + + +### SUCCEEDED: Drill click: Third button size small plain text + +Solution: Clicks the small third text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size small leading icon + +Solution: Clicks the small third text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Third button size small trailing icon + +Solution: Clicks the small third text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Third button size small leading and trailing icons + +Solution: Clicks the small third text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Third button size small icon only + +Solution: Clicks the small third icon-only button. + +```javascript +I.click("button.third-btn.btn-only-icon.btn-sm") +``` + + +### SUCCEEDED: Drill click: Third button size small selected + +Solution: Toggles the selected state of the small third selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size medium plain text + +Solution: Clicks the medium third text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size medium leading icon + +Solution: Clicks the medium third text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Third button size medium trailing icon + +Solution: Clicks the medium third text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Third button size medium leading and trailing icons + +Solution: Clicks the medium third text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Third button size medium icon only + +Solution: Clicks the medium third icon-only button. + +```javascript +I.click("button.third-btn.btn-only-icon.btn-md") +``` + + +### SUCCEEDED: Drill click: Third button size medium selected + +Solution: Toggles the selected state of the medium third selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size large plain text + +Solution: Clicks the large third text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size large leading icon + +Solution: Clicks the large third text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Third button size large trailing icon + +Solution: Clicks the large third text button with a trailing chevron icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") +``` + + +### SUCCEEDED: Drill click: Third button size large leading and trailing icons + +Solution: Clicks the large third text button with both leading and trailing icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") +``` + + +### SUCCEEDED: Drill click: Third button size large icon only + +Solution: Clicks the large third icon-only button. + +```javascript +I.click("button.third-btn.btn-only-icon.btn-lg") +``` + + +### SUCCEEDED: Drill click: Third button size large two icons only + +Solution: Clicks the large third button that contains two icons and no text. + +```javascript +I.click("button.third-btn.btn-only-two-icons.btn-lg.btn-icon-after") +``` + + +### SUCCEEDED: Drill click: Third button size large selected + +Solution: Toggles the selected state of the large third selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size extra large plain text + +Solution: Clicks the extra large third text button without icons. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Third button size extra large leading icon + +Solution: Clicks the extra large third text button with a leading icon. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Third button size extra large icon only + +Solution: Clicks the extra large third icon-only button. + +```javascript +I.click("button.third-btn.btn-only-icon.btn-xl") +``` + + +### SUCCEEDED: Drill click: Third button size extra large selected + +Solution: Toggles the selected state of the extra large third selected button. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") +``` diff --git a/experience/Code_Input.md b/experience/Code_Input.md new file mode 100644 index 0000000..e79b4ff --- /dev/null +++ b/experience/Code_Input.md @@ -0,0 +1,15 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Code%20Input +title: Testomat.io +summary: Curated code input editor interaction only. +--- +### SUCCEEDED: Drill type code: Code editor + +Solution: Switches into the code editor iframe, clicks the Monaco editor, types example code, and returns to the main page. + +```javascript +I.switchTo("(//iframe[contains(@src,\"/ember-monaco/frame.html\")])[1]"); +I.click(".monaco-editor"); +I.type("const value = \"test\";"); +I.switchTo(); +``` diff --git a/experience/Form_Elements.md b/experience/Form_Elements.md new file mode 100644 index 0000000..444dff0 --- /dev/null +++ b/experience/Form_Elements.md @@ -0,0 +1,32 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Form%20Elements +title: Testomat.io +summary: Curated form element interactions only. +--- +### SUCCEEDED: Drill click: Toggle off switch + +Solution: Clicks the Toggle - off switch and toggles it to the on state. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Toggle - off"]]//button[@role="switch" and not(contains(@class,"cursor-not-allowed"))]') +``` + + +### SUCCEEDED: Drill click: Toggle on switch + +Solution: Clicks the Toggle - on switch and toggles it to the off state. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Toggle - on"]]//button[@role="switch" and not(contains(@class,"cursor-not-allowed"))]') +``` + + +### SUCCEEDED: Drill select date range: DateRange textbox + +Solution: Clicks the DateRange input, opens the date picker, and selects a date range. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="DateRange"]]//input[@placeholder="Select date range"]'); +I.click('span.flatpickr-day[aria-label="April 12, 2026"]'); +I.click('span.flatpickr-day[aria-label="April 13, 2026"]'); +``` diff --git a/experience/General_Inputs.md b/experience/General_Inputs.md new file mode 100644 index 0000000..2b35638 --- /dev/null +++ b/experience/General_Inputs.md @@ -0,0 +1,111 @@ +--- +url: /projects/test-d6178/components/?m=false&s=General%20Inputs +title: Testomat.io +summary: Curated general input interactions only. +--- +### SUCCEEDED: Drill fill: Basic input "Basic input" + +Solution: Fills the basic text input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Basic input"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Basic input"]', 'Test Input') +``` + + +### SUCCEEDED: Drill fill: Input with value "Input with value" + +Solution: Replaces the existing input value with sample text. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Input with value"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Input with value"]', 'Sample Text') +``` + + +### SUCCEEDED: Drill fill: Text input "Text" + +Solution: Fills the text input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Text"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Text"]', 'Sample Text') +``` + + +### SUCCEEDED: Drill fill: Number input "Number" + +Solution: Fills the number input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Number"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Number"]', '42') +``` + + +### SUCCEEDED: Drill fill: Date input "Date" + +Solution: Fills the date input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Date"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Date"]', '2026-04-12') +``` + + +### SUCCEEDED: Drill fill: Time input "Time" + +Solution: Fills the time input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Time"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Time"]', '12:30') +``` + + +### SUCCEEDED: Drill fill: Password input "Password" + +Solution: Fills the password input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Password"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Password"]', 'myPassword') +``` + + +### SUCCEEDED: Drill fill: Email input "Email" + +Solution: Fills the email input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Email"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Email"]', 'user@example.com') +``` + + +### SUCCEEDED: Drill fill: Search input "Search" + +Solution: Fills the search input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Search"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Search"]', 'example search') +``` + + +### SUCCEEDED: Drill click: Checkbox input "Checkbox" + +Solution: Toggles the checkbox input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Checkbox"]') +``` + + +### SUCCEEDED: Drill click: Radio input "Radio" + +Solution: Selects the radio input. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Radio"]') +``` diff --git a/experience/Input_Empty_Handler.md b/experience/Input_Empty_Handler.md new file mode 100644 index 0000000..d66503b --- /dev/null +++ b/experience/Input_Empty_Handler.md @@ -0,0 +1,12 @@ +--- +url: /projects/test-d6178/components/?s=Input%20Empty%20Handler +title: Testomat.io +summary: Curated input empty handler interaction only. +--- +### SUCCEEDED: Drill click: Title input "Enter title" + +Solution: Clicks the title input and focuses it. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Title"]]//input[@placeholder="Enter title"]') +``` diff --git a/experience/Input_With_Tags.md b/experience/Input_With_Tags.md new file mode 100644 index 0000000..7ed77c0 --- /dev/null +++ b/experience/Input_With_Tags.md @@ -0,0 +1,14 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Input%20With%20Tags +title: Testomat.io +summary: Curated input with tags interaction only. +--- +### SUCCEEDED: Drill addTag: Tags input "Type @ to add tags" + +Solution: Clicks the tag combobox, enters a tag value, and confirms it with Enter. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input With Tags"]]//input[@placeholder="Type @ to add tags"]'); +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input With Tags"]]//input[@placeholder="Type @ to add tags"]', '@foo'); +I.pressKey('Enter'); +``` diff --git a/experience/Legacy.md b/experience/Legacy.md new file mode 100644 index 0000000..0fb07a4 --- /dev/null +++ b/experience/Legacy.md @@ -0,0 +1,30 @@ +--- +url: /projects/test-d6178/components/?s=Legacy +title: Testomat.io +summary: Curated legacy dropdown trigger interactions only. +--- +### SUCCEEDED: Drill click: Passed 1 legacy status dropdown + +Solution: Clicks the Passed 1 legacy status dropdown trigger and opens its dropdown. + +```javascript +I.click("//*[self::div and contains(@class,\"secondary-btn\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Passed 1\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Failed 2 legacy status dropdown + +Solution: Clicks the Failed 2 legacy status dropdown trigger and opens its dropdown. + +```javascript +I.click("//*[self::div and contains(@class,\"secondary-btn\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Failed 2\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Skipped 3 legacy status dropdown + +Solution: Clicks the Skipped 3 legacy status dropdown trigger and opens its dropdown. + +```javascript +I.click("//*[self::div and contains(@class,\"secondary-btn\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Skipped 3\" and not(.//svg)]") +``` diff --git a/experience/Link.md b/experience/Link.md new file mode 100644 index 0000000..23852de --- /dev/null +++ b/experience/Link.md @@ -0,0 +1,113 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Link +title: Testomat.io +summary: Curated link interactions only. +--- +### SUCCEEDED: Drill click: Primary link size medium leading icon + +Solution: Clicks the medium primary link with a leading icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary link size medium plain text + +Solution: Clicks the medium primary text link without icons. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary link size medium trailing icon + +Solution: Clicks the medium primary link with a trailing icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary link size small leading icon + +Solution: Clicks the small primary link with a leading icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Primary link size small plain text + +Solution: Clicks the small primary text link without icons. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Primary link size small trailing icon + +Solution: Clicks the small primary link with a trailing icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary link size medium leading icon + +Solution: Clicks the medium secondary link with a leading icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary link size medium plain text + +Solution: Clicks the medium secondary text link without icons. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary link size medium trailing icon + +Solution: Clicks the medium secondary link with a trailing icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary link size small leading icon + +Solution: Clicks the small secondary link with a leading icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") +``` + + +### SUCCEEDED: Drill click: Secondary link size small plain text + +Solution: Clicks the small secondary text link without icons. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Secondary link size small trailing icon + +Solution: Clicks the small secondary link with a trailing icon. Opens the link in a new tab (target="_blank"). + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") +``` + + diff --git a/experience/New_Counter.md b/experience/New_Counter.md new file mode 100644 index 0000000..15aed73 --- /dev/null +++ b/experience/New_Counter.md @@ -0,0 +1,183 @@ +--- +url: /projects/test-d6178/components/?m=false&s=New%20counter +title: Testomat.io +summary: Curated counter button interactions only. +--- +### SUCCEEDED: Drill click: Counter in third button large icon counter + +Solution: Clicks the third large icon-counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"icon-counter\") and normalize-space(.)=\"7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large icon counter + +Solution: Clicks the secondary large icon-counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"icon-counter\") and normalize-space(.)=\"7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button extra large Pending + +Solution: Clicks the third extra large Pending counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button medium Failed + +Solution: Clicks the third medium Failed counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Failed\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button small Passed + +Solution: Clicks the third small Passed counter button; the counter increments from 7. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button extra large Pending + +Solution: Clicks the secondary extra large Pending counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button medium Failed + +Solution: Clicks the secondary medium Failed counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Failed\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button small Passed + +Solution: Clicks the secondary small Passed counter button; the counter increments from 7. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button large selected + +Solution: Clicks the selected third large counter button and toggles its selected state. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and normalize-space(.)=\"Button text\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large selected + +Solution: Clicks the selected secondary large counter button and toggles its selected state. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and normalize-space(.)=\"Button text\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button large Pending dropdown + +Solution: Clicks the third large Pending counter button and opens its dropdown. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button large Skipped dropdown + +Solution: Clicks the third large Skipped counter button and opens its dropdown. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Skipped\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large Pending dropdown + +Solution: Clicks the secondary large Pending counter button and opens its dropdown. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large Skipped dropdown + +Solution: Clicks the secondary large Skipped counter button and opens its dropdown. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Skipped\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button large Failed with icon + +Solution: Clicks the third large Failed counter button with icon. + +```javascript +I.click("button.third-btn.btn-text-and-icon.btn-lg:has-text(\"Failed\\n \\n \\n 7\"):has(svg)") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large Failed with icon + +Solution: Clicks the secondary large Failed counter button with icon. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Failed\n \n \n 7\" and .//svg]") +``` + + +### SUCCEEDED: Drill click: Counter in third button large Passed + +Solution: Clicks the third large Passed counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in third button large Skipped + +Solution: Clicks the third large Skipped counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Skipped\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large Failed + +Solution: Clicks the secondary large Failed counter button with value 7. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Failed\n \n \n 7\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Counter in secondary button large Passed + +Solution: Clicks the secondary large Passed counter button; the counter increments from 7. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") +``` diff --git a/experience/Other_Buttons.md b/experience/Other_Buttons.md new file mode 100644 index 0000000..e1d4ad0 --- /dev/null +++ b/experience/Other_Buttons.md @@ -0,0 +1,66 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Other%20buttons +title: Testomat.io +summary: Curated other button interactions only. +--- +### SUCCEEDED: Drill click: Template button Use Template + +Solution: Clicks the Use Template button and opens the templates dropdown menu. + +```javascript +I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(@class,\"truncate\") and normalize-space(.)=\"Use Template\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Substatus button Passed + +Solution: Clicks the Passed substatus button and keeps the selected substatus state active. + +```javascript +I.click("//*[self::button and contains(@class,\"substatus\") and contains(@class,\"passed\") and contains(@class,\"selected\") and normalize-space(.)=\"Passed\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Substatus button Skipped + +Solution: Clicks the Skipped substatus button and keeps the selected substatus state active. + +```javascript +I.click("//*[self::button and contains(@class,\"substatus\") and contains(@class,\"skipped\") and contains(@class,\"selected\") and normalize-space(.)=\"Skipped\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Substatus button Failed + +Solution: Clicks the Failed substatus button and keeps the selected substatus state active. + +```javascript +I.click("//*[self::button and contains(@class,\"substatus\") and contains(@class,\"failed\") and contains(@class,\"selected\") and normalize-space(.)=\"Failed\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Substatus button Click me + +Solution: Clicks the Click me substatus action button and opens its loading spinner after click. + +```javascript +I.click("button.substatus.click:has-text(\"Click me\"):has(svg):not(:has(svg + svg))") +``` + + +### SUCCEEDED: Drill click: Lang button beautify + +Solution: Clicks the small beautify language button. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"beautify\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: Async button Click Me + +Solution: Clicks the async Click Me button and starts its loading state. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-md\") and contains(@class,\"btn-text-and-icon\") and normalize-space(.)=\"Click Me\" and not(.//svg)]") +``` diff --git a/experience/PowerSelect.md b/experience/PowerSelect.md new file mode 100644 index 0000000..fac5761 --- /dev/null +++ b/experience/PowerSelect.md @@ -0,0 +1,212 @@ +--- +url: /projects/test-d6178/components/?m=false&s=PowerSelect +title: Testomat.io +summary: Curated PowerSelect interactions grouped by distinct custom component behaviors. +--- +### SUCCEEDED: Drill select option: requirement source dropdown + +Solution: Opens the requirement source PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Select a requirement source\")]"); +I.click({"role":"option","text":"Confluence"}); +``` + + +### SUCCEEDED: Drill clear option: requirement source dropdown + +Solution: Clicks the selected PowerSelect value to clear it. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Confluence\")]") +``` + + +### SUCCEEDED: Drill click: assignee dropdown + +Solution: Opens the Assign to PowerSelect dropdown. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Assign to\")]") +``` + + +### SUCCEEDED: Drill click: TQL data source dropdown + +Solution: Opens the TQL search context PowerSelect dropdown. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"TQL search context\")]") +``` + + +### SUCCEEDED: Drill select option: tests data source dropdown + +Solution: Opens the Data Source PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Tests\")]"); +I.click({"role":"option","text":"Runs"}); +``` + + +### SUCCEEDED: Drill clear option: export mode dropdown + +Solution: Clicks the selected PowerSelect value to clear it. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Only found Tests\")]") +``` + + +### SUCCEEDED: Drill select option: format dropdown + +Solution: Opens the format PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"beautify\")]"); +I.click({"role":"option","text":"json"}); +``` + + +### SUCCEEDED: Drill click: invite users dialog + +Solution: Clicks the Invite users button and opens the invite user dialog. + +```javascript +I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Invite users\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill click: priority dropdown + +Solution: Clicks the icon-only priority PowerSelect dropdown and opens its options. + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and contains(@class,\"ember-power-select-trigger\")]") +``` + + +### SUCCEEDED: Drill select option: OS dropdown + +Solution: Opens the OS PowerSelect dropdown and selects an option. + +```javascript +I.click({"role":"button","text":"Select an OS"}); +I.click({"role":"option","text":"Windows"}); +``` + + +### SUCCEEDED: Drill click: action dropdown + +Solution: Clicks the icon-only PowerSelect dropdown in the Test section and opens its options. + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and contains(@class,\"ember-power-select-trigger\")]") +``` + + +### SUCCEEDED: Drill click: run dropdown + +Solution: Opens the run selection PowerSelect dropdown. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Select a run to check\")]") +``` + + +### SUCCEEDED: Drill select option: started by dropdown + +Solution: Opens the Started by PowerSelect dropdown and selects a user. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Started by\")]"); +I.click({"role":"option","text":"Denys Kuchma"}); +``` + + +### SUCCEEDED: Drill clear option: selected user dropdown + +Solution: Clicks the selected PowerSelect value to clear it. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"(Me)\")]") +``` + + +### SUCCEEDED: Drill select option: automation framework dropdown + +Solution: Opens the automation framework PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Select an automation framework you use\")]"); +I.click({"role":"option","text":"Cucumber"}); +``` + + +### SUCCEEDED: Drill select option: language dropdown + +Solution: Opens the language PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"Select a language you use\")]"); +I.click({"role":"option","text":"JavaScript"}); +``` + + +### SUCCEEDED: Drill verify disabled: disabled dropdowns + +Solution: Verifies that the Setup section contains disabled PowerSelect dropdowns. + +```javascript +I.seeElement("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and @aria-disabled=\"true\" and contains(.,\"vitest\")]"); +I.seeElement("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and @aria-disabled=\"true\" and contains(.,\"JavaScript\")]"); +``` + + +### SUCCEEDED: Drill click: timezone dropdown + +Solution: Opens the project timezone PowerSelect dropdown. + +```javascript +I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and contains(@class,\"ember-power-select-trigger\") and not(normalize-space(.))][1]") +``` + + +### SUCCEEDED: Drill select option: framework dropdown + +Solution: Opens the project framework PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"vitest\")]"); +I.click({"role":"option","text":"Cucumber"}); +``` + + +### SUCCEEDED: Drill click: language dropdown + +Solution: Opens the project language PowerSelect dropdown. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"JavaScript\")]") +``` + + +### SUCCEEDED: Drill select option: notification dropdown + +Solution: Opens the notification PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"How to notify\")]"); +I.click({"role":"option","text":"Email"}); +``` + + +### SUCCEEDED: Drill select option: manual type dropdown + +Solution: Opens the manual test type PowerSelect dropdown and selects an option. + +```javascript +I.click("//*[self::div and @role=\"button\" and contains(.,\"manual\")]"); +I.click("//li[contains(@class,\"ember-power-select-option\") and contains(.,\"automated\")]"); +``` diff --git a/experience/PowerSelect_Filters.md b/experience/PowerSelect_Filters.md new file mode 100644 index 0000000..605f518 --- /dev/null +++ b/experience/PowerSelect_Filters.md @@ -0,0 +1,244 @@ +--- +url: /projects/test-d6178/components/?m=false&s=PowerSelect%20Filters +title: Testomat.io +summary: Curated PowerSelect Filters interactions for simple selects, multiselects, date range, and filter actions. +--- +### SUCCEEDED: Drill select option: PowerSelect Filters Type dropdown + +Solution: Opens the Type PowerSelect filter and selects an option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Type\"]]//*[self::div and @role=\"button\" and contains(.,\"Select Type\")]"); +I.click({"role":"option","text":"Suite"}); +``` + + +### SUCCEEDED: Drill change option: PowerSelect Filters selected Type dropdown + +Solution: Opens the selected Type PowerSelect filter and selects another option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Type\"]]//*[self::div and @role=\"button\" and contains(.,\"Suite\")]"); +I.click({"role":"option","text":"Test"}); +``` + + +### SUCCEEDED: Drill clear option: PowerSelect Filters selected Type dropdown + +Solution: Clicks the selected Type PowerSelect filter value to clear it. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Type\"]]//*[self::div and @role=\"button\" and contains(.,\"Suite\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") +``` + + +### SUCCEEDED: Drill open date picker: PowerSelect Filters Date Range field + +Solution: Opens the Date Range filter date picker. + +```javascript +I.click("(//*[self::li and .//p[normalize-space(.)=\"Date Range\"]]//input[@placeholder=\"Select range\"])[1]") +``` + + +### SUCCEEDED: Drill select date: PowerSelect Filters Date Range field + +Solution: Opens the Date Range filter date picker and selects a date. + +```javascript +I.click("(//*[self::li and .//p[normalize-space(.)=\"Date Range\"]]//input[@placeholder=\"Select range\"])[1]"); +I.click("span[aria-label=\"April 15, 2026\"]"); +``` + + +### SUCCEEDED: Drill close date picker: PowerSelect Filters Date Range field + +Solution: Opens the Date Range filter date picker and closes it. + +```javascript +I.click("(//*[self::li and .//p[normalize-space(.)=\"Date Range\"]]//input[@placeholder=\"Select range\"])[1]"); +I.pressKey("Escape"); +``` + + +### SUCCEEDED: Drill select option: PowerSelect Filters Changed by dropdown + +Solution: Opens the Changed by PowerSelect filter and selects a user. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Changed by\"]]//*[self::div and @role=\"button\" and contains(.,\"Select user\")]"); +I.click("//ul[@role=\"listbox\"]//li[@role=\"option\" and contains(.,\"Denys Kuchma (me)\")]"); +``` + + +### SUCCEEDED: Drill clear option: PowerSelect Filters selected Changed by dropdown + +Solution: Clicks the selected Changed by PowerSelect filter value to clear it. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Changed by\"]]//*[self::div and @role=\"button\" and contains(.,\"(me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") +``` + + +### SUCCEEDED: Drill select option: PowerSelect Filters State dropdown + +Solution: Opens the State PowerSelect filter and selects an option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"State\"]]//*[self::div and @role=\"button\" and contains(.,\"Select State\")]"); +I.click({"role":"option","text":"automated"}); +``` + + +### SUCCEEDED: Drill change option: PowerSelect Filters selected State dropdown + +Solution: Opens the selected State PowerSelect filter and selects another option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"State\"]]//*[self::div and @role=\"button\" and contains(.,\"manual\")]"); +I.click({"role":"option","text":"automated"}); +``` + + +### SUCCEEDED: Drill clear option: PowerSelect Filters selected State dropdown + +Solution: Clicks the selected State PowerSelect filter value to clear it. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"State\"]]//*[self::div and @role=\"button\" and contains(.,\"manual\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") +``` + + +### SUCCEEDED: Drill select option: PowerSelect Filters Tag multiselect + +Solution: Opens the Tag PowerSelect multiselect filter and selects an option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"]]//input[@placeholder=\"Select Tag\"]"); +I.click({"role":"option","text":"@tag1"}); +``` + + +### SUCCEEDED: Drill toggle option: PowerSelect Filters selected Tag multiselect + +Solution: Opens the selected Tag PowerSelect multiselect filter and toggles an option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"]]//*[self::div and @role=\"button\" and contains(.,\"@tag1\") and contains(.,\"@tag2\") and contains(.,\"@tag3\")]"); +I.click({"role":"option","text":"@tag2"}); +``` + + +### SUCCEEDED: Drill type option: PowerSelect Filters selected Tag multiselect + +Solution: Focuses the selected Tag PowerSelect multiselect search input and confirms a typed option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"] and .//*[contains(.,\"@tag1\")]]//input[contains(@class,\"ember-power-select-trigger-multiple-input\")]"); +I.type("normal{Enter}"); +``` + + +### SUCCEEDED: Drill remove option: PowerSelect Filters selected Tag multiselect + +Solution: Clicks a selected Tag PowerSelect multiselect remove control. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"] and .//*[contains(.,\"@tag1\")]]//span[@role=\"button\" and @aria-label=\"remove element\"]") +``` + + +### SUCCEEDED: Drill select option: PowerSelect Filters Priority multiselect + +Solution: Opens the Priority PowerSelect multiselect filter and selects an option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Priority\"]]//input[@placeholder=\"Select Priority\"]"); +I.click("//li[@role=\"option\" and contains(.,\"high\")]"); +``` + + +### SUCCEEDED: Drill open selected: PowerSelect Filters selected Priority multiselect + +Solution: Opens the selected Priority PowerSelect multiselect filter. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Priority\"]]//*[self::div and @role=\"button\" and contains(.,\"low\") and contains(.,\"normal\") and contains(.,\"critical\")]") +``` + + +### SUCCEEDED: Drill remove option: PowerSelect Filters selected Priority multiselect + +Solution: Clicks a selected Priority PowerSelect multiselect remove control. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Priority\"] and .//*[contains(.,\"low\")]]//span[@role=\"button\" and @aria-label=\"remove element\"]") +``` + + +### SUCCEEDED: Drill select option: PowerSelect Filters Assigned to dropdown + +Solution: Opens the Assigned to PowerSelect filter and selects a user. + +```javascript +I.click({"role":"button","text":"Select Assignee"}); +I.click({"role":"option","text":"Denys Kuchma (me)"}); +``` + + +### SUCCEEDED: Drill clear option: PowerSelect Filters selected Assigned to dropdown + +Solution: Clicks the selected Assigned to PowerSelect filter value to clear it. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Assigned to\"]]//*[self::div and @role=\"button\" and contains(.,\"(me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") +``` + + +### SUCCEEDED: Drill clear option: PowerSelect Filters selected Field dropdown + +Solution: Clicks the selected Field PowerSelect filter value to clear it. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Field\"]]//*[self::div and @role=\"button\" and contains(.,\"Test\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") +``` + + +### SUCCEEDED: Drill change option: PowerSelect Filters selected Field dropdown + +Solution: Opens the selected Field PowerSelect filter, searches, and selects another option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Field\"]]//*[self::div and @role=\"button\" and contains(.,\"Test\")]"); +I.fillField("//div[contains(@class,\"ember-basic-dropdown-content\") and not(contains(@style,\"display: none\"))]//input[@role=\"combobox\"]", "test"); +I.pressKey("Enter"); +``` + + +### SUCCEEDED: Drill search option: PowerSelect Filters Value dropdown + +Solution: Opens the Value PowerSelect filter, searches, and confirms the typed option. + +```javascript +I.click("//*[self::li and .//p[normalize-space(.)=\"Value\"]]//*[self::div and @role=\"button\" and contains(.,\"Select value\")]"); +I.fillField("//div[contains(@class,\"ember-basic-dropdown-content\") and not(contains(@style,\"display: none\"))]//input[@role=\"combobox\"]", "test"); +I.pressKey("Enter"); +``` + + +### SUCCEEDED: Drill click: PowerSelect Filters Apply button + +Solution: Clicks Apply to apply the selected filters. + +```javascript +I.click("(//*[self::button and contains(@class,\"primary-btn\") and normalize-space(.)=\"Apply\"])[1]") +``` + + +### SUCCEEDED: Drill click: PowerSelect Filters Cancel button + +Solution: Clicks Cancel to discard the filter changes. + +```javascript +I.click("(//*[self::button and contains(@class,\"secondary-btn\") and normalize-space(.)=\"Cancel\"])[1]") +``` diff --git a/experience/PowerSelect_Input.md b/experience/PowerSelect_Input.md new file mode 100644 index 0000000..55909bf --- /dev/null +++ b/experience/PowerSelect_Input.md @@ -0,0 +1,175 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Powerselect%3A%3AInput +title: Testomat.io +summary: Curated PowerSelect::Input interactions for search, single select, error states, and tag multiselect inputs. +--- +### SUCCEEDED: Drill fill: PowerSelect::Input medium Search input + +Solution: Fills the medium Search input. + +```javascript +I.fillField("input[type=\"search\"][placeholder=\"Search\"].size-md", "text") +``` + + +### SUCCEEDED: Drill clear: PowerSelect::Input medium Search input + +Solution: Clears the typed medium Search input value. + +```javascript +I.clearField("input[type=\"search\"][placeholder=\"Search\"].size-md") +``` + + +### SUCCEEDED: Drill fill: PowerSelect::Input large Search input + +Solution: Fills the large Search input. + +```javascript +I.fillField("input[type=\"search\"][placeholder=\"Search\"].size-lg", "text") +``` + + +### SUCCEEDED: Drill clear: PowerSelect::Input large Search input + +Solution: Clears the typed large Search input value. + +```javascript +I.clearField("input[type=\"search\"][placeholder=\"Search\"].size-lg") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input User dropdown + +Solution: Opens the User PowerSelect::Input dropdown. + +```javascript +I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select user\")])[1]") +``` + + +### SUCCEEDED: Drill clear option: PowerSelect::Input selected User dropdown + +Solution: Clicks the selected User PowerSelect::Input value to clear it. + +```javascript +I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"(Me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[1]") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input Name of select dropdown + +Solution: Opens the Name of select PowerSelect::Input dropdown. + +```javascript +I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select item\")])[1]") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input selected Name of select dropdown + +Solution: Opens the selected Name of select PowerSelect::Input dropdown. + +```javascript +I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")])[1]") +``` + + +### SUCCEEDED: Drill clear option: PowerSelect::Input selected Name of select dropdown + +Solution: Clicks the selected Name of select PowerSelect::Input value to clear it. + +```javascript +I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[1]") +``` + + +### SUCCEEDED: Drill clear option: PowerSelect::Input error Name of select dropdown + +Solution: Clicks the selected error-state Name of select PowerSelect::Input value to clear it. + +```javascript +I.click("(//*[normalize-space(.)=\"Name of select (Error State)\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[1]") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input Tags multiselect + +Solution: Opens the Tags PowerSelect::Input multiselect. + +```javascript +I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select a requirement source\")])[1]") +``` + + +### SUCCEEDED: Drill type option: PowerSelect::Input Tags multiselect searchbox + +Solution: Focuses the Tags PowerSelect::Input multiselect searchbox and types a tag. + +```javascript +I.click("(//*[normalize-space(.)=\"Tags\"]/following::input[@type=\"search\" and @placeholder=\"Select a requirement source\"])[1]"); +I.type("@tag1{Enter}"); +``` + + +### SUCCEEDED: Drill remove option: PowerSelect::Input selected Tags multiselect + +Solution: Clicks a selected Tags PowerSelect::Input multiselect remove control. + +```javascript +I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"@tag1\") and contains(.,\"@tag2\")]//span[@role=\"button\" and @aria-label=\"remove element\"])[1]") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input large User dropdown + +Solution: Opens the large User PowerSelect::Input dropdown. + +```javascript +I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select user\")])[2]") +``` + + +### SUCCEEDED: Drill clear option: PowerSelect::Input large selected User dropdown + +Solution: Clicks the selected large User PowerSelect::Input value to clear it. + +```javascript +I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"(Me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[2]") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input large Name of select dropdown + +Solution: Opens the large Name of select PowerSelect::Input dropdown. + +```javascript +I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select item\")])[3]") +``` + + +### SUCCEEDED: Drill clear option: PowerSelect::Input large selected Name of select dropdown + +Solution: Clicks the selected large Name of select PowerSelect::Input value to clear it. + +```javascript +I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[3]") +``` + + +### SUCCEEDED: Drill click: PowerSelect::Input large Tags multiselect + +Solution: Opens the large Tags PowerSelect::Input multiselect. + +```javascript +I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select a requirement source\")])[2]") +``` + + +### SUCCEEDED: Drill remove option: PowerSelect::Input large selected Tags multiselect + +Solution: Clicks a selected large Tags PowerSelect::Input multiselect remove control. + +```javascript +I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"@tag1\") and contains(.,\"@tag2\")]//span[@role=\"button\" and @aria-label=\"remove element\"])[2]") +``` diff --git a/experience/PowerSelect_Multiple.md b/experience/PowerSelect_Multiple.md new file mode 100644 index 0000000..55892da --- /dev/null +++ b/experience/PowerSelect_Multiple.md @@ -0,0 +1,31 @@ +--- +url: /projects/test-d6178/components/?m=false&s=PowerSelect%20Multiple +title: Testomat.io +summary: Curated PowerSelect Multiple interactions for AssignMultiple. +--- +### SUCCEEDED: Drill select all: assign users multiselect + +Solution: Clicks the Select All button for the assign users multiselect. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Select All\" and not(.//svg)]") +``` + + +### SUCCEEDED: Drill select option: assign users multiselect + +Solution: Opens the assign users multiselect and selects a user. + +```javascript +I.click({"role":"searchbox","text":"Assign Users"}); +I.click({"role":"option","text":"Denys Kuchma"}); +``` + + +### SUCCEEDED: Drill remove selected users: assign users multiselect + +Solution: Clicks the Remove assign users button for the assign users multiselect. + +```javascript +I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Remove assign users\" and not(.//svg)]") +``` diff --git a/experience/PowerSelect_Typeahead.md b/experience/PowerSelect_Typeahead.md new file mode 100644 index 0000000..3f0673f --- /dev/null +++ b/experience/PowerSelect_Typeahead.md @@ -0,0 +1,27 @@ +--- +url: /projects/test-d6178/components/?m=false&s=PowerSelect%20Typeahead +title: Testomat.io +summary: Curated PowerSelect Typeahead interactions for empty and selected Group type inputs. +--- +### SUCCEEDED: Drill search option: PowerSelect Typeahead empty Group type combobox + +Solution: Focuses the empty Group type typeahead combobox, searches, and selects an option. + +```javascript +I.click("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[1]"); +I.fillField("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[1]", "Build"); +I.pressKey("ArrowDown"); +I.pressKey("Enter"); +``` + + +### SUCCEEDED: Drill change option: PowerSelect Typeahead selected Group type combobox + +Solution: Focuses the selected Group type typeahead combobox, searches, and selects another option. + +```javascript +I.click("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[2]"); +I.fillField("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[2]", "Release"); +I.pressKey("ArrowDown"); +I.pressKey("Enter"); +``` diff --git a/experience/Search_Input.md b/experience/Search_Input.md new file mode 100644 index 0000000..b3d481a --- /dev/null +++ b/experience/Search_Input.md @@ -0,0 +1,13 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Search%20Input +title: Testomat.io +summary: Curated search input interaction only. +--- +### SUCCEEDED: Drill fill: Search input "Search" + +Solution: Clicks the search input and fills it with a search query. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Search"]]//input[@placeholder="Search"]') +I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Search"]]//input[@placeholder="Search"]', 'test') +``` diff --git a/experience/Tabs.md b/experience/Tabs.md new file mode 100644 index 0000000..4f6ab92 --- /dev/null +++ b/experience/Tabs.md @@ -0,0 +1,111 @@ +--- +url: /projects/test-d6178/components/?m=false&s=Tabs +title: Testomat.io +summary: Curated tab interactions from the Tabs component showcase. +--- +### SUCCEEDED: Drill click: Inactive tab "Tab text" plain text + +Solution: Clicks the inactive plain text tab. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and not(.//*[local-name()="svg"]) and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Inactive tab "Tab text" leading icon + +Solution: Clicks the inactive tab with a leading autorenew icon. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Inactive tab "Tab text" trailing action icon + +Solution: Clicks the inactive tab with a copy action icon. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//button[contains(@class,"third-btn")] and not(.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Inactive tab "Tab text" leading and trailing action icons + +Solution: Clicks the inactive tab with a leading autorenew icon and copy action icon. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and .//button[contains(@class,"third-btn")] and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Inactive tab "Tab text 1" leading icon counter + +Solution: Clicks the inactive tab with a counter. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[contains(@class,"new-counter")] and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Inactive tab "Tab text" leading icon with run status icons + +Solution: Clicks the inactive tab with run status indicators. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[contains(@class,"run-status")]]') +``` + + +### SUCCEEDED: Drill click: Active tab "Tab text" plain text + +Solution: Clicks the active plain text tab. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and not(.//*[local-name()="svg"]) and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Active tab "Tab text" leading icon + +Solution: Clicks the active tab with a leading autorenew icon. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Active tab "Tab text" trailing action icon + +Solution: Clicks the active tab with a copy action icon. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//button[contains(@class,"third-btn")] and not(.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Active tab "Tab text" leading and trailing action icons + +Solution: Clicks the active tab with a leading autorenew icon and copy action icon. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and .//button[contains(@class,"third-btn")] and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Active tab "Tab text 1" leading icon counter + +Solution: Clicks the active tab with a counter. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[contains(@class,"new-counter")] and not(.//*[contains(@class,"run-status")])]') +``` + + +### SUCCEEDED: Drill click: Active tab "Tab text" leading icon with run status icons + +Solution: Clicks the active tab with run status indicators. + +```javascript +I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[contains(@class,"run-status")]]') +``` diff --git a/src/action-result.ts b/src/action-result.ts index affc1b4..ff1ea47 100644 --- a/src/action-result.ts +++ b/src/action-result.ts @@ -457,8 +457,9 @@ export class ActionResult implements ActionResultData { try { const urlObj = new URL(this.url); const path = urlObj.pathname.replace(/\/$/, '') || '/'; + const search = urlObj.search || ''; const hash = urlObj.hash || ''; - return path + hash; + return path + search + hash; } catch { // If URL parsing fails, assume it's already a relative URL return this.url; diff --git a/src/ai/bosun.ts b/src/ai/bosun.ts deleted file mode 100644 index 96e3b49..0000000 --- a/src/ai/bosun.ts +++ /dev/null @@ -1,557 +0,0 @@ -import { tool } from 'ai'; -import dedent from 'dedent'; -import { z } from 'zod'; -import { ActionResult } from '../action-result.ts'; -import { setActivity } from '../activity.ts'; -import type { ExperienceTracker } from '../experience-tracker.ts'; -import type Explorer from '../explorer.ts'; -import type { KnowledgeTracker } from '../knowledge-tracker.ts'; -import { Observability } from '../observability.ts'; -import { Plan, Task, Test, TestResult } from '../test-plan.ts'; -import { diffAriaSnapshots } from '../utils/aria.ts'; -import { HooksRunner } from '../utils/hooks-runner.ts'; -import { createDebug, tag } from '../utils/logger.ts'; -import { loop, pause } from '../utils/loop.ts'; -import type { Agent } from './agent.ts'; -import type { Conversation } from './conversation.ts'; -import type { Navigator } from './navigator.ts'; -import type { Provider } from './provider.ts'; -import type { Researcher } from './researcher.ts'; -import { locatorRule } from './rules.ts'; -import { TaskAgent, isInteractive } from './task-agent.ts'; -import { createAgentTools, createCodeceptJSTools } from './tools.ts'; - -const debugLog = createDebug('explorbot:bosun'); - -interface ComponentInfo { - name: string; - role: string; - locator: string; - section?: string; -} - -interface InteractionResult { - component: string; - action: string; - result: 'success' | 'failed' | 'unknown'; - description: string; - code?: string; -} - -interface ComponentTest extends Test { - component?: ComponentInfo; - interactions?: InteractionResult[]; -} - -interface DrillOptions { - knowledgePath?: string; - maxComponents?: number; - interactive?: boolean; -} - -export class Bosun extends TaskAgent implements Agent { - protected readonly ACTION_TOOLS = ['click', 'pressKey', 'form']; - emoji = '⚓'; - private explorer: Explorer; - private provider: Provider; - private researcher: Researcher; - private navigator: Navigator; - private hooksRunner: HooksRunner; - private currentPlan?: Plan; - private currentConversation: Conversation | null = null; - private allResults: InteractionResult[] = []; - private agentTools: any; - - MAX_ITERATIONS = 50; - - constructor(explorer: Explorer, provider: Provider, researcher: Researcher, navigator: Navigator, agentTools?: any) { - super(); - this.explorer = explorer; - this.provider = provider; - this.researcher = researcher; - this.navigator = navigator; - this.hooksRunner = new HooksRunner(explorer, explorer.getConfig()); - this.agentTools = agentTools; - } - - protected getNavigator(): Navigator { - return this.navigator; - } - - protected getExperienceTracker(): ExperienceTracker { - return this.explorer.getStateManager().getExperienceTracker(); - } - - protected getKnowledgeTracker(): KnowledgeTracker { - return this.explorer.getKnowledgeTracker(); - } - - protected getProvider(): Provider { - return this.provider; - } - - getSystemMessage(): string { - const currentUrl = this.explorer.getStateManager().getCurrentState()?.url; - const customPrompt = this.provider.getSystemPromptForAgent('bosun', currentUrl); - return dedent` - - You are a senior QA automation engineer focused on learning how to interact with UI components. - Your goal is to systematically discover all possible interactions with each component and document what works. - - - - 1. Review the UI map to understand all available components - 2. Create a plan listing all components to drill using drill_plan tool - 3. For each component, try appropriate interactions using click, form tools - 4. Use drill_record to document successful interactions - 5. If an interaction fails multiple times, use drill_ask for help (in interactive mode) - 6. Call drill_finish when all components have been tested - - - - - Focus on one component at a time - - Try multiple locator strategies if one fails - - Document what each interaction does (opens modal, navigates, etc.) - - Skip decorative or non-interactive elements - - Restore page state after each interaction (press Escape or navigate back) - - - ${locatorRule} - - ${customPrompt || ''} - `; - } - - async drill(opts: DrillOptions = {}): Promise { - const { knowledgePath, maxComponents = 20, interactive = isInteractive() } = opts; - const state = this.explorer.getStateManager().getCurrentState(); - if (!state) throw new Error('No page state available'); - - const sessionName = `bosun_${Date.now().toString(36)}`; - this.allResults = []; - - return Observability.run(`bosun: ${state.url}`, { tags: ['bosun'], sessionId: sessionName }, async () => { - tag('info').log(`Bosun starting drill on ${state.url}`); - setActivity(`${this.emoji} Researching page for drilling...`, 'action'); - - await this.hooksRunner.runBeforeHook('bosun', state.url); - - const research = await this.researcher.research(state, { screenshot: true, force: true }); - - this.currentPlan = new Plan(`Drill: ${state.url}`); - this.currentPlan.url = state.url; - - const conversation = this.provider.startConversation(this.getSystemMessage(), 'bosun'); - this.currentConversation = conversation; - - const initialPrompt = await this.buildInitialPrompt(state, research, maxComponents); - conversation.addUserText(initialPrompt); - - const drillTask = new Task(`Drill session: ${state.url}`, state.url); - const codeceptjsTools = createCodeceptJSTools(this.explorer, drillTask); - const drillFlowTools = this.createDrillFlowTools(state, interactive); - - const tools = { - ...codeceptjsTools, - ...drillFlowTools, - ...this.agentTools, - }; - - let drillFinished = false; - - await loop( - async ({ stop, iteration }) => { - debugLog(`Drill iteration ${iteration}`); - setActivity(`${this.emoji} Drilling components...`, 'action'); - - const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState()!); - - if (iteration > 1) { - conversation.cleanupTag('page_aria', '...cleaned aria snapshot...', 2); - const contextUpdate = await this.buildContextUpdate(currentState); - conversation.addUserText(contextUpdate); - } - - const result = await this.provider.invokeConversation(conversation, tools, { - maxToolRoundtrips: 5, - toolChoice: 'required', - }); - - if (!result) throw new Error('Failed to get response from provider'); - - const toolExecutions = result.toolExecutions || []; - this.trackToolExecutions(toolExecutions); - - for (const execution of toolExecutions) { - if (execution.wasSuccessful && this.ACTION_TOOLS.includes(execution.toolName)) { - const componentName = execution.input?.explanation || 'unknown'; - this.allResults.push({ - component: componentName, - action: execution.toolName, - result: 'success', - description: execution.output?.message || 'Action completed', - code: execution.output?.code, - }); - } - } - - const finishExecution = toolExecutions.find((e: any) => e.toolName === 'drill_finish'); - if (finishExecution) { - drillFinished = true; - stop(); - return; - } - - if (iteration >= this.MAX_ITERATIONS) { - tag('warning').log('Max iterations reached'); - stop(); - } - }, - { - maxAttempts: this.MAX_ITERATIONS, - interruptPrompt: 'Drill interrupted. Enter instruction (or "stop" to end):', - observability: { - agent: 'bosun', - sessionId: sessionName, - }, - catch: async ({ error, stop }) => { - tag('error').log(`Drill error: ${error}`); - stop(); - }, - } - ); - - await this.saveToExperience(state, this.allResults); - - if (knowledgePath) { - await this.saveToKnowledge(knowledgePath, state, this.allResults); - } - - await this.hooksRunner.runAfterHook('bosun', state.url); - this.logSummary(); - - return this.currentPlan; - }); - } - - private async buildInitialPrompt(state: any, research: string, maxComponents: number): Promise { - const actionResult = ActionResult.fromState(state); - const knowledge = this.getKnowledge(actionResult); - const experience = this.getExperience(actionResult); - - return dedent` - - Drill all interactive components on this page to learn how to interact with them. - Maximum components to drill: ${maxComponents} - - - - URL: ${state.url} - Title: ${state.title || 'Unknown'} - - - - ${research} - - - - ${actionResult.getInteractiveARIA()} - - - ${knowledge} - ${experience} - - - 1. First, call drill_plan to create a list of components to test - 2. Then systematically test each component using click or form tools - 3. Use drill_record to save observations about what each component does - 4. Press Escape or use drill_restore to reset state between tests - 5. Call drill_finish when all components have been tested - - `; - } - - private async buildContextUpdate(currentState: ActionResult): Promise { - const remainingComponents = this.currentPlan?.tests.filter((t) => !t.hasFinished).length || 0; - - return dedent` - - Current URL: ${currentState.url} - Components remaining: ${remainingComponents} - Successful interactions so far: ${this.allResults.filter((r) => r.result === 'success').length} - - - - ${currentState.getInteractiveARIA()} - - - Continue drilling components. Test each one and record what it does. - `; - } - - private createDrillFlowTools(originalState: any, interactive: boolean) { - const originalUrl = originalState.url; - - return { - drill_plan: tool({ - description: 'Create a plan of components to drill. Call this first to identify all testable components from the UI map.', - inputSchema: z.object({ - components: z.array( - z.object({ - name: z.string().describe('Display name of the component'), - role: z.string().describe('ARIA role (button, link, textbox, combobox, etc.)'), - locator: z.string().describe('Best locator for this component'), - section: z.string().optional().describe('Section of the page where component is located'), - }) - ), - }), - execute: async ({ components }) => { - for (const comp of components) { - const task = new Test(`Learn: ${comp.name} (${comp.role})`, 'normal', [`Discover interactions for ${comp.name}`], originalUrl) as ComponentTest; - task.component = comp; - task.interactions = []; - this.currentPlan!.addTest(task); - } - - tag('info').log(`Created drill plan with ${components.length} components`); - - return { - success: true, - message: `Plan created with ${components.length} components`, - components: components.map((c) => `${c.name} (${c.role})`), - instruction: 'Now test each component using click or form tools. Record observations with drill_record.', - }; - }, - }), - - drill_record: tool({ - description: 'Record what a component does after testing it. Call this after each successful interaction.', - inputSchema: z.object({ - component: z.string().describe('Component name that was tested'), - action: z.string().describe('Action performed (click, form)'), - result: z.string().describe('What happened (opened modal, navigated to X, showed dropdown, etc.)'), - code: z.string().optional().describe('The CodeceptJS code that worked'), - }), - execute: async ({ component, action, result, code }) => { - const task = this.findComponentTask(component); - if (task) { - task.addNote(`${action}: ${result}`, TestResult.PASSED); - task.finish(TestResult.PASSED); - } - - this.allResults.push({ - component, - action, - result: 'success', - description: result, - code, - }); - - tag('success').log(`${component}: ${action} -> ${result}`); - - return { - success: true, - recorded: `${component}: ${action} -> ${result}`, - instruction: 'Continue testing other components or call drill_finish when done.', - }; - }, - }), - - drill_restore: tool({ - description: 'Restore page state after testing a component. Use when page navigated away or modal opened.', - inputSchema: z.object({ - reason: z.string().describe('Why restoration is needed'), - }), - execute: async ({ reason }) => { - const currentState = this.explorer.getStateManager().getCurrentState(); - const action = this.explorer.createAction(); - - if (currentState?.url !== originalUrl) { - await action.execute(`I.amOnPage("${originalUrl}")`); - return { success: true, action: 'navigated back', url: originalUrl }; - } - - await action.execute('I.pressKey("Escape")'); - return { success: true, action: 'pressed Escape' }; - }, - }), - - drill_skip: tool({ - description: 'Skip a component that cannot be drilled.', - inputSchema: z.object({ - component: z.string().describe('Component to skip'), - reason: z.string().describe('Why this component is being skipped'), - }), - execute: async ({ component, reason }) => { - const task = this.findComponentTask(component); - if (task) { - task.addNote(`Skipped: ${reason}`, TestResult.FAILED); - task.finish(TestResult.FAILED); - } - - this.allResults.push({ - component, - action: 'skip', - result: 'unknown', - description: reason, - }); - - tag('warning').log(`Skipped ${component}: ${reason}`); - return { success: true, skipped: component, reason }; - }, - }), - - drill_ask: tool({ - description: 'Ask the user for help when stuck on a component. Only available in interactive mode.', - inputSchema: z.object({ - component: z.string().describe('Component you need help with'), - question: z.string().describe('What you need help with'), - triedLocators: z.array(z.string()).optional().describe('Locators already tried'), - }), - execute: async ({ component, question, triedLocators }) => { - if (!interactive) { - return { success: false, message: 'Not in interactive mode. Skip this component.' }; - } - - let prompt = `Help needed for "${component}"\n${question}`; - if (triedLocators?.length) { - prompt += `\n\nAlready tried:\n${triedLocators.map((l) => ` - ${l}`).join('\n')}`; - } - prompt += '\n\nYour CodeceptJS command ("skip" to continue):'; - - const userInput = await pause(prompt); - - if (!userInput || userInput.toLowerCase() === 'skip') { - return { success: false, skipped: true, instruction: 'Use drill_skip to skip this component.' }; - } - - return { - success: true, - userSuggestion: userInput, - instruction: `Try this command: ${userInput}`, - }; - }, - }), - - drill_finish: tool({ - description: 'Finish the drill session. Call when all components have been tested.', - inputSchema: z.object({ - summary: z.string().describe('Summary of what was learned during drilling'), - }), - execute: async ({ summary }) => { - for (const test of this.currentPlan!.tests) { - if (!test.hasFinished) { - test.addNote('Not tested'); - test.finish(TestResult.FAILED); - } - } - - tag('info').log(`Drill completed: ${summary}`); - - return { - success: true, - totalComponents: this.currentPlan!.tests.length, - successfulInteractions: this.allResults.filter((r) => r.result === 'success').length, - summary, - }; - }, - }), - }; - } - - private findComponentTask(componentName: string): ComponentTest | undefined { - return this.currentPlan?.tests.find((t) => { - const ct = t as ComponentTest; - return ct.component?.name === componentName || t.scenario.includes(componentName); - }) as ComponentTest | undefined; - } - - private async saveToExperience(state: any, results: InteractionResult[]): Promise { - const experienceTracker = this.getExperienceTracker(); - const actionResult = ActionResult.fromState(state); - - const successfulInteractions = results.filter((r) => r.result === 'success' && r.code); - - for (const interaction of successfulInteractions) { - await experienceTracker.saveSuccessfulResolution(actionResult, `Drill ${interaction.action}: ${interaction.component}`, interaction.code!, interaction.description); - } - - if (successfulInteractions.length > 0) { - tag('success').log(`Saved ${successfulInteractions.length} interactions to experience`); - } - } - - private async saveToKnowledge(knowledgePath: string, state: any, results: InteractionResult[]): Promise { - const knowledgeTracker = this.getKnowledgeTracker(); - const successfulInteractions = results.filter((r) => r.result === 'success'); - - if (successfulInteractions.length === 0) { - tag('warning').log('No successful interactions to save to knowledge'); - return; - } - - const content = this.generateKnowledgeContent(state, successfulInteractions); - const result = knowledgeTracker.addKnowledge(knowledgePath, content); - - tag('success').log(`Knowledge saved to: ${result.filePath}`); - } - - private generateKnowledgeContent(state: any, interactions: InteractionResult[]): string { - const lines: string[] = []; - lines.push('# Component Interactions\n'); - lines.push(`Learned interactions from drilling ${state.url}\n`); - - const groupedByComponent = new Map(); - for (const interaction of interactions) { - const existing = groupedByComponent.get(interaction.component) || []; - existing.push(interaction); - groupedByComponent.set(interaction.component, existing); - } - - for (const [component, items] of groupedByComponent) { - lines.push(`\n## ${component}\n`); - for (const item of items) { - lines.push(`- **${item.action}**: ${item.description}`); - if (item.code) { - lines.push('```js'); - lines.push(item.code); - lines.push('```'); - } - } - } - - return lines.join('\n'); - } - - private logSummary(): void { - if (!this.currentPlan) return; - - const total = this.currentPlan.tests.length; - const passed = this.currentPlan.tests.filter((t) => t.isSuccessful).length; - const failed = this.currentPlan.tests.filter((t) => t.hasFailed).length; - const successfulInteractions = this.allResults.filter((r) => r.result === 'success').length; - - tag('info').log('\nDrill Summary:'); - tag('info').log(` Total components: ${total}`); - tag('success').log(` Successful: ${passed}`); - if (failed > 0) { - tag('warning').log(` Failed: ${failed}`); - } - tag('info').log(` Total interactions learned: ${successfulInteractions}`); - - for (const test of this.currentPlan.tests) { - const componentTask = test as ComponentTest; - const status = test.isSuccessful ? '✓' : '✗'; - const successCount = componentTask.interactions?.filter((i) => i.result === 'success').length || 0; - tag('step').log(` ${status} ${componentTask.component?.name || test.scenario}: ${successCount} interactions`); - } - } - - getCurrentPlan(): Plan | undefined { - return this.currentPlan; - } - - getConversation(): Conversation | null { - return this.currentConversation; - } -} diff --git a/src/ai/driller.ts b/src/ai/driller.ts new file mode 100644 index 0000000..121c7ca --- /dev/null +++ b/src/ai/driller.ts @@ -0,0 +1,1160 @@ +import { tool } from 'ai'; +import dedent from 'dedent'; +import { z } from 'zod'; +import { ActionResult } from '../action-result.ts'; +import { setActivity } from '../activity.ts'; +import type { ExperienceTracker } from '../experience-tracker.ts'; +import type Explorer from '../explorer.ts'; +import type { KnowledgeTracker } from '../knowledge-tracker.ts'; +import { Observability } from '../observability.ts'; +import { Plan, Test, TestResult } from '../test-plan.ts'; +import { collectInteractiveNodes } from '../utils/aria.ts'; +import { HooksRunner } from '../utils/hooks-runner.ts'; +import { createDebug, tag } from '../utils/logger.ts'; +import { loop, pause } from '../utils/loop.ts'; +import { WebElement } from '../utils/web-element.ts'; +import type { Agent } from './agent.ts'; +import type { Conversation } from './conversation.ts'; +import type { Navigator } from './navigator.ts'; +import type { Provider } from './provider.ts'; +import { locatorRule } from './rules.ts'; +import { TaskAgent, isInteractive } from './task-agent.ts'; +import { createCodeceptJSTools } from './tools.ts'; + +const debugLog = createDebug('explorbot:driller'); + +interface ComponentInfo { + id: string; + name: string; + role: string; + locator: string; + preferredCode: string; + eidx: string; + description: string; + html: string; + text: string; + tag: string; + classes: string[]; + context: string; + variant: string; + placeholder: string; + disabled: boolean; + ariaMatches: string[]; +} + +interface InteractionResult { + componentId: string; + component: string; + action: string; + result: 'success' | 'failed' | 'unknown'; + description: string; + code?: string; +} + +interface ComponentTest extends Test { + component?: ComponentInfo; + interactions?: InteractionResult[]; +} + +interface DrillOptions { + knowledgePath?: string; + maxComponents?: number; + interactive?: boolean; +} + +export class Driller extends TaskAgent implements Agent { + protected readonly ACTION_TOOLS = ['click', 'pressKey', 'form']; + emoji = 'D'; + private explorer: Explorer; + private provider: Provider; + private navigator: Navigator; + private hooksRunner: HooksRunner; + private currentPlan?: Plan; + private currentConversation: Conversation | null = null; + private allResults: InteractionResult[] = []; + private verifiedAction: { componentId: string; toolName: string; code?: string; canonicalCode?: string } | null = null; + private pendingNestedContext: string | null = null; + + MAX_COMPONENT_ITERATIONS = 12; + + constructor(explorer: Explorer, provider: Provider, navigator: Navigator) { + super(); + this.explorer = explorer; + this.provider = provider; + this.navigator = navigator; + this.hooksRunner = new HooksRunner(explorer, explorer.getConfig()); + } + + protected getNavigator(): Navigator { + return this.navigator; + } + + protected getExperienceTracker(): ExperienceTracker { + return this.explorer.getStateManager().getExperienceTracker(); + } + + protected getKnowledgeTracker(): KnowledgeTracker { + return this.explorer.getKnowledgeTracker(); + } + + protected getProvider(): Provider { + return this.provider; + } + + getSystemMessage(component?: ComponentInfo): string { + const currentUrl = this.explorer.getStateManager().getCurrentState()?.url; + const customPrompt = this.provider.getSystemPromptForAgent('driller', currentUrl); + + return dedent` + + You are a senior QA automation engineer focused on drilling one UI component at a time. + Your goal is to discover reusable interactions for the component using HTML and ARIA only. + + + + 1. Study the provided page HTML and ARIA snapshot + 2. Focus on exactly one component at a time + 3. Try the smallest useful interaction using click, form, and pressKey tools + 4. Restore the page state after navigations, popups, or destructive attempts + 5. Record reusable interactions with drill_record + 6. Call drill_done only after you have finished exploring the component + + + + - Never ask for researcher output or rely on page UI maps + - Work from , , and the provided component HTML snippet + - Never use data-explorbot-eidx in locators + - Never use container locators in recorded code + - Prefer one-argument locators or self-contained XPath/CSS locators + - If the component is decorative, duplicated beyond recovery, or not drillable, call drill_skip + ${component ? `- Current component: ${component.name} (${component.role})` : ''} + + + ${drillLocatorRule} + + ${customPrompt || ''} + `; + } + + async drill(opts: DrillOptions = {}): Promise { + const { knowledgePath, maxComponents = 30, interactive = isInteractive() } = opts; + const currentState = this.explorer.getStateManager().getCurrentState(); + if (!currentState) throw new Error('No page state available'); + + const sessionName = `driller_${Date.now().toString(36)}`; + this.allResults = []; + + return Observability.run(`driller: ${currentState.url}`, { tags: ['driller'], sessionId: sessionName }, async () => { + tag('info').log(`Driller starting on ${currentState.url}`); + await this.hooksRunner.runBeforeHook('driller', currentState.url); + + const originalState = await this.captureAnnotatedState(); + const components = await this.collectComponents(originalState, maxComponents); + + this.currentPlan = new Plan(`Drill: ${originalState.url}`); + this.currentPlan.url = originalState.url; + + for (const component of components) { + const test = new Test(`Drill: ${component.name} [${component.id}]`, 'normal', [`Learn a reusable interaction for ${component.name}`], originalState.url) as ComponentTest; + test.component = component; + test.interactions = []; + this.currentPlan.addTest(test); + } + + if (components.length === 0) { + tag('warning').log('No drillable components found on page'); + await this.hooksRunner.runAfterHook('driller', originalState.url); + return this.currentPlan; + } + + for (const test of this.currentPlan.tests) { + const componentTest = test as ComponentTest; + if (!componentTest.component) continue; + await this.restoreOriginalState(originalState, `Prepare component ${componentTest.component.name}`); + await this.captureAnnotatedState(); + await this.drillComponent(componentTest, originalState, interactive); + } + + await this.saveToExperience(originalState, this.allResults); + if (knowledgePath) await this.saveToKnowledge(knowledgePath, originalState, this.allResults); + + await this.hooksRunner.runAfterHook('driller', originalState.url); + this.logSummary(); + return this.currentPlan; + }); + } + + private async captureAnnotatedState(): Promise { + setActivity(`${this.emoji} Capturing annotated page state...`, 'action'); + const action = this.explorer.createAction(); + try { + const annotated = await Promise.race([ + this.explorer.annotateElements(), + new Promise((_, reject) => { + setTimeout(() => reject(new Error('annotateElements timeout')), 15000); + }), + ]); + return action.capturePageState({ ariaSnapshot: annotated.ariaSnapshot }); + } catch (error) { + tag('warning').log(`Annotated capture failed, falling back to plain page state: ${error instanceof Error ? error.message : error}`); + return action.capturePageState(); + } finally { + setActivity(`${this.emoji} Annotated page state captured`, 'action'); + } + } + + private async collectComponents(state: ActionResult, maxComponents: number): Promise { + setActivity(`${this.emoji} Collecting components...`, 'action'); + const page = this.explorer.playwrightHelper.page; + const eidxList = await this.explorer.getEidxInContainer(null); + const webElements = await WebElement.fromEidxList(page, eidxList); + const ariaNodes = collectInteractiveNodes(state.ariaSnapshot); + const scored = webElements + .filter((element) => isDrillableElement(element)) + .map((element) => ({ element, score: scoreComponentPriority(element) })) + .sort((left, right) => right.score - left.score); + const primary = scored.filter((entry) => entry.score >= 0).map((entry) => entry.element); + const fallback = scored.filter((entry) => entry.score < 0).map((entry) => entry.element); + const primaryButtonLike = primary.filter((element) => isButtonLikeElement(element)); + const primaryOther = primary.filter((element) => !isButtonLikeElement(element)); + const fallbackButtonLike = fallback.filter((element) => isButtonLikeElement(element)); + const fallbackOther = fallback.filter((element) => !isButtonLikeElement(element)); + const prioritized = primaryButtonLike.length >= maxComponents + ? primaryButtonLike + : [...primaryButtonLike, ...fallbackButtonLike, ...primaryOther, ...fallbackOther]; + const components: ComponentInfo[] = []; + const seen = new Set(); + + for (const element of prioritized) { + if (components.length >= maxComponents) break; + const eidx = element.eidx; + if (!eidx || !element.clickXPath) continue; + const component = this.toComponentInfo(element, ariaNodes); + if (seen.has(component.id)) continue; + seen.add(component.id); + components.push(component); + } + + tag('info').log(`Prepared ${components.length} components for drilling (main content first)`); + return components; + } + + private toComponentInfo(element: WebElement, ariaNodes: Array>): ComponentInfo { + const role = inferRole(element); + const text = element.text || element.attrs['aria-label'] || element.attrs.placeholder || element.attrs.name || ''; + const fallbackName = element.attrs.id || element.attrs.class || element.tag; + const context = truncate(element.contextLabel, 80); + const variant = formatVariant(element.variantHints); + const name = formatComponentName(role, text || fallbackName, context, variant); + const normalizedText = normalized(text); + const ariaMatches = ariaNodes + .filter((node) => { + const nodeRole = typeof node.role === 'string' ? node.role : ''; + if (nodeRole !== role) return false; + const nodeName = typeof node.name === 'string' ? node.name : ''; + const normalizedName = normalized(nodeName); + if (normalizedName === '' || normalizedText === '') return false; + return normalizedName === normalizedText || normalizedName.includes(normalizedText) || normalizedText.includes(normalizedName); + }) + .slice(0, 3) + .map((node) => formatAriaNode(node)); + + const component: ComponentInfo = { + id: buildComponentId(element, role, text), + name, + role, + locator: element.clickXPath, + preferredCode: '', + eidx: element.eidx!, + description: element.description, + html: element.outerHTML, + text, + tag: element.tag, + classes: element.filteredClasses, + context, + variant, + placeholder: element.attrs.placeholder || '', + disabled: element.variantHints.includes('disabled') || element.filteredClasses.includes('cursor-not-allowed') || element.attrs.disabled !== undefined || element.attrs['aria-disabled'] === 'true', + ariaMatches, + }; + component.preferredCode = buildCanonicalClickCode(component); + return component; + } + + private async drillComponent(test: ComponentTest, originalState: ActionResult, interactive: boolean): Promise { + const component = test.component; + if (!component) return; + + if (component.disabled) { + const description = 'Component is disabled and has no drillable interactive behavior.'; + test.start(); + test.interactions ||= []; + test.interactions.push({ componentId: component.id, component: component.name, action: 'skip', result: 'unknown', description }); + test.addNote(`Skipped: ${description}`, TestResult.SKIPPED); + test.finish(TestResult.SKIPPED); + this.allResults.push({ componentId: component.id, component: component.name, action: 'skip', result: 'unknown', description }); + tag('warning').log(`Skipped ${component.name}: disabled component`); + return; + } + + test.start(); + this.verifiedAction = null; + this.pendingNestedContext = null; + const conversation = this.provider.startConversation(this.getSystemMessage(component), 'driller'); + this.currentConversation = conversation; + conversation.addUserText(await this.buildComponentPrompt(originalState, component)); + + let finished = false; + const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.explorer, test), component); + const tools = { ...actionTools, ...this.createDrillFlowTools(originalState, test, interactive) }; + + await loop(async ({ stop, iteration }) => { + debugLog(`Drilling component ${component.name}, iteration ${iteration}`); + setActivity(`${this.emoji} Drilling ${component.name}...`, 'action'); + + if (iteration > 1) { + const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState() || originalState); + conversation.addUserText(await this.buildContextUpdate(currentState, component)); + if (this.pendingNestedContext) { + conversation.addUserText(this.pendingNestedContext); + this.pendingNestedContext = null; + } + } + + const result = await this.provider.invokeConversation(conversation, tools, { + maxToolRoundtrips: 5, + toolChoice: 'required', + agentName: 'driller', + }); + + if (!result) throw new Error('Failed to get response from provider'); + + const toolExecutions = result.toolExecutions || []; + this.trackToolExecutions(toolExecutions); + const failedActionCount = toolExecutions.filter((execution: any) => this.ACTION_TOOLS.includes(execution.toolName) && !execution.wasSuccessful).length; + if (failedActionCount >= 4) stop(); + + const hasDone = toolExecutions.some((execution: any) => execution.toolName === 'drill_done' && execution.wasSuccessful); + const hasSkip = toolExecutions.some((execution: any) => execution.toolName === 'drill_skip' && execution.wasSuccessful); + if (hasDone || hasSkip) { + finished = true; + stop(); + } + + if (iteration >= this.MAX_COMPONENT_ITERATIONS) stop(); + }, { + maxAttempts: this.MAX_COMPONENT_ITERATIONS, + interruptPrompt: `Drill interrupted while testing "${component.name}". Enter instruction (or "stop" to end):`, + observability: { agent: 'driller', sessionId: `${test.id}_${component.eidx}` }, + catch: async ({ error, stop }) => { + tag('error').log(`Drill error for ${component.name}: ${error}`); + stop(); + }, + }); + + if (finished || test.hasFinished) return; + if ((test.interactions || []).some((interaction) => interaction.result === 'success')) { + test.addNote('Recorded reusable interactions before loop stopped', TestResult.PASSED); + test.finish(TestResult.PASSED); + return; + } + + test.addNote('No reusable interaction recorded', TestResult.FAILED); + test.finish(TestResult.FAILED); + this.allResults.push({ componentId: component.id, component: component.name, action: 'drill', result: 'failed', description: 'No reusable interaction recorded' }); + } + + private async buildComponentPrompt(originalState: ActionResult, component: ComponentInfo): Promise { + const html = await this.getComponentScopeHtml(component, originalState); + const knowledge = this.getKnowledge(originalState); + const experience = this.getExperience(originalState); + const ariaMatches = component.ariaMatches.length > 0 ? component.ariaMatches.map((line) => `- ${line}`).join('\n') : '- no direct ARIA match'; + + return dedent` + + Drill exactly one component and learn a reusable interaction for it. + + + + URL: ${originalState.url} + Title: ${originalState.title || 'Unknown'} + + + + ID: ${component.id} + Name: ${component.name} + Role: ${component.role} + Preferred locator: ${component.locator} + Preferred click code: ${component.preferredCode || '-'} + eidx: ${component.eidx} + DOM summary: ${component.description} + Text: ${component.text || '-'} + Context: ${component.context || '-'} + Variant: ${component.variant || '-'} + Matching ARIA candidates: + ${ariaMatches} + + + + ${component.html} + + + + ${html} + + + + ${originalState.getInteractiveARIA()} + + + ${knowledge} + ${experience} + + + 1. Work only with this component + 2. Use Preferred click code first unless it clearly fails, then try other self-contained locators from page HTML + 3. Never use container locators in code + 4. Never use data-explorbot-eidx in code + 5. If the page changes, use drill_restore before continuing + 6. Call drill_record for each reusable interaction you discover + 7. When you are done exploring the component, call drill_done + 8. If the component is not drillable, call drill_skip + 9. If similar components exist, use Context and Variant to distinguish this exact variant instead of skipping immediately + 10. Do not switch to a sibling with the same text but different variant or size. Stay anchored to the current component's Preferred locator, Context, and Variant. + + `; + } + + private async buildContextUpdate(currentState: ActionResult, component: ComponentInfo): Promise { + return dedent` + + Current URL: ${currentState.url} + Continue drilling component: ${component.name} + Context: ${component.context || '-'} + Variant: ${component.variant || '-'} + If the component moved or disappeared, reassess using the current ARIA tree. + + + + ${currentState.getInteractiveARIA()} + + `; + } + + private createDrillFlowTools(originalState: ActionResult, test: ComponentTest, interactive: boolean) { + return { + drill_record: tool({ + description: 'Record a reusable interaction for the current component. Use only when the code is reusable and does not depend on a container locator.', + inputSchema: z.object({ + action: z.string().describe('Action performed, for example click, fill, select, open, toggle'), + result: z.string().describe('What happened after the interaction'), + code: z.string().describe('Reusable CodeceptJS code that worked'), + }), + execute: async ({ action, result, code }) => { + const component = test.component; + if (!component) return { success: false, message: 'No active component' }; + if (!this.hasVerifiedAction(component.id)) { + return { success: false, message: 'drill_record requires a real successful click, form, or pressKey for this component in the current drill run.' }; + } + + const exactCode = this.verifiedAction?.code?.trim(); + const canonicalCode = this.verifiedAction?.canonicalCode?.trim(); + const recordedCode = code.trim(); + if (exactCode && canonicalCode && recordedCode !== exactCode && recordedCode !== canonicalCode && !recordedCode.includes(exactCode) && !recordedCode.includes(canonicalCode)) { + return { success: false, message: `drill_record must save the verified code for this component: ${canonicalCode}` }; + } + if (exactCode && !canonicalCode && recordedCode !== exactCode && !recordedCode.includes(exactCode)) { + return { success: false, message: `drill_record must save the exact code that just worked for this component: ${this.verifiedAction?.code || exactCode}` }; + } + if (hasContainerLocator(code)) { + return { success: false, message: 'Container locators are not allowed in driller records. Rewrite the code with a self-contained locator.' }; + } + + const normalizedResult = normalizeInteractionResult(component, action, result); + const interaction: InteractionResult = { + componentId: component.id, + component: component.name, + action, + result: 'success', + description: normalizedResult, + code: recordedCode === exactCode || recordedCode === canonicalCode ? canonicalCode || code : code, + }; + + test.interactions ||= []; + test.interactions.push(interaction); + test.addNote(`${action}: ${normalizedResult}`, TestResult.PASSED); + this.allResults.push(interaction); + + tag('success').log(`${component.name}: ${action} -> ${normalizedResult}`); + return { success: true, recorded: `${component.name}: ${action} -> ${normalizedResult}` }; + }, + }), + + drill_done: tool({ + description: 'Finish drilling the current component after all useful interactions have been recorded.', + inputSchema: z.object({ + summary: z.string().describe('What was learned about this component'), + }), + execute: async ({ summary }) => { + const component = test.component; + if (!component) return { success: false, message: 'No active component' }; + if (this.pendingNestedContext) { + return { success: false, message: 'A nested overlay or popup opened after the last action. Drill useful interactions inside it before calling drill_done.' }; + } + const successCount = (test.interactions || []).filter((interaction) => interaction.result === 'success').length; + if (successCount === 0) { + return { success: false, message: 'Record at least one reusable interaction before calling drill_done, or use drill_skip.' }; + } + + test.addNote(`Completed: ${summary}`, TestResult.PASSED); + test.finish(TestResult.PASSED); + return { success: true, summary, recorded: successCount }; + }, + }), + + drill_skip: tool({ + description: 'Skip the current component when it is decorative, duplicated beyond recovery, or not drillable.', + inputSchema: z.object({ + reason: z.string().describe('Why the component is being skipped'), + }), + execute: async ({ reason }) => { + const component = test.component; + if (!component) return { success: false, message: 'No active component' }; + + const interaction: InteractionResult = { + componentId: component.id, + component: component.name, + action: 'skip', + result: 'unknown', + description: reason, + }; + + test.interactions ||= []; + test.interactions.push(interaction); + test.addNote(`Skipped: ${reason}`, TestResult.SKIPPED); + test.finish(TestResult.SKIPPED); + this.allResults.push(interaction); + + tag('warning').log(`Skipped ${component.name}: ${reason}`); + return { success: true, skipped: component.name }; + }, + }), + + drill_restore: tool({ + description: 'Restore the original page state before continuing drilling.', + inputSchema: z.object({ + reason: z.string().describe('Why restoration is needed'), + }), + execute: async ({ reason }) => { + await this.restoreOriginalState(originalState, reason); + await this.captureAnnotatedState(); + const currentState = this.explorer.getStateManager().getCurrentState(); + return { success: true, url: currentState?.url || originalState.url }; + }, + }), + + drill_ask: tool({ + description: 'Ask the user for help when stuck. Only available in interactive mode.', + inputSchema: z.object({ + question: z.string().describe('What help is needed'), + }), + execute: async ({ question }) => { + if (!interactive) return { success: false, message: 'Not in interactive mode' }; + const userInput = await pause(`${question}\n\nYour CodeceptJS command ("skip" to continue):`); + if (!userInput || userInput.toLowerCase() === 'skip') return { success: false, skipped: true }; + return { success: true, userSuggestion: userInput, instruction: `Execute this suggestion if it helps: ${userInput}` }; + }, + }), + }; + } + + private async restoreOriginalState(originalState: ActionResult, reason: string): Promise { + const currentState = this.explorer.getStateManager().getCurrentState(); + const targetUrl = originalState.fullUrl || originalState.url; + const action = this.explorer.createAction(); + + if (currentState?.url !== originalState.url) { + await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`, false); + return; + } + + await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`, false); + } + + private async saveToExperience(state: ActionResult, results: InteractionResult[]): Promise { + const experienceTracker = this.getExperienceTracker(); + const successfulInteractions = results.filter((result) => result.result === 'success' && result.code); + + for (const interaction of successfulInteractions) { + await experienceTracker.saveSuccessfulResolution(state, `Drill ${interaction.action}: ${interaction.component}`, interaction.code!, interaction.description); + } + + if (successfulInteractions.length > 0) { + tag('success').log(`Saved ${successfulInteractions.length} drill interactions to experience`); + } + } + + private createVerifiedActionTools(baseTools: Record, component: ComponentInfo): Record { + const wrappedTools = { ...baseTools }; + + for (const toolName of this.ACTION_TOOLS) { + const originalTool = wrappedTools[toolName]; + if (!originalTool) continue; + wrappedTools[toolName] = tool({ + description: originalTool.description, + inputSchema: originalTool.inputSchema, + execute: async (input: any) => { + const result = await originalTool.execute(input); + if (result?.success) { + this.verifiedAction = { + componentId: component.id, + toolName, + code: typeof result.code === 'string' ? result.code : undefined, + canonicalCode: typeof result.code === 'string' ? canonicalizeRecordedClick(component, result.code) : undefined, + }; + this.pendingNestedContext = await this.detectNestedOverlayContext(component, result); + } + return result; + }, + }); + } + + return wrappedTools; + } + + private hasVerifiedAction(componentId: string): boolean { + return this.verifiedAction?.componentId === componentId; + } + + private async detectNestedOverlayContext(component: ComponentInfo, result: any): Promise { + if (!result?.pageDiff?.ariaChanges || result.pageDiff.urlChanged) return null; + + const overlayHtml = await this.getVisibleOverlayHtml(); + if (!overlayHtml) return null; + + const state = this.explorer.getStateManager().getCurrentState(); + if (!state) return null; + const currentState = ActionResult.fromState(state); + return dedent` + + The last action on ${component.name} opened a nested overlay, popup, dropdown, menu, or calendar. + Drill useful interactions inside this nested UI before calling drill_done. + Keep the recorded code reusable and include the parent-opening action when the nested element requires the overlay to be open. + + + ${overlayHtml} + + + + ${currentState.getInteractiveARIA()} + + + `; + } + + private async getVisibleOverlayHtml(): Promise { + const page = this.explorer.playwrightHelper.page; + return page.evaluate(() => { + const selectors = [ + '.flatpickr-calendar.open', + '.flatpickr-calendar:not([style*="display: none"]):not([style*="visibility: hidden"])', + '.ember-attacher:not([style*="display: none"]):not([style*="visibility: hidden"])', + '[role="dialog"]', + '[role="listbox"]', + '[role="menu"]', + '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])', + '[x-placement]:not([style*="display: none"]):not([style*="visibility: hidden"])', + '.dropdown-menu:not([style*="display: none"]):not([style*="visibility: hidden"])', + '.popover:not([style*="display: none"]):not([style*="visibility: hidden"])', + ]; + + function isVisible(element: Element): boolean { + const html = element as HTMLElement; + const style = window.getComputedStyle(html); + const rect = html.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return false; + if (style.display === 'none' || style.visibility === 'hidden') return false; + if (Number.parseFloat(style.opacity || '1') < 0.1) return false; + return true; + } + + const overlays: string[] = []; + const seen = new Set(); + for (const selector of selectors) { + for (const element of Array.from(document.querySelectorAll(selector))) { + if (seen.has(element)) continue; + seen.add(element); + if (!isVisible(element)) continue; + const text = (element.textContent || '').replace(/\s+/g, ' ').trim(); + const interactiveCount = element.querySelectorAll('button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [tabindex]').length; + if (interactiveCount === 0 && text.length === 0) continue; + overlays.push((element as HTMLElement).outerHTML.slice(0, 6000)); + } + } + + return overlays.slice(0, 3).join('\n\n--- overlay ---\n\n'); + }); + } + + private async getComponentScopeHtml(component: ComponentInfo, originalState: ActionResult): Promise { + const page = this.explorer.playwrightHelper.page; + const scopedHtml = await page.evaluate((eidx: string) => { + const element = document.querySelector(`[data-explorbot-eidx="${eidx}"]`); + if (!element) return ''; + + function countInteractive(node: Element): number { + return node.querySelectorAll('button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]').length; + } + + let current = element.parentElement; + while (current) { + const count = countInteractive(current); + if (count > 0 && count <= 16) return current.outerHTML.slice(0, 8000); + current = current.parentElement; + } + + if (element instanceof HTMLElement) return element.outerHTML.slice(0, 8000); + return ''; + }, component.eidx); + + if (scopedHtml) return scopedHtml; + return await originalState.combinedHtml(); + } + + private async saveToKnowledge(knowledgePath: string, state: ActionResult, results: InteractionResult[]): Promise { + const knowledgeTracker = this.getKnowledgeTracker(); + const successfulInteractions = results.filter((result) => result.result === 'success'); + if (successfulInteractions.length === 0) { + tag('warning').log('No successful interactions to save to knowledge'); + return; + } + + const content = this.generateKnowledgeContent(state, successfulInteractions); + const result = knowledgeTracker.addKnowledge(knowledgePath, content); + tag('success').log(`Knowledge saved to: ${result.filePath}`); + } + + private generateKnowledgeContent(state: ActionResult, interactions: InteractionResult[]): string { + const lines: string[] = []; + lines.push('# Component Interactions\n'); + lines.push(`Learned interactions from drilling ${state.url}\n`); + + const groupedByComponent = new Map(); + for (const interaction of interactions) { + const existing = groupedByComponent.get(interaction.component) || []; + existing.push(interaction); + groupedByComponent.set(interaction.component, existing); + } + + for (const [component, items] of groupedByComponent) { + lines.push(`\n## ${component}\n`); + for (const item of items) { + lines.push(`- **${item.action}**: ${item.description}`); + if (item.code) { + lines.push('```js'); + lines.push(item.code); + lines.push('```'); + } + } + } + + return lines.join('\n'); + } + + private logSummary(): void { + if (!this.currentPlan) return; + + const total = this.currentPlan.tests.length; + const passed = this.currentPlan.tests.filter((test) => test.isSuccessful).length; + const skipped = this.currentPlan.tests.filter((test) => test.isSkipped).length; + const failed = this.currentPlan.tests.filter((test) => test.hasFailed).length; + + tag('info').log('\nDrill Summary:'); + tag('info').log(` Total components: ${total}`); + tag('success').log(` Successful: ${passed}`); + if (skipped > 0) tag('warning').log(` Skipped: ${skipped}`); + if (failed > 0) tag('warning').log(` Failed: ${failed}`); + + for (const test of this.currentPlan.tests) { + const componentTest = test as ComponentTest; + const status = test.isSuccessful ? 'PASS' : test.isSkipped ? 'SKIP' : 'FAIL'; + tag('step').log(` ${status} ${componentTest.component?.name || test.scenario}`); + } + } + + getCurrentPlan(): Plan | undefined { + return this.currentPlan; + } + + getConversation(): Conversation | null { + return this.currentConversation; + } +} + +function formatAriaNode(node: Record): string { + const role = typeof node.role === 'string' ? node.role : 'unknown'; + const name = typeof node.name === 'string' ? node.name : ''; + const value = typeof node.value === 'string' ? `: ${node.value}` : ''; + return [role, name ? `"${name}"` : '', value].filter(Boolean).join(' ').trim(); +} + +function inferRole(element: WebElement): string { + if (element.tag === 'iframe' && element.variantHints.includes('code-editor')) return 'code-editor'; + if (element.role) return element.role.toLowerCase(); + const explicitRole = element.attrs.role; + if (explicitRole) return explicitRole.toLowerCase(); + if (element.tag === 'a' && element.attrs.href) return 'link'; + if (element.tag === 'button') return 'button'; + if (element.tag === 'iframe') return 'iframe'; + if (element.tag === 'select') return 'combobox'; + if (element.tag === 'textarea') return 'textbox'; + if (element.tag === 'input') { + const type = (element.attrs.type || 'text').toLowerCase(); + if (type === 'checkbox') return 'checkbox'; + if (type === 'radio') return 'radio'; + return 'textbox'; + } + return element.tag; +} + +function normalized(value: string): string { + return value.trim().toLowerCase(); +} + +function capitalize(value: string): string { + if (!value) return value; + return value[0].toUpperCase() + value.slice(1); +} + +function truncate(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + return `${value.slice(0, maxLength - 3)}...`; +} + +function buildComponentId(element: WebElement, role: string, text: string): string { + const parts = [role, normalized(text), normalized(element.contextLabel), element.variantHints.join('|'), element.clickXPath, String(element.eidx || '')]; + return parts.join('|').toLowerCase(); +} + +function canonicalizeRecordedClick(component: ComponentInfo, fallbackCode: string): string { + const preferred = buildCanonicalClickCode(component); + if (preferred) return preferred; + return fallbackCode; +} + +function buildCanonicalClickCode(component: ComponentInfo): string { + if (component.tag === 'a') return ''; + if (component.tag === 'iframe' || component.role === 'code-editor') return buildEmbeddedFrameCode(component); + + const scopedCode = buildScopedFreestyleClickCode(component); + if (scopedCode) return scopedCode; + + const variantHints = parseVariantHints(component.variant); + const classSelector = buildClassSelector(component.tag, component.classes); + if (!classSelector) return component.locator ? `I.click(${JSON.stringify(component.locator)})` : ''; + + if (!component.text) { + let selector = classSelector; + if (variantHints.has('double-icon')) selector += ':has(svg):has(svg + svg)'; + else if (variantHints.has('has-icon') || variantHints.has('icon-only')) selector += ':has(svg)'; + return `I.click(${JSON.stringify(selector)})`; + } + + let selector = `${classSelector}:has-text(${JSON.stringify(component.text)})`; + if (variantHints.has('double-icon')) selector += ':has(svg):has(svg + svg)'; + else if (variantHints.has('trailing-icon')) selector += ':has(svg):not(:has(svg + svg))'; + else if (variantHints.has('leading-icon') || variantHints.has('has-icon')) selector += ':has(svg)'; + + if (!variantHints.has('has-icon') && !variantHints.has('icon-only') && !variantHints.has('leading-icon') && !variantHints.has('trailing-icon') && !variantHints.has('double-icon')) { + const textLiteral = component.text.replace(/"/g, '\\"'); + const classConditions = component.classes.slice(0, 5).map((cls) => `contains(@class,"${cls}")`); + const xpathConditions = [`self::${component.tag}`]; + xpathConditions.push(...classConditions); + xpathConditions.push(`normalize-space(.)="${textLiteral}"`); + xpathConditions.push('not(.//svg)'); + return `I.click(${JSON.stringify(`//*[${xpathConditions.join(' and ')}]`)})`; + } + + return `I.click(${JSON.stringify(selector)})`; +} + +function buildScopedFreestyleClickCode(component: ComponentInfo): string { + if (!component.context) return ''; + + const scope = `//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)=${xpathLiteral(component.context)}]]`; + if (component.role === 'tab') { + const tabCondition = buildTabVariantXPathCondition(component); + return `I.click(${JSON.stringify(`${scope}//li[@role="tab"${tabCondition}]`)})`; + } + + if (component.role === 'switch') { + const enabled = component.classes.includes('cursor-not-allowed') ? '' : ' and not(contains(@class,"cursor-not-allowed"))'; + return `I.click(${JSON.stringify(`${scope}//button[@role="switch"${enabled}]`)})`; + } + + if (component.tag === 'input' || component.role === 'textbox' || component.role === 'searchbox') { + const placeholder = component.placeholder; + if (placeholder) return `I.click(${JSON.stringify(`${scope}//input[@placeholder=${xpathLiteral(placeholder)}]`)})`; + if (component.classes.length > 0) { + const classConditions = component.classes.slice(0, 4).map((cls) => `contains(@class,${xpathLiteral(cls)})`).join(' and '); + return `I.click(${JSON.stringify(`${scope}//input[${classConditions}]`)})`; + } + } + + return ''; +} + +function buildEmbeddedFrameCode(component: ComponentInfo): string { + const src = component.html.match(/\ssrc=["']([^"']+)["']/i)?.[1] || ''; + const sourceIndex = component.html.match(/\sdata-explorbot-frame-source-index=["'](\d+)["']/i)?.[1] || ''; + const srcCondition = src ? `contains(@src,${xpathLiteral(src)})` : ''; + let scope = ''; + if (component.context) { + scope = `//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)=${xpathLiteral(component.context)}]]`; + } + + let iframeLocator = '//iframe'; + if (scope && !sourceIndex) iframeLocator = `${scope}//iframe`; + if (srcCondition) iframeLocator += `[${srcCondition}]`; + if (sourceIndex) iframeLocator = `(${iframeLocator})[${sourceIndex}]`; + + let editorLocator = 'body'; + let text = 'test'; + if (component.variant.includes('code-editor')) { + editorLocator = '.monaco-editor'; + text = 'const value = "test";'; + } + + return [ + `I.switchTo(${JSON.stringify(iframeLocator)})`, + `I.click(${JSON.stringify(editorLocator)})`, + `I.type(${JSON.stringify(text)})`, + 'I.switchTo()', + ].join('\n'); +} + +function buildTabVariantXPathCondition(component: ComponentInfo): string { + const html = component.html.toLowerCase(); + const hasAutorenew = html.includes('md-icon-autorenew'); + const hasPlay = html.includes('md-icon-play'); + const hasCopyButton = html.includes('third-btn') || html.includes('md-icon-content-copy'); + const hasCounter = html.includes('new-counter'); + const hasStatus = html.includes('run-status'); + const conditions: string[] = []; + + if (hasStatus) conditions.push('.//*[contains(@class,"run-status")]'); + else conditions.push('not(.//*[contains(@class,"run-status")])'); + + if (hasCounter) conditions.push('.//*[contains(@class,"new-counter")]'); + else conditions.push('not(.//*[contains(@class,"new-counter")])'); + + if (hasCopyButton) conditions.push('.//button[contains(@class,"third-btn")]'); + else conditions.push('not(.//button[contains(@class,"third-btn")])'); + + if (hasAutorenew) conditions.push('.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")]'); + else conditions.push('not(.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")])'); + + if (hasPlay) conditions.push('.//*[local-name()="svg" and contains(@class,"md-icon-play")]'); + else conditions.push('not(.//*[local-name()="svg" and contains(@class,"md-icon-play")])'); + + return conditions.length > 0 ? ` and ${conditions.join(' and ')}` : ''; +} + +function formatVariant(variantHints: string[]): string { + if (variantHints.length === 0) return ''; + return variantHints.slice(0, 4).join(', '); +} + +function formatComponentName(role: string, label: string, context: string, variant: string): string { + const safeLabel = label.trim(); + const quotedLabel = safeLabel ? `"${truncate(safeLabel, 48)}"` : role === 'button' ? '"Icon button"' : capitalize(role); + const parts = [`${capitalize(role)} ${quotedLabel}`.trim()]; + if (context) parts.push(`[${context}]`); + if (variant) parts.push(`(${variant})`); + return parts.join(' ').trim(); +} + +function normalizeInteractionResult(component: ComponentInfo, action: string, result: string): string { + const value = result.trim(); + if (!value) return fallbackInteractionResult(component, action); + + const normalizedValue = value.toLowerCase(); + const weakPhrases = [ + 'button clicked', + 'clicked button', + 'button was clicked', + 'component clicked', + 'page remains same', + 'page stayed the same', + 'no visible change', + 'action performed', + 'clicked', + ]; + + if (weakPhrases.some((phrase) => normalizedValue === phrase || normalizedValue.includes(phrase))) { + return fallbackInteractionResult(component, action); + } + + if (!/[.!?]$/.test(value)) return `${value}.`; + return value; +} + +function fallbackInteractionResult(component: ComponentInfo, action: string): string { + const role = component.role || component.tag; + const label = component.text ? `"${truncate(component.text, 40)}"` : `the ${role}`; + const variant = component.variant ? ` (${component.variant})` : ''; + if (action === 'click') return `Clicked ${label}${variant}.`; + if (action === 'pressKey') return `Pressed key on ${label}${variant}.`; + if (action === 'form') return `Submitted interaction for ${label}${variant}.`; + return `${capitalize(action)} executed for ${label}${variant}.`; +} + +function hasContainerLocator(code: string): boolean { + for (const line of code.split('\n').map((entry) => entry.trim()).filter(Boolean)) { + const argCount = countTopLevelArgs(line); + if (line.startsWith('I.click(') && argCount >= 2) return true; + if (line.startsWith('I.fillField(') && argCount >= 3) return true; + if (line.startsWith('I.selectOption(') && argCount >= 3) return true; + if (line.startsWith('I.attachFile(') && argCount >= 3) return true; + if (line.startsWith('I.checkOption(') && argCount >= 2) return true; + if (line.startsWith('I.uncheckOption(') && argCount >= 2) return true; + } + return false; +} + +function countTopLevelArgs(line: string): number { + const start = line.indexOf('('); + const end = line.lastIndexOf(')'); + if (start === -1 || end === -1 || end <= start + 1) return 0; + + const body = line.slice(start + 1, end); + let count = 1; + let depth = 0; + let quote = ''; + + for (let i = 0; i < body.length; i++) { + const char = body[i]; + const escaped = body[i - 1] === '\\'; + + if (quote) { + if (char === quote && !escaped) quote = ''; + continue; + } + + if (char === '"' || char === "'" || char === '`') { + quote = char; + continue; + } + + if (char === '(' || char === '[' || char === '{') { + depth++; + continue; + } + + if (char === ')' || char === ']' || char === '}') { + depth = Math.max(0, depth - 1); + continue; + } + + if (char === ',' && depth === 0) count++; + } + + return count; +} + +function buildClassSelector(tag: string, classes: string[]): string { + const safeClasses = classes.filter((cls) => /^[a-z0-9_-]+$/i.test(cls)).slice(0, 5); + if (safeClasses.length === 0) return ''; + return `${tag}${safeClasses.map((cls) => `.${cls}`).join('')}`; +} + +function parseVariantHints(variant: string): Set { + return new Set(variant.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean)); +} + +function xpathLiteral(value: string): string { + if (!value.includes('"')) return `"${value}"`; + if (!value.includes("'")) return `'${value}'`; + return `concat("${value.replace(/"/g, '", \'"\', "')}")`; +} + +function scoreComponentPriority(element: WebElement): number { + let score = 0; + const hints = element.areaHints; + const text = normalized(element.text); + const attrs = Object.values(element.attrs).join(' ').toLowerCase(); + const role = (element.role || element.attrs.role || element.tag).toLowerCase(); + + if (hints.includes('main')) score += 50; + if (hints.includes('article')) score += 40; + if (hints.includes('section')) score += 20; + if (hints.some((hint) => hint.includes('content'))) score += 20; + if (role === 'tab') score += 35; + if (isSemanticFormControl(element)) score += 35; + if (element.tag === 'iframe') score += 35; + if (element.variantHints.includes('code-editor')) score += 60; + if (element.tag === 'button') score += 20; + if (element.tag === 'input' || element.tag === 'textarea' || element.tag === 'select') score += 18; + if (element.tag === 'a') score -= 40; + if (text.length > 0) score += Math.min(text.length, 20); + if (hints.includes('nav') || hints.includes('menu') || hints.includes('header') || hints.includes('footer') || hints.includes('aside')) score -= 90; + if (hints.some((hint) => hint.startsWith('role:navigation') || hint.startsWith('role:menu') || hint.startsWith('role:menubar') || hint.startsWith('role:tablist'))) score -= 90; + if (attrs.includes('sidebar') || attrs.includes('sidemenu') || attrs.includes('topnav') || attrs.includes('navbar') || attrs.includes('breadcrumb')) score -= 40; + if (text === 'home' || text === 'settings' || text === 'profile' || text === 'logout') score -= 10; + if (attrs.includes('tooltip') || attrs.includes('attacher') || attrs.includes('popover') || attrs.includes('dropdown')) score -= 20; + return score; +} + +function isDrillableElement(element: WebElement): boolean { + const attrs = Object.values(element.attrs).join(' ').toLowerCase(); + const text = normalized(element.text); + if (attrs.includes('tooltip') || attrs.includes('attacher')) return false; + if (isNestedCompositeControl(element)) return false; + if (element.tag === 'iframe') return true; + if (text === '') { + if (!isInteractiveElement(element)) return false; + if (isSemanticFormControl(element)) return true; + if (!element.variantHints.includes('icon-only') && !element.variantHints.includes('has-icon')) return false; + } + return true; +} + +function isNestedCompositeControl(element: WebElement): boolean { + const role = (element.role || element.attrs.role || element.tag).toLowerCase(); + if (COMPOSITE_TARGET_ROLES.has(role)) return false; + if (!isInteractiveElement(element)) return false; + return element.areaHints.some((hint) => COMPOSITE_AREA_HINTS.has(hint)); +} + +function isSemanticFormControl(element: WebElement): boolean { + const role = (element.role || element.attrs.role || element.tag).toLowerCase(); + if (element.tag === 'input' || element.tag === 'select' || element.tag === 'textarea') return true; + return FORM_CONTROL_ROLES.has(role); +} + +function isButtonLikeElement(element: WebElement): boolean { + if (!isInteractiveElement(element)) return false; + const role = (element.role || element.attrs.role || element.tag).toLowerCase(); + if (role === 'link' || element.tag === 'a') return false; + return true; +} + +function isInteractiveElement(element: WebElement): boolean { + if (element.tag === 'button') return true; + if (element.tag === 'a' && element.attrs.href) return true; + if (element.tag === 'iframe') return true; + if (element.tag === 'input' || element.tag === 'select' || element.tag === 'textarea') return true; + const role = (element.role || element.attrs.role || element.tag).toLowerCase(); + if (INTERACTIVE_ROLES.has(role)) return true; + if (element.attrs.contenteditable === 'true') return true; + if (element.attrs.tabindex && Number(element.attrs.tabindex) >= 0) return true; + if (element.attrs['aria-haspopup'] || element.attrs['aria-expanded'] || element.attrs['aria-controls']) return true; + return false; +} + +const INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); +const FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']); +const COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']); +const COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']); + +const drillLocatorRule = locatorRule.replace(/[\s\S]*?<\/context_simplification>/, '').trim(); diff --git a/src/commands/drill-command.ts b/src/commands/drill-command.ts index c547eaa..c9c4b12 100644 --- a/src/commands/drill-command.ts +++ b/src/commands/drill-command.ts @@ -3,7 +3,7 @@ import { BaseCommand } from './base-command.js'; export class DrillCommand extends BaseCommand { name = 'drill'; description = 'Drill all components on current page to learn interactions'; - aliases = ['bosun']; + aliases = ['driller']; suggestions = ['/research - to see UI map first', '/navigate - to go to another page']; async execute(args: string): Promise { @@ -15,7 +15,7 @@ export class DrillCommand extends BaseCommand { throw new Error('No active page to drill'); } - await this.explorBot.agentBosun().drill({ + await this.explorBot.agentDriller().drill({ knowledgePath, maxComponents, interactive: true, @@ -28,7 +28,7 @@ export class DrillCommand extends BaseCommand { } private parseMaxArg(args: string): number | undefined { - const match = args.match(/--max\s+(\d+)/); + const match = args.match(/--max-components\s+(\d+)/); return match ? Number.parseInt(match[1], 10) : undefined; } } diff --git a/src/components/AddRule.tsx b/src/components/AddRule.tsx index 8753936..bc8c5f7 100644 --- a/src/components/AddRule.tsx +++ b/src/components/AddRule.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'; import { AddRuleCommand } from '../commands/add-rule-command.js'; import InputReadline from './InputReadline.js'; -const KNOWN_AGENTS = ['researcher', 'tester', 'planner', 'pilot', 'captain', 'bosun', 'navigator']; +const KNOWN_AGENTS = ['researcher', 'tester', 'planner', 'pilot', 'captain', 'driller', 'navigator']; interface AddRuleProps { initialAgent?: string; diff --git a/src/config.ts b/src/config.ts index 28b5050..c499440 100644 --- a/src/config.ts +++ b/src/config.ts @@ -112,6 +112,7 @@ interface AgentsConfig { researcher?: ResearcherAgentConfig; planner?: PlannerAgentConfig; pilot?: PilotAgentConfig; + driller?: AgentConfig; 'experience-compactor'?: AgentConfig; captain?: AgentConfig; quartermaster?: AgentConfig; diff --git a/src/explorbot.ts b/src/explorbot.ts index ae45e21..5c6fffd 100644 --- a/src/explorbot.ts +++ b/src/explorbot.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { ApiClient } from './api/api-client.ts'; import { RequestStore } from './api/request-store.ts'; import { loadSpec } from './api/spec-reader.ts'; -import { Bosun } from './ai/bosun.ts'; +import { Driller } from './ai/driller.ts'; import { Captain } from './ai/captain.ts'; import { ExperienceCompactor } from './ai/experience-compactor.ts'; import { Fisherman } from './ai/fisherman.ts'; @@ -261,12 +261,10 @@ export class ExplorBot { return this.agents.rerunner; } - agentBosun(): Bosun { - return (this.agents.bosun ||= this.createAgent(({ ai, explorer }) => { - const researcher = this.agentResearcher(); + agentDriller(): Driller { + return (this.agents.driller ||= this.createAgent(({ ai, explorer }) => { const navigator = this.agentNavigator(); - const tools = createAgentTools({ explorer, researcher, navigator }); - return new Bosun(explorer, ai, researcher, navigator, tools); + return new Driller(explorer, ai, navigator); })); } diff --git a/src/explorer.ts b/src/explorer.ts index 55afe38..dec146a 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -755,6 +755,32 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s } } + try { + const rawList = await page.locator('iframe').evaluateAll((domElements: Element[], extractFnStr: string) => { + const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; + const results: any[] = []; + const sourceCounts: Record = {}; + let iframeIdx = 0; + for (const el of domElements) { + iframeIdx++; + const sourceKey = el.getAttribute('src') || ''; + sourceCounts[sourceKey] ||= 0; + sourceCounts[sourceKey]++; + const existing = el.getAttribute('data-explorbot-eidx'); + el.setAttribute('data-explorbot-eidx', existing || `iframe-${iframeIdx}`); + el.setAttribute('data-explorbot-frame-source-index', String(sourceCounts[sourceKey])); + const elData = extract(el); + if (elData) results.push(elData); + } + return results; + }, extractElementData.toString()); + for (const raw of rawList) { + elements.push(WebElement.fromRawData(raw, 'iframe')); + } + } catch { + debugLog('Failed to annotate iframes'); + } + return { ariaSnapshot, elements }; } diff --git a/src/state-manager.ts b/src/state-manager.ts index 3668cb2..0fbb4a2 100644 --- a/src/state-manager.ts +++ b/src/state-manager.ts @@ -142,8 +142,8 @@ export class StateManager { /** * Extract state path from full URL - * Removes domain, port, protocol, and query params - * Keeps path and hash: /path/to/page#section + * Removes domain, port, protocol + * Keeps path, query, and hash: /path/to/page?tab=users#section */ /** * Update current state from ActionResult and record transition if state changed @@ -549,7 +549,8 @@ export class StateManager { export function normalizeUrl(url: string): string { try { const parsed = new URL(url, 'http://localhost'); - return parsed.pathname.replace(/^\/+|\/+$/g, ''); + const path = parsed.pathname.replace(/^\/+|\/+$/g, ''); + return `${path}${parsed.search}${parsed.hash}`; } catch { return url.replace(/^\/+|\/+$/g, ''); } diff --git a/src/utils/hooks-runner.ts b/src/utils/hooks-runner.ts index 568fe29..31d84b0 100644 --- a/src/utils/hooks-runner.ts +++ b/src/utils/hooks-runner.ts @@ -1,6 +1,7 @@ import type { ExplorbotConfig, Hook, HookConfig } from '../config.ts'; import type Explorer from '../explorer.ts'; import { createDebug } from './logger.ts'; +import { extractStatePath } from './url-matcher.ts'; import { matchesUrl } from './url-matcher.ts'; const debugLog = createDebug('explorbot:hooks'); @@ -69,11 +70,6 @@ export class HooksRunner { } private extractPath(url: string): string { - if (url.startsWith('/')) return url; - try { - return new URL(url).pathname; - } catch { - return url; - } + return extractStatePath(url); } } diff --git a/src/utils/url-matcher.ts b/src/utils/url-matcher.ts index 68e144c..572d968 100644 --- a/src/utils/url-matcher.ts +++ b/src/utils/url-matcher.ts @@ -38,7 +38,7 @@ export function extractStatePath(url: string): string { if (url.startsWith('/')) return url; try { const urlObj = new URL(url); - return urlObj.pathname + urlObj.hash; + return `${urlObj.pathname}${urlObj.search}${urlObj.hash}`; } catch { return url; } diff --git a/src/utils/web-element.ts b/src/utils/web-element.ts index 556d814..55d149b 100644 --- a/src/utils/web-element.ts +++ b/src/utils/web-element.ts @@ -57,6 +57,26 @@ export class WebElement { return cls.split(/\s+/).filter((c) => c.length > 2 && !isDynamicId(c) && !isGenericClass(c)); } + get areaHints(): string[] { + const raw = this.attrs['data-explorbot-area'] || ''; + return raw + .split('|') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); + } + + get contextLabel(): string { + return (this.attrs['data-explorbot-context'] || '').trim(); + } + + get variantHints(): string[] { + const raw = this.attrs['data-explorbot-variant'] || ''; + return raw + .split('|') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); + } + static fromRawData(d: RawElementData, role?: string): WebElement { return new WebElement({ tag: d.tag, @@ -65,6 +85,7 @@ export class WebElement { clickXPath: buildClickableXPath({ tag: d.tag, allAttrs: d.allAttrs, text: d.text } as XPathMatch), attrs: d.allAttrs, text: d.text, + outerHTML: d.outerHTML, x: d.x, y: d.y, }); @@ -128,8 +149,143 @@ export class WebElement { } export function extractElementData(el: Element) { + function normalizeText(value: string): string { + return value.replace(/\s+/g, ' ').trim(); + } + + function readText(node: Element | null): string { + if (!node) return ''; + return normalizeText(node.textContent || '').slice(0, 120); + } + + function getLabelLikeText(node: Element | null): string { + if (!node) return ''; + const direct = readText(node); + if (direct) return direct; + const labelLike = node.querySelector('h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]'); + return readText(labelLike); + } + + function collectVariantHints(target: Element): string[] { + const tokens = new Set(); + const className = target.getAttribute('class') || ''; + const tagName = target.tagName.toLowerCase(); + + for (const cls of className.split(/\s+/).filter(Boolean)) { + const lower = cls.toLowerCase(); + if (/^(xs|sm|md|lg|xl|xxl)$/.test(lower)) tokens.add(lower); + if (/^(mini|small|medium|large|xlarge|xl|compact|dense)$/.test(lower)) tokens.add(lower); + if (/(^|[-_])(xs|sm|md|lg|xl|xxl|mini|small|medium|large|compact|dense)([-_]|$)/.test(lower)) tokens.add(lower); + if (/(selected|disabled|primary|secondary|tertiary|danger|success|warning|outline|ghost|icon|dropdown)/.test(lower)) tokens.add(lower); + } + + const type = (target.getAttribute('type') || '').toLowerCase(); + if (type) tokens.add(type); + if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true') tokens.add('disabled'); + if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true') tokens.add('selected'); + if (tagName === 'iframe') tokens.add('iframe'); + if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target)) tokens.add('code-editor'); + + const svgCount = target.querySelectorAll('svg').length; + if (svgCount > 0) tokens.add('has-icon'); + if (svgCount > 1) tokens.add('double-icon'); + + const normalizedText = normalizeText(target.textContent || ''); + if (!normalizedText && svgCount > 0) tokens.add('icon-only'); + if (normalizedText && svgCount > 0) { + const first = target.firstElementChild?.tagName.toLowerCase(); + const last = target.lastElementChild?.tagName.toLowerCase(); + if (first === 'svg') tokens.add('leading-icon'); + if (last === 'svg') tokens.add('trailing-icon'); + } + + if (tagName === 'a' && target.getAttribute('href')) tokens.add('navigates'); + + return Array.from(tokens).slice(0, 8); + } + + function isEmbeddedCodeEditorFrame(target: Element): boolean { + const src = (target.getAttribute('src') || '').toLowerCase(); + const parentClasses = (target.parentElement?.getAttribute('class') || '').toLowerCase(); + const ancestorClasses = (target.closest('[class*="monaco"], [class*="codemirror"], [class*="ace_editor"], [class*="code"]')?.getAttribute('class') || '').toLowerCase(); + return src.includes('monaco') || src.includes('codemirror') || src.includes('ace') || parentClasses.includes('frame-container') || ancestorClasses.includes('monaco') || ancestorClasses.includes('codemirror') || ancestorClasses.includes('ace_editor'); + } + + function findContextLabel(target: Element): string { + const labelTags = 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]'; + const labelledby = target.getAttribute('aria-labelledby'); + const candidates: string[] = []; + if (labelledby) { + for (const id of labelledby.split(/\s+/).filter(Boolean)) { + const ref = document.getElementById(id); + const text = readText(ref); + if (text) candidates.push(text); + } + } + + const freestyleUsage = target.closest('[class*="FreestyleUsage"]'); + if (freestyleUsage) { + const title = freestyleUsage.querySelector('[class*="FreestyleUsage-title"]'); + const titleText = readText(title); + if (titleText) candidates.push(titleText); + } + + const semanticContainer = target.closest('section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]'); + if (semanticContainer) { + const ownHeading = semanticContainer.querySelector(labelTags); + const ownHeadingText = readText(ownHeading); + if (ownHeadingText) candidates.push(ownHeadingText); + + let previous: Element | null = semanticContainer.previousElementSibling; + let hops = 0; + while (previous && hops < 3) { + const previousText = getLabelLikeText(previous); + if (previousText) { + candidates.push(previousText); + break; + } + previous = previous.previousElementSibling; + hops++; + } + } + + let parent: Element | null = target.parentElement; + let depth = 0; + while (parent && depth < 4) { + let sibling: Element | null = parent.previousElementSibling; + let hops = 0; + while (sibling && hops < 2) { + const siblingText = getLabelLikeText(sibling); + if (siblingText) { + candidates.push(siblingText); + sibling = null; + break; + } + sibling = sibling.previousElementSibling; + hops++; + } + parent = parent.parentElement; + depth++; + } + + const ownText = normalizeText(target.textContent || ''); + for (const candidate of candidates) { + if (!candidate) continue; + if (candidate === ownText) continue; + if (candidate.toLowerCase().includes('title should not be empty')) continue; + return candidate.slice(0, 120); + } + + return ''; + } + const rect = el.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return null; + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden') return null; + if (Number.parseFloat(style.opacity || '1') < 0.1) return null; + if (el.getAttribute('aria-hidden') === 'true' || el.hasAttribute('hidden')) return null; + if ((el as HTMLElement).offsetParent === null && style.position !== 'fixed') return null; const allAttrs: Record = {}; for (let i = 0; i < el.attributes.length; i++) { @@ -137,10 +293,39 @@ export function extractElementData(el: Element) { allAttrs[attr.name] = attr.value; } + const areaHints: string[] = []; + let current: Element | null = el; + let depth = 0; + while (current && depth < 5) { + const tag = current.tagName.toLowerCase(); + areaHints.push(tag); + + const role = current.getAttribute('role'); + if (role) areaHints.push(`role:${role.toLowerCase()}`); + + const id = current.getAttribute('id'); + if (id) areaHints.push(`id:${id.toLowerCase()}`); + + const className = current.getAttribute('class'); + if (className) { + for (const cls of className.split(/\s+/).filter(Boolean)) { + areaHints.push(`class:${cls.toLowerCase()}`); + } + } + + current = current.parentElement; + depth++; + } + + allAttrs['data-explorbot-area'] = areaHints.join('|'); + allAttrs['data-explorbot-context'] = findContextLabel(el); + allAttrs['data-explorbot-variant'] = collectVariantHints(el).join('|'); + return { tag: el.tagName.toLowerCase(), - text: (el.textContent || '').trim().slice(0, 80), + text: normalizeText(el.textContent || '').slice(0, 80), allAttrs, + outerHTML: el.outerHTML.slice(0, 2000), x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2), }; diff --git a/src/utils/xpath.ts b/src/utils/xpath.ts index d32814e..45be3e3 100644 --- a/src/utils/xpath.ts +++ b/src/utils/xpath.ts @@ -48,7 +48,7 @@ function getAbsoluteXPath(el: Element): string { } export const isDynamicId = (id: string) => /^(ember|react|__next)\d|^\d+$/.test(id); -export const isGenericClass = (cls: string) => /^ember-view$|^ember\d|^react-|^__next/.test(cls); +export const isGenericClass = (cls: string) => /^ember-view$|^ember\d|^ember-|^react-|^__next/.test(cls); export function buildClickableXPath(el: XPathMatch): string { const a = el.allAttrs; diff --git a/tests/unit/annotate-elements.test.ts b/tests/unit/annotate-elements.test.ts index beedd03..1338461 100644 --- a/tests/unit/annotate-elements.test.ts +++ b/tests/unit/annotate-elements.test.ts @@ -140,4 +140,47 @@ describe('annotatePageElements', () => { expect(result.ariaSnapshot).toBe(ariaSnapshot); expect(result.elements).toHaveLength(0); }); + + describe('component metadata', () => { + let page: Page; + let elements: WebElement[]; + + beforeAll(async () => { + page = await browser.newPage(); + await page.setContent(` +
+
+

Toggle - off

+ +
+
+

Code Input

+ +
+
+ `); + const result = await annotatePageElements(page); + elements = result.elements; + }); + + afterAll(async () => { + await page?.close(); + }); + + it('adds context and variant hints for drillable controls', () => { + const toggle = elements.find((el) => el.role === 'switch'); + expect(toggle?.contextLabel).toBe('Toggle - off'); + expect(toggle?.areaHints).toContain('role:switch'); + expect(toggle?.areaHints).toContain('main'); + expect(toggle?.outerHTML).toContain('aria-checked="false"'); + }); + + it('annotates code editor iframes for driller discovery', () => { + const frame = elements.find((el) => el.role === 'iframe'); + expect(frame?.contextLabel).toBe('Code Input'); + expect(frame?.variantHints).toContain('iframe'); + expect(frame?.variantHints).toContain('code-editor'); + expect(frame?.attrs['data-explorbot-frame-source-index']).toBe('1'); + }); + }); }); diff --git a/tests/unit/web-element.test.ts b/tests/unit/web-element.test.ts new file mode 100644 index 0000000..f91de68 --- /dev/null +++ b/tests/unit/web-element.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'bun:test'; +import { JSDOM } from 'jsdom'; +import { extractElementData } from '../../src/utils/web-element.ts'; + +describe('extractElementData', () => { + it('adds context, area, and variant hints for component drilling', () => { + const dom = new JSDOM(` +
+
+

Toggle - off

+ +
+
+ `); + useDom(dom); + const button = dom.window.document.querySelector('button')!; + mockVisibleBox(button); + + const data = extractElementData(button); + + expect(data?.allAttrs['data-explorbot-context']).toBe('Toggle - off'); + expect(data?.allAttrs['data-explorbot-area']).toContain('main'); + expect(data?.allAttrs['data-explorbot-area']).toContain('role:switch'); + expect(data?.allAttrs['data-explorbot-variant']).toContain('primary-btn'); + expect(data?.allAttrs['data-explorbot-variant']).toContain('btn-md'); + expect(data?.outerHTML).toContain('aria-checked="false"'); + }); + + it('marks embedded code editor iframes', () => { + const dom = new JSDOM(` +
+
+

Code Input

+
+ +
+
+
+ `); + useDom(dom); + const frame = dom.window.document.querySelector('iframe')!; + mockVisibleBox(frame); + + const data = extractElementData(frame); + + expect(data?.allAttrs['data-explorbot-context']).toBe('Code Input'); + expect(data?.allAttrs['data-explorbot-variant']).toContain('iframe'); + expect(data?.allAttrs['data-explorbot-variant']).toContain('code-editor'); + expect(data?.allAttrs['data-explorbot-frame-source-index']).toBe('1'); + }); +}); + +function useDom(dom: JSDOM) { + (globalThis as any).window = dom.window; + (globalThis as any).document = dom.window.document; +} + +function mockVisibleBox(element: Element) { + element.getBoundingClientRect = () => ({ + x: 10, + y: 20, + width: 100, + height: 30, + top: 20, + left: 10, + right: 110, + bottom: 50, + toJSON: () => ({}), + }); + (element as HTMLElement).style.position = 'fixed'; +} From b9d24d0ec94b7679d037e75ac2338b6f9557cf7b Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Tue, 14 Apr 2026 13:56:55 +0300 Subject: [PATCH 02/11] fix reporter result --- src/ai/tester.ts | 11 +++++++++-- tests/unit/reporter.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ai/tester.ts b/src/ai/tester.ts index 7214181..6398b8d 100644 --- a/src/ai/tester.ts +++ b/src/ai/tester.ts @@ -549,8 +549,15 @@ export class Tester extends TaskAgent implements Agent { } private finishTest(task: Test): void { - if (!task.hasFinished) { - task.finish(TestResult.FAILED); + if (!task.result) { + const checkedNotes = task.getCheckedNotes(); + const hasPassedNotes = checkedNotes.some((note) => note.status === TestResult.PASSED); + const hasFailedNotes = checkedNotes.some((note) => note.status === TestResult.FAILED); + if ((task.hasAchievedAny() || hasPassedNotes) && !hasFailedNotes) { + task.finish(TestResult.PASSED); + } else { + task.finish(TestResult.FAILED); + } } tag('info').log(`Finished: ${task.scenario}`); diff --git a/tests/unit/reporter.test.ts b/tests/unit/reporter.test.ts index bc5ad5b..8218b59 100644 --- a/tests/unit/reporter.test.ts +++ b/tests/unit/reporter.test.ts @@ -1,3 +1,5 @@ +import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import type { ReporterConfig } from '../../src/config.ts'; import { ConfigParser } from '../../src/config.ts'; @@ -318,4 +320,24 @@ describe('Reporter config', () => { expect(process.env.TESTOMATIO_HTML_REPORT_SAVE).toBe('1'); expect(process.env.TESTOMATIO_HTML_REPORT_FOLDER).toContain('reports'); }); + + test('writes finished Explorbot test into HTML report', async () => { + const outputDir = ConfigParser.getInstance().getOutputDir(); + rmSync(join(outputDir, 'reports'), { recursive: true, force: true }); + + const reporter = new Reporter({ enabled: true, html: true }); + const test = new Test('Verify sign in page is visible', 'normal', ['Sign In is visible'], 'https://example.com/users/sign_in'); + test.start(); + test.addNote('Sign In is visible', TestResult.PASSED); + test.addStep('I.see("Sign In", "h2")', 10, 'passed'); + test.finish(TestResult.PASSED); + + await reporter.reportTestStart(test); + await reporter.reportTest(test); + await reporter.finishRun(); + + const reportFile = join(outputDir, 'reports', 'testomatio-report.html'); + expect(existsSync(reportFile)).toBe(true); + expect(readFileSync(reportFile, 'utf8')).toContain('Verify sign in page is visible'); + }); }); From 51a257190b25506145f409aed9bde806f832c1fc Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Tue, 14 Apr 2026 16:32:41 +0300 Subject: [PATCH 03/11] fix format --- bin/explorbot-cli.ts | 7 +-- src/ai/driller.ts | 113 +++++++++++++++++++++---------------------- 2 files changed, 56 insertions(+), 64 deletions(-) diff --git a/bin/explorbot-cli.ts b/bin/explorbot-cli.ts index 8d9984c..952f065 100755 --- a/bin/explorbot-cli.ts +++ b/bin/explorbot-cli.ts @@ -485,12 +485,7 @@ addCommonOptions(program.command('research ').description('Research a page ); addCommonOptions( - program - .command('drill ') - .alias('driller') - .description('Drill all components on a page to learn interactions') - .option('--knowledge ', 'Save learned interactions to knowledge file at this URL path') - .option('--max-components ', 'Maximum number of components to drill') + program.command('drill ').alias('driller').description('Drill all components on a page to learn interactions').option('--knowledge ', 'Save learned interactions to knowledge file at this URL path').option('--max-components ', 'Maximum number of components to drill') ).action(async (url, options) => { try { const explorBot = new ExplorBot(buildExplorBotOptions(url, options)); diff --git a/src/ai/driller.ts b/src/ai/driller.ts index 121c7ca..4a3c9d0 100644 --- a/src/ai/driller.ts +++ b/src/ai/driller.ts @@ -219,9 +219,7 @@ export class Driller extends TaskAgent implements Agent { const primaryOther = primary.filter((element) => !isButtonLikeElement(element)); const fallbackButtonLike = fallback.filter((element) => isButtonLikeElement(element)); const fallbackOther = fallback.filter((element) => !isButtonLikeElement(element)); - const prioritized = primaryButtonLike.length >= maxComponents - ? primaryButtonLike - : [...primaryButtonLike, ...fallbackButtonLike, ...primaryOther, ...fallbackOther]; + const prioritized = primaryButtonLike.length >= maxComponents ? primaryButtonLike : [...primaryButtonLike, ...fallbackButtonLike, ...primaryOther, ...fallbackOther]; const components: ComponentInfo[] = []; const seen = new Set(); @@ -308,49 +306,52 @@ export class Driller extends TaskAgent implements Agent { const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.explorer, test), component); const tools = { ...actionTools, ...this.createDrillFlowTools(originalState, test, interactive) }; - await loop(async ({ stop, iteration }) => { - debugLog(`Drilling component ${component.name}, iteration ${iteration}`); - setActivity(`${this.emoji} Drilling ${component.name}...`, 'action'); + await loop( + async ({ stop, iteration }) => { + debugLog(`Drilling component ${component.name}, iteration ${iteration}`); + setActivity(`${this.emoji} Drilling ${component.name}...`, 'action'); - if (iteration > 1) { - const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState() || originalState); - conversation.addUserText(await this.buildContextUpdate(currentState, component)); - if (this.pendingNestedContext) { - conversation.addUserText(this.pendingNestedContext); - this.pendingNestedContext = null; + if (iteration > 1) { + const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState() || originalState); + conversation.addUserText(await this.buildContextUpdate(currentState, component)); + if (this.pendingNestedContext) { + conversation.addUserText(this.pendingNestedContext); + this.pendingNestedContext = null; + } } - } - const result = await this.provider.invokeConversation(conversation, tools, { - maxToolRoundtrips: 5, - toolChoice: 'required', - agentName: 'driller', - }); + const result = await this.provider.invokeConversation(conversation, tools, { + maxToolRoundtrips: 5, + toolChoice: 'required', + agentName: 'driller', + }); - if (!result) throw new Error('Failed to get response from provider'); + if (!result) throw new Error('Failed to get response from provider'); - const toolExecutions = result.toolExecutions || []; - this.trackToolExecutions(toolExecutions); - const failedActionCount = toolExecutions.filter((execution: any) => this.ACTION_TOOLS.includes(execution.toolName) && !execution.wasSuccessful).length; - if (failedActionCount >= 4) stop(); + const toolExecutions = result.toolExecutions || []; + this.trackToolExecutions(toolExecutions); + const failedActionCount = toolExecutions.filter((execution: any) => this.ACTION_TOOLS.includes(execution.toolName) && !execution.wasSuccessful).length; + if (failedActionCount >= 4) stop(); - const hasDone = toolExecutions.some((execution: any) => execution.toolName === 'drill_done' && execution.wasSuccessful); - const hasSkip = toolExecutions.some((execution: any) => execution.toolName === 'drill_skip' && execution.wasSuccessful); - if (hasDone || hasSkip) { - finished = true; - stop(); - } + const hasDone = toolExecutions.some((execution: any) => execution.toolName === 'drill_done' && execution.wasSuccessful); + const hasSkip = toolExecutions.some((execution: any) => execution.toolName === 'drill_skip' && execution.wasSuccessful); + if (hasDone || hasSkip) { + finished = true; + stop(); + } - if (iteration >= this.MAX_COMPONENT_ITERATIONS) stop(); - }, { - maxAttempts: this.MAX_COMPONENT_ITERATIONS, - interruptPrompt: `Drill interrupted while testing "${component.name}". Enter instruction (or "stop" to end):`, - observability: { agent: 'driller', sessionId: `${test.id}_${component.eidx}` }, - catch: async ({ error, stop }) => { - tag('error').log(`Drill error for ${component.name}: ${error}`); - stop(); + if (iteration >= this.MAX_COMPONENT_ITERATIONS) stop(); }, - }); + { + maxAttempts: this.MAX_COMPONENT_ITERATIONS, + interruptPrompt: `Drill interrupted while testing "${component.name}". Enter instruction (or "stop" to end):`, + observability: { agent: 'driller', sessionId: `${test.id}_${component.eidx}` }, + catch: async ({ error, stop }) => { + tag('error').log(`Drill error for ${component.name}: ${error}`); + stop(); + }, + } + ); if (finished || test.hasFinished) return; if ((test.interactions || []).some((interaction) => interaction.result === 'success')) { @@ -896,7 +897,10 @@ function buildScopedFreestyleClickCode(component: ComponentInfo): string { const placeholder = component.placeholder; if (placeholder) return `I.click(${JSON.stringify(`${scope}//input[@placeholder=${xpathLiteral(placeholder)}]`)})`; if (component.classes.length > 0) { - const classConditions = component.classes.slice(0, 4).map((cls) => `contains(@class,${xpathLiteral(cls)})`).join(' and '); + const classConditions = component.classes + .slice(0, 4) + .map((cls) => `contains(@class,${xpathLiteral(cls)})`) + .join(' and '); return `I.click(${JSON.stringify(`${scope}//input[${classConditions}]`)})`; } } @@ -925,12 +929,7 @@ function buildEmbeddedFrameCode(component: ComponentInfo): string { text = 'const value = "test";'; } - return [ - `I.switchTo(${JSON.stringify(iframeLocator)})`, - `I.click(${JSON.stringify(editorLocator)})`, - `I.type(${JSON.stringify(text)})`, - 'I.switchTo()', - ].join('\n'); + return [`I.switchTo(${JSON.stringify(iframeLocator)})`, `I.click(${JSON.stringify(editorLocator)})`, `I.type(${JSON.stringify(text)})`, 'I.switchTo()'].join('\n'); } function buildTabVariantXPathCondition(component: ComponentInfo): string { @@ -979,17 +978,7 @@ function normalizeInteractionResult(component: ComponentInfo, action: string, re if (!value) return fallbackInteractionResult(component, action); const normalizedValue = value.toLowerCase(); - const weakPhrases = [ - 'button clicked', - 'clicked button', - 'button was clicked', - 'component clicked', - 'page remains same', - 'page stayed the same', - 'no visible change', - 'action performed', - 'clicked', - ]; + const weakPhrases = ['button clicked', 'clicked button', 'button was clicked', 'component clicked', 'page remains same', 'page stayed the same', 'no visible change', 'action performed', 'clicked']; if (weakPhrases.some((phrase) => normalizedValue === phrase || normalizedValue.includes(phrase))) { return fallbackInteractionResult(component, action); @@ -1010,7 +999,10 @@ function fallbackInteractionResult(component: ComponentInfo, action: string): st } function hasContainerLocator(code: string): boolean { - for (const line of code.split('\n').map((entry) => entry.trim()).filter(Boolean)) { + for (const line of code + .split('\n') + .map((entry) => entry.trim()) + .filter(Boolean)) { const argCount = countTopLevelArgs(line); if (line.startsWith('I.click(') && argCount >= 2) return true; if (line.startsWith('I.fillField(') && argCount >= 3) return true; @@ -1069,7 +1061,12 @@ function buildClassSelector(tag: string, classes: string[]): string { } function parseVariantHints(variant: string): Set { - return new Set(variant.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean)); + return new Set( + variant + .split(',') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean) + ); } function xpathLiteral(value: string): string { From 22839daecc6205033fb2ede34284ec322cc1e05c Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Tue, 14 Apr 2026 16:36:42 +0300 Subject: [PATCH 04/11] rename exp --- {experience => exp_custom}/Button_AI.md | 0 {experience => exp_custom}/Button_Dropdown.md | 0 {experience => exp_custom}/Button_Extra.md | 0 {experience => exp_custom}/Button_Green.md | 0 {experience => exp_custom}/Button_Merge.md | 0 {experience => exp_custom}/Button_Primary.md | 0 {experience => exp_custom}/Button_Red.md | 0 {experience => exp_custom}/Button_Secondary.md | 0 {experience => exp_custom}/Button_Third.md | 0 {experience => exp_custom}/Code_Input.md | 0 {experience => exp_custom}/Form_Elements.md | 0 {experience => exp_custom}/General_Inputs.md | 0 {experience => exp_custom}/Input_Empty_Handler.md | 0 {experience => exp_custom}/Input_With_Tags.md | 0 {experience => exp_custom}/Legacy.md | 0 {experience => exp_custom}/Link.md | 0 {experience => exp_custom}/New_Counter.md | 0 {experience => exp_custom}/Other_Buttons.md | 0 {experience => exp_custom}/PowerSelect.md | 0 {experience => exp_custom}/PowerSelect_Filters.md | 0 {experience => exp_custom}/PowerSelect_Input.md | 0 {experience => exp_custom}/PowerSelect_Multiple.md | 0 {experience => exp_custom}/PowerSelect_Typeahead.md | 0 {experience => exp_custom}/Search_Input.md | 0 {experience => exp_custom}/Tabs.md | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename {experience => exp_custom}/Button_AI.md (100%) rename {experience => exp_custom}/Button_Dropdown.md (100%) rename {experience => exp_custom}/Button_Extra.md (100%) rename {experience => exp_custom}/Button_Green.md (100%) rename {experience => exp_custom}/Button_Merge.md (100%) rename {experience => exp_custom}/Button_Primary.md (100%) rename {experience => exp_custom}/Button_Red.md (100%) rename {experience => exp_custom}/Button_Secondary.md (100%) rename {experience => exp_custom}/Button_Third.md (100%) rename {experience => exp_custom}/Code_Input.md (100%) rename {experience => exp_custom}/Form_Elements.md (100%) rename {experience => exp_custom}/General_Inputs.md (100%) rename {experience => exp_custom}/Input_Empty_Handler.md (100%) rename {experience => exp_custom}/Input_With_Tags.md (100%) rename {experience => exp_custom}/Legacy.md (100%) rename {experience => exp_custom}/Link.md (100%) rename {experience => exp_custom}/New_Counter.md (100%) rename {experience => exp_custom}/Other_Buttons.md (100%) rename {experience => exp_custom}/PowerSelect.md (100%) rename {experience => exp_custom}/PowerSelect_Filters.md (100%) rename {experience => exp_custom}/PowerSelect_Input.md (100%) rename {experience => exp_custom}/PowerSelect_Multiple.md (100%) rename {experience => exp_custom}/PowerSelect_Typeahead.md (100%) rename {experience => exp_custom}/Search_Input.md (100%) rename {experience => exp_custom}/Tabs.md (100%) diff --git a/experience/Button_AI.md b/exp_custom/Button_AI.md similarity index 100% rename from experience/Button_AI.md rename to exp_custom/Button_AI.md diff --git a/experience/Button_Dropdown.md b/exp_custom/Button_Dropdown.md similarity index 100% rename from experience/Button_Dropdown.md rename to exp_custom/Button_Dropdown.md diff --git a/experience/Button_Extra.md b/exp_custom/Button_Extra.md similarity index 100% rename from experience/Button_Extra.md rename to exp_custom/Button_Extra.md diff --git a/experience/Button_Green.md b/exp_custom/Button_Green.md similarity index 100% rename from experience/Button_Green.md rename to exp_custom/Button_Green.md diff --git a/experience/Button_Merge.md b/exp_custom/Button_Merge.md similarity index 100% rename from experience/Button_Merge.md rename to exp_custom/Button_Merge.md diff --git a/experience/Button_Primary.md b/exp_custom/Button_Primary.md similarity index 100% rename from experience/Button_Primary.md rename to exp_custom/Button_Primary.md diff --git a/experience/Button_Red.md b/exp_custom/Button_Red.md similarity index 100% rename from experience/Button_Red.md rename to exp_custom/Button_Red.md diff --git a/experience/Button_Secondary.md b/exp_custom/Button_Secondary.md similarity index 100% rename from experience/Button_Secondary.md rename to exp_custom/Button_Secondary.md diff --git a/experience/Button_Third.md b/exp_custom/Button_Third.md similarity index 100% rename from experience/Button_Third.md rename to exp_custom/Button_Third.md diff --git a/experience/Code_Input.md b/exp_custom/Code_Input.md similarity index 100% rename from experience/Code_Input.md rename to exp_custom/Code_Input.md diff --git a/experience/Form_Elements.md b/exp_custom/Form_Elements.md similarity index 100% rename from experience/Form_Elements.md rename to exp_custom/Form_Elements.md diff --git a/experience/General_Inputs.md b/exp_custom/General_Inputs.md similarity index 100% rename from experience/General_Inputs.md rename to exp_custom/General_Inputs.md diff --git a/experience/Input_Empty_Handler.md b/exp_custom/Input_Empty_Handler.md similarity index 100% rename from experience/Input_Empty_Handler.md rename to exp_custom/Input_Empty_Handler.md diff --git a/experience/Input_With_Tags.md b/exp_custom/Input_With_Tags.md similarity index 100% rename from experience/Input_With_Tags.md rename to exp_custom/Input_With_Tags.md diff --git a/experience/Legacy.md b/exp_custom/Legacy.md similarity index 100% rename from experience/Legacy.md rename to exp_custom/Legacy.md diff --git a/experience/Link.md b/exp_custom/Link.md similarity index 100% rename from experience/Link.md rename to exp_custom/Link.md diff --git a/experience/New_Counter.md b/exp_custom/New_Counter.md similarity index 100% rename from experience/New_Counter.md rename to exp_custom/New_Counter.md diff --git a/experience/Other_Buttons.md b/exp_custom/Other_Buttons.md similarity index 100% rename from experience/Other_Buttons.md rename to exp_custom/Other_Buttons.md diff --git a/experience/PowerSelect.md b/exp_custom/PowerSelect.md similarity index 100% rename from experience/PowerSelect.md rename to exp_custom/PowerSelect.md diff --git a/experience/PowerSelect_Filters.md b/exp_custom/PowerSelect_Filters.md similarity index 100% rename from experience/PowerSelect_Filters.md rename to exp_custom/PowerSelect_Filters.md diff --git a/experience/PowerSelect_Input.md b/exp_custom/PowerSelect_Input.md similarity index 100% rename from experience/PowerSelect_Input.md rename to exp_custom/PowerSelect_Input.md diff --git a/experience/PowerSelect_Multiple.md b/exp_custom/PowerSelect_Multiple.md similarity index 100% rename from experience/PowerSelect_Multiple.md rename to exp_custom/PowerSelect_Multiple.md diff --git a/experience/PowerSelect_Typeahead.md b/exp_custom/PowerSelect_Typeahead.md similarity index 100% rename from experience/PowerSelect_Typeahead.md rename to exp_custom/PowerSelect_Typeahead.md diff --git a/experience/Search_Input.md b/exp_custom/Search_Input.md similarity index 100% rename from experience/Search_Input.md rename to exp_custom/Search_Input.md diff --git a/experience/Tabs.md b/exp_custom/Tabs.md similarity index 100% rename from experience/Tabs.md rename to exp_custom/Tabs.md From e54a7a7dad0b95852808ef1c8d20b9674ccfe23a Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Tue, 14 Apr 2026 16:47:27 +0300 Subject: [PATCH 05/11] fix unit tests --- tests/unit/annotate-elements.test.ts | 75 ++++++++++++++++++---------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/tests/unit/annotate-elements.test.ts b/tests/unit/annotate-elements.test.ts index 1338461..a3d6219 100644 --- a/tests/unit/annotate-elements.test.ts +++ b/tests/unit/annotate-elements.test.ts @@ -28,10 +28,14 @@ function createMockElement(tag: string, attrs: Record, text = '' allAttrs[name] = value; }, extractData() { + const attrs = Object.entries(allAttrs) + .map(([name, value]) => `${name}="${value}"`) + .join(' '); return { tag, text, allAttrs: { ...allAttrs }, + outerHTML: `<${tag} ${attrs}>${text}`, x: 100, y: 200, }; @@ -142,32 +146,25 @@ describe('annotatePageElements', () => { }); describe('component metadata', () => { - let page: Page; - let elements: WebElement[]; - - beforeAll(async () => { - page = await browser.newPage(); - await page.setContent(` -
-
-

Toggle - off

- -
-
-

Code Input

- -
-
- `); - const result = await annotatePageElements(page); - elements = result.elements; - }); - - afterAll(async () => { - await page?.close(); - }); - - it('adds context and variant hints for drillable controls', () => { + it('adds context and variant hints for drillable controls', async () => { + const ariaSnapshot = '- switch "Enable feature" [ref=e1]'; + const page = createMockPage(ariaSnapshot, { + switch: [ + createMockElement( + 'button', + { + role: 'switch', + 'aria-checked': 'false', + 'data-explorbot-context': 'Toggle - off', + 'data-explorbot-area': 'button|role:switch|main', + 'data-explorbot-variant': 'rounded-full', + }, + 'Enable feature' + ), + ], + }); + + const { elements } = await annotatePageElements(page); const toggle = elements.find((el) => el.role === 'switch'); expect(toggle?.contextLabel).toBe('Toggle - off'); expect(toggle?.areaHints).toContain('role:switch'); @@ -175,7 +172,31 @@ describe('annotatePageElements', () => { expect(toggle?.outerHTML).toContain('aria-checked="false"'); }); - it('annotates code editor iframes for driller discovery', () => { + it('annotates code editor iframes for driller discovery', async () => { + const page = { + locator: (selector: string) => { + if (selector === 'body') { + return { + ariaSnapshot: async () => '', + }; + } + return { + evaluateAll: async () => [ + createMockElement('iframe', { + src: '/ember-monaco/frame.html', + 'data-explorbot-context': 'Code Input', + 'data-explorbot-variant': 'iframe|code-editor', + 'data-explorbot-frame-source-index': '1', + }).extractData(), + ], + }; + }, + getByRole: () => ({ + evaluateAll: async () => [], + }), + }; + + const { elements } = await annotatePageElements(page); const frame = elements.find((el) => el.role === 'iframe'); expect(frame?.contextLabel).toBe('Code Input'); expect(frame?.variantHints).toContain('iframe'); From 9d6a5bb021862088ba01b7e4652bcbf26d50be35 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Sat, 18 Apr 2026 01:46:44 +0300 Subject: [PATCH 06/11] upd --- exp_custom/Button_AI.md | 147 ----------- exp_custom/Button_Dropdown.md | 84 ------ exp_custom/Button_Extra.md | 147 ----------- exp_custom/Button_Green.md | 210 --------------- exp_custom/Button_Merge.md | 30 --- exp_custom/Button_Primary.md | 210 --------------- exp_custom/Button_Red.md | 210 --------------- exp_custom/Button_Secondary.md | 219 ---------------- exp_custom/Button_Third.md | 219 ---------------- exp_custom/Code_Input.md | 15 -- exp_custom/Form_Elements.md | 32 --- exp_custom/General_Inputs.md | 111 -------- exp_custom/Input_Empty_Handler.md | 12 - exp_custom/Input_With_Tags.md | 14 - exp_custom/Legacy.md | 30 --- exp_custom/Link.md | 113 -------- exp_custom/New_Counter.md | 183 ------------- exp_custom/Other_Buttons.md | 66 ----- exp_custom/PowerSelect.md | 212 --------------- exp_custom/PowerSelect_Filters.md | 244 ------------------ exp_custom/PowerSelect_Input.md | 175 ------------- exp_custom/PowerSelect_Multiple.md | 31 --- exp_custom/PowerSelect_Typeahead.md | 27 -- exp_custom/Search_Input.md | 13 - exp_custom/Tabs.md | 111 -------- src/ai/driller.ts | 306 +++++++++++----------- src/explorer.ts | 29 ++- src/utils/html.ts | 385 ++++++++++++++++++++++++++++ src/utils/web-element.ts | 208 +-------------- tests/unit/web-element.test.ts | 10 +- 30 files changed, 578 insertions(+), 3225 deletions(-) delete mode 100644 exp_custom/Button_AI.md delete mode 100644 exp_custom/Button_Dropdown.md delete mode 100644 exp_custom/Button_Extra.md delete mode 100644 exp_custom/Button_Green.md delete mode 100644 exp_custom/Button_Merge.md delete mode 100644 exp_custom/Button_Primary.md delete mode 100644 exp_custom/Button_Red.md delete mode 100644 exp_custom/Button_Secondary.md delete mode 100644 exp_custom/Button_Third.md delete mode 100644 exp_custom/Code_Input.md delete mode 100644 exp_custom/Form_Elements.md delete mode 100644 exp_custom/General_Inputs.md delete mode 100644 exp_custom/Input_Empty_Handler.md delete mode 100644 exp_custom/Input_With_Tags.md delete mode 100644 exp_custom/Legacy.md delete mode 100644 exp_custom/Link.md delete mode 100644 exp_custom/New_Counter.md delete mode 100644 exp_custom/Other_Buttons.md delete mode 100644 exp_custom/PowerSelect.md delete mode 100644 exp_custom/PowerSelect_Filters.md delete mode 100644 exp_custom/PowerSelect_Input.md delete mode 100644 exp_custom/PowerSelect_Multiple.md delete mode 100644 exp_custom/PowerSelect_Typeahead.md delete mode 100644 exp_custom/Search_Input.md delete mode 100644 exp_custom/Tabs.md diff --git a/exp_custom/Button_AI.md b/exp_custom/Button_AI.md deleted file mode 100644 index 20469a7..0000000 --- a/exp_custom/Button_AI.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=AI%3A%3AButton -title: Testomat.io -summary: Curated AI button interactions only. ---- -### SUCCEEDED: Drill click: AI button size mini icon only - -Solution: Clicks the mini icon-only AI button and opens the AI modal. - -```javascript -I.click("button.ai-btn.btn-only-icon.btn-mini:has(svg)") -``` - - -### SUCCEEDED: Drill click: AI button size mini icon only selected - -Solution: Clicks the selected mini icon-only AI button, opens the AI modal, and toggles selected state. - -```javascript -I.click("button.ai-btn.btn-only-icon.btn-mini.btn-selected:has(svg)") -``` - - -### SUCCEEDED: Drill click: AI button size small icon only - -Solution: Clicks the small icon-only AI button and opens the AI modal. - -```javascript -I.click("button.ai-btn.btn-only-icon.btn-sm:has(svg)") -``` - - -### SUCCEEDED: Drill click: AI button size small Default AI leading icon - -Solution: Clicks the small Default AI button with a leading icon and opens the AI modal. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Default AI\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: AI button size small Default AI leading and trailing icons - -Solution: Clicks the small Default AI button with leading and trailing icons and opens the AI modal/dropdown action. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: AI button size small Default AI selected - -Solution: Clicks the selected small Default AI dropdown-style button, opens the AI modal, and toggles selected state. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: AI button size medium icon only - -Solution: Clicks the medium icon-only AI button and opens the AI modal. - -```javascript -I.click("button.ai-btn.btn-only-icon.btn-md:has(svg)") -``` - - -### SUCCEEDED: Drill click: AI button size medium Default AI leading icon - -Solution: Clicks the medium Default AI button with a leading icon and opens the AI modal. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Default AI\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: AI button size medium Default AI leading and trailing icons - -Solution: Clicks the medium Default AI button with leading and trailing icons and opens the AI modal/dropdown menu. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: AI button size medium Default AI selected - -Solution: Clicks the selected medium Default AI dropdown-style button, opens the AI modal, and toggles selected state. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: AI button size large icon only - -Solution: Clicks the large icon-only AI button and opens the AI feature modal. - -```javascript -I.click("button.ai-btn.btn-only-icon.btn-lg:has(svg)") -``` - - -### SUCCEEDED: Drill click: AI button size large embedded text leading icon - -Solution: Clicks the large embedded-text AI button and opens the AI feature modal. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"embedded text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: AI button size large Default AI leading icon - -Solution: Clicks the large Default AI button with a leading icon and opens the AI modal. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Default AI\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: AI button size large Default AI leading and trailing icons - -Solution: Clicks the large Default AI button with leading and trailing icons and opens the AI modal/dropdown menu. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: AI button size large Default AI selected - -Solution: Clicks the selected large Default AI dropdown-style button, opens the AI modal, and toggles selected state. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Default AI\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: AI button with select dropdown - -Solution: Clicks the left side of the AI split button to open the AI modal; clicking the right side opens the dropdown. - -```javascript -I.click("//*[self::button and contains(@class,\"ai-btn\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-dropdown\") and contains(normalize-space(.),\"AI btn + select\")]") -``` diff --git a/exp_custom/Button_Dropdown.md b/exp_custom/Button_Dropdown.md deleted file mode 100644 index a8cf45e..0000000 --- a/exp_custom/Button_Dropdown.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3ADropdown -title: Testomat.io -summary: Curated dropdown button interactions only. ---- -### SUCCEEDED: Drill click: Dropdown button small icon trigger - -Solution: Clicks the small dropdown button trigger and opens dropdown menu. - -```javascript -I.click("div.primary-btn.btn-icon-after.btn-sm") -``` - - -### SUCCEEDED: Drill click: Dropdown button small Default - -Solution: Clicks the small Default dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Default\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Dropdown button small Without icon - -Solution: Clicks the small Without icon dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Without icon\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Dropdown button medium icon trigger - -Solution: Clicks the medium dropdown button trigger and opens dropdown menu. - -```javascript -I.click("div.primary-btn.btn-icon-after.btn-md") -``` - - -### SUCCEEDED: Drill click: Dropdown button medium Default - -Solution: Clicks the medium Default dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Default\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Dropdown button medium Without icon - -Solution: Clicks the medium Without icon dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Without icon\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Dropdown button large icon trigger - -Solution: Clicks the large dropdown button trigger and opens dropdown menu. - -```javascript -I.click("div.primary-btn.btn-icon-after.btn-lg") -``` - - -### SUCCEEDED: Drill click: Dropdown button large Default - -Solution: Clicks the large Default dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Default\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Dropdown button large Without icon - -Solution: Clicks the large Without icon dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"primary-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Without icon\" and not(.//svg)]") -``` diff --git a/exp_custom/Button_Extra.md b/exp_custom/Button_Extra.md deleted file mode 100644 index 5b34ec3..0000000 --- a/exp_custom/Button_Extra.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3AExtra -title: Testomat.io -summary: Curated extra button interactions only. ---- -### SUCCEEDED: Drill click: Extra button mini default - -Solution: Clicks the mini extra button and opens its dropdown. - -```javascript -I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-xs\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") -``` - - -### SUCCEEDED: Drill click: Extra button mini above - -Solution: Clicks the mini extra button with cog icon and opens its dropdown above. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-xs:has(svg.md-icon-cog)") -``` - - -### SUCCEEDED: Drill click: Extra button mini render in body - -Solution: Clicks the mini extra button and opens its dropdown. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-xs:has(svg.md-icon-timer)") -``` - - -### SUCCEEDED: Drill click: Extra button mini beta - -Solution: Clicks the mini beta extra button and opens its dropdown. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-xs:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") -``` - - -### SUCCEEDED: Drill click: Extra button small default - -Solution: Clicks the small extra button and opens its dropdown. - -```javascript -I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-sm\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") -``` - - -### SUCCEEDED: Drill click: Extra button small above - -Solution: Clicks the small extra button with cog icon and opens its dropdown above. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-sm:has(svg.md-icon-cog)") -``` - - -### SUCCEEDED: Drill click: Extra button small render in body - -Solution: Clicks the small extra button and opens its dropdown. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-sm:has(svg.md-icon-timer)") -``` - - -### SUCCEEDED: Drill click: Extra button small beta - -Solution: Clicks the small beta extra button and opens its dropdown. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-sm:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") -``` - - -### SUCCEEDED: Drill click: Extra button medium default - -Solution: Clicks the medium extra button and opens its dropdown. - -```javascript -I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-md\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") -``` - - -### SUCCEEDED: Drill click: Extra button medium above - -Solution: Clicks the medium extra button with cog icon and opens its dropdown above. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-md:has(svg.md-icon-cog)") -``` - - -### SUCCEEDED: Drill click: Extra button medium render in body - -Solution: Clicks the medium extra button and opens its dropdown. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-md:has(svg.md-icon-timer)") -``` - - -### SUCCEEDED: Drill click: Extra button medium beta - -Solution: Clicks the medium beta extra button and opens its dropdown. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-md:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") -``` - - -### SUCCEEDED: Drill click: Extra button large default - -Solution: Clicks the large extra button and opens its dropdown. - -```javascript -I.click("//div[contains(@class,\"secondary-btn\") and contains(@class,\"btn-only-icon\") and contains(@class,\"btn-lg\") and .//svg[contains(@class,\"md-icon-dots-horizontal\")] and not(.//svg[contains(@class,\"md-icon-circle-medium\")])]") -``` - - -### SUCCEEDED: Drill click: Extra button large above - -Solution: Clicks the large extra button with cog icon and opens its dropdown above. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-lg:has(svg.md-icon-cog)") -``` - - -### SUCCEEDED: Drill click: Extra button large render in body - -Solution: Clicks the large extra button and opens its dropdown. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-lg:has(svg.md-icon-timer)") -``` - - -### SUCCEEDED: Drill click: Extra button large beta - -Solution: Clicks the large beta extra button and opens its dropdown. - -```javascript -I.click("div.secondary-btn.btn-only-icon.btn-lg:has(svg.md-icon-circle-medium):has(svg.md-icon-dots-horizontal)") -``` diff --git a/exp_custom/Button_Green.md b/exp_custom/Button_Green.md deleted file mode 100644 index 8323625..0000000 --- a/exp_custom/Button_Green.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3AGreen -title: Testomat.io -summary: Curated green button interactions only. ---- -### SUCCEEDED: Drill click: Green button size small plain text - -Solution: Clicks the small green text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size small leading icon - -Solution: Clicks the small green text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Green button size small trailing icon - -Solution: Clicks the small green text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Green button size small leading and trailing icons - -Solution: Clicks the small green text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Green button size small icon only - -Solution: Clicks the small green icon-only button. - -```javascript -I.click("button.green-btn.btn-only-icon.btn-sm:has(svg)") -``` - - -### SUCCEEDED: Drill click: Green button size small selected - -Solution: Toggles the selected state of the small green selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size medium plain text - -Solution: Clicks the medium green text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size medium leading icon - -Solution: Clicks the medium green text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Green button size medium trailing icon - -Solution: Clicks the medium green text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Green button size medium leading and trailing icons - -Solution: Clicks the medium green text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Green button size medium icon only - -Solution: Clicks the medium green icon-only button. - -```javascript -I.click("button.green-btn.btn-only-icon.btn-md:has(svg)") -``` - - -### SUCCEEDED: Drill click: Green button size medium selected - -Solution: Toggles the selected state of the medium green selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size large plain text - -Solution: Clicks the large green text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size large leading icon - -Solution: Clicks the large green text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Green button size large trailing icon - -Solution: Clicks the large green text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Green button size large leading and trailing icons - -Solution: Clicks the large green text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Green button size large icon only - -Solution: Clicks the large green icon-only button. - -```javascript -I.click("button.green-btn.btn-only-icon.btn-lg:has(svg)") -``` - - -### SUCCEEDED: Drill click: Green button size large two icons only - -Solution: Clicks the large green button that contains two icons and no text. - -```javascript -I.click("button.green-btn.btn-only-two-icons.btn-lg.btn-icon-after") -``` - - -### SUCCEEDED: Drill click: Green button size large selected - -Solution: Toggles the selected state of the large green selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size extra large plain text - -Solution: Clicks the extra large green text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Green button size extra large leading icon - -Solution: Clicks the extra large green text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Green button size extra large icon only - -Solution: Clicks the extra large green icon-only button. - -```javascript -I.click("button.green-btn.btn-only-icon.btn-xl:has(svg)") -``` - - -### SUCCEEDED: Drill click: Green button size extra large selected - -Solution: Toggles the selected state of the extra large green selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"green-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` diff --git a/exp_custom/Button_Merge.md b/exp_custom/Button_Merge.md deleted file mode 100644 index 1936678..0000000 --- a/exp_custom/Button_Merge.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3AMerge -title: Testomat.io -summary: Curated merge button interactions only. ---- -### SUCCEEDED: Drill click: Merge button small - -Solution: Clicks the small Merge dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"merge-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Merge\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Merge button medium - -Solution: Clicks the medium Merge dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"merge-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Merge\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Merge button large - -Solution: Clicks the large Merge dropdown button and opens dropdown menu. - -```javascript -I.click("//*[self::div and contains(@class,\"merge-btn\") and contains(@class,\"btn-icon-after\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Merge\" and not(.//svg)]") -``` diff --git a/exp_custom/Button_Primary.md b/exp_custom/Button_Primary.md deleted file mode 100644 index b5f3037..0000000 --- a/exp_custom/Button_Primary.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3APrimary -title: Testomat.io -summary: Curated primary button interactions only. ---- -### SUCCEEDED: Drill click: Primary button size small plain text - -Solution: Clicks the small primary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size small leading icon - -Solution: Clicks the small primary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary button size small trailing icon - -Solution: Clicks the small primary text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Primary button size small leading and trailing icons - -Solution: Clicks the small primary text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Primary button size small icon only - -Solution: Clicks the small primary icon-only button. - -```javascript -I.click("button.primary-btn.btn-only-icon.btn-sm:has(svg)") -``` - - -### SUCCEEDED: Drill click: Primary button size small selected - -Solution: Toggles the selected state of the small primary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size medium plain text - -Solution: Clicks the medium primary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size medium leading icon - -Solution: Clicks the medium primary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary button size medium trailing icon - -Solution: Clicks the medium primary text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Primary button size medium leading and trailing icons - -Solution: Clicks the medium primary text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Primary button size medium icon only - -Solution: Clicks the medium primary icon-only button. - -```javascript -I.click("button.primary-btn.btn-only-icon.btn-md:has(svg)") -``` - - -### SUCCEEDED: Drill click: Primary button size medium selected - -Solution: Toggles the selected state of the medium primary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size large plain text - -Solution: Clicks the large primary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size large leading icon - -Solution: Clicks the large primary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary button size large trailing icon - -Solution: Clicks the large primary text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Primary button size large leading and trailing icons - -Solution: Clicks the large primary text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Primary button size large icon only - -Solution: Clicks the large primary icon-only button. - -```javascript -I.click("button.primary-btn.btn-only-icon.btn-lg:has(svg)") -``` - - -### SUCCEEDED: Drill click: Primary button size large two icons only - -Solution: Clicks the large primary button that contains two icons and no text. - -```javascript -I.click("button.primary-btn.btn-only-two-icons.btn-lg.btn-icon-after") -``` - - -### SUCCEEDED: Drill click: Primary button size large selected - -Solution: Toggles the selected state of the large primary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size extra large plain text - -Solution: Clicks the extra large primary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary button size extra large leading icon - -Solution: Clicks the extra large primary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary button size extra large icon only - -Solution: Clicks the extra large primary icon-only button. - -```javascript -I.click("button.primary-btn.btn-only-icon.btn-xl:has(svg)") -``` - - -### SUCCEEDED: Drill click: Primary button size extra large selected - -Solution: Toggles the selected state of the extra large primary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` diff --git a/exp_custom/Button_Red.md b/exp_custom/Button_Red.md deleted file mode 100644 index 2cb86cf..0000000 --- a/exp_custom/Button_Red.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3ARed -title: Testomat.io -summary: Curated red button interactions only. ---- -### SUCCEEDED: Drill click: Red button size small plain text - -Solution: Clicks the small red text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size small leading icon - -Solution: Clicks the small red text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Red button size small trailing icon - -Solution: Clicks the small red text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Red button size small leading and trailing icons - -Solution: Clicks the small red text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Red button size small icon only - -Solution: Clicks the small red icon-only button. - -```javascript -I.click("button.red-btn.btn-only-icon.btn-sm:has(svg)") -``` - - -### SUCCEEDED: Drill click: Red button size small selected - -Solution: Toggles the selected state of the small red selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size medium plain text - -Solution: Clicks the medium red text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size medium leading icon - -Solution: Clicks the medium red text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Red button size medium trailing icon - -Solution: Clicks the medium red text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Red button size medium leading and trailing icons - -Solution: Clicks the medium red text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Red button size medium icon only - -Solution: Clicks the medium red icon-only button. - -```javascript -I.click("button.red-btn.btn-only-icon.btn-md:has(svg)") -``` - - -### SUCCEEDED: Drill click: Red button size medium selected - -Solution: Toggles the selected state of the medium red selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size large plain text - -Solution: Clicks the large red text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size large leading icon - -Solution: Clicks the large red text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Red button size large trailing icon - -Solution: Clicks the large red text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Red button size large leading and trailing icons - -Solution: Clicks the large red text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Red button size large icon only - -Solution: Clicks the large red icon-only button. - -```javascript -I.click("button.red-btn.btn-only-icon.btn-lg:has(svg)") -``` - - -### SUCCEEDED: Drill click: Red button size large two icons only - -Solution: Clicks the large red button that contains two icons and no text. - -```javascript -I.click("button.red-btn.btn-only-two-icons.btn-lg.btn-icon-after") -``` - - -### SUCCEEDED: Drill click: Red button size large selected - -Solution: Toggles the selected state of the large red selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size extra large plain text - -Solution: Clicks the extra large red text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Red button size extra large leading icon - -Solution: Clicks the extra large red text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Red button size extra large icon only - -Solution: Clicks the extra large red icon-only button. - -```javascript -I.click("button.red-btn.btn-only-icon.btn-xl:has(svg)") -``` - - -### SUCCEEDED: Drill click: Red button size extra large selected - -Solution: Toggles the selected state of the extra large red selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"red-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` diff --git a/exp_custom/Button_Secondary.md b/exp_custom/Button_Secondary.md deleted file mode 100644 index 828ca17..0000000 --- a/exp_custom/Button_Secondary.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3ASecondary -title: Testomat.io -summary: Curated secondary button interactions only. ---- -### SUCCEEDED: Drill click: Secondary button size mini icon only - -Solution: Clicks the mini secondary icon-only button. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-mini") -``` - - -### SUCCEEDED: Drill click: Secondary button size small plain text - -Solution: Clicks the small secondary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size small leading icon - -Solution: Clicks the small secondary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary button size small trailing icon - -Solution: Clicks the small secondary text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Secondary button size small leading and trailing icons - -Solution: Clicks the small secondary text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Secondary button size small icon only - -Solution: Clicks the small secondary icon-only button. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-sm") -``` - - -### SUCCEEDED: Drill click: Secondary button size small selected - -Solution: Toggles the selected state of the small secondary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size medium plain text - -Solution: Clicks the medium secondary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size medium leading icon - -Solution: Clicks the medium secondary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary button size medium trailing icon - -Solution: Clicks the medium secondary text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Secondary button size medium leading and trailing icons - -Solution: Clicks the medium secondary text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Secondary button size medium icon only - -Solution: Clicks the medium secondary icon-only button. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-md") -``` - - -### SUCCEEDED: Drill click: Secondary button size medium selected - -Solution: Toggles the selected state of the medium secondary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size large plain text - -Solution: Clicks the large secondary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size large leading icon - -Solution: Clicks the large secondary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary button size large trailing icon - -Solution: Clicks the large secondary text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Secondary button size large leading and trailing icons - -Solution: Clicks the large secondary text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Secondary button size large icon only - -Solution: Clicks the large secondary icon-only button. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-lg") -``` - - -### SUCCEEDED: Drill click: Secondary button size large two icons only - -Solution: Clicks the large secondary button that contains two icons and no text. - -```javascript -I.click("button.secondary-btn.btn-only-two-icons.btn-lg.btn-icon-after") -``` - - -### SUCCEEDED: Drill click: Secondary button size large selected - -Solution: Toggles the selected state of the large secondary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size extra large plain text - -Solution: Clicks the extra large secondary text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary button size extra large leading icon - -Solution: Clicks the extra large secondary text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary button size extra large icon only - -Solution: Clicks the extra large secondary icon-only button. - -```javascript -I.click("button.secondary-btn.btn-only-icon.btn-xl") -``` - - -### SUCCEEDED: Drill click: Secondary button size extra large selected - -Solution: Toggles the selected state of the extra large secondary selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` diff --git a/exp_custom/Button_Third.md b/exp_custom/Button_Third.md deleted file mode 100644 index 93b5002..0000000 --- a/exp_custom/Button_Third.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Button%3A%3AThird -title: Testomat.io -summary: Curated third button interactions only. ---- -### SUCCEEDED: Drill click: Third button size mini icon only - -Solution: Clicks the mini third icon-only button. - -```javascript -I.click("button.third-btn.btn-only-icon.btn-mini") -``` - - -### SUCCEEDED: Drill click: Third button size small plain text - -Solution: Clicks the small third text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size small leading icon - -Solution: Clicks the small third text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Third button size small trailing icon - -Solution: Clicks the small third text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Third button size small leading and trailing icons - -Solution: Clicks the small third text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Third button size small icon only - -Solution: Clicks the small third icon-only button. - -```javascript -I.click("button.third-btn.btn-only-icon.btn-sm") -``` - - -### SUCCEEDED: Drill click: Third button size small selected - -Solution: Toggles the selected state of the small third selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size medium plain text - -Solution: Clicks the medium third text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size medium leading icon - -Solution: Clicks the medium third text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Third button size medium trailing icon - -Solution: Clicks the medium third text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Third button size medium leading and trailing icons - -Solution: Clicks the medium third text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Third button size medium icon only - -Solution: Clicks the medium third icon-only button. - -```javascript -I.click("button.third-btn.btn-only-icon.btn-md") -``` - - -### SUCCEEDED: Drill click: Third button size medium selected - -Solution: Toggles the selected state of the medium third selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size large plain text - -Solution: Clicks the large third text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size large leading icon - -Solution: Clicks the large third text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Third button size large trailing icon - -Solution: Clicks the large third text button with a trailing chevron icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)=1]") -``` - - -### SUCCEEDED: Drill click: Third button size large leading and trailing icons - -Solution: Clicks the large third text button with both leading and trailing icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and contains(normalize-space(.),\"Button text\") and count(.//svg)>=2]") -``` - - -### SUCCEEDED: Drill click: Third button size large icon only - -Solution: Clicks the large third icon-only button. - -```javascript -I.click("button.third-btn.btn-only-icon.btn-lg") -``` - - -### SUCCEEDED: Drill click: Third button size large two icons only - -Solution: Clicks the large third button that contains two icons and no text. - -```javascript -I.click("button.third-btn.btn-only-two-icons.btn-lg.btn-icon-after") -``` - - -### SUCCEEDED: Drill click: Third button size large selected - -Solution: Toggles the selected state of the large third selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size extra large plain text - -Solution: Clicks the extra large third text button without icons. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and not(contains(@class,\"btn-icon-after\")) and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Third button size extra large leading icon - -Solution: Clicks the extra large third text button with a leading icon. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(normalize-space(.),\"Button text\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Third button size extra large icon only - -Solution: Clicks the extra large third icon-only button. - -```javascript -I.click("button.third-btn.btn-only-icon.btn-xl") -``` - - -### SUCCEEDED: Drill click: Third button size extra large selected - -Solution: Toggles the selected state of the extra large third selected button. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and contains(@class,\"btn-selected\") and contains(normalize-space(.),\"Button selected\") and not(.//svg)]") -``` diff --git a/exp_custom/Code_Input.md b/exp_custom/Code_Input.md deleted file mode 100644 index e79b4ff..0000000 --- a/exp_custom/Code_Input.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Code%20Input -title: Testomat.io -summary: Curated code input editor interaction only. ---- -### SUCCEEDED: Drill type code: Code editor - -Solution: Switches into the code editor iframe, clicks the Monaco editor, types example code, and returns to the main page. - -```javascript -I.switchTo("(//iframe[contains(@src,\"/ember-monaco/frame.html\")])[1]"); -I.click(".monaco-editor"); -I.type("const value = \"test\";"); -I.switchTo(); -``` diff --git a/exp_custom/Form_Elements.md b/exp_custom/Form_Elements.md deleted file mode 100644 index 444dff0..0000000 --- a/exp_custom/Form_Elements.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Form%20Elements -title: Testomat.io -summary: Curated form element interactions only. ---- -### SUCCEEDED: Drill click: Toggle off switch - -Solution: Clicks the Toggle - off switch and toggles it to the on state. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Toggle - off"]]//button[@role="switch" and not(contains(@class,"cursor-not-allowed"))]') -``` - - -### SUCCEEDED: Drill click: Toggle on switch - -Solution: Clicks the Toggle - on switch and toggles it to the off state. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Toggle - on"]]//button[@role="switch" and not(contains(@class,"cursor-not-allowed"))]') -``` - - -### SUCCEEDED: Drill select date range: DateRange textbox - -Solution: Clicks the DateRange input, opens the date picker, and selects a date range. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="DateRange"]]//input[@placeholder="Select date range"]'); -I.click('span.flatpickr-day[aria-label="April 12, 2026"]'); -I.click('span.flatpickr-day[aria-label="April 13, 2026"]'); -``` diff --git a/exp_custom/General_Inputs.md b/exp_custom/General_Inputs.md deleted file mode 100644 index 2b35638..0000000 --- a/exp_custom/General_Inputs.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=General%20Inputs -title: Testomat.io -summary: Curated general input interactions only. ---- -### SUCCEEDED: Drill fill: Basic input "Basic input" - -Solution: Fills the basic text input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Basic input"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Basic input"]', 'Test Input') -``` - - -### SUCCEEDED: Drill fill: Input with value "Input with value" - -Solution: Replaces the existing input value with sample text. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Input with value"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input"]]//input[@placeholder="Input with value"]', 'Sample Text') -``` - - -### SUCCEEDED: Drill fill: Text input "Text" - -Solution: Fills the text input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Text"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Text"]', 'Sample Text') -``` - - -### SUCCEEDED: Drill fill: Number input "Number" - -Solution: Fills the number input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Number"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Number"]', '42') -``` - - -### SUCCEEDED: Drill fill: Date input "Date" - -Solution: Fills the date input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Date"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Date"]', '2026-04-12') -``` - - -### SUCCEEDED: Drill fill: Time input "Time" - -Solution: Fills the time input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Time"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Time"]', '12:30') -``` - - -### SUCCEEDED: Drill fill: Password input "Password" - -Solution: Fills the password input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Password"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Password"]', 'myPassword') -``` - - -### SUCCEEDED: Drill fill: Email input "Email" - -Solution: Fills the email input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Email"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Email"]', 'user@example.com') -``` - - -### SUCCEEDED: Drill fill: Search input "Search" - -Solution: Fills the search input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Search"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Search"]', 'example search') -``` - - -### SUCCEEDED: Drill click: Checkbox input "Checkbox" - -Solution: Toggles the checkbox input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Checkbox"]') -``` - - -### SUCCEEDED: Drill click: Radio input "Radio" - -Solution: Selects the radio input. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="General Input with types"]]//input[@placeholder="Radio"]') -``` diff --git a/exp_custom/Input_Empty_Handler.md b/exp_custom/Input_Empty_Handler.md deleted file mode 100644 index d66503b..0000000 --- a/exp_custom/Input_Empty_Handler.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -url: /projects/test-d6178/components/?s=Input%20Empty%20Handler -title: Testomat.io -summary: Curated input empty handler interaction only. ---- -### SUCCEEDED: Drill click: Title input "Enter title" - -Solution: Clicks the title input and focuses it. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Title"]]//input[@placeholder="Enter title"]') -``` diff --git a/exp_custom/Input_With_Tags.md b/exp_custom/Input_With_Tags.md deleted file mode 100644 index 7ed77c0..0000000 --- a/exp_custom/Input_With_Tags.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Input%20With%20Tags -title: Testomat.io -summary: Curated input with tags interaction only. ---- -### SUCCEEDED: Drill addTag: Tags input "Type @ to add tags" - -Solution: Clicks the tag combobox, enters a tag value, and confirms it with Enter. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input With Tags"]]//input[@placeholder="Type @ to add tags"]'); -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Input With Tags"]]//input[@placeholder="Type @ to add tags"]', '@foo'); -I.pressKey('Enter'); -``` diff --git a/exp_custom/Legacy.md b/exp_custom/Legacy.md deleted file mode 100644 index 0fb07a4..0000000 --- a/exp_custom/Legacy.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -url: /projects/test-d6178/components/?s=Legacy -title: Testomat.io -summary: Curated legacy dropdown trigger interactions only. ---- -### SUCCEEDED: Drill click: Passed 1 legacy status dropdown - -Solution: Clicks the Passed 1 legacy status dropdown trigger and opens its dropdown. - -```javascript -I.click("//*[self::div and contains(@class,\"secondary-btn\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Passed 1\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Failed 2 legacy status dropdown - -Solution: Clicks the Failed 2 legacy status dropdown trigger and opens its dropdown. - -```javascript -I.click("//*[self::div and contains(@class,\"secondary-btn\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Failed 2\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Skipped 3 legacy status dropdown - -Solution: Clicks the Skipped 3 legacy status dropdown trigger and opens its dropdown. - -```javascript -I.click("//*[self::div and contains(@class,\"secondary-btn\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Skipped 3\" and not(.//svg)]") -``` diff --git a/exp_custom/Link.md b/exp_custom/Link.md deleted file mode 100644 index 23852de..0000000 --- a/exp_custom/Link.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Link -title: Testomat.io -summary: Curated link interactions only. ---- -### SUCCEEDED: Drill click: Primary link size medium leading icon - -Solution: Clicks the medium primary link with a leading icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary link size medium plain text - -Solution: Clicks the medium primary text link without icons. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary link size medium trailing icon - -Solution: Clicks the medium primary link with a trailing icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary link size small leading icon - -Solution: Clicks the small primary link with a leading icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Primary link size small plain text - -Solution: Clicks the small primary text link without icons. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Primary link size small trailing icon - -Solution: Clicks the small primary link with a trailing icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Primary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"primary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary link size medium leading icon - -Solution: Clicks the medium secondary link with a leading icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary link size medium plain text - -Solution: Clicks the medium secondary text link without icons. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary link size medium trailing icon - -Solution: Clicks the medium secondary link with a trailing icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - md\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-md\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary link size small leading icon - -Solution: Clicks the small secondary link with a leading icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[1][self::svg]]") -``` - - -### SUCCEEDED: Drill click: Secondary link size small plain text - -Solution: Clicks the small secondary text link without icons. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Secondary link size small trailing icon - -Solution: Clicks the small secondary link with a trailing icon. Opens the link in a new tab (target="_blank"). - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(@class,\"FreestyleUsage-title\") and normalize-space(.)=\"Link::Secondary - sm\"]]//a[contains(@class,\"baseLink\") and contains(@class,\"secondary\") and contains(@class,\"link-sm\") and contains(@class,\"text-xs\") and contains(normalize-space(.),\"Link\") and ./*[last()][self::svg]]") -``` - - diff --git a/exp_custom/New_Counter.md b/exp_custom/New_Counter.md deleted file mode 100644 index 15aed73..0000000 --- a/exp_custom/New_Counter.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=New%20counter -title: Testomat.io -summary: Curated counter button interactions only. ---- -### SUCCEEDED: Drill click: Counter in third button large icon counter - -Solution: Clicks the third large icon-counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"icon-counter\") and normalize-space(.)=\"7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large icon counter - -Solution: Clicks the secondary large icon-counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"icon-counter\") and normalize-space(.)=\"7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button extra large Pending - -Solution: Clicks the third extra large Pending counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button medium Failed - -Solution: Clicks the third medium Failed counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Failed\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button small Passed - -Solution: Clicks the third small Passed counter button; the counter increments from 7. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button extra large Pending - -Solution: Clicks the secondary extra large Pending counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-xl\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button medium Failed - -Solution: Clicks the secondary medium Failed counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Failed\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button small Passed - -Solution: Clicks the secondary small Passed counter button; the counter increments from 7. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button large selected - -Solution: Clicks the selected third large counter button and toggles its selected state. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and normalize-space(.)=\"Button text\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large selected - -Solution: Clicks the selected secondary large counter button and toggles its selected state. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-selected\") and normalize-space(.)=\"Button text\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button large Pending dropdown - -Solution: Clicks the third large Pending counter button and opens its dropdown. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button large Skipped dropdown - -Solution: Clicks the third large Skipped counter button and opens its dropdown. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Skipped\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large Pending dropdown - -Solution: Clicks the secondary large Pending counter button and opens its dropdown. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Pending\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large Skipped dropdown - -Solution: Clicks the secondary large Skipped counter button and opens its dropdown. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and contains(@class,\"btn-icon-after\") and normalize-space(.)=\"Skipped\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button large Failed with icon - -Solution: Clicks the third large Failed counter button with icon. - -```javascript -I.click("button.third-btn.btn-text-and-icon.btn-lg:has-text(\"Failed\\n \\n \\n 7\"):has(svg)") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large Failed with icon - -Solution: Clicks the secondary large Failed counter button with icon. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Failed\n \n \n 7\" and .//svg]") -``` - - -### SUCCEEDED: Drill click: Counter in third button large Passed - -Solution: Clicks the third large Passed counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in third button large Skipped - -Solution: Clicks the third large Skipped counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Skipped\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large Failed - -Solution: Clicks the secondary large Failed counter button with value 7. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Failed\n \n \n 7\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Counter in secondary button large Passed - -Solution: Clicks the secondary large Passed counter button; the counter increments from 7. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Passed\n \n \n 7\" and not(.//svg)]") -``` diff --git a/exp_custom/Other_Buttons.md b/exp_custom/Other_Buttons.md deleted file mode 100644 index e1d4ad0..0000000 --- a/exp_custom/Other_Buttons.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Other%20buttons -title: Testomat.io -summary: Curated other button interactions only. ---- -### SUCCEEDED: Drill click: Template button Use Template - -Solution: Clicks the Use Template button and opens the templates dropdown menu. - -```javascript -I.click("//*[self::button and contains(@class,\"secondary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and contains(@class,\"btn-icon-after\") and contains(@class,\"truncate\") and normalize-space(.)=\"Use Template\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Substatus button Passed - -Solution: Clicks the Passed substatus button and keeps the selected substatus state active. - -```javascript -I.click("//*[self::button and contains(@class,\"substatus\") and contains(@class,\"passed\") and contains(@class,\"selected\") and normalize-space(.)=\"Passed\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Substatus button Skipped - -Solution: Clicks the Skipped substatus button and keeps the selected substatus state active. - -```javascript -I.click("//*[self::button and contains(@class,\"substatus\") and contains(@class,\"skipped\") and contains(@class,\"selected\") and normalize-space(.)=\"Skipped\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Substatus button Failed - -Solution: Clicks the Failed substatus button and keeps the selected substatus state active. - -```javascript -I.click("//*[self::button and contains(@class,\"substatus\") and contains(@class,\"failed\") and contains(@class,\"selected\") and normalize-space(.)=\"Failed\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Substatus button Click me - -Solution: Clicks the Click me substatus action button and opens its loading spinner after click. - -```javascript -I.click("button.substatus.click:has-text(\"Click me\"):has(svg):not(:has(svg + svg))") -``` - - -### SUCCEEDED: Drill click: Lang button beautify - -Solution: Clicks the small beautify language button. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-sm\") and normalize-space(.)=\"beautify\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: Async button Click Me - -Solution: Clicks the async Click Me button and starts its loading state. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-md\") and contains(@class,\"btn-text-and-icon\") and normalize-space(.)=\"Click Me\" and not(.//svg)]") -``` diff --git a/exp_custom/PowerSelect.md b/exp_custom/PowerSelect.md deleted file mode 100644 index fac5761..0000000 --- a/exp_custom/PowerSelect.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=PowerSelect -title: Testomat.io -summary: Curated PowerSelect interactions grouped by distinct custom component behaviors. ---- -### SUCCEEDED: Drill select option: requirement source dropdown - -Solution: Opens the requirement source PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Select a requirement source\")]"); -I.click({"role":"option","text":"Confluence"}); -``` - - -### SUCCEEDED: Drill clear option: requirement source dropdown - -Solution: Clicks the selected PowerSelect value to clear it. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Confluence\")]") -``` - - -### SUCCEEDED: Drill click: assignee dropdown - -Solution: Opens the Assign to PowerSelect dropdown. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Assign to\")]") -``` - - -### SUCCEEDED: Drill click: TQL data source dropdown - -Solution: Opens the TQL search context PowerSelect dropdown. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"TQL search context\")]") -``` - - -### SUCCEEDED: Drill select option: tests data source dropdown - -Solution: Opens the Data Source PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Tests\")]"); -I.click({"role":"option","text":"Runs"}); -``` - - -### SUCCEEDED: Drill clear option: export mode dropdown - -Solution: Clicks the selected PowerSelect value to clear it. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Only found Tests\")]") -``` - - -### SUCCEEDED: Drill select option: format dropdown - -Solution: Opens the format PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"beautify\")]"); -I.click({"role":"option","text":"json"}); -``` - - -### SUCCEEDED: Drill click: invite users dialog - -Solution: Clicks the Invite users button and opens the invite user dialog. - -```javascript -I.click("//*[self::button and contains(@class,\"primary-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-lg\") and normalize-space(.)=\"Invite users\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill click: priority dropdown - -Solution: Clicks the icon-only priority PowerSelect dropdown and opens its options. - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and contains(@class,\"ember-power-select-trigger\")]") -``` - - -### SUCCEEDED: Drill select option: OS dropdown - -Solution: Opens the OS PowerSelect dropdown and selects an option. - -```javascript -I.click({"role":"button","text":"Select an OS"}); -I.click({"role":"option","text":"Windows"}); -``` - - -### SUCCEEDED: Drill click: action dropdown - -Solution: Clicks the icon-only PowerSelect dropdown in the Test section and opens its options. - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and contains(@class,\"ember-power-select-trigger\")]") -``` - - -### SUCCEEDED: Drill click: run dropdown - -Solution: Opens the run selection PowerSelect dropdown. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Select a run to check\")]") -``` - - -### SUCCEEDED: Drill select option: started by dropdown - -Solution: Opens the Started by PowerSelect dropdown and selects a user. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Started by\")]"); -I.click({"role":"option","text":"Denys Kuchma"}); -``` - - -### SUCCEEDED: Drill clear option: selected user dropdown - -Solution: Clicks the selected PowerSelect value to clear it. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"(Me)\")]") -``` - - -### SUCCEEDED: Drill select option: automation framework dropdown - -Solution: Opens the automation framework PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Select an automation framework you use\")]"); -I.click({"role":"option","text":"Cucumber"}); -``` - - -### SUCCEEDED: Drill select option: language dropdown - -Solution: Opens the language PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"Select a language you use\")]"); -I.click({"role":"option","text":"JavaScript"}); -``` - - -### SUCCEEDED: Drill verify disabled: disabled dropdowns - -Solution: Verifies that the Setup section contains disabled PowerSelect dropdowns. - -```javascript -I.seeElement("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and @aria-disabled=\"true\" and contains(.,\"vitest\")]"); -I.seeElement("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and @aria-disabled=\"true\" and contains(.,\"JavaScript\")]"); -``` - - -### SUCCEEDED: Drill click: timezone dropdown - -Solution: Opens the project timezone PowerSelect dropdown. - -```javascript -I.click("//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage \") and .//*[contains(concat(\" \", normalize-space(@class), \" \"), \" FreestyleUsage-title \") and normalize-space(.)=\"\"]]//div[@role=\"button\" and contains(@class,\"ember-power-select-trigger\") and not(normalize-space(.))][1]") -``` - - -### SUCCEEDED: Drill select option: framework dropdown - -Solution: Opens the project framework PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"vitest\")]"); -I.click({"role":"option","text":"Cucumber"}); -``` - - -### SUCCEEDED: Drill click: language dropdown - -Solution: Opens the project language PowerSelect dropdown. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"JavaScript\")]") -``` - - -### SUCCEEDED: Drill select option: notification dropdown - -Solution: Opens the notification PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"How to notify\")]"); -I.click({"role":"option","text":"Email"}); -``` - - -### SUCCEEDED: Drill select option: manual type dropdown - -Solution: Opens the manual test type PowerSelect dropdown and selects an option. - -```javascript -I.click("//*[self::div and @role=\"button\" and contains(.,\"manual\")]"); -I.click("//li[contains(@class,\"ember-power-select-option\") and contains(.,\"automated\")]"); -``` diff --git a/exp_custom/PowerSelect_Filters.md b/exp_custom/PowerSelect_Filters.md deleted file mode 100644 index 605f518..0000000 --- a/exp_custom/PowerSelect_Filters.md +++ /dev/null @@ -1,244 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=PowerSelect%20Filters -title: Testomat.io -summary: Curated PowerSelect Filters interactions for simple selects, multiselects, date range, and filter actions. ---- -### SUCCEEDED: Drill select option: PowerSelect Filters Type dropdown - -Solution: Opens the Type PowerSelect filter and selects an option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Type\"]]//*[self::div and @role=\"button\" and contains(.,\"Select Type\")]"); -I.click({"role":"option","text":"Suite"}); -``` - - -### SUCCEEDED: Drill change option: PowerSelect Filters selected Type dropdown - -Solution: Opens the selected Type PowerSelect filter and selects another option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Type\"]]//*[self::div and @role=\"button\" and contains(.,\"Suite\")]"); -I.click({"role":"option","text":"Test"}); -``` - - -### SUCCEEDED: Drill clear option: PowerSelect Filters selected Type dropdown - -Solution: Clicks the selected Type PowerSelect filter value to clear it. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Type\"]]//*[self::div and @role=\"button\" and contains(.,\"Suite\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") -``` - - -### SUCCEEDED: Drill open date picker: PowerSelect Filters Date Range field - -Solution: Opens the Date Range filter date picker. - -```javascript -I.click("(//*[self::li and .//p[normalize-space(.)=\"Date Range\"]]//input[@placeholder=\"Select range\"])[1]") -``` - - -### SUCCEEDED: Drill select date: PowerSelect Filters Date Range field - -Solution: Opens the Date Range filter date picker and selects a date. - -```javascript -I.click("(//*[self::li and .//p[normalize-space(.)=\"Date Range\"]]//input[@placeholder=\"Select range\"])[1]"); -I.click("span[aria-label=\"April 15, 2026\"]"); -``` - - -### SUCCEEDED: Drill close date picker: PowerSelect Filters Date Range field - -Solution: Opens the Date Range filter date picker and closes it. - -```javascript -I.click("(//*[self::li and .//p[normalize-space(.)=\"Date Range\"]]//input[@placeholder=\"Select range\"])[1]"); -I.pressKey("Escape"); -``` - - -### SUCCEEDED: Drill select option: PowerSelect Filters Changed by dropdown - -Solution: Opens the Changed by PowerSelect filter and selects a user. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Changed by\"]]//*[self::div and @role=\"button\" and contains(.,\"Select user\")]"); -I.click("//ul[@role=\"listbox\"]//li[@role=\"option\" and contains(.,\"Denys Kuchma (me)\")]"); -``` - - -### SUCCEEDED: Drill clear option: PowerSelect Filters selected Changed by dropdown - -Solution: Clicks the selected Changed by PowerSelect filter value to clear it. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Changed by\"]]//*[self::div and @role=\"button\" and contains(.,\"(me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") -``` - - -### SUCCEEDED: Drill select option: PowerSelect Filters State dropdown - -Solution: Opens the State PowerSelect filter and selects an option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"State\"]]//*[self::div and @role=\"button\" and contains(.,\"Select State\")]"); -I.click({"role":"option","text":"automated"}); -``` - - -### SUCCEEDED: Drill change option: PowerSelect Filters selected State dropdown - -Solution: Opens the selected State PowerSelect filter and selects another option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"State\"]]//*[self::div and @role=\"button\" and contains(.,\"manual\")]"); -I.click({"role":"option","text":"automated"}); -``` - - -### SUCCEEDED: Drill clear option: PowerSelect Filters selected State dropdown - -Solution: Clicks the selected State PowerSelect filter value to clear it. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"State\"]]//*[self::div and @role=\"button\" and contains(.,\"manual\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") -``` - - -### SUCCEEDED: Drill select option: PowerSelect Filters Tag multiselect - -Solution: Opens the Tag PowerSelect multiselect filter and selects an option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"]]//input[@placeholder=\"Select Tag\"]"); -I.click({"role":"option","text":"@tag1"}); -``` - - -### SUCCEEDED: Drill toggle option: PowerSelect Filters selected Tag multiselect - -Solution: Opens the selected Tag PowerSelect multiselect filter and toggles an option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"]]//*[self::div and @role=\"button\" and contains(.,\"@tag1\") and contains(.,\"@tag2\") and contains(.,\"@tag3\")]"); -I.click({"role":"option","text":"@tag2"}); -``` - - -### SUCCEEDED: Drill type option: PowerSelect Filters selected Tag multiselect - -Solution: Focuses the selected Tag PowerSelect multiselect search input and confirms a typed option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"] and .//*[contains(.,\"@tag1\")]]//input[contains(@class,\"ember-power-select-trigger-multiple-input\")]"); -I.type("normal{Enter}"); -``` - - -### SUCCEEDED: Drill remove option: PowerSelect Filters selected Tag multiselect - -Solution: Clicks a selected Tag PowerSelect multiselect remove control. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Tag\"] and .//*[contains(.,\"@tag1\")]]//span[@role=\"button\" and @aria-label=\"remove element\"]") -``` - - -### SUCCEEDED: Drill select option: PowerSelect Filters Priority multiselect - -Solution: Opens the Priority PowerSelect multiselect filter and selects an option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Priority\"]]//input[@placeholder=\"Select Priority\"]"); -I.click("//li[@role=\"option\" and contains(.,\"high\")]"); -``` - - -### SUCCEEDED: Drill open selected: PowerSelect Filters selected Priority multiselect - -Solution: Opens the selected Priority PowerSelect multiselect filter. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Priority\"]]//*[self::div and @role=\"button\" and contains(.,\"low\") and contains(.,\"normal\") and contains(.,\"critical\")]") -``` - - -### SUCCEEDED: Drill remove option: PowerSelect Filters selected Priority multiselect - -Solution: Clicks a selected Priority PowerSelect multiselect remove control. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Priority\"] and .//*[contains(.,\"low\")]]//span[@role=\"button\" and @aria-label=\"remove element\"]") -``` - - -### SUCCEEDED: Drill select option: PowerSelect Filters Assigned to dropdown - -Solution: Opens the Assigned to PowerSelect filter and selects a user. - -```javascript -I.click({"role":"button","text":"Select Assignee"}); -I.click({"role":"option","text":"Denys Kuchma (me)"}); -``` - - -### SUCCEEDED: Drill clear option: PowerSelect Filters selected Assigned to dropdown - -Solution: Clicks the selected Assigned to PowerSelect filter value to clear it. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Assigned to\"]]//*[self::div and @role=\"button\" and contains(.,\"(me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") -``` - - -### SUCCEEDED: Drill clear option: PowerSelect Filters selected Field dropdown - -Solution: Clicks the selected Field PowerSelect filter value to clear it. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Field\"]]//*[self::div and @role=\"button\" and contains(.,\"Test\")]//span[contains(@class,\"ember-power-select-clear-btn\")]") -``` - - -### SUCCEEDED: Drill change option: PowerSelect Filters selected Field dropdown - -Solution: Opens the selected Field PowerSelect filter, searches, and selects another option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Field\"]]//*[self::div and @role=\"button\" and contains(.,\"Test\")]"); -I.fillField("//div[contains(@class,\"ember-basic-dropdown-content\") and not(contains(@style,\"display: none\"))]//input[@role=\"combobox\"]", "test"); -I.pressKey("Enter"); -``` - - -### SUCCEEDED: Drill search option: PowerSelect Filters Value dropdown - -Solution: Opens the Value PowerSelect filter, searches, and confirms the typed option. - -```javascript -I.click("//*[self::li and .//p[normalize-space(.)=\"Value\"]]//*[self::div and @role=\"button\" and contains(.,\"Select value\")]"); -I.fillField("//div[contains(@class,\"ember-basic-dropdown-content\") and not(contains(@style,\"display: none\"))]//input[@role=\"combobox\"]", "test"); -I.pressKey("Enter"); -``` - - -### SUCCEEDED: Drill click: PowerSelect Filters Apply button - -Solution: Clicks Apply to apply the selected filters. - -```javascript -I.click("(//*[self::button and contains(@class,\"primary-btn\") and normalize-space(.)=\"Apply\"])[1]") -``` - - -### SUCCEEDED: Drill click: PowerSelect Filters Cancel button - -Solution: Clicks Cancel to discard the filter changes. - -```javascript -I.click("(//*[self::button and contains(@class,\"secondary-btn\") and normalize-space(.)=\"Cancel\"])[1]") -``` diff --git a/exp_custom/PowerSelect_Input.md b/exp_custom/PowerSelect_Input.md deleted file mode 100644 index 55909bf..0000000 --- a/exp_custom/PowerSelect_Input.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Powerselect%3A%3AInput -title: Testomat.io -summary: Curated PowerSelect::Input interactions for search, single select, error states, and tag multiselect inputs. ---- -### SUCCEEDED: Drill fill: PowerSelect::Input medium Search input - -Solution: Fills the medium Search input. - -```javascript -I.fillField("input[type=\"search\"][placeholder=\"Search\"].size-md", "text") -``` - - -### SUCCEEDED: Drill clear: PowerSelect::Input medium Search input - -Solution: Clears the typed medium Search input value. - -```javascript -I.clearField("input[type=\"search\"][placeholder=\"Search\"].size-md") -``` - - -### SUCCEEDED: Drill fill: PowerSelect::Input large Search input - -Solution: Fills the large Search input. - -```javascript -I.fillField("input[type=\"search\"][placeholder=\"Search\"].size-lg", "text") -``` - - -### SUCCEEDED: Drill clear: PowerSelect::Input large Search input - -Solution: Clears the typed large Search input value. - -```javascript -I.clearField("input[type=\"search\"][placeholder=\"Search\"].size-lg") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input User dropdown - -Solution: Opens the User PowerSelect::Input dropdown. - -```javascript -I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select user\")])[1]") -``` - - -### SUCCEEDED: Drill clear option: PowerSelect::Input selected User dropdown - -Solution: Clicks the selected User PowerSelect::Input value to clear it. - -```javascript -I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"(Me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[1]") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input Name of select dropdown - -Solution: Opens the Name of select PowerSelect::Input dropdown. - -```javascript -I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select item\")])[1]") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input selected Name of select dropdown - -Solution: Opens the selected Name of select PowerSelect::Input dropdown. - -```javascript -I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")])[1]") -``` - - -### SUCCEEDED: Drill clear option: PowerSelect::Input selected Name of select dropdown - -Solution: Clicks the selected Name of select PowerSelect::Input value to clear it. - -```javascript -I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[1]") -``` - - -### SUCCEEDED: Drill clear option: PowerSelect::Input error Name of select dropdown - -Solution: Clicks the selected error-state Name of select PowerSelect::Input value to clear it. - -```javascript -I.click("(//*[normalize-space(.)=\"Name of select (Error State)\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[1]") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input Tags multiselect - -Solution: Opens the Tags PowerSelect::Input multiselect. - -```javascript -I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select a requirement source\")])[1]") -``` - - -### SUCCEEDED: Drill type option: PowerSelect::Input Tags multiselect searchbox - -Solution: Focuses the Tags PowerSelect::Input multiselect searchbox and types a tag. - -```javascript -I.click("(//*[normalize-space(.)=\"Tags\"]/following::input[@type=\"search\" and @placeholder=\"Select a requirement source\"])[1]"); -I.type("@tag1{Enter}"); -``` - - -### SUCCEEDED: Drill remove option: PowerSelect::Input selected Tags multiselect - -Solution: Clicks a selected Tags PowerSelect::Input multiselect remove control. - -```javascript -I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"@tag1\") and contains(.,\"@tag2\")]//span[@role=\"button\" and @aria-label=\"remove element\"])[1]") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input large User dropdown - -Solution: Opens the large User PowerSelect::Input dropdown. - -```javascript -I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select user\")])[2]") -``` - - -### SUCCEEDED: Drill clear option: PowerSelect::Input large selected User dropdown - -Solution: Clicks the selected large User PowerSelect::Input value to clear it. - -```javascript -I.click("(//*[normalize-space(.)=\"User\"]/following::*[self::div and @role=\"button\" and contains(.,\"(Me)\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[2]") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input large Name of select dropdown - -Solution: Opens the large Name of select PowerSelect::Input dropdown. - -```javascript -I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select item\")])[3]") -``` - - -### SUCCEEDED: Drill clear option: PowerSelect::Input large selected Name of select dropdown - -Solution: Clicks the selected large Name of select PowerSelect::Input value to clear it. - -```javascript -I.click("(//*[normalize-space(.)=\"Name of select\"]/following::*[self::div and @role=\"button\" and contains(.,\"Item 2\")]//span[contains(@class,\"ember-power-select-clear-btn\")])[3]") -``` - - -### SUCCEEDED: Drill click: PowerSelect::Input large Tags multiselect - -Solution: Opens the large Tags PowerSelect::Input multiselect. - -```javascript -I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"Select a requirement source\")])[2]") -``` - - -### SUCCEEDED: Drill remove option: PowerSelect::Input large selected Tags multiselect - -Solution: Clicks a selected large Tags PowerSelect::Input multiselect remove control. - -```javascript -I.click("(//*[normalize-space(.)=\"Tags\"]/following::*[self::div and @role=\"button\" and contains(.,\"@tag1\") and contains(.,\"@tag2\")]//span[@role=\"button\" and @aria-label=\"remove element\"])[2]") -``` diff --git a/exp_custom/PowerSelect_Multiple.md b/exp_custom/PowerSelect_Multiple.md deleted file mode 100644 index 55892da..0000000 --- a/exp_custom/PowerSelect_Multiple.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=PowerSelect%20Multiple -title: Testomat.io -summary: Curated PowerSelect Multiple interactions for AssignMultiple. ---- -### SUCCEEDED: Drill select all: assign users multiselect - -Solution: Clicks the Select All button for the assign users multiselect. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Select All\" and not(.//svg)]") -``` - - -### SUCCEEDED: Drill select option: assign users multiselect - -Solution: Opens the assign users multiselect and selects a user. - -```javascript -I.click({"role":"searchbox","text":"Assign Users"}); -I.click({"role":"option","text":"Denys Kuchma"}); -``` - - -### SUCCEEDED: Drill remove selected users: assign users multiselect - -Solution: Clicks the Remove assign users button for the assign users multiselect. - -```javascript -I.click("//*[self::button and contains(@class,\"third-btn\") and contains(@class,\"btn-text-and-icon\") and contains(@class,\"btn-md\") and normalize-space(.)=\"Remove assign users\" and not(.//svg)]") -``` diff --git a/exp_custom/PowerSelect_Typeahead.md b/exp_custom/PowerSelect_Typeahead.md deleted file mode 100644 index 3f0673f..0000000 --- a/exp_custom/PowerSelect_Typeahead.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=PowerSelect%20Typeahead -title: Testomat.io -summary: Curated PowerSelect Typeahead interactions for empty and selected Group type inputs. ---- -### SUCCEEDED: Drill search option: PowerSelect Typeahead empty Group type combobox - -Solution: Focuses the empty Group type typeahead combobox, searches, and selects an option. - -```javascript -I.click("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[1]"); -I.fillField("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[1]", "Build"); -I.pressKey("ArrowDown"); -I.pressKey("Enter"); -``` - - -### SUCCEEDED: Drill change option: PowerSelect Typeahead selected Group type combobox - -Solution: Focuses the selected Group type typeahead combobox, searches, and selects another option. - -```javascript -I.click("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[2]"); -I.fillField("(//label[normalize-space(.)=\"Group type\"]/following::input[@role=\"combobox\"])[2]", "Release"); -I.pressKey("ArrowDown"); -I.pressKey("Enter"); -``` diff --git a/exp_custom/Search_Input.md b/exp_custom/Search_Input.md deleted file mode 100644 index b3d481a..0000000 --- a/exp_custom/Search_Input.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Search%20Input -title: Testomat.io -summary: Curated search input interaction only. ---- -### SUCCEEDED: Drill fill: Search input "Search" - -Solution: Clicks the search input and fills it with a search query. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Search"]]//input[@placeholder="Search"]') -I.fillField('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Search"]]//input[@placeholder="Search"]', 'test') -``` diff --git a/exp_custom/Tabs.md b/exp_custom/Tabs.md deleted file mode 100644 index 4f6ab92..0000000 --- a/exp_custom/Tabs.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -url: /projects/test-d6178/components/?m=false&s=Tabs -title: Testomat.io -summary: Curated tab interactions from the Tabs component showcase. ---- -### SUCCEEDED: Drill click: Inactive tab "Tab text" plain text - -Solution: Clicks the inactive plain text tab. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and not(.//*[local-name()="svg"]) and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Inactive tab "Tab text" leading icon - -Solution: Clicks the inactive tab with a leading autorenew icon. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Inactive tab "Tab text" trailing action icon - -Solution: Clicks the inactive tab with a copy action icon. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//button[contains(@class,"third-btn")] and not(.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Inactive tab "Tab text" leading and trailing action icons - -Solution: Clicks the inactive tab with a leading autorenew icon and copy action icon. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and .//button[contains(@class,"third-btn")] and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Inactive tab "Tab text 1" leading icon counter - -Solution: Clicks the inactive tab with a counter. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[contains(@class,"new-counter")] and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Inactive tab "Tab text" leading icon with run status icons - -Solution: Clicks the inactive tab with run status indicators. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Inactive tab"]]//li[@role="tab" and not(contains(@class,"ember-tabs__tab--selected")) and .//*[contains(@class,"run-status")]]') -``` - - -### SUCCEEDED: Drill click: Active tab "Tab text" plain text - -Solution: Clicks the active plain text tab. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and not(.//*[local-name()="svg"]) and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Active tab "Tab text" leading icon - -Solution: Clicks the active tab with a leading autorenew icon. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and not(.//button[contains(@class,"third-btn")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Active tab "Tab text" trailing action icon - -Solution: Clicks the active tab with a copy action icon. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//button[contains(@class,"third-btn")] and not(.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")]) and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Active tab "Tab text" leading and trailing action icons - -Solution: Clicks the active tab with a leading autorenew icon and copy action icon. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[local-name()="svg" and contains(@class,"md-icon-autorenew")] and .//button[contains(@class,"third-btn")] and not(.//*[contains(@class,"new-counter")]) and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Active tab "Tab text 1" leading icon counter - -Solution: Clicks the active tab with a counter. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[contains(@class,"new-counter")] and not(.//*[contains(@class,"run-status")])]') -``` - - -### SUCCEEDED: Drill click: Active tab "Tab text" leading icon with run status icons - -Solution: Clicks the active tab with run status indicators. - -```javascript -I.click('//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)="Active tab"]]//li[@role="tab" and contains(@class,"ember-tabs__tab--selected") and .//*[contains(@class,"run-status")]]') -``` diff --git a/src/ai/driller.ts b/src/ai/driller.ts index 4a3c9d0..4bc0fc3 100644 --- a/src/ai/driller.ts +++ b/src/ai/driller.ts @@ -9,6 +9,7 @@ import type { KnowledgeTracker } from '../knowledge-tracker.ts'; import { Observability } from '../observability.ts'; import { Plan, Test, TestResult } from '../test-plan.ts'; import { collectInteractiveNodes } from '../utils/aria.ts'; +import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole } from '../utils/html.ts'; import { HooksRunner } from '../utils/hooks-runner.ts'; import { createDebug, tag } from '../utils/logger.ts'; import { loop, pause } from '../utils/loop.ts'; @@ -35,6 +36,7 @@ interface ComponentInfo { text: string; tag: string; classes: string[]; + attrs: Record; context: string; variant: string; placeholder: string; @@ -126,6 +128,11 @@ export class Driller extends TaskAgent implements Agent { - Never use data-explorbot-eidx in locators - Never use container locators in recorded code - Prefer one-argument locators or self-contained XPath/CSS locators + - Before choosing a locator, identify what makes the current component semantically different from its siblings + - If siblings look similar, use text, aria labels, icon clues, variant hints, role, navigation behavior, border/outline classes, or state to target the exact component + - Component size alone is not enough to choose a sibling instead of the current component, but if the current drilling target differs only by size, keep that exact size variant and record it + - When an icon is visible, infer its purpose from aria labels, title, class names, SVG names, or nearby text, and mention that purpose in drill_record + - If there is no meaningful difference between matching siblings, pick the first matching component and say that no semantic difference was found - If the component is decorative, duplicated beyond recovery, or not drillable, call drill_skip ${component ? `- Current component: ${component.name} (${component.role})` : ''} @@ -269,6 +276,7 @@ export class Driller extends TaskAgent implements Agent { text, tag: element.tag, classes: element.filteredClasses, + attrs: element.attrs, context, variant, placeholder: element.attrs.placeholder || '', @@ -392,6 +400,7 @@ export class Driller extends TaskAgent implements Agent { Text: ${component.text || '-'} Context: ${component.context || '-'} Variant: ${component.variant || '-'} + Differentiators: ${formatComponentDifferentiators(component)} Matching ARIA candidates: ${ariaMatches} @@ -422,6 +431,9 @@ export class Driller extends TaskAgent implements Agent { 8. If the component is not drillable, call drill_skip 9. If similar components exist, use Context and Variant to distinguish this exact variant instead of skipping immediately 10. Do not switch to a sibling with the same text but different variant or size. Stay anchored to the current component's Preferred locator, Context, and Variant. + 10a. If same-text components differ only by size, still record the current size variant instead of treating it as a duplicate. + 11. In drill_record result, describe the component precisely: color/variant, border/outline, icon purpose, text, role, navigation behavior, state, and why this component was chosen over similar siblings. + 12. Prefer results like "Clicked the red outlined button with a leading refresh icon." over generic results like "Button clicked." `; } @@ -433,6 +445,7 @@ export class Driller extends TaskAgent implements Agent { Continue drilling component: ${component.name} Context: ${component.context || '-'} Variant: ${component.variant || '-'} + Differentiators: ${formatComponentDifferentiators(component)} If the component moved or disappeared, reassess using the current ARIA tree. @@ -448,7 +461,7 @@ export class Driller extends TaskAgent implements Agent { description: 'Record a reusable interaction for the current component. Use only when the code is reusable and does not depend on a container locator.', inputSchema: z.object({ action: z.string().describe('Action performed, for example click, fill, select, open, toggle'), - result: z.string().describe('What happened after the interaction'), + result: z.string().describe('What happened after the interaction, including the component differentiators used: icon purpose, color/variant, border/outline, text, role, navigation behavior, or state'), code: z.string().describe('Reusable CodeceptJS code that worked'), }), execute: async ({ action, result, code }) => { @@ -587,7 +600,7 @@ export class Driller extends TaskAgent implements Agent { const successfulInteractions = results.filter((result) => result.result === 'success' && result.code); for (const interaction of successfulInteractions) { - await experienceTracker.saveSuccessfulResolution(state, `Drill ${interaction.action}: ${interaction.component}`, interaction.code!, interaction.description); + await experienceTracker.saveSuccessfulResolution(state, formatExperienceTitle(interaction), interaction.code!, interaction.description); } if (successfulInteractions.length > 0) { @@ -655,68 +668,40 @@ export class Driller extends TaskAgent implements Agent { private async getVisibleOverlayHtml(): Promise { const page = this.explorer.playwrightHelper.page; - return page.evaluate(() => { - const selectors = [ - '.flatpickr-calendar.open', - '.flatpickr-calendar:not([style*="display: none"]):not([style*="visibility: hidden"])', - '.ember-attacher:not([style*="display: none"]):not([style*="visibility: hidden"])', - '[role="dialog"]', - '[role="listbox"]', - '[role="menu"]', - '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])', - '[x-placement]:not([style*="display: none"]):not([style*="visibility: hidden"])', - '.dropdown-menu:not([style*="display: none"]):not([style*="visibility: hidden"])', - '.popover:not([style*="display: none"]):not([style*="visibility: hidden"])', - ]; - - function isVisible(element: Element): boolean { - const html = element as HTMLElement; - const style = window.getComputedStyle(html); - const rect = html.getBoundingClientRect(); - if (rect.width === 0 && rect.height === 0) return false; - if (style.display === 'none' || style.visibility === 'hidden') return false; - if (Number.parseFloat(style.opacity || '1') < 0.1) return false; - return true; - } - - const overlays: string[] = []; - const seen = new Set(); - for (const selector of selectors) { - for (const element of Array.from(document.querySelectorAll(selector))) { - if (seen.has(element)) continue; - seen.add(element); - if (!isVisible(element)) continue; - const text = (element.textContent || '').replace(/\s+/g, ' ').trim(); - const interactiveCount = element.querySelectorAll('button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [tabindex]').length; - if (interactiveCount === 0 && text.length === 0) continue; - overlays.push((element as HTMLElement).outerHTML.slice(0, 6000)); - } + return page.evaluate( + ({ extractorSource, config }) => { + const extract = new Function(`return ${extractorSource}`)() as (config: any) => string; + return extract(config); + }, + { + extractorSource: getVisibleOverlayHtmlExtractorSource(), + config: { + interactiveContentSelector: HTML_SELECTORS.interactiveContent, + limits: HTML_EXTRACTION_LIMITS, + overlaySelectors: HTML_SELECTORS.semanticOverlays, + visibilityLimits: HTML_VISIBILITY_LIMITS, + }, } - - return overlays.slice(0, 3).join('\n\n--- overlay ---\n\n'); - }); + ); } private async getComponentScopeHtml(component: ComponentInfo, originalState: ActionResult): Promise { const page = this.explorer.playwrightHelper.page; - const scopedHtml = await page.evaluate((eidx: string) => { - const element = document.querySelector(`[data-explorbot-eidx="${eidx}"]`); - if (!element) return ''; - - function countInteractive(node: Element): number { - return node.querySelectorAll('button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]').length; - } - - let current = element.parentElement; - while (current) { - const count = countInteractive(current); - if (count > 0 && count <= 16) return current.outerHTML.slice(0, 8000); - current = current.parentElement; + const scopedHtml = await page.evaluate( + ({ eidx, extractorSource, config }) => { + const extract = new Function(`return ${extractorSource}`)() as (eidx: string, config: any) => string; + return extract(eidx, config); + }, + { + eidx: component.eidx, + extractorSource: getComponentScopeHtmlExtractorSource(), + config: { + eidxAttr: EXPLORBOT_ATTRS.eidx, + interactiveControlSelector: HTML_SELECTORS.interactiveControl, + limits: HTML_EXTRACTION_LIMITS, + }, } - - if (element instanceof HTMLElement) return element.outerHTML.slice(0, 8000); - return ''; - }, component.eidx); + ); if (scopedHtml) return scopedHtml; return await originalState.combinedHtml(); @@ -800,22 +785,7 @@ function formatAriaNode(node: Record): string { } function inferRole(element: WebElement): string { - if (element.tag === 'iframe' && element.variantHints.includes('code-editor')) return 'code-editor'; - if (element.role) return element.role.toLowerCase(); - const explicitRole = element.attrs.role; - if (explicitRole) return explicitRole.toLowerCase(); - if (element.tag === 'a' && element.attrs.href) return 'link'; - if (element.tag === 'button') return 'button'; - if (element.tag === 'iframe') return 'iframe'; - if (element.tag === 'select') return 'combobox'; - if (element.tag === 'textarea') return 'textbox'; - if (element.tag === 'input') { - const type = (element.attrs.type || 'text').toLowerCase(); - if (type === 'checkbox') return 'checkbox'; - if (type === 'radio') return 'radio'; - return 'textbox'; - } - return element.tag; + return inferHtmlRole(element); } function normalized(value: string): string { @@ -847,8 +817,8 @@ function buildCanonicalClickCode(component: ComponentInfo): string { if (component.tag === 'a') return ''; if (component.tag === 'iframe' || component.role === 'code-editor') return buildEmbeddedFrameCode(component); - const scopedCode = buildScopedFreestyleClickCode(component); - if (scopedCode) return scopedCode; + const semanticCode = buildSemanticClickCode(component); + if (semanticCode) return semanticCode; const variantHints = parseVariantHints(component.variant); const classSelector = buildClassSelector(component.tag, component.classes); @@ -879,84 +849,71 @@ function buildCanonicalClickCode(component: ComponentInfo): string { return `I.click(${JSON.stringify(selector)})`; } -function buildScopedFreestyleClickCode(component: ComponentInfo): string { - if (!component.context) return ''; +function buildSemanticClickCode(component: ComponentInfo): string { + const conditions: string[] = []; + const target = component.tag && /^[a-z][a-z0-9-]*$/i.test(component.tag) ? component.tag : '*'; + conditions.push(`self::${target}`); - const scope = `//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)=${xpathLiteral(component.context)}]]`; - if (component.role === 'tab') { - const tabCondition = buildTabVariantXPathCondition(component); - return `I.click(${JSON.stringify(`${scope}//li[@role="tab"${tabCondition}]`)})`; - } + const role = component.attrs.role || ''; + if (role) conditions.push(`@role=${xpathLiteral(role)}`); - if (component.role === 'switch') { - const enabled = component.classes.includes('cursor-not-allowed') ? '' : ' and not(contains(@class,"cursor-not-allowed"))'; - return `I.click(${JSON.stringify(`${scope}//button[@role="switch"${enabled}]`)})`; - } + const labelledBy = component.attrs['aria-labelledby'] || ''; + if (labelledBy) conditions.push(`@aria-labelledby=${xpathLiteral(labelledBy)}`); - if (component.tag === 'input' || component.role === 'textbox' || component.role === 'searchbox') { - const placeholder = component.placeholder; - if (placeholder) return `I.click(${JSON.stringify(`${scope}//input[@placeholder=${xpathLiteral(placeholder)}]`)})`; - if (component.classes.length > 0) { - const classConditions = component.classes - .slice(0, 4) - .map((cls) => `contains(@class,${xpathLiteral(cls)})`) - .join(' and '); - return `I.click(${JSON.stringify(`${scope}//input[${classConditions}]`)})`; + const label = component.attrs['aria-label'] || component.attrs.title || component.attrs.name || ''; + if (label) conditions.push(`@${getLabelAttrName(component)}=${xpathLiteral(label)}`); + + const placeholder = component.placeholder || component.attrs.placeholder || ''; + if (placeholder) conditions.push(`@placeholder=${xpathLiteral(placeholder)}`); + + const stateAttrs = ['aria-checked', 'aria-pressed', 'aria-expanded', 'aria-selected', 'checked']; + for (const attr of stateAttrs) { + const value = component.attrs[attr]; + if (attr === 'checked') { + if (value === undefined) continue; + conditions.push('@checked'); + continue; } + if (!value) continue; + conditions.push(`@${attr}=${xpathLiteral(value)}`); } - return ''; + if (component.text && role && !label && !labelledBy) { + conditions.push(`normalize-space(.)=${xpathLiteral(component.text)}`); + } + + if (conditions.length <= 1) return ''; + if (!role && !label && !labelledBy && !placeholder) return ''; + + return `I.click(${JSON.stringify(`//*[${conditions.join(' and ')}]`)})`; +} + +function getLabelAttrName(component: ComponentInfo): string { + if (component.attrs['aria-label']) return 'aria-label'; + if (component.attrs.title) return 'title'; + return 'name'; } function buildEmbeddedFrameCode(component: ComponentInfo): string { const src = component.html.match(/\ssrc=["']([^"']+)["']/i)?.[1] || ''; - const sourceIndex = component.html.match(/\sdata-explorbot-frame-source-index=["'](\d+)["']/i)?.[1] || ''; + const sourceIndex = getHtmlAttrValue(component.html, EXPLORBOT_ATTRS.frameSourceIndex); const srcCondition = src ? `contains(@src,${xpathLiteral(src)})` : ''; - let scope = ''; - if (component.context) { - scope = `//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage ") and .//*[contains(concat(" ", normalize-space(@class), " "), " FreestyleUsage-title ") and normalize-space(.)=${xpathLiteral(component.context)}]]`; - } let iframeLocator = '//iframe'; - if (scope && !sourceIndex) iframeLocator = `${scope}//iframe`; if (srcCondition) iframeLocator += `[${srcCondition}]`; if (sourceIndex) iframeLocator = `(${iframeLocator})[${sourceIndex}]`; - let editorLocator = 'body'; let text = 'test'; if (component.variant.includes('code-editor')) { - editorLocator = '.monaco-editor'; text = 'const value = "test";'; } - return [`I.switchTo(${JSON.stringify(iframeLocator)})`, `I.click(${JSON.stringify(editorLocator)})`, `I.type(${JSON.stringify(text)})`, 'I.switchTo()'].join('\n'); + return [`I.switchTo(${JSON.stringify(iframeLocator)})`, 'I.click("body")', `I.type(${JSON.stringify(text)})`, 'I.switchTo()'].join('\n'); } -function buildTabVariantXPathCondition(component: ComponentInfo): string { - const html = component.html.toLowerCase(); - const hasAutorenew = html.includes('md-icon-autorenew'); - const hasPlay = html.includes('md-icon-play'); - const hasCopyButton = html.includes('third-btn') || html.includes('md-icon-content-copy'); - const hasCounter = html.includes('new-counter'); - const hasStatus = html.includes('run-status'); - const conditions: string[] = []; - - if (hasStatus) conditions.push('.//*[contains(@class,"run-status")]'); - else conditions.push('not(.//*[contains(@class,"run-status")])'); - - if (hasCounter) conditions.push('.//*[contains(@class,"new-counter")]'); - else conditions.push('not(.//*[contains(@class,"new-counter")])'); - - if (hasCopyButton) conditions.push('.//button[contains(@class,"third-btn")]'); - else conditions.push('not(.//button[contains(@class,"third-btn")])'); - - if (hasAutorenew) conditions.push('.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")]'); - else conditions.push('not(.//*[local-name()="svg" and contains(@class,"md-icon-autorenew")])'); - - if (hasPlay) conditions.push('.//*[local-name()="svg" and contains(@class,"md-icon-play")]'); - else conditions.push('not(.//*[local-name()="svg" and contains(@class,"md-icon-play")])'); - - return conditions.length > 0 ? ` and ${conditions.join(' and ')}` : ''; +function getHtmlAttrValue(html: string, attr: string): string { + const escapedAttr = attr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return html.match(new RegExp(`\\s${escapedAttr}=["']([^"']+)["']`, 'i'))?.[1] || ''; } function formatVariant(variantHints: string[]): string { @@ -973,6 +930,55 @@ function formatComponentName(role: string, label: string, context: string, varia return parts.join(' ').trim(); } +function formatComponentDifferentiators(component: ComponentInfo): string { + const details: string[] = []; + const classes = component.classes.join(' ').toLowerCase(); + const variantHints = parseVariantHints(component.variant); + + if (component.text) details.push(`text "${truncate(component.text, 48)}"`); + if (component.context) details.push(`context "${truncate(component.context, 48)}"`); + if (component.placeholder) details.push(`placeholder "${truncate(component.placeholder, 48)}"`); + addAriaDifferentiators(component, details); + if (component.variant) details.push(`variant hints: ${component.variant}`); + if (component.role) details.push(`role ${component.role}`); + if (component.tag === 'a') details.push('navigates'); + if (component.disabled) details.push('disabled state'); + if (variantHints.has('has-icon') || variantHints.has('leading-icon') || variantHints.has('trailing-icon') || variantHints.has('icon-only') || variantHints.has('double-icon')) { + details.push(`icon clues: ${formatIconClues(component)}`); + } + if (classes.includes('border') || variantHints.has('outline')) details.push('border or outline styling'); + if (classes.includes('red') || classes.includes('danger')) details.push('red/danger styling'); + if (classes.includes('green') || classes.includes('success')) details.push('green/success styling'); + if (classes.includes('primary')) details.push('primary styling'); + if (classes.includes('secondary')) details.push('secondary styling'); + + return details.length > 0 ? details.join('; ') : 'No clear semantic difference from similar components.'; +} + +function addAriaDifferentiators(component: ComponentInfo, details: string[]): void { + const label = component.attrs['aria-label'] || component.attrs.title || component.attrs.name || ''; + if (label) details.push(`accessible label "${truncate(label, 48)}"`); + + const labelledBy = component.attrs['aria-labelledby'] || ''; + if (labelledBy) details.push(`aria-labelledby ${labelledBy}`); + + const stateAttrs = ['aria-checked', 'aria-pressed', 'aria-expanded', 'aria-selected']; + for (const attr of stateAttrs) { + const value = component.attrs[attr]; + if (!value) continue; + details.push(`${attr} ${value}`); + } + + if (component.attrs.checked !== undefined) details.push('checked state'); +} + +function formatIconClues(component: ComponentInfo): string { + const iconClasses = component.classes.filter((cls) => /(icon|svg|refresh|reload|renew|copy|play|pause|edit|delete|trash|search|plus|minus|close|check|arrow|calendar|date)/i.test(cls)); + if (iconClasses.length > 0) return iconClasses.slice(0, 5).join(', '); + if (component.variant) return component.variant; + return 'icon present, purpose not explicit'; +} + function normalizeInteractionResult(component: ComponentInfo, action: string, result: string): string { const value = result.trim(); if (!value) return fallbackInteractionResult(component, action); @@ -988,14 +994,27 @@ function normalizeInteractionResult(component: ComponentInfo, action: string, re return value; } +function formatExperienceTitle(interaction: InteractionResult): string { + const descriptionTitle = interaction.description + .split(/[.;]/)[0] + .replace(/\s+/g, ' ') + .trim(); + + if (descriptionTitle.length > 0) return truncate(descriptionTitle, 90); + + const action = interaction.action ? capitalize(interaction.action) : 'Interact with'; + return truncate(`${action} ${interaction.component}`, 90); +} + function fallbackInteractionResult(component: ComponentInfo, action: string): string { const role = component.role || component.tag; const label = component.text ? `"${truncate(component.text, 40)}"` : `the ${role}`; const variant = component.variant ? ` (${component.variant})` : ''; - if (action === 'click') return `Clicked ${label}${variant}.`; - if (action === 'pressKey') return `Pressed key on ${label}${variant}.`; - if (action === 'form') return `Submitted interaction for ${label}${variant}.`; - return `${capitalize(action)} executed for ${label}${variant}.`; + const details = formatComponentDifferentiators(component); + if (action === 'click') return `Clicked ${label}${variant}; differentiators: ${details}.`; + if (action === 'pressKey') return `Pressed key on ${label}${variant}; differentiators: ${details}.`; + if (action === 'form') return `Submitted interaction for ${label}${variant}; differentiators: ${details}.`; + return `${capitalize(action)} executed for ${label}${variant}; differentiators: ${details}.`; } function hasContainerLocator(code: string): boolean { @@ -1118,15 +1137,15 @@ function isDrillableElement(element: WebElement): boolean { function isNestedCompositeControl(element: WebElement): boolean { const role = (element.role || element.attrs.role || element.tag).toLowerCase(); - if (COMPOSITE_TARGET_ROLES.has(role)) return false; + if (HTML_COMPOSITE_TARGET_ROLES.has(role)) return false; if (!isInteractiveElement(element)) return false; - return element.areaHints.some((hint) => COMPOSITE_AREA_HINTS.has(hint)); + return element.areaHints.some((hint) => HTML_COMPOSITE_AREA_HINTS.has(hint)); } function isSemanticFormControl(element: WebElement): boolean { const role = (element.role || element.attrs.role || element.tag).toLowerCase(); - if (element.tag === 'input' || element.tag === 'select' || element.tag === 'textarea') return true; - return FORM_CONTROL_ROLES.has(role); + if (HTML_FORM_CONTROL_TAGS.has(element.tag)) return true; + return HTML_FORM_CONTROL_ROLES.has(role); } function isButtonLikeElement(element: WebElement): boolean { @@ -1140,18 +1159,13 @@ function isInteractiveElement(element: WebElement): boolean { if (element.tag === 'button') return true; if (element.tag === 'a' && element.attrs.href) return true; if (element.tag === 'iframe') return true; - if (element.tag === 'input' || element.tag === 'select' || element.tag === 'textarea') return true; + if (HTML_FORM_CONTROL_TAGS.has(element.tag)) return true; const role = (element.role || element.attrs.role || element.tag).toLowerCase(); - if (INTERACTIVE_ROLES.has(role)) return true; + if (HTML_INTERACTIVE_ROLES.has(role)) return true; if (element.attrs.contenteditable === 'true') return true; if (element.attrs.tabindex && Number(element.attrs.tabindex) >= 0) return true; if (element.attrs['aria-haspopup'] || element.attrs['aria-expanded'] || element.attrs['aria-controls']) return true; return false; } -const INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); -const FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']); -const COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']); -const COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']); - const drillLocatorRule = locatorRule.replace(/[\s\S]*?<\/context_simplification>/, '').trim(); diff --git a/src/explorer.ts b/src/explorer.ts index dec146a..9bb0f6d 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -18,8 +18,9 @@ import { StateManager } from './state-manager.js'; import { Test } from './test-plan.ts'; import { RequestStore } from './api/request-store.ts'; import { XhrCapture } from './api/xhr-capture.ts'; +import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from './utils/html.ts'; import { createDebug, log, tag } from './utils/logger.js'; -import { WebElement, extractElementData } from './utils/web-element.ts'; +import { WebElement } from './utils/web-element.ts'; declare global { namespace NodeJS { @@ -321,11 +322,11 @@ class Explorer { async getEidxInContainer(containerCss: string | null): Promise { const page = this.playwrightHelper.page; try { - const selector = containerCss ? `${containerCss} [data-explorbot-eidx]` : '[data-explorbot-eidx]'; + const selector = containerCss ? `${containerCss} [${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]` : `[${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]`; const elements = await page.locator(selector).all(); const result: string[] = []; for (const el of elements) { - const attr = await el.getAttribute('data-explorbot-eidx'); + const attr = await el.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx); if (attr) result.push(attr); } return result; @@ -343,7 +344,7 @@ class Explorer { const page = this.playwrightHelper.page; const base = container ? page.locator(container) : page; const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator); - return await el.first().getAttribute('data-explorbot-eidx'); + return await el.first().getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx); } catch (error) { if (this.isFatalBrowserError(error)) { tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`); @@ -732,20 +733,20 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s for (const [role, entries] of byRole) { try { const rawList = await page.getByRole(role).evaluateAll( - (domElements: Element[], [data, extractFnStr]: [Array<{ name: string; ref: string }>, string]) => { + (domElements: Element[], [data, extractFnStr, config]: [Array<{ name: string; ref: string }>, string, typeof ELEMENT_EXTRACTION_CONFIG]) => { const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; const results: any[] = []; let ariaIdx = 0; for (const el of domElements) { if (ariaIdx >= data.length) break; - el.setAttribute('data-explorbot-eidx', data[ariaIdx].ref); - const elData = extract(el); + el.setAttribute(config.attrs.eidx, data[ariaIdx].ref); + const elData = extract(el, config); if (elData) results.push(elData); ariaIdx++; } return results; }, - [entries, extractElementData.toString()] + [entries, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] ); for (const raw of rawList) { elements.push(WebElement.fromRawData(raw, role)); @@ -756,7 +757,7 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s } try { - const rawList = await page.locator('iframe').evaluateAll((domElements: Element[], extractFnStr: string) => { + const rawList = await page.locator('iframe').evaluateAll((domElements: Element[], [extractFnStr, config]: [string, typeof ELEMENT_EXTRACTION_CONFIG]) => { const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; const results: any[] = []; const sourceCounts: Record = {}; @@ -766,14 +767,14 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s const sourceKey = el.getAttribute('src') || ''; sourceCounts[sourceKey] ||= 0; sourceCounts[sourceKey]++; - const existing = el.getAttribute('data-explorbot-eidx'); - el.setAttribute('data-explorbot-eidx', existing || `iframe-${iframeIdx}`); - el.setAttribute('data-explorbot-frame-source-index', String(sourceCounts[sourceKey])); - const elData = extract(el); + const existing = el.getAttribute(config.attrs.eidx); + el.setAttribute(config.attrs.eidx, existing || `iframe-${iframeIdx}`); + el.setAttribute(config.attrs.frameSourceIndex, String(sourceCounts[sourceKey])); + const elData = extract(el, config); if (elData) results.push(elData); } return results; - }, extractElementData.toString()); + }, [getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]); for (const raw of rawList) { elements.push(WebElement.fromRawData(raw, 'iframe')); } diff --git a/src/utils/html.ts b/src/utils/html.ts index a73afa7..13c506e 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -83,6 +83,391 @@ const INTERACTIVE_EVENT_ATTRIBUTES = new Set(['onclick', 'onchange', 'onblur', ' const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', 'u-hidden', 'is-hidden', 'visually-hidden', 'sr-only', 'screen-reader-only', 'visuallyhidden', 'opacity-0']); +export const EXPLORBOT_ATTRS = { + area: 'data-explorbot-area', + context: 'data-explorbot-context', + eidx: 'data-explorbot-eidx', + frameSourceIndex: 'data-explorbot-frame-source-index', + variant: 'data-explorbot-variant', +} as const; + +export const HTML_SELECTORS = { + headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]', + interactiveContent: + 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [aria-label], [tabindex]', + interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]', + labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]', + semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]', + semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])'], +} as const; + +export const HTML_VISIBILITY_LIMITS = { + maxViewportOverlayRatio: 0.95, + minOpacity: 0.1, + minOverlayHeight: 40, + minOverlayWidth: 80, +} as const; + +export const HTML_EXTRACTION_LIMITS = { + componentScopeHtmlLength: 8000, + maxOverlayCount: 3, + maxScopeInteractiveCount: 16, + overlayHtmlLength: 6000, +} as const; + +export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'] as const; + +export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); +export const HTML_FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']); +export const HTML_COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']); +export const HTML_COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']); +export const HTML_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']); + +export function inferHtmlRole(data: { attrs: Record; role?: string; tag: string; variantHints?: string[] }): string { + if (data.tag === 'iframe' && data.variantHints?.includes('code-editor')) return 'code-editor'; + if (data.role) return data.role.toLowerCase(); + const explicitRole = data.attrs.role; + if (explicitRole) return explicitRole.toLowerCase(); + if (data.tag === 'a' && data.attrs.href) return 'link'; + if (data.tag === 'button') return 'button'; + if (data.tag === 'iframe') return 'iframe'; + if (data.tag === 'select') return 'combobox'; + if (data.tag === 'textarea') return 'textbox'; + if (data.tag === 'input') { + const type = (data.attrs.type || 'text').toLowerCase(); + if (type === 'checkbox') return 'checkbox'; + if (type === 'radio') return 'radio'; + return 'textbox'; + } + return data.tag; +} + +export const ELEMENT_EXTRACTION_CONFIG = { + attrs: EXPLORBOT_ATTRS, + codeEditorMarkers: CODE_EDITOR_MARKERS, + maxAreaDepth: 5, + maxContextLength: 120, + maxOuterHTMLLength: 2000, + maxTextLength: 80, + minOpacity: HTML_VISIBILITY_LIMITS.minOpacity, + selectors: { + headingLabel: HTML_SELECTORS.headingLabel, + labelLike: HTML_SELECTORS.labelLike, + semanticContextContainer: HTML_SELECTORS.semanticContextContainer, + }, +} as const; + +export type ElementExtractionConfig = typeof ELEMENT_EXTRACTION_CONFIG; +export type RawElementData = NonNullable>; +export type VisibleOverlayExtractionConfig = { + interactiveContentSelector: string; + limits: typeof HTML_EXTRACTION_LIMITS; + overlaySelectors: readonly string[]; + visibilityLimits: typeof HTML_VISIBILITY_LIMITS; +}; +export type ComponentScopeExtractionConfig = { + eidxAttr: string; + interactiveControlSelector: string; + limits: typeof HTML_EXTRACTION_LIMITS; +}; + +export function extractElementData(el: Element, config?: ElementExtractionConfig) { + const cfg = + config || + ({ + attrs: { + area: 'data-explorbot-area', + context: 'data-explorbot-context', + eidx: 'data-explorbot-eidx', + frameSourceIndex: 'data-explorbot-frame-source-index', + variant: 'data-explorbot-variant', + }, + codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'], + maxAreaDepth: 5, + maxContextLength: 120, + maxOuterHTMLLength: 2000, + maxTextLength: 80, + minOpacity: 0.1, + selectors: { + headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]', + labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]', + semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]', + }, + } as ElementExtractionConfig); + + function normalizeText(value: string): string { + return value.replace(/\s+/g, ' ').trim(); + } + + function readText(node: Element | null): string { + if (!node) return ''; + return normalizeText(node.textContent || '').slice(0, cfg.maxContextLength); + } + + function getLabelLikeText(node: Element | null): string { + if (!node) return ''; + const direct = readText(node); + if (direct) return direct; + const labelLike = node.querySelector(cfg.selectors.labelLike); + return readText(labelLike); + } + + function collectVariantHints(target: Element): string[] { + const tokens = new Set(); + const className = target.getAttribute('class') || ''; + const tagName = target.tagName.toLowerCase(); + + for (const cls of className.split(/\s+/).filter(Boolean)) { + const lower = cls.toLowerCase(); + if (/^(xs|sm|md|lg|xl|xxl)$/.test(lower)) tokens.add(lower); + if (/^(mini|small|medium|large|xlarge|xl|compact|dense)$/.test(lower)) tokens.add(lower); + if (/(^|[-_])(xs|sm|md|lg|xl|xxl|mini|small|medium|large|compact|dense)([-_]|$)/.test(lower)) tokens.add(lower); + if (/(selected|disabled|primary|secondary|tertiary|danger|success|warning|outline|ghost|icon|dropdown)/.test(lower)) tokens.add(lower); + } + + const type = (target.getAttribute('type') || '').toLowerCase(); + if (type) tokens.add(type); + if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true') tokens.add('disabled'); + if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true') tokens.add('selected'); + if (tagName === 'iframe') tokens.add('iframe'); + if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target)) tokens.add('code-editor'); + + const svgCount = target.querySelectorAll('svg').length; + if (svgCount > 0) tokens.add('has-icon'); + if (svgCount > 1) tokens.add('double-icon'); + + const normalizedText = normalizeText(target.textContent || ''); + if (!normalizedText && svgCount > 0) tokens.add('icon-only'); + if (normalizedText && svgCount > 0) { + const first = target.firstElementChild?.tagName.toLowerCase(); + const last = target.lastElementChild?.tagName.toLowerCase(); + if (first === 'svg') tokens.add('leading-icon'); + if (last === 'svg') tokens.add('trailing-icon'); + } + + if (tagName === 'a' && target.getAttribute('href')) tokens.add('navigates'); + + return Array.from(tokens).slice(0, 8); + } + + function isEmbeddedCodeEditorFrame(target: Element): boolean { + const src = (target.getAttribute('src') || '').toLowerCase(); + const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', '); + const ancestorClasses = (target.closest(markerSelector)?.getAttribute('class') || '').toLowerCase(); + return cfg.codeEditorMarkers.some((marker) => src.includes(marker) || ancestorClasses.includes(marker)); + } + + function findContextLabel(target: Element): string { + const labelledby = target.getAttribute('aria-labelledby'); + const candidates: string[] = []; + if (labelledby) { + for (const id of labelledby.split(/\s+/).filter(Boolean)) { + const ref = document.getElementById(id); + const text = readText(ref); + if (text) candidates.push(text); + } + } + + const semanticContainer = target.closest(cfg.selectors.semanticContextContainer); + if (semanticContainer) { + const ownHeading = semanticContainer.querySelector(cfg.selectors.headingLabel); + const ownHeadingText = readText(ownHeading); + if (ownHeadingText) candidates.push(ownHeadingText); + + let previous: Element | null = semanticContainer.previousElementSibling; + let hops = 0; + while (previous && hops < 3) { + const previousText = getLabelLikeText(previous); + if (previousText) { + candidates.push(previousText); + break; + } + previous = previous.previousElementSibling; + hops++; + } + } + + let parent: Element | null = target.parentElement; + let depth = 0; + while (parent && depth < 4) { + let sibling: Element | null = parent.previousElementSibling; + let hops = 0; + while (sibling && hops < 2) { + const siblingText = getLabelLikeText(sibling); + if (siblingText) { + candidates.push(siblingText); + sibling = null; + break; + } + sibling = sibling.previousElementSibling; + hops++; + } + parent = parent.parentElement; + depth++; + } + + const ownText = normalizeText(target.textContent || ''); + for (const candidate of candidates) { + if (!candidate) continue; + if (candidate === ownText) continue; + if (candidate.toLowerCase().includes('title should not be empty')) continue; + return candidate.slice(0, cfg.maxContextLength); + } + + return ''; + } + + const rect = el.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return null; + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden') return null; + if (Number.parseFloat(style.opacity || '1') < cfg.minOpacity) return null; + if (el.getAttribute('aria-hidden') === 'true' || el.hasAttribute('hidden')) return null; + if ((el as HTMLElement).offsetParent === null && style.position !== 'fixed') return null; + + const allAttrs: Record = {}; + for (let i = 0; i < el.attributes.length; i++) { + const attr = el.attributes[i]; + allAttrs[attr.name] = attr.value; + } + + const areaHints: string[] = []; + let current: Element | null = el; + let depth = 0; + while (current && depth < cfg.maxAreaDepth) { + const tag = current.tagName.toLowerCase(); + areaHints.push(tag); + + const role = current.getAttribute('role'); + if (role) areaHints.push(`role:${role.toLowerCase()}`); + + const id = current.getAttribute('id'); + if (id) areaHints.push(`id:${id.toLowerCase()}`); + + const className = current.getAttribute('class'); + if (className) { + for (const cls of className.split(/\s+/).filter(Boolean)) { + areaHints.push(`class:${cls.toLowerCase()}`); + } + } + + current = current.parentElement; + depth++; + } + + allAttrs[cfg.attrs.area] = areaHints.join('|'); + allAttrs[cfg.attrs.context] = findContextLabel(el); + allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|'); + + return { + tag: el.tagName.toLowerCase(), + text: normalizeText(el.textContent || '').slice(0, cfg.maxTextLength), + allAttrs, + outerHTML: el.outerHTML.slice(0, cfg.maxOuterHTMLLength), + x: Math.round(rect.x + rect.width / 2), + y: Math.round(rect.y + rect.height / 2), + }; +} + +export function getElementDataExtractorSource(): string { + return extractElementData.toString(); +} + +export function extractVisibleOverlayHtml(config: VisibleOverlayExtractionConfig): string { + function isVisible(element: Element): boolean { + const html = element as HTMLElement; + const style = window.getComputedStyle(html); + const rect = html.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return false; + if (style.display === 'none' || style.visibility === 'hidden') return false; + if (Number.parseFloat(style.opacity || '1') < config.visibilityLimits.minOpacity) return false; + return true; + } + + function getUsefulContent(element: Element): { interactiveCount: number; text: string } { + const text = (element.textContent || '').replace(/\s+/g, ' ').trim(); + const interactiveCount = element.querySelectorAll(config.interactiveContentSelector).length; + return { interactiveCount, text }; + } + + function isLikelyFloatingOverlay(element: Element): boolean { + const html = element as HTMLElement; + const style = window.getComputedStyle(html); + const rect = html.getBoundingClientRect(); + const zIndex = Number.parseInt(style.zIndex || '0', 10); + const isFloating = style.position === 'fixed' || style.position === 'absolute' || style.position === 'sticky' || zIndex > 0; + if (!isFloating) return false; + if (rect.width < config.visibilityLimits.minOverlayWidth || rect.height < config.visibilityLimits.minOverlayHeight) return false; + if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth) return false; + if (rect.width >= window.innerWidth * config.visibilityLimits.maxViewportOverlayRatio && rect.height >= window.innerHeight * config.visibilityLimits.maxViewportOverlayRatio) return false; + const { interactiveCount, text } = getUsefulContent(element); + return interactiveCount > 0 || text.length > 0; + } + + const overlays: string[] = []; + const seen = new Set(); + for (const selector of config.overlaySelectors) { + for (const element of Array.from(document.querySelectorAll(selector))) { + if (seen.has(element)) continue; + seen.add(element); + if (!isVisible(element)) continue; + const { interactiveCount, text } = getUsefulContent(element); + if (interactiveCount === 0 && text.length === 0) continue; + overlays.push((element as HTMLElement).outerHTML.slice(0, config.limits.overlayHtmlLength)); + } + } + + if (overlays.length === 0) { + const floatingCandidates = Array.from(document.body.querySelectorAll('*')) + .filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element)) + .sort((left, right) => { + const leftStyle = window.getComputedStyle(left as HTMLElement); + const rightStyle = window.getComputedStyle(right as HTMLElement); + const leftZ = Number.parseInt(leftStyle.zIndex || '0', 10) || 0; + const rightZ = Number.parseInt(rightStyle.zIndex || '0', 10) || 0; + if (leftZ !== rightZ) return rightZ - leftZ; + const leftRect = (left as HTMLElement).getBoundingClientRect(); + const rightRect = (right as HTMLElement).getBoundingClientRect(); + return leftRect.width * leftRect.height - rightRect.width * rightRect.height; + }); + + for (const element of floatingCandidates.slice(0, config.limits.maxOverlayCount)) { + overlays.push((element as HTMLElement).outerHTML.slice(0, config.limits.overlayHtmlLength)); + } + } + + return overlays.slice(0, config.limits.maxOverlayCount).join('\n\n--- overlay ---\n\n'); +} + +export function extractComponentScopeHtml(eidx: string, config: ComponentScopeExtractionConfig): string { + const element = document.querySelector(`[${config.eidxAttr}="${eidx}"]`); + if (!element) return ''; + + function countInteractive(node: Element): number { + return node.querySelectorAll(config.interactiveControlSelector).length; + } + + let current = element.parentElement; + while (current) { + const count = countInteractive(current); + if (count > 0 && count <= config.limits.maxScopeInteractiveCount) { + return current.outerHTML.slice(0, config.limits.componentScopeHtmlLength); + } + current = current.parentElement; + } + + if (element instanceof HTMLElement) return element.outerHTML.slice(0, config.limits.componentScopeHtmlLength); + return ''; +} + +export function getVisibleOverlayHtmlExtractorSource(): string { + return extractVisibleOverlayHtml.toString(); +} + +export function getComponentScopeHtmlExtractorSource(): string { + return extractComponentScopeHtml.toString(); +} + export const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/; export const TAILWIND_CLASS_PATTERNS: RegExp[] = [ diff --git a/src/utils/web-element.ts b/src/utils/web-element.ts index 55d149b..7e0e593 100644 --- a/src/utils/web-element.ts +++ b/src/utils/web-element.ts @@ -1,10 +1,11 @@ +import { ELEMENT_EXTRACTION_CONFIG, EXPLORBOT_ATTRS, type ElementExtractionConfig, type RawElementData, extractElementData, getElementDataExtractorSource } from './html.ts'; import { type XPathMatch, buildClickableXPath, evaluateXPath, isDynamicId, isGenericClass } from './xpath.ts'; +export { extractElementData } from './html.ts'; + const KEY_DISPLAY_ATTRS = ['role', 'id', 'class', 'aria-label']; const KEY_ATTRS = ['role', 'aria-label', 'id', 'name', 'type', 'href']; -type RawElementData = NonNullable>; - export class WebElement { tag: string; role: string; @@ -43,7 +44,7 @@ export class WebElement { } get eidx(): string | null { - return this.attrs['data-explorbot-eidx'] || this.attrs.eidx || null; + return this.attrs[EXPLORBOT_ATTRS.eidx] || this.attrs.eidx || null; } get isNavigationLink(): boolean { @@ -58,7 +59,7 @@ export class WebElement { } get areaHints(): string[] { - const raw = this.attrs['data-explorbot-area'] || ''; + const raw = this.attrs[EXPLORBOT_ATTRS.area] || ''; return raw .split('|') .map((entry) => entry.trim().toLowerCase()) @@ -66,11 +67,11 @@ export class WebElement { } get contextLabel(): string { - return (this.attrs['data-explorbot-context'] || '').trim(); + return (this.attrs[EXPLORBOT_ATTRS.context] || '').trim(); } get variantHints(): string[] { - const raw = this.attrs['data-explorbot-variant'] || ''; + const raw = this.attrs[EXPLORBOT_ATTRS.variant] || ''; return raw .split('|') .map((entry) => entry.trim().toLowerCase()) @@ -108,7 +109,7 @@ export class WebElement { try { const count = await locator.count(); if (count === 0) return null; - const data = await locator.first().evaluate(extractElementData); + const data = await locator.first().evaluate(extractElementData, ELEMENT_EXTRACTION_CONFIG); if (!data) return null; return WebElement.fromRawData(data); } catch { @@ -117,25 +118,25 @@ export class WebElement { } static async fromEidx(page: any, eidx: string): Promise { - return WebElement.fromPlaywrightLocator(page.locator(`[data-explorbot-eidx="${eidx}"]`)); + return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`)); } static async fromEidxList(page: any, eidxList: string[]): Promise { if (eidxList.length === 0) return []; const rawList: RawElementData[] = await page.evaluate( - ([list, extractFnStr]: [string[], string]) => { + ([list, extractFnStr, config]: [string[], string, ElementExtractionConfig]) => { const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; const results: any[] = []; for (const eidx of list) { - const el = document.querySelector(`[data-explorbot-eidx="${eidx}"]`); + const el = document.querySelector(`[${config.attrs.eidx}="${eidx}"]`); if (!el) continue; - const data = extract(el); + const data = extract(el, config); if (data) results.push(data); } return results; }, - [eidxList, extractElementData.toString()] as [string[], string] + [eidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] as [string[], string, ElementExtractionConfig] ); return rawList.map((d) => WebElement.fromRawData(d)); @@ -147,186 +148,3 @@ export class WebElement { return { totalFound: result.totalFound, elements: result.matches.map((m) => WebElement.fromXPathMatch(m)) }; } } - -export function extractElementData(el: Element) { - function normalizeText(value: string): string { - return value.replace(/\s+/g, ' ').trim(); - } - - function readText(node: Element | null): string { - if (!node) return ''; - return normalizeText(node.textContent || '').slice(0, 120); - } - - function getLabelLikeText(node: Element | null): string { - if (!node) return ''; - const direct = readText(node); - if (direct) return direct; - const labelLike = node.querySelector('h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]'); - return readText(labelLike); - } - - function collectVariantHints(target: Element): string[] { - const tokens = new Set(); - const className = target.getAttribute('class') || ''; - const tagName = target.tagName.toLowerCase(); - - for (const cls of className.split(/\s+/).filter(Boolean)) { - const lower = cls.toLowerCase(); - if (/^(xs|sm|md|lg|xl|xxl)$/.test(lower)) tokens.add(lower); - if (/^(mini|small|medium|large|xlarge|xl|compact|dense)$/.test(lower)) tokens.add(lower); - if (/(^|[-_])(xs|sm|md|lg|xl|xxl|mini|small|medium|large|compact|dense)([-_]|$)/.test(lower)) tokens.add(lower); - if (/(selected|disabled|primary|secondary|tertiary|danger|success|warning|outline|ghost|icon|dropdown)/.test(lower)) tokens.add(lower); - } - - const type = (target.getAttribute('type') || '').toLowerCase(); - if (type) tokens.add(type); - if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true') tokens.add('disabled'); - if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true') tokens.add('selected'); - if (tagName === 'iframe') tokens.add('iframe'); - if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target)) tokens.add('code-editor'); - - const svgCount = target.querySelectorAll('svg').length; - if (svgCount > 0) tokens.add('has-icon'); - if (svgCount > 1) tokens.add('double-icon'); - - const normalizedText = normalizeText(target.textContent || ''); - if (!normalizedText && svgCount > 0) tokens.add('icon-only'); - if (normalizedText && svgCount > 0) { - const first = target.firstElementChild?.tagName.toLowerCase(); - const last = target.lastElementChild?.tagName.toLowerCase(); - if (first === 'svg') tokens.add('leading-icon'); - if (last === 'svg') tokens.add('trailing-icon'); - } - - if (tagName === 'a' && target.getAttribute('href')) tokens.add('navigates'); - - return Array.from(tokens).slice(0, 8); - } - - function isEmbeddedCodeEditorFrame(target: Element): boolean { - const src = (target.getAttribute('src') || '').toLowerCase(); - const parentClasses = (target.parentElement?.getAttribute('class') || '').toLowerCase(); - const ancestorClasses = (target.closest('[class*="monaco"], [class*="codemirror"], [class*="ace_editor"], [class*="code"]')?.getAttribute('class') || '').toLowerCase(); - return src.includes('monaco') || src.includes('codemirror') || src.includes('ace') || parentClasses.includes('frame-container') || ancestorClasses.includes('monaco') || ancestorClasses.includes('codemirror') || ancestorClasses.includes('ace_editor'); - } - - function findContextLabel(target: Element): string { - const labelTags = 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]'; - const labelledby = target.getAttribute('aria-labelledby'); - const candidates: string[] = []; - if (labelledby) { - for (const id of labelledby.split(/\s+/).filter(Boolean)) { - const ref = document.getElementById(id); - const text = readText(ref); - if (text) candidates.push(text); - } - } - - const freestyleUsage = target.closest('[class*="FreestyleUsage"]'); - if (freestyleUsage) { - const title = freestyleUsage.querySelector('[class*="FreestyleUsage-title"]'); - const titleText = readText(title); - if (titleText) candidates.push(titleText); - } - - const semanticContainer = target.closest('section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]'); - if (semanticContainer) { - const ownHeading = semanticContainer.querySelector(labelTags); - const ownHeadingText = readText(ownHeading); - if (ownHeadingText) candidates.push(ownHeadingText); - - let previous: Element | null = semanticContainer.previousElementSibling; - let hops = 0; - while (previous && hops < 3) { - const previousText = getLabelLikeText(previous); - if (previousText) { - candidates.push(previousText); - break; - } - previous = previous.previousElementSibling; - hops++; - } - } - - let parent: Element | null = target.parentElement; - let depth = 0; - while (parent && depth < 4) { - let sibling: Element | null = parent.previousElementSibling; - let hops = 0; - while (sibling && hops < 2) { - const siblingText = getLabelLikeText(sibling); - if (siblingText) { - candidates.push(siblingText); - sibling = null; - break; - } - sibling = sibling.previousElementSibling; - hops++; - } - parent = parent.parentElement; - depth++; - } - - const ownText = normalizeText(target.textContent || ''); - for (const candidate of candidates) { - if (!candidate) continue; - if (candidate === ownText) continue; - if (candidate.toLowerCase().includes('title should not be empty')) continue; - return candidate.slice(0, 120); - } - - return ''; - } - - const rect = el.getBoundingClientRect(); - if (rect.width === 0 && rect.height === 0) return null; - const style = window.getComputedStyle(el); - if (style.display === 'none' || style.visibility === 'hidden') return null; - if (Number.parseFloat(style.opacity || '1') < 0.1) return null; - if (el.getAttribute('aria-hidden') === 'true' || el.hasAttribute('hidden')) return null; - if ((el as HTMLElement).offsetParent === null && style.position !== 'fixed') return null; - - const allAttrs: Record = {}; - for (let i = 0; i < el.attributes.length; i++) { - const attr = el.attributes[i]; - allAttrs[attr.name] = attr.value; - } - - const areaHints: string[] = []; - let current: Element | null = el; - let depth = 0; - while (current && depth < 5) { - const tag = current.tagName.toLowerCase(); - areaHints.push(tag); - - const role = current.getAttribute('role'); - if (role) areaHints.push(`role:${role.toLowerCase()}`); - - const id = current.getAttribute('id'); - if (id) areaHints.push(`id:${id.toLowerCase()}`); - - const className = current.getAttribute('class'); - if (className) { - for (const cls of className.split(/\s+/).filter(Boolean)) { - areaHints.push(`class:${cls.toLowerCase()}`); - } - } - - current = current.parentElement; - depth++; - } - - allAttrs['data-explorbot-area'] = areaHints.join('|'); - allAttrs['data-explorbot-context'] = findContextLabel(el); - allAttrs['data-explorbot-variant'] = collectVariantHints(el).join('|'); - - return { - tag: el.tagName.toLowerCase(), - text: normalizeText(el.textContent || '').slice(0, 80), - allAttrs, - outerHTML: el.outerHTML.slice(0, 2000), - x: Math.round(rect.x + rect.width / 2), - y: Math.round(rect.y + rect.height / 2), - }; -} diff --git a/tests/unit/web-element.test.ts b/tests/unit/web-element.test.ts index f91de68..910aac6 100644 --- a/tests/unit/web-element.test.ts +++ b/tests/unit/web-element.test.ts @@ -6,8 +6,8 @@ describe('extractElementData', () => { it('adds context, area, and variant hints for component drilling', () => { const dom = new JSDOM(`
-
-

Toggle - off

+
+

Toggle - off

@@ -29,9 +29,9 @@ describe('extractElementData', () => { it('marks embedded code editor iframes', () => { const dom = new JSDOM(`
-
-

Code Input

-
+
+

Code Input

+
From 0a974914aca8517f197d446b2ff4f95c6953cc1f Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Sat, 18 Apr 2026 01:54:42 +0300 Subject: [PATCH 07/11] fix test --- bin/explorbot-cli.ts | 21 ++++++++++----------- src/ai/driller.ts | 20 +++++++++++++++----- src/explorer.ts | 39 +++++++++++++++++++++------------------ src/utils/html.ts | 3 +-- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/bin/explorbot-cli.ts b/bin/explorbot-cli.ts index 118cebe..9888505 100755 --- a/bin/explorbot-cli.ts +++ b/bin/explorbot-cli.ts @@ -513,7 +513,7 @@ addCommonOptions( const explorBot = new ExplorBot(buildExplorBotOptions(url, options)); await explorBot.start(); - await explorBot.visit(url); + await explorBot.visit(url); const plan = await explorBot.agentDriller().drill({ knowledgePath: options.knowledge, @@ -521,18 +521,17 @@ addCommonOptions( interactive: false, }); - console.log(`\nDrill completed: ${plan.tests.length} components`); - console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`); - console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`); + console.log(`\nDrill completed: ${plan.tests.length} components`); + console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`); + console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`); - await explorBot.stop(); - await showStatsAndExit(0); - } catch (error) { - console.error('Failed:', error instanceof Error ? error.message : 'Unknown error'); - await showStatsAndExit(1); - } + await explorBot.stop(); + await showStatsAndExit(0); + } catch (error) { + console.error('Failed:', error instanceof Error ? error.message : 'Unknown error'); + await showStatsAndExit(1); } -); +}); program .command('context ') diff --git a/src/ai/driller.ts b/src/ai/driller.ts index 4bc0fc3..09dbc72 100644 --- a/src/ai/driller.ts +++ b/src/ai/driller.ts @@ -9,7 +9,20 @@ import type { KnowledgeTracker } from '../knowledge-tracker.ts'; import { Observability } from '../observability.ts'; import { Plan, Test, TestResult } from '../test-plan.ts'; import { collectInteractiveNodes } from '../utils/aria.ts'; -import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole } from '../utils/html.ts'; +import { + EXPLORBOT_ATTRS, + HTML_COMPOSITE_AREA_HINTS, + HTML_COMPOSITE_TARGET_ROLES, + HTML_EXTRACTION_LIMITS, + HTML_FORM_CONTROL_ROLES, + HTML_FORM_CONTROL_TAGS, + HTML_INTERACTIVE_ROLES, + HTML_SELECTORS, + HTML_VISIBILITY_LIMITS, + getComponentScopeHtmlExtractorSource, + getVisibleOverlayHtmlExtractorSource, + inferHtmlRole, +} from '../utils/html.ts'; import { HooksRunner } from '../utils/hooks-runner.ts'; import { createDebug, tag } from '../utils/logger.ts'; import { loop, pause } from '../utils/loop.ts'; @@ -995,10 +1008,7 @@ function normalizeInteractionResult(component: ComponentInfo, action: string, re } function formatExperienceTitle(interaction: InteractionResult): string { - const descriptionTitle = interaction.description - .split(/[.;]/)[0] - .replace(/\s+/g, ' ') - .trim(); + const descriptionTitle = interaction.description.split(/[.;]/)[0].replace(/\s+/g, ' ').trim(); if (descriptionTitle.length > 0) return truncate(descriptionTitle, 90); diff --git a/src/explorer.ts b/src/explorer.ts index b76cbe4..722275a 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -766,24 +766,27 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s } try { - const rawList = await page.locator('iframe').evaluateAll((domElements: Element[], [extractFnStr, config]: [string, typeof ELEMENT_EXTRACTION_CONFIG]) => { - const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; - const results: any[] = []; - const sourceCounts: Record = {}; - let iframeIdx = 0; - for (const el of domElements) { - iframeIdx++; - const sourceKey = el.getAttribute('src') || ''; - sourceCounts[sourceKey] ||= 0; - sourceCounts[sourceKey]++; - const existing = el.getAttribute(config.attrs.eidx); - el.setAttribute(config.attrs.eidx, existing || `iframe-${iframeIdx}`); - el.setAttribute(config.attrs.frameSourceIndex, String(sourceCounts[sourceKey])); - const elData = extract(el, config); - if (elData) results.push(elData); - } - return results; - }, [getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]); + const rawList = await page.locator('iframe').evaluateAll( + (domElements: Element[], [extractFnStr, config]: [string, typeof ELEMENT_EXTRACTION_CONFIG]) => { + const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; + const results: any[] = []; + const sourceCounts: Record = {}; + let iframeIdx = 0; + for (const el of domElements) { + iframeIdx++; + const sourceKey = el.getAttribute('src') || ''; + sourceCounts[sourceKey] ||= 0; + sourceCounts[sourceKey]++; + const existing = el.getAttribute(config.attrs.eidx); + el.setAttribute(config.attrs.eidx, existing || `iframe-${iframeIdx}`); + el.setAttribute(config.attrs.frameSourceIndex, String(sourceCounts[sourceKey])); + const elData = extract(el, config); + if (elData) results.push(elData); + } + return results; + }, + [getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] + ); for (const raw of rawList) { elements.push(WebElement.fromRawData(raw, 'iframe')); } diff --git a/src/utils/html.ts b/src/utils/html.ts index c1765ef..1babf10 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -93,8 +93,7 @@ export const EXPLORBOT_ATTRS = { export const HTML_SELECTORS = { headingLabel: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"]', - interactiveContent: - 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [aria-label], [tabindex]', + interactiveContent: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="option"], [role="menuitem"], [role="switch"], [role="checkbox"], [role="radio"], [aria-label], [tabindex]', interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]', labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]', semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]', From d38420b7de8c17e84da6a239daa249ae8ded6e81 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Sat, 18 Apr 2026 01:56:36 +0300 Subject: [PATCH 08/11] fix test 2 --- src/utils/aria.ts | 2 +- tests/integration/planner.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/aria.ts b/src/utils/aria.ts index 667b666..4651bfc 100644 --- a/src/utils/aria.ts +++ b/src/utils/aria.ts @@ -421,7 +421,7 @@ export const detectFocusArea = (snapshot: string | null): FocusAreaResult => { if (result) return result; const fallback = findOverlayByCloseButton(nodes); - if (fallback && fallback.name) return fallback; + if (fallback?.name) return fallback; return { detected: false, type: null, name: null }; }; diff --git a/tests/integration/planner.test.ts b/tests/integration/planner.test.ts index f125981..a5c5d38 100644 --- a/tests/integration/planner.test.ts +++ b/tests/integration/planner.test.ts @@ -180,7 +180,7 @@ describe('Planner with aimock', () => { const prompt = extractPromptText(mock.getLastRequest()); expect(prompt).toContain(''); expect(prompt).toContain('Stress-test'); - expect(prompt).toContain('invalid, empty, and extreme values'); + expect(prompt).toContain('invalid, empty, or extreme values'); }); it('injects feature focus directive in prompt', async () => { From 4b3e90b593ba443d3fa4a38a12b85300a7a23fb8 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Wed, 29 Apr 2026 18:27:21 +0300 Subject: [PATCH 09/11] upd bosun --- src/ai/driller.ts | 111 ++++++++++++++++++++---------------- src/explorer.ts | 29 ---------- src/utils/html.ts | 13 +---- tests/unit/driller.test.ts | 112 +++++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 88 deletions(-) create mode 100644 tests/unit/driller.test.ts diff --git a/src/ai/driller.ts b/src/ai/driller.ts index 09dbc72..826ff18 100644 --- a/src/ai/driller.ts +++ b/src/ai/driller.ts @@ -141,6 +141,9 @@ export class Driller extends TaskAgent implements Agent { - Never use data-explorbot-eidx in locators - Never use container locators in recorded code - Prefer one-argument locators or self-contained XPath/CSS locators + - Prefer aria-* attributes first when they uniquely identify the component: aria-label, aria-labelledby, aria-checked, aria-pressed, aria-expanded, aria-selected + - Prefer semantic attributes next: role, checked, name, placeholder, title, href, and other stable state-bearing attributes + - Prefer semantic/state locators over raw classes whenever they are available and specific enough - Before choosing a locator, identify what makes the current component semantically different from its siblings - If siblings look similar, use text, aria labels, icon clues, variant hints, role, navigation behavior, border/outline classes, or state to target the exact component - Component size alone is not enough to choose a sibling instead of the current component, but if the current drilling target differs only by size, keep that exact size variant and record it @@ -436,17 +439,19 @@ export class Driller extends TaskAgent implements Agent { 1. Work only with this component 2. Use Preferred click code first unless it clearly fails, then try other self-contained locators from page HTML - 3. Never use container locators in code - 4. Never use data-explorbot-eidx in code - 5. If the page changes, use drill_restore before continuing - 6. Call drill_record for each reusable interaction you discover - 7. When you are done exploring the component, call drill_done - 8. If the component is not drillable, call drill_skip - 9. If similar components exist, use Context and Variant to distinguish this exact variant instead of skipping immediately - 10. Do not switch to a sibling with the same text but different variant or size. Stay anchored to the current component's Preferred locator, Context, and Variant. - 10a. If same-text components differ only by size, still record the current size variant instead of treating it as a duplicate. - 11. In drill_record result, describe the component precisely: color/variant, border/outline, icon purpose, text, role, navigation behavior, state, and why this component was chosen over similar siblings. - 12. Prefer results like "Clicked the red outlined button with a leading refresh icon." over generic results like "Button clicked." + 3. When you need a new locator, prefer aria-* attributes first, then semantic/state attributes like role, checked, name, placeholder, title, href + 4. Only fall back to classes or text-heavy XPath when aria/semantic attributes are not sufficient to target the exact component + 5. Never use container locators in code + 6. Never use data-explorbot-eidx in code + 7. If the page changes, use drill_restore before continuing + 8. Call drill_record for each reusable interaction you discover + 9. When you are done exploring the component, call drill_done + 10. If the component is not drillable, call drill_skip + 11. If similar components exist, use Context and Variant to distinguish this exact variant instead of skipping immediately + 12. Do not switch to a sibling with the same text but different variant or size. Stay anchored to the current component's Preferred locator, Context, and Variant. + 12a. If same-text components differ only by size, still record the current size variant instead of treating it as a duplicate. + 13. In drill_record result, describe the component precisely: color/variant, border/outline, icon purpose, text, role, navigation behavior, state, and why this component was chosen over similar siblings. + 14. Prefer results like "Clicked the red outlined button with a leading refresh icon." over generic results like "Button clicked." `; } @@ -613,7 +618,11 @@ export class Driller extends TaskAgent implements Agent { const successfulInteractions = results.filter((result) => result.result === 'success' && result.code); for (const interaction of successfulInteractions) { - await experienceTracker.saveSuccessfulResolution(state, formatExperienceTitle(interaction), interaction.code!, interaction.description); + experienceTracker.writeAction(state, { + title: formatExperienceTitle(interaction), + code: interaction.code!, + explanation: interaction.description, + }); } if (successfulInteractions.length > 0) { @@ -826,9 +835,8 @@ function canonicalizeRecordedClick(component: ComponentInfo, fallbackCode: strin return fallbackCode; } -function buildCanonicalClickCode(component: ComponentInfo): string { +export function buildCanonicalClickCode(component: ComponentInfo): string { if (component.tag === 'a') return ''; - if (component.tag === 'iframe' || component.role === 'code-editor') return buildEmbeddedFrameCode(component); const semanticCode = buildSemanticClickCode(component); if (semanticCode) return semanticCode; @@ -907,28 +915,6 @@ function getLabelAttrName(component: ComponentInfo): string { return 'name'; } -function buildEmbeddedFrameCode(component: ComponentInfo): string { - const src = component.html.match(/\ssrc=["']([^"']+)["']/i)?.[1] || ''; - const sourceIndex = getHtmlAttrValue(component.html, EXPLORBOT_ATTRS.frameSourceIndex); - const srcCondition = src ? `contains(@src,${xpathLiteral(src)})` : ''; - - let iframeLocator = '//iframe'; - if (srcCondition) iframeLocator += `[${srcCondition}]`; - if (sourceIndex) iframeLocator = `(${iframeLocator})[${sourceIndex}]`; - - let text = 'test'; - if (component.variant.includes('code-editor')) { - text = 'const value = "test";'; - } - - return [`I.switchTo(${JSON.stringify(iframeLocator)})`, 'I.click("body")', `I.type(${JSON.stringify(text)})`, 'I.switchTo()'].join('\n'); -} - -function getHtmlAttrValue(html: string, attr: string): string { - const escapedAttr = attr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - return html.match(new RegExp(`\\s${escapedAttr}=["']([^"']+)["']`, 'i'))?.[1] || ''; -} - function formatVariant(variantHints: string[]): string { if (variantHints.length === 0) return ''; return variantHints.slice(0, 4).join(', '); @@ -1007,13 +993,12 @@ function normalizeInteractionResult(component: ComponentInfo, action: string, re return value; } -function formatExperienceTitle(interaction: InteractionResult): string { - const descriptionTitle = interaction.description.split(/[.;]/)[0].replace(/\s+/g, ' ').trim(); - - if (descriptionTitle.length > 0) return truncate(descriptionTitle, 90); - - const action = interaction.action ? capitalize(interaction.action) : 'Interact with'; - return truncate(`${action} ${interaction.component}`, 90); +export function formatExperienceTitle(interaction: InteractionResult): string { + const verb = normalizeHowToVerb(interaction.action); + const target = extractHowToTarget(interaction.component); + if (target) return truncate(`${verb} ${target}`, 90); + if (interaction.component.trim()) return truncate(`${verb} ${interaction.component.trim().toLowerCase()}`, 90); + return truncate(`${verb} component`, 90); } function fallbackInteractionResult(component: ComponentInfo, action: string): string { @@ -1027,12 +1012,42 @@ function fallbackInteractionResult(component: ComponentInfo, action: string): st return `${capitalize(action)} executed for ${label}${variant}; differentiators: ${details}.`; } +function normalizeHowToVerb(action: string): string { + const normalizedAction = action.trim().toLowerCase(); + if (normalizedAction === 'click') return 'click'; + if (normalizedAction === 'presskey') return 'press key on'; + if (normalizedAction === 'form') return 'submit'; + if (normalizedAction === 'type') return 'type into'; + if (normalizedAction === 'select') return 'select'; + if (normalizedAction === 'open') return 'open'; + if (normalizedAction === 'toggle') return 'toggle'; + if (normalizedAction) return normalizedAction; + return 'use'; +} + +function extractHowToTarget(component: string): string { + const roleMatch = component.match(/^([A-Za-z-]+)/); + const quotedMatch = component.match(/"([^"]+)"/); + const role = roleMatch?.[1]?.trim().toLowerCase() || ''; + const label = quotedMatch?.[1]?.trim().toLowerCase() || ''; + + if (label && role) return `${label} ${role}`; + if (label) return label; + if (role) return role; + return component + .replace(/\[[^\]]*\]/g, '') + .replace(/\([^)]*\)/g, '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); +} + function hasContainerLocator(code: string): boolean { for (const line of code .split('\n') .map((entry) => entry.trim()) .filter(Boolean)) { - const argCount = countTopLevelArgs(line); + const argCount = countTopLevelArgCount(line); if (line.startsWith('I.click(') && argCount >= 2) return true; if (line.startsWith('I.fillField(') && argCount >= 3) return true; if (line.startsWith('I.selectOption(') && argCount >= 3) return true; @@ -1043,7 +1058,9 @@ function hasContainerLocator(code: string): boolean { return false; } -function countTopLevelArgs(line: string): number { +// Lightweight scanner for a single JS call expression line. +// It counts commas only at top level and ignores nested (), [], {}, and quoted strings. +function countTopLevelArgCount(line: string): number { const start = line.indexOf('('); const end = line.lastIndexOf(')'); if (start === -1 || end === -1 || end <= start + 1) return 0; @@ -1117,8 +1134,6 @@ function scoreComponentPriority(element: WebElement): number { if (hints.some((hint) => hint.includes('content'))) score += 20; if (role === 'tab') score += 35; if (isSemanticFormControl(element)) score += 35; - if (element.tag === 'iframe') score += 35; - if (element.variantHints.includes('code-editor')) score += 60; if (element.tag === 'button') score += 20; if (element.tag === 'input' || element.tag === 'textarea' || element.tag === 'select') score += 18; if (element.tag === 'a') score -= 40; @@ -1136,7 +1151,6 @@ function isDrillableElement(element: WebElement): boolean { const text = normalized(element.text); if (attrs.includes('tooltip') || attrs.includes('attacher')) return false; if (isNestedCompositeControl(element)) return false; - if (element.tag === 'iframe') return true; if (text === '') { if (!isInteractiveElement(element)) return false; if (isSemanticFormControl(element)) return true; @@ -1168,7 +1182,6 @@ function isButtonLikeElement(element: WebElement): boolean { function isInteractiveElement(element: WebElement): boolean { if (element.tag === 'button') return true; if (element.tag === 'a' && element.attrs.href) return true; - if (element.tag === 'iframe') return true; if (HTML_FORM_CONTROL_TAGS.has(element.tag)) return true; const role = (element.role || element.attrs.role || element.tag).toLowerCase(); if (HTML_INTERACTIVE_ROLES.has(role)) return true; diff --git a/src/explorer.ts b/src/explorer.ts index f1717e9..dbcba86 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -775,35 +775,6 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s } } - try { - const rawList = await page.locator('iframe').evaluateAll( - (domElements: Element[], [extractFnStr, config]: [string, typeof ELEMENT_EXTRACTION_CONFIG]) => { - const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any; - const results: any[] = []; - const sourceCounts: Record = {}; - let iframeIdx = 0; - for (const el of domElements) { - iframeIdx++; - const sourceKey = el.getAttribute('src') || ''; - sourceCounts[sourceKey] ||= 0; - sourceCounts[sourceKey]++; - const existing = el.getAttribute(config.attrs.eidx); - el.setAttribute(config.attrs.eidx, existing || `iframe-${iframeIdx}`); - el.setAttribute(config.attrs.frameSourceIndex, String(sourceCounts[sourceKey])); - const elData = extract(el, config); - if (elData) results.push(elData); - } - return results; - }, - [getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] - ); - for (const raw of rawList) { - elements.push(WebElement.fromRawData(raw, 'iframe')); - } - } catch { - debugLog('Failed to annotate iframes'); - } - return { ariaSnapshot, elements }; } diff --git a/src/utils/html.ts b/src/utils/html.ts index 1babf10..94917eb 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -75,7 +75,7 @@ function matchesAnySelector(element: parse5TreeAdapter.Element, selectors: strin const TEXT_ELEMENT_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'td', 'th', 'label', 'div', 'span']); -const INTERACTIVE_TAGS = new Set(['a', 'button', 'details', 'input', 'option', 'select', 'summary', 'textarea', 'iframe']); +const INTERACTIVE_TAGS = new Set(['a', 'button', 'details', 'input', 'option', 'select', 'summary', 'textarea']); const INTERACTIVE_ROLES = new Set(['button', 'checkbox', 'combobox', 'link', 'listbox', 'radio', 'search', 'switch', 'tab', 'textbox']); @@ -87,7 +87,6 @@ export const EXPLORBOT_ATTRS = { area: 'data-explorbot-area', context: 'data-explorbot-context', eidx: 'data-explorbot-eidx', - frameSourceIndex: 'data-explorbot-frame-source-index', variant: 'data-explorbot-variant', } as const; @@ -116,20 +115,18 @@ export const HTML_EXTRACTION_LIMITS = { export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'] as const; -export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); +export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); export const HTML_FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']); export const HTML_COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']); export const HTML_COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']); export const HTML_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']); export function inferHtmlRole(data: { attrs: Record; role?: string; tag: string; variantHints?: string[] }): string { - if (data.tag === 'iframe' && data.variantHints?.includes('code-editor')) return 'code-editor'; if (data.role) return data.role.toLowerCase(); const explicitRole = data.attrs.role; if (explicitRole) return explicitRole.toLowerCase(); if (data.tag === 'a' && data.attrs.href) return 'link'; if (data.tag === 'button') return 'button'; - if (data.tag === 'iframe') return 'iframe'; if (data.tag === 'select') return 'combobox'; if (data.tag === 'textarea') return 'textbox'; if (data.tag === 'input') { @@ -178,7 +175,6 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig area: 'data-explorbot-area', context: 'data-explorbot-context', eidx: 'data-explorbot-eidx', - frameSourceIndex: 'data-explorbot-frame-source-index', variant: 'data-explorbot-variant', }, codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'], @@ -228,9 +224,6 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig if (type) tokens.add(type); if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true') tokens.add('disabled'); if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true') tokens.add('selected'); - if (tagName === 'iframe') tokens.add('iframe'); - if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target)) tokens.add('code-editor'); - const svgCount = target.querySelectorAll('svg').length; if (svgCount > 0) tokens.add('has-icon'); if (svgCount > 1) tokens.add('double-icon'); @@ -781,7 +774,7 @@ export function htmlMinimalUISnapshot(html: string, htmlConfig?: HtmlConfig['min } // Define default interactive elements - const interactiveElements = ['a', 'input', 'button', 'select', 'textarea', 'option', 'iframe']; + const interactiveElements = ['a', 'input', 'button', 'select', 'textarea', 'option']; const textElements = ['label', 'h1', 'h2']; const allowedRoles = ['button', 'checkbox', 'search', 'textbox', 'tab']; const allowedAttrs = ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'width', 'height', 'role']; diff --git a/tests/unit/driller.test.ts b/tests/unit/driller.test.ts new file mode 100644 index 0000000..88435e1 --- /dev/null +++ b/tests/unit/driller.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'bun:test'; +import { buildCanonicalClickCode, formatExperienceTitle } from '../../src/ai/driller.ts'; + +describe('buildCanonicalClickCode', () => { + it('returns empty code for links', () => { + const code = buildCanonicalClickCode(createComponent({ tag: 'a' })); + expect(code).toBe(''); + }); + + it('builds semantic xpath click when role and aria-label are available', () => { + const code = buildCanonicalClickCode( + createComponent({ + tag: 'button', + attrs: { + role: 'switch', + 'aria-label': 'Enable feature', + 'aria-checked': 'false', + }, + }) + ); + + expect(code).toBe('I.click("//*[self::button and @role=\\"switch\\" and @aria-label=\\"Enable feature\\" and @aria-checked=\\"false\\"]")'); + }); + + it('falls back to provided locator when classes are not usable', () => { + const code = buildCanonicalClickCode( + createComponent({ + tag: 'button', + locator: '//button[@data-test="save"]', + classes: ['bad class', '###'], + attrs: {}, + }) + ); + + expect(code).toBe('I.click("//button[@data-test=\\"save\\"]")'); + }); + + it('builds icon-aware selector for textless icon buttons', () => { + const code = buildCanonicalClickCode( + createComponent({ + tag: 'button', + text: '', + classes: ['icon-btn', 'secondary'], + variant: 'has-icon, icon-only', + attrs: {}, + }) + ); + + expect(code).toBe('I.click("button.icon-btn.secondary:has(svg)")'); + }); +}); + +describe('formatExperienceTitle', () => { + it('creates imperative how-to title for button clicks', () => { + const title = formatExperienceTitle({ + componentId: '1', + component: 'Button "Hide guidelines" [Component Showcase] (secondary-btn, btn-md)', + action: 'click', + result: 'success', + description: 'Clicked "Hide guidelines".', + }); + + expect(title).toBe('click hide guidelines button'); + }); + + it('creates imperative how-to title for links', () => { + const title = formatExperienceTitle({ + componentId: '2', + component: 'Link "Requirements Shift + 2" [Tests Shift + 1] (has-icon, navigates)', + action: 'click', + result: 'success', + description: 'Clicked the requirements link.', + }); + + expect(title).toBe('click requirements shift + 2 link'); + }); + + it('uses action-specific verb mapping for typing', () => { + const title = formatExperienceTitle({ + componentId: '3', + component: 'Textbox "Email" [Login form]', + action: 'type', + result: 'success', + description: 'Typed into the email field.', + }); + + expect(title).toBe('type into email textbox'); + }); +}); + +function createComponent(overrides: Partial = {}) { + return { + id: 'component-id', + name: 'Component', + role: '', + locator: '//default-locator', + preferredCode: '', + eidx: 'e1', + description: 'component', + html: '', + text: 'Enable feature', + tag: 'button', + classes: ['primary-btn', 'btn-md'], + attrs: {}, + context: '', + variant: '', + placeholder: '', + disabled: false, + ariaMatches: [], + ...overrides, + }; +} From 6e9f05cef755bf0e620ca0dc0c85c53f585f9715 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Wed, 29 Apr 2026 18:30:23 +0300 Subject: [PATCH 10/11] upd bosun iframe --- src/utils/html.ts | 10 ++++++--- tests/unit/annotate-elements.test.ts | 32 ---------------------------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/src/utils/html.ts b/src/utils/html.ts index 94917eb..5284588 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -75,7 +75,7 @@ function matchesAnySelector(element: parse5TreeAdapter.Element, selectors: strin const TEXT_ELEMENT_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'td', 'th', 'label', 'div', 'span']); -const INTERACTIVE_TAGS = new Set(['a', 'button', 'details', 'input', 'option', 'select', 'summary', 'textarea']); +const INTERACTIVE_TAGS = new Set(['a', 'button', 'details', 'input', 'option', 'select', 'summary', 'textarea', 'iframe']); const INTERACTIVE_ROLES = new Set(['button', 'checkbox', 'combobox', 'link', 'listbox', 'radio', 'search', 'switch', 'tab', 'textbox']); @@ -115,18 +115,20 @@ export const HTML_EXTRACTION_LIMITS = { export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'] as const; -export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); +export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']); export const HTML_FORM_CONTROL_ROLES = new Set(['checkbox', 'radio', 'switch', 'combobox', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox']); export const HTML_COMPOSITE_TARGET_ROLES = new Set(['tab', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'treeitem']); export const HTML_COMPOSITE_AREA_HINTS = new Set(['role:tab', 'role:option', 'role:menuitem', 'role:menuitemcheckbox', 'role:menuitemradio', 'role:treeitem']); export const HTML_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']); export function inferHtmlRole(data: { attrs: Record; role?: string; tag: string; variantHints?: string[] }): string { + if (data.tag === 'iframe' && data.variantHints?.includes('code-editor')) return 'code-editor'; if (data.role) return data.role.toLowerCase(); const explicitRole = data.attrs.role; if (explicitRole) return explicitRole.toLowerCase(); if (data.tag === 'a' && data.attrs.href) return 'link'; if (data.tag === 'button') return 'button'; + if (data.tag === 'iframe') return 'iframe'; if (data.tag === 'select') return 'combobox'; if (data.tag === 'textarea') return 'textbox'; if (data.tag === 'input') { @@ -224,6 +226,8 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig if (type) tokens.add(type); if (target.hasAttribute('disabled') || target.getAttribute('aria-disabled') === 'true') tokens.add('disabled'); if (className.toLowerCase().includes('selected') || target.getAttribute('aria-pressed') === 'true') tokens.add('selected'); + if (tagName === 'iframe') tokens.add('iframe'); + if (tagName === 'iframe' && isEmbeddedCodeEditorFrame(target)) tokens.add('code-editor'); const svgCount = target.querySelectorAll('svg').length; if (svgCount > 0) tokens.add('has-icon'); if (svgCount > 1) tokens.add('double-icon'); @@ -774,7 +778,7 @@ export function htmlMinimalUISnapshot(html: string, htmlConfig?: HtmlConfig['min } // Define default interactive elements - const interactiveElements = ['a', 'input', 'button', 'select', 'textarea', 'option']; + const interactiveElements = ['a', 'input', 'button', 'select', 'textarea', 'option', 'iframe']; const textElements = ['label', 'h1', 'h2']; const allowedRoles = ['button', 'checkbox', 'search', 'textbox', 'tab']; const allowedAttrs = ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'width', 'height', 'role']; diff --git a/tests/unit/annotate-elements.test.ts b/tests/unit/annotate-elements.test.ts index a3d6219..1d40c61 100644 --- a/tests/unit/annotate-elements.test.ts +++ b/tests/unit/annotate-elements.test.ts @@ -171,37 +171,5 @@ describe('annotatePageElements', () => { expect(toggle?.areaHints).toContain('main'); expect(toggle?.outerHTML).toContain('aria-checked="false"'); }); - - it('annotates code editor iframes for driller discovery', async () => { - const page = { - locator: (selector: string) => { - if (selector === 'body') { - return { - ariaSnapshot: async () => '', - }; - } - return { - evaluateAll: async () => [ - createMockElement('iframe', { - src: '/ember-monaco/frame.html', - 'data-explorbot-context': 'Code Input', - 'data-explorbot-variant': 'iframe|code-editor', - 'data-explorbot-frame-source-index': '1', - }).extractData(), - ], - }; - }, - getByRole: () => ({ - evaluateAll: async () => [], - }), - }; - - const { elements } = await annotatePageElements(page); - const frame = elements.find((el) => el.role === 'iframe'); - expect(frame?.contextLabel).toBe('Code Input'); - expect(frame?.variantHints).toContain('iframe'); - expect(frame?.variantHints).toContain('code-editor'); - expect(frame?.attrs['data-explorbot-frame-source-index']).toBe('1'); - }); }); }); From 86c1d160ee20ddafe4abe1fcae36960381679891 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Wed, 29 Apr 2026 22:33:32 +0300 Subject: [PATCH 11/11] reporter html tester remove --- src/ai/tester.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/ai/tester.ts b/src/ai/tester.ts index 8c7632c..bfd3991 100644 --- a/src/ai/tester.ts +++ b/src/ai/tester.ts @@ -624,15 +624,8 @@ export class Tester extends TaskAgent implements Agent { } private finishTest(task: Test): void { - if (!task.result) { - const checkedNotes = task.getCheckedNotes(); - const hasPassedNotes = checkedNotes.some((note) => note.status === TestResult.PASSED); - const hasFailedNotes = checkedNotes.some((note) => note.status === TestResult.FAILED); - if ((task.hasAchievedAny() || hasPassedNotes) && !hasFailedNotes) { - task.finish(TestResult.PASSED); - } else { - task.finish(TestResult.FAILED); - } + if (!task.hasFinished) { + task.finish(TestResult.FAILED); } tag('info').log(`Finished: ${task.scenario}`);