venetian 0.2.3-x86_64-linux → 0.3.1-x86_64-linux

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abef3d62208edc71db8dec14abfd92da60bfb0791e50d16567de1212f9a97335
4
- data.tar.gz: d64d6de657f07cda5d8e91728f9c330cb09a5725d4faed2f63408253495c2d07
3
+ metadata.gz: 04cf1a9d2cb860a3fee950970f7cb0529dbb1b946fd14e14eb0cbd7f12fcb70c
4
+ data.tar.gz: 177a863861a18743443274c31168e951874abf6fffe779ba3b862e8dd581e29f
5
5
  SHA512:
6
- metadata.gz: ecd5a90a62f78c438ef06f533f4046cfc5c060172e420680695b8adbb38c0a933f204992666fd1b2ec36382bf074814d367a5a1bf0f7980b8f3edd6397fee72f
7
- data.tar.gz: a8e8f5eb0552ed7ef3b3162697ab4b1c5dbc8452af1b801f49919d63ec9c63587901f886eef48515a0d1e0066f7c78fb8716b81818c52d0672af6f207219dcdd
6
+ metadata.gz: 2cee57c64e5e3c8f6190fec1338907378022cffffcad1da056489cc5024a49166f80fa3d3cdf1d40243ddf961c8b1c38d2582658f0ecc85d5a70c8be04aeb538
7
+ data.tar.gz: f260215cfd16a918d1a06996f84f838e2a159f447cb712e398987b3966ca4bbada1e6ff8cd359c49157e7f623a8b0a847b0d47c1d74030f2e0cf74a338868149
data/README.md CHANGED
@@ -38,9 +38,37 @@ Capybara.register_driver :playwright do |app|
38
38
  end
39
39
  ```
40
40
 
41
+ ### Using `playwright-ruby-client` without Capybara
42
+
43
+ Require the Capybara-free entry point instead, either in your Gemfile if you use `Bundler.require`:
44
+
45
+ ```ruby
46
+ gem "venetian", require: "venetian/playwright"
47
+ ```
48
+
49
+ Or directly:
50
+
51
+ ```ruby
52
+ require "venetian/playwright"
53
+ ```
54
+
55
+ Then use `Venetian.start` in place of `Playwright.create`. It takes the same arguments, with the bundled executable as the default
56
+ `:playwright_cli_executable_path`. Browsers aren't installed automatically here, so install them first, either with
57
+ the [Rake task](#installing-browsers-manually) or `Venetian::BrowserInstaller.install`:
58
+
59
+ ```ruby
60
+ Venetian::BrowserInstaller.install(:chromium)
61
+
62
+ Venetian.start do |playwright|
63
+ playwright.chromium.launch do |browser|
64
+ # ...
65
+ end
66
+ end
67
+ ```
68
+
41
69
  ### Automatic browser and dependency installation
42
70
 
43
- The gem will automatically install a browser and dependencies for you before attempting to use it.
71
+ With Capybara or Rails system tests, the gem will automatically install a browser and dependencies for you before attempting to use it.
44
72
  Browsers are installed to `$XDG_CACHE_HOME/ms-playwright` on Linux, `~/Library/Caches/ms-playwright` on macOS,
45
73
  and `%LOCALAPPDATA%\ms-playwright` on Windows. In CI, you will want to
46
74
  [cache this directory](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)
@@ -52,15 +80,27 @@ due to e.g. differences between package managers and package names across distro
52
80
  so dependency installation happens only if a compatible package manager is found.
53
81
  Depending on what you're doing, you may not need to install dependencies,
54
82
  but if you do and your distro is unsupported, you will need to work out what you need and deal with that first.
55
-
56
- When parallelizing tests, there's also the issue of multiple processes all trying to acquire the package manager lock:
57
- if you use Rails system tests with `ActionDispatch::SystemTestCase`,
58
- this is handled automatically by installing all required browsers before disabling auto-installation and then forking.
83
+ On Windows, Firefox also needs the [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist),
84
+ which Playwright doesn't install; you'll get a warning after installation if it's missing.
85
+
86
+ Browsers already installed (e.g. from a restored cache) are left alone. If the browsers directory
87
+ (or `PLAYWRIGHT_BROWSERS_PATH`, if set) is read-only, as with a prepopulated cache, browsers aren't installed at all,
88
+ since Playwright can't take its lock there, though dependencies still are if needed.
89
+ When parallelizing tests, there's also the issue of multiple processes all trying to acquire the package manager lock,
90
+ so installations are serialized across processes. On top of this, if you use Rails system tests with `ActionDispatch::SystemTestCase`,
91
+ all required browsers are installed before disabling auto-installation and then forking.
92
+
93
+ Browsers aren't installed automatically if you pass your own `:playwright_cli_executable_path`, or connect to a server with
94
+ `:playwright_server_endpoint_url` or `:browser_server_endpoint_url`, since the bundled executable won't be the one running them.
95
+ Nor are they if you pass `:executablePath`, or a `:channel` for a system-wide browser like `chrome` or `msedge`
96
+ (install these with e.g. `playwright install chrome`), but other channels are installed instead of the browser type.
97
+ You can also turn installation off with `Venetian.auto_install_browsers = false`, or just dependency installation with
98
+ `Venetian.auto_install_dependencies = false`.
59
99
 
60
100
  ### Installing browsers manually
61
101
 
62
102
  If you can't rely on automatic installation, you can use the included Rake task to install browsers.
63
- With Rails this is included automatically when the gem is required.
103
+ With Rails this is included automatically when `venetian` is required (but not `venetian/playwright`).
64
104
 
65
105
  ```bash
