├── javascript ├── .eslintignore ├── .clang-format ├── .gitignore ├── src │ ├── semantic_locators.ts │ ├── gen │ │ └── index.ts │ └── lib │ │ ├── outer.ts │ │ ├── error.ts │ │ ├── parse_locator.ts │ │ ├── semantic_locator.pegjs │ │ ├── accessible_name.ts │ │ ├── batch_cache.ts │ │ ├── attribute.ts │ │ ├── util.ts │ │ ├── semantic_locator.ts │ │ ├── types.ts │ │ ├── table.ts │ │ └── find_by_semantic_locator.ts ├── tsconfig.json ├── README.md ├── test │ └── lib │ │ ├── role_map_test.ts │ │ ├── outer_test.ts │ │ ├── accessible_name_test.ts │ │ ├── parse_locator_test.ts │ │ ├── semantic_locator_test.ts │ │ └── lookup_result_test.ts ├── karma.conf.js ├── wrapper │ └── wrapper.ts ├── DEVELOPING.md ├── package.json └── .eslintrc.json ├── webdriver_ts ├── .eslintignore ├── .clang-format ├── .gitignore ├── jasmine.json ├── tsconfig.json ├── README.md ├── src │ ├── web_drivers.ts │ └── semantic_locators.ts ├── package.json └── .eslintrc.json ├── webdriver_python ├── src │ ├── data │ │ ├── .gitignore │ │ └── __init__.py │ ├── __init__.py │ └── semantic_locators.py ├── .gitignore ├── scripts │ ├── type_check.sh │ ├── lint.sh │ ├── build.sh │ ├── test.sh │ └── publish.sh ├── .pylintrc ├── test │ ├── __init__.py │ └── webdrivers │ │ ├── __init__.py │ │ └── webdrivers.py ├── DEVELOPING.md ├── README.md └── pyproject.toml ├── webdriver_java ├── src │ ├── main │ │ ├── resources │ │ │ └── com │ │ │ │ └── google │ │ │ │ └── semanticlocators │ │ │ │ └── .gitignore │ │ └── java │ │ │ └── com │ │ │ └── google │ │ │ └── semanticlocators │ │ │ └── SemanticLocatorException.java │ └── test │ │ └── java │ │ └── com │ │ └── google │ │ └── semanticlocators │ │ └── WebDrivers.java ├── DEVELOPING.md ├── README.md └── pom.xml ├── docs ├── img │ ├── outer.png │ ├── a11y_tree.png │ ├── wildcard_name.png │ ├── explicit_semantics.png │ ├── native_semantics_link.png │ ├── aria_labelledby_example.png │ ├── native_semantics_button.png │ └── icon_64dp.svg ├── faq.md ├── features.md ├── DEVELOPING.md └── tutorial.md ├── webdriver_go ├── go.mod └── semloc │ ├── semloc.go │ └── semloc_test.go ├── pages ├── index.html └── playground │ └── index.html ├── AUTHORS ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── github_pages.yml │ ├── lint_webdriver_java.yml │ ├── test_javascript.yml │ ├── test_webdriver_dotnet.yml │ ├── test_webdriver_java.yml │ ├── lint_javascript.yml │ ├── lint_webdriver_ts.yml │ ├── test_webdriver_ts.yml │ ├── lint_webdriver_python.yml │ ├── types_webdriver_python.yml │ ├── test_webdriver_go.yml │ ├── test_webdriver_python.yml │ ├── scorecards.yml │ └── codeql.yml ├── webdriver_dotnet ├── DEVELOPING.md ├── SemanticLocators │ ├── SemanticLocators.Tests │ │ ├── SemanticLocators.Tests.csproj │ │ ├── TestAssignment.cs │ │ └── UnitTests.cs │ ├── SemanticLocators │ │ ├── SemanticLocatorException.cs │ │ └── SemanticLocators.csproj │ ├── SemanticLocators.sln │ └── .gitignore └── README.md ├── CONTRIBUTING.md └── README.md /javascript/.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/parser.ts -------------------------------------------------------------------------------- /webdriver_ts/.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/parser.ts -------------------------------------------------------------------------------- /javascript/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | -------------------------------------------------------------------------------- /webdriver_python/src/data/.gitignore: -------------------------------------------------------------------------------- 1 | wrapper_bin.js -------------------------------------------------------------------------------- /webdriver_ts/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | -------------------------------------------------------------------------------- /javascript/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /webdriver_ts/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /webdriver_java/src/main/resources/com/google/semanticlocators/.gitignore: -------------------------------------------------------------------------------- 1 | wrapper_bin.js -------------------------------------------------------------------------------- /webdriver_python/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | dist/ 3 | .pytype/ 4 | 5 | geckodriver.log 6 | -------------------------------------------------------------------------------- /webdriver_python/scripts/type_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | poetry run pytype src test 4 | -------------------------------------------------------------------------------- /docs/img/outer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/outer.png -------------------------------------------------------------------------------- /webdriver_python/scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | poetry run pylint src test --rcfile=.pylintrc 4 | -------------------------------------------------------------------------------- /docs/img/a11y_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/a11y_tree.png -------------------------------------------------------------------------------- /docs/img/wildcard_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/wildcard_name.png -------------------------------------------------------------------------------- /docs/img/explicit_semantics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/explicit_semantics.png -------------------------------------------------------------------------------- /docs/img/native_semantics_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/native_semantics_link.png -------------------------------------------------------------------------------- /docs/img/aria_labelledby_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/aria_labelledby_example.png -------------------------------------------------------------------------------- /docs/img/native_semantics_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/semantic-locators/HEAD/docs/img/native_semantics_button.png -------------------------------------------------------------------------------- /webdriver_python/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp ../javascript/wrapper/wrapper_bin.js src/data/ 4 | 5 | poetry build 6 | -------------------------------------------------------------------------------- /webdriver_python/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp ../javascript/wrapper/wrapper_bin.js src/data/ 4 | 5 | poetry run python -m unittest 6 | -------------------------------------------------------------------------------- /webdriver_go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/semantic-locators/webdriver_go 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/tebeka/selenium v0.9.9 // indirect 7 | ) -------------------------------------------------------------------------------- /webdriver_python/scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp ../javascript/wrapper/wrapper_bin.js src/data/ 4 | 5 | poetry run python -m unittest 6 | poetry publish --build -u semantic-locators -p $1 7 | -------------------------------------------------------------------------------- /javascript/src/semantic_locators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export {findElementBySemanticLocator, findElementsBySemanticLocator} from './lib/find_by_semantic_locator'; 8 | -------------------------------------------------------------------------------- /webdriver_ts/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporters": [ 3 | { 4 | "name": "jasmine-spec-reporter#SpecReporter", 5 | "options": { 6 | "displayStacktrace": "all" 7 | } 8 | } 9 | ], 10 | "spec_dir": "test", 11 | "spec_files": ["**/*.ts"] 12 | } -------------------------------------------------------------------------------- /pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to playground 4 | 5 | 6 | -------------------------------------------------------------------------------- /javascript/src/gen/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export {batchClosestPreciseLocatorFor, batchClosestSimpleLocatorFor, batchPreciseLocatorFor, closestPreciseLocatorFor, closestSimpleLocatorFor, preciseLocatorFor, simpleLocatorFor} from '../lib/locator_gen'; 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of significant contributors to Semantic Locators. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Google LLC 8 | Jordan Mace (jordan-mace) 9 | -------------------------------------------------------------------------------- /docs/img/icon_64dp.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a feature for Semantic Locators 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature** 11 | A clear and concise description of what the new feature would be. 12 | 13 | **Use case** 14 | Why would you like this feature? What impact will it have for you? 15 | -------------------------------------------------------------------------------- /webdriver_python/.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | 3 | # Allow tests and private functions without docstrings 4 | no-docstring-rgx=(test_.*|_.*) 5 | 6 | [FORMAT] 7 | 8 | # Maximum number of characters on a single line. 9 | max-line-length=80 10 | 11 | # String used as indentation unit. We differ from PEP8's normal 4 spaces. 12 | indent-string=' ' 13 | 14 | # Allow long lines which are just a string 15 | ignore-long-lines = ^\s*\"[^\"]+\",?$ 16 | 17 | 18 | [MESSAGES CONTROL] 19 | 20 | # The order must be different internally vs externally 21 | disable=wrong-import-order 22 | -------------------------------------------------------------------------------- /webdriver_dotnet/DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing Semantic Locators C# 2 | 3 | Install 4 | [ChromeDriver](https://chromedriver.chromium.org/getting-started) and 5 | [geckodriver](https://github.com/mozilla/geckodriver) if they're not already 6 | installed. 7 | 8 | Get a copy of the code and verify that tests pass on your system 9 | 10 | ```bash 11 | git clone https://github.com/google/semantic-locators.git 12 | cd semantic-locators/webdriver_dotnet/SemanticLocators/SemanticLocators.Tests 13 | dotnet test 14 | ``` 15 | 16 | ## Design 17 | 18 | this version of Semantic Locators is effectively a port of the Java integration. 19 | -------------------------------------------------------------------------------- /webdriver_python/src/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Semantic Locators Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /webdriver_python/test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Semantic Locators Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /webdriver_python/src/data/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Semantic Locators Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /webdriver_dotnet/SemanticLocators/SemanticLocators.Tests/SemanticLocators.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /webdriver_python/DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing Semantic Locators Python 2 | 3 | Install [Poetry](https://python-poetry.org/docs/), 4 | [ChromeDriver](https://chromedriver.chromium.org/getting-started) and 5 | [geckodriver](https://github.com/mozilla/geckodriver) if they're not already 6 | installed. 7 | 8 | Get a copy of the code and verify that tests pass on your system 9 | 10 | ```bash 11 | git clone https://github.com/google/semantic-locators.git 12 | cd semantic-locators/webdriver_python 13 | poetry install 14 | ./scripts/test.sh 15 | ``` 16 | 17 | ## Design 18 | 19 | The WebDriver version of Semantic Locators is a thin wrapper around the 20 | JavaScript implementation. It essentially performs `driver.execute_script` plus 21 | some error handling. 22 | -------------------------------------------------------------------------------- /webdriver_java/DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing Semantic Locators Java 2 | 3 | Install 4 | [Maven](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html), 5 | [ChromeDriver](https://chromedriver.chromium.org/getting-started) and 6 | [geckodriver](https://github.com/mozilla/geckodriver) if they're not already 7 | installed. 8 | 9 | Get a copy of the code and verify that tests pass on your system 10 | 11 | ```bash 12 | git clone https://github.com/google/semantic-locators.git 13 | cd semantic-locators/webdriver_java 14 | mvn test 15 | ``` 16 | 17 | ## Design 18 | 19 | The WebDriver version of Semantic Locators is a thin wrapper around the 20 | JavaScript implementation. It essentially performs `driver.executeScript` plus 21 | some error handling. 22 | -------------------------------------------------------------------------------- /javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2019", 7 | "dom", 8 | "dom.iterable" 9 | ], 10 | "types": [ 11 | "jasmine" 12 | ], 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "inlineSources": true, 17 | "outDir": "dist/", 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitAny": true, 23 | "noImplicitThis": true 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "test/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "dist/", 31 | "node_modules/" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /webdriver_ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2019", 7 | ], 8 | "types": [ 9 | "jasmine", 10 | "node", 11 | "selenium-webdriver", 12 | ], 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "inlineSources": true, 17 | "outDir": "dist/", 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitAny": true, 23 | "noImplicitThis": true 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "test/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "dist/", 31 | "node_modules/" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /javascript/README.md: -------------------------------------------------------------------------------- 1 | # Semantic Locators in JS/TS 2 | 3 | Semantic locators can be used in JS or TS running in the browser. 4 | 5 | ```bash 6 | $ npm install --save-dev semantic-locators 7 | ``` 8 | 9 | Once installed, use Semantic Locators as follows: 10 | 11 | ```typescript 12 | import {findElementBySemanticLocator, findElementsBySemanticLocator} from 'semantic-locators'; 13 | import {closestPreciseLocatorFor} from 'semantic-locators/gen' 14 | ... 15 | const searchButton = findElementBySemanticLocator("{button 'Google search'}"); 16 | const allButtons = findElementsBySemanticLocator("{button}"); 17 | 18 | const generated = closestPreciseLocatorFor(searchButton); // {button 'Google search'} 19 | ``` 20 | 21 | General Semantic Locator documentation can be found on 22 | [GitHub](http://github.com/google/semantic-locators#readme). 23 | -------------------------------------------------------------------------------- /webdriver_python/test/webdrivers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Semantic Locators Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """A separate package for test webdriver instance. 15 | 16 | This is to allow for different webdrivers internally vs externally. 17 | """ 18 | 19 | from . import webdrivers 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in Semantic Locators 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Browser and version: [e.g. Chrome v87] 29 | - Semantic Locators environment [e.g. JavaScript, Java WebDriver, All] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /webdriver_python/README.md: -------------------------------------------------------------------------------- 1 | # Semantic Locators in Python WebDriver 2 | 3 | Semantic locators can be used with Selenium WebDriver in a similar way to 4 | `ByXPath` or `ByCssSelector`. Currently only available for Python 3.7+. 5 | 6 | Install from PyPi: 7 | 8 | `python -m pip install semantic-locators` 9 | 10 | Once installed, use Semantic Locators as follows: 11 | 12 | ```python 13 | from semantic_locators import ( 14 | find_element_by_semantic_locator, 15 | find_elements_by_semantic_locator, 16 | closest_precise_locator_for, 17 | ) 18 | ... 19 | 20 | search_button = find_element_by_semantic_locator(driver, "{button 'Google search'}") 21 | all_buttons = find_elements_by_semantic_locator(driver, "{button}") 22 | 23 | generated = closest_precise_locator_for(search_button); # {button 'Google search'} 24 | ``` 25 | 26 | General Semantic Locator documentation can be found on 27 | [GitHub](http://github.com/google/semantic-locators#readme). 28 | -------------------------------------------------------------------------------- /javascript/src/lib/outer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {assertInDocumentOrder} from './util'; 8 | 9 | /** 10 | * Return an Array containing only the outer nodes from the input array. i.e. 11 | * remove nodes which are contained by other nodes in the list. Also remove 12 | * duplicate elements as a.contains(a) === true 13 | * 14 | * Throws a ValueError if nodes are not in document order. 15 | */ 16 | export function outerNodesOnly(nodes: readonly T[]): 17 | readonly T[] { 18 | assertInDocumentOrder(nodes); 19 | 20 | if (nodes.length === 0) { 21 | return []; 22 | } 23 | const filtered = [nodes[0]]; 24 | for (const node of nodes) { 25 | // The last element of filtered is the current outer node 26 | if (!filtered[filtered.length - 1].contains(node)) { 27 | filtered.push(node); 28 | } 29 | } 30 | return filtered; 31 | } 32 | -------------------------------------------------------------------------------- /javascript/src/lib/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Base class for all semantic locator errors. Errors thrown by semantic 9 | * locators will extend this error. 10 | */ 11 | export class SemanticLocatorError extends Error { 12 | errorName = 'SemanticLocatorError'; 13 | extendedMessage(): string { 14 | return `${this.errorName}: ${this.message}`; 15 | } 16 | } 17 | 18 | /** No element found for the given locator. */ 19 | export class NoSuchElementError extends SemanticLocatorError { 20 | override errorName = 'NoSuchElementError'; 21 | } 22 | 23 | /** Invalid value passed to a function. */ 24 | export class ValueError extends SemanticLocatorError { 25 | override errorName = 'ValueError'; 26 | } 27 | 28 | /** A locator is invalid. */ 29 | export class InvalidLocatorError extends SemanticLocatorError { 30 | override errorName = 'InvalidLocatorError'; 31 | } 32 | -------------------------------------------------------------------------------- /webdriver_dotnet/README.md: -------------------------------------------------------------------------------- 1 | # Semantic Locators in C# WebDriver 2 | 3 | Semantic locators can be used with Selenium WebDriver in a similar way to 4 | `ByXPath` or `ByCssSelector`. 5 | 6 | .NET 5 is currently the only supported version of dotnet 7 | 8 | Add the NuGet package to your project (Note that this is not on NuGet yet) 9 | To build the package, run `dotnet publish` in the `webdriver_dotnet\SemanticLocators` directory 10 | 11 | Once installed, use Semantic Locators as follows: 12 | 13 | ```csharp 14 | using SemanticLocators; 15 | ... 16 | 17 | IWebElement searchButton = driver.FindElement(new BySemanticLocator("{button 'Google search'}")); 18 | List allButtons = driver.FindElements(new BySemanticLocator("{button}")); 19 | 20 | string generated = BySemanticLocator.ClosestPreciseLocatorFor(searchButton); // {button 'Google search'} 21 | ``` 22 | 23 | General Semantic Locator documentation can be found on 24 | [GitHub](http://github.com/google/semantic-locators#readme). 25 | -------------------------------------------------------------------------------- /.github/workflows/github_pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'pages/**' 8 | 9 | jobs: 10 | deploy_pages: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 5 13 | steps: 14 | - name: Harden Runner 15 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 16 | with: 17 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 18 | 19 | - name: Cancel previous 20 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 21 | with: 22 | access_token: ${{ github.token }} 23 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 24 | 25 | - name: Deploy 26 | uses: JamesIves/github-pages-deploy-action@164583b9e44b4fc5910e78feb607ea7c98d3c7b9 # 4.1.1 27 | with: 28 | branch: gh-pages 29 | folder: pages 30 | -------------------------------------------------------------------------------- /webdriver_java/README.md: -------------------------------------------------------------------------------- 1 | # Semantic Locators in Java WebDriver 2 | 3 | Semantic locators can be used with Selenium WebDriver in a similar way to 4 | `ByXPath` or `ByCssSelector`. Currently only available for Java 8+. 5 | 6 | Add the following to your `pom.xml`: 7 | 8 | ```xml 9 | 10 | com.google.semanticlocators 11 | semantic-locators 12 | 2.1.0 13 | test 14 | 15 | ``` 16 | 17 | Once installed, use Semantic Locators as follows: 18 | 19 | ```java 20 | import com.google.semanticlocators.BySemanticLocator; 21 | ... 22 | 23 | WebElement searchButton = driver.findElement(new BySemanticLocator("{button 'Google search'}")); 24 | ArrayList allButtons = driver.findElements(new BySemanticLocator("{button}")); 25 | 26 | String generated = BySemanticLocator.closestPreciseLocatorFor(searchButton); // {button 'Google search'} 27 | ``` 28 | 29 | General Semantic Locator documentation can be found on 30 | [GitHub](http://github.com/google/semantic-locators#readme). 31 | -------------------------------------------------------------------------------- /webdriver_ts/README.md: -------------------------------------------------------------------------------- 1 | # Semantic Locators in JS/TS 2 | 3 | Semantic locators can be used in JS or TS running in the browser. 4 | 5 | ```bash 6 | $ npm install --save-dev webdriver-semantic-locators 7 | ``` 8 | 9 | Once installed, use Semantic Locators as follows: 10 | 11 | ```typescript 12 | import {bySemanticLocator, findElementBySemanticLocator, findElementsBySemanticLocator, closestPreciseLocatorFor} from 'webdriver-semantic-locators'; 13 | import {closestPreciseLocatorFor} from 'semantic-locators/gen' 14 | ... 15 | const searchBar = driver.findElement(bySemanticLocator("{header 'Search'}")) 16 | const searchButton = findElementBySemanticLocator(driver, "{button 'Google search'}", searchBar); // or searchBar.findElement(bySemanticLocator("{button 'Google search'}")); 17 | const allButtons = findElementsBySemanticLocator(driver, "{button}"); 18 | 19 | const generated = closestPreciseLocatorFor(driver, searchButton); // {button 'Google search'} 20 | ``` 21 | 22 | General Semantic Locator documentation can be found on 23 | [GitHub](http://github.com/google/semantic-locators#readme). 24 | -------------------------------------------------------------------------------- /webdriver_python/test/webdrivers/webdrivers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Semantic Locators Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """WebDriver instances for unit tests.""" 15 | from selenium.webdriver import (Chrome, Firefox, ChromeOptions, FirefoxOptions) 16 | 17 | chrome_options = ChromeOptions() 18 | chrome_options.headless = True 19 | 20 | firefox_options = FirefoxOptions() 21 | firefox_options.headless = True 22 | 23 | DRIVERS = { 24 | "chrome": Chrome(options=chrome_options), 25 | "firefox": Firefox(options=firefox_options), 26 | } 27 | -------------------------------------------------------------------------------- /webdriver_ts/src/web_drivers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {Builder, WebDriver} from 'selenium-webdriver'; 8 | import {Options as ChromeOptions} from 'selenium-webdriver/chrome'; 9 | import {Options as FirefoxOptions} from 'selenium-webdriver/firefox'; 10 | 11 | /** A map of WebDriver instances to test over. */ 12 | export const DRIVERS = new Map WebDriver>([ 13 | [ 14 | 'Chrome', 15 | () => { 16 | const chromeOptions = new ChromeOptions(); 17 | chromeOptions.addArguments('--headless'); 18 | return new Builder() 19 | .forBrowser('chrome') 20 | .setChromeOptions(chromeOptions) 21 | .build(); 22 | }, 23 | ], 24 | [ 25 | 'FireFox', 26 | () => { 27 | const firefoxOptions = new FirefoxOptions(); 28 | firefoxOptions.addArguments('--headless'); 29 | return new Builder() 30 | .forBrowser('firefox') 31 | .setFirefoxOptions(firefoxOptions) 32 | .build(); 33 | }, 34 | ], 35 | ]); 36 | -------------------------------------------------------------------------------- /javascript/test/lib/role_map_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {CHILDREN_PRESENTATIONAL, isAriaOnlyRole, ROLE_MAP} from '../../src/lib/role_map'; 8 | 9 | describe('CHILDREN_PRESENTATIONAL', () => { 10 | // If every role with presentational children is either: 11 | // (a) ARIA only; 12 | // (b) `exactSelector` only; or 13 | // (c) cannot have html descendants 14 | // Then we don't need to explicitly evaluate any conditions when checking for 15 | // presentational children as it can all be expressed with css selectors 16 | for (const role of CHILDREN_PRESENTATIONAL) { 17 | it(`role ${role} doesn't need explicit conditions to be checked`, () => { 18 | expect( 19 | isAriaOnlyRole(role) || 20 | ROLE_MAP[role].conditionalSelectors === undefined || 21 | // cannot have html descendants 22 | ROLE_MAP[role].conditionalSelectors!.every( 23 | selector => selector.greedySelector === 'input')) 24 | .toBeTrue(); 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /webdriver_java/src/main/java/com/google/semanticlocators/SemanticLocatorException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.semanticlocators; 18 | 19 | /** 20 | * General exception from Semantic Locators. If a native WebDriver exception is more specific (such 21 | * as NoSuchElementException), that will be thrown instead. 22 | */ 23 | public class SemanticLocatorException extends RuntimeException { 24 | public SemanticLocatorException(String message) { 25 | super(message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /javascript/karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | module.exports = (config) => { 8 | config.set({ 9 | frameworks: ['jasmine', 'karma-typescript'], 10 | 11 | plugins: [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher', 14 | 'karma-firefox-launcher', 15 | 'karma-typescript', 16 | 'karma-spec-reporter', 17 | ], 18 | 19 | karmaTypescriptConfig: { 20 | tsconfig: './tsconfig.json', 21 | bundlerOptions: { 22 | transforms: [ 23 | require('karma-typescript-es6-transform')({ 24 | plugins: ["@babel/transform-runtime"] 25 | }), 26 | ], 27 | }, 28 | }, 29 | 30 | files: [ 31 | {pattern: 'src/**/*.ts'}, 32 | {pattern: 'test/**/*.ts'}, 33 | ], 34 | 35 | preprocessors: {'**/*.ts': 'karma-typescript'}, 36 | 37 | reporters: ['progress', 'spec', 'karma-typescript'], 38 | 39 | port: 9876, 40 | colors: true, 41 | logLevel: config.LOG_INFO, 42 | autoWatch: true, 43 | browsers: ['Chrome', 'Firefox'], 44 | singleRun: false, 45 | concurrency: Infinity 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /webdriver_python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "semantic-locators" 3 | version = "2.1.0" 4 | description = "Semantic Locators are a human readable, resilient and accessibility-enforcing way to find web elements. This package adds semantic locator support to webdriver" 5 | authors = ["Alex Lloyd "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/google/semantic-locators" 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Operating System :: OS Independent", 12 | "Intended Audience :: Developers", 13 | "Topic :: Software Development :: Testing" 14 | ] 15 | include = [ 16 | "src/**/*.py", 17 | "src/data/wrapper_bin.js" 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | python = ">=3.7,<3.11" # TODO(ovn) allow python 3.11 once pytype does (https://github.com/google/pytype/blob/master/setup.cfg#L30) 22 | importlib-resources = ">=5.1.2" 23 | selenium = ">=3.141.0" 24 | 25 | [tool.poetry.dev-dependencies] 26 | absl-py = ">=0.12.0" 27 | pylint = ">=2.7.4" 28 | pytype = "^2023.04.27" 29 | 30 | [build-system] 31 | requires = ["poetry-core>=1.0.0"] 32 | build-backend = "poetry.core.masonry.api" 33 | -------------------------------------------------------------------------------- /.github/workflows/lint_webdriver_java.yml: -------------------------------------------------------------------------------- 1 | name: Lint webdriver_java 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'webdriver_java/**' 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - 'webdriver_java/**' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | lint_webdriver_java: 19 | permissions: 20 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 21 | contents: read # for actions/checkout to fetch code 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 27 | with: 28 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 29 | 30 | - name: Cancel previous 31 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 32 | with: 33 | access_token: ${{ github.token }} 34 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 35 | 36 | - name: Lint webdriver_java 37 | uses: axel-op/googlejavaformat-action@fe78db8a90171b6a836449f8d0e982d5d71e5c5a # v3 38 | with: 39 | args: "--dry-run --set-exit-if-changed" 40 | -------------------------------------------------------------------------------- /javascript/src/lib/parse_locator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {InvalidLocatorError} from './error'; 8 | import {parse as pegParse} from './parser'; 9 | import {SemanticLocator} from './semantic_locator'; 10 | import {debug} from './util'; 11 | 12 | /** 13 | * Parse the input string (e.g. `{button 'OK'}`) to a SemanticLocator. 14 | * Validation is performed that (for example) all roles are correct, so all 15 | * parsed locators should be valid. 16 | */ 17 | export function parse(input: string): SemanticLocator { 18 | let parsed; 19 | try { 20 | parsed = pegParse(input); 21 | } catch (error: unknown) { 22 | if (error instanceof InvalidLocatorError) { 23 | throw error; 24 | } 25 | throw new InvalidLocatorError( 26 | `Failed to parse semantic locator "${input}". ` + 27 | `${(error as Error).message ?? error}`); 28 | } 29 | 30 | if (debug() && !(parsed instanceof SemanticLocator)) { 31 | throw new Error( 32 | `parse(${input}) didn't return a SemanticLocator.` + 33 | ` Return value ${JSON.stringify(parsed)}`); 34 | } 35 | const locator = parsed as SemanticLocator; 36 | 37 | if (locator.preOuter.length === 0 && locator.postOuter.length === 0) { 38 | throw new InvalidLocatorError('Locator is empty'); 39 | } 40 | 41 | return locator; 42 | } 43 | -------------------------------------------------------------------------------- /webdriver_dotnet/SemanticLocators/SemanticLocators/SemanticLocatorException.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // 4 | // Copyright 2021 The Semantic Locators Authors 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | // 19 | //----------------------------------------------------------------------- 20 | using System; 21 | 22 | namespace SemanticLocators 23 | { 24 | /** 25 | * General exception from Semantic Locators. If a native WebDriver exception is more specific (such 26 | * as NoSuchElementException), that will be thrown instead. 27 | */ 28 | public class SemanticLocatorException : Exception 29 | { 30 | public SemanticLocatorException(string message) : base(message) { } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/test_javascript.yml: -------------------------------------------------------------------------------- 1 | name: Test javascript 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - 'javascript/**' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | test_javascript: 19 | permissions: 20 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 21 | contents: read # for actions/checkout to fetch code 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 27 | with: 28 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 29 | 30 | - name: Cancel previous 31 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 32 | with: 33 | access_token: ${{ github.token }} 34 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 35 | - name: Setup node 36 | uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2 37 | with: 38 | node-version: '14' 39 | 40 | - name: Test javascript 41 | working-directory: ./javascript 42 | run: | 43 | yarn install --frozen-lockfile 44 | yarn test 45 | -------------------------------------------------------------------------------- /.github/workflows/test_webdriver_dotnet.yml: -------------------------------------------------------------------------------- 1 | name: Test webdriver_dotnet 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | - 'webdriver_dotnet/**' 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - 'javascript/**' 14 | - 'webdriver_dotnet/**' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test_webdriver_dotnet: 21 | permissions: 22 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 23 | contents: read # for actions/checkout to fetch code 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 5 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 29 | with: 30 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 31 | 32 | - name: Cancel previous 33 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 34 | with: 35 | access_token: ${{ github.token }} 36 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 37 | - name: Setup .NET Core SDK 38 | uses: actions/setup-dotnet@a71d1eb2c86af85faa8c772c03fb365e377e45ea # v1.8.0 39 | - name: Test webdriver_dotnet 40 | run: | 41 | cd webdriver_dotnet/SemanticLocators 42 | dotnet test 43 | -------------------------------------------------------------------------------- /.github/workflows/test_webdriver_java.yml: -------------------------------------------------------------------------------- 1 | name: Test webdriver_java 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | - 'webdriver_java/**' 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - 'javascript/**' 14 | - 'webdriver_java/**' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test_webdriver_java: 21 | permissions: 22 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 23 | contents: read # for actions/checkout to fetch code 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 5 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 29 | with: 30 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 31 | 32 | - name: Cancel previous 33 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 34 | with: 35 | access_token: ${{ github.token }} 36 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 37 | - name: Setup JDK 1.8 38 | uses: actions/setup-java@e54a62b3df9364d4b4c1c29c7225e57fe605d7dd # v1 39 | with: 40 | java-version: 1.8 41 | 42 | - name: Test webdriver_java 43 | run: | 44 | cd webdriver_java 45 | mvn test 46 | -------------------------------------------------------------------------------- /.github/workflows/lint_javascript.yml: -------------------------------------------------------------------------------- 1 | name: Lint javascript 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - 'javascript/**' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | lint_javascript: 19 | permissions: 20 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 21 | contents: read # for actions/checkout to fetch code 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 27 | with: 28 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 29 | 30 | - name: Cancel previous 31 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 32 | with: 33 | access_token: ${{ github.token }} 34 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 35 | 36 | - name: Setup node 37 | uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2 38 | with: 39 | node-version: '14' 40 | 41 | - name: Lint javascript 42 | working-directory: ./javascript 43 | run: | 44 | yarn install --frozen-lockfile 45 | yarn check-lint 46 | yarn check-format 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## File an issue 20 | 21 | Before you start developing with Semantic Locators, please file an issue (or 22 | comment on an existing one) so we can track what's being worked on. 23 | 24 | ## Developing 25 | 26 | See [DEVELOPING.md](docs/DEVELOPING.md) 27 | 28 | ## Code reviews 29 | 30 | All submissions, including submissions by project members, require review. 31 | Please open a pull request and it will be reviewed. See 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. 34 | 35 | ## Community Guidelines 36 | 37 | This project follows 38 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 39 | -------------------------------------------------------------------------------- /.github/workflows/lint_webdriver_ts.yml: -------------------------------------------------------------------------------- 1 | name: Lint javascript 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'webdriver_ts/**' 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - 'webdriver_ts/**' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | lint_webdriver_ts: 19 | permissions: 20 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 21 | contents: read # for actions/checkout to fetch code 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 27 | with: 28 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 29 | 30 | - name: Cancel previous 31 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 32 | with: 33 | access_token: ${{ github.token }} 34 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 35 | 36 | - name: Setup node 37 | uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2 38 | with: 39 | node-version: '14' 40 | 41 | - name: Lint javascript 42 | working-directory: ./webdriver_ts 43 | run: | 44 | yarn install --frozen-lockfile 45 | yarn check-lint 46 | yarn check-format 47 | -------------------------------------------------------------------------------- /javascript/src/lib/semantic_locator.pegjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** PEG.js grammar for semantic locators. */ 8 | 9 | Locator = preOuter:Node* postOuter:OuterAndPostOuter? { 10 | postOuter = postOuter || []; 11 | return new SemanticLocator(preOuter, postOuter); 12 | } 13 | 14 | OuterAndPostOuter = "outer"? _ postOuter:Node+ {return postOuter;} 15 | 16 | Node = "{" _ role:Word _ name:QuotedString? _ attributes:Attribute* _ "}" _ { 17 | if (name) { 18 | return new SemanticNode(role, attributes, name); 19 | } 20 | return new SemanticNode(role, attributes); 21 | } 22 | 23 | Attribute = _ name:Word ":" value:AlphaNum _ {return {name, value};} 24 | 25 | // A string of lower case letters. 26 | Word = chars:[a-z]+ {return chars.join(''); } 27 | 28 | AlphaNum = chars:[a-z0-9_]+ {return chars.join(''); } 29 | 30 | /* String matching support with escapes */ 31 | QuotedString 32 | = '"' chars:DoubleStringCharacter* '"' { return chars.join(''); } 33 | / "'" chars:SingleStringCharacter* "'" { return chars.join(''); } 34 | 35 | DoubleStringCharacter 36 | = !('"' / "\\") char:. { return char; } 37 | / "\\" sequence:EscapeSequence { return sequence; } 38 | 39 | SingleStringCharacter 40 | = !("'" / "\\") char:. { return char; } 41 | / "\\" sequence:EscapeSequence { return sequence; } 42 | 43 | EscapeSequence 44 | = "'" 45 | / '"' 46 | / "\\" 47 | 48 | _ "space" 49 | = [ \t]* 50 | -------------------------------------------------------------------------------- /.github/workflows/test_webdriver_ts.yml: -------------------------------------------------------------------------------- 1 | name: Test javascript 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | - 'webdriver_ts/**' 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - 'javascript/**' 14 | - 'webdriver_ts/**' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test_webdriver_ts: 21 | permissions: 22 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 23 | contents: read # for actions/checkout to fetch code 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 5 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 29 | with: 30 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 31 | 32 | - name: Cancel previous 33 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 34 | with: 35 | access_token: ${{ github.token }} 36 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 37 | - name: Setup node 38 | uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2 39 | with: 40 | node-version: '14' 41 | 42 | - name: Test webdriver_ts 43 | working-directory: ./webdriver_ts 44 | run: | 45 | yarn install --frozen-lockfile 46 | yarn test 47 | -------------------------------------------------------------------------------- /.github/workflows/lint_webdriver_python.yml: -------------------------------------------------------------------------------- 1 | name: Lint webdriver_python 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'webdriver_python/**' 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - 'webdriver_python/**' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | lint_webdriver_python: 19 | permissions: 20 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 21 | contents: read # for actions/checkout to fetch code 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 27 | with: 28 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 29 | 30 | - name: Cancel previous 31 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 32 | with: 33 | access_token: ${{ github.token }} 34 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 35 | - uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a # v2 36 | with: 37 | python-version: 3.6 38 | - uses: abatilo/actions-poetry@1f9adef0261964471fcf93ba269e1762a33a8a26 # v2.0.0 39 | 40 | - name: Lint webdriver_python 41 | run: | 42 | cd webdriver_python 43 | poetry install 44 | ./scripts/lint.sh 45 | -------------------------------------------------------------------------------- /.github/workflows/types_webdriver_python.yml: -------------------------------------------------------------------------------- 1 | name: Type check webdriver_python 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'webdriver_python/**' 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - 'webdriver_python/**' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | types_webdriver_python: 19 | permissions: 20 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 21 | contents: read # for actions/checkout to fetch code 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 27 | with: 28 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 29 | 30 | - name: Cancel previous 31 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 32 | with: 33 | access_token: ${{ github.token }} 34 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 35 | - uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a # v2 36 | with: 37 | python-version: 3.6 38 | - uses: abatilo/actions-poetry@1f9adef0261964471fcf93ba269e1762a33a8a26 # v2.0.0 39 | 40 | - name: Type check webdriver_python 41 | run: | 42 | cd webdriver_python 43 | poetry install 44 | ./scripts/type_check.sh -------------------------------------------------------------------------------- /webdriver_dotnet/SemanticLocators/SemanticLocators.Tests/TestAssignment.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // 4 | // Copyright 2021 The Semantic Locators Authors 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | // 19 | //----------------------------------------------------------------------- 20 | using System; 21 | using System.Collections.Generic; 22 | using System.Linq; 23 | using System.Text; 24 | using System.Threading.Tasks; 25 | 26 | namespace SemanticLocators.Tests 27 | { 28 | 29 | public class TestAssignment 30 | { 31 | public string Semantic { get; set; } 32 | public string Html { get; set; } 33 | public string BrowserName { get; set; } 34 | 35 | public override string ToString() 36 | { 37 | return $"{BrowserName}, {Semantic}"; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webdriver_dotnet/SemanticLocators/SemanticLocators/SemanticLocators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | SemanticLocators Contributors 7 | Google 8 | SemanticLocators 9 | Semantic Locators let you specify HTML elements in code similar to how you might 10 | describe them to a human. Semantic Locators are stable, readable, enforce accessibility, and can be 11 | auto-generated. 12 | https://github.com/google/semantic-locators 13 | https://github.com/google/semantic-locators 14 | Apache-2.0 15 | true 16 | 1.0.0 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | true 26 | true 27 | Content 28 | true 29 | Always 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/test_webdriver_go.yml: -------------------------------------------------------------------------------- 1 | name: Test webdriver_go 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | - 'webdriver_go/**' 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - 'javascript/**' 14 | - 'webdriver_go/**' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test_webdriver_go: 21 | permissions: 22 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 23 | contents: read # for actions/checkout to fetch code 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 5 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 29 | with: 30 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 31 | 32 | - name: Cancel previous 33 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 34 | with: 35 | access_token: ${{ github.token }} 36 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 37 | - name: Setup Go 38 | uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2 39 | with: 40 | go-version: 1.16.5 41 | - name: Go Build 42 | run: cd webdriver_go && go build ./... 43 | # TODO - Uncomment when newWebdriver in semloc_test.go is implemented 44 | # - name: Go Test 45 | # run: cd webdriver_go && go test ./... 46 | -------------------------------------------------------------------------------- /.github/workflows/test_webdriver_python.yml: -------------------------------------------------------------------------------- 1 | name: Test webdriver_python 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'javascript/**' 8 | - 'webdriver_python/**' 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - 'javascript/**' 14 | - 'webdriver_python/**' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test_webdriver_python: 21 | permissions: 22 | actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows 23 | contents: read # for actions/checkout to fetch code 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 5 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 29 | with: 30 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 31 | 32 | - name: Cancel previous 33 | uses: styfle/cancel-workflow-action@3d86a7cc43670094ac248017207be0295edbc31d # 0.8.0 34 | with: 35 | access_token: ${{ github.token }} 36 | - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 37 | - uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a # v2 38 | with: 39 | python-version: 3.6 40 | - uses: abatilo/actions-poetry@1f9adef0261964471fcf93ba269e1762a33a8a26 # v2.0.0 41 | 42 | - name: Test webdriver_python 43 | run: | 44 | cd webdriver_python 45 | poetry install 46 | ./scripts/test.sh 47 | -------------------------------------------------------------------------------- /javascript/src/lib/accessible_name.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {getAccessibleName} from 'accname'; 8 | 9 | import {cachedDuringBatch} from './batch_cache'; 10 | import {ValueError} from './error'; 11 | 12 | 13 | /** 14 | * Check if `actual` matches `expected`, where `expected` can include leading 15 | * and trailing wildcards. 16 | */ 17 | export function nameMatches(expected: string, actual: string): boolean { 18 | if (expected === '*') { 19 | throw new ValueError( 20 | '* is invalid as an accessible name. To match any ' + 21 | 'accessible name omit it from the locator e.g. {button}.'); 22 | } 23 | // TODO(alexlloyd) support escaping * to use in the accname (then this logic 24 | // should probably be moved to the pegjs parser) 25 | const nameParts = expected.split('*'); 26 | 27 | // If the expected string doesn't start/end with * then we must check the 28 | // start/end of the actual value 29 | if (!actual.startsWith(nameParts[0]) || 30 | !actual.endsWith(nameParts[nameParts.length - 1])) { 31 | return false; 32 | } 33 | 34 | let currentIndex = 0; 35 | for (const part of nameParts) { 36 | currentIndex = actual.indexOf(part, currentIndex); 37 | if (currentIndex === -1) { 38 | return false; 39 | } 40 | currentIndex += part.length; 41 | } 42 | return true; 43 | } 44 | 45 | /** 46 | * Return the accessible name for the given element according to 47 | * https://www.w3.org/TR/accname-1.1/ 48 | */ 49 | export const getNameFor = 50 | cachedDuringBatch((el: HTMLElement) => getAccessibleName(el)); 51 | -------------------------------------------------------------------------------- /webdriver_dotnet/SemanticLocators/SemanticLocators.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31205.134 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticLocators", "SemanticLocators\SemanticLocators.csproj", "{E5617EFE-7BAA-4769-8870-95F8540D68F2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticLocators.Tests", "SemanticLocators.Tests\SemanticLocators.Tests.csproj", "{D5ED4070-F0D9-4005-86EB-CFC81D7670DA}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {E5617EFE-7BAA-4769-8870-95F8540D68F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {E5617EFE-7BAA-4769-8870-95F8540D68F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {E5617EFE-7BAA-4769-8870-95F8540D68F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {E5617EFE-7BAA-4769-8870-95F8540D68F2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {D5ED4070-F0D9-4005-86EB-CFC81D7670DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {D5ED4070-F0D9-4005-86EB-CFC81D7670DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {D5ED4070-F0D9-4005-86EB-CFC81D7670DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {D5ED4070-F0D9-4005-86EB-CFC81D7670DA}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2256EAD7-E2B6-480B-8546-A269B2CF8F3B} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /webdriver_java/src/test/java/com/google/semanticlocators/WebDrivers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.semanticlocators; 18 | 19 | import com.google.common.collect.ImmutableMap; 20 | import org.openqa.selenium.WebDriver; 21 | import org.openqa.selenium.chrome.ChromeDriver; 22 | import org.openqa.selenium.chrome.ChromeOptions; 23 | import org.openqa.selenium.firefox.FirefoxDriver; 24 | import org.openqa.selenium.firefox.FirefoxOptions; 25 | 26 | /** 27 | * Provides WebDriver instances to the test. This constant is kept in a separate class so internal + 28 | * open source code can differ. 29 | */ 30 | final class WebDrivers { 31 | // Use a string to identify browsers so the test names are readable 32 | public static final ImmutableMap DRIVERS; 33 | 34 | static { 35 | ChromeOptions chromeOptions = new ChromeOptions(); 36 | chromeOptions.addArguments("--headless"); 37 | 38 | FirefoxOptions firefoxOptions = new FirefoxOptions(); 39 | firefoxOptions.addArguments("--headless"); 40 | 41 | DRIVERS = 42 | ImmutableMap.of( 43 | "chrome", new ChromeDriver(chromeOptions), 44 | "firefox", new FirefoxDriver(firefoxOptions)); 45 | } 46 | 47 | private WebDrivers() {} 48 | } 49 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## How can I handle internationalization/localization (i18n/L10n)? 4 | 5 | Semantic locators don't yet have built-in support for tests with strong 6 | cross-language requirements. In general you'll need a different locator for each 7 | locale, as `{button 'Hello'}` won't find `<button>Bonjour</button>`. 8 | Solutions to localized tests will be specific to your L10n method and test 9 | environment, but there are a few general approaches which may work. 10 | 11 | One solution is to parameterize tests based on the locale, and get the localized 12 | string from an id and locale. A pseudocode example: 13 | 14 | ``` 15 | function myTest(@Parameter locale): 16 | submitMsg = readFromTranslationFile("SUBMIT", locale) 17 | searchButton = findElementBySemanticLocator("{button '" + submitMsg + "'}") 18 | ``` 19 | 20 | Some libraries (such as [Closure](https://developers.google.com/closure/library) 21 | ) perform L10n when compiling JavaScript. It may be possible to access L10n APIs 22 | from your test and inject the localized strings into locators. For example in 23 | Closure: 24 | 25 | ```typescript 26 | const searchButton = findElementBySemanticLocator(`{button '${goog.getMsg('Search')}'}`); 27 | ``` 28 | 29 | ## Which browsers are supported? 30 | 31 | Semantic locators are tested on recent versions of: 32 | 33 | - Chrome 34 | - Firefox 35 | - Internet Explorer 36 | 37 | Bugs and patches are accepted for other major browsers. 38 | 39 | ## Please add support for XXX language/environment 40 | 41 | See the section "Integrating with your tests". If you can't add support yourself 42 | for a certain platform, feel free to file an issue on 43 | [GitHub](https://github.com/google/semantic-locators/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) 44 | 45 | ## I have more questions 46 | 47 | Please 48 | [file an issue on GitHub](https://github.com/google/semantic-locators/issues/new) 49 | to get in touch. 50 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # Only the default branch is supported. 4 | branch_protection_rule: 5 | schedule: 6 | - cron: '24 21 * * 2' 7 | push: 8 | branches: [ main ] 9 | 10 | # Declare default permissions as read only. 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # Needed to upload the results to code-scanning dashboard. 19 | security-events: write 20 | actions: read 21 | contents: read 22 | 23 | steps: 24 | - name: "Checkout code" 25 | uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 26 | with: 27 | persist-credentials: false 28 | 29 | - name: "Run analysis" 30 | uses: ossf/scorecard-action@c1aec4ac820532bab364f02a81873c555a0ba3a1 # v1.0.4 31 | with: 32 | results_file: results.sarif 33 | results_format: sarif 34 | # Read-only PAT token. To create it, 35 | # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. 36 | repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 37 | # Publish the results to enable scorecard badges. For more details, see 38 | # https://github.com/ossf/scorecard-action#publishing-results. 39 | # For private repositories, `publish_results` will automatically be set to `false`, 40 | # regardless of the value entered here. 41 | publish_results: true 42 | 43 | # Upload the results as artifacts (optional). 44 | - name: "Upload artifact" 45 | uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1 46 | with: 47 | name: SARIF file 48 | path: results.sarif 49 | retention-days: 5 50 | 51 | # Upload the results to GitHub's code scanning dashboard. 52 | - name: "Upload to code-scanning" 53 | uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26 54 | with: 55 | sarif_file: results.sarif -------------------------------------------------------------------------------- /javascript/wrapper/wrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {SemanticLocatorError} from 'google3/third_party/semantic_locators/javascript/lib/error'; 8 | import {QuoteChar} from 'google3/third_party/semantic_locators/javascript/lib/types'; 9 | import {findElementBySemanticLocator, findElementsBySemanticLocator} from 'semantic-locators'; 10 | import {closestPreciseLocatorFor, closestSimpleLocatorFor, preciseLocatorFor, simpleLocatorFor} from 'semantic-locators/gen'; 11 | 12 | /** 13 | * Error class is lost when returning from WebDriver.executeScript, so include 14 | * the class name in the error message. 15 | */ 16 | function wrapError(func: Function): Function { 17 | return (...args: unknown[]) => { 18 | try { 19 | return func(...args); 20 | } catch (error: unknown) { 21 | if (error instanceof SemanticLocatorError) { 22 | error = new Error(error.extendedMessage()); 23 | } 24 | throw error; 25 | } 26 | }; 27 | } 28 | 29 | function exportGlobal(name: string, value: unknown) { 30 | if (typeof value === 'function') { 31 | value = wrapError(value); 32 | } 33 | // tslint:disable-next-line:no-any Set global. 34 | (window as any)[name] = value; 35 | } 36 | 37 | exportGlobal('findElementsBySemanticLocator', findElementsBySemanticLocator); 38 | exportGlobal('findElementBySemanticLocator', findElementBySemanticLocator); 39 | exportGlobal( 40 | 'closestPreciseLocatorFor', 41 | (element: HTMLElement, rootEl?: HTMLElement, quoteChar?: QuoteChar) => 42 | closestPreciseLocatorFor(element, {rootEl, quoteChar})); 43 | exportGlobal( 44 | 'preciseLocatorFor', 45 | (element: HTMLElement, rootEl?: HTMLElement, quoteChar?: QuoteChar) => 46 | preciseLocatorFor(element, {rootEl, quoteChar})); 47 | exportGlobal( 48 | 'closestSimpleLocatorFor', 49 | (element: HTMLElement, rootEl?: HTMLElement, quoteChar?: QuoteChar) => 50 | closestSimpleLocatorFor(element, {rootEl, quoteChar})); 51 | exportGlobal('simpleLocatorFor', simpleLocatorFor); 52 | 53 | // Marker for clients to check that semantic locators have been loaded 54 | exportGlobal('semanticLocatorsReady', true); 55 | -------------------------------------------------------------------------------- /javascript/test/lib/outer_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {html, render} from 'lit'; 8 | 9 | import {outerNodesOnly} from '../../src/lib/outer'; 10 | 11 | describe('outerNodesOnly', () => { 12 | let container: HTMLElement; 13 | 14 | let first: HTMLElement; 15 | let second: HTMLElement; 16 | let secondFirst: HTMLElement; 17 | let secondFirstFirst: HTMLElement; 18 | let third: HTMLElement; 19 | let thirdFirst: HTMLElement; 20 | let thirdFirstFirst: HTMLElement; 21 | 22 | beforeAll(() => { 23 | container = document.createElement('div'); 24 | document.body.appendChild(container); 25 | render( 26 | html` 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
`, 38 | container); 39 | first = document.getElementById('first')!; 40 | second = document.getElementById('second')!; 41 | secondFirst = document.getElementById('second-first')!; 42 | secondFirstFirst = document.getElementById('second-first-first')!; 43 | third = document.getElementById('third')!; 44 | thirdFirst = document.getElementById('third-first')!; 45 | thirdFirstFirst = document.getElementById('third-first-first')!; 46 | }); 47 | 48 | afterAll(() => { 49 | document.body.removeChild(container); 50 | }); 51 | 52 | 53 | it('removes inner nodes', () => { 54 | expect(outerNodesOnly([ 55 | first, second, secondFirst, secondFirstFirst, third, thirdFirst, 56 | thirdFirstFirst 57 | ])).toEqual([first, second, third]); 58 | expect(outerNodesOnly([ 59 | first, secondFirst, secondFirstFirst, thirdFirst, thirdFirstFirst 60 | ])).toEqual([first, secondFirst, thirdFirst]); 61 | }); 62 | 63 | it('removes repeated nodes', () => { 64 | expect(outerNodesOnly([first, first, first])).toEqual([first]); 65 | }); 66 | 67 | it('preserves an array of only outer nodes', () => { 68 | expect(outerNodesOnly([first, second, third])).toEqual([ 69 | first, second, third 70 | ]); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /javascript/src/lib/batch_cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {assert} from './util'; 8 | 9 | const caches: Array> = []; 10 | let isBatchOp = false; 11 | 12 | /** Whether we're currently in a batch operation. */ 13 | export function inBatchOp() { 14 | return isBatchOp; 15 | } 16 | 17 | /** 18 | * Run a function during which all relevant functions get their results cached 19 | */ 20 | export function runBatchOp(fn: () => void): void { 21 | assert(!inBatchOp(), 'Already in a batch operation'); 22 | isBatchOp = true; 23 | try { 24 | fn(); 25 | } finally { 26 | assert(inBatchOp(), 'Not in a batch operation'); 27 | isBatchOp = false; 28 | for (const cache of caches) { 29 | cache.clear(); 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * Return a version of a function whose results get cached during batch 36 | * operations 37 | */ 38 | export function cachedDuringBatch( 39 | fn: (...args: Args) => Ret): (...args: Args) => Ret { 40 | const cache = new Map(); 41 | caches.push(cache); 42 | 43 | return (...args: Args) => { 44 | if (!isBatchOp) { 45 | return fn(...args); 46 | } 47 | 48 | // Find the local cache 49 | let localCache = cache; 50 | for (const arg of args) { 51 | const key = isCacheableObject(arg) ? arg.hashCode() : arg; 52 | localCache = 53 | getOrElse(localCache, key, () => new Map()) as Map; 54 | } 55 | 56 | // Use `undefined` as the last key to simplify implementation & support 0 57 | // arguments 58 | return getOrElse(localCache, undefined, () => fn(...args)) as Ret; 59 | }; 60 | } 61 | 62 | /** A valid key for the map used as a cache. */ 63 | // NOTE: the `declare` here is a signal to some tools to not rename the property 64 | export declare interface CacheableObject { 65 | hashCode(): string; 66 | } 67 | 68 | function isCacheableObject(o: CacheableArg): o is CacheableObject { 69 | return typeof (o as CacheableObject).hashCode === 'function'; 70 | } 71 | 72 | type CacheableArg = HTMLElement|string|number|boolean|CacheableObject; 73 | 74 | function getOrElse(map: Map, key: K, valueFn: () => V): V { 75 | let value = map.get(key); 76 | if (value === undefined) { 77 | value = valueFn(); 78 | map.set(key, value); 79 | } 80 | return value; 81 | } 82 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | ## Basic syntax 4 | 5 | The following locator identifies an element anywhere on the page with a role of 6 | `button` and an accessible name of `OK`: 7 | 8 | `{button 'OK'}` 9 | 10 | The accessible name can also be omitted to match all `button`s: 11 | 12 | `{button}` 13 | 14 | Both single and double quotes can be used for the accessible name - `{button 15 | 'OK'}` or `{button "It's OK"}`. 16 | 17 | ## Locating descendants 18 | 19 | Locators can be combined to find descendants - for example `{dialog} {button 20 | 'Send'}` would find the following send button: 21 | 22 | ```html 23 |
24 |
25 |
26 |
27 |
28 | ``` 29 | 30 | ## Wildcards 31 | 32 | Matching on a substring of the value is also possible, using `*` as a wildcard. 33 | 34 | `{button 'example.com*'}` 35 | 36 | matches: 37 | 38 | ```html {highlight="content:example\.com"} 39 | 40 | ``` 41 | 42 | Wildcards can also be used anywhere in a name e.g. `{button 43 | 'https://*.example.com/*'}` for 44 | 45 | ```html {highlight="content:https:// content:\.example\.com/"} 46 | 47 | ``` 48 | 49 | ## Attributes 50 | 51 | It's possible to select elements based on attributes (ARIA 52 | [states and properties](https://www.w3.org/WAI/PF/aria/states_and_properties)). 53 | Both explicit `aria-` prefixed attributes and implicit equivalents are checked. 54 | For example 55 | 56 | `{button disabled:true}` 57 | 58 | will match either of the following elements 59 | 60 | ```html 61 | 62 |
foo
63 | ``` 64 | 65 | The source of truth for currently supported attributes is 66 | [`SUPPORTED_ATTRIBUTES`](https://github.com/google/semantic-locators/search?q=SUPPORTED_ATTRIBUTES+filename%3Atypes.ts). 67 | 68 | ## Nested elements - 'outer' syntax 69 | 70 | If multiple nested elements match the same semantic locator then it's possible 71 | to specify only matching the outermost matching element using the 'outer' 72 | modifier. For example: 73 | 74 | ```html 75 | 76 |
    77 |
  • 78 |
