playwright-ruby-client 1.14.beta1 → 1.15.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/accessibility.md +16 -17
  3. data/documentation/docs/api/browser.md +2 -0
  4. data/documentation/docs/api/browser_type.md +1 -0
  5. data/documentation/docs/api/element_handle.md +4 -5
  6. data/documentation/docs/api/experimental/android.md +15 -2
  7. data/documentation/docs/api/experimental/android_device.md +1 -0
  8. data/documentation/docs/api/frame.md +62 -106
  9. data/documentation/docs/api/locator.md +41 -41
  10. data/documentation/docs/api/mouse.md +3 -4
  11. data/documentation/docs/api/page.md +6 -6
  12. data/documentation/docs/api/request.md +15 -19
  13. data/documentation/docs/api/selectors.md +29 -3
  14. data/documentation/docs/api/tracing.md +13 -12
  15. data/documentation/docs/api/worker.md +12 -11
  16. data/documentation/docs/article/guides/inspector.md +1 -1
  17. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +1 -1
  18. data/documentation/docs/article/guides/semi_automation.md +1 -1
  19. data/documentation/docs/article/guides/use_storage_state.md +78 -0
  20. data/lib/playwright.rb +44 -4
  21. data/lib/playwright/channel_owners/browser_type.rb +0 -1
  22. data/lib/playwright/channel_owners/frame.rb +5 -0
  23. data/lib/playwright/channel_owners/page.rb +4 -0
  24. data/lib/playwright/channel_owners/playwright.rb +9 -0
  25. data/lib/playwright/channel_owners/request.rb +8 -8
  26. data/lib/playwright/connection.rb +3 -8
  27. data/lib/playwright/locator_impl.rb +3 -3
  28. data/lib/playwright/tracing_impl.rb +9 -8
  29. data/lib/playwright/utils.rb +0 -1
  30. data/lib/playwright/version.rb +2 -2
  31. data/lib/playwright_api/android.rb +21 -8
  32. data/lib/playwright_api/android_device.rb +8 -7
  33. data/lib/playwright_api/browser.rb +10 -8
  34. data/lib/playwright_api/browser_context.rb +6 -6
  35. data/lib/playwright_api/browser_type.rb +8 -7
  36. data/lib/playwright_api/cdp_session.rb +6 -6
  37. data/lib/playwright_api/console_message.rb +6 -6
  38. data/lib/playwright_api/dialog.rb +6 -6
  39. data/lib/playwright_api/element_handle.rb +7 -7
  40. data/lib/playwright_api/frame.rb +14 -14
  41. data/lib/playwright_api/js_handle.rb +6 -6
  42. data/lib/playwright_api/locator.rb +16 -3
  43. data/lib/playwright_api/page.rb +13 -13
  44. data/lib/playwright_api/playwright.rb +6 -6
  45. data/lib/playwright_api/request.rb +6 -6
  46. data/lib/playwright_api/response.rb +6 -6
  47. data/lib/playwright_api/route.rb +6 -6
  48. data/lib/playwright_api/selectors.rb +38 -7
  49. data/lib/playwright_api/web_socket.rb +6 -6
  50. data/lib/playwright_api/worker.rb +6 -6
  51. metadata +4 -3
@@ -7,10 +7,9 @@ sidebar_position: 10
7
7
  Locator represents a view to the element(s) on the page. It captures the logic sufficient to retrieve the element at any