66
106
  $ RAILS_ENV=test rails venetian:install
data/exe/playwright CHANGED
@@ -1,7 +1,7 @@
1
1
  #! /usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "venetian"
4
+ require "venetian/core"
5
5
 
6
6
  begin
7
7
  Venetian.execute(*ARGV, echo: ENV.fetch("VENETIAN_DEBUG", nil))
@@ -5,7 +5,7 @@ namespace :venetian do
5
5
  Install Playwright browsers. Pass a name to install a specific browser (e.g. venetian:install[chromium]), else installs default browsers.
6
6
  USAGE
7
7
  task :install, [:browser] do |_task, args|
8
- require "venetian"
8
+ require "venetian/core"
9
9
  Venetian::BrowserInstaller.install(args[:browser])
10
10
  end
11
11
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+ require "tmpdir"
5
+
3
6
  module Venetian
4
7
  # # Browser Installer
5
8
  #
@@ -8,12 +11,75 @@ module Venetian
8
11
  class << self
9
12
  private
10
13
 
11
- def install_dependencies?
12
- Venetian.auto_install_dependencies && dependencies_supported?
14
+ def install_dependencies?(browser = nil, force: nil)
15
+ return force unless force.nil?
16
+
17
+ Venetian.auto_install_dependencies && dependencies_to_install?(browser)
18
+ end
19
+
20
+ def install_dry_run(browser = nil)
21
+ Executable.capture("install-deps", *browser&.to_s, "--dry-run", echo: debug?)
22
+ end
23
+
24
+ def install_into_browsers_path(browser, deps: nil)
25
+ if browsers_to_install.fetch(browser) { browsers_path_writable? }
26
+ run_installer "install", *browser, *("--with-deps" if deps)
27
+ elsif deps
28
+ run_installer "install-deps", *browser
29
+ end
30
+ browsers_to_install[browser] = false
31
+ dependencies_to_install[browser] = false if deps
32
+ end
33
+
34
+ def browsers_path_writable?
35
+ [expanded_browsers_path, expanded_browsers_path.join(LINKS_DIRECTORY)].all? do |path|
36
+ Dir.mktmpdir(WRITE_PROBE_PREFIX, path.ascend.find(&:directory?)) { true }
37
+ end
38
+ rescue SystemCallError
39
+ false
40
+ end
41
+
42
+ def default_cache_path
43
+ if Gem.win_platform?
44
+ env_path("LOCALAPPDATA") || Pathname(Dir.home).join("AppData", "Local")
45
+ elsif Gem::Platform.local.os == "darwin"
46
+ Pathname(Dir.home).join("Library", "Caches")
47
+ else
48
+ env_path("XDG_CACHE_HOME") || Pathname(Dir.home).join(".cache")
49
+ end
50
+ end
51
+
52
+ def env_path(name)
53
+ return if (path = ENV.fetch(name, "")).empty?
54
+
55
+ Pathname(path)
56
+ end
57
+
58
+ def dependencies_to_install
59
+ @dependencies_to_install ||= {}
60
+ end
61
+
62
+ def browsers_to_install
63
+ @browsers_to_install ||= {}
64
+ end
65
+
66
+ def with_install_lock
67
+ File.open(File.join(Dir.tmpdir, "venetian-#{Process.uid}.lock"), File::RDWR | File::CREAT, 0o600) do |file|
68
+ file.flock(File::LOCK_EX)
69
+ yield
70
+ end
13
71
  end