79 |
80 | 81 | 82 | 83 | 84 | ``` 85 | 86 | * `{table}` matches all 3 tables. 87 | * `outer {table}` matches only the outer table. 88 | * `{listitem} outer {table}` matches only the middle table. 89 | * `{table} {table} {table}` matches only the inner table. 90 | -------------------------------------------------------------------------------- /javascript/DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing Semantic Locators JS/TS 2 | 3 | ## Setup 4 | 5 | Install yarn if it's not already installed 6 | 7 | ```bash 8 | npm install -g yarn 9 | ``` 10 | 11 | Get a copy of the code and verify that tests pass on your system 12 | 13 | ```bash 14 | git clone https://github.com/google/semantic-locators.git 15 | cd semantic-locators/javascript 16 | yarn test 17 | ``` 18 | 19 | ## Testing 20 | 21 | Tests live in the `test` directory, and can be run with `yarn test`. All new 22 | code must have associated tests. 23 | 24 | ## Design 25 | 26 | ### Code Structure 27 | 28 | [`src/semantic_locators.ts`](src/semantic_locators.ts) is the main entry point, 29 | which re-exports functions from 30 | [`find_by_semantic_locator.ts`](src/lib/find_by_semantic_locator.ts). 31 | 32 | The definitions of ARIA semantics (e.g. defining that `` is 33 | a `button`) can be found in [`role_map.ts`](src/lib/role_map.ts). 34 | 35 | ### Locating by Selectors 36 | 37 | Let's take a high-level look at how a Semantic Locator is resolved - how do we 38 | go from `{button 'OK'}` to the correct elements in the DOM? 39 | 40 | #### Locating by Role 41 | 42 | First we find all elements which have a role of `button` in 43 | [`role.ts`](src/lib/role.ts). In order to do this efficiently we use CSS 44 | selectors wherever possible. [`role_map.ts`](src/lib/role_map.ts) gives us 2 45 | things for the role `button`: 46 | 47 | 1. An exact selector - `button,summary`. We know that any element matching this 48 | selector has a role of button (unless that is overridden with an explicit 49 | `role=something`) 50 | 2. A conditional selector, comprising a greedy selector (`input`) and some 51 | conditions (`type` is `button`, `image`, `reset` or `submit`). For an 52 | element to match this conditional selector, it must match the greedy 53 | selector, and all conditions must be true. 54 | 55 | From these selectors we can find all elements with the role `button`. 56 | 57 | #### Filtering by name 58 | 59 | Then we calculate the accessible name of each of these elements using the 60 | [accname](https://github.com/google/accname) library, returning any with the 61 | accessible name `OK`. 62 | 63 | #### Filtering by attributes 64 | 65 | Similarly to filtering by name, any attributes in the locator are calculated for 66 | candidate elements, and those which don't match are filtered out. 67 | 68 | ## Deploying 69 | 70 | Once your change is ready, bump the version number in 71 | [`package.json`](package.json) according to 72 | [Semantic Versioning](https://semver.org/) and open a PR. After it has been 73 | reviewed and merged, an admin will deploy the new version to NPM. 74 | -------------------------------------------------------------------------------- /webdriver_ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-locators-webdriver", 3 | "version": "2.1.0", 4 | "description": "Semantic Locators are a human readable, resilient and accessibility-enforcing way to find web elements.", 5 | "main": "dist/src/semantic_locators.js", 6 | "module": "dist/src/semantic_locators.js", 7 | "types": "dist/src/semantic_locators.d.ts", 8 | "exports": { 9 | ".": {"require": "./dist/src/semantic_locators.js"}, 10 | "./gen":{"require": "./dist/src/gen/index.js"} 11 | }, 12 | "typesVersions": { "*": { "gen": ["dist/src/gen/index.d.ts"] }}, 13 | "directories": { 14 | "lib": "lib" 15 | }, 16 | "files": [ 17 | "dist/src" 18 | ], 19 | "scripts": { 20 | "build": "yarn && tsc", 21 | "ibuild": "yarn && tsc --watch", 22 | "pretest": "yarn build", 23 | "test": "jasmine-ts --config=jasmine.json", 24 | "itest": "nodemon --ext ts --exec 'jasmine-ts --config=./jasmine.json'", 25 | "prepack": "yarn test", 26 | "fix": "yarn fix-lint && yarn fix-format", 27 | "check-lint": "eslint --ext .ts src test", 28 | "fix-lint": "eslint --ext .ts --fix src test", 29 | "check-format": "clang-format --version; find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file --dry-run -Werror", 30 | "fix-format": "clang-format --version; find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/google/semantic-locators.git" 35 | }, 36 | "keywords": [ 37 | "testing", 38 | "accessibility" 39 | ], 40 | "author": "Scott Pledger", 41 | "license": "Apache-2.0", 42 | "bugs": { 43 | "url": "https://github.com/google/semantic-locators/issues" 44 | }, 45 | "homepage": "https://github.com/google/semantic-locators#readme", 46 | "dependencies": { 47 | "accname": "^1.1.0", 48 | "selenium-webdriver": "^3.0.0" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.17.7", 52 | "@babel/plugin-transform-runtime": "^7.17.0", 53 | "@types/jasmine": "^3.6.2", 54 | "@types/node": "^18.11.7", 55 | "@types/selenium-webdriver": "^3.0.0", 56 | "@typescript-eslint/eslint-plugin": "^3.6.0", 57 | "@typescript-eslint/parser": "^3.6.0", 58 | "clang-format": "^1.4.0", 59 | "eslint-plugin-ban": "^1.5.2", 60 | "eslint-plugin-import": "^2.22.1", 61 | "eslint-plugin-jsdoc": "^32.2.0", 62 | "eslint-plugin-prefer-arrow": "^1.2.3", 63 | "eslint": "^7.21.0", 64 | "jasmine-spec-reporter": "^7.0.0", 65 | "jasmine-ts": "^0.4.0", 66 | "jasmine": "^3.6.0", 67 | "lit": "^2.0.2", 68 | "nodemon": "^2.0.20", 69 | "ts-node": "^10.9.1", 70 | "typescript": "^4.1.3" 71 | }, 72 | "publishConfig":{ 73 | "registry":"https://wombat-dressing-room.appspot.com" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-locators", 3 | "version": "2.1.0", 4 | "description": "Semantic Locators are a human readable, resilient and accessibility-enforcing way to find web elements.", 5 | "main": "dist/src/semantic_locators.js", 6 | "module": "dist/src/semantic_locators.js", 7 | "types": "dist/src/semantic_locators.d.ts", 8 | "exports": { 9 | ".": {"require": "./dist/src/semantic_locators.js"}, 10 | "./gen":{"require": "./dist/src/gen/index.js"} 11 | }, 12 | "typesVersions": { "*": { "gen": ["dist/src/gen/index.d.ts"] }}, 13 | "directories": { 14 | "lib": "lib" 15 | }, 16 | "files": [ 17 | "dist/src" 18 | ], 19 | "scripts": { 20 | "build": "yarn && tsc", 21 | "ibuild": "yarn && tsc --watch", 22 | "pretest": "yarn build", 23 | "test": "karma start --browsers ChromeHeadless,FirefoxHeadless --single-run --no-auto-watch", 24 | "itest": "karma start --browsers Chrome,Firefox", 25 | "prepack": "yarn test", 26 | "fix": "yarn fix-lint && yarn fix-format", 27 | "check-lint": "eslint --ext .ts src test", 28 | "fix-lint": "eslint --ext .ts --fix src test", 29 | "check-format": "clang-format --version; find src test ! -path 'src/lib/parser.ts' | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file --dry-run -Werror", 30 | "fix-format": "clang-format --version; find src test ! -path 'src/lib/parser.ts' | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/google/semantic-locators.git" 35 | }, 36 | "keywords": [ 37 | "testing", 38 | "accessibility" 39 | ], 40 | "author": "Alex Lloyd", 41 | "license": "Apache-2.0", 42 | "bugs": { 43 | "url": "https://github.com/google/semantic-locators/issues" 44 | }, 45 | "homepage": "https://github.com/google/semantic-locators#readme", 46 | "dependencies": { 47 | "accname": "^1.1.0" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.17.7", 51 | "@babel/plugin-transform-runtime": "^7.17.0", 52 | "@types/jasmine": "^3.6.2", 53 | "@typescript-eslint/eslint-plugin": "^3.6.0", 54 | "@typescript-eslint/parser": "^3.6.0", 55 | "clang-format": "^1.4.0", 56 | "eslint": "^7.21.0", 57 | "eslint-plugin-ban": "^1.5.2", 58 | "eslint-plugin-import": "^2.22.1", 59 | "eslint-plugin-jsdoc": "^32.2.0", 60 | "eslint-plugin-prefer-arrow": "^1.2.3", 61 | "jasmine-core": "^3.6.0", 62 | "karma": "^6.3.2", 63 | "karma-chrome-launcher": "^3.1.0", 64 | "karma-firefox-launcher": "^2.1.0", 65 | "karma-jasmine": "^4.0.1", 66 | "karma-spec-reporter": "0.0.32", 67 | "karma-typescript": "^5.2.0", 68 | "karma-typescript-es6-transform": "~5.3.0", 69 | "lit": "^2.0.2", 70 | "typescript": "^4.1.3" 71 | }, 72 | "publishConfig":{ 73 | "registry":"https://wombat-dressing-room.appspot.com" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | paths-ignore: 18 | - pages/playground 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ main ] 22 | paths-ignore: 23 | - pages/playground 24 | schedule: 25 | - cron: '25 19 * * 0' 26 | 27 | jobs: 28 | analyze: 29 | name: Analyze 30 | runs-on: ubuntu-latest 31 | permissions: 32 | actions: read 33 | contents: read 34 | security-events: write 35 | 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | language: [ 'csharp', 'go', 'java', 'javascript', 'python', 'typescript' ] 40 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 41 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 42 | 43 | steps: 44 | - name: Harden Runner 45 | uses: step-security/harden-runner@bdb12b622a910dfdc99a31fdfe6f45a16bc287a4 # v1 46 | with: 47 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 48 | 49 | - name: Checkout repository 50 | uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 51 | 52 | # Initializes the CodeQL tools for scanning. 53 | - name: Initialize CodeQL 54 | uses: github/codeql-action/init@f5d822707ee6e8fb81b04a5c0040b736da22e587 # v1 55 | with: 56 | languages: ${{ matrix.language }} 57 | # If you wish to specify custom queries, you can do so here or in a config file. 58 | # By default, queries listed here will override any specified in a config file. 59 | # Prefix the list here with "+" to use these queries and those in the config file. 60 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 61 | 62 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 63 | # If this step fails, then you should remove it and run the build manually (see below) 64 | - name: Autobuild 65 | uses: github/codeql-action/autobuild@f5d822707ee6e8fb81b04a5c0040b736da22e587 # v1 66 | 67 | # ℹ️ Command-line programs to run using the OS shell. 68 | # 📚 https://git.io/JvXDl 69 | 70 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 71 | # and modify them (or add more) to build your code if your project 72 | # uses a compiled language 73 | 74 | #- run: | 75 | # make bootstrap 76 | # make release 77 | 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@f5d822707ee6e8fb81b04a5c0040b736da22e587 # v1 80 | -------------------------------------------------------------------------------- /javascript/test/lib/accessible_name_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {html, render} from 'lit'; 8 | 9 | import {getNameFor, nameMatches} from '../../src/lib/accessible_name'; 10 | import {runBatchOp} from '../../src/lib/batch_cache'; 11 | 12 | describe('nameMatches', () => { 13 | let container: HTMLElement; 14 | beforeEach(() => { 15 | container = document.createElement('div'); 16 | document.body.appendChild(container); 17 | }); 18 | 19 | afterEach(() => { 20 | document.body.removeChild(container); 21 | }); 22 | 23 | it('mathes the same string without wildcards', () => { 24 | expect(nameMatches('foo', 'foo')).toBeTrue(); 25 | expect(nameMatches('', '')).toBeTrue(); 26 | }); 27 | 28 | it('doesn\'t match different strings without wildcards', () => { 29 | expect(nameMatches('foo', 'foo bar')).toBeFalse(); 30 | }); 31 | 32 | it('matches with leading wildcard if actual ends with expected', () => { 33 | expect(nameMatches('*foo', 'foo')).toBeTrue(); 34 | expect(nameMatches('*foo', 'this string end with foo')).toBeTrue(); 35 | }); 36 | 37 | it('doesn\'t match with leading wildcard if actual doesn\'t end with expected', 38 | () => { 39 | expect(nameMatches('*foo', 'foo bar')).toBeFalse(); 40 | }); 41 | 42 | it('matches with trailing wildcard', () => { 43 | expect(nameMatches('foo*', 'foo')).toBeTrue(); 44 | expect(nameMatches('foo*', 'foo is at the start of this string')) 45 | .toBeTrue(); 46 | }); 47 | 48 | it('doesn\'t match with trailing wildcard if actual doesn\'t start with expected', 49 | () => { 50 | expect(nameMatches('foo*', 'bar foo')).toBeFalse(); 51 | }); 52 | 53 | it('matches with wildcard in the middle', () => { 54 | expect(nameMatches('foo*baz', 'foobarbaz')).toBeTrue(); 55 | expect(nameMatches('foo*baz', 'foobaz')).toBeTrue(); 56 | expect(nameMatches('foo*baz', 'fooaz')).toBeFalse(); 57 | }); 58 | 59 | 60 | it('matches with wildcards everywhere', () => { 61 | expect(nameMatches('I am * Bat*', 'I am not Batman')).toBeTrue(); 62 | expect(nameMatches('I am * Bat*', 'I am a Bat')).toBeTrue(); 63 | expect(nameMatches('I am * Bat*', 'I am Bat')).toBeFalse(); 64 | }); 65 | 66 | it('doesn\'t match with leading and trailing wildcard if actual doesn\'t contain expected', 67 | () => { 68 | expect(nameMatches('foo', 'fo oo of fo0')).toBeFalse(); 69 | }); 70 | 71 | it('throws for a name of *', () => { 72 | expect(() => nameMatches('*', '')).toThrow(); 73 | }); 74 | 75 | it('caches values if cache is enabled', () => { 76 | render(html`
original name
`, container); 77 | const button = document.getElementById('button')!; 78 | runBatchOp(() => { 79 | // Seed cache 80 | getNameFor(button); 81 | button.innerText = 'new name'; 82 | 83 | expect(getNameFor(button)).toBe('original name'); 84 | }); 85 | expect(getNameFor(button)).toBe('new name'); 86 | }); 87 | 88 | it('clears cache between runBatchOp calls', () => { 89 | render(html`
original name
`, container); 90 | const button = document.getElementById('button')!; 91 | runBatchOp(() => { 92 | getNameFor(button); 93 | button.innerText = 'new name'; 94 | 95 | expect(getNameFor(button)).toBe('original name'); 96 | }); 97 | runBatchOp(() => { 98 | expect(getNameFor(button)).toBe('new name'); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /javascript/src/lib/attribute.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {getRole, positionWithinAncestorRole} from './role'; 8 | import {SupportedAttributeType} from './types'; 9 | import {checkExhaustive, hasTagName} from './util'; 10 | 11 | /** 12 | * Get the value of an ARIA attribute on a HTMLElement. Include implicit values 13 | * (e.g. `` implies `aria-checked="true"`). Returns values 14 | * according to https://www.w3.org/WAI/PF/aria-1.1/states_and_properties, but 15 | * returning null rather than undefined if the attribute isn't defined for the 16 | * element. 17 | */ 18 | export function computeARIAAttributeValue( 19 | element: HTMLElement, 20 | attribute: SupportedAttributeType, 21 | ): string|null { 22 | if (element.hasAttribute(`aria-${attribute}`)) { 23 | return element.getAttribute(`aria-${attribute}`); 24 | } 25 | 26 | // Check native HTML equivalents 27 | switch (attribute) { 28 | // States: 29 | case 'checked': 30 | if (hasTagName(element, 'input') && 31 | ['checkbox', 'radio'].includes(element.type)) { 32 | return element.checked.toString(); 33 | } 34 | return null; 35 | case 'current': 36 | // There's no native equivalent of "aria-current" so if it's not 37 | // explicitly specified it takes the default of false. 38 | return 'false'; 39 | case 'disabled': 40 | return ((hasTagName(element, 'button') || 41 | hasTagName(element, 'fieldset') || 42 | hasTagName(element, 'input') || 43 | hasTagName(element, 'optgroup') || 44 | hasTagName(element, 'option') || hasTagName(element, 'select') || 45 | hasTagName(element, 'textarea')) && 46 | element.disabled) 47 | .toString(); 48 | case 'pressed': 49 | // There's no native equivalent of "aria-pressed" so if it's not 50 | // explicitly specified it takes the default of false. 51 | return 'false'; 52 | case 'selected': 53 | if (hasTagName(element, 'option')) { 54 | return element.selected.toString(); 55 | } 56 | return null; 57 | 58 | // Properties: 59 | case 'colindex': 60 | const colindex = 61 | positionWithinAncestorRole(element, 'row', ['columnheader', 'cell']); 62 | return colindex ? String(colindex) : null; 63 | case 'level': 64 | const match = element.tagName.match(/^H([1-6])$/); 65 | if (match === null) { 66 | return null; 67 | } 68 | return match[1]; 69 | case 'rowindex': 70 | const rowindex = positionWithinAncestorRole(element, 'table', ['row']); 71 | return rowindex ? String(rowindex) : null; 72 | case 'posinset': 73 | const role = getRole(element); 74 | if (role === 'listitem') { 75 | const posinset = 76 | positionWithinAncestorRole(element, 'list', ['listitem']); 77 | return posinset ? String(posinset) : null; 78 | } else if (role === 'treeitem') { 79 | const posinset = 80 | positionWithinAncestorRole(element, 'tree', ['treeitem']); 81 | return posinset ? String(posinset) : null; 82 | } else { 83 | return null; 84 | } 85 | case 'readonly': 86 | return ((hasTagName(element, 'input') && element.readOnly) || 87 | // TODO(alexlloyd) is this correct? 88 | // https://www.w3.org/TR/html-aria/#docconformance says that 89 | // aria-readonly="false" for an 'Element with contenteditable 90 | // attribute', but surely an element with contenteditable="true" 91 | // is not readonly? 92 | element.contentEditable === 'false') 93 | .toString(); 94 | default: 95 | checkExhaustive(attribute); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pages/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Semantic Locators Playground 8 | 9 | 10 | 11 | 12 | 13 |

14 | Enter a HTML snippet below to generate a semantic locator for each element which has semantics. 15 |

16 | 17 |
18 |

Snapshot Options

19 |
20 |
21 |

Input

22 | 23 |
24 |
25 |

Rendered HTML

26 |
#content#
27 |
28 |
29 |

Semantic Locators snapshot

30 |
#content#
31 |
32 |
33 | Debug info 34 |
35 |

HTML Sanitization Differences

36 |
#content#
37 |
38 |
39 | 40 | 41 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /docs/DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing Semantic Locators 2 | 3 | ## Get a local copy of the code 4 | 5 | ```bash 6 | git clone https://github.com/google/semantic-locators.git 7 | ``` 8 | 9 | ## Directory structure 10 | 11 | The core implementation of Semantic Locators lives in the `javascript` 12 | directory. The code in that directory runs in the browser, finding elements by 13 | Semantic Locators. See [`javascript/DEVELOPING.md`](../javascript/DEVELOPING.md) 14 | for more details about the implementation. 15 | 16 | Other directories such as `webdriver_java` contain "wrapper libraries", allowing 17 | for using Semantic Locators from other languages and environments. The purpose 18 | of these libraries is to execute the javascript implementation of Semantic 19 | Locators in the browser. 20 | 21 | ## Adding a new integration 22 | 23 | Integrating Semantic Locators with a new language/environment/library is usually 24 | a straightforward task (<1 day of effort). The following instructions assume 25 | you want to contribute the code back to 26 | [`google/semantic-locators`](https://github.com/google/semantic-locators). 27 | 28 | For an example see the implementation for 29 | [`Webdriver Java`](https://github.com/google/semantic-locators/tree/main/webdriver_java). 30 | 31 | ### Create a README.md and DEVELOPING.md 32 | 33 | Create the two markdown files required for new integrations: 34 | 35 | 1. `README.md` explaining how a user can install and use the library. 36 | 2. `DEVELOPING.md` explaining how a developer can test and deploy a new version 37 | of the library. 38 | 39 | ### Wrapper Binary 40 | 41 | [`javascript/wrapper_bin.js`](https://github.com/google/semantic-locators/tree/main/javascript/wrapper/wrapper_bin.js) 42 | contains the compiled definition of Semantic Locators to be used from wrapper 43 | libraries. Avoid duplicating the file within this repo, instead it should be 44 | copied as part of a build script. For example see the `copy-resources` section 45 | in 46 | [`webdriver_java/pom.xml`](https://github.com/google/semantic-locators/tree/main/webdriver_java/pom.xml). 47 | 48 | ### Execution 49 | 50 | Semantic locator resolution is implemented in JavaScript, so you just need a way 51 | to execute JS from your test. Testing frameworks usually provide an API for 52 | this. 53 | 54 | See 55 | [`BySemanticLocator.java`](https://github.com/google/semantic-locators/tree/main/webdriver_java/src/main/java/com/google/semanticlocators/BySemanticLocator.java) 56 | for a reference implementation. The basic flow is: 57 | 58 | * Read `wrapper_bin.js`. 59 | * Execute the script to load Semantic Locators in the browser. 60 | * Execute `return window.<function>.apply(null, arguments);` where 61 | `<function>` is a function exported in 62 | [`wrapper.ts`](https://github.com/google/semantic-locators/blob/main/javascript/wrapper/wrapper.ts) 63 | (e.g., `return window.findElementBySemanticLocator.apply(null, 64 | arguments);`). 65 | * Parse any failures and throw an appropriate exception. 66 | 67 | ### Tests 68 | 69 | All new code must be tested. There's no need to test the full behaviour of 70 | semantic locators, but please include smoke tests, and test things which might 71 | break on serializing/deserializing to send to the browser. See 72 | [`BySemanticLocatorTest.java`](https://github.com/google/semantic-locators/tree/main/webdriver_java/src/test/java/com/google/semanticlocators/BySemanticLocatorTest.java) 73 | for an example. 74 | 75 | ### CI 76 | 77 | We strongly recommended adding Continuous Integration to test and lint your 78 | code. We use GitHub actions - see the existing workflows in 79 | `.github/workflows/*.yml`. 80 | 81 | ### Send a Pull Request 82 | 83 | Open a pull request in 84 | [`google/semantic-locators`](https://github.com/google/semantic-locators) so a 85 | project maintainer can review it. 86 | 87 | ### Codelab 88 | 89 | Please ask a Googler contributor to add code examples to this codelab. 90 | -------------------------------------------------------------------------------- /webdriver_java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.google.semanticlocators 8 | semantic-locators 9 | 2.1.0 10 | 11 | semantic-locators 12 | http://github.com/google/semantic-locators 13 | 14 | 15 | UTF-8 16 | 1.8 17 | 1.8 18 | 19 | 20 | 21 | org.sonatype.oss 22 | oss-parent 23 | 7 24 | 25 | 26 | 27 | 28 | org.seleniumhq.selenium 29 | selenium-java 30 | 3.141.59 31 | 32 | 33 | 34 | junit 35 | junit 36 | 4.13.1 37 | test 38 | 39 | 40 | com.google.truth 41 | truth 42 | 1.1.2 43 | test 44 | 45 | 46 | pl.pragmatists 47 | JUnitParams 48 | 1.1.1 49 | test 50 | 51 | 52 | com.google.guava 53 | guava 54 | 32.0.0-jre 55 | test 56 | 57 | 58 | 59 | 60 | 61 | 62 | src/main/resources 63 | 64 | 65 | 66 | 67 | 68 | 69 | maven-resources-plugin 70 | 3.2.0 71 | 72 | 73 | copy-resources 74 | validate 75 | 76 | copy-resources 77 | 78 | 79 | ${basedir}/src/main/resources/com/google/semanticlocators 80 | 81 | 82 | ${basedir}/../javascript/wrapper 83 | 84 | **/wrapper_bin.js 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | maven-assembly-plugin 94 | 3.3.0 95 | 96 | 97 | jar-with-dependencies 98 | 99 | 100 | 101 | 102 | make-assembly 103 | package 104 | 105 | single 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /javascript/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": {"browser": true, "es6": true, "node": true}, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": {"sourceType": "module"}, 5 | "plugins": [ 6 | "eslint-plugin-ban", 7 | "eslint-plugin-jsdoc", 8 | "eslint-plugin-import", 9 | "eslint-plugin-prefer-arrow", 10 | "@typescript-eslint" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/array-type": ["error", {"default": "array-simple"}], 14 | "@typescript-eslint/ban-ts-comment": "error", 15 | "@typescript-eslint/ban-types": [ 16 | "error", 17 | { 18 | "types": { 19 | "Object": {"message": "Use {} or 'object' instead."}, 20 | "String": {"message": "Use 'string' instead."}, 21 | "Number": {"message": "Use 'number' instead."}, 22 | "Boolean": {"message": "Use 'boolean' instead."} 23 | } 24 | } 25 | ], 26 | "@typescript-eslint/consistent-type-assertions": "error", 27 | "@typescript-eslint/consistent-type-definitions": "error", 28 | "@typescript-eslint/explicit-member-accessibility": 29 | ["error", {"accessibility": "no-public"}], 30 | "@typescript-eslint/member-delimiter-style": [ 31 | "error", 32 | { 33 | "multiline": {"delimiter": "semi", "requireLast": true}, 34 | "singleline": {"delimiter": "semi", "requireLast": false} 35 | } 36 | ], 37 | "@typescript-eslint/naming-convention": [ 38 | "error", 39 | { 40 | "selector": "variable", 41 | "modifiers": ["const"], 42 | "format": ["UPPER_CASE", "camelCase"] 43 | }, 44 | { 45 | "selector": "variable", 46 | "format": ["camelCase"] 47 | }, 48 | { 49 | "selector": "typeLike", 50 | "format": ["PascalCase"] 51 | } 52 | ], 53 | "@typescript-eslint/no-explicit-any": "error", 54 | "@typescript-eslint/no-namespace": "error", 55 | "@typescript-eslint/no-require-imports": "error", 56 | "@typescript-eslint/no-unused-expressions": 57 | ["error", {"allowShortCircuit": true}], 58 | "@typescript-eslint/no-unused-vars": "error", 59 | "@typescript-eslint/semi": ["error", "always"], 60 | "@typescript-eslint/triple-slash-reference": "error", 61 | "arrow-body-style": "error", 62 | "curly": ["error", "multi-line"], 63 | "default-case": "error", 64 | "eqeqeq": ["error", "smart"], 65 | "guard-for-in": "error", 66 | "import/no-default-export": "error", 67 | "jsdoc/check-alignment": "error", 68 | "new-parens": "error", 69 | "no-cond-assign": "error", 70 | "no-debugger": "error", 71 | "no-duplicate-case": "error", 72 | "no-new-wrappers": "error", 73 | "no-return-await": "error", 74 | "no-throw-literal": "error", 75 | "no-unsafe-finally": "error", 76 | "no-unused-labels": "error", 77 | "no-useless-constructor": "off", 78 | "@typescript-eslint/no-useless-constructor": ["error"], 79 | "no-var": "error", 80 | "object-shorthand": "error", 81 | "prefer-const": ["error", {"destructuring": "all"}], 82 | "radix": "error", 83 | "ban/ban": [ 84 | "error", 85 | {"name": "fit"}, 86 | {"name": "fdescribe"}, 87 | {"name": "xit"}, 88 | {"name": "xdescribe"}, 89 | {"name": "fitAsync"}, 90 | {"name": "xitAsync"}, 91 | {"name": "fitFakeAsync"}, 92 | {"name": "xitFakeAsync"}, 93 | {"name": ["it", "skip"]}, 94 | {"name": ["it", "only"]}, 95 | {"name": ["it", "async", "skip"]}, 96 | {"name": ["it", "async", "only"]}, 97 | {"name": ["describe", "skip"]}, 98 | {"name": ["describe", "only"]}, 99 | {"name": ["describeWithDate", "skip"]}, 100 | {"name": ["describeWithDate", "only"]}, 101 | { 102 | "name": "parseInt", 103 | "message": "Use Number() and Math.floor or Math.trunc" 104 | }, 105 | {"name": "parseFloat", "message": "Use Number()"}, 106 | {"name": "Array", "message": "Use square brackets"}, 107 | {"name": ["*", "innerText"], "message": "Use .textContent instead."} 108 | ] 109 | } 110 | } -------------------------------------------------------------------------------- /webdriver_ts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": {"browser": true, "es6": true, "node": true}, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": {"sourceType": "module"}, 5 | "plugins": [ 6 | "eslint-plugin-ban", 7 | "eslint-plugin-jsdoc", 8 | "eslint-plugin-import", 9 | "eslint-plugin-prefer-arrow", 10 | "@typescript-eslint" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/array-type": ["error", {"default": "array-simple"}], 14 | "@typescript-eslint/ban-ts-comment": "error", 15 | "@typescript-eslint/ban-types": [ 16 | "error", 17 | { 18 | "types": { 19 | "Object": {"message": "Use {} or 'object' instead."}, 20 | "String": {"message": "Use 'string' instead."}, 21 | "Number": {"message": "Use 'number' instead."}, 22 | "Boolean": {"message": "Use 'boolean' instead."} 23 | } 24 | } 25 | ], 26 | "@typescript-eslint/consistent-type-assertions": "error", 27 | "@typescript-eslint/consistent-type-definitions": "error", 28 | "@typescript-eslint/explicit-member-accessibility": 29 | ["error", {"accessibility": "no-public"}], 30 | "@typescript-eslint/member-delimiter-style": [ 31 | "error", 32 | { 33 | "multiline": {"delimiter": "semi", "requireLast": true}, 34 | "singleline": {"delimiter": "semi", "requireLast": false} 35 | } 36 | ], 37 | "@typescript-eslint/naming-convention": [ 38 | "error", 39 | { 40 | "selector": "variable", 41 | "modifiers": ["const"], 42 | "format": ["UPPER_CASE", "camelCase"] 43 | }, 44 | { 45 | "selector": "variable", 46 | "format": ["camelCase"] 47 | }, 48 | { 49 | "selector": "typeLike", 50 | "format": ["PascalCase"] 51 | } 52 | ], 53 | "@typescript-eslint/no-explicit-any": "error", 54 | "@typescript-eslint/no-namespace": "error", 55 | "@typescript-eslint/no-require-imports": "error", 56 | "@typescript-eslint/no-unused-expressions": 57 | ["error", {"allowShortCircuit": true}], 58 | "@typescript-eslint/no-unused-vars": "error", 59 | "@typescript-eslint/semi": ["error", "always"], 60 | "@typescript-eslint/triple-slash-reference": "error", 61 | "arrow-body-style": "error", 62 | "curly": ["error", "multi-line"], 63 | "default-case": "error", 64 | "eqeqeq": ["error", "smart"], 65 | "guard-for-in": "error", 66 | "import/no-default-export": "error", 67 | "jsdoc/check-alignment": "error", 68 | "new-parens": "error", 69 | "no-cond-assign": "error", 70 | "no-debugger": "error", 71 | "no-duplicate-case": "error", 72 | "no-new-wrappers": "error", 73 | "no-return-await": "error", 74 | "no-throw-literal": "error", 75 | "no-unsafe-finally": "error", 76 | "no-unused-labels": "error", 77 | "no-useless-constructor": "off", 78 | "@typescript-eslint/no-useless-constructor": ["error"], 79 | "no-var": "error", 80 | "object-shorthand": "error", 81 | "prefer-const": ["error", {"destructuring": "all"}], 82 | "radix": "error", 83 | "ban/ban": [ 84 | "error", 85 | {"name": "fit"}, 86 | {"name": "fdescribe"}, 87 | {"name": "xit"}, 88 | {"name": "xdescribe"}, 89 | {"name": "fitAsync"}, 90 | {"name": "xitAsync"}, 91 | {"name": "fitFakeAsync"}, 92 | {"name": "xitFakeAsync"}, 93 | {"name": ["it", "skip"]}, 94 | {"name": ["it", "only"]}, 95 | {"name": ["it", "async", "skip"]}, 96 | {"name": ["it", "async", "only"]}, 97 | {"name": ["describe", "skip"]}, 98 | {"name": ["describe", "only"]}, 99 | {"name": ["describeWithDate", "skip"]}, 100 | {"name": ["describeWithDate", "only"]}, 101 | { 102 | "name": "parseInt", 103 | "message": "Use Number() and Math.floor or Math.trunc" 104 | }, 105 | {"name": "parseFloat", "message": "Use Number()"}, 106 | {"name": "Array", "message": "Use square brackets"}, 107 | {"name": ["*", "innerText"], "message": "Use .textContent instead."} 108 | ] 109 | } 110 | } -------------------------------------------------------------------------------- /javascript/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** Flip to `true` to enable assertions and extra validation checks. */ 8 | const DEBUG_MODE = false; 9 | 10 | /** 11 | * For use at the end of a switch/series of checks. This function fails to 12 | * compile or run if it may ever be called with a real value. 13 | */ 14 | export function checkExhaustive(value: never): never { 15 | throw new Error(`unexpected value ${value}!`); 16 | } 17 | 18 | /** 19 | * In debug builds, throw an error if elements are not in document order. In 20 | * non-debug builds this function is a no-op. 21 | */ 22 | export function assertInDocumentOrder(elements: readonly Node[]) { 23 | if (debug()) { 24 | // Assert document order 25 | for (let i = 1; i < elements.length; i++) { 26 | if (compareNodeOrder(elements[i - 1], elements[i]) > 0) { 27 | throw new Error( 28 | `Elements not passed in document order. Index ${(i - 1)} comes ` + 29 | `after index ${i}. Elements: ${JSON.stringify(elements)}`); 30 | } 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Compares the document order of two nodes, returning 0 if they are the same 37 | * node, a negative number if node1 is before node2, and a positive number if 38 | * node2 is before node1. Note that we compare the order the tags appear in the 39 | * document so in the tree text the B node is considered to be 40 | * before the I node. 41 | * 42 | * Based on Closure goog.dom.compareNodeOrder, translated to TypeScript, with 43 | * closure dependencies removed and with less support for older browsers 44 | */ 45 | export function compareNodeOrder(node1: Node, node2: Node): number { 46 | // Fall out quickly for equality. 47 | if (node1 === node2) { 48 | return 0; 49 | } 50 | 51 | const node2PreceedsNode1 = 52 | node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING; 53 | return node2PreceedsNode1 ? 1 : -1; 54 | } 55 | 56 | /** Remove duplicates from a sorted array. */ 57 | export function removeDuplicates(arr: T[]): T[] { 58 | return arr.filter((el, i) => arr[i - 1] !== el); 59 | } 60 | 61 | /** 62 | * Checks whether the `tagName` of a particular element matches a known 63 | * tagName. The `tagName` is constrained by the same type mappings that are used 64 | * in `document.querySelector`, which allows us to constrain the return type as 65 | * well. 66 | */ 67 | export function hasTagName( 68 | el: HTMLElement, name: TagName): el is HTMLElementTagNameMap[TagName]; 69 | export function hasTagName( 70 | el: SVGElement, name: TagName): el is SVGElementTagNameMap[TagName]; 71 | export function hasTagName(el: Element, name: string): boolean { 72 | return el.tagName.toLowerCase() === name; 73 | } 74 | 75 | /** Type guard for Node */ 76 | export function isNode(e: EventTarget): e is Node { 77 | return (e as Node).nodeName !== null; 78 | } 79 | 80 | /** Type guard for Element */ 81 | function isElement(node: Node): node is Element { 82 | return node.nodeType === Node.ELEMENT_NODE; 83 | } 84 | 85 | const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 86 | 87 | /** Check whether `node` is a HTMLElement. */ 88 | export function isHTMLElement(node: Node): node is HTMLElement { 89 | return isElement(node) && node.namespaceURI === HTML_NAMESPACE; 90 | } 91 | 92 | /** Throw an exception if the condition is false. */ 93 | export function assert(condition: boolean, givenMessage?: string): boolean { 94 | if (debug() && !condition) { 95 | let message = 'Assertion failed'; 96 | if (givenMessage !== undefined) { 97 | message += ': ' + givenMessage; 98 | } 99 | throw new Error(message); 100 | } 101 | return condition; 102 | } 103 | 104 | /** 105 | * Throw an exception if the condition is false, evaluating `messageSupplier` 106 | * as the error message 107 | */ 108 | export function lazyAssert( 109 | condition: boolean, messageSupplier: () => string): boolean { 110 | if (debug() && !condition) { 111 | throw new Error(`Assertion failed: ${messageSupplier()}`); 112 | } 113 | return condition; 114 | } 115 | 116 | /** Are we in debug mode? */ 117 | export function debug(): boolean { 118 | return (window as unknown as {goog?: {DEBUG?: boolean}})?.goog?.DEBUG || 119 | DEBUG_MODE; 120 | } 121 | -------------------------------------------------------------------------------- /javascript/src/lib/semantic_locator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {CacheableObject} from './batch_cache'; 8 | import {InvalidLocatorError} from './error'; 9 | import {AriaRole, isAriaRole, isChildrenPresentational} from './role_map'; 10 | import {QuoteChar, SUPPORTED_ATTRIBUTES, SupportedAttributeType} from './types'; 11 | 12 | /** An attribute-value pair. */ 13 | export interface Attribute { 14 | name: SupportedAttributeType; 15 | value: string; 16 | } 17 | 18 | /** A parsed semantic locator. */ 19 | export class SemanticLocator implements CacheableObject { 20 | constructor( 21 | readonly preOuter: readonly SemanticNode[], 22 | readonly postOuter: readonly SemanticNode[]) { 23 | const allNodes = preOuter.concat(postOuter); 24 | for (const node of allNodes) { 25 | // The TypeScript compiler should check this at compile time, but parsing 26 | // user-provided locators is not type-safe 27 | if (!isAriaRole(node.role)) { 28 | throw new InvalidLocatorError( 29 | `Invalid locator: ${this}` + 30 | ` Unknown role: ${node.role}.` + 31 | ` The list of valid roles can be found at` + 32 | ` https://www.w3.org/TR/wai-aria/#role_definitions`); 33 | } 34 | 35 | if (node !== allNodes[allNodes.length - 1] && 36 | isChildrenPresentational(node.role)) { 37 | throw new InvalidLocatorError( 38 | `Invalid locator: ${this}` + 39 | ` The role "${node.role}" has presentational children.` + 40 | ` That means its descendants cannot have semantics, so an element` + 41 | ` with a role of ${node.role} may only` + 42 | ` be the final element of a Semantic Locator.` + 43 | ` https://www.w3.org/WAI/ARIA/apg/practices/hiding-semantics/#children_presentational`); 44 | } 45 | 46 | // The TypeScript compiler should check this at compile time, but parsing 47 | // user-provided locators is not type-safe 48 | for (const attribute of node.attributes) { 49 | if (!SUPPORTED_ATTRIBUTES.includes(attribute.name)) { 50 | throw new InvalidLocatorError( 51 | `Invalid locator: ${this}` + 52 | ` Unsupported attribute: ${attribute.name}.` + 53 | ` Supported attributes: ${SUPPORTED_ATTRIBUTES}`); 54 | } 55 | // TODO(alexlloyd) validate the type of attributes (e.g. true/false) 56 | } 57 | } 58 | } 59 | 60 | toString(quoteChar?: QuoteChar): string { 61 | const resultBuilder: string[] = []; 62 | for (const node of this.preOuter) { 63 | resultBuilder.push(node.toString(quoteChar)); 64 | } 65 | 66 | if (this.postOuter.length > 0) { 67 | resultBuilder.push('outer'); 68 | for (const node of this.postOuter) { 69 | resultBuilder.push(node.toString(quoteChar)); 70 | } 71 | } 72 | 73 | return resultBuilder.join(' '); 74 | } 75 | 76 | hashCode() { 77 | return this.toString(); 78 | } 79 | } 80 | 81 | /** A single node of a semantic locator (e.g. {button 'OK'}). */ 82 | export class SemanticNode { 83 | constructor( 84 | readonly role: AriaRole, 85 | readonly attributes: readonly Attribute[], 86 | readonly name?: string, 87 | ) {} 88 | 89 | toString(quoteChar?: QuoteChar): string { 90 | let result: string = ''; 91 | result += '{'; 92 | result += this.role; 93 | if (this.name) { 94 | result += ' '; 95 | result += escapeAndSurroundWithQuotes(this.name, quoteChar); 96 | } 97 | 98 | for (const attribute of this.attributes) { 99 | result += ` ${attribute.name}:${attribute.value}`; 100 | } 101 | result += '}'; 102 | 103 | return result; 104 | } 105 | } 106 | 107 | /** 108 | * Surrounds the raw string with quotes, escaping any quote characters in the 109 | * string. If the raw string contains one type of quote character then it will 110 | * be surrounded by the other. 111 | * 112 | * e.g. `You're up` -> `"You're up"` 113 | * 114 | * `"Quote" - Author` -> `'"Quote" - Author'`. 115 | */ 116 | function escapeAndSurroundWithQuotes( 117 | raw: string, quoteChar?: QuoteChar): string { 118 | if (quoteChar === undefined) { 119 | if (raw.includes('\'') && !raw.includes('"')) { 120 | quoteChar = '"'; 121 | } else { 122 | quoteChar = `'`; 123 | } 124 | } 125 | 126 | const escaped = raw.replace(new RegExp(quoteChar, 'g'), `\\${quoteChar}`); 127 | return `${quoteChar}${escaped}${quoteChar}`; 128 | } 129 | -------------------------------------------------------------------------------- /webdriver_go/semloc/semloc.go: -------------------------------------------------------------------------------- 1 | // Package semloc provides facilities for locating web elements using semantic 2 | // locators. See http://github.com/google/semantic-locators#readme for more 3 | // info. 4 | // 5 | // Copyright 2021 The Semantic Locators Authors 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | package semloc 19 | 20 | import ( 21 | // Enable go:embed 22 | _ "embed" 23 | "errors" 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/tebeka/selenium" 28 | ) 29 | 30 | //go:embed wrapper_bin.js 31 | var jsImplementation string 32 | 33 | func installJS(wd selenium.WebDriver) error { 34 | installedRaw, err := wd.ExecuteScript("return window.semanticLocatorsReady === true;", nil) 35 | if err != nil { 36 | return fmt.Errorf("wd.ExecuteScript(isInstalled): %w", err) 37 | } 38 | installed, ok := installedRaw.(bool) 39 | if !ok { 40 | return fmt.Errorf("expected isInstalled script to return a bool, but got %T", installedRaw) 41 | } 42 | if installed { 43 | return nil 44 | } 45 | 46 | _, err = wd.ExecuteScript(jsImplementation, nil) 47 | return err 48 | } 49 | 50 | // interpretError takes in an error resulting from executing the semantic 51 | // locator javascript and re-interprets them. 52 | // In selenium, all errors resulting from running a javascript script have the 53 | // "javascript error" error code. However, it makes more sense for errors thrown 54 | // when there is no matching element to have the same "no such element" error 55 | // code that is returned by selenium.WebDriver.FindElement when it cannot find 56 | // an element. The same is performed for errors due to invalid locators. 57 | // See third_party/semantic_locators/javascript/lib/error.ts for semantic 58 | // locator javascript error message values. See 59 | // https://w3c.github.io/webdriver/#errors for webdriver error message values. 60 | func interpretError(err error) error { 61 | var serr *selenium.Error 62 | if !errors.As(err, &serr) { 63 | return err 64 | } 65 | if serr.Err != "javascript error" { 66 | return err 67 | } 68 | 69 | // Error messages are in the format 70 | // "javascript error: : " 71 | // we want to switch on the 2nd part (error name) 72 | parts := strings.SplitN(serr.Message, ":", 3) 73 | if len(parts) != 3 { 74 | return err 75 | } 76 | errName := strings.TrimSpace(parts[1]) 77 | 78 | switch errName { 79 | case "NoSuchElementError": 80 | return &selenium.Error{ 81 | Err: "no such element", 82 | Message: serr.Message, 83 | Stacktrace: serr.Stacktrace, 84 | HTTPCode: 404, 85 | LegacyCode: serr.LegacyCode, 86 | } 87 | case "InvalidLocatorError": 88 | return &selenium.Error{ 89 | Err: "invalid selector", 90 | Message: serr.Message, 91 | Stacktrace: serr.Stacktrace, 92 | HTTPCode: 400, 93 | LegacyCode: serr.LegacyCode, 94 | } 95 | default: 96 | return err 97 | } 98 | } 99 | 100 | // FindElement returns the first WebElement located by the given semantic 101 | // locator. If 'context' is non-nil, only elements that are descendants of 102 | // 'context' are considered. 103 | func FindElement(wd selenium.WebDriver, locator string, context selenium.WebElement) (selenium.WebElement, error) { 104 | if err := installJS(wd); err != nil { 105 | return nil, fmt.Errorf("installJS: %w", err) 106 | } 107 | 108 | args := []interface{}{locator} 109 | if context != nil { 110 | args = append(args, context) 111 | } 112 | 113 | elmRaw, err := wd.ExecuteScriptRaw("return window.findElementBySemanticLocator.apply(null, arguments)", args) 114 | err = interpretError(err) 115 | if err != nil { 116 | return nil, fmt.Errorf("wd.ExecuteScriptRaw(window.findElementBySemanticLocator): %w", err) 117 | } 118 | 119 | return wd.DecodeElement(elmRaw) 120 | } 121 | 122 | // FindElements returns all the WebElements located by the given semantic 123 | // locator. If 'context' is non-nil, only elements that are descendants of 124 | // 'context' are included. 125 | func FindElements(wd selenium.WebDriver, locator string, context selenium.WebElement) ([]selenium.WebElement, error) { 126 | if err := installJS(wd); err != nil { 127 | return nil, fmt.Errorf("installJS: %w", err) 128 | } 129 | 130 | args := []interface{}{locator} 131 | if context != nil { 132 | args = append(args, context) 133 | } 134 | 135 | elmsRaw, err := wd.ExecuteScriptRaw("return window.findElementsBySemanticLocator.apply(null, arguments)", args) 136 | err = interpretError(err) 137 | if err != nil { 138 | return nil, fmt.Errorf("wd.ExecuteScriptRaw(window.findElementsBySemanticLocator): %w", err) 139 | } 140 | 141 | return wd.DecodeElements(elmsRaw) 142 | } 143 | -------------------------------------------------------------------------------- /javascript/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * @fileoverview Common types used across semantic locators. 9 | */ 10 | 11 | /** 12 | * The native selector definition(s) to match elements with a specific role. If 13 | * `exactSelector` is present then any element which matches it as a CSS 14 | * selector has the relevant role. An element is matched when either 15 | * `exactSelector` or at least one of `conditionalSelectors` matches. 16 | */ 17 | export interface RoleSelector { 18 | readonly exactSelector?: string; 19 | readonly conditionalSelectors?: readonly ConditionalSelector[]; 20 | } 21 | 22 | /** 23 | * Selector for a role. Elements must match `greedySelector` as a CSS selector 24 | * and all `conditions` must evaluate to true. 25 | */ 26 | export interface ConditionalSelector { 27 | readonly greedySelector: string; 28 | readonly conditions: readonly Condition[]; 29 | } 30 | 31 | /** 32 | * The attributes (states and properties) currently supported by Semantic 33 | * Locators. These correspond to the ARIA states and properties at 34 | * https://www.w3.org/WAI/PF/aria/states_and_properties. The 'aria-' prefix is 35 | * dropped, so `checked` represents the `aria-checked` state. 36 | * 37 | * We will add more supported attributes as use cases arise - please file a bug 38 | * (internally) or a GitHub issue with your use case. 39 | */ 40 | export const SUPPORTED_ATTRIBUTES = [ 41 | // States: 42 | 'checked', 43 | 'current', 44 | 'disabled', 45 | 'pressed', 46 | 'selected', 47 | // Properties: 48 | 'colindex', 49 | 'level', 50 | 'posinset', 51 | 'readonly', 52 | 'rowindex', 53 | ] as const; 54 | 55 | /** Union type of all supported attributes. */ 56 | export type SupportedAttributeType = typeof SUPPORTED_ATTRIBUTES[number]; 57 | 58 | /** 59 | * Conditions which must be satisfied for HTML elements to take certain 60 | * explicit roles. These are the conditions which cannot be expressed as CSS 61 | * selectors. 62 | */ 63 | export type Condition = 64 | ForbiddenAncestors|AttributeValueLessThan|AttributeValueGreaterThan| 65 | HasAccessibleName|PropertyTakesBoolValue|PropertyTakesOneOfStringValues| 66 | ClosestAncestorTagHasRole|DataInRow|DataInColumn; 67 | 68 | /** Type of `Condition` */ 69 | export enum ConditionType { 70 | PROPERTY_TAKES_BOOL_VALUE, 71 | FORBIDDEN_ANCESTORS, 72 | ATTRIBUTE_VALUE_GREATER_THAN, 73 | ATTRIBUTE_VALUE_LESS_THAN, 74 | HAS_ACCESSIBLE_NAME, 75 | PROPERTY_TAKES_ONE_OF_STRING_VALUES, 76 | CLOSEST_ANCESTOR_TAG_HAS_ROLE, 77 | DATA_IN_ROW, 78 | DATA_IN_COLUMN, 79 | } 80 | 81 | /** A property (IDL attribute) must take a certain boolean value. */ 82 | export interface PropertyTakesBoolValue { 83 | readonly type: ConditionType.PROPERTY_TAKES_BOOL_VALUE; 84 | readonly propertyName: keyof HTMLElement; 85 | readonly value: boolean; 86 | } 87 | 88 | /** A condition forbidding ancestors matching a certain selector. */ 89 | export interface ForbiddenAncestors { 90 | readonly type: ConditionType.FORBIDDEN_ANCESTORS; 91 | readonly forbiddenAncestorSelector: string; 92 | } 93 | 94 | /** The value of an attribute must be greater than a certain value. */ 95 | export interface AttributeValueGreaterThan { 96 | readonly type: ConditionType.ATTRIBUTE_VALUE_GREATER_THAN; 97 | readonly attribute: string; 98 | readonly value: number; 99 | } 100 | 101 | /** The value of an attribute must be less than a certain value. */ 102 | export interface AttributeValueLessThan { 103 | readonly type: ConditionType.ATTRIBUTE_VALUE_LESS_THAN; 104 | readonly attribute: string; 105 | readonly value: number; 106 | } 107 | 108 | /** The element must have an accessible name. */ 109 | export interface HasAccessibleName { 110 | readonly type: ConditionType.HAS_ACCESSIBLE_NAME; 111 | } 112 | 113 | /** A property must take one of a list of values. */ 114 | export interface PropertyTakesOneOfStringValues { 115 | readonly type: ConditionType.PROPERTY_TAKES_ONE_OF_STRING_VALUES; 116 | readonly propertyName: string; 117 | readonly values: readonly string[]; 118 | } 119 | 120 | /** The closest element must have the given role. */ 121 | export interface ClosestAncestorTagHasRole { 122 | readonly type: ConditionType.CLOSEST_ANCESTOR_TAG_HAS_ROLE; 123 | readonly tag: keyof HTMLElementTagNameMap; 124 | readonly role: string; 125 | } 126 | 127 | /** 128 | * If the containing table has data in slots which overlap with the 129 | * y-coordinates of the target element. 130 | * https://html.spec.whatwg.org/multipage/tables.html#column-header 131 | */ 132 | export interface DataInRow { 133 | readonly type: ConditionType.DATA_IN_ROW; 134 | readonly dataInRow: boolean; 135 | } 136 | 137 | /** 138 | * If the containing table has data in slots which overlap with the 139 | * x-coordinates of the target element. 140 | * https://html.spec.whatwg.org/multipage/tables.html#column-header 141 | */ 142 | export interface DataInColumn { 143 | readonly type: ConditionType.DATA_IN_COLUMN; 144 | readonly dataInColumn: boolean; 145 | } 146 | 147 | /** A quote character. */ 148 | export type QuoteChar = `'`|'"'; 149 | -------------------------------------------------------------------------------- /javascript/test/lib/parse_locator_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {parse} from '../../src/lib/parse_locator'; 8 | import {SemanticLocator, SemanticNode} from '../../src/lib/semantic_locator'; 9 | 10 | describe('parser', () => { 11 | it('should allow a role without a name', () => { 12 | expect(parse('{button}')) 13 | .toEqual(new SemanticLocator([new SemanticNode('button', [])], [])); 14 | }); 15 | it('should fail to parse if role is invalid', () => { 16 | expect(() => parse('{foo}')).toThrowError(/Unknown role: foo/); 17 | }); 18 | it('should allow double quotes', () => { 19 | expect(parse('{button "OK"}')) 20 | .toEqual( 21 | new SemanticLocator([new SemanticNode('button', [], 'OK')], [])); 22 | }); 23 | it('should allow single qutoes within a double quoted string', () => { 24 | expect(parse(`{button "I'm OK"}`)) 25 | .toEqual(new SemanticLocator( 26 | [new SemanticNode('button', [], `I'm OK`)], [])); 27 | }); 28 | it('should allow escaped double quotes within a double quoted string', () => { 29 | expect(parse('{button "\\"OK\\""}')) 30 | .toEqual( 31 | new SemanticLocator([new SemanticNode('button', [], '"OK"')], [])); 32 | }); 33 | it('should allow single quotes', () => { 34 | expect(parse(`{button 'OK'}`)) 35 | .toEqual( 36 | new SemanticLocator([new SemanticNode('button', [], 'OK')], [])); 37 | }); 38 | it('should allow escaped single qutoes within a single quoted string', () => { 39 | expect(parse(`{button 'I\\'m OK'}`)) 40 | .toEqual(new SemanticLocator( 41 | [new SemanticNode('button', [], `I'm OK`)], [])); 42 | }); 43 | it('should allow double quotes within a single quoted string', () => { 44 | expect(parse(`{button '"OK"'}`)) 45 | .toEqual( 46 | new SemanticLocator([new SemanticNode('button', [], '"OK"')], [])); 47 | }); 48 | it('should fail to parse if quotes are missing', () => { 49 | expect(() => parse('{button OK}')).toThrow(); 50 | }); 51 | it('should fail to parse if quotes are unclosed', () => { 52 | expect(() => parse('{button "OK}')).toThrow(); 53 | }); 54 | it('should allow multiple semantic nodes', () => { 55 | expect(parse(`{list 'foo'} {listitem} {button}`)) 56 | .toEqual(new SemanticLocator( 57 | [ 58 | new SemanticNode('list', [], 'foo'), 59 | new SemanticNode('listitem', []), 60 | new SemanticNode('button', []), 61 | ], 62 | [])); 63 | }); 64 | it('should allow outer at the start', () => { 65 | expect(parse('outer {button}')) 66 | .toEqual(new SemanticLocator([], [new SemanticNode('button', [])])); 67 | }); 68 | it('should allow outer in the middle', () => { 69 | expect(parse('{list} outer {listitem}')) 70 | .toEqual(new SemanticLocator( 71 | [new SemanticNode('list', [])], 72 | [new SemanticNode('listitem', [])], 73 | )); 74 | }); 75 | it('should not allow outer at the end', () => { 76 | expect(() => parse('{list} {listitem} outer ')) 77 | .toThrowError( 78 | 'Failed to parse semantic locator "{list} {listitem} outer ". ' + 79 | 'Expected "{" but end of input found.'); 80 | }); 81 | it('should allow attributes', () => { 82 | expect( 83 | parse( 84 | '{list "name" disabled:true selected:false checked:mixed} {listitem readonly:true}')) 85 | .toEqual(new SemanticLocator( 86 | [ 87 | new SemanticNode( 88 | 'list', 89 | [ 90 | {name: 'disabled' as const, value: 'true'}, 91 | {name: 'selected' as const, value: 'false'}, 92 | {name: 'checked' as const, value: 'mixed'}, 93 | ], 94 | 'name'), 95 | new SemanticNode( 96 | 'listitem', 97 | [ 98 | {name: 'readonly' as const, value: 'true'}, 99 | ]) 100 | ], 101 | [])); 102 | }); 103 | it('should allow heading level attributes', () => { 104 | expect(parse('{heading "Foo" level:1}')) 105 | .toEqual(new SemanticLocator( 106 | [new SemanticNode( 107 | 'heading', 108 | [ 109 | {name: 'level' as const, value: '1'}, 110 | ], 111 | 'Foo')], 112 | [])); 113 | }); 114 | it('should fail to parse if attribute is unsupported', () => { 115 | expect(() => parse('{list foo:bar}')) 116 | .toThrowError(/Unsupported attribute: foo/); 117 | }); 118 | it('should fail to parse an empty locator', () => { 119 | expect(() => parse('')).toThrowError('Locator is empty'); 120 | }); 121 | it('should fail to parse if braces aren\'t closed', () => { 122 | expect(() => parse('{button "OK"')) 123 | .toThrowError( 124 | 'Failed to parse semantic locator "{button "OK"". ' + 125 | 'Expected "}" or [a-z] but end of input found.'); 126 | }); 127 | it('should isolate any Unicode BiDi control chars in the accname', () => { 128 | expect(parse('{button "\u202bfoo*"}')) 129 | .toEqual(new SemanticLocator( 130 | [new SemanticNode('button', [], '\u202bfoo*')], [])); 131 | 132 | expect(parse('{button "foo\u202b"}')) 133 | .toEqual(new SemanticLocator( 134 | [new SemanticNode('button', [], 'foo\u202b')], [])); 135 | 136 | expect(parse('{button "\u202efoo*"}')) 137 | .toEqual(new SemanticLocator( 138 | [new SemanticNode('button', [], '\u202efoo*')], [])); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /webdriver_ts/src/semantic_locators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {promise as webDriverPromise, WebDriver, WebElement} from 'selenium-webdriver'; // from //third_party/javascript/typings/selenium_webdriver 8 | 9 | import {wrapperBin} from '../data/wrapper_bin'; // from //third_party/semantic_locators/webdriver_ts/data 10 | 11 | /** 12 | * A helper method which can be passed into the `findElement` or `findElements` 13 | * methods of either a `WebDriver` or a `WebElement` instance. 14 | * 15 | * Example: 16 | * const list = driver.findElement(bySemanticLocator(`{list 'Tasks'}`)) 17 | * const listItems = list.findElements(bySemanticLocator(`{listitem}`)) 18 | */ 19 | export function bySemanticLocator(locator: string): 20 | (driverOrElement: WebDriver|WebElement) => 21 | webDriverPromise.Promise { 22 | return (driverOrElement: WebDriver|WebElement) => { 23 | const driver = driverOrElement instanceof WebElement ? 24 | driverOrElement.getDriver() : 25 | driverOrElement; 26 | const rootEl = 27 | driverOrElement instanceof WebElement ? driverOrElement : undefined; 28 | return findElementsBySemanticLocator(driver, locator, rootEl); 29 | }; 30 | } 31 | 32 | /** 33 | * Find all elements in the DOM by the given semantic locator and returns them 34 | * in the correct order. 35 | */ 36 | export function findElementsBySemanticLocator( 37 | driver: WebDriver, 38 | locator: string, 39 | root?: WebElement, 40 | ): webDriverPromise.Promise { 41 | return callJsFunction( 42 | driver, 'findElementsBySemanticLocator', [locator, root]); 43 | } 44 | 45 | /** 46 | * Find the first element in the DOM by the given semantic locator. Throws 47 | * NoSuchElementError if no matching elements are found. 48 | */ 49 | export function findElementBySemanticLocator( 50 | driver: WebDriver, 51 | locator: string, 52 | root?: WebElement, 53 | ): webDriverPromise.Promise { 54 | return callJsFunction( 55 | driver, 'findElementBySemanticLocator', [locator, root]); 56 | } 57 | 58 | /** 59 | * Builds the most precise locator which matches `element` relative to `root` 60 | * (or the whole document, if not specified). If `element` does not have a role, 61 | * return a semantic locator which matches the closest ancestor with a role. 62 | * "Precise" means that it matches the fewest other elements, while being as 63 | * short as possible. 64 | * 65 | *

