-
Notifications
You must be signed in to change notification settings - Fork 104
/
browser.py
executable file
·1324 lines (1112 loc) · 59.1 KB
/
browser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2020- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import re
import shutil
import string
import sys
import time
import types
from concurrent.futures._base import Future
from datetime import timedelta
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Pattern, Set, Union
from assertionengine import AssertionOperator, Formatter
from overrides import overrides
from robot.errors import DataError
from robot.libraries.BuiltIn import EXECUTION_CONTEXTS, BuiltIn
from robot.running.arguments import PythonArgumentParser
from robot.running.arguments.typeconverters import TypeConverter
from robot.utils import secs_to_timestr, timestr_to_secs
from robotlibcore import DynamicCore, PluginParser # type: ignore
from .base import ContextCache, LibraryComponent
from .generated.playwright_pb2 import Request, Response
from .keywords import (
Control,
Cookie,
Devices,
Evaluation,
Getters,
Interaction,
Network,
PlaywrightState,
Promises,
RunOnFailureKeywords,
StrictMode,
Waiter,
WebAppState,
)
from .keywords.crawling import Crawling
from .playwright import Playwright
from .utils import (
AutoClosingLevel,
Scope,
SettingsStack,
get_normalized_keyword,
is_falsy,
keyword,
logger,
)
# Importing this directly from .utils break the stub type checks
from .utils.data_types import DelayedKeyword, HighLightElement, SupportedBrowsers
from .version import __version__ as VERSION
KW_CALL_CONTENT_TEMPLATE = """body::before {{
content: '{keyword_call}';
position: fixed;
z-index: 9999;
border: 1px solid lightblue;
border-radius: 1rem;
background: #00008b90;
color: white;
padding: 2px 10px;
pointer-events: none;
font-family: monospace;
font-size: medium;
font-weight: normal;
white-space: pre;
bottom: 5px;
left: 5px;
{additional_styles}
}}"""
KW_CALL_BANNER_FUNCTION = """(content) => {
const kwCallBanner = document.getElementById('kwCallBanner');
if (kwCallBanner) {
kwCallBanner.textContent = content;
} else {
const kwCallBanner = document.createElement("style");
kwCallBanner.setAttribute("id", 'kwCallBanner');
kwCallBanner.textContent = content;
document.head.appendChild(kwCallBanner);
}
}"""
class Browser(DynamicCore):
"""Browser library is a browser automation library for Robot Framework.
This is the keyword documentation for Browser library. For information
about installation, support, and more please visit the
[https://github.com/MarketSquare/robotframework-playwright|project pages].
For more information about Robot Framework itself, see [https://robotframework.org|robotframework.org].
Browser library uses
[https://github.com/microsoft/playwright|Playwright Node module]
to automate [https://www.chromium.org/Home|Chromium],
[https://www.mozilla.org/en-US/firefox/new/|Firefox]
and [https://webkit.org/|WebKit] with a single library.
== Table of contents ==
%TOC%
= Browser, Context and Page =
Browser library works with three different layers that build on each other:
*Browser*, *Context* and *Page*.
== Browsers ==
A *browser* can be started with one of the three
different engines Chromium, Firefox or Webkit.
=== Supported Browsers ===
| Browser | Browser with this engine |
| ``chromium`` | Google Chrome, Microsoft Edge (since 2020), Opera |
| ``firefox`` | Mozilla Firefox |
| ``webkit`` | Apple Safari, Mail, AppStore on MacOS and iOS |
Since [https://github.com/microsoft/playwright|Playwright] comes with a pack of builtin
binaries for all browsers, no additional drivers e.g. geckodriver are needed.
All these browsers that cover more than 85% of the world wide used browsers,
can be tested on Windows, Linux and MacOS.
There is no need for dedicated machines anymore.
A browser process is started ``headless`` (without a GUI) by default.
Run `New Browser` with specified arguments if a browser with a GUI is requested
or if a proxy has to be configured.
A browser process can contain several contexts.
== Contexts ==
A *context* corresponds to a set of independent incognito pages in a browser
that share cookies, sessions or profile settings. Pages in two separate
contexts do not share cookies, sessions or profile settings.
Compared to Selenium, these do *not* require their own browser process.
To get a clean environment a test can just open a new context.
Due to this new independent browser sessions can be opened with
Robot Framework Browser about 10 times faster than with Selenium by
just opening a `New Context` within the opened browser.
To make pages in the same suite share state, use the same context by opening the
context with `New Context` on suite setup.
The context layer is useful e.g. for testing different user sessions on the
same webpage without opening a whole new browser context.
Contexts can also have detailed configurations, such as geo-location, language settings,
the viewport size or color scheme.
Contexts do also support http credentials to be set, so that basic authentication
can also be tested. To be able to download files within the test,
the ``acceptDownloads`` argument must be set to ``True`` in `New Context` keyword.
A context can contain different pages.
== Pages ==
A *page* does contain the content of the loaded web site and has a browsing history.
Pages and browser tabs are the same.
Typical usage could be:
| *** Test Cases ***
| Starting a browser with a page
| New Browser chromium headless=false
| New Context viewport={'width': 1920, 'height': 1080}
| New Page https://marketsquare.github.io/robotframework-browser/Browser.html
| Get Title == Browser
The `Open Browser` keyword opens a new browser, a new context and a new page.
This keyword is useful for quick experiments or debugging sessions.
When a `New Page` is called without an open browser, `New Browser`
and `New Context` are executed with default values first.
Each Browser, Context and Page has a unique ID with which they can be addressed.
A full catalog of what is open can be received by `Get Browser Catalog` as a dictionary.
= Automatic page and context closing =
%AUTO_CLOSING_LEVEL%
= Finding elements =
All keywords in the library that need to interact with an element
on a web page take an argument typically named ``selector`` that specifies
how to find the element. Keywords can find elements with strict mode. If
strict mode is true and locator finds multiple elements from the page, keyword
will fail. If keyword finds one element, keyword does not fail because of
strict mode. If strict mode is false, keyword does not fail if selector points
many elements. Strict mode is enabled by default, but can be changed in library
`importing` or `Set Strict Mode` keyword. Keyword documentation states if keyword
uses strict mode. If keyword does not state that strict mode is used, then strict
mode is not applied for the keyword. For more details, see Playwright
[https://playwright.dev/docs/api/class-page#page-query-selector|strict documentation].
Selector strategies that are supported by default are listed in the table
below.
| = Strategy = | = Match based on = | = Example = |
| ``css`` | CSS selector. | ``css=.class > \\#login_btn`` |
| ``xpath`` | XPath expression. | ``xpath=//input[@id="login_btn"]`` |
| ``text`` | Browser text engine. | ``text=Login`` |
| ``id`` | Element ID Attribute. | ``id=login_btn`` |
CSS Selectors can also be recorded with `Record selector` keyword.
== Explicit Selector Strategy ==
The explicit selector strategy is specified with a prefix using syntax
``strategy=value``. Spaces around the separator are ignored, so
``css=foo``, ``css= foo`` and ``css = foo`` are all equivalent.
== Implicit Selector Strategy ==
*The default selector strategy is `css`.*
If selector does not contain one of the know explicit selector strategies, it is
assumed to contain css selector.
Selectors that are starting with ``//`` or ``..`` are considered as xpath selectors.
Selectors that are in quotes are considered as text selectors.
Examples:
| # CSS selectors are default.
| `Click` span > button.some_class # This is equivalent
| `Click` css=span > button.some_class # to this.
|
| # // or .. leads to xpath selector strategy
| `Click` //span/button[@class="some_class"]
| `Click` xpath=//span/button[@class="some_class"]
|
| # "text" in quotes leads to exact text selector strategy
| `Click` "Login"
| `Click` text="Login"
== CSS ==
As written before, the default selector strategy is `css`. See
[https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | css selector]
for more information.
Any malformed selector not starting with ``//`` or ``..`` nor starting and ending
with a quote is assumed to be a css selector.
Note that ``#`` is a comment character in [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#ignored-data | Robot Framework syntax] and needs to be
escaped like ``\\#`` to work as a [https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors | css ID selector].
Examples:
| `Click` span > button.some_class
| `Get Text` \\#username_field == George
== XPath ==
XPath engine is equivalent to [https://developer.mozilla.org/en/docs/Web/API/Document/evaluate|Document.evaluate].
Example: ``xpath=//html/body//span[text()="Hello World"]``.
Malformed selector starting with ``//`` or ``..`` is assumed to be an xpath selector.
For example, ``//html/body`` is converted to ``xpath=//html/body``. More
examples are displayed in `Examples`.
Note that xpath does not pierce [https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM|shadow_roots].
== Text ==
Text engine finds an element that contains a text node with the passed text.
For example, ``Click text=Login`` clicks on a login button, and
``Wait For Elements State text="lazy loaded text"`` waits for the "lazy loaded text"
to appear in the page.
Text engine finds fields based on their labels in text inserting keywords.
Malformed selector starting and ending with a quote (either ``"`` or ``'``) is assumed
to be a text selector. For example, ``Click "Login"`` is converted to ``Click text="Login"``.
Be aware that these leads to exact matches only!
More examples are displayed in `Examples`.
=== Insensitive match ===
By default, the match is case-insensitive, ignores leading/trailing whitespace and
searches for a substring. This means ``text= Login`` matches
``<button>Button loGIN (click me)</button>``.
=== Exact match ===
Text body can be escaped with single or double quotes for precise matching,
insisting on exact match, including specified whitespace and case.
This means ``text="Login "`` will only match ``<button>Login </button>`` with exactly
one space after "Login". Quoted text follows the usual escaping rules, e.g.
use ``\\"`` to escape double quote in a double-quoted string: ``text="foo\\"bar"``.
=== RegEx ===
Text body can also be a JavaScript-like regex wrapped in / symbols.
This means ``text=/^hello .*!$/i`` or ``text=/^Hello .*!$/`` will match ``<span>Hello Peter Parker!</span>``
with any name after ``Hello``, ending with ``!``.
The first one flagged with ``i`` for case-insensitive.
See [https://regex101.com/|https://regex101.com] for more information about RegEx.
=== Button and Submit Values ===
Input elements of the type button and submit are rendered with their value as text,
and text engine finds them. For example, ``text=Login`` matches
``<input type=button value="Login">``.
== Cascaded selector syntax ==
Browser library supports the same selector strategies as the underlying
Playwright node module: xpath, css, id and text. The strategy can either
be explicitly specified with a prefix or the strategy can be implicit.
A major advantage of Browser is that multiple selector engines can be used
within one selector. It is possible to mix XPath, CSS and Text selectors while
selecting a single element.
Selectors are strings that consists of one or more clauses separated by
``>>`` token, e.g. ``clause1 >> clause2 >> clause3``. When multiple clauses
are present, next one is queried relative to the previous one's result.
Browser library supports concatenation of different selectors separated by ``>>``.
For example:
| `Highlight Elements` "Hello" >> ../.. >> .select_button
| `Highlight Elements` text=Hello >> xpath=../.. >> css=.select_button
Each clause contains a selector engine name and selector body, e.g.
``engine=body``. Here ``engine`` is one of the supported engines (e.g. css or
a custom one). Selector ``body`` follows the format of the particular engine,
e.g. for css engine it should be a [https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | css selector].
Body format is assumed to ignore leading and trailing white spaces,
so that extra whitespace can be added for readability. If the selector
engine needs to include ``>>`` in the body, it should be escaped
inside a string to not be confused with clause separator,
e.g. ``text="some >> text"``.
Selector engine name can be prefixed with ``*`` to capture an element that
matches the particular clause instead of the last one. For example,
``css=article >> text=Hello`` captures the element with the text ``Hello``,
and ``*css=article >> text=Hello`` (note the *) captures the article element
that contains some element with the text Hello.
For convenience, selectors in the wrong format are heuristically converted
to the right format. See `Implicit Selector Strategy`
== Examples ==
| # queries 'div' css selector
| Get Element css=div
|
| # queries '//html/body/div' xpath selector
| Get Element //html/body/div
|
| # queries '"foo"' text selector
| Get Element text=foo
|
| # queries 'span' css selector inside the result of '//html/body/div' xpath selector
| Get Element xpath=//html/body/div >> css=span
|
| # converted to 'css=div'
| Get Element div
|
| # converted to 'xpath=//html/body/div'
| Get Element //html/body/div
|
| # converted to 'text="foo"'
| Get Element "foo"
|
| # queries the div element of every 2nd span element inside an element with the id foo
| Get Element \\#foo >> css=span:nth-child(2n+1) >> div
| Get Element id=foo >> css=span:nth-child(2n+1) >> div
Be aware that using ``#`` as a starting character in Robot Framework would be interpreted as comment.
Due to that fact a ``#id`` must be escaped as ``\\#id``.
== Frames ==
By default, selector chains do not cross frame boundaries. It means that a
simple CSS selector is not able to select an element located inside an iframe
or a frameset. For this use case, there is a special selector ``>>>`` which can
be used to combine a selector for the frame and a selector for an element
inside a frame.
Given this simple pseudo html snippet:
| <iframe id="iframe" src="src.html">
| #document
| <!DOCTYPE html>
| <html>
| <head></head>
| <body>
| <button id="btn">Click Me</button>
| </body>
| </html>
| </iframe>
Here's a keyword call that clicks the button inside the frame.
| Click id=iframe >>> id=btn
The selectors on the left and right side of ``>>>`` can be any valid selectors.
The selector clause directly before the frame opener ``>>>`` must select the frame element.
Frame selection is the only place where Browser Library modifies the selector, as explained in above.
In all cases, the library does not alter the selector in any way, instead it is passed as is to the
Playwright side.
== WebComponents and Shadow DOM ==
Playwright and so also Browser are able to do automatic piercing of Shadow DOMs
and therefore are the best automation technology when working with WebComponents.
Also other technologies claim that they can handle
[https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM|Shadow DOM and Web Components].
However, none of them do pierce shadow roots automatically,
which may be inconvenient when working with Shadow DOM and Web Components.
For that reason, the css engine pierces shadow roots. More specifically, every
[https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator|Descendant combinator]
pierces an arbitrary number of open shadow roots, including the implicit descendant combinator
at the start of the selector.
That means, it is not necessary to select each shadow host, open its shadow root and
select the next shadow host until you reach the element that should be controlled.
=== CSS:light ===
``css:light`` engine is equivalent to [https://developer.mozilla.org/en/docs/Web/API/Document/querySelector | Document.querySelector]
and behaves according to the CSS spec.
However, it does not pierce shadow roots.
``css`` engine first searches for elements in the light dom in the iteration order,
and then recursively inside open shadow roots in the iteration order. It does not
search inside closed shadow roots or iframes.
Examples:
| <article>
| <div>In the light dom</div>
| <div slot='myslot'>In the light dom, but goes into the shadow slot</div>
| <open mode shadow root>
| <div class='in-the-shadow'>
| <span class='content'>
| In the shadow dom
| <open mode shadow root>
| <li id='target'>Deep in the shadow</li>
| </open mode shadow root>
| </span>
| </div>
| <slot name='myslot'></slot>
| </open mode shadow root>
| </article>
Note that ``<open mode shadow root>`` is not an html element, but rather a shadow root
created with ``element.attachShadow({mode: 'open'})``.
- Both ``"css=article div"`` and ``"css:light=article div"`` match the first ``<div>In the light dom</div>``.
- Both ``"css=article > div"`` and ``"css:light=article > div"`` match two ``div`` elements that are direct children of the ``article``.
- ``"css=article .in-the-shadow"`` matches the ``<div class='in-the-shadow'>``, piercing the shadow root, while ``"css:light=article .in-the-shadow"`` does not match anything.
- ``"css:light=article div > span"`` does not match anything, because both light-dom ``div`` elements do not contain a ``span``.
- ``"css=article div > span"`` matches the ``<span class='content'>``, piercing the shadow root.
- ``"css=article > .in-the-shadow"`` does not match anything, because ``<div class='in-the-shadow'>`` is not a direct child of ``article``
- ``"css:light=article > .in-the-shadow"`` does not match anything.
- ``"css=article li#target"`` matches the ``<li id='target'>Deep in the shadow</li>``, piercing two shadow roots.
=== text:light ===
``text`` engine open pierces shadow roots similarly to ``css``, while ``text:light`` does not.
Text engine first searches for elements in the light dom in the iteration order, and then
recursively inside open shadow roots in the iteration order. It does not search inside
closed shadow roots or iframes.
=== id, data-testid, data-test-id, data-test and their :light counterparts ===
Attribute engines are selecting based on the corresponding attribute value.
For example: ``data-test-id=foo`` is equivalent to ``css=[data-test-id="foo"]``,
and ``id:light=foo`` is equivalent to ``css:light=[id="foo"]``.
== Element reference syntax ==
It is possible to get a reference to a Locator by using `Get Element` and `Get Elements` keywords.
Keywords do not save reference to an element in the HTML document, instead it saves reference to a Playwright
[https://playwright.dev/docs/api/class-locator|Locator]. In nutshell Locator captures the logic of how to
retrieve that element from the page. Each time an action is performed, the locator re-searches the elements
in the page. This reference can be used as a *first* part of a selector by using a special selector
syntax `element=`. like this:
| ${ref}= Get Element .some_class
| Click ${ref} >> .some_child # Locator searches an element from the page.
| Click ${ref} >> .other_child # Locator searches again an element from the page.
The `.some_child` and `.other_child` selectors in the example are relative to the element referenced
by ${ref}. Please note that frame piercing is not possible with element reference.
= Assertions =
Keywords that accept arguments ``assertion_operator`` <`AssertionOperator`> and ``assertion_expected``
can optionally assert that a specified condition holds. Keywords will return the value even when the
assertion is performed by the keyword.
Assert will retry and fail only after a specified timeout.
See `Importing` and ``retry_assertions_for`` (default is 1 second) for configuring this timeout.
%ASSERTION_TABLE%
By default, keywords will provide an error message if an assertion fails.
Default error messages can be overwritten with a ``message`` argument.
The ``message`` argument accepts `{value}`, `{value_type}`, `{expected}` and
`{expected_type}` [https://docs.python.org/3/library/stdtypes.html#str.format|format]
options.
The `{value}` is the value returned by the keyword and the `{expected}`
is the expected value defined by the user, usually the value in the
``assertion_expected`` argument. The `{value_type}` and
`{expected_type}` are the type definitions from `{value}` and `{expected}`
arguments. In similar fashion as Python
[https://docs.python.org/3/library/functions.html#type|type] returns type definition.
Assertions will retry until ``timeout`` has expired if they do not pass.
The assertion ``assertion_expected`` value is not converted by the library and
is used as is. Therefore when assertion is made, the ``assertion_expected``
argument value and value returned the keyword must have the same type. If types
are not the same, assertion will fail. Example `Get Text` always returns a string
and has to be compared with a string, even the returned value might look like
a number.
Other Keywords have other specific types they return.
`Get Element Count` always returns an integer.
`Get Bounding Box` and `Get Viewport Size` can be filtered.
They return a dictionary without a filter and a number when filtered.
These Keywords do automatic conversion for the expected value if a number is returned.
* < less or greater > With Strings*
Comparisons of strings with ``greater than`` or ``less than`` compares each character,
starting from 0 regarding where it stands in the code page.
Example: ``A < Z``, ``Z < a``, ``ac < dc`
It does never compare the length of elements. Neither lists nor strings.
The comparison stops at the first character that is different.
Examples: ``'abcde' < 'abd'``, ``'100.000' < '2'``
In Python 3 and therefore also in Browser it is not possible to compare numbers
with strings with a greater or less operator.
On keywords that return numbers, the given expected value is automatically
converted to a number before comparison.
The getters `Get Page State` and `Get Browser Catalog` return a dictionary. Values of the dictionary can directly asserted.
Pay attention of possible types because they are evaluated in Python. For example:
| Get Page State validate 2020 >= value['year'] # Comparison of numbers
| Get Page State validate "IMPORTANT MESSAGE!" == value['message'] # Comparison of strings
== The 'then' or 'evaluate' closure ==
Keywords that accept arguments ``assertion_operator`` and ``assertion_expected``
can optionally also use ``then`` or ``evaluate`` closure to modify the returned value with
BuiltIn Evaluate. Actual value can be accessed with ``value``.
For example ``Get Title then 'TITLE: '+value``.
See
[https://robotframework.org/robotframework/latest/libraries/BuiltIn.html#Evaluating%20expressions|
Builtin Evaluating expressions]
for more info on the syntax.
== Examples ==
| # *Keyword* *Selector* *Key* *Assertion Operator* *Assertion Expected*
| Get Title equal Page Title
| Get Title ^= Page
| Get Style //*[@id="div-element"] width > 100
| Get Title matches \\\\w+\\\\s\\\\w+
| Get Title validate value == "Login Page"
| Get Title evaluate value if value == "some value" else "something else"
= Implicit waiting =
Browser library and Playwright have many mechanisms to help in waiting for elements.
Playwright will auto-wait before performing actions on elements.
Please see [https://playwright.dev/docs/actionability/ | Auto-waiting on Playwright documentation]
for more information.
On top of Playwright auto-waiting Browser assertions will wait and retry
for specified time before failing any `Assertions`.
Time is specified in Browser library initialization with ``retry_assertions_for``.
Browser library also includes explicit waiting keywords such as `Wait for Elements State`
if more control for waiting is needed.
= Experimental: Re-using same node process =
Browser library integrated nodejs and python. The NodeJS side can be also executed as a standalone process.
Browser libraries running on the same machine can talk to that instead of starting new node processes.
This can speed execution when running tests parallel.
To start node side run on the directory when the Browser package is
``PLAYWRIGHT_BROWSERS_PATH=0 node Browser/wrapper/index.js PORT``.
``PORT`` is the port you want to use for the node process.
To execute tests then with pabot for example do ``ROBOT_FRAMEWORK_BROWSER_NODE_PORT=PORT pabot ..``.
= Experimental: Provide parameters to node process =
Browser library is integrated with NodeJSand and Python. Browser library starts a node process, to communicate
Playwright API in NodeJS side. It is possible to provide parameters for the started node process by defining
ROBOT_FRAMEWORK_BROWSER_NODE_DEBUG_OPTIONS environment variable, before starting the test execution. Example:
``ROBOT_FRAMEWORK_BROWSER_NODE_DEBUG_OPTIONS=--inspect;robot path/to/tests``.
There can be multiple arguments defined in the environment variable and arguments must be separated with comma.
= Scope Setting =
Some keywords which manipulates library settings have a scope argument.
With that scope argument one can set the "live time" of that setting.
Available Scopes are: `Global`, `Suite` and `Test`/`Task`
See `Scope`.
Is a scope finished, this scoped setting, like timeout, will no longer be used.
Live Times:
- A `Global` scope will live forever until it is overwritten by another `Global` scope. Or locally temporarily overridden by a more narrow scope.
- A `Suite` scope will locally override the `Global` scope and live until the end of the Suite within it is set, or if it is overwritten by a later setting with `Global` or same scope. Children suite does inherit the setting from the parent suite but also may have its own local `Suite` setting that then will be inherited to its children suites.
- A `Test` or `Task` scope will be inherited from its parent suite but when set, lives until the end of that particular test or task.
A new set higher order scope will always remove the lower order scope which may be in charge.
So the setting of a `Suite` scope from a test, will set that scope to the robot file suite where that test is and removes the `Test` scope that may have been in place.
= Extending Browser library with a JavaScript module =
Browser library can be extended with JavaScript. The module must be in CommonJS format that Node.js uses.
You can translate your ES6 module to Node.js CommonJS style with Babel. Many other languages
can be also translated to modules that can be used from Node.js. For example TypeScript, PureScript and
ClojureScript just to mention few.
| async function myGoToKeyword(url, args, page, logger, playwright) {
| logger(args.toString())
| playwright.coolNewFeature()
| return await page.goto(url);
| }
Functions can contain any number of arguments and arguments may have default values.
There are some reserved arguments that are not accessible from Robot Framework side.
They are injected to the function if they are in the arguments:
``page``: [https://playwright.dev/docs/api/class-page|the playwright Page object].
``args``: the rest of values from Robot Framework keyword call ``*args``.
``logger``: callback function that takes strings as arguments and writes them to robot log. Can be called multiple times.
``playwright``: playwright module (* from 'playwright'). Useful for integrating with Playwright features that Browser library doesn't support with it's own keywords. [https://playwright.dev/docs/api/class-playwright| API docs]
also argument name ``self`` can not be used.
== Example module.js ==
| async function myGoToKeyword(pageUrl, page) {
| await page.goto(pageUrl);
| return await page.title();
| }
| exports.__esModule = true;
| exports.myGoToKeyword = myGoToKeyword;
== Example Robot Framework side ==
| *** Settings ***
| Library Browser jsextension=${CURDIR}/module.js
|
| *** Test Cases ***
| Hello
| New Page
| ${title}= myGoToKeyword https://playwright.dev
| Should be equal ${title} Playwright
Also selector syntax can be extended with a custom selector using a js module
== Example module keyword for custom selector registering ==
| async function registerMySelector(playwright) {
| playwright.selectors.register("myselector", () => ({
| // Returns the first element matching given selector in the root's subtree.
| query(root, selector) {
| return root.querySelector(`a[data-title="${selector}"]`);
| },
|
| // Returns all elements matching given selector in the root's subtree.
| queryAll(root, selector) {
| return Array.from(root.querySelectorAll(`a[data-title="${selector}"]`));
| }
| }));
| return 1;
| }
| exports.__esModule = true;
| exports.registerMySelector = registerMySelector;
= Plugins =
Browser library offers plugins as a way to modify and add library keywords and modify some of the internal
functionality without creating a new library or hacking the source code. See plugin API
[https://github.com/MarketSquare/robotframework-browser/blob/main/docs/plugins/README.md | documentation] for
further details.
"""
ROBOT_LIBRARY_VERSION = VERSION
ROBOT_LISTENER_API_VERSION = 2
ROBOT_LIBRARY_LISTENER: "Browser"
ROBOT_LIBRARY_SCOPE = "GLOBAL"
ERROR_AUGMENTATION: ClassVar[Pattern[str]] = {
re.compile(r"Timeout .+ exceeded."): lambda msg: (
f'{msg}\nTip: Use "Set Browser Timeout" for increasing the timeout or '
"double check your locator as the targeted element(s) couldn't be found."
)
}
_context_cache = ContextCache()
_suite_cleanup_done = False
_old_init_args: ClassVar[dict] = {
"timeout": timedelta,
"enable_playwright_debug": bool,
"auto_closing_level": AutoClosingLevel,
"retry_assertions_for": timedelta,
"run_on_failure": str,
"external_browser_executable": Optional[Dict[SupportedBrowsers, str]],
"jsextension": Optional[str],
"enable_presenter_mode": Union[HighLightElement, bool],
"playwright_process_port": Optional[int],
"strict": bool,
"show_keyword_call_banner": Optional[bool],
}
def __init__(
self,
*deprecated_pos_args,
auto_closing_level: AutoClosingLevel = AutoClosingLevel.TEST,
enable_playwright_debug: bool = False,
enable_presenter_mode: Union[HighLightElement, bool] = False,
external_browser_executable: Optional[Dict[SupportedBrowsers, str]] = None,
jsextension: Optional[str] = None,
playwright_process_port: Optional[int] = None,
retry_assertions_for: timedelta = timedelta(seconds=1),
run_on_failure: str = "Take Screenshot fail-screenshot-{index}",
selector_prefix: Optional[str] = None,
show_keyword_call_banner: Optional[bool] = None,
strict: bool = True,
timeout: timedelta = timedelta(seconds=10),
plugins: Optional[str] = None,
):
"""Browser library can be taken into use with optional arguments:
| =Argument= | =Description= |
| ``*deprecated_pos_args`` | Positional arguments are deprecated for Library import. Please use named arguments instead. We will remove positional arguments after RoboCon 2023 Online in March. Old positional order was: ``timeout``, ``enable_playwright_debug``, ``auto_closing_level``, ``retry_assertions_for``, ``run_on_failure``, ``external_browser_executable``, ``jsextension``, ``enable_presenter_mode``, ``playwright_process_port``, ``strict``, ``show_keyword_call_banner``. |
| ``auto_closing_level`` | Configure context and page automatic closing. Default is ``TEST``, for more details, see `AutoClosingLevel` |
| ``enable_playwright_debug`` | Enable low level debug information from the playwright to playwright-log.txt file. Mainly Useful for the library developers and for debugging purposes. Will og everything as plain text, also including secrects. |
| ``enable_presenter_mode`` | Automatic highlights to interacted components, slowMo and a small pause at the end. Can be enabled by giving True or can be customized by giving a dictionary: `{"duration": "2 seconds", "width": "2px", "style": "dotted", "color": "blue"}` Where `duration` is time format in Robot Framework format, defaults to 2 seconds. `width` is width of the marker in pixels, defaults the `2px`. `style` is the style of border, defaults to `dotted`. `color` is the color of the marker, defaults to `blue`. |
| ``external_browser_executable`` | Dict mapping name of browser to path of executable of a browser. Will make opening new browsers of the given type use the set executablePath. Currently only configuring of `chromium` to a separate executable (chrome, chromium and Edge executables all work with recent versions) works. |
| ``jsextension`` | Path to Javascript module exposed as extra keywords. The module must be in CommonJS. |
| ``playwright_process_port`` | Experimental reusing of playwright process. ``playwright_process_port`` is preferred over environment variable ``ROBOT_FRAMEWORK_BROWSER_NODE_PORT``. See `Experimental: Re-using same node process` for more details. |
| ``retry_assertions_for`` | Timeout for retrying assertions on keywords before failing the keywords. This timeout starts counting from the first failure. Global ``timeout`` will still be in effect. This allows stopping execution faster to assertion failure when element is found fast. |
| ``run_on_failure`` | Sets the keyword to execute in case of a failing Browser keyword. It can be the name of any keyword. If the keyword has arguments those must be separated with two spaces for example ``My keyword \\ arg1 \\ arg2``. If no extra action should be done after a failure, set it to ``None`` or any other robot falsy value. Run on failure is not applied when library methods are executed directly from Python. |
| ``selector_prefix`` | Prefix for all selectors. This is useful when you need to use add an iframe selector before each selector. |
| ``show_keyword_call_banner`` | If set to ``True``, will show a banner with the keyword name and arguments before the keyword is executed at the bottom of the page. If set to ``False``, will not show the banner. If set to None, which is the default, will show the banner only if the presenter mode is enabled. `Get Page Source` and `Take Screenshot` will not show the banner, because that could negatively affect your test cases/tasks. This feature may be super helpful when you are debugging your tests and using tracing from `New Context` or `Video recording` features. |
| ``strict`` | If keyword selector points multiple elements and keywords should interact with one element, keyword will fail if ``strict`` mode is true. Strict mode can be changed individually in keywords or by ```et Strict Mode`` keyword. |
| ``timeout`` | Timeout for keywords that operate on elements. The keywords will wait for this time for the element to appear into the page. Defaults to "10s" => 10 seconds. |
| ``plugins`` | Allows extending the Browser library with external Python classes. |
Old deprecated argument order:
``timeout``, ``enable_playwright_debug``, ``auto_closing_level``, ``retry_assertions_for``, ``run_on_failure``,
``external_browser_executable``, ``jsextension``, ``enable_presenter_mode``, ``playwright_process_port``,
``strict``, ``show_keyword_call_banner``
"""
self.ROBOT_LIBRARY_LISTENER = self
self.scope_stack: Dict = {}
old_args_list = list(self._old_init_args.items())
pos_params = {}
for index, pos_arg in enumerate(deprecated_pos_args):
argument_name = old_args_list[index][0]
argument_type = old_args_list[index][1]
converted_pos = TypeConverter.converter_for(argument_type).convert(
argument_name, pos_arg
)
pos_params[argument_name] = converted_pos
if pos_params:
logger.warn(
"Deprecated positional arguments are used in 'Library import of Browser library'. Please use named arguments instead."
)
params = dict(locals())
params = {**pos_params, **params}
self._playwright_state = PlaywrightState(self)
self._browser_control = Control(self)
libraries = [
self._playwright_state,
self._browser_control,
Cookie(self),
Crawling(self),
Devices(self),
Evaluation(self),
Formatter(self),
Interaction(self),
Getters(self),
Network(self),
RunOnFailureKeywords(self),
StrictMode(self),
Promises(self),
Waiter(self),
WebAppState(self),
]
self._playwright_log = Path(self.outputdir, "playwright-log.txt")
self.playwright = Playwright(
self,
params["enable_playwright_debug"],
playwright_process_port,
self._playwright_log,
)
self._auto_closing_level: AutoClosingLevel = params["auto_closing_level"]
# Parsing needs keywords to be discovered.
self.external_browser_executable: Dict[SupportedBrowsers, str] = (
params["external_browser_executable"] or {}
)
if params["jsextension"] is not None:
libraries.append(
self._create_lib_component_from_jsextension(params["jsextension"])
)
if params["plugins"] is not None:
parser = PluginParser(LibraryComponent, [self])
parsed_plugins = parser.parse_plugins(params["plugins"])
libraries.extend(parsed_plugins)
self._plugin_keywords = parser.get_plugin_keywords(parsed_plugins)
else:
self._plugin_keywords = []
self.presenter_mode: Union[HighLightElement, bool] = params[
"enable_presenter_mode"
]
self._execution_stack: List[dict] = []
self._running_on_failure_keyword = False
self.pause_on_failure: Set[str] = set()
self._unresolved_promises: Set[Future] = set()
self._keyword_formatters: dict = {}
self._current_loglevel: Optional[str] = None
self.is_test_case_running = False
DynamicCore.__init__(self, libraries)
self.scope_stack["timeout"] = SettingsStack(
self.convert_timeout(params["timeout"]),
self,
self._browser_control.set_playwright_timeout,
)
self.scope_stack["retry_assertions_for"] = SettingsStack(
self.convert_timeout(params["retry_assertions_for"]), self
)
self.scope_stack["strict_mode"] = SettingsStack(params["strict"], self)
self.scope_stack["selector_prefix"] = SettingsStack(selector_prefix, self)
self.scope_stack["run_on_failure"] = SettingsStack(
self._parse_run_on_failure_keyword(params["run_on_failure"]), self
)
self.scope_stack["show_keyword_call_banner"] = SettingsStack(
params["show_keyword_call_banner"], self
)
self.scope_stack["keyword_call_banner_add_style"] = SettingsStack("", self)
@property
def keyword_call_banner_add_style(self):
return self.scope_stack["keyword_call_banner_add_style"].get()
@property
def show_keyword_call_banner(self):
return self.scope_stack["show_keyword_call_banner"].get()
@property
def run_on_failure_keyword(self) -> DelayedKeyword:
return self.scope_stack["run_on_failure"].get()
@property
def timeout(self):
return self.scope_stack["timeout"].get()
def _parse_run_on_failure_keyword(
self, keyword_name: Union[str, None]
) -> DelayedKeyword:
if keyword_name is None or is_falsy(keyword_name):
return DelayedKeyword(None, None, (), {})
parts = keyword_name.split(" ")
keyword_name = parts[0]
normalized_keyword_name = get_normalized_keyword(keyword_name)
args = parts[1:]
if normalized_keyword_name not in self.keywords:
return DelayedKeyword(keyword_name, keyword_name, tuple(args), {})
spec = PythonArgumentParser().parse(self.keywords[normalized_keyword_name])
varargs = []
kwargs = {}
for arg in spec.resolve(args):
for item in arg:
if isinstance(item, tuple):
kwargs[item[0]] = item[1]
else:
varargs.append(item)
return DelayedKeyword(
normalized_keyword_name, keyword_name, tuple(varargs), kwargs
)
def _create_lib_component_from_jsextension(
self, jsextension: str
) -> LibraryComponent:
component = LibraryComponent(self)
response = self.init_js_extension(Path(jsextension))
for name, args, doc in zip(
response.keywords,
response.keywordArguments,
response.keywordDocumentations,
):
self._jskeyword_call(component, name, args, doc)
return component
def init_js_extension(
self, js_extension_path: Union[Path, str]
) -> Response.Keywords:
with self.playwright.grpc_channel() as stub:
return stub.InitializeExtension(
Request().FilePath(
path=str(Path(js_extension_path).resolve().absolute())
)
)
def _js_value_to_python_value(self, value: str) -> str:
return {
"true": "True",
"false": "False",
"null": "None",
"undefined": "None",
"NaN": "float('nan')",
"Infinity": "float('inf')",
"-Infinity": "float('-inf')",
}.get(value, value)
def _jskeyword_call(
self,
component: LibraryComponent,
name: str,
argument_names_and_default_values: str,
doc: str,
):
argument_names_and_vals = [
[a.strip() for a in arg.split("=")]
for arg in (argument_names_and_default_values or "").split(",")
if arg
]
argument_names_and_default_values_texts = []
arg_set_texts = []
for item in argument_names_and_vals:
arg_name = item[0]
if arg_name in ["logger", "playwright", "page"]:
arg_set_texts.append(f'("{arg_name}", "RESERVED")')
else:
arg_set_texts.append(f'("{arg_name}", {arg_name})')
if arg_name == "args":
argument_names_and_default_values_texts.append("*args")
elif len(item) > 1:
argument_names_and_default_values_texts.append(
f"{arg_name}={self._js_value_to_python_value(item[1])}"
)
else:
argument_names_and_default_values_texts.append(f"{arg_name}")
text = f"""
@keyword
def {name}(self, {", ".join(argument_names_and_default_values_texts)}):
\"\"\"{doc}\"\"\"
_args_browser_internal = dict()
_args_browser_internal["arguments"] = [{", ".join(arg_set_texts)}]
with self.playwright.grpc_channel() as stub:
responses = stub.CallExtensionKeyword(
Request().KeywordCall(name="{name}", arguments=json.dumps(_args_browser_internal))
)
for response in responses:
logger.info(response.log)
if response.json == "":
return
return json.loads(response.json)
"""
try:
exec(
text,