playwright-on-rails 0.7.1

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +21 -0
  3. data/.gitignore +8 -0
  4. data/.standard.yml +5 -0
  5. data/.travis.yml +7 -0
  6. data/CHANGELOG.md +115 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +155 -0
  9. data/LICENSE.txt +25 -0
  10. data/README.md +241 -0
  11. data/Rakefile +11 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/exe/playwright-on-rails +17 -0
  15. data/lib/playwright-on-rails/config.rb +41 -0
  16. data/lib/playwright-on-rails/env.rb +14 -0
  17. data/lib/playwright-on-rails/finds_bin.rb +16 -0
  18. data/lib/playwright-on-rails/init.rb +93 -0
  19. data/lib/playwright-on-rails/initializer_hooks.rb +53 -0
  20. data/lib/playwright-on-rails/launches_playwright.rb +63 -0
  21. data/lib/playwright-on-rails/manages_transactions.rb +78 -0
  22. data/lib/playwright-on-rails/open.rb +14 -0
  23. data/lib/playwright-on-rails/railtie.rb +12 -0
  24. data/lib/playwright-on-rails/rake.rb +18 -0
  25. data/lib/playwright-on-rails/resets_state.rb +20 -0
  26. data/lib/playwright-on-rails/run.rb +14 -0
  27. data/lib/playwright-on-rails/server/checker.rb +42 -0
  28. data/lib/playwright-on-rails/server/middleware.rb +67 -0
  29. data/lib/playwright-on-rails/server/puma.rb +31 -0
  30. data/lib/playwright-on-rails/server/timer.rb +24 -0
  31. data/lib/playwright-on-rails/server.rb +121 -0
  32. data/lib/playwright-on-rails/starts_rails_server.rb +34 -0
  33. data/lib/playwright-on-rails/tracks_resets.rb +26 -0
  34. data/lib/playwright-on-rails/version.rb +3 -0
  35. data/lib/playwright-on-rails.rb +11 -0
  36. data/playwright_rails.gemspec +32 -0
  37. data/script/test +21 -0
  38. data/script/test_example_app +21 -0
  39. metadata +166 -0