14
72
 
15
- def install_dry_run
16
- Venetian.system "install-deps", "--dry-run", exception: false, echo: ENV.fetch("VENETIAN_DEBUG", nil)
73
+ def run_installer(*args)
74
+ return Venetian.system(*args, exception: true, echo: true) if debug?
75
+
76
+ Executable.capture(*args, echo: false, merge_stderr: false).then do |output, status|
77
+ raise InstallError, "exit status #{status.exitstatus}\n#{output}".strip unless status.success?
78
+ end
79
+ end
80
+
81
+ def debug?
82
+ ENV.fetch("VENETIAN_DEBUG", nil)
17
83
  end
18
84
  end
19
85
 
@@ -26,21 +92,63 @@ module Venetian
26
92
  def initialize(message = nil)
27
93
  super([INSTALL_FAILED_MESSAGE, *message].join(": "))
28
94
  end
95
+
96
+ def self.wrap(error) # :nodoc:
97
+ error.is_a?(self) ? error : new(error.message)
98
+ end
29
99
  end
30
100
 
31
- # Installs a browser. Raises InstallError if installation fails.
32
- def self.install(browser = nil, install_dependencies: install_dependencies?)
33
- Venetian.system "install", *browser&.to_s, *("--with-deps" if install_dependencies),
34
- exception: true, echo: ENV.fetch("VENETIAN_DEBUG", nil)
101
+ # Installs a browser, unless it and any dependencies are already installed. Playwright skips browsers already in the
102
+ # browsers path, and if that's read-only (e.g. a prepopulated cache), only dependencies are installed. Raises
103
+ # InstallError if installation fails.
104
+ def self.install(browser = nil, install_dependencies: nil)
105
+ with_install_lock do
106
+ install_into_browsers_path(browser&.to_s, deps: install_dependencies?(browser, force: install_dependencies))
107
+ end
35
108
  rescue StandardError => e
36
- raise InstallError, e.message
109
+ raise InstallError.wrap(e)
110
+ end
111
+
112
+ LINKS_DIRECTORY = ".links" # :nodoc:
113
+ WRITE_PROBE_PREFIX = ".venetian-write-probe" # :nodoc:
114
+
115
+ # Output from a dry run where Playwright couldn't spawn its package manager, so has no way to install dependencies.
116
+ NO_PACKAGE_MANAGER_OUTPUT = "ENOENT" # :nodoc:
117
+
118
+ # Returns true if Playwright's dependency installer has something to install on the current OS for a browser,
119
+ # or for the default browsers if none given.
120
+ #
121
+ # On Linux, the dry run simulates the install with the package manager, exiting non-zero if packages are missing or
122
+ # if it can't simulate (e.g. no package lists yet, which the installer fixes by updating them first), unless there's
123
+ # no package manager it can use. On Windows, the dry run only prints the command it would run, so there is always
124
+ # something to install.
125
+ def self.dependencies_to_install?(browser = nil, force: false)
126
+ return true if Gem.win_platform?
127
+
128
+ dependencies_to_install.delete(browser&.to_s) if force
129
+ dependencies_to_install.fetch(browser&.to_s) do |key|
130
+ dependencies_to_install[key] = install_dry_run(browser).then do |output, status|
131
+ !status.success? && !output.include?(NO_PACKAGE_MANAGER_OUTPUT)
132
+ end
133
+ end
37
134
  end
38
135
 
39
- # Returns true if the current OS supports Playwright's dependency installer.
40
- def self.dependencies_supported?(force: false)
41
- return @dependencies_supported unless @dependencies_supported.nil? || force
136
+ def self.browsers_path # :nodoc:
137
+ case ENV.fetch("PLAYWRIGHT_BROWSERS_PATH", "")
138
+ when "" then default_cache_path.join("ms-playwright")
139
+ when "0" then Pathname(Executable.base_command.last).dirname.join(".local-browsers")
140
+ else Pathname(ENV.fetch("PLAYWRIGHT_BROWSERS_PATH"))
141
+ end
142
+ end
143
+
144
+ def self.expanded_browsers_path # :nodoc:
145
+ browsers_path.expand_path(ENV.fetch("INIT_CWD", nil) || Dir.pwd)
146
+ end
42
147
 
