Skip to content

Commit

Permalink
NEW: core to web + mobile split
Browse files Browse the repository at this point in the history
- extend web.Element with more web-specific commands
  - element.shadow_root based on `weblement.shadow_root`
    - wrapped as _SearchContext class object with only .element and .all methods
  - collection.shadow_roots based on webelement.shadow_root
  - element.frame_context
  • Loading branch information
yashaka committed Jan 30, 2025
1 parent c6eae7f commit 39b38aa
Show file tree
Hide file tree
Showing 20 changed files with 920 additions and 512 deletions.
72 changes: 49 additions & 23 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,19 @@ check vscode pylance, mypy, jetbrains qodana...

## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024)

### TODO: in addition to browser – _page for pure web and _device for pure mobile?

### DOING: draft Element descriptors POC?

#### TODO: ensure works with frames and shadow roots

#### TODO: make descriptor based PageObjects be used as descriptors on their own

#### TODO: implement pom-descriptor-like decorators to name objects returned from methods

... maybe even from properties? (but should work out of the box if @property is applied as last)

### DOING: split into core, web, mobile
### TODO: in addition to browser – _page for pure web and _device for pure mobile?
#### DOING: split into core, web, mobile

Done:
- copy&paste Browser, Element, Collection into selene.web.*
Expand All @@ -191,10 +193,17 @@ Done:
- `switch_to_previous_tab`
- `switch_to_tab`
- `switch_to`
- extend web.Element with more web-specific commands
- element.shadow_root based on `weblement.shadow_root`
- wrapped as _SearchContext class object with only .element and .all methods
- collection.shadow_roots based on webelement.shadow_root
- element.frame_context

Next:
- extend web.Element with more web-specific commands
- ...
- make core.Element a base class for web.Element
- extend web.Element with more web-specific commands (shadow-root, frames, etc.)
- ensure query.* and command.* use proper classes

### Deprecated conditions

Expand All @@ -208,7 +217,7 @@ Next:

### Added be.hidden_in_dom in addition to be.hidden

Consider `be.hidden` as "hidden somewhere, maybe in DOM with "display:none", or even on frontend/backend, i.e. totally absent from the page". Then `be.hidden_in_dom` is stricter, and means "hidden in DOM, i.e. available in the page DOM, but not visible".
Consider `be.hidden` as hidden somewhere, maybe in DOM with `"display:none"`, or even on frontend/backend, i.e. totally absent from the page. Then `be.hidden_in_dom` is stricter, and means "hidden in DOM, i.e. available in the page DOM, but not visible".

### Added experimental 4-in-1 be._empty over deprecated collection-condition be.empty

