e2e 0.3.2

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.
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(RSpec::Matchers)
4
+ RSpec::Matchers.define :have_class do |expected_class|
5
+ match do |element|
6
+ element.has_class?(expected_class)
7
+ end
8
+
9
+ failure_message do |element|
10
+ "expected element to have class '#{expected_class}', but it had '#{element.classes.join(" ")}'"
11
+ end
12
+
13
+ failure_message_when_negated do |element|
14
+ "expected element not to have class '#{expected_class}', but it did"
15
+ end
16
+ end
17
+
18
+ RSpec::Matchers.define :have_text do |expected_text|
19
+ match do |element|
20
+ if expected_text.is_a?(Regexp)
21
+ element.text.match?(expected_text)
22
+ else
23
+ element.text.include?(expected_text)
24
+ end
25
+ end
26
+
27
+ failure_message do |element|
28
+ "expected element to have text '#{expected_text}', but it had '#{element.text}'"
29
+ end
30
+
31
+ failure_message_when_negated do |element|
32
+ "expected element not to have text '#{expected_text}', but it did"
33
+ end
34
+ end
35
+
36
+ RSpec::Matchers.alias_matcher :have_content, :have_text
37
+
38
+ RSpec::Matchers.define :have_value do |expected_value|
39
+ match do |element|
40
+ element.value == expected_value
41
+ end
42
+
43
+ failure_message do |element|
44
+ "expected element to have value '#{expected_value}', but it had '#{element.value}'"
45
+ end
46
+
47
+ failure_message_when_negated do |element|
48
+ "expected element not to have value '#{expected_value}', but it did"
49
+ end
50
+ end
51
+
52
+ RSpec::Matchers.define :have_attribute do |attribute, expected_value|
53
+ match do |element|
54
+ element[attribute] == expected_value
55
+ end
56
+
57
+ failure_message do |element|
58
+ "expected element to have attribute '#{attribute}' with value '#{expected_value}', but it had '#{element[attribute]}'"
59
+ end
60
+
61
+ failure_message_when_negated do |element|
62
+ "expected element not to have attribute '#{attribute}' with value '#{expected_value}', but it did"
63
+ end
64
+ end
65
+
66
+ RSpec::Matchers.define :be_checked do
67
+ match do |element|
68
+ element.checked?
69
+ end
70
+
71
+ failure_message do |element|
72
+ "expected element to be checked, but it wasn't"
73
+ end
74
+
75
+ failure_message_when_negated do |element|
76
+ "expected element not to be checked, but it was"
77
+ end
78
+ end
79
+
80
+ RSpec::Matchers.define :be_disabled do
81
+ match do |element|
82
+ element.disabled?
83
+ end
84
+
85
+ failure_message do |element|
86
+ "expected element to be disabled, but it was enabled"
87
+ end
88
+
89
+ failure_message_when_negated do |element|
90
+ "expected element to be enabled, but it was disabled"
91
+ end
92
+ end
93
+
94
+ RSpec::Matchers.define :be_enabled do
95
+ match do |element|
96
+ element.enabled?
97
+ end
98
+
99
+ failure_message do |element|
100
+ "expected element to be enabled, but it was disabled"
101
+ end
102
+
103
+ failure_message_when_negated do |element|
104
+ "expected element to be disabled, but it was enabled"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "e2e"
5
+
6
+ module E2E
7
+ module Minitest
8
+ class TestCase < ::Minitest::Test
9
+ include E2E::DSL
10
+
11
+ def teardown
12
+ take_failed_screenshot if !passed? && !skipped?
13
+
14
+ # Reset session but keep browser open for speed
15
+ E2E.session.reset! if E2E.instance_variable_get(:@session)
16
+ super
17
+ end
18
+
19
+ private
20
+
21
+ def take_failed_screenshot
22
+ screenshots_dir = File.expand_path("tmp/screenshots", Dir.pwd)
23
+ FileUtils.mkdir_p(screenshots_dir)
24
+
25
+ name = "#{self.class.name}_#{name}".gsub(/[^0-9A-Za-z]/, "_")
26
+ path = File.join(screenshots_dir, "#{name}.png")
27
+
28
+ begin
29
+ save_screenshot(path) # rubocop:disable Lint/Debugger
30
+ puts "\n[E2E] Screenshot saved to #{path}"
31
+ rescue => e
32
+ puts "\n[E2E] Failed to save screenshot: #{e.message}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/e2e/rails.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E2E
4
+ module Rails
5
+ # This module allows the test thread and the server thread to share the same
6
+ # ActiveRecord connection. This is crucial for running tests in transactions
7
+ # which is much faster than using truncation.
8
+ module ActiveRecordSharedConnection
9
+ def connection
10
+ @shared_connection || retrieve_connection
11
+ end
12
+
13
+ def shared_connection=(connection)
14
+ @shared_connection = connection
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # We only want to apply this when Rails is present and we want to enable it.
21
+ def E2E.enable_shared_connection!
22
+ return unless defined?(ActiveRecord::Base)
23
+
24
+ ActiveRecord::Base.extend(E2E::Rails::ActiveRecordSharedConnection)
25
+ ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
26
+ end
data/lib/e2e/rspec.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e2e"
4
+ begin
5
+ require "rspec/expectations"
6
+ rescue LoadError
7
+ end
8
+ require_relative "matchers"
9
+
10
+ RSpec.configure do |config|
11
+ config.include E2E::DSL, type: :e2e
12
+
13
+ config.after(:each, type: :e2e) do |example|
14
+ if example.exception
15
+ # Create tmp/screenshots directory if it doesn't exist
16
+ screenshots_dir = File.expand_path("tmp/screenshots", Dir.pwd)
17
+ FileUtils.mkdir_p(screenshots_dir)
18
+
19
+ # Generate filename based on example description
20
+ filename = "#{example.full_description.gsub(/[^0-9A-Za-z]/, "_")}.png"
21
+ path = File.join(screenshots_dir, filename)
22
+
23
+ begin
24
+ E2E.session.save_screenshot(path)
25
+ rescue => e
26
+ puts "Failed to save screenshot: #{e.message}"
27
+ end
28
+ end
29
+
30
+ # Reset session (clear cookies/storage) but keep browser open
31
+ E2E.session.reset! if E2E.instance_variable_get(:@session)
32
+ end
33
+
34
+ config.after(:suite) do
35
+ E2E.session.quit if E2E.instance_variable_get(:@session)
36
+ end
37
+ end
data/lib/e2e/server.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "rackup"
5
+ require "webrick"
6
+ require "socket"
7
+
8
+ module E2E
9
+ class Server
10
+ attr_reader :app, :host, :port
11
+
12
+ def initialize(app, port: nil)
13
+ @app = app
14
+ @host = "127.0.0.1"
15
+ @port = port || find_available_port
16
+ end
17
+
18
+ def start
19
+ @thread = Thread.new do
20
+ # Use Rackup handler for Rack 3 compatibility
21
+ Rackup::Handler::WEBrick.run(
22
+ @app,
23
+ Host: @host,
24
+ Port: @port,
25
+ AccessLog: [],
26
+ Logger: WEBrick::Log.new(nil, 0) # Silence logs
27
+ )
28
+ end
29
+ wait_for_boot
30
+ end
31
+
32
+ def base_url
33
+ "http://#{@host}:#{@port}"
34
+ end
35
+
36
+ private
37
+
38
+ def find_available_port
39
+ server = TCPServer.new("127.0.0.1", 0)
40
+ port = server.addr[1]
41
+ server.close
42
+ port
43
+ end
44
+
45
+ def wait_for_boot
46
+ start_time = Time.now
47
+ loop do
48
+ socket = TCPSocket.new("127.0.0.1", @port)
49
+ socket.close
50
+ break
51
+ rescue Errno::ECONNREFUSED
52
+ raise "Server failed to boot" if Time.now - start_time > 10
53
+ sleep 0.1
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module E2E
6
+ class Session
7
+ extend Forwardable
8
+
9
+ attr_reader :driver
10
+
11
+ def_delegators :@driver, :current_url, :click, :click_button, :click_link, :fill_in, :check, :uncheck, :attach_file, :body, :evaluate, :save_screenshot, :native, :pause, :reset!, :quit
12
+
13
+ def initialize(driver_name = E2E.config.driver)
14
+ @driver = initialize_driver(driver_name)
15
+ end
16
+
17
+ def find(...)
18
+ @driver.find(...)
19
+ end
20
+
21
+ def all(...)
22
+ @driver.all(...)
23
+ end
24
+
25
+ def visit(url)
26
+ if url.start_with?("/") && E2E.server
27
+ url = "#{E2E.server.base_url}#{url}"
28
+ end
29
+ @driver.visit(url)
30
+ end
31
+
32
+ private
33
+
34
+ def initialize_driver(name)
35
+ case name
36
+ when :playwright
37
+ Drivers::Playwright.new
38
+ else
39
+ raise ArgumentError, "Unknown driver: #{name}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E2E
4
+ VERSION = "0.3.2"
5
+ end
data/lib/e2e.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "e2e/version"
4
+ require_relative "e2e/driver"
5
+ require_relative "e2e/server"
6
+ require_relative "e2e/session"
7
+ require_relative "e2e/dsl"
8
+ require_relative "e2e/element"
9
+ require_relative "e2e/rails"
10
+ require_relative "e2e/drivers/playwright"
11
+
12
+ module E2E
13
+ class Error < StandardError; end
14
+
15
+ class << self
16
+ def session
17
+ @session ||= Session.new
18
+ end
19
+
20
+ def reset_session!
21
+ @session&.quit
22
+ @session = nil
23
+ end
24
+
25
+ def configure
26
+ yield(config)
27
+ end
28
+
29
+ def config
30
+ @config ||= Config.new
31
+ end
32
+
33
+ def server
34
+ @server ||= if config.app
35
+ srv = Server.new(config.app)
36
+ srv.start
37
+ srv
38
+ end
39
+ end
40
+ end
41
+
42
+ class Config
43
+ attr_accessor :driver, :headless, :app, :browser_type
44
+
45
+ def initialize
46
+ @driver = :playwright
47
+ @browser_type = :chromium
48
+ @headless = ENV.fetch("HEADLESS", "true") == "true"
49
+ @app = nil
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module E2e
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ class_option :test_framework, type: :string, desc: "Test framework to be invoked"
10
+
11
+ def create_helper_file
12
+ case test_framework
13
+ when :rspec
14
+ create_rspec_helper
15
+ when :minitest
16
+ create_minitest_helper
17
+ else
18
+ say "Could not detect test framework. Please specify --test-framework=rspec or minitest.", :red
19
+ end
20
+ end
21
+
22
+ def display_instructions
23
+ case test_framework
24
+ when :rspec
25
+ say "E2E gem installed for RSpec! Use `require 'e2e_helper'` in specs.", :green
26
+ when :minitest
27
+ say "E2E gem installed for Minitest! Inherit from `E2E::Minitest::TestCase` in your tests.", :green
28
+ end
29
+ end
30
+
31
+ def configure_rubocop
32
+ return unless File.exist?(".rubocop.yml")
33
+
34
+ config_content = File.read(".rubocop.yml")
35
+
36
+ if config_content.include?("inherit_gem:")
37
+ inject_into_file ".rubocop.yml", after: "inherit_gem:\n" do
38
+ " e2e: config/rubocop.yml\n"
39
+ end
40
+ else
41
+ prepend_to_file ".rubocop.yml" do
42
+ "inherit_gem:\n e2e: config/rubocop.yml\n\n"
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def test_framework
50
+ return options[:test_framework].to_sym if options[:test_framework]
51
+
52
+ # Check Rails configuration
53
+ rails_config = Rails.application.config.generators.options[:rails][:test_framework]
54
+ return rails_config if rails_config
55
+
56
+ # Fallback to directory detection
57
+ return :rspec if File.directory?("spec")
58
+ :minitest if File.directory?("test")
59
+ end
60
+
61
+ def create_rspec_helper
62
+ create_file "spec/e2e_helper.rb", <<~RUBY
63
+ # frozen_string_literal: true
64
+
65
+ require "rails_helper"
66
+ require "e2e/rspec"
67
+
68
+ E2E.configure do |config|
69
+ config.app = Rails.application
70
+ config.headless = ENV.fetch("HEADLESS", "true") == "true"
71
+ end
72
+
73
+ # If you want to use transactional tests (faster), enable shared connection:
74
+ # E2E.enable_shared_connection!
75
+
76
+ RSpec.configure do |config|
77
+ # Add additional configuration here
78
+ end
79
+ RUBY
80
+ end
81
+
82
+ def create_minitest_helper
83
+ create_file "test/e2e_helper.rb", <<~RUBY
84
+ # frozen_string_literal: true
85
+
86
+ require "test_helper"
87
+ require "e2e/minitest"
88
+
89
+ E2E.configure do |config|
90
+ config.app = Rails.application
91
+ config.headless = ENV.fetch("HEADLESS", "true") == "true"
92
+ end
93
+
94
+ # If you want to use transactional tests (faster), enable shared connection:
95
+ # E2E.enable_shared_connection!
96
+ RUBY
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e2e_helper"
4
+
5
+ class <%= class_name %>Test < E2E::Minitest::TestCase
6
+ def test_works
7
+ visit "/"
8
+ # assert_includes page.body, "Home"
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e2e_helper"
4
+
5
+ RSpec.describe "<%= class_name %>", type: :e2e do
6
+ it "works" do
7
+ visit "/"
8
+ # expect(page.body).to include("Home")
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module E2e
6
+ module Generators
7
+ class TestGenerator < ::Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+ class_option :test_framework, type: :string, desc: "Test framework to be invoked"
10
+
11
+ def create_test_file
12
+ case test_framework
13
+ when :rspec
14
+ create_rspec_test
15
+ when :minitest
16
+ create_minitest_test
17
+ else
18
+ say "Could not detect test framework. Please specify --test-framework=rspec or minitest.", :red
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def test_framework
25
+ return options[:test_framework].to_sym if options[:test_framework]
26
+
27
+ rails_config = Rails.application.config.generators.options[:rails][:test_framework]
28
+ return rails_config if rails_config
29
+
30
+ return :rspec if File.directory?("spec")
31
+ :minitest if File.directory?("test")
32
+ end
33
+
34
+ def create_rspec_test
35
+ template "rspec_test.rb.erb", "spec/e2e/#{file_name}_spec.rb"
36
+ end
37
+
38
+ def create_minitest_test
39
+ template "minitest_test.rb.erb", "test/e2e/#{file_name}_test.rb"
40
+ end
41
+ end
42
+ end
43
+ end
data/sig/e2e.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module E2E
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end