43
- @dependencies_supported = install_dry_run || false
148
+ def self.dependencies_supported?(...) # :nodoc:
149
+ warn "Venetian::BrowserInstaller.dependencies_supported? is deprecated; use dependencies_to_install? instead.",
150
+ category: :deprecated, uplevel: 1
151
+ dependencies_to_install?(...)
44
152
  end
45
153
  end
46
154
  end
@@ -2,13 +2,24 @@
2
2
 
3
3
  module Venetian
4
4
  module BrowserRunnerExtensions # :nodoc:
5
+ ALTERNATIVE_DRIVER_OPTIONS = %i[playwright_cli_executable_path playwright_server_endpoint_url
6
+ browser_server_endpoint_url].freeze
7
+ SYSTEM_CHANNEL_PATTERN = /\A(chrome|msedge)/
8
+
9
+ def self.browser_to_preinstall_from(options)
10
+ return if options.values_at(*ALTERNATIVE_DRIVER_OPTIONS, :executablePath).any?
11
+ return if options[:channel].to_s.match?(SYSTEM_CHANNEL_PATTERN)
12
+
13
+ (options[:channel] || options[:browser_type] || :chromium).to_sym
14
+ end
15
+
5
16
  def initialize(options = {}, *args)
6
- @venetian_browser_type = options[:browser_type] || :chromium
17
+ @venetian_browser = BrowserRunnerExtensions.browser_to_preinstall_from(options)
7
18
  super
8
19
  end
9
20
 
10
21
  def start
11
- BrowserInstaller.install(@venetian_browser_type) if Venetian.auto_install_browsers
22
+ BrowserInstaller.install(@venetian_browser) if @venetian_browser && Venetian.auto_install_browsers
12
23
  super
13
24
  end
14
25
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Venetian
4
+ # # Forked Connection Error
5
+ #
6
+ # Raised when a Playwright connection is used by a process forked from the one that started it.
7
+ class ForkedConnectionError < Error
8
+ def initialize(pid) # :nodoc:
9
+ super("This Playwright connection belongs to process #{pid}, so can't be used after forking. " \
10
+ "Start Playwright again in this process instead.")
11
+ end
12
+ end
13
+
14
+ module ConnectionExtensions # :nodoc:
15
+ def initialize(...)
16
+ @venetian_pid = Process.pid
17
+ super
18
+ end
19
+
20
+ def async_send_message_to_server(...)
21
+ raise ForkedConnectionError, @venetian_pid unless Process.pid == @venetian_pid
22
+
23
+ super
24
+ end
25
+ end
26
+ end
27
+
28
+ Playwright::Connection.prepend(Venetian::ConnectionExtensions)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "venetian/version"
4
+ require "venetian/upstream"
5
+ require "venetian/executable"
6
+ require "venetian/browser_installer"
7
+ require "venetian/gemspec"
8
+
9
+ module Venetian # :nodoc:
10
+ class << self
11
+ attr_accessor :auto_install_dependencies
12
+ end
13
+
14
+ class Error < StandardError; end
15
+
16
+ self.auto_install_dependencies = true
17
+
18
+ def self.execute(*, echo: true, **)
19
+ Executable.execute(*, echo: echo, **)
20
+ end
21
+
22
+ def self.system(*, echo: true, **)
23
+ Executable.system(*, echo: echo, **)
24
+ end
25
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+ require "shellwords"
5
+
3
6
  module Venetian
4
7
  # # \Executable
5
8
  #
@@ -7,6 +10,14 @@ module Venetian
7
10
  module Executable
8
11
  class Executor # :nodoc:
9
12
  public :system, :exec
13
+
14
+ def capture(*command, merge_stderr: true, **)
15
+ merge_stderr ? Open3.capture2e(*command, **) : Open3.capture2(*command, **)
16
+ end
17
+
18
+ def run(*command, **)
19
+ Process.wait2(Process.spawn(*command, **)).last
20
+ end
10
21
  end
11
22
 
12
23
  DEFAULT_DIR = File.expand_path(File.join(__dir__, "..", "..", "exe")) # :nodoc:
@@ -44,11 +55,14 @@ module Venetian
44
55
  #
45
56
  # Raised when the Playwright executable cannot be found for the current platform.
46
57
  class ExecutableNotFoundError < StandardError