Expand Down Expand Up @@ -393,7 +402,7 @@ browser.all('li').should(have._exact_texts_like(
).where(zero_or_more=...))
```

### Text related now supports ignore_case (including regex conditions)
### Text related conditions now supports ignore_case (including regex conditions)

```python
from selene import browser, have
Expand Down Expand Up @@ -439,6 +448,21 @@ browser.all('li').first.with_(_match_ignoring_case=True).should(have.exact_text(

```

### Shadow DOM support via element.shadow_root or collection.shadow_roots

As simple as:

```python
from selene import browser, have

...

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

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

As simple as:
Expand All @@ -456,31 +480,33 @@ browser.all('.item-with-shadow-dom').get(query.js.shadow_roots).should(have.size

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)
### A context manager, decorator and search context to work with iFrames

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

my_frame_context = browser.element('#my-iframe').get(query._frame_context)
my_frame_context = browser.element('#my-iframe').frame_context
# now simply:
my_frame_context._element('#inside-iframe').click()
my_frame_context._all('.items-inside-iframe').should(have.size(3))
my_frame_context.element('#inside-iframe').click()
my_frame_context.all('.items-inside-iframe').should(have.size(3))
# – here switching to frame and back happens for each command implicitly
...
# or
with my_frame_context:
# here elements inside frame will be found when searching via browser
browser.element('#inside-iframe').click()
browser.all('.items-inside-iframe').should(have.size(3))
# this is the most speedy version,
# because switching to frame happens on entering the context
# and switching back to default content happens on exiting the context
...
# here elements inside frame will be found when searching via browser
browser.element('#inside-iframe').click()
browser.all('.items-inside-iframe').should(have.size(3))
# this is the most speedy version,
# because switching to frame happens on entering the context
# and switching back to default content happens on exiting the context
...


@my_frame_context.within
def do_something():
# and here too ;)
...

@my_frame_context._within
def do_something(self):
# and here too ;)
...

# so now you can simply call it:
do_something()
Expand All @@ -489,11 +515,11 @@ do_something()
# Switch to default content happens automatically, nevertheless;)
```

See a bit more in documented ["FAQ: How to work with iFrames in Selene?"](https://yashaka.github.io/selene/faq/iframes-howto/) and much more in ["Reference: `query.*`](https://yashaka.github.io/selene/reference/query).
See a bit more in documented ["FAQ: How to work with iFrames in Selene?"](https://yashaka.github.io/selene/faq/iframes-howto/) and much more in ["Reference: `Web/Elements`](https://yashaka.github.io/selene/reference/web/elements).

### config._disable_wait_decorator_on_get_query

`True` by default, is needed for cleaner logging implemented via `config._wait_decorator` and more optimal performance for `.get(query._frame_context)` in case of nested frames.
`True` by default, is needed for cleaner logging implemented via `config._wait_decorator` and more optimal performance for `.get(query.frame_context)` in case of nested frames.

### config.selector_to_by_strategy

Expand Down
73 changes: 38 additions & 35 deletions docs/faq/iframes-howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ browser.element('.textarea').should(

In addition to that...

## Selene provides an experimental feature – [query._frame_context][selene.core.query._frame_context] that...
## Selene provides a built-into-Element feature – [element.frame_context][selene.web._elements.Element.frame_context] that...

### 1. Either removes a lot of boilerplate but might result in performance drawback

Expand All @@ -45,23 +45,23 @@ In addition to that...

...
# GIVEN
iframe = browser.element('#editor-iframe').get(query._frame_context)
iframe = browser.element('#editor-iframe').frame_context




# THEN work with elements as if iframe is a normal parent element
iframe._all('strong').should(have.size(0))
iframe._element('.textarea').type('Hello, World!').perform(command.select_all)
iframe.all('strong').should(have.size(0))
iframe.element('.textarea').type('Hello, World!').perform(command.select_all)


# AND still dealing with elements outside iframe as usual
browser.element('#toolbar').element('#bold').click()


# AND ...
iframe._all('strong').should(have.size(1))
iframe._element('.textarea').should(
iframe.all('strong').should(have.size(1))
iframe.element('.textarea').should(
have.js_property('innerHTML').value(
'<p><strong>Hello, world!</strong></p>'
)
Expand All @@ -76,12 +76,12 @@ In addition to that...

...

iframe = browser.element('#editor-iframe').get(query._frame_context)
iframe._all('strong').should(have.size(0))
iframe._element('.textarea').type('Hello, World!').perform(command.select_all)
iframe = browser.element('#editor-iframe').frame_context
iframe.all('strong').should(have.size(0))
iframe.element('.textarea').type('Hello, World!').perform(command.select_all)
browser.element('#toolbar').element('#bold').click()
iframe._all('strong').should(have.size(1))
iframe._element('.textarea').should(
iframe.all('strong').should(have.size(1))
iframe.element('.textarea').should(
have.js_property('innerHTML').value(
'<p><strong>Hello, world!</strong></p>'
)
Expand All @@ -96,23 +96,23 @@ In addition to that...

...

iframe = browser.element('#editor-iframe').get(query._frame_context)
iframe = browser.element('#editor-iframe').frame_context





iframe._all('strong').should(have.size(0))
iframe._element('.textarea').type('Hello, World!').perform(command.select_all)
iframe.all('strong').should(have.size(0))
iframe.element('.textarea').type('Hello, World!').perform(command.select_all)



browser.element('#toolbar').element('#bold').click()



iframe._all('strong').should(have.size(1))
iframe._element('.textarea').should(
iframe.all('strong').should(have.size(1))
iframe.element('.textarea').should(
have.js_property('innerHTML').value(
'<p><strong>Hello, world!</strong></p>'
)
Expand Down Expand Up @@ -159,13 +159,15 @@ The performance may decrease because Selene under the hood has to switch to the
It may decrease even more if you use such syntax for nested frames in cases more complex (have more commands to execute) than the example below:

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

browser.open('https://the-internet.herokuapp.com/nested_frames')

browser.element('[name=frame-top]').get(query._frame_context)._element(
browser.element('[name=frame-top]').frame_context.element(
'[name=frame-middle]'
).get(query._frame_context)._element('#content',).should(have.exact_text('MIDDLE'))
).frame_context.element(
'#content'
).should(have.exact_text('MIDDLE'))
```

We recommend to not do premature optimization and start with this feature, and then switch to more optimal ways described below if you face significant performance drawbacks.
Expand All @@ -184,7 +186,7 @@ We recommend to not do premature optimization and start with this feature, and t
# WHEN

# THEN
with iframe.get(query._frame_context):
with iframe.frame_context:
# AND work with elements inside frame:
browser.all('strong').should(have.size(0))
browser.element('.textarea').type('Hello, World!').perform(command.select_all)
Expand All @@ -193,7 +195,7 @@ We recommend to not do premature optimization and start with this feature, and t
# AND deal with elements outside iframe
browser.element('#toolbar').element('#bold').click()
# AND come back to ...
with iframe.get(query._frame_context):
with iframe.frame_context:
# AND ...
browser.all('strong').should(have.size(1))
browser.element('.textarea').should(
Expand All @@ -215,7 +217,7 @@ We recommend to not do premature optimization and start with this feature, and t



with iframe.get(query._frame_context):
with iframe.frame_context:

browser.all('strong').should(have.size(0))
browser.element('.textarea').type('Hello, World!').perform(command.select_all)
Expand All @@ -224,7 +226,7 @@ We recommend to not do premature optimization and start with this feature, and t

browser.element('#toolbar').element('#bold').click()

with iframe.get(query._frame_context):
with iframe.frame_context:

browser.all('strong').should(have.size(1))
browser.element('.textarea').should(
Expand Down Expand Up @@ -270,14 +272,15 @@ The performance is kept optimal because via `with` statement we can group action
Will also work for nested context:

```python
import selene.web._elements
from selene import browser, have, query, be

# GIVEN even before opened browser
browser.open('https://the-internet.herokuapp.com/nested_frames')

# WHEN
with browser.element('[name=frame-top]').get(query._frame_context):
with browser.element('[name=frame-middle]').get(query._frame_context):
with browser.element('[name=frame-top]').frame_context:
with browser.element('[name=frame-middle]').frame_context:
browser.element(
'#content',
# THEN
Expand All @@ -286,31 +289,31 @@ with browser.element('[name=frame-top]').get(query._frame_context):
browser.element('[name=frame-right]').should(be.visible)
```

### 3. It also has a handy [_within][selene.core.query._frame_context._within] decorator to tune PageObject steps to work with iframes
### 3. It also has a handy [within][selene.web._elements._FrameContext.within] decorator to tune PageObject steps to work with iframes

=== "_within decorator"
=== "within decorator"

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


class Editor:

area_frame = browser.element('#editor-iframe').get(query._frame_context)
area_frame = browser.element('#editor-iframe').frame_context
text_area = browser.element('.textarea')
toolbar = browser.element('#toolbar')

@area_frame._within
@area_frame.within
def type(self, text):
self.text_area.type(text)
return self

@area_frame._within
@area_frame.within
def should_have_bold_text_parts(self, count):
self.text_area.all('strong').should(have.size(count))
return self

@area_frame._within
@area_frame.within
def select_all(self):
self.text_area.perform(command.select_all)
return self
Expand All @@ -319,7 +322,7 @@ with browser.element('[name=frame-top]').get(query._frame_context):
self.toolbar.element('#bold').click()
return self

@area_frame._within
@area_frame.within
def should_have_content_html(self, text):
self.text_area.should(
have.js_property('innerHTML').value(
Expand Down Expand Up @@ -390,15 +393,15 @@ with browser.element('[name=frame-top]').get(query._frame_context):



with iframe.get(query._frame_context):
with iframe.frame_context:

browser.all('strong').should(have.size(0))
browser.element('.textarea').type('Hello, World!').perform(command.select_all)


browser.element('#toolbar').element('#bold').click()

with iframe.get(query._frame_context):
with iframe.frame_context:

browser.all('strong').should(have.size(1))
browser.element('.textarea').should(
Expand All @@ -412,4 +415,4 @@ with browser.element('[name=frame-top]').get(query._frame_context):

Take into account, that because we break previously groupped actions into separate methods, the performance might decrease same way as it was with a "search context" style, as we have to switch to the frame context and back for each method call.

See a more detailed explanation and examples on [the feature reference][selene.core.query._frame_context].
See a more detailed explanation and examples on [the feature reference][selene.web._elements._FrameContext].
Loading

0 comments on commit 39b38aa

Please sign in to comment.