diff --git a/.claude/skills/change-keyword/SKILL.md b/.claude/skills/change-keyword/SKILL.md index 8e6cd7e76..eb0baba3e 100644 --- a/.claude/skills/change-keyword/SKILL.md +++ b/.claude/skills/change-keyword/SKILL.md @@ -100,6 +100,20 @@ Done when every argument is described, the rendered page shows no broken links o Done when `inv utest`, `inv utest-node` and the affected `inv atest --suite` runs pass, and `inv lint` is clean. +### 7. Review + +Run this step without being asked, once step 6 is done. The review reads commits, so commit the +work on its branch first. Then dispatch a subagent to run the `mattpocock-skills:code-review` +skill, with the branch's merge-base with `main` as the fixed point. + +The subagent sees only its prompt, so give it the spec: the issue, plus every decision the +maintainer made that overrides or adds to it, such as a dropped argument. + +Relay both axes to the maintainer, and treat each finding as theirs to accept or reject. Check a +finding against the code before relaying it. Apply the accepted ones, then repeat step 6. + +Done when the maintainer has ruled on every finding and step 6 passes with the accepted fixes in. + ## Removing or renaming a keyword A keyword is never removed in the release that deprecates it. It is deprecated first, and removed diff --git a/Browser/keywords/interaction.py b/Browser/keywords/interaction.py index 796a098ef..7ecfca056 100644 --- a/Browser/keywords/interaction.py +++ b/Browser/keywords/interaction.py @@ -635,6 +635,51 @@ def focus(self, selector: str): ) logger.debug(response.log) + @keyword(tags=("Setter", "PageContent")) + def blur(self, selector: str): + """Removes focus from the element found by ``selector``. + + Use this when a page runs validation, formatting or other logic only + after a field loses focus. `Fill Text` and `Type Text` leave the field + focused, so that logic does not run until focus moves away. + + *Arguments:* + - ``selector``: Selector of the element. See the `Finding elements` section + for details about the selectors. + + Keyword uses strict mode, see `Finding elements` for more details about strict mode. + + If there is no element matching the selector, the keyword waits until a + matching element appears in the DOM. It fails if the element does not + appear within the library timeout, which is 10 seconds by default and + can be changed with `Set Browser Timeout`. + + If the element does not have focus, the keyword does nothing and passes, + and no blur event is fired. To verify the state of the element, use + `Get Element States`. + + ``css=:focus`` selects the focused element only when it is in the main + frame and outside shadow DOM. If the focused element is inside an iframe + or a shadow DOM, the selector does not find it reliably, so use a selector + of the element itself. If nothing has focus, the selector matches nothing + and the keyword fails after the timeout. + + Example: + | `Fill Text` id=email not-an-email + | `Blur` id=email + | `Get Element States` id=email contains defocused + | `Get Text` css=.validation-error == Please enter a valid email address + | + | # Removes focus from the focused element in the main frame + | `Blur` css=:focus + """ + selector = self.presenter_mode(selector, self.strict_mode) + with self.playwright.grpc_channel() as stub: + response = stub.Blur( + Request().ElementSelector(selector=selector, strict=self.strict_mode) + ) + logger.debug(response.log) + @keyword(tags=("Setter", "PageContent")) def scroll_to( self, diff --git a/atest/test/02_Content_Keywords/focus.robot b/atest/test/02_Content_Keywords/focus.robot index 6682ec1b1..7a1a24235 100644 --- a/atest/test/02_Content_Keywords/focus.robot +++ b/atest/test/02_Content_Keywords/focus.robot @@ -16,3 +16,41 @@ Focus With Strict ... Focus //input Set Strict Mode False scope=Test Focus //input + +Blur Removes Focus From Element + Focus \#username_field + Get Element States \#username_field contains focused + Blur \#username_field + Get Element States \#username_field contains defocused + +Blur Fires Blur Event + [Setup] New Page ${EVENTS_URL} + Focus \#event_test_input + Blur \#event_test_input + Get Text \#event_log_text *= "type":"blur" + +Blur On Element Without Focus Does Nothing + [Setup] New Page ${EVENTS_URL} + Blur \#event_test_input + Get Text \#event_log_text not contains "type":"blur" + Get Element States \#event_test_input contains defocused + +Blur On Element Without Focus Keeps Focus On Other Element + [Setup] New Page ${EVENTS_URL} + Focus \#event_test_button + Blur \#event_test_input + Get Element States \#event_test_button contains focused + +Blur Element Which Has Focus + Focus \#username_field + Blur css=:focus + Get Element States \#username_field contains defocused + +Blur With Strict + Run Keyword And Expect Error + ... *strict mode violation*//input*resolved to ${INPUT_ELEMENT_COUNT_IN_LOGIN} elements* + ... Blur //input + Set Strict Mode False scope=Test + Focus //input + Blur //input + Get Element States //input >> nth=0 contains defocused diff --git a/node/playwright-wrapper/grpc-service.ts b/node/playwright-wrapper/grpc-service.ts index 99e5ae6f5..90ad61319 100644 --- a/node/playwright-wrapper/grpc-service.ts +++ b/node/playwright-wrapper/grpc-service.ts @@ -421,6 +421,7 @@ export class PlaywrightServer { tap = this.wrapping(interaction.tap); hover = this.wrapping(interaction.hover); focus = this.wrapping(interaction.focus); + blur = this.wrapping(interaction.blur); checkCheckbox = this.wrapping(interaction.checkCheckbox); uncheckCheckbox = this.wrapping(interaction.uncheckCheckbox); getElement = this.wrapping(evaluation.getElement); diff --git a/node/playwright-wrapper/interaction.ts b/node/playwright-wrapper/interaction.ts index a37eb5a48..89fcda9dc 100644 --- a/node/playwright-wrapper/interaction.ts +++ b/node/playwright-wrapper/interaction.ts @@ -167,6 +167,14 @@ export async function focus(request: pb.Request_ElementSelector, state: Playwrig return emptyWithLog(`Focused element: ${selector}`); } +export async function blur(request: pb.Request_ElementSelector, state: PlaywrightState): Promise { + const selector = request.selector; + const strictMode = request.strict; + const locator = await findLocator(state, selector, strictMode, true); + await locator.blur(); + return emptyWithLog(`Blurred element: ${selector}`); +} + export async function checkCheckbox( request: pb.Request_ElementSelector, state: PlaywrightState, diff --git a/protobuf/playwright.proto b/protobuf/playwright.proto index 6ab886ea5..e932a94fe 100644 --- a/protobuf/playwright.proto +++ b/protobuf/playwright.proto @@ -589,6 +589,8 @@ service Playwright { rpc Hover(Request.ElementSelectorWithOptions) returns (Response.Empty); /* Focuses element specified by selector */ rpc Focus(Request.ElementSelector) returns (Response.Empty); + /* Removes focus from element specified by selector */ + rpc Blur(Request.ElementSelector) returns (Response.Empty); /* Waits for element be in a specific state*/ rpc WaitForElementsState(Request.ElementSelectorWithOptions) returns (Response.Empty); /* Executes javascript on the active page and retries until the expression is true */