47
- def initialize(reason, **context) # :nodoc:
58
+ def initialize(reason, exe_dir: nil, exe_path: nil, cli_path: nil, platform: nil) # :nodoc:
48
59
  super(case reason
49
- in :directory_missing then directory_missing_message(context[:exe_dir])
50
- in :executable_missing then executable_missing_message(context[:exe_dir])
51
- else unsupported_platform_message(context[:platform], context[:exe_dir])
60
+ in :directory_missing then directory_missing_message(exe_dir)
61
+ in :executable_missing then executable_missing_message(exe_dir)
62
+ in :missing_platform then unsupported_platform_message(platform, exe_dir)
63
+ in :not_a_file then incomplete_message(exe_path, "#{exe_path} is not a file")
64
+ in :not_executable then incomplete_message(exe_path, "#{exe_path} is not executable")
65
+ in :cli_missing then incomplete_message(exe_path, "#{cli_path} is missing")
52
66
  end)
53
67
  end
54
68
 
@@ -62,6 +76,14 @@ module Venetian
62
76
  "#{INSTALL_DIR_ENV_VAR} is set to #{exe_dir} but playwright was not found there."
63
77
  end
64
78
 
79
+ def incomplete_message(exe_path, problem)
80
+ <<~MSG
81
+ The Playwright driver at #{File.dirname(exe_path)} is incomplete: #{problem}.
82
+
83
+ Reinstall the gem, or check #{INSTALL_DIR_ENV_VAR} points to a complete driver.
84
+ MSG
85
+ end
86
+
65
87
  def unsupported_platform_message(platform, exe_dir)
66
88
  <<~MSG
67
89
  Cannot find the Playwright executable for #{platform} in #{exe_dir}.
@@ -75,28 +97,103 @@ module Venetian
75
97
  end
76
98
  end
77
99
 
78
- class << self
79
- # Returns the path to the Node executable. Raises an error if the executable cannot be found.
80
- def path
100
+ # # Unsupported Path Error
101
+ #
102
+ # Raised on Windows when the path to the Playwright executable contains an environment variable reference such as
103
+ # +%USERNAME%+, as Ruby would then run the command with +cmd.exe+, which expands it.
104
+ class UnsupportedPathError < StandardError
105
+ def initialize(path) # :nodoc:
106
+ super(<<~MSG)
107
+ Cannot run Playwright from #{path} as Windows would expand the environment variable references in its path.
108
+
109
+ Set #{INSTALL_DIR_ENV_VAR} to a directory without them (e.g. %NAME%) containing your Playwright executable.
110
+ MSG
111
+ end
112
+ end
113
+
114
+ module Command # :nodoc:
115
+ TOKEN_PATTERN = /\\.?|%[A-Za-z_][A-Za-z0-9_]*.?|./m
116
+ VARIABLE_REFERENCE_PATTERN = /\A%[A-Za-z_][A-Za-z0-9_]*%\z/
117
+
118
+ def self.shelljoin(command)
119
+ return command.shelljoin unless Gem.win_platform?
120
+
121
+ command.collect { |arg| %("#{arg}") }
122
+ .join(" ")
123
+ .tap { |string| raise UnsupportedPathError, command.first if run_by_cmd?(string) }
124
+ end
125
+
126
+ def self.run_by_cmd?(string)
127
+ string.scan(TOKEN_PATTERN).any? { |token| token.match?(VARIABLE_REFERENCE_PATTERN) }
128
+ end
129
+ end
130
+
131
+ module Validations # :nodoc:
132
+ private
133
+
134
+ def validate!
81
135
  ensure_exe_dir_exists!
82
136
  ensure_gem_platform_supported! unless ENV.key?(INSTALL_DIR_ENV_VAR)
83
137
  ensure_executable_exists!
