playwright-ruby-client 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CODE_OF_CONDUCT.md +74 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +49 -0
  7. data/Rakefile +3 -0
  8. data/bin/console +11 -0
  9. data/bin/setup +8 -0
  10. data/lib/playwright.rb +39 -0
  11. data/lib/playwright/channel.rb +28 -0
  12. data/lib/playwright/channel_owner.rb +80 -0
  13. data/lib/playwright/channel_owners/android.rb +3 -0
  14. data/lib/playwright/channel_owners/binding_call.rb +4 -0
  15. data/lib/playwright/channel_owners/browser.rb +80 -0
  16. data/lib/playwright/channel_owners/browser_context.rb +13 -0
  17. data/lib/playwright/channel_owners/browser_type.rb +26 -0
  18. data/lib/playwright/channel_owners/chromium_browser.rb +8 -0
  19. data/lib/playwright/channel_owners/chromium_browser_context.rb +8 -0
  20. data/lib/playwright/channel_owners/electron.rb +3 -0
  21. data/lib/playwright/channel_owners/firefox_browser.rb +8 -0
  22. data/lib/playwright/channel_owners/frame.rb +44 -0
  23. data/lib/playwright/channel_owners/page.rb +53 -0
  24. data/lib/playwright/channel_owners/playwright.rb +57 -0
  25. data/lib/playwright/channel_owners/request.rb +5 -0
  26. data/lib/playwright/channel_owners/response.rb +5 -0
  27. data/lib/playwright/channel_owners/selectors.rb +4 -0
  28. data/lib/playwright/channel_owners/webkit_browser.rb +8 -0
  29. data/lib/playwright/connection.rb +238 -0
  30. data/lib/playwright/errors.rb +35 -0
  31. data/lib/playwright/event_emitter.rb +62 -0
  32. data/lib/playwright/events.rb +86 -0
  33. data/lib/playwright/playwright_api.rb +75 -0
  34. data/lib/playwright/transport.rb +86 -0
  35. data/lib/playwright/version.rb +5 -0
  36. data/lib/playwright_api/accessibility.rb +39 -0
  37. data/lib/playwright_api/binding_call.rb +5 -0
  38. data/lib/playwright_api/browser.rb +123 -0
  39. data/lib/playwright_api/browser_context.rb +285 -0
  40. data/lib/playwright_api/browser_type.rb +144 -0
  41. data/lib/playwright_api/cdp_session.rb +34 -0
  42. data/lib/playwright_api/chromium_browser_context.rb +26 -0
  43. data/lib/playwright_api/console_message.rb +22 -0
  44. data/lib/playwright_api/dialog.rb +46 -0
  45. data/lib/playwright_api/download.rb +54 -0
  46. data/lib/playwright_api/element_handle.rb +361 -0
  47. data/lib/playwright_api/file_chooser.rb +31 -0
  48. data/lib/playwright_api/frame.rb +526 -0
  49. data/lib/playwright_api/js_handle.rb +69 -0
  50. data/lib/playwright_api/keyboard.rb +101 -0
  51. data/lib/playwright_api/mouse.rb +47 -0
  52. data/lib/playwright_api/page.rb +986 -0
  53. data/lib/playwright_api/playwright.rb +35 -0
  54. data/lib/playwright_api/request.rb +119 -0
  55. data/lib/playwright_api/response.rb +61 -0
  56. data/lib/playwright_api/route.rb +53 -0
  57. data/lib/playwright_api/selectors.rb +51 -0
  58. data/lib/playwright_api/touchscreen.rb +10 -0
  59. data/lib/playwright_api/video.rb +14 -0
  60. data/lib/playwright_api/web_socket.rb +21 -0
  61. data/lib/playwright_api/worker.rb +34 -0
  62. data/playwright.gemspec +35 -0
  63. metadata +216 -0