@@ -0,0 +1,41 @@
1
+ require_relative "env"
2
+
3
+ module PlaywrightOnRails
4
+ class Config
5
+ attr_accessor :rails_dir, :playwright_dir, :host, :port, :base_path, :transactional_server, :playwright_cli_opts
6
+
7
+ def initialize(
8
+ rails_dir: Env.fetch("PLAYWRIGHT_RAILS_DIR", default: Dir.pwd),
9
+ playwright_dir: Env.fetch("PLAYWRIGHT_RAILS_PLAYWRIGHT_DIR", default: rails_dir),
10
+ host: Env.fetch("PLAYWRIGHT_RAILS_HOST", default: "127.0.0.1"),
11
+ port: Env.fetch("PLAYWRIGHT_RAILS_PORT"),
12
+ base_path: Env.fetch("PLAYWRIGHT_RAILS_BASE_PATH", default: "/"),
13
+ transactional_server: Env.fetch("PLAYWRIGHT_RAILS_TRANSACTIONAL_SERVER", type: :boolean, default: true),
14
+ playwright_cli_opts: Env.fetch("PLAYWRIGHT_RAILS_PLAYWRIGHT_OPTS", default: "")
15
+ )
16
+ @rails_dir = rails_dir
17
+ @playwright_dir = playwright_dir
18
+ @host = host
19
+ @port = port
20
+ @base_path = base_path
21
+ @transactional_server = transactional_server
22
+ @playwright_cli_opts = playwright_cli_opts
23
+ end
24
+
25
+ def to_s
26
+ <<~DESC
27
+
28
+ playwright-on-rails configuration:
29
+ ============================
30
+ PLAYWRIGHT_RAILS_DIR.....................#{rails_dir.inspect}
31
+ PLAYWRIGHT_RAILS_PLAYWRIGHT_DIR.............#{playwright_dir.inspect}
32
+ PLAYWRIGHT_RAILS_HOST....................#{host.inspect}
33
+ PLAYWRIGHT_RAILS_PORT....................#{port.inspect}
34
+ PLAYWRIGHT_RAILS_BASE_PATH...............#{base_path.inspect}
35
+ PLAYWRIGHT_RAILS_TRANSACTIONAL_SERVER....#{transactional_server.inspect}
36
+ PLAYWRIGHT_RAILS_PLAYWRIGHT_OPTS............#{playwright_cli_opts.inspect}
37
+
38
+ DESC
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ module PlaywrightOnRails
2
+ module Env
3
+ def self.fetch(name, type: :string, default: nil)
4
+ return default unless ENV.key?(name)
5
+
6
+ if type == :boolean
7
+ no_like_flag = ["", "0", "n", "no", "false"].include?(ENV.fetch(name))
8
+ !no_like_flag
9
+ else
10
+ ENV.fetch(name)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ require "pathname"
2
+
3
+ module PlaywrightOnRails
4
+ class FindsBin
5
+ LOCAL_PATH = "node_modules/.bin/playwright"
6
+
7
+ def call(playwright_dir = Dir.pwd)
8
+ local_path = Pathname.new(playwright_dir).join(LOCAL_PATH)
9
+ if File.exist?(local_path)
10
+ local_path
11
+ else
12
+ "playwright"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,93 @@
1
+ module PlaywrightOnRails
2
+ class Init
3
+ DEFAULT_CONFIG = <<~JS
4
+ import { defineConfig, devices } from '@playwright/test';
5
+
6
+ /**
7
+ * Read environment variables from file.
8
+ * https://github.com/motdotla/dotenv
9
+ */
10
+ // require('dotenv').config();
11
+
12
+ /**
13
+ * See https://playwright.dev/docs/test-configuration.
14
+ */
15
+ export default defineConfig({
16
+ testDir: './tests',
17
+ /* Run tests in files in parallel */
18
+ fullyParallel: true,
19
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
20
+ forbidOnly: !!process.env.CI,
21
+ /* Retry on CI only */
22
+ retries: process.env.CI ? 2 : 0,
23
+ /* Opt out of parallel tests on CI. */
24
+ workers: process.env.CI ? 1 : undefined,
25
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
26
+ reporter: 'html',
27
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
28
+ use: {
29
+ /* Base URL to use in actions like `await page.goto('/')`. */
30
+ baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000',
31
+
32
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
33
+ trace: 'on-first-retry',
34
+ },
35
+
36
+ /* Configure projects for major browsers */
37
+ projects: [
38
+ {
39
+ name: 'chromium',
40
+ use: { ...devices['Desktop Chrome'] },
41
+ },
42
+
43
+ {
44
+ name: 'firefox',
45
+ use: { ...devices['Desktop Firefox'] },
46
+ },
47
+
48
+ {
49
+ name: 'webkit',
50
+ use: { ...devices['Desktop Safari'] },
51
+ },
52
+
53
+ /* Test against mobile viewports. */
54
+ // {
55
+ // name: 'Mobile Chrome',
56
+ // use: { ...devices['Pixel 5'] },
57
+ // },
58
+ // {
59
+ // name: 'Mobile Safari',
60
+ // use: { ...devices['iPhone 12'] },
61
+ // },
62
+
63
+ /* Test against branded browsers. */
64
+ // {
65
+ // name: 'Microsoft Edge',
66
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
67
+ // },
68
+ // {
69
+ // name: 'Google Chrome',
70
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
71
+ // },
72
+ ],
73
+
74
+ /* Run your local dev server before starting the tests */
75
+ // webServer: {
76
+ // command: 'npm run start',
77
+ // url: 'http://127.0.0.1:3000',
78
+ // reuseExistingServer: !process.env.CI,
79
+ // },
80
+ });
81
+ JS
82
+
83
+ def call(playwright_dir = Config.new.playwright_dir)
84
+ config_path = File.join(playwright_dir, "playwright.config.js")
85
+ if !File.exist?(config_path)
86
+ File.write(config_path, DEFAULT_CONFIG)
87
+ puts "Playwright config initialized in `#{config_path}'"
88
+ else
89
+ warn "Playwright config already exists in `#{config_path}'. Skipping."
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,53 @@
1
+ module PlaywrightOnRails
2
+ def self.hooks
3
+ InitializerHooks.instance
4
+ end
5
+
6
+ class InitializerHooks
7
+ def self.instance
8
+ @instance ||= new
9
+ end
10
+
11
+ def before_server_start(&blk)
12
+ register(:before_server_start, blk)
13
+ end
14
+
15
+ def after_server_start(&blk)
16
+ register(:after_server_start, blk)
17
+ end
18
+
19
+ def after_transaction_start(&blk)
20
+ register(:after_transaction_start, blk)
21
+ end
22
+
23
+ def after_state_reset(&blk)
24
+ register(:after_state_reset, blk)
25
+ end
26
+
27
+ def before_server_stop(&blk)
28
+ register(:before_server_stop, blk)
29
+ end
30
+
31
+ def reset!
32
+ @hooks = {}
33
+ end
34
+
35
+ def run(name)
36
+ return unless @hooks[name]
37
+ @hooks[name].each do |blk|
38
+ blk.call
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def register(name, blk)
45
+ @hooks[name] ||= []
46
+ @hooks[name] << blk
47
+ end
48
+
49
+ def initialize
50
+ reset!
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,63 @@
1
+ require_relative "finds_bin"
2
+ require_relative "config"
3
+ require_relative "initializer_hooks"
4
+ require_relative "manages_transactions"
5
+ require_relative "starts_rails_server"
6
+
7
+ module PlaywrightOnRails
8
+ class LaunchesPlaywright
9
+ def initialize
10
+ @initializer_hooks = InitializerHooks.instance
11
+ @manages_transactions = ManagesTransactions.instance
12
+ @starts_rails_server = StartsRailsServer.new
13
+ @finds_bin = FindsBin.new
14
+ end
15
+
16
+ def call(command, config)
17
+ puts config
18
+ @initializer_hooks.run(:before_server_start)
19
+ if config.transactional_server
20
+ @manages_transactions.begin_transaction
21
+ end
22
+ server = @starts_rails_server.call(
23
+ host: config.host,
24
+ port: config.port,
25
+ transactional_server: config.transactional_server
26
+ )
27
+ bin = @finds_bin.call(config.playwright_dir)
28
+
29
+ set_exit_hooks!(config)
30
+
31
+ command = <<~EXEC
32
+ PLAYWRIGHT_BASE_URL="http://#{server.host}:#{server.port}#{config.base_path}" "#{bin}" #{command} --config "#{config.playwright_dir}/playwright.config.js" #{config.playwright_cli_opts}
33
+ EXEC
34
+
35
+ puts "\nLaunching Playwright…\n$ #{command}\n"
36
+ system command
37
+ end
38
+
39
+ private
40
+
41
+ def set_exit_hooks!(config)
42
+ at_exit do
43
+ run_exit_hooks_if_necessary!(config)
44
+ end
45
+ Signal.trap("INT") do
46
+ puts "Exiting playwright-on-rails…"
47
+ exit
48
+ end
49
+ end
50
+
51
+ def run_exit_hooks_if_necessary!(config)
52
+ @at_exit_hooks_have_fired ||= false # avoid warning
53
+ return if @at_exit_hooks_have_fired
54
+
55
+ if config.transactional_server
56
+ @manages_transactions.rollback_transaction
57
+ end
58
+ @initializer_hooks.run(:before_server_stop)
59
+
60
+ @at_exit_hooks_have_fired = true
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,78 @@
1
+ require_relative "initializer_hooks"
2
+
3
+ module PlaywrightOnRails
4
+ class ManagesTransactions
5
+ def self.instance
6
+ @instance ||= new
7
+ end
8
+
9
+ def begin_transaction
10
+ @connections = gather_connections
11
+ @connections.each do |connection|
12
+ connection.begin_transaction joinable: false, _lazy: false
13
+ connection.pool.lock_thread = true
14
+ end
15
+
16
+ # When connections are established in the future, begin a transaction too
17
+ @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") { |_, _, _, _, payload|
18
+ if payload.key?(:spec_name) && (spec_name = payload[:spec_name])
19
+ setup_shared_connection_pool
20
+
21
+ begin
22
+ connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name)
23
+ rescue ActiveRecord::ConnectionNotEstablished
24
+ connection = nil
25
+ end
26
+
27
+ if connection && !@connections.include?(connection)
28
+ connection.begin_transaction joinable: false, _lazy: false
29
+ connection.pool.lock_thread = true
30
+ @connections << connection
31
+ end
32
+ end
33
+ }
34
+
35
+ @initializer_hooks.run(:after_transaction_start)
36
+ end
37
+
38
+ def rollback_transaction
39
+ return unless @connections.present?
40
+
41
+ ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
42
+
43
+ @connections.each do |connection|
44
+ connection.rollback_transaction if connection.transaction_open?
45
+ connection.pool.lock_thread = false
46
+ end
47
+ @connections.clear
48
+
49
+ ActiveRecord::Base.connection_handler.clear_active_connections!
50
+ end
51
+
52
+ private
53
+
54
+ def initialize
55
+ @initializer_hooks = InitializerHooks.instance
56
+ end
57
+
58
+ def gather_connections
59
+ setup_shared_connection_pool
60
+
61
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
62
+ end
63
+
64
+ # Shares the writing connection pool with connections on
65
+ # other handlers.
66
+ #
67
+ # In an application with a primary and replica the test fixtures
68
+ # need to share a connection pool so that the reading connection
69
+ # can see data in the open transaction on the writing connection.
70
+ def setup_shared_connection_pool
71
+ return unless ActiveRecord::TestFixtures.respond_to?(:setup_shared_connection_pool)
72
+ @legacy_saved_pool_configs ||= Hash.new { |hash, key| hash[key] = {} }
73
+ @saved_pool_configs ||= Hash.new { |hash, key| hash[key] = {} }
74
+
75
+ ActiveRecord::TestFixtures.instance_method(:setup_shared_connection_pool).bind(self).call
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "launches_playwright"
2
+ require_relative "config"
3
+
4
+ module PlaywrightOnRails
5
+ class Open
6
+ def initialize
7
+ @launches_playwright = LaunchesPlaywright.new
8
+ end
9
+
10
+ def call(config = Config.new)
11
+ @launches_playwright.call("test --ui", config)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require "rails/railtie"
2
+ require "pathname"
3
+
4
+ module PlaywrightOnRails
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :"playwright-on-rails"
7
+
8
+ rake_tasks do
9
+ load Pathname.new(__dir__).join("rake.rb")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ require "pathname"
2
+ CLI = Pathname.new(File.dirname(__FILE__)).join("../../exe/playwright-on-rails")
3
+
4
+ desc "Initialize playwright.config.js"
5
+ task :"playwright:init" do
6
+ system "#{CLI} init"
7
+ end
8
+
9
+ desc "Open interactive Playwright app for developing tests"
10
+ task :"playwright:open" do
11
+ trap("SIGINT") {} # avoid traceback
12
+ system "#{CLI} open"
13
+ end
14
+
15
+ desc "Run Playwright tests headlessly"
16
+ task :"playwright:run" do
17
+ abort unless system "#{CLI} run"
18
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "config"
2
+ require_relative "manages_transactions"
3
+ require_relative "initializer_hooks"
4
+
5
+ module PlaywrightOnRails
6
+ class ResetsState
7
+ def initialize
8
+ @manages_transactions = ManagesTransactions.instance
9
+ @initializer_hooks = InitializerHooks.instance
10
+ end
11
+
12
+ def call(transactional_server:)
13
+ if transactional_server
14
+ @manages_transactions.rollback_transaction
15
+ @manages_transactions.begin_transaction
16
+ end
17
+ @initializer_hooks.run(:after_state_reset)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "launches_playwright"
2
+ require_relative "config"
3
+
4
+ module PlaywrightOnRails
5
+ class Run
6
+ def initialize
7
+ @launches_playwright = LaunchesPlaywright.new
8
+ end
9
+
10
+ def call(config = Config.new)
11
+ @launches_playwright.call("test", config)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ module PlaywrightOnRails
2
+ class Server
3
+ class Checker
4
+ TRY_HTTPS_ERRORS = [EOFError, Net::ReadTimeout, Errno::ECONNRESET].freeze
5
+
6
+ def initialize(host, port)
7
+ @host, @port = host, port
8
+ @ssl = false
9
+ end
10
+
11
+ def request(&block)
12
+ ssl? ? https_request(&block) : http_request(&block)
13
+ rescue *TRY_HTTPS_ERRORS
14
+ res = https_request(&block)
15
+ @ssl = true
16
+ res
17
+ end
18
+
19
+ def ssl?
20
+ @ssl
21
+ end
22
+
23
+ private
24
+
25
+ def http_request(&block)
26
+ make_request(read_timeout: 2, &block)
27
+ end
28
+
29
+ def https_request(&block)
30
+ make_request(**ssl_options, &block)
31
+ end
32
+
33
+ def make_request(**options, &block)
34
+ Net::HTTP.start(@host, @port, options.merge(max_retries: 0), &block)
35
+ end
36
+
37
+ def ssl_options
38
+ {use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE}
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,67 @@
1
+ module PlaywrightOnRails
2
+ class Server
3
+ class Middleware
4
+ class Counter
5
+ def initialize
6
+ @value = []
7
+ @mutex = Mutex.new
8
+ end
9
+
10
+ def increment(uri)
11
+ @mutex.synchronize { @value.push(uri) }
12
+ end
13
+
14
+ def decrement(uri)
15
+ @mutex.synchronize { @value.delete_at(@value.index(uri) || @value.length) }
16
+ end
17
+
18
+ def positive?
19
+ @mutex.synchronize { @value.length.positive? }
20
+ end
21
+
22
+ def value
23
+ @mutex.synchronize { @value.dup }
24
+ end
25
+ end
26
+
27
+ attr_reader :error
28
+
29
+ def initialize(app, server_errors, extra_middleware = [])
30
+ @app = app
31
+ @extended_app = extra_middleware.inject(@app) { |ex_app, klass|
32
+ klass.new(ex_app)
33
+ }
34
+ @counter = Counter.new
35
+ @server_errors = server_errors
36
+ end
37
+
38
+ def pending_requests
39
+ @counter.value
40
+ end
41
+
42
+ def pending_requests?
43
+ @counter.positive?
44
+ end
45
+
46
+ def clear_error
47
+ @error = nil
48
+ end
49
+
50
+ def call(env)
51
+ if env["PATH_INFO"] == "/__identify__"
52
+ [200, {}, [@app.object_id.to_s]]
53
+ else
54
+ @counter.increment(env["REQUEST_URI"])
55
+ begin
56
+ @extended_app.call(env)
57
+ rescue *@server_errors => e
58
+ @error ||= e
59
+ raise e
60
+ ensure
61
+ @counter.decrement(env["REQUEST_URI"])
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,31 @@
1
+ module PlaywrightOnRails
2
+ class Server
3
+ module Puma
4
+ def self.create(app, port, host)
5
+ require "rack/handler/puma"
6
+
7
+ # If we just run the Puma Rack handler it installs signal handlers which prevent us from being able to interrupt tests.
8
+ # Therefore construct and run the Server instance ourselves.
9
+ # Rack::Handler::Puma.run(app, { Host: host, Port: port, Threads: "0:4", workers: 0, daemon: false }.merge(options))
10
+ default_options = {Host: host, Port: port, Threads: "0:4", workers: 0, daemon: false}
11
+ options = default_options # .merge(options)
12
+
13
+ conf = Rack::Handler::Puma.config(app, options)
14
+ conf.clamp
15
+ logger = (defined?(::Puma::LogWriter) ? ::Puma::LogWriter : ::Puma::Events).stdio
16
+
17
+ puma_ver = Gem::Version.new(::Puma::Const::PUMA_VERSION)
18
+ require_relative "patches/puma_ssl" if (Gem::Version.new("4.0.0")...Gem::Version.new("4.1.0")).cover? puma_ver
19
+
20
+ logger.log "Starting Puma..."
21
+ logger.log "* Version #{::Puma::Const::PUMA_VERSION} , codename: #{::Puma::Const::CODE_NAME}"
22
+ logger.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
23
+
24
+ ::Puma::Server.new(conf.app, defined?(::Puma::LogWriter) ? nil : logger, conf.options).tap do |s|
25
+ s.binder.parse conf.options[:binds], s.respond_to?(:log_writer) ? s.log_writer : s.events
26
+ s.min_threads, s.max_threads = conf.options[:min_threads], conf.options[:max_threads] if s.respond_to?(:min_threads=)
27
+ end.run.join
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ module PlaywrightOnRails
2
+ class Server
3
+ class Timer
4
+ def initialize(expire_in)
5
+ @start = current
6
+ @expire_in = expire_in
7
+ end
8
+
9
+ def expired?
10
+ current - @start >= @expire_in
11
+ end
12
+
13
+ def stalled?
14
+ @start == current
15
+ end
16
+
17
+ private
18
+
19
+ def current
20
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
21
+ end
22
+ end
23
+ end
24
+ end