138
+ ensure_exe_file!
139
+ ensure_exe_executable!
140
+ ensure_cli_path_exists!
141
+ end
142
+
143
+ def ensure_exe_dir_exists!
144
+ raise ExecutableNotFoundError.new(:directory_missing, exe_dir: exe_dir) unless exe_dir_exists?
145
+ end
146
+
147
+ def ensure_gem_platform_supported!
148
+ raise UnsupportedPlatformError, platform if gem_platforms_unsupported?
149
+ end
150
+
151
+ def ensure_executable_exists!
152
+ return unless exe_path.nil?
153
+
154
+ raise ExecutableNotFoundError.new ENV.key?(INSTALL_DIR_ENV_VAR) ? :executable_missing : :missing_platform,
155
+ exe_dir: exe_dir, platform: platform
156
+ end
157
+
158
+ def ensure_exe_file!
159
+ return if exe_file?
160
+
161
+ raise ExecutableNotFoundError.new(:not_a_file, exe_path: exe_path)
162
+ end
163
+
164
+ def ensure_exe_executable!
165
+ return if exe_executable?
166
+
167
+ raise ExecutableNotFoundError.new(:not_executable, exe_path: exe_path)
168
+ end
169
+
170
+ def ensure_cli_path_exists!
171
+ return if cli_path_exists?
172
+
173
+ raise ExecutableNotFoundError.new(:cli_missing, exe_path: exe_path, cli_path: cli_path_for(exe_path))
174
+ end
175
+ end
176
+
177
+ extend Validations
178
+
179
+ class << self
180
+ # Returns the path to the Node executable. Raises an error if the executable cannot be found.
181
+ def path
182
+ validate!
84
183
 
85
184
  exe_path
86
185
  end
87
186
 
88
- # Executes the Playwright executable with the given arguments.
187
+ # Executes the Playwright executable with the given arguments, replacing the current process.
89
188
  def execute(*args, echo: true, exception: true, **)
90
189
  [*base_command, *args].then do |command|
91
- # due to mysterious Windows behavior; see equivalent in `tailwindcss-ruby`
92
- next system(*args, exception:, echo:) if Gem.win_platform?
93
-
94
190
  puts command.shelljoin if echo
95
- begin
96
- executor.exec(*command)
97
- rescue StandardError
98
- exception ? raise : false
99
- end
191
+ # exec misbehaves on Windows (see equivalent in `tailwindcss-ruby`), so run Playwright and exit likewise
192
+ exit executor.run(*command, **).exitstatus if Gem.win_platform?
193
+
194
+ executor.exec(*command, **)
195
+ rescue StandardError
196
+ exception ? raise : false
100
197
  end
101
198
  end
102
199
 
@@ -104,47 +201,61 @@ module Venetian
104
201
  def system(*args, echo: true, exception: true, **)
105
202
  [*base_command, *args].then do |command|
106
203
  puts command.shelljoin if echo
107
- executor.system(*command,
108
- exception:, **{ out: echo ? nil : File::NULL, err: echo ? nil : File::NULL }.compact)
204
+ executor.system(*command, exception:, **(echo ? {} : { out: File::NULL, err: File::NULL }), **)
205
+ end
206
+ end
207
+
208
+ # Runs the Playwright executable with the given arguments, returning its combined output and status.
209
+ def capture(*args, echo: true, **)
210
+ [*base_command, *args].then do |command|
211
+ puts command.shelljoin if echo
212
+ executor.capture(*command, **).tap do |output, _|
213
+ puts output if echo
214
+ end
109
215
  end
110
216
  end
111
217
 
112
218
  # Returns the base command to execute Playwright.
113
219
  def base_command
114
- [path, File.join(File.dirname(path), "package", "cli.js")]
220
+ path.then { |path| [path, cli_path_for(path)] }
221
+ end
222
+
223
+ # Returns the base command as a single string, as expected by +:playwright_cli_executable_path+.
224
+ def cli_command
225
+ Command.shelljoin(base_command)
115
226
  end
116
227
 
117
228
  private
118
229
 
119
- def ensure_exe_dir_exists!
120
- raise ExecutableNotFoundError.new(:directory_missing, exe_dir: exe_dir) unless exe_dir_exists?
230
+ def exe_dir_exists?
231
+ File.directory?(exe_dir)
121
232
  end
122
233
 
123
- def ensure_gem_platform_supported!
124
- raise UnsupportedPlatformError, platform if gem_platforms_unsupported?
234
+ def exe_dir
235
+ ENV[INSTALL_DIR_ENV_VAR] || DEFAULT_DIR
125
236
  end
126
237
 
127
- def ensure_executable_exists!
128
- return unless exe_path.nil?
238
+ def exe_file?
239
+ File.file?(exe_path.to_s)
240
+ end
129
241
 
130
- raise ExecutableNotFoundError.new ENV.key?(INSTALL_DIR_ENV_VAR) ? :executable_missing : :missing_platform,
131
- exe_dir: exe_dir, platform: platform
242
+ def exe_executable?
243
+ File.executable?(exe_path.to_s)
132
244
  end