Returns null if no semantic locator exists for any ancestor. 66 | */ 67 | export function closestPreciseLocatorFor( 68 | element: WebElement, 69 | root?: WebElement): webDriverPromise.Promise { 70 | return callJsFunction( 71 | element.getDriver(), 'closestPreciseLocatorFor', [element, root]); 72 | } 73 | 74 | /** 75 | * Builds the most precise locator which matches `element` relative to `root` 76 | * (or the whole document, if not specified). "Precise" means that it matches 77 | * the fewest other elements, while being as short as possible. 78 | * 79 | * Returns null if no semantic locator exists. 80 | */ 81 | export function preciseLocatorFor(element: WebElement, root?: WebElement): 82 | webDriverPromise.Promise { 83 | return callJsFunction( 84 | element.getDriver(), 'preciseLocatorFor', [element, root]); 85 | } 86 | 87 | /** 88 | * Builds a semantic locator which matches `element` relative to `root` 89 | * (or the whole document, if not specified). If `element` does not have a role, 90 | * return a semantic locator which matches the closest ancestor with a role. 91 | * "Simple" means it will only ever specify one node, even if more nodes would 92 | * be more precise. i.e. returns `{button 'OK'}`, never `{listitem} {button 93 | * 'OK'}`. To generate locators for tests, `closestPreciseLocatorFor` or 94 | * `preciseLocatorFor` are usually more suitable. 95 | * 96 | * Returns null if no semantic locator exists for any ancestor. 97 | */ 98 | export function closestSimpleLocatorFor(element: WebElement, root?: WebElement): 99 | webDriverPromise.Promise { 100 | return callJsFunction( 101 | element.getDriver(), 'closestSimpleLocatorFor', [element, root]); 102 | } 103 | 104 | /** 105 | * Builds a semantic locator which matches `element`. If `element` does not have 106 | * a role, return a semantic locator which matches the closest ancestor with a 107 | * role. "Simple" means it will only ever specify one node, even if more nodes 108 | * would be more precise. i.e. returns `{button 'OK'}`, never 109 | * `{listitem} {button 'OK'}`. To generate locators for tests, 110 | * `closestPreciseLocatorFor` or `preciseLocatorFor` are usually more suitable. 111 | * 112 | * Returns null if no semantic locator exists for any ancestor. 113 | */ 114 | export function simpleLocatorFor(element: WebElement): 115 | webDriverPromise.Promise { 116 | return callJsFunction(element.getDriver(), 'simpleLocatorFor', [element]); 117 | } 118 | 119 | /** General semantic locator exception. */ 120 | export class SemanticLocatorError extends Error {} 121 | 122 | function callJsFunction( 123 | driver: WebDriver, functionName: string, 124 | args: Array): webDriverPromise.Promise { 125 | return loadDefinition(driver).then(() => { 126 | // Trim entries after 'undefined' 127 | args = args.slice(0, args.concat(undefined).indexOf(undefined)); 128 | const script = `return window.${functionName}.apply(null, arguments);`; 129 | return driver.executeScript(script, ...args); 130 | }); 131 | } 132 | 133 | 134 | function loadDefinition(driver: WebDriver): webDriverPromise.Promise { 135 | return driver.executeScript('return window.semanticLocatorsReady === true;') 136 | .then((semanticLocatorsReady: unknown) => { 137 | if (semanticLocatorsReady !== true) { 138 | return driver.executeScript(wrapperBin); 139 | } 140 | return; 141 | }); 142 | } -------------------------------------------------------------------------------- /javascript/test/lib/semantic_locator_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {SemanticLocator, SemanticNode} from '../../src/lib/semantic_locator'; 8 | 9 | // TODO(alexlloyd) test that `parse(locator.toString()) === locator` 10 | 11 | describe('SemanticNode.toString', () => { 12 | it('returns a valid locator if only a role is specified', () => { 13 | expect(new SemanticNode('button', []).toString()).toEqual('{button}'); 14 | }); 15 | 16 | it('returns a valid locator if name is specified', () => { 17 | expect(new SemanticNode('button', [], 'OK').toString()) 18 | .toEqual(`{button 'OK'}`); 19 | }); 20 | 21 | it('returns a valid locator if attributes are specified', () => { 22 | expect(new SemanticNode( 23 | 'button', 24 | [ 25 | {name: 'checked', value: 'false'}, 26 | {name: 'disabled', value: 'true'}, 27 | ]) 28 | .toString()) 29 | .toEqual('{button checked:false disabled:true}'); 30 | }); 31 | 32 | it('returns a valid locator if name and attributes are specified', () => { 33 | expect(new SemanticNode( 34 | 'button', 35 | [ 36 | {name: 'checked', value: 'false'}, 37 | {name: 'disabled', value: 'true'}, 38 | ], 39 | 'OK', 40 | ) 41 | .toString()) 42 | .toEqual(`{button 'OK' checked:false disabled:true}`); 43 | }); 44 | 45 | it('returns a double quoted locator if name contains single quotes', () => { 46 | expect(new SemanticNode('button', [], `What's new?`).toString()) 47 | .toEqual(`{button "What's new?"}`); 48 | }); 49 | 50 | it('returns a single quoted locator if name contains double quotes', () => { 51 | expect(new SemanticNode('button', [], '"Coming up"').toString()) 52 | .toEqual(`{button '"Coming up"'}`); 53 | }); 54 | 55 | it('returns a single quoted locator with escaped quotes if name contains single and double quotes', 56 | () => { 57 | expect(new SemanticNode('button', [], `"What's new?"`).toString()) 58 | .toEqual(`{button '"What\\'s new?"'}`); 59 | }); 60 | 61 | it('returns a locator using the specified quoteChar', () => { 62 | expect(new SemanticNode('button', [], `What's new?`).toString(`'`)) 63 | .toEqual(`{button 'What\\'s new?'}`); 64 | expect(new SemanticNode('button', [], '"Coming up"').toString('"')) 65 | .toEqual(`{button "\\"Coming up\\""}`); 66 | expect(new SemanticNode('button', [], `"What's new?"`).toString('"')) 67 | .toEqual(`{button "\\"What's new?\\""}`); 68 | }); 69 | }); 70 | 71 | describe('SemanticLocator', () => { 72 | describe('constructor', () => { 73 | it('throws an error if a role with presentational children is in a non=final SemanticNode', 74 | () => { 75 | expect( 76 | () => new SemanticLocator( 77 | [new SemanticNode('button', [])], 78 | [new SemanticNode('list', [])])) 79 | .toThrowError(/The role "button" has presentational children./); 80 | }); 81 | }); 82 | 83 | describe('toString', () => { 84 | it('returns a valid locator for a single preOuter SemanticNode', () => { 85 | expect(new SemanticLocator( 86 | [new SemanticNode( 87 | 'button', 88 | [ 89 | {name: 'checked', value: 'false'}, 90 | {name: 'disabled', value: 'true'}, 91 | ], 92 | 'OK', 93 | )], 94 | []) 95 | .toString()) 96 | .toEqual(`{button 'OK' checked:false disabled:true}`); 97 | }); 98 | 99 | it('returns a valid locator for multiple preOuter SemanticNodes', () => { 100 | expect(new SemanticLocator( 101 | [ 102 | new SemanticNode('list', [], 'My calendars'), 103 | new SemanticNode( 104 | 'button', 105 | [ 106 | {name: 'checked', value: 'false'}, 107 | {name: 'disabled', value: 'true'}, 108 | ], 109 | 'OK', 110 | ) 111 | ], 112 | []) 113 | .toString()) 114 | .toEqual( 115 | `{list 'My calendars'} {button 'OK' checked:false disabled:true}`); 116 | }); 117 | 118 | it('returns a valid locator for multiple postOuter SemanticNodes', () => { 119 | expect(new SemanticLocator( 120 | [], 121 | [ 122 | new SemanticNode('list', [], 'My calendars'), 123 | new SemanticNode( 124 | 'button', 125 | [ 126 | {name: 'checked', value: 'false'}, 127 | {name: 'disabled', value: 'true'}, 128 | ], 129 | 'OK', 130 | ) 131 | ]) 132 | .toString()) 133 | .toEqual( 134 | `outer {list 'My calendars'} {button 'OK' checked:false disabled:true}`); 135 | }); 136 | 137 | it('returns a valid locator for both preOuter and postOuter SemanticNodes', 138 | () => { 139 | expect(new SemanticLocator( 140 | [ 141 | new SemanticNode('list', [], 'My calendars'), 142 | ], 143 | [ 144 | new SemanticNode( 145 | 'button', 146 | [ 147 | {name: 'checked', value: 'false'}, 148 | {name: 'disabled', value: 'true'}, 149 | ], 150 | 'OK', 151 | ), 152 | ]) 153 | .toString()) 154 | .toEqual( 155 | `{list 'My calendars'} outer {button 'OK' checked:false disabled:true}`); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Semantic Locators 2 | 3 | ![Magnifying glass icon](docs/img/icon_64dp.svg) 4 | 5 | Semantic Locators let you specify HTML elements in code similar to how you might 6 | describe them to a human. For example, a create button might have a semantic 7 | locator of `{button 'Create'}`. 8 | 9 | Semantic Locators are stable, readable, enforce accessibility, and can be 10 | auto-generated. 11 | 12 | Just want to get started writing semantic locators? See the 13 | [tutorial](docs/tutorial.md), or read on for an introduction. 14 | 15 | ## Getting started 16 | 17 | See the getting started instructions for your environment: 18 | 19 | * [JavaScript/TypeScript in the browser](javascript/README.md) 20 | * [Java WebDriver](webdriver_java/README.md) 21 | * [Python WebDriver](webdriver_python/README.md) 22 | * [.NET WebDriver](webdriver_dotnet/README.md) 23 | * Something else? Adding support for a new platform is simple. See 24 | [DEVELOPING.md](docs/DEVELOPING.md) for instructions. 25 | 26 | ## Examples 27 | 28 | HTML | Semantic Locator 29 | -------------------------------------------------- | -------------------------- 30 | `` | `{button 'OK'}` 31 | `

` | `{tab 'Meeting'}` 32 | `
  • ` | `{list} {listitem}` 33 | `` | `{button 'User id: *'}` 34 | `
    ` | `{checkbox checked:false}` 35 | 36 | ## Why Semantic Locators? 37 | 38 | As the name suggests, Semantic Locators find elements based on their 39 | **semantics**. This has a number of benefits over other types of locators. 40 | 41 | ### Semantics 42 | 43 | First we should define the term "semantics". 44 | 45 | The semantics of an element describe its meaning to a user. Is it a button or a 46 | checkbox? Will it submit or cancel an operation? When using assistive 47 | technologies like screen readers, the semantics of an element determine how it 48 | is described to users. 49 | 50 | There are many semantically equivalent ways to implement OK buttons. The 51 | following elements are all matched by the semantic locator `{button 'OK'}`. 52 | 53 | ```html 54 | 55 | 56 |
    OK
    57 | 58 |
    OK
    59 | 60 | ``` 61 | 62 | To be precise, `button` refers to the 63 | **[ARIA role](https://www.w3.org/TR/wai-aria/#usage_intro)** expressed by the 64 | element. `'OK'` refers to the 65 | **[accessible name](https://www.w3.org/TR/accname/#dfn-accessible-name)** of the 66 | element. 67 | 68 | What benefits does finding elements by their semantics provide? 69 | 70 | ### Stability 71 | 72 | Semantic locators are less brittle to user-invisible changes. Matching semantics 73 | abstracts away implementation details. For example if 74 | 75 | ```html 76 |
    77 | ``` 78 | 79 | changes to 80 | 81 | ```html 82 |
    ", 55 | }, 56 | { 57 | desc: "double quotes", 58 | locator: "{button \"OK\"}", 59 | html: "", 60 | }, 61 | { 62 | desc: "nested elements", 63 | locator: "{list} {listitem}", 64 | html: "
    • foo
    ", 65 | }, 66 | { 67 | desc: "wildcard at start", 68 | locator: "{button '*and_end'}", 69 | html: "", 70 | }, 71 | { 72 | desc: "wildcard at end", 73 | locator: "{button 'beginning_and*'}", 74 | html: "", 75 | }, 76 | { 77 | desc: "wildcard at start and end", 78 | locator: "{button '*and*'}", 79 | html: "", 80 | }, 81 | { 82 | desc: "outer list", 83 | locator: "{region} outer {list}", 84 | html: "
      • foo
    ", 85 | }, 86 | { 87 | desc: "unicode and control characters", 88 | locator: "{button 'блČλñéç‪हिन्दी‬日本語‬‪한국어‬й‪ไ🤖-—–;|<>!\"_+'}", 89 | html: "", 90 | }, 91 | { 92 | desc: "escaped quotes", 93 | locator: "{ button '\\'escaped quotes\\\\\\' and unescaped\\\\\\\\'}", 94 | html: "", 95 | }, 96 | } 97 | 98 | for _, test := range tests { 99 | t.Run(test.desc, func(t *testing.T) { 100 | renderHTML(t, wd, test.html) 101 | elm, err := FindElement(wd, test.locator, nil) 102 | if err != nil { 103 | t.Fatalf("semloc.FindElement: %v", err) 104 | } 105 | id, err := elm.GetAttribute("id") 106 | if err != nil { 107 | t.Fatalf("elm.GetAttribute: %v", err) 108 | } 109 | if id != "target" { 110 | t.Errorf("wrong element selected, wanted element with id 'target'") 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func TestFindElements_FindsDuplicates(t *testing.T) { 117 | wd := setup(t) 118 | renderHTML(t, wd, "
    OK
    ") 119 | 120 | elms, err := FindElements(wd, "{button 'OK'}", nil) 121 | if err != nil { 122 | t.Fatalf("FindElements: %v", err) 123 | } 124 | if len(elms) != 3 { 125 | t.Errorf("semloc.FindElements returned %d elements, but wanted 3", len(elms)) 126 | } 127 | } 128 | 129 | func TestFindWithContext(t *testing.T) { 130 | wd := setup(t) 131 | renderHTML(t, wd, "
    ") 132 | 133 | container, err := wd.FindElement(selenium.ByID, "container") 134 | if err != nil { 135 | t.Fatalf("FindElement(#container): %v", err) 136 | } 137 | 138 | elm, err := FindElement(wd, "{button 'OK'}", container) 139 | if err != nil { 140 | t.Fatalf("semloc.FindElements: %v", err) 141 | } 142 | id, err := elm.GetAttribute("id") 143 | if err != nil { 144 | t.Fatalf("elm.GetAttribute: %v", err) 145 | } 146 | if id != "target" { 147 | t.Errorf("wrong element selected, wanted element with id 'target'") 148 | } 149 | 150 | elms, err := FindElements(wd, "{button 'OK'}", container) 151 | if err != nil { 152 | t.Fatalf("semloc.FindElements: %v", err) 153 | } 154 | if len(elms) != 1 { 155 | t.Fatalf("semloc.FindElements returned %d elements, but expected 1", len(elms)) 156 | } 157 | id, err = elms[0].GetAttribute("id") 158 | if err != nil { 159 | t.Fatalf("elm.GetAttribute: %v", err) 160 | } 161 | if id != "target" { 162 | t.Errorf("wrong element selected, wanted element with id 'target'") 163 | } 164 | } 165 | 166 | func TestInvalidSyntax(t *testing.T) { 167 | wd := setup(t) 168 | renderHTML(t, wd, "
    ") 169 | 170 | tests := []struct { 171 | desc string 172 | locator string 173 | }{ 174 | {"unterminated", "{button 'OK'"}, 175 | {"bad escaping", "{button 'OK\\'}"}, 176 | } 177 | 178 | for _, test := range tests { 179 | t.Run(test.desc, func(t *testing.T) { 180 | _, err := FindElement(wd, test.locator, nil) 181 | if got, want := getErrMsg(t, err), "invalid selector"; got != want { 182 | t.Errorf("err returned by FindElement had message %q, but wanted %q", got, want) 183 | } 184 | 185 | _, err = FindElements(wd, test.locator, nil) 186 | if got, want := getErrMsg(t, err), "invalid selector"; got != want { 187 | t.Errorf("err returned by FindElements had message %q, but wanted %q", got, want) 188 | } 189 | }) 190 | } 191 | } 192 | 193 | func TestNoSuchElement(t *testing.T) { 194 | wd := setup(t) 195 | _, err := FindElement(wd, "{button 'this label does not exist'}", nil) 196 | if got, want := getErrMsg(t, err), "no such element"; got != want { 197 | t.Errorf("err returned by FindElements had message %q, but wanted %q", got, want) 198 | } 199 | } 200 | 201 | func getErrMsg(t *testing.T, err error) string { 202 | if err == nil { 203 | return "" 204 | } 205 | var serr *selenium.Error 206 | if !errors.As(err, &serr) { 207 | t.Errorf("expected error to be a selenium.Error, but was %#v", err) 208 | } 209 | return serr.Err 210 | } 211 | -------------------------------------------------------------------------------- /javascript/test/lib/lookup_result_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {html, render} from 'lit'; 8 | 9 | import {buildFailureMessage} from '../../src/lib/lookup_result'; 10 | import {SemanticLocator, SemanticNode} from '../../src/lib/semantic_locator'; 11 | 12 | const DUMMY_LOCATOR = new SemanticLocator([new SemanticNode('button', [])], []); 13 | const START = `Didn't find any elements matching semantic locator {button}.`; 14 | 15 | let container: HTMLElement; 16 | 17 | beforeEach(() => { 18 | container = document.createElement('div'); 19 | document.body.appendChild(container); 20 | }); 21 | 22 | afterEach(() => { 23 | document.body.removeChild(container); 24 | }); 25 | 26 | describe('buildFailureMessage', () => { 27 | it('explains if no elements match any of the locator', () => { 28 | expect(buildFailureMessage( 29 | DUMMY_LOCATOR, 30 | { 31 | closestFind: [], 32 | elementsFound: [], 33 | notFound: {role: 'button'}, 34 | }, 35 | [], 36 | [], 37 | )) 38 | .toEqual(`${START} No elements have an ARIA role of button.`); 39 | }); 40 | 41 | it('explains if role fails to match', () => { 42 | expect( 43 | buildFailureMessage( 44 | DUMMY_LOCATOR, 45 | { 46 | closestFind: [ 47 | new SemanticNode('list', []), new SemanticNode('listitem', []) 48 | ], 49 | elementsFound: [ 50 | document.createElement('div'), document.createElement('div') 51 | ], 52 | notFound: {role: 'button'} 53 | }, 54 | [], 55 | [], 56 | )) 57 | .toEqual(`${ 58 | START} 2 elements matched the locator {list} {listitem}, but none had a descendant with an ARIA role of button.`); 59 | }); 60 | 61 | it('explains if accname fails to match and no elements have accnames', () => { 62 | render(html`
    foo
    bar
    `, container); 63 | expect( 64 | buildFailureMessage( 65 | DUMMY_LOCATOR, 66 | { 67 | closestFind: [new SemanticNode('list', [])], 68 | partialFind: {role: 'listitem'}, 69 | elementsFound: [ 70 | document.getElementById('foo')!, document.getElementById('bar')! 71 | ], 72 | notFound: {name: 'OK'} 73 | }, 74 | [], 75 | [], 76 | )) 77 | .toEqual(`${ 78 | START} 2 descendants of {list} with an ARIA role of listitem were found. However none had an accessible name of "OK". No matching elements had an accessible name.`); 79 | }); 80 | 81 | it('contains accnames for near misses', () => { 82 | render(html`
    OK
    ", driver); 45 | Assert.That(driver.FindElements(new BySemanticLocator("{button 'OK'}")).Count == 3); 46 | } 47 | 48 | [Test] 49 | [TestCaseSource(nameof(TestBase.GetAllDriverNames))] 50 | public void FindElement_FindsWithinContext(string driverName) 51 | { 52 | IWebDriver driver = GetDriver(driverName); 53 | RenderHtml("
    ", driver); 54 | IWebElement element = driver.FindElement(By.Id("container")).FindElement(new BySemanticLocator("{button 'OK'}")); 55 | Assert.That(element.GetAttribute("id").Equals("target")); 56 | } 57 | 58 | [Test] 59 | [TestCaseSource(nameof(TestBase.GetAllDriverNames))] 60 | public void FindElements_FindsWithinContext(string driverName) 61 | { 62 | IWebDriver driver = GetDriver(driverName); 63 | RenderHtml("
    ", driver); 64 | var elements = driver.FindElement(By.Id("container")).FindElements(new BySemanticLocator("{button 'OK'}")); 65 | Assert.That(elements.Count == 1); 66 | } 67 | 68 | [Test] 69 | [TestCaseSource(nameof(TestBase.InvalidSyntaxTests))] 70 | public void FindElements_ThrowsExceptionForInvalidSyntax(TestAssignment assignment) 71 | { 72 | IWebDriver driver = GetDriver(assignment.BrowserName); 73 | Assert.Throws(typeof(InvalidSelectorException), () => driver.FindElements(new BySemanticLocator(assignment.Semantic))); 74 | } 75 | 76 | [Test] 77 | [TestCaseSource(nameof(TestBase.GetAllDriverNames))] 78 | public void FindElement_ThrowsExceptionForNoElementFound(string driverName) 79 | { 80 | IWebDriver driver = GetDriver(driverName); 81 | Assert.Throws(typeof(NoSuchElementException), () => driver.FindElement(new BySemanticLocator("{button 'this label does not exist'}"))); 82 | } 83 | 84 | [Test] 85 | [TestCaseSource(nameof(TestBase.PreciseLocatorForWithoutRootTests))] 86 | public void PreciseLocatorFor_GeneratesLocatorForElement(TestAssignment assignment) 87 | { 88 | IWebDriver driver = GetDriver(assignment.BrowserName); 89 | RenderHtml(assignment.Html, driver); 90 | 91 | IWebElement target = driver.FindElement(By.Id("target")); 92 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.PreciseLocatorFor(target)); 93 | } 94 | 95 | [Test] 96 | [TestCaseSource(nameof(TestBase.PreciseLocatorForWithRootTests))] 97 | public void PreciseLocatorFor_AcceptsRootEl(TestAssignment assignment) 98 | { 99 | IWebDriver driver = GetDriver(assignment.BrowserName); 100 | RenderHtml(assignment.Html, driver); 101 | 102 | IWebElement target = driver.FindElement(By.Id("target")); 103 | IWebElement root = driver.FindElement(By.Id("root")); 104 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.PreciseLocatorFor(target, root)); 105 | } 106 | 107 | [Test] 108 | [TestCaseSource(nameof(TestBase.PreciseLocatorForWithoutRootTests))] 109 | public void ClosestPreciseLocatorFor_GeneratesLocatorForElement(TestAssignment assignment) 110 | { 111 | IWebDriver driver = GetDriver(assignment.BrowserName); 112 | RenderHtml(assignment.Html, driver); 113 | 114 | IWebElement target = driver.FindElement(By.Id("target")); 115 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.ClosestPreciseLocatorFor(target)); 116 | } 117 | 118 | [Test] 119 | [TestCaseSource(nameof(TestBase.ClosestPreciseLocatorForWithRootTests))] 120 | public void ClosestPreciseLocatorFor_AcceptsRootEl(TestAssignment assignment) 121 | { 122 | IWebDriver driver = GetDriver(assignment.BrowserName); 123 | RenderHtml(assignment.Html, driver); 124 | 125 | IWebElement target = driver.FindElement(By.Id("target")); 126 | IWebElement root = driver.FindElement(By.Id("root")); 127 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.ClosestPreciseLocatorFor(target, root)); 128 | } 129 | 130 | [Test] 131 | [TestCaseSource(nameof(TestBase.ClosestSimpleLocatorForWithoutRootTests))] 132 | public void ClosestSimpleLocatorFor_GeneratesSimpleLocators(TestAssignment assignment) 133 | { 134 | IWebDriver driver = GetDriver(assignment.BrowserName); 135 | RenderHtml(assignment.Html, driver); 136 | 137 | IWebElement target = driver.FindElement(By.Id("target")); 138 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.ClosestSimpleLocatorFor(target)); 139 | } 140 | 141 | [Test] 142 | [TestCaseSource(nameof(TestBase.ClosestSimpleLocatorForWithRootTests))] 143 | public void ClosestSimpleLocatorFor_AcceptsRootEl(TestAssignment assignment) 144 | { 145 | IWebDriver driver = GetDriver(assignment.BrowserName); 146 | RenderHtml(assignment.Html, driver); 147 | 148 | IWebElement target = driver.FindElement(By.Id("target")); 149 | IWebElement root = driver.FindElement(By.Id("root")); 150 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.ClosestSimpleLocatorFor(target, root)); 151 | } 152 | 153 | [Test] 154 | [TestCaseSource(nameof(TestBase.SimpleLocatorForTests))] 155 | public void SimpleLocatorFor_GeneratesSimpleLocators(TestAssignment assignment) 156 | { 157 | IWebDriver driver = GetDriver(assignment.BrowserName); 158 | RenderHtml(assignment.Html, driver); 159 | 160 | IWebElement target = driver.FindElement(By.Id("target")); 161 | Assert.AreEqual(assignment.Semantic, BySemanticLocator.SimpleLocatorFor(target)); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /webdriver_dotnet/SemanticLocators/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudio 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio 4 | 5 | ### VisualStudio ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Ww][Ii][Nn]32/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | [Ll]ogs/ 39 | 40 | # Visual Studio 2015/2017 cache/options directory 41 | .vs/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # ASP.NET Scaffolding 71 | ScaffoldingReadMe.txt 72 | 73 | # StyleCop 74 | StyleCopReport.xml 75 | 76 | # Files built by Visual Studio 77 | *_i.c 78 | *_p.c 79 | *_h.h 80 | *.ilk 81 | *.meta 82 | *.obj 83 | *.iobj 84 | *.pch 85 | *.pdb 86 | *.ipdb 87 | *.pgc 88 | *.pgd 89 | *.rsp 90 | *.sbr 91 | *.tlb 92 | *.tli 93 | *.tlh 94 | *.tmp 95 | *.tmp_proj 96 | *_wpftmp.csproj 97 | *.log 98 | *.vspscc 99 | *.vssscc 100 | .builds 101 | *.pidb 102 | *.svclog 103 | *.scc 104 | 105 | # Chutzpah Test files 106 | _Chutzpah* 107 | 108 | # Visual C++ cache files 109 | ipch/ 110 | *.aps 111 | *.ncb 112 | *.opendb 113 | *.opensdf 114 | *.sdf 115 | *.cachefile 116 | *.VC.db 117 | *.VC.VC.opendb 118 | 119 | # Visual Studio profiler 120 | *.psess 121 | *.vsp 122 | *.vspx 123 | *.sap 124 | 125 | # Visual Studio Trace Files 126 | *.e2e 127 | 128 | # TFS 2012 Local Workspace 129 | $tf/ 130 | 131 | # Guidance Automation Toolkit 132 | *.gpState 133 | 134 | # ReSharper is a .NET coding add-in 135 | _ReSharper*/ 136 | *.[Rr]e[Ss]harper 137 | *.DotSettings.user 138 | 139 | # TeamCity is a build add-in 140 | _TeamCity* 141 | 142 | # DotCover is a Code Coverage Tool 143 | *.dotCover 144 | 145 | # AxoCover is a Code Coverage Tool 146 | .axoCover/* 147 | !.axoCover/settings.json 148 | 149 | # Coverlet is a free, cross platform Code Coverage Tool 150 | coverage*[.json, .xml, .info] 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ### VisualStudio Patch ### 368 | # Additional files built by Visual Studio 369 | *.tlog 370 | 371 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio -------------------------------------------------------------------------------- /javascript/src/lib/find_by_semantic_locator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2021 The Semantic Locators Authors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import {getNameFor, nameMatches} from './accessible_name'; 8 | import {computeARIAAttributeValue} from './attribute'; 9 | import {cachedDuringBatch, inBatchOp, runBatchOp} from './batch_cache'; 10 | import {NoSuchElementError} from './error'; 11 | import {buildFailureMessage, combineMostSpecific, EmptyResultsMetadata, isEmptyResultsMetadata, isNonEmptyResult, Result} from './lookup_result'; 12 | import {outerNodesOnly} from './outer'; 13 | import {parse} from './parse_locator'; 14 | import {findByRole} from './role'; 15 | import {SemanticLocator, SemanticNode} from './semantic_locator'; 16 | import {assertInDocumentOrder, compareNodeOrder, removeDuplicates} from './util'; 17 | 18 | /** 19 | * Find all elements in the DOM by the given semantic locator and returns them 20 | * in the correct order. 21 | */ 22 | export function findElementsBySemanticLocator( 23 | locator: string, 24 | root: HTMLElement = document.body, 25 | ): HTMLElement[] { 26 | const result = findBySemanticLocator(parse(locator), root); 27 | if (isEmptyResultsMetadata(result)) { 28 | return []; 29 | } 30 | return result.found as HTMLElement[]; 31 | } 32 | 33 | /** 34 | * Find the first element in the DOM by the given semantic locator. Throws 35 | * NoSuchElementError if no matching elements are found. 36 | */ 37 | export function findElementBySemanticLocator( 38 | locator: string, 39 | root: HTMLElement = document.body, 40 | ): HTMLElement { 41 | const parsed = parse(locator); 42 | const result = findBySemanticLocator(parsed, root); 43 | if (isEmptyResultsMetadata(result)) { 44 | throw new NoSuchElementError(getFailureMessage(parsed, root, result)); 45 | } 46 | return result.found[0]; 47 | } 48 | 49 | /** 50 | * Build a string explaining the failure in `result`. 51 | * 52 | * This function performs locator resolution while investigating the failure. 53 | */ 54 | export function getFailureMessage( 55 | locator: SemanticLocator, root: HTMLElement, result: EmptyResultsMetadata) { 56 | const hiddenResult = findBySemanticLocator(locator, root, true); 57 | const hiddenMatches = 58 | isEmptyResultsMetadata(hiddenResult) ? [] : hiddenResult.found; 59 | const presentationalResult = 60 | findBySemanticLocator(locator, root, false, true); 61 | const presentationalMatches = isEmptyResultsMetadata(presentationalResult) ? 62 | [] : 63 | presentationalResult.found; 64 | return buildFailureMessage( 65 | locator, result, hiddenMatches, presentationalMatches); 66 | } 67 | 68 | /** 69 | * @return a list of elements in the document which are matched by the locator. 70 | * Returns elements in document order. 71 | */ 72 | export function findBySemanticLocator( 73 | locator: SemanticLocator, 74 | root: HTMLElement = document.body, 75 | includeHidden: boolean = false, 76 | includePresentational: boolean = false, 77 | ): Result { 78 | let result: Result|null = null; 79 | if (inBatchOp()) { 80 | result = findBySemanticLocatorCached( 81 | locator, root, includeHidden, includePresentational); 82 | } else { 83 | runBatchOp(() => { 84 | result = findBySemanticLocatorCached( 85 | locator, root, includeHidden, includePresentational); 86 | }); 87 | } 88 | return result!; 89 | } 90 | 91 | const findBySemanticLocatorCached = 92 | cachedDuringBatch(findBySemanticLocatorInternal); 93 | 94 | function findBySemanticLocatorInternal( 95 | locator: SemanticLocator, 96 | root: HTMLElement, 97 | includeHidden: boolean, 98 | includePresentational: boolean, 99 | ): Result { 100 | const searchBase = findBySemanticNodes( 101 | locator.preOuter, [root], includeHidden, includePresentational); 102 | if (isEmptyResultsMetadata(searchBase)) { 103 | return searchBase; 104 | } 105 | if (locator.postOuter.length === 0) { 106 | return searchBase; 107 | } 108 | const results = 109 | searchBase 110 | .found 111 | // 'outer' semantics are relative to the search base so we must do a 112 | // separate call to findBySemanticNodes for each base, then filter the 113 | // results for each base individually 114 | // TODO(alexlloyd) this could be optimised with a k-way merge removing 115 | // duplicates rather than concat + sort in separate steps. 116 | .map( 117 | base => findBySemanticNodes( 118 | locator.postOuter, [base], includeHidden, 119 | includePresentational)); 120 | const elementsFound = results.filter(isNonEmptyResult) 121 | .flatMap(result => outerNodesOnly(result.found)); 122 | 123 | if (elementsFound.length === 0) { 124 | const noneFound = combineMostSpecific(results as EmptyResultsMetadata[]); 125 | return { 126 | closestFind: locator.preOuter.concat(noneFound.closestFind), 127 | elementsFound: noneFound.elementsFound, 128 | notFound: noneFound.notFound, 129 | partialFind: noneFound.partialFind, 130 | }; 131 | } 132 | 133 | // If node.outer then there's no guarantee that elements are 134 | // unique or in document order. 135 | // 136 | // e.g. locator "{list} outer {listitem}" and DOM: 137 | // 138 | //
      139 | //
        140 | //
      • 141 | //
      142 | //
    • 143 | //
    144 | // 145 | // searchBase = [a, b] so found = [c, d, c] 146 | // So sort by document order to maintain the invariant 147 | return {found: removeDuplicates(elementsFound.sort(compareNodeOrder))}; 148 | } 149 | 150 | function findBySemanticNodes( 151 | nodes: readonly SemanticNode[], 152 | searchBase: readonly HTMLElement[], 153 | includeHidden: boolean, 154 | includePresentational: boolean, 155 | ): Result { 156 | for (let i = 0; i < nodes.length; i++) { 157 | const result = findBySemanticNode( 158 | nodes[i], searchBase, includeHidden, includePresentational); 159 | if (isEmptyResultsMetadata(result)) { 160 | return { 161 | closestFind: nodes.slice(0, i), 162 | elementsFound: result.elementsFound, 163 | notFound: result.notFound, 164 | partialFind: result.partialFind 165 | }; 166 | } 167 | searchBase = result.found; 168 | } 169 | return {found: searchBase}; 170 | } 171 | 172 | /** 173 | * @param `searchBase` elements to search below. These elements must be in 174 | * document order. 175 | * @return a list of elements under `searchBase` in document order. 176 | */ 177 | function findBySemanticNode( 178 | node: SemanticNode, 179 | searchBase: readonly HTMLElement[], 180 | includeHidden: boolean, 181 | includePresentational: boolean, 182 | ): Result { 183 | // Filter out non-outer elements as an optimisation. Suppose A and B are in 184 | // searchBase, and A contains B. Then all nodes below B are also below A so 185 | // there's no point searching below B. 186 | // 187 | // Filtering here has the added benefit of making it easy to return elements 188 | // in document order. 189 | searchBase = outerNodesOnly(searchBase); 190 | 191 | let elements = searchBase.flatMap( 192 | base => 193 | findByRole(node.role, base, includeHidden, includePresentational)); 194 | if (elements.length === 0) { 195 | return { 196 | closestFind: [], 197 | elementsFound: searchBase, 198 | notFound: {role: node.role}, 199 | }; 200 | } 201 | 202 | const attributes = node.attributes; 203 | for (let i = 0; i < attributes.length; i++) { 204 | const nextElements = elements.filter( 205 | element => computeARIAAttributeValue(element, attributes[i].name) === 206 | attributes[i].value); 207 | 208 | if (nextElements.length === 0) { 209 | return { 210 | closestFind: [], 211 | elementsFound: elements, 212 | notFound: {attribute: attributes[i]}, 213 | partialFind: {role: node.role, attributes: attributes.slice(0, i)}, 214 | }; 215 | } 216 | elements = nextElements; 217 | } 218 | 219 | if (node.name) { 220 | const nextElements = elements.filter( 221 | element => nameMatches(node.name!, getNameFor(element))); 222 | if (nextElements.length === 0) { 223 | return { 224 | closestFind: [], 225 | elementsFound: elements, 226 | notFound: {name: node.name}, 227 | partialFind: {role: node.role, attributes: node.attributes}, 228 | }; 229 | } 230 | elements = nextElements; 231 | } 232 | assertInDocumentOrder(elements); 233 | return {found: elements}; 234 | } 235 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This doc is a step-by-step guide to writing (or auto-generating) semantic 4 | locators. 5 | 6 | Semantic locators have one required part, the ARIA role, and two optional parts, 7 | accessible name and ARIA attributes. The role and name are almost always enough 8 | to identify elements. 9 | 10 | In the locator `{button 'OK'}` the role is `button` and the accessible name is 11 | `OK`. 12 | 13 | Usually you won't have to write semantic locators by hand as they can be easily 14 | auto-generated from a Chrome extension, an interactive playground, or from code. 15 | 16 | ## Chrome Extension 17 | 18 | An easy way to create Semantic Locators for your app is to auto-generate them 19 | with a 20 | [Chrome Extension](https://chrome.google.com/webstore/detail/semantic-locators/cgjejnjgdbcogfgamjebgceckcmfcmji). 21 | 22 | Install the extension and click the icon next to the URL bar to get started. 23 | 24 | ## Playground 25 | 26 | The interactive 27 | [semantic locator playground](https://google.github.io/semantic-locators/playground) 28 | auto-generates semantic locators for elements in some HTML you enter. It can be 29 | useful for: 30 | 31 | * Writing locators for many elements at once 32 | * Sharing HTML snippets with their a11y data 33 | * Debugging the semantics of an element 34 | 35 | ## Generate locators from code 36 | 37 | Locator generation is available from the semantic locator libraries. If you 38 | already have some other types of locators you can generate semantic locators for 39 | these elements by temporarily adding generation code to existing tests. 40 | 41 | The following example logs generated semantic locators to the console. However, 42 | you could go further, for example, automatically re-writing your tests to use 43 | semantic locators. We'd love to see what you build in this space! 44 | 45 | ### Java 46 | 47 | ```java 48 | import com.google.semanticlocators.BySemanticLocator; 49 | ... 50 | 51 | WebElement targetElement = driver.findElement(By.xpath("//div[@aria-label='Cheese']")); 52 | System.out.println("Semantic locator: " + BySemanticLocator.closestPreciseLocatorFor(targetElement)); 53 | ``` 54 | 55 | ### Python 56 | 57 | ```python 58 | from semantic_locators import closest_precise_locator_for 59 | ... 60 | 61 | target_element = driver.find_element(By.XPATH, "//div[@aria-label='Cheese']"); 62 | print("Semantic locator: " + closest_precise_locator_for(target_element)); 63 | ``` 64 | 65 | ### JavaScript/TypeScript 66 | 67 | ```javascript 68 | import {closestPreciseLocatorFor} from 'semantic-locators/gen' 69 | ... 70 | 71 | const targetElement = document.getElementById('cheese'); 72 | console.log('Semantic locator: ' + closestPreciseLocatorFor(targetElement)); 73 | ``` 74 | 75 | ## Developer Console 76 | 77 | If for some reason auto-generation doesn't work for you, the Accessibility tab 78 | of browser developer tools can help you easily write semantic locators. 79 | 80 | 1. Open the Developer Console by pressing F12. 81 | 2. **[Chrome]** Select the target element with the element picker (⌘+Shift+C or 82 | Ctrl+Shift+C) then navigate to the Accessibility tab.
    . 83 | **[Firefox]** Navigate to the Accessibility tab, click the picker icon (top left 84 | of Dev tools), then click the target element. 85 | 3. Check the name and role of the element. For an element with the role 86 | `button` and name `Create`, the semantic locator is `{button 'Create'}`. 87 | 88 | ![Screenshot of the accessibility tree in Chrome developer console. The 89 | highlighted element is described in the a11y tree as button 90 | "create"](img/a11y_tree.png) 91 | 92 | ## Dynamic or very long accessible names 93 | 94 | If the accessible name is dynamic, or is too long for your test, you can use a 95 | wildcard value. Values accept `*` as a wildcard (e.g., `'* view'`, 96 | `'https://*.google.com/*'`). 97 | 98 | [Try it in the playground](https://google.github.io/semantic-locators/playground?input=PGJ1dHRvbiBhcmlhLWxhYmVsPSJUb2RheSwgMXN0IEFwcmlsIj4gICAgPCEtLSB7YnV0dG9uICdUb2RheSonfSAtLT4KICBUb2RheQo8L2J1dHRvbj4%3D&includeTextNodes=false) 99 | 100 | ```html 101 | 104 | ``` 105 | 106 | ## Optional names 107 | 108 | It's not always necessary to specify a name - some elements have no accessible 109 | name, or a completely dynamic one. `{list}` is a valid locator if you know 110 | there's only going to be one list on the page. 111 | 112 | [Try it in the playground](https://google.github.io/semantic-locators/playground?input=PHVsPiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tIHtsaXN0fSAtLT4KICA8bGkgYXJpYS1sYWJlbD0iQ2hlZXNlIj5DaGVlc2U8L2xpPiAgICAgICAgICA8IS0tIHtsaXN0aXRlbSAnQ2hlZXNlJ30gLS0%2BCiAgPGxpIGFyaWEtbGFiZWw9IkNob2NvbGF0ZSI%2BQ2hvY29sYXRlPC9saT4gICAgPCEtLSB7bGlzdGl0ZW0gJ0Nob2NvbGF0ZSd9IC0tPgo8L3VsPg%3D%3D&includeTextNodes=false) 113 | 114 | ```html 115 |
      116 |
    • Cheese
    • 117 |
    • Chocolate
    • 118 |
    119 | ``` 120 | 121 | ## Refining locators 122 | 123 | Using the above strategies might still return multiple elements. In this case 124 | you can make a semantic locator more precise in a few ways. 125 | 126 | ### Multiple Semantic Locator elements 127 | 128 | Semantic locators can be combined, with later elements being descendants of 129 | earlier elements. 130 | 131 | Auto-generated locators will contain multiple elements if a single element 132 | doesn't uniquely identify the target. 133 | 134 | [Try it in the playground](https://google.github.io/semantic-locators/playground?input=PHVsPgogIDxsaSBhcmlhLWxhYmVsPSJDaGVlc2UiPgogICAgPGJ1dHRvbiBhcmlhLWxhYmVsPSJFYXQiPiAgIDwhLS0ge2xpc3RpdGVtICdDaGVlc2UnfSB7YnV0dG9uICdFYXQnfSAtLT4KICAgICAgRWF0IGNoZWVzZQogICAgPC9idXR0b24%2BCiAgPC9saT4KICA8bGkgYXJpYS1sYWJlbD0iQ2hvY29sYXRlIj4KICAgIDxidXR0b24gYXJpYS1sYWJlbD0iRWF0Ij4gICA8IS0tIHtsaXN0aXRlbSAnQ2hvY29sYXRlJ30ge2J1dHRvbiAnRWF0J30gLS0%2BCiAgICAgIEVhdCBjaG9jb2xhdGUKICAgIDwvYnV0dG9uPgogIDwvbGk%2BCjwvdWw%2B&includeTextNodes=false) 135 | 136 | ```html 137 |
      138 |
    • 139 | 142 |
    • 143 |
    • 144 | 147 |
    • 148 |
    149 | ``` 150 | 151 | ### Attributes 152 | 153 | Semantic locators can locate elements based on attributes such as `checked` and 154 | `disabled`. Both native html (`<button disabled>`) and explicit semantics 155 | ( `aria-disabled="true"`) are included. 156 | 157 | The source of truth for supported attributes is 158 | [`SUPPORTED_ATTRIBUTES`](https://github.com/google/semantic-locators/search?q=SUPPORTED_ATTRIBUTES+filename%3Atypes.ts). 159 | 160 | Note: Auto-generated semantic locators don't yet include attributes. 161 | 162 | [Try it in the playground](https://google.github.io/semantic-locators/playground?input=PCEtLSBOb3RlOiBUaGUgYXV0by1nZW5lcmF0ZWQgbG9jYXRvcnMgYmVsb3cgZG9uJ3QgeWV0IGluY2x1ZGUgYXR0cmlidXRlcyAtLT4KCjxoMT5DaGVlc2U8L2gxPgo8bGFiZWw%2BCiAgPGlucHV0IHR5cGU9ImNoZWNrYm94Ij4gICAgICAgPCEtLSB7Y2hlY2tib3ggJ0VkaWJsZScgY2hlY2tlZDpmYWxzZX0gLS0%2BCiAgRWRpYmxlCjwvbGFiZWw%2BCjxicj4KPGJ1dHRvbiBkaXNhYmxlZD5FYXQ8L2J1dHRvbj4gICA8IS0tIHtidXR0b24gJ0VhdCcgZGlzYWJsZWQ6dHJ1ZX0gLS0%2B&includeTextNodes=false) 163 | 164 | ```html 165 |

    Cheese

    166 | 170 |
    171 | 172 | ``` 173 | 174 | ### Outer 175 | 176 | Sometimes (e.g., when working with lists) nested elements may both match the 177 | same locator. In this case you can use the `outer` keyword to match only the 178 | outermost element. 179 | 180 | [Try it in the playground](https://google.github.io/semantic-locators/playground?input=PHVsPiAgICAgICAgICAgICAgICAgICAgPCEtLSBvdXRlciB7bGlzdH0gLS0%2BCiAgPGxpPiAgICAgICAgICAgICAgICAgIDwhLS0gb3V0ZXIge2xpc3RpdGVtfSAtLT4KICAgIDx1bD4gICAgICAgICAgICAgICAgPCEtLSB7bGlzdGl0ZW19IHtsaXN0fSAtLT4KICAgICAgPGxpPkNoZWVzZTwvbGk%2BICAgPCEtLSB7bGlzdGl0ZW19IHtsaXN0aXRlbX0gLS0%2BCiAgICA8L3VsPgogIDwvbGk%2BCjwvdWw%2B&includeTextNodes=false) 181 | 182 | ```html 183 |
      184 |
    • 185 |
        186 |
      • Cheese
      • 187 |
      188 |
    • 189 |
    190 | ``` 191 | 192 | 193 | -------------------------------------------------------------------------------- /webdriver_python/src/semantic_locators.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Semantic Locators Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Semantic locator support for WebDriver. 15 | 16 | See https://github.com/google/semantic-locators for docs. 17 | """ 18 | 19 | from typing import List, Optional, Sequence, Union 20 | 21 | import importlib_resources 22 | from selenium.common.exceptions import InvalidSelectorException 23 | from selenium.common.exceptions import JavascriptException 24 | from selenium.common.exceptions import NoSuchElementException 25 | from selenium.webdriver.remote.webdriver import WebDriver 26 | from selenium.webdriver.remote.webdriver import WebElement 27 | 28 | 29 | JS_IMPLEMENTATION = ( 30 | importlib_resources.files( 31 | "src.data" 32 | ) 33 | .joinpath("wrapper_bin.js") 34 | .read_text() 35 | ) 36 | 37 | 38 | def find_elements_by_semantic_locator( 39 | driver: WebDriver, 40 | locator: str, 41 | root: Optional[WebElement] = None, 42 | ) -> List[WebElement]: 43 | """Find all elements which match the semantic locator `locator`. 44 | 45 | Args: 46 | driver: A WebDriver instance. 47 | locator: A semantic locator to search for. 48 | root: Optional; The element within which to search for `locator`. Defaults 49 | to all elements in the document. 50 | 51 | Returns: 52 | All elements which match `locator` (under `root` if specified) 53 | 54 | Raises: 55 | InvalidSelectorException: Locator syntax is invalid. 56 | SemanticLocatorException: Failed to find elements by locator. 57 | """ 58 | args = [locator] if root is None else [locator, root] 59 | return _call_js_function(driver, "findElementsBySemanticLocator", args) 60 | 61 | 62 | def find_element_by_semantic_locator( 63 | driver: WebDriver, 64 | locator: str, 65 | root: Optional[WebElement] = None, 66 | ) -> WebElement: 67 | """Find the first element which matches the semantic locator `locator`. 68 | 69 | Args: 70 | driver: A WebDriver instance. 71 | locator: A semantic locator to search for. 72 | root: Optional; The element within which to search for `locator`. Defaults 73 | to all elements in the document. 74 | 75 | Returns: 76 | The first element which matches `locator` (under `root` if specified) 77 | 78 | Raises: 79 | NoSuchElementException: No element matched `locator`. 80 | InvalidSelectorException: Locator syntax is invalid. 81 | SemanticLocatorException: Failed to find elements by locator. 82 | """ 83 | args = [locator] if root is None else [locator, root] 84 | return _call_js_function(driver, "findElementBySemanticLocator", args) 85 | 86 | 87 | def closest_precise_locator_for(element: WebElement, 88 | root: Optional[WebElement] = None): 89 | """Builds the most precise locator which matches `element`. 90 | 91 | If `element` does not have a role, return a semantic locator which matches the 92 | closest ancestor with a role. "Precise" means that it matches the fewest other 93 | elements, while being as short as possible. 94 | 95 | Args: 96 | element: A WebElement to generate a locator for. 97 | root: Optional; The element relative to which the locator will be generated. 98 | Defaults to the whole document. 99 | 100 | Returns: 101 | The locator for `element` or its closest semantic ancestor. Returns None if 102 | no semantic locator exists for any ancestor. 103 | 104 | Raises: 105 | SemanticLocatorException: Failed to generate locator. 106 | """ 107 | args = [element] if root is None else [element, root] 108 | return _call_js_function(element.parent, "closestPreciseLocatorFor", args) 109 | 110 | 111 | def precise_locator_for(element: WebElement, root: Optional[WebElement] = None): 112 | """Builds a precise locator matching `element`. 113 | 114 | "Precise" means that it matches the fewest other elements, while being as 115 | short as possible. 116 | 117 | Args: 118 | element: A WebElement to generate a locator for. 119 | root: Optional; The element relative to which the locator will be generated. 120 | Defaults to the whole document. 121 | 122 | Returns: 123 | The locator for `element` or its closest semantic ancestor. Returns None if 124 | no semantic locator exists. 125 | 126 | Raises: 127 | SemanticLocatorException: Failed to generate locator. 128 | """ 129 | args = [element] if root is None else [element, root] 130 | return _call_js_function(element.parent, "preciseLocatorFor", args) 131 | 132 | 133 | def closest_simple_locator_for(element: WebElement, 134 | root: Optional[WebElement] = None): 135 | """Builds a semantic locator which matches `element`. 136 | 137 | If `element` does not have a role, return a semantic locator which matches the 138 | closest ancestor with a role. "Simple" means it will only ever specify one 139 | node, even if more nodes would be more precise. i.e. returns `{button 'OK'}`, 140 | never `{listitem} {button 'OK'}`. To generate locators for tests, 141 | `closestPreciseLocatorFor` or `preciseLocatorFor` are usually more suitable. 142 | 143 | Args: 144 | element: A WebElement to generate a locator for. 145 | root: Optional; The element relative to which the locator will be generated. 146 | Defaults to the whole document. 147 | 148 | Returns: 149 | The locator for `element` or its closest semantic ancestor. Returns None if 150 | no semantic locator exists for any ancestor. 151 | 152 | Raises: 153 | SemanticLocatorException: Failed to generate locator. 154 | """ 155 | args = [element] if root is None else [element, root] 156 | return _call_js_function(element.parent, "closestSimpleLocatorFor", args) 157 | 158 | 159 | def simple_locator_for(element: WebElement): 160 | """Builds a locator with only one part which matches `element`. 161 | 162 | "Simple" means it will only ever specify one node, even if more nodes would be 163 | more precise. i.e. returns `{button 'OK'}`, never `{listitem} {button 'OK'}`. 164 | To generate locators for tests, `closestPreciseLocatorFor` or 165 | `preciseLocatorFor` are usually more suitable. 166 | 167 | Args: 168 | element: A WebElement to generate a locator for. 169 | 170 | Returns: 171 | The locator for `element` or its closest semantic ancestor. Returns None if 172 | no semantic locator exists. 173 | 174 | Raises: 175 | SemanticLocatorException: Failed to generate locator. 176 | """ 177 | return _call_js_function(element.parent, "simpleLocatorFor", [element]) 178 | 179 | 180 | class SemanticLocatorException(Exception): 181 | """General semantic locator exception.""" 182 | 183 | def __init__(self, *args: object): 184 | super().__init__(args) 185 | 186 | 187 | def _call_js_function( 188 | driver: WebDriver, 189 | function: str, 190 | # Collection is better than sequence here, blocked by 191 | # https://github.com/PyCQA/pylint/issues/2377 192 | args: Sequence[Union[str, WebElement]], 193 | ): 194 | _load_definition(driver) 195 | script = f"return window.{function}.apply(null, arguments);" 196 | try: 197 | return driver.execute_script(script, *args) 198 | except JavascriptException as err: 199 | raise _deserialize_exception(err) from err 200 | 201 | 202 | def _load_definition(driver: WebDriver): 203 | if driver.execute_script("return window.semanticLocatorsReady !== true;"): 204 | driver.execute_script(JS_IMPLEMENTATION) 205 | 206 | 207 | def _deserialize_exception(err: JavascriptException) -> Exception: 208 | # The message sent back from browsers looks something like: 209 | # "Error in javascript: NoSuchElementError: nothing found...." 210 | # Where the "Error in javascript" string varies between browsers 211 | full_message = err.msg 212 | parts = full_message.split(":", 2) 213 | if len(parts) != 3: 214 | return SemanticLocatorException( 215 | f"Failed to find elements by semantic locators. {full_message}") 216 | 217 | error_type = parts[1].strip() 218 | message = parts[2].strip() 219 | 220 | if error_type == "NoSuchElementError": 221 | return NoSuchElementException(message) 222 | if error_type == "InvalidLocatorError": 223 | return InvalidSelectorException(message) 224 | return SemanticLocatorException( 225 | f"Failed to find elements by semantic locators. {error_type}: {message}") 226 | --------------------------------------------------------------------------------