@@ -0,0 +1,35 @@
1
+ module Playwright
2
+ class Error < StandardError
3
+ # ref: https://github.com/microsoft/playwright-python/blob/0b4a980fed366c4c1dee9bfcdd72662d629fdc8d/playwright/_impl/_helper.py#L155
4
+ def self.parse(error_payload)
5
+ if error_payload['name'] == 'TimeoutError'
6
+ Playwright::TimeoutError.new(
7
+ message: error_payload['message'],
8
+ stack: error_payload['stack'].split("\n"),
9
+ )
10
+ else
11
+ new(
12
+ name: error_payload['name'],
13
+ message: error_payload['message'],
14
+ stack: error_payload['stack'].split("\n"),
15
+ )
16
+ end
17
+ end
18
+
19
+ # @param name [String]
20
+ # @param message [String]
21
+ # @param stack [Array<String>]
22
+ def initialize(name:, message:, stack:)
23
+ super("#{name}: #{message}")
24
+ @name = name
25
+ @message = message
26
+ @stack = stack
27
+ end
28
+ end
29
+
30
+ class TimeoutError < Error
31
+ def initialize(message:, stack:)
32
+ super(name: 'TimeoutError', message: message, stack: stack)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ module Playwright
2
+ class EventEmitterCallback
3
+ def initialize(callback_proc)
4
+ @proc = callback_proc
5
+ end
6
+
7
+ def call(*args)
8
+ @proc.call(*args)
9
+ true
10
+ end
11
+ end
12
+
13
+ class EventEmitterOnceCallback < EventEmitterCallback
14
+ def call(*args)
15
+ @__result ||= super
16
+ true
17
+ end
18
+ end
19
+
20
+
21
+ # A subset of Events/EventEmitter in Node.js
22
+ module EventEmitter
23
+ # @param event [String]
24
+ def emit(event, *args)
25
+ (@__event_emitter ||= {})[event.to_s]&.each do |callback|
26
+ callback.call(*args)
27
+ end
28
+ self
29
+ end
30
+
31
+ # @param event [String]
32
+ # @param callback [Proc]
33
+ def on(event, callback)
34
+ raise ArgumentError.new('callback must not be nil') if callback.nil?
35
+ cb = (@__event_emitter_callback ||= {})["#{event}/#{callback.object_id}"] ||= EventEmitterCallback.new(callback)
36
+ ((@__event_emitter ||= {})[event.to_s] ||= Set.new) << cb
37
+ self
38
+ end
39
+
40
+ # @param event [String]
41
+ # @param callback [Proc]
42
+ def once(event, callback)
43
+ raise ArgumentError.new('callback must not be nil') if callback.nil?
44
+
45
+ cb = (@__event_emitter_callback ||= {})["#{event}/once/#{callback.object_id}"] ||= EventEmitterOnceCallback.new(callback)
46
+ ((@__event_emitter ||= {})[event.to_s] ||= Set.new) << cb
47
+ self
48
+ end
49
+
50
+ # @param event [String]
51
+ # @param callback [Proc]
52
+ def off(event, callback)
53
+ raise ArgumentError.new('callback must not be nil') if callback.nil?
54
+
55
+ cb = (@__event_emitter_callback ||= {})["#{event}/#{callback.object_id}"]
56
+ if cb
57
+ (@__event_emitter ||= {})[event.to_s]&.delete(cb)
58
+ end
59
+ self
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,86 @@
1
+ module Playwright
2
+ module Events
3
+ end
4
+ end
5
+
6
+ # @see https://github.com/microsoft/playwright/blob/master/src/client/events.ts
7
+ {
8
+ AndroidDevice: {
9
+ WebView: 'webview',
10
+ Close: 'close'
11
+ },
12
+
13
+ AndroidSocket: {
14
+ Data: 'data',
15
+ Close: 'close'
16
+ },
17
+
18
+ AndroidWebView: {
19
+ Close: 'close'
20
+ },
21
+
22
+ Browser: {
23
+ Disconnected: 'disconnected'
24
+ },
25
+
26
+ BrowserContext: {
27
+ Close: 'close',
28
+ Page: 'page',
29
+ },
30
+
31
+ BrowserServer: {
32
+ Close: 'close',
33
+ },
34
+
35
+ Page: {
36
+ Close: 'close',
37
+ Crash: 'crash',
38
+ Console: 'console',
39
+ Dialog: 'dialog',
40
+ Download: 'download',
41
+ FileChooser: 'filechooser',
42
+ DOMContentLoaded: 'domcontentloaded',
43
+ # Can't use just 'error' due to node.js special treatment of error events.
44
+ # @see https://nodejs.org/api/events.html#events_error_events
45
+ PageError: 'pageerror',
46
+ Request: 'request',
47
+ Response: 'response',
48
+ RequestFailed: 'requestfailed',
49
+ RequestFinished: 'requestfinished',
50
+ FrameAttached: 'frameattached',
51
+ FrameDetached: 'framedetached',
52
+ FrameNavigated: 'framenavigated',
53
+ Load: 'load',
54
+ Popup: 'popup',
55
+ WebSocket: 'websocket',
56
+ Worker: 'worker',
57
+ },
58
+
59
+ WebSocket: {
60
+ Close: 'close',
61
+ Error: 'socketerror',
62
+ FrameReceived: 'framereceived',
63
+ FrameSent: 'framesent',
64
+ },
65
+
66
+ Worker: {
67
+ Close: 'close',
68
+ },
69
+
70
+ ChromiumBrowserContext: {
71
+ BackgroundPage: 'backgroundpage',
72
+ ServiceWorker: 'serviceworker',
73
+ },
74
+
75
+ ElectronApplication: {
76
+ Close: 'close',
77
+ Window: 'window',
78
+ },
79
+ }.each do |key, events|
80
+ events_module = Module.new
81
+ events.each do |event_key, event_value|
82
+ events_module.const_set(event_key, event_value)
83
+ end
84
+ events_module.define_singleton_method(:keys) { events.keys }
85
+ ::Playwright::Events.const_set(key, events_module)
86
+ end
@@ -0,0 +1,75 @@
1
+ module Playwright
2
+ class PlaywrightApi
3
+ # Wrap ChannelOwner.
4
+ # Playwright::ChannelOwners::XXXXX will be wrapped as Playwright::XXXXX
5
+ # Playwright::XXXXX is automatically generated by development/generate_api
6
+ #
7
+ # @param channel_owner [ChannelOwner]
8
+ # @note Intended for internal use only.
9
+ def self.from_channel_owner(channel_owner)
10
+ Factory.new(channel_owner).create
11
+ end
12
+
13
+ private
14
+
15
+ class Factory
16
+ def initialize(channel_owner)
17
+ channel_owner_class_name = channel_owner.class.name
18
+ raise "#{channel_owner_class_name} is not a channel owner" unless channel_owner_class_name.include?('::ChannelOwners::')
19
+
20
+ @channel_owner = channel_owner
21
+ end
22
+
23
+ def create
24
+ api_class = detect_class_for(@channel_owner.class)
25
+ if api_class
26
+ api_class.new(@channel_owner)
27
+ else
28
+ raise NotImplementedError.new("Playwright::#{expected_class_name_for(@channel_owner.class)} is not implemented")
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def expected_class_name_for(klass)
35
+ klass.name.split('::ChannelOwners::').last
36
+ end
37
+
38
+ def detect_class_for(klass)
39
+ class_name = expected_class_name_for(klass)
40
+ if ::Playwright.const_defined?(class_name)
41
+ ::Playwright.const_get(class_name)
42
+ else
43
+ if [::Playwright::ChannelOwner, Object].include?(klass.superclass)
44
+ nil
45
+ else
46
+ detect_class_for(klass.superclass)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # @param channel_owner [Playwright::ChannelOwner]
53
+ def initialize(channel_owner)
54
+ @channel_owner = channel_owner
55
+ end
56
+
57
+ # @param block [Proc]
58
+ def wrap_block_call(block)
59
+ -> (*args) {
60
+ wrapped_args = args.map { |arg| wrap_channel_owner(arg) }
61
+ block.call(*wrapped_args)
62
+ }
63
+ end
64
+
65
+ def wrap_channel_owner(object)
66
+ if object.is_a?(ChannelOwner)
67
+ PlaywrightApi.from_channel_owner(object)
68
+ elsif object.is_a?(Array)
69
+ object.map { |obj| wrap_channel_owner(obj) }
70
+ else
71
+ object
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require "stringio"
6
+
7
+ module Playwright
8
+ # ref: https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_transport.py
9
+ class Transport
10
+ # @param playwright_cli_executable_path [String] path to playwright-cli.
11
+ # @param debug [Boolean]
12
+ def initialize(playwright_cli_executable_path:)
13
+ @driver_executable_path = playwright_cli_executable_path
14
+ @debug = ENV['DEBUG'].to_s == 'true' || ENV['DEBUG'].to_s == '1'
15
+ end
16
+
17
+ def on_message_received(&block)
18
+ @on_message = block
19
+ end
20
+
21
+ class AlreadyDisconnectedError < StandardError ; end
22
+
23
+ # @param message [Hash]
24
+ def send_message(message)
25
+ debug_send_message(message) if @debug
26
+ msg = JSON.dump(message)
27
+ @stdin.write([msg.size].pack('V')) # unsigned 32bit, little endian
28
+ @stdin.write(msg)
29
+ rescue Errno::EPIPE
30
+ raise AlreadyDisconnectedError.new('send_message failed')
31
+ end
32
+
33
+ # Terminate playwright-cli driver.
34
+ def stop
35
+ [@stdin, @stdout, @stderr].each { |io| io.close unless io.closed? }
36
+ end
37
+
38
+ # Start `playwright-cli run-driver`
39
+ #
40
+ # @note This method blocks until playwright-cli exited. Consider using Thread or Future.
41
+ def run
42
+ @stdin, @stdout, @stderr, @thread = Open3.popen3(@driver_executable_path, 'run-driver')
43
+
44
+ Thread.new { handle_stdout }
45
+ Thread.new { handle_stderr }
46
+
47
+ @thread.join
48
+ end
49
+
50
+ private
51
+
52
+ def handle_stdout(packet_size: 32_768)
53
+ while chunk = @stdout.read(4)
54
+ length = chunk.unpack1('V') # unsigned 32bit, little endian
55
+ buffer = StringIO.new
56
+ (length / packet_size).to_i.times do
57
+ buffer << @stdout.read(packet_size)
58
+ end
59
+ buffer << @stdout.read(length % packet_size)
60
+ buffer.rewind
61
+ obj = JSON.parse(buffer.read)
62
+
63
+ debug_recv_message(obj) if @debug
64
+ @on_message&.call(obj)
65
+ end
66
+ rescue IOError
67
+ # disconnected by remote.
68
+ end
69
+
70
+ def handle_stderr
71
+ while err = @stderr.read
72
+ $stderr.write(err)
73
+ end
74
+ rescue IOError
75
+ # disconnected by remote.
76
+ end
77
+
78
+ def debug_send_message(message)
79
+ puts "\x1b[33mSEND>\x1b[0m#{message}"
80
+ end
81
+
82
+ def debug_recv_message(message)
83
+ puts "\x1b[33mRECV>\x1b[0m#{message}"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Playwright
4
+ VERSION = '0.0.3'
5
+ end
@@ -0,0 +1,39 @@
1
+ module Playwright
2
+ # The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by assistive technology such as screen readers or switches.
3
+ # Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might have wildly different output.
4
+ # Blink - Chromium's rendering engine - has a concept of "accessibility tree", which is then translated into different platform-specific APIs. Accessibility namespace gives users access to the Blink Accessibility Tree.
5
+ # Most of the accessibility tree gets filtered out when converting from Blink AX Tree to Platform-specific AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing only the "interesting" nodes of the tree.
6
+ class Accessibility < PlaywrightApi
7
+
8
+ # Captures the current state of the accessibility tree. The returned object represents the root accessible node of the page.
9
+ #
10
+ # **NOTE** The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers. Playwright will discard them as well for an easier to process tree, unless `interestingOnly` is set to `false`.
11
+ #
12
+ # An example of dumping the entire accessibility tree:
13
+ #
14
+ # ```js
15
+ # const snapshot = await page.accessibility.snapshot();
16
+ # console.log(snapshot);
17
+ # ```
18
+ # An example of logging the focused node's name:
19
+ #
20
+ # ```js
21
+ # const snapshot = await page.accessibility.snapshot();
22
+ # const node = findFocusedNode(snapshot);
23
+ # console.log(node && node.name);
24
+ #
25
+ # function findFocusedNode(node) {
26
+ # if (node.focused)
27
+ # return node;
28
+ # for (const child of node.children || []) {
29
+ # const foundNode = findFocusedNode(child);
30
+ # return foundNode;
31
+ # }
32
+ # return null;
33
+ # }
34
+ # ```
35
+ def snapshot(interestingOnly: nil, root: nil)
36
+ raise NotImplementedError.new('snapshot is not implemented yet.')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ module Playwright
2
+ # @nodoc
3
+ class BindingCall < PlaywrightApi
4
+ end
5
+ end
@@ -0,0 +1,123 @@
1
+ module Playwright
2
+ # A Browser is created when Playwright connects to a browser instance, either through `browserType.launch([options])` or `browserType.connect(params)`.
3
+ # An example of using a Browser to create a Page:
4
+ #
5
+ # ```js
6
+ # const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
7
+ #
8
+ # (async () => {
9
+ # const browser = await firefox.launch();
10
+ # const page = await browser.newPage();
11
+ # await page.goto('https://example.com');
12
+ # await browser.close();
13
+ # })();
14
+ # ```
15
+ # See ChromiumBrowser, FirefoxBrowser and WebKitBrowser for browser-specific features. Note that `browserType.connect(params)` and `browserType.launch([options])` always return a specific browser instance, based on the browser being connected to or launched.
16
+ class Browser < PlaywrightApi
17
+
18
+ # In case this browser is obtained using `browserType.launch([options])`, closes the browser and all of its pages (if any were opened).
19
+ # In case this browser is obtained using `browserType.connect(params)`, clears all created contexts belonging to this browser and disconnects from the browser server.
20
+ # The Browser object itself is considered to be disposed and cannot be used anymore.
21
+ def close
22
+ wrap_channel_owner(@channel_owner.close)
23
+ end
24
+
25
+ # Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts.
26
+ #
27
+ # ```js
28
+ # const browser = await pw.webkit.launch();
29
+ # console.log(browser.contexts().length); // prints `0`
30
+ #
31
+ # const context = await browser.newContext();
32
+ # console.log(browser.contexts().length); // prints `1`
33
+ # ```
34
+ def contexts
35
+ wrap_channel_owner(@channel_owner.contexts)
36
+ end
37
+
38
+ # Indicates that the browser is connected.
39
+ def connected?
40
+ wrap_channel_owner(@channel_owner.connected?)
41
+ end
42
+
43
+ # Creates a new browser context. It won't share cookies/cache with other browser contexts.
44
+ #
45
+ # ```js
46
+ # (async () => {
47
+ # const browser = await playwright.firefox.launch(); // Or 'chromium' or 'webkit'.
48
+ # // Create a new incognito browser context.
49
+ # const context = await browser.newContext();
50
+ # // Create a new page in a pristine context.
51
+ # const page = await context.newPage();
52
+ # await page.goto('https://example.com');
53
+ # })();
54
+ # ```
55
+ def new_context(
56
+ acceptDownloads: nil,
57
+ ignoreHTTPSErrors: nil,
58
+ bypassCSP: nil,
59
+ viewport: nil,
60
+ userAgent: nil,
61
+ deviceScaleFactor: nil,
62
+ isMobile: nil,
63
+ hasTouch: nil,
64
+ javaScriptEnabled: nil,
65
+ timezoneId: nil,
66
+ geolocation: nil,
67
+ locale: nil,
68
+ permissions: nil,
69
+ extraHTTPHeaders: nil,
70
+ offline: nil,
71
+ httpCredentials: nil,
72
+ colorScheme: nil,
73
+ logger: nil,
74
+ videosPath: nil,
75
+ videoSize: nil,
76
+ recordHar: nil,
77
+ recordVideo: nil,
78
+ proxy: nil,
79
+ storageState: nil)
80
+ wrap_channel_owner(@channel_owner.new_context(acceptDownloads: acceptDownloads, ignoreHTTPSErrors: ignoreHTTPSErrors, bypassCSP: bypassCSP, viewport: viewport, userAgent: userAgent, deviceScaleFactor: deviceScaleFactor, isMobile: isMobile, hasTouch: hasTouch, javaScriptEnabled: javaScriptEnabled, timezoneId: timezoneId, geolocation: geolocation, locale: locale, permissions: permissions, extraHTTPHeaders: extraHTTPHeaders, offline: offline, httpCredentials: httpCredentials, colorScheme: colorScheme, logger: logger, videosPath: videosPath, videoSize: videoSize, recordHar: recordHar, recordVideo: recordVideo, proxy: proxy, storageState: storageState))
81
+ end
82
+
83
+ # Creates a new page in a new browser context. Closing this page will close the context as well.
84
+ # This is a convenience API that should only be used for the single-page scenarios and short snippets. Production code and testing frameworks should explicitly create `browser.newContext([options])` followed by the `browserContext.newPage()` to control their exact life times.
85
+ def new_page(
86
+ acceptDownloads: nil,
87
+ ignoreHTTPSErrors: nil,
88
+ bypassCSP: nil,
89
+ viewport: nil,
90
+ userAgent: nil,
91
+ deviceScaleFactor: nil,
92
+ isMobile: nil,
93
+ hasTouch: nil,
94
+ javaScriptEnabled: nil,
95
+ timezoneId: nil,
96
+ geolocation: nil,
97
+ locale: nil,
98
+ permissions: nil,
99
+ extraHTTPHeaders: nil,
100
+ offline: nil,
101
+ httpCredentials: nil,
102
+ colorScheme: nil,
103
+ logger: nil,
104
+ videosPath: nil,
105
+ videoSize: nil,
106
+ recordHar: nil,
107
+ recordVideo: nil,
108
+ proxy: nil,
109
+ storageState: nil)
110
+ wrap_channel_owner(@channel_owner.new_page(acceptDownloads: acceptDownloads, ignoreHTTPSErrors: ignoreHTTPSErrors, bypassCSP: bypassCSP, viewport: viewport, userAgent: userAgent, deviceScaleFactor: deviceScaleFactor, isMobile: isMobile, hasTouch: hasTouch, javaScriptEnabled: javaScriptEnabled, timezoneId: timezoneId, geolocation: geolocation, locale: locale, permissions: permissions, extraHTTPHeaders: extraHTTPHeaders, offline: offline, httpCredentials: httpCredentials, colorScheme: colorScheme, logger: logger, videosPath: videosPath, videoSize: videoSize, recordHar: recordHar, recordVideo: recordVideo, proxy: proxy, storageState: storageState))
111
+ end
112
+
113
+ # Returns the browser version.
114
+ def version
115
+ wrap_channel_owner(@channel_owner.version)
116
+ end
117
+
118
+ # @nodoc
119
+ def after_initialize
120
+ wrap_channel_owner(@channel_owner.after_initialize)
121
+ end
122
+ end
123
+ end