133
245
 
134
- def exe_dir_exists?
135
- File.directory?(exe_dir)
246
+ def cli_path_exists?
247
+ File.file?(cli_path_for(exe_path))
136
248
  end
137
249
 
138
- def exe_dir
139
- ENV[INSTALL_DIR_ENV_VAR] || DEFAULT_DIR
250
+ def cli_path_for(exe_path)
251
+ File.join(File.dirname(exe_path), "package", "cli.js")
140
252
  end
141
253
 
142
254
  def exe_path
143
255
  return custom_exe_path if ENV.key?(INSTALL_DIR_ENV_VAR)
144
256
 
145
- Upstream::NATIVE_PLATFORMS.select { |platform, _| Gem::Platform.match_gem?(Gem::Platform.new(platform), gem_name) }
146
- .collect { |platform, info| File.join(exe_dir, platform, info.executable_name) }
147
- .detect { |candidate| File.exist?(candidate) }
257
+ native_platforms.collect { |platform, info| File.join(exe_dir, platform, info.executable_name) }
258
+ .detect { |candidate| File.exist?(candidate) }
148
259
  end
149
260
 
150
261
  def custom_exe_path
@@ -156,7 +267,17 @@ module Venetian
156
267
  end
157
268
 
158
269
  def gem_platforms_unsupported?
159
- Upstream::NATIVE_PLATFORMS.keys.none? { |platform| Gem::Platform.match_gem?(Gem::Platform.new(platform), gem_name) }
270
+ native_platforms.none?
271
+ end
272
+
273
+ def native_platforms
274
+ return {} if musl? # musl isn't supported because official Linux Node binaries require glibc
275
+
276
+ Upstream::NATIVE_PLATFORMS.select { |platform, _| Gem::Platform.match_gem?(Gem::Platform.new(platform), gem_name) }
277
+ end
278
+
279
+ def musl?
280
+ Gem::Platform.local.then { |local| local.os == "linux" && local.version.to_s.start_with?("musl") }
160
281
  end
161
282
 
162
283
  def gem_name
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  module Venetian
4
6
  GEMSPEC = Gem.loaded_specs.values.reverse.find do |spec|
5
7
  spec.gem_dir == Pathname.new(__dir__).join("..", "..").expand_path.to_path
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "playwright"
4
+
5
+ require "venetian/core"
6
+ require "venetian/connection_extensions"
7
+
8
+ module Venetian # :nodoc:
9
+ # Starts Playwright using the bundled executable. Accepts the same arguments as +Playwright.create+,
10
+ # so +:playwright_cli_executable_path+ can still be overridden.
11
+ def self.start(**options, &)
12
+ options[:playwright_cli_executable_path] ||= Executable.cli_command
13
+ Playwright.create(**options, &)
14
+ end
15
+ end
@@ -3,7 +3,10 @@
3
3
  module Venetian
4
4
  module PlaywrightCreateExtensions # :nodoc:
5
5
  def initialize(options = {}, *)
6
- super({ playwright_cli_executable_path: Executable.base_command.shelljoin }.merge(options), *)
6
+ unless options[:playwright_cli_executable_path]
7
+ options = options.merge(playwright_cli_executable_path: Executable.cli_command)
8
+ end
9
+ super
7
10
  end
8
11
  end
9
12
  end
@@ -1,22 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Venetian
4
- module SystemTestCaseExtension # :nodoc:
4
+ module SystemTestCaseExtension # :nodoc: all
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ module ParallelizationExtension
8
+ def start
9
+ ActionDispatch::SystemTestCase.venetian_before_fork
10
+ super
11
+ end
12
+ end
13
+
7
14
  prepended do
8
- class_attribute :venetian_browser_type, default: :chromium, instance_writer: false
15
+ # Set by +driven_by+, so classes using a driver registered under another name are still included.
16
+ class_attribute :venetian_browser_type, default: nil, instance_writer: false
9
17
 
10
- parallelize_before_fork do
11
- install_playwright_browsers if venetian_preinstall_browsers_before_fork?
18
+ if respond_to?(:parallelize_before_fork)
19
+ parallelize_before_fork { venetian_before_fork }
20
+ else
21
+ ActiveSupport::Testing::Parallelization.prepend(ParallelizationExtension)
12
22
  end
13
23
  end
14
24
 