8
8
  given moment. Locator can be created with the [Page#locator](./page#locator) method.
9
9
 
10
- ```python sync title=example_9f72eed0cd4b2405e6a115b812b36ff2624e889f9086925c47665333a7edabbc.py
10
+ ```ruby
11
11
  locator = page.locator("text=Submit")
12
- locator.click()
13
-
12
+ locator.click
14
13
  ```
15
14
 
16
15
  The difference between the Locator and [ElementHandle](./element_handle) is that the latter points to a particular element, while Locator
@@ -35,6 +34,19 @@ locator.hover
35
34
  locator.click
36
35
  ```
37
36
 
37
+ **Strictness**
38
+
39
+ Locators are strict. This means that all operations on locators that imply some target DOM element will throw if more
40
+ than one element matches given selector.
41
+
42
+ ```ruby
43
+ # Throws if there are several buttons in DOM:
44
+ page.locator('button').click
45
+
46
+ # Works because we explicitly tell locator to pick the first element:
47
+ page.locator('button').first.click
48
+ ```
49
+
38
50
 
39
51
 
40
52
  ## all_inner_texts
@@ -72,10 +84,12 @@ Elements from child frames return the bounding box relative to the main frame, u
72
84
  Assuming the page is static, it is safe to use bounding box coordinates to perform input. For example, the following
73
85
  snippet should click the center of the element.
74
86
 
75
- ```python sync title=example_4d635e937854fa2ee56b7c43151ded535940f0bbafc00cf48e8214bed86715eb.py
76
- box = element.bounding_box()
77
- page.mouse.click(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2)
78
-
87
+ ```ruby
88
+ box = element.bounding_box
89
+ page.mouse.click(
90
+ box["x"] + box["width"] / 2,
91
+ box["y"] + box["height"] / 2,
92
+ )
79
93
  ```
80
94
 
81
95
 
@@ -177,9 +191,8 @@ The snippet below dispatches the `click` event on the element. Regardless of the
177
191
  `click` is dispatched. This is equivalent to calling
178
192
  [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click).
179
193
 
180
- ```python sync title=example_8d92b900a98c237ffdcb102ddc35660e37101bde7d107dc64d97a7edeed62a43.py
194
+ ```ruby
181
195
  element.dispatch_event("click")
182
-
183
196
  ```
184
197
 
185
198
  Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` properties
@@ -196,11 +209,10 @@ Since `eventInit` is event-specific, please refer to the events documentation fo
196
209
 
197
210
  You can also specify [JSHandle](./js_handle) as the property value if you want live objects to be passed into the event:
198
211
 
199
- ```python sync title=example_e369442a3ff291ab476da408ef63a63dacf47984dc766ff7189d82008ae2848b.py
212
+ ```ruby
200
213
  # note you can only create data_transfer in chromium and firefox
201
214
  data_transfer = page.evaluate_handle("new DataTransfer()")
202
- element.dispatch_event("#source", "dragstart", {"dataTransfer": data_transfer})
203
-
215
+ element.dispatch_event("dragstart", eventInit: { dataTransfer: data_transfer })
204
216
  ```
205
217
 
206
218
 
@@ -236,10 +248,9 @@ If `expression` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web
236
248
 
237
249
  Examples:
238
250
 
239
- ```python sync title=example_df39b3df921f81e7cfb71cd873b76a5e91e46b4aa41e1f164128cb322aa38305.py
240
- tweets = page.locator(".tweet .retweets")
241
- assert tweets.evaluate("node => node.innerText") == "10 retweets"
242
-
251
+ ```ruby
252
+ tweet = page.query_selector(".tweet .retweets")
253
+ tweet.evaluate("node => node.innerText") # => "10 retweets"
243
254
  ```
244
255
 
245
256
 
@@ -253,15 +264,14 @@ def evaluate_all(expression, arg: nil)
253
264
  The method finds all elements matching the specified locator and passes an array of matched elements as a first argument
254
265
  to `expression`. Returns the result of `expression` invocation.
255
266
 
256
- If `expression` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), then [`Locator.evaluateAll`] would wait for the promise to resolve and return its
257
- value.
267
+ If `expression` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), then [Locator#evaluate_all](./locator#evaluate_all) would wait for the promise to resolve and
268
+ return its value.
258
269
 
259
270
  Examples:
260
271
 
261
- ```python sync title=example_32478e941514ed28b6ac221e6d54b55cf117038ecac6f4191db676480ab68d44.py
272
+ ```ruby
262
273
  elements = page.locator("div")
263
- div_counts = elements("(divs, min) => divs.length >= min", 10)
264
-
274
+ elements.evaluate_all("(divs, min) => divs.length >= min", arg: 10)
265
275
  ```
266
276
 
267
277
 
@@ -368,7 +378,7 @@ Returns the `element.innerText`.
368
378
  def input_value(timeout: nil)
369
379
  ```
370
380
 
371
- Returns `input.value` for `<input>` or `<textarea>` element. Throws for non-input elements.
381
+ Returns `input.value` for `<input>` or `<textarea>` or `<select>` element. Throws for non-input elements.
372
382
 
373
383
  ## checked?
374
384
 
@@ -518,26 +528,18 @@ Returns the array of option values that have been successfully selected.
518
528
 
519
529
  Triggers a `change` and `input` event once all the provided options have been selected.
520
530
 
521
- ```python sync title=example_2825b0a50091868d1ce3ea0752d94ba32d826d504c1ac6842522796ca405913e.py
531
+ ```ruby
522
532
  # single selection matching the value
523
- element.select_option("blue")
533
+ element.select_option(value: "blue")
524
534
  # single selection matching both the label
525
- element.select_option(label="blue")
535
+ element.select_option(label: "blue")
526
536
  # multiple selection
527
- element.select_option(value=["red", "green", "blue"])
528
-
537
+ element.select_option(value: ["red", "green", "blue"])
529
538
  ```
530
539
 
531
- ```python sync title=example_3aaff4985dc38e64fad34696c88a6a68a633e26aabee6fc749125f3ee1784e34.py
532
- # single selection matching the value
533
- element.select_option("blue")
534
- # single selection matching both the value and the label
535
- element.select_option(label="blue")
536
- # multiple selection
537
- element.select_option("red", "green", "blue")
540
+ ```ruby
538
541
  # multiple selection for blue, red and second option
539
- element.select_option(value="blue", { index: 2 }, "red")
540
-
542
+ element.select_option(value: "blue", index: 2, label: "red")
541
543
  ```
542
544
 
543
545
 
@@ -607,19 +609,17 @@ Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup`
607
609
 
608
610
  To press a special key, like `Control` or `ArrowDown`, use [Locator#press](./locator#press).
609
611
 
610
- ```python sync title=example_fa1712c0b6ceb96fcaa74790d33f2c2eefe2bd1f06e61b78e0bb84a6f22c7961.py
612
+ ```ruby
611
613
  element.type("hello") # types instantly
612
- element.type("world", delay=100) # types slower, like a user
613
-
614
+ element.type("world", delay: 100) # types slower, like a user
614
615
  ```
615
616
 
616
617
  An example of typing into a text field and then submitting the form:
617
618
 
618
- ```python sync title=example_adefe90dee78708d4375c20f081f12f2b71f2becb472a2e0d4fdc8cc49c37809.py
619
+ ```ruby
619
620
  element = page.locator("input")
620
621
  element.type("some text")
621
622
  element.press("Enter")
622
-
623
623
  ```
624
624
 
625
625
 
@@ -8,16 +8,15 @@ The Mouse class operates in main-frame CSS pixels relative to the top-left corne
8
8
 
9
9
  Every `page` object has its own Mouse, accessible with [Page#mouse](./page#mouse).
10
10
 
11
- ```python sync title=example_ba01da1f358cafb4c22b792488ff2f3de4dbd82d4ee1cc4050e3f0c24a2bd7dd.py
11
+ ```ruby
12
12
  # using ‘page.mouse’ to trace a 100x100 square.
13
13
  page.mouse.move(0, 0)
14
- page.mouse.down()
14
+ page.mouse.down
15
15
  page.mouse.move(0, 100)
16
16
  page.mouse.move(100, 100)
17
17
  page.mouse.move(100, 0)
18
18
  page.mouse.move(0, 0)
19
- page.mouse.up()
20
-
19
+ page.mouse.up
21
20
  ```
22
21
 
23
22
 
@@ -269,7 +269,9 @@ def drag_and_drop(
269
269
  target,
270
270
  force: nil,
271
271
  noWaitAfter: nil,
272
+ sourcePosition: nil,
272
273
  strict: nil,
274
+ targetPosition: nil,
273
275
  timeout: nil,
274
276
  trial: nil)
275
277
  ```
@@ -620,18 +622,18 @@ def goto(url, referer: nil, timeout: nil, waitUntil: nil)
620
622
  Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
621
623
  last redirect.
622
624
 
623
- `page.goto` will throw an error if:
625
+ The method will throw an error if:
624
626
  - there's an SSL error (e.g. in case of self-signed certificates).
625
627
  - target URL is invalid.
626
628
  - the `timeout` is exceeded during navigation.
627
629
  - the remote server does not respond or is unreachable.
628
630
  - the main resource failed to load.
629
631
 
630
- `page.goto` will not throw an error when any valid HTTP status code is returned by the remote server, including 404 "Not
632
+ The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 "Not
631
633
  Found" and 500 "Internal Server Error". The status code for such responses can be retrieved by calling
632
634
  [Response#status](./response#status).
633
635
 
634
- > NOTE: `page.goto` either throws an error or returns a main resource response. The only exceptions are navigation to
636
+ > NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
635
637
  `about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
636
638
  > NOTE: Headless mode doesn't support navigation to a PDF document. See the
637
639
  [upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
@@ -686,7 +688,7 @@ Returns `element.innerText`.
686
688
  def input_value(selector, strict: nil, timeout: nil)
687
689
  ```
688
690
 
689
- Returns `input.value` for the selected `<input>` or `<textarea>` element. Throws for non-input elements.
691
+ Returns `input.value` for the selected `<input>` or `<textarea>` or `<select>` element. Throws for non-input elements.
690
692
 
691
693
  ## checked?
692
694
 
@@ -756,8 +758,6 @@ The method returns an element locator that can be used to perform actions on the
756
758
  element immediately before performing an action, so a series of actions on the same locator can in fact be performed on
757
759
  different DOM elements. That would happen if the DOM structure between those actions has changed.
758
760
 
759
- Note that locator always implies visibility, so it will always be locating visible elements.
760
-
761
761
  Shortcut for main frame's [Frame#locator](./frame#locator).
762
762
 
763
763
  ## main_frame
@@ -28,9 +28,8 @@ The method returns `null` unless this request has failed, as reported by `reques
28
28
 
29
29
  Example of logging of all the failed requests:
30
30
 
31
- ```py title=example_5f3f4534ab17f584cfd41ca38448ce7de9490b6588e29e73116ede3cb15a25a5.py
32
- page.on("requestfailed", lambda request: print(request.url + " " + request.failure))
33
-
31
+ ```ruby
32
+ page.on("requestfailed", ->(request) { puts "#{request.url} #{request.failure}" })
34
33
  ```
35
34
 
36
35
 
@@ -108,18 +107,17 @@ construct the whole redirect chain by repeatedly calling `redirectedFrom()`.
108
107
 
109
108
  For example, if the website `http://example.com` redirects to `https://example.com`:
110
109
 
111
- ```python sync title=example_89568fc86bf623eef37b68c6659b1a8524647c8365bb32a7a8af63bd86111075.py
112
- response = page.goto("http://example.com")
113
- print(response.request.redirected_from.url) # "http://example.com"
114
-
110
+ ```ruby
111
+ response = page.goto("http://github.com")
112
+ puts response.url # => "https://github.com"
113
+ puts response.request.redirected_from&.url # => "http://github.com"
115
114
  ```
116
115
 
117
116
  If the website `https://google.com` has no redirects:
118
117
 
119
- ```python sync title=example_6d7b3fbf8d69dbe639b71fedc5a8977777fca29dfb16d38012bb07c496342472.py
118
+ ```ruby
120
119
  response = page.goto("https://google.com")
121
- print(response.request.redirected_from) # None
122
-
120
+ puts response.request.redirected_from&.url # => nil
123
121
  ```
124
122
 
125
123
 
@@ -134,9 +132,8 @@ New request issued by the browser if the server responded with redirect.
134
132
 
135
133
  This method is the opposite of [Request#redirected_from](./request#redirected_from):
136
134
 
137
- ```py title=example_922623f4033e7ec2158787e54a8554655f7e1e20a024e4bf4f69337f781ab88a.py
138
- assert request.redirected_from.redirected_to == request
139
-
135
+ ```ruby
136
+ request.redirected_from.redirected_to # equals to request
140
137
  ```
141
138
 
142
139
 
@@ -169,12 +166,11 @@ Returns resource timing information for given request. Most of the timing values
169
166
  `responseEnd` becomes available when request finishes. Find more information at
170
167
  [Resource Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming).
171
168
 
172
- ```python sync title=example_e2a297fe95fd0699b6a856c3be2f28106daa2615c0f4d6084f5012682a619d20.py
173
- with page.expect_event("requestfinished") as request_info:
174
- page.goto("http://example.com")
175
- request = request_info.value
176
- print(request.timing)
177
-
169
+ ```ruby
170
+ request = page.expect_event("requestfinished") do
171
+ page.goto("https://example.com")
172
+ end
173
+ puts request.timing
178
174
  ```
179
175
 
180
176
 
@@ -15,9 +15,35 @@ def register(name, contentScript: nil, path: nil, script: nil)
15
15
 
16
16
  An example of registering selector engine that queries elements based on a tag name:
17
17
 
18
- ```python sync title=example_49f0cb9b5a21d0d5fe2b180c847bdb21068b335b4c2f42d5c05eb1957297899f.py
19
- # FIXME: add snippet
20
-
18
+ ```ruby
19
+ tag_selector = <<~JAVASCRIPT
20
+ {
21
+ // Returns the first element matching given selector in the root's subtree.
22
+ query(root, selector) {
23
+ return root.querySelector(selector);
24
+ },
25
+ // Returns all elements matching given selector in the root's subtree.
26
+ queryAll(root, selector) {
27
+ return Array.from(root.querySelectorAll(selector));
28
+ }
29
+ }
30
+ JAVASCRIPT
31
+
32
+ # Register the engine. Selectors will be prefixed with "tag=".
33
+ playwright.selectors.register("tag", script: tag_selector)
34
+ playwright.chromium.launch do |browser|
35
+ page = browser.new_page()
36
+ page.content = '<div><button>Click me</button></div>'
37
+
38
+ # Use the selector prefixed with its name.
39
+ button = page.query_selector('tag=button')
40
+ # Combine it with other selector engines.
41
+ page.click('tag=div >> text="Click me"')
42
+
43
+ # Can use it in any methods supporting selectors.
44
+ button_count = page.eval_on_selector_all('tag=button', 'buttons => buttons.length')
45
+ button_count # => 1
46
+ end
21
47
  ```
22
48
 
23
49
 
@@ -9,13 +9,14 @@ Playwright script runs.
9
9
 
10
10
  Start with specifying the folder traces will be stored in:
11
11
 
12
- ```python sync title=example_a767dfb400d98aef50f2767b94171d23474ea1ac1cf9b4d75d412936208e652d.py
13
- browser = chromium.launch()
14
- context = browser.new_context()
15
- context.tracing.start(screenshots=True, snapshots=True)
16
- page.goto("https://playwright.dev")
17
- context.tracing.stop(path = "trace.zip")
18
-
12
+ ```ruby
13
+ browser.new_page do |page|
14
+ context = page.context
15
+
16
+ context.tracing.start(screenshots: true, snapshots: true)
17
+ page.goto('https://playwright.dev')
18
+ context.tracing.stop(path: 'trace.zip')
19
+ end
19
20
  ```
20
21
 
21
22
 
@@ -28,12 +29,12 @@ def start(name: nil, screenshots: nil, snapshots: nil)
28
29
 
29
30
  Start tracing.
30
31
 
31
- ```python sync title=example_e611abc8b1066118d0c87eae1bbbb08df655f36d50a94402fc56b8713150997b.py
32
- context.tracing.start(name="trace", screenshots=True, snapshots=True)
33
- page.goto("https://playwright.dev")
34
- context.tracing.stop()
35
- context.tracing.stop(path = "trace.zip")
32
+ ```ruby
33
+ context = page.context
36
34
 
35
+ context.tracing.start(name: 'trace', screenshots: true, snapshots: true)
36
+ page.goto('https://playwright.dev')
37
+ context.tracing.stop(path: 'trace.zip')
37
38
  ```
38
39
 
39
40
 
@@ -8,17 +8,18 @@ The Worker class represents a [WebWorker](https://developer.mozilla.org/en-US/do
8
8
  event is emitted on the page object to signal a worker creation. `close` event is emitted on the worker object when the
9
9
  worker is gone.
10
10
 
11
- ```py title=example_29716fdd4471a97923a64eebeee96330ab508226a496ae8fd13f12eb07d55ee6.py
12
- def handle_worker(worker):
13
- print("worker created: " + worker.url)
14
- worker.on("close", lambda: print("worker destroyed: " + worker.url))
15
-
16
- page.on('worker', handle_worker)
17
-
18
- print("current workers:")
19
- for worker in page.workers:
20
- print(" " + worker.url)
21
-
11
+ ```ruby
12
+ def handle_worker(worker)
13
+ puts "worker created: #{worker.url}"
14
+ worker.once("close", -> (w) { puts "worker destroyed: #{w.url}" })
15
+ end
16
+
17
+ page.on('worker', method(:handle_worker))
18
+
19
+ puts "current workers:"
20
+ page.workers.each do |worker|
21
+ puts " #{worker.url}"
22
+ end
22
23
  ```
23
24
 
24
25
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- sidebar_position: 6
2
+ sidebar_position: 7
3
3
  ---
4
4
 
5
5
  # Playwright inspector
@@ -1,5 +1,5 @@
1
1
  ---
2
- sidebar_position: 7
2
+ sidebar_position: 8
3
3
  ---
4
4
 
5
5
  # Playwright on Alpine Linux
@@ -10,7 +10,7 @@ This allow us to intermediate into automation, for example
10
10
  * Authenticate with OAuth2 manually before automation
11
11
  * Testing a page after some chrome extensions are installed manually
12
12
 
13
- Keep in mind repeatedly that persistent browser context is NOT RECOMMENDED for most cases because it would bring many side effects.
13
+ Keep in mind repeatedly that persistent browser context is NOT RECOMMENDED for most cases because it would bring many side effects. Consider [reusing cookie and local storage](./use_storage_state) when you just want to keep authenticated across browser contexts.
14
14
 
15
15
  ## Pause automation for manual operation
16
16
 
@@ -0,0 +1,78 @@
1
+ ---
2
+ sidebar_position: 6
3
+ ---
4
+
5
+ # Reuse Cookie and LocalStorage
6
+
7
+ In most cases, authentication state is stored in cookie or local storage. When we just want to keep authenticated, it is a good solution to dump/load 'storage state' (= Cookie + LocalStorage).
8
+ https://playwright.dev/docs/next/auth#reuse-authentication-state
9
+
10
+ * Dump storage state using [BrowserContext#storage_state](/docs/api/browser_context#storage_state) with `path: /path/to/state.json`
11
+ * Load storage state by specifying the parameter `storageState: /path/to/state.json` into [Browser#new_context](/docs/api/browser#new_context) or [Browser#new_page](/docs/api/browser#new_page)
12
+
13
+ ## Example
14
+
15
+ Generally in browser automation, it is very difficult to bypass 2FA or reCAPTCHA in login screen. In such cases, we would consider
16
+
17
+ * Authenticate manually by hand
18
+ * Resume automation with the authentication result
19
+
20
+
21
+ ```ruby {16,21}
22
+ require 'playwright'
23
+ require 'pry'
24
+
25
+ force_login = !File.exist?('github_state.json')
26
+
27
+ Playwright.create(playwright_cli_executable_path: 'npx playwright') do |playwright|
28
+ if force_login
29
+ # Use headful mode for manual operation.
30
+ playwright.chromium.launch(headless: false, channel: 'chrome') do |browser|
31
+ page = browser.new_page
32
+ page.goto('https://github.com/login')
33
+
34
+ # Login manually.
35
+ binding.pry
36
+
37
+ page.context.storage_state(path: 'github_state.json')
38
+ end
39
+ end
40
+
41
+ playwright.chromium.launch do |browser|
42
+ page = browser.new_page(storageState: 'github_state.json')
43
+ page.goto('https://github.com/notifications')
44
+ page.screenshot(path: 'github_notification.png')
45
+ end
46
+ end
47
+ ```
48
+
49
+ When we execute this script at the first time (without github_state.json), login screen is shown:
50
+
51
+ ![login screen is shown](https://user-images.githubusercontent.com/11763113/129394130-7a248f6a-56f0-40b0-a4dd-f0f65d71b3a9.png)
52
+
53
+ and input credentials manually:
54
+
55
+ ![input credentials manually](https://user-images.githubusercontent.com/11763113/129394155-fccc280e-5e6b-46c7-8a4d-a99d7db02c7f.png)
56
+
57
+ and hit `exit` in Pry console.
58
+
59
+ ```
60
+
61
+ 9:
62
+ 10: # Login manually. Hit `exit` in Pry console after authenticated.
63
+ 11: require 'pry'
64
+ 12: binding.pry
65
+ 13:
66
+ => 14: page.context.storage_state(path: 'github_state.json')
67
+ 15: end if force_login
68
+ 16:
69
+ 17: playwright.chromium.launch do |browser|
70
+ 18: page = browser.new_page(storageState: 'github_state.json')
71
+ 19: page.goto('https://github.com/notifications')
72
+
73
+ [1] pry(main)> exit
74
+ ```
75
+
76
+ then we can enjoy automation with keeping authenticated. Login screen is never shown until github_state.json is deleted :)
77
+
78
+ ![github_notification.png](https://user-images.githubusercontent.com/11763113/129394879-838797eb-135f-41ab-b965-8d6fabde6109.png)