Skip to content

Commit

Permalink
[#228] NEW: query.shadow_root + query.shadow_roots
Browse files Browse the repository at this point in the history
  • Loading branch information
yashaka committed May 19, 2024
1 parent 5dffb3f commit 609e28a
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 0 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ TODOs:

## 2.0.0rc10 (to be released on DD.05.2024)

### Shadow DOM support via query.js.shadow_root(s)

As simple as:

```python
from selene import browser, query, have

...

browser.element('#element-with-shadow-dom').get(query.js.shadow_root).element(
'#shadowed-element'
).click()
browser.all('.item-with-shadow-dom').get(query.js.shadow_roots).should(have.size(3))
```

See one more example at [FAQ: How to work with Shadow DOM in Selene?](https://yashaka.github.io/selene/faq/shadow-dom-howto/)

### A context manager, decorator and search context to work with iFrames (Experimental)

```python
Expand Down
25 changes: 25 additions & 0 deletions docs/faq/shadow-dom-howto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# How to work with Shadow DOM in Selene?

– By using advanced [query.js.shadow_root][selene.core.query.js.shadow_root] and [query.js.shadow_roots][selene.core.query.js.shadow_roots] queries, as simply as:

```python
from selene import browser, have, query

# GIVEN
paragraphs = browser.all('my-paragraph')

# WHEN it's enough to access specific elements
paragraph_2_shadow = paragraphs.second.get(query.js.shadow_root) # 💡
my_shadowed_text_2 = paragraph_2_shadow.element('[name=my-text]')
# OR when you need all shadow roots
my_shadowed_texts = paragraphs.get(query.js.shadow_roots) # 💡

# As you can see these queries are lazy,
# so you were able to store them in vars ↖️
# even before open ↙️
browser.open('https://the-internet.herokuapp.com/shadowdom')

# THEN
my_shadowed_text_2.should(have.exact_text("My default text")) # ⬅️
my_shadowed_texts.should(have.exact_texts("My default text", "My default text")) # ⬅️
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ nav:
# - Stub Title 1: learn-advanced/automate-testing-guide.md
- FAQ:
- How to work with iFrames: faq/iframes-howto.md
- How to work with Shadow DOM: faq/shadow-dom-howto.md
- How to use custom profile: faq/custom-user-profile-howto.md
- How to extend Selene [TBD]: faq/extending-selene-howto.md
# - Use Cases:
Expand Down
37 changes: 37 additions & 0 deletions selene/core/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ def fn(element: Element):
normalized text of element
"""

# TODO: add texts collection condition

# TODO: do we need condition for the following?
location_once_scrolled_into_view: Query[Element, Dict[str, int]] = Query(
'location once scrolled into view',
Expand Down Expand Up @@ -781,3 +783,38 @@ def page_source_saved(
return query.__call__(browser) # type: ignore

return query


class js:
shadow_root: Query[Element, Element] = Query(
'shadow root',
lambda element: Element(
Locator(
f'{element}: shadow root',
lambda: element.config.driver.execute_script(
'return arguments[0].shadowRoot', element.locate()
),
),
element.config,
),
)
"""A lazy query that actually builds an Element entity
based on element's shadow root acquired via JavaScript.
"""

shadow_roots: Query[Collection, Collection] = Query(
'shadow roots',
lambda collection: Collection(
Locator(
f'{collection}: shadow roots',
lambda: collection.config.driver.execute_script(
'return [...arguments[0]].map(arg => arg.shadowRoot)',
collection.locate(),
),
),
collection.config,
),
)
"""A lazy query that actually builds a Collection entity
based on elements' shadow roots acquired via JavaScript.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# MIT License
#
# Copyright (c) 2024 Iakiv Kramarenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import logging

import pytest

from selene import command, have, query, support


def test_actions_on_shadow_roots_of_all_elements(session_browser):
# GIVEN
browser = session_browser.with_(timeout=0.5)
paragraphs = browser.all('my-paragraph')

# WHEN even before opened browser
paragraph_shadow_roots = paragraphs.get(query.js.shadow_roots)
my_shadowed_texts = paragraph_shadow_roots.all('[name=my-text]')
# AND
browser.open('https://the-internet.herokuapp.com/shadowdom')

# THEN
my_shadowed_texts.should(have.exact_texts('My default text', 'My default text'))
paragraphs.should(
have.exact_texts(
"Let's have some different text!",
"Let's have some different text!\nIn a list!",
)
)

# WHEN failed
try:
my_shadowed_texts.should(have.exact_texts('My WRONG text', 'My WRONG text'))
pytest.fail('should have failed on size mismatch')
except AssertionError as error:
# THEN
assert (
'Message: \n'
'\n'
'Timed out after 0.5s, while waiting for:\n'
"browser.all(('css selector', 'my-paragraph')): shadow roots.all(('css "
"selector', '[name=my-text]')).has exact texts ('My WRONG text', 'My WRONG "
"text')\n"
'\n'
"Reason: AssertionError: actual visible_texts: ['My default text', 'My "
"default text']\n"
) in str(error)
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# MIT License
#
# Copyright (c) 2024 Iakiv Kramarenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import logging

import pytest

from selene import command, have, query, support


def test_actions_on_shadow_root_element(session_browser):
# GIVEN
browser = session_browser.with_(timeout=0.5)
paragraphs = browser.all('my-paragraph')

# WHEN even before opened browser
paragraph_1_shadow = paragraphs.first.get(query.js.shadow_root)
paragraph_2_shadow = paragraphs.second.get(query.js.shadow_root)
my_shadowed_text_1 = paragraph_1_shadow.element('[name=my-text]')
my_shadowed_text_2 = paragraph_2_shadow.element('[name=my-text]')
# AND
browser.open('https://the-internet.herokuapp.com/shadowdom')

# THEN
paragraphs.first.should(have.exact_text("Let's have some different text!"))
my_shadowed_text_1.should(have.exact_text("My default text"))
paragraphs.second.should(
have.exact_text("Let's have some different text!\nIn a list!")
)
my_shadowed_text_2.should(have.exact_text("My default text"))

# WHEN failed
try:
my_shadowed_text_1.should(have.exact_text("My WRONG text"))
pytest.fail('should have failed on size mismatch')
except AssertionError as error:
# THEN
assert (
'Message: \n'
'\n'
'Timed out after 0.5s, while waiting for:\n'
"browser.all(('css selector', 'my-paragraph'))[0]: shadow root.element(('css "
"selector', '[name=my-text]')).has exact text My WRONG text\n"
'\n'
'Reason: AssertionError: actual text: My default text\n'
) in str(error)

0 comments on commit 609e28a

Please sign in to comment.