15
- class_methods do # :nodoc:
25
+ class_methods do
16
26
  def driven_by(driver, options: {}, **)
27
+ self.venetian_browser_type = if driver == :playwright
28
+ BrowserRunnerExtensions.browser_to_preinstall_from(options)
29
+ end
17
30
  super
18
-
19
- self.venetian_browser_type = driver == :playwright ? options.fetch(:browser_type, :chromium).to_sym : nil
20
31
  end
21
32
 
22
33
  def install_playwright_browsers
@@ -26,6 +37,10 @@ module Venetian
26
37
  end
27
38
  end
28
39
 
40
+ def venetian_before_fork
41
+ install_playwright_browsers if venetian_preinstall_browsers_before_fork?
42
+ end
43
+
29
44
  private
30
45
 
31
46
  def venetian_preinstall_browsers_before_fork?
@@ -34,7 +49,7 @@ module Venetian
34
49
 
35
50
  def venetian_browsers
36
51
  Minitest::Runnable.runnables
37
- .select { |klass| klass < ActionDispatch::SystemTestCase && klass.driver&.name == :playwright }
52
+ .select { |klass| klass < ActionDispatch::SystemTestCase }
38
53
  .filter_map(&:venetian_browser_type)
39
54
  .uniq
40
55
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "playwright"
3
+ require "venetian/version"
4
4
 
5
5
  module Venetian
6
6
  # # \Upstream
@@ -43,7 +43,7 @@ module Venetian
43
43
 
44
44
  # Returns the URL to download the playwright-core npm package containing the driver's JS sources.
45
45
  def self.playwright_core_url
46
- "#{NPM_REGISTRY_URL}/playwright-core/-/playwright-core-#{Playwright::COMPATIBLE_PLAYWRIGHT_VERSION}.tgz"
46
+ "#{NPM_REGISTRY_URL}/playwright-core/-/playwright-core-#{COMPATIBLE_PLAYWRIGHT_VERSION}.tgz"
47
47
  end
48
48
 
49
49
  # Returns the gemspec files for the base gem.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Venetian
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.1"
5
5
  COMPATIBLE_PLAYWRIGHT_VERSION = "1.63.0"
6
6
  end
data/lib/venetian.rb CHANGED
@@ -2,33 +2,18 @@
2
2
 
3
3
  require "capybara/playwright"
4
4
 
5
- require "venetian/version"
6
- require "venetian/upstream"
7
- require "venetian/executable"
5
+ require "venetian/playwright"
8
6
  require "venetian/playwright_create_extensions"
9
- require "venetian/browser_installer"
10
7
  require "venetian/browser_runner_extensions"
11
- require "venetian/gemspec"
12
- require "venetian/railtie" if defined? Rails
8
+ require "venetian/railtie" if defined?(Rails::Railtie)
13
9
 
14
10
  # # Venetian
15
11
  #
16
- # Native Playwright driver for Capybara.
12
+ # Native Playwright driver for Ruby.
17
13
  module Venetian
18
14
  class << self
19
- attr_accessor :auto_install_browsers, :auto_install_dependencies
15
+ attr_accessor :auto_install_browsers
20
16
  end
21
17
 
22
- class Error < StandardError; end
23
-
24
18
  self.auto_install_browsers = true
25
- self.auto_install_dependencies = true
26
-
27
- def self.execute(*, echo: true, **)
28
- Executable.execute(*, echo: echo, **)
29
- end
30
-
31
- def self.system(*, echo: true, **)
32
- Executable.system(*, echo: echo, **)
33
- end
34
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: venetian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.1
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Justin Malčić
@@ -27,14 +27,14 @@ dependencies:
27
27
  name: playwright-ruby-client
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - ">="
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 1.63.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 1.63.0
40
40
  description: Bundles the Playwright driver binary in platform-specific gem variants
@@ -171,8 +171,11 @@ files:
171
171
  - lib/venetian.rb
172
172
  - lib/venetian/browser_installer.rb
173
173
  - lib/venetian/browser_runner_extensions.rb
174
+ - lib/venetian/connection_extensions.rb
175
+ - lib/venetian/core.rb
174
176
  - lib/venetian/executable.rb
175
177
  - lib/venetian/gemspec.rb
178
+ - lib/venetian/playwright.rb
176
179
  - lib/venetian/playwright_create_extensions.rb
177
180
  - lib/venetian/railtie.rb
178
181
  - lib/venetian/system_test_case_extension.rb