playwright-ruby-client 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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