integration-tests-rails 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 30f959f27b2ebdf182bc6a8107f851baeb5f77805e02bfff5bf2dc591faecb5c
4
+ data.tar.gz: 4b5dbdb852a9aecef5346044bf3b6ddb2f3349fa92e62411d39af6bd6c8a1b05
5
+ SHA512:
6
+ metadata.gz: f52d395e2dd5ab6fe1726e050f28c9dd34b8153a6d6e7b45dabd61ac15b0a1bac9f5aa15d49e1bc5247d0a2c42e84b9ee9ec0c98bb831ec6a535d526a77c5cb0
7
+ data.tar.gz: f370d6841dfc1a179f8cf20aee07ff137f6f38f45eceeabb1838e0a5cde9902d9d04c5df8b7e5e498ce5c9fdb43d3db1de03f27bb5069359bc5d46f099fc614c
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # Integration Tests Rails
2
+
3
+ This gem is designed to facilitate integration testing in Ruby on Rails applications. It provides a streamlined way to configure and run integration tests while capturing coverage data for JavaScript files.
4
+
5
+ ## Tech Stack
6
+
7
+ - **Ruby on Rails** - The primary framework for building web applications.
8
+ - **RSpec** - A testing tool for Ruby, used for writing and executing test cases.
9
+ - **Capybara** - A library that helps you test web applications by simulating how a real user would interact with your app.
10
+ - **Cuprite** - A Capybara driver that uses Chrome DevTools Protocol.
11
+ - **Istanbul** - A JavaScript code coverage tool.
12
+ - **Puma** - A concurrent web server.
13
+
14
+ ## Getting Started
15
+
16
+ ### Installation
17
+
18
+ Add this line to your Rails application's Gemfile:
19
+
20
+ ```ruby
21
+ group :development, :test do
22
+ gem 'integration_tests_rails', require: false
23
+ end
24
+ ```
25
+
26
+ Test environment is needed to run integration tests. Development environment is needed to be able to use terminal commands provided by the gem.
27
+
28
+ After adding the gem, run the following command to install it:
29
+
30
+ ```sh
31
+ bundle install
32
+ ```
33
+
34
+ After installation, run:
35
+
36
+ ```sh
37
+ rails generate integration_tests_rails:install
38
+ ```
39
+
40
+ If you do not have `Yarn` installed, the above command will prompt you to install it. Follow the instructions to complete the installation. Re-run the command after installing `Yarn`.
41
+
42
+ The generator will do the following:
43
+ - Install Instanbul using Yarn.
44
+ - Create a controller that can be used to *unit test JavaScript code*.
45
+ - Add a line in `routes.rb` to route requests to the above controller.
46
+ - Add an entry in `.gitignore` to ignore coverage reports and locally installed Istanbul packages.
47
+
48
+ ### Configuration
49
+
50
+ Since test suites can vary greatly between applications, manual setup of the configuration may vary. It is recommended to create a separate helper file alongside `spec_helper.rb` and `rails_helper.rb`.
51
+
52
+ ```ruby
53
+ # spec/capybara_helper.rb
54
+
55
+ require 'integration_tests_rails'
56
+
57
+ IntegrationTestsRails.setup
58
+
59
+ require_relative 'features/tests_controller' # Loads the controller for unit testing JavaScript.
60
+ ```
61
+
62
+ The `IntegrationTestsRails.setup` method accepts an optional block for further customization. Below is an example how to use and contains the default values:
63
+
64
+ ```ruby
65
+ IntegrationTestsRails.setup do |config|
66
+ config.chrome_url = nil # Used for remote Chrome instances. Needs remote to be true.
67
+ config.max_server_retries = 1000 # Before running the tests, Cuprite starts a server to communicate with Chrome. This sets the maximum number of retries to connect to that server.
68
+ config.puma_threads = '1:1' # Number of threads for the Puma server used by Cuprite.
69
+ config.remote = false # Whether to use a remote Chrome instance.
70
+ config.server_host = '0.0.0.0' # Host for the Puma server used by Cuprite.
71
+ config.server_port = nil # Port for the Puma server used by Cuprite.
72
+ config.source_dir = 'app/javascript' # Directory containing the JavaScript files to be instrumented.
73
+ config.verbose = false # Whether to enable verbose logging.
74
+ config.wait_time = 5 # Max time in seconds to wait after each request by Capybara to load content.
75
+ config.window_size = [1920, 1080] # Size of the browser window used by Cuprite.
76
+ end
77
+ ```
78
+
79
+ ## Unit Testing JavaScript Code
80
+
81
+ ### Usage
82
+
83
+ To unit test JavaScript code, the provided `TestsController (spec/support/features/tests_controller)` can be modified. By default, it only renders a complete HTML page that also loads importmap-supporting JavaScript code to set up the environment for testing.
84
+
85
+ ```ruby
86
+ class TestsController < ActionController::Base
87
+ def index
88
+ render inline: <<~HTML.squish
89
+ <!DOCTYPE html>
90
+ <html lang="en">
91
+ <head>
92
+ <meta charset="UTF-8">
93
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
94
+ <meta name="turbo-visit-control" content="reload">
95
+ <%%= csrf_meta_tags %>
96
+ <%%= csp_meta_tag %>
97
+ <%%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
98
+ <%%= stylesheet_link_tag 'custom', "data-turbo-track": "reload" %>
99
+ <%%= javascript_importmap_tags %>
100
+ </head>
101
+ <body>
102
+ </body>
103
+ </html>
104
+ HTML
105
+ end
106
+ end
107
+ ```
108
+
109
+ Since vendored JavaScript are not included by default, additional tags may be required to load them. For example, if there exists a `custom_code.js` file in `app/javascript`:
110
+
111
+ ```ruby
112
+ <<~HTML.squish
113
+ <!DOCTYPE html>
114
+ <html lang="en">
115
+ <head>
116
+ <meta charset="UTF-8">
117
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
118
+ <meta name="turbo-visit-control" content="reload">
119
+ <%%= csrf_meta_tags %>
120
+ <%%= csp_meta_tag %>
121
+ <%%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
122
+ <%%= stylesheet_link_tag 'custom', "data-turbo-track": "reload" %>
123
+ <%%= javascript_importmap_tags %>
124
+
125
+ <script type="module">
126
+ import CustomCode from 'custom_code';
127
+ window.CustomCode = CustomCode;
128
+ </script>
129
+ </head>
130
+ <body>
131
+ </body>
132
+ </html>
133
+ HTML
134
+ ```
135
+
136
+ ### Writing Tests
137
+
138
+ Tests can be written using RSpec and Capybara as follows:
139
+
140
+ ```ruby
141
+ require 'capybara_helper'
142
+
143
+ RSpec.describe 'Custom Code Unit Test', type: :feature, unit: true do
144
+ describe '#doSomething' do
145
+ let(:script) do
146
+ 'CustomCode.doSomething();'
147
+ end
148
+
149
+ it 'does something' do
150
+ expect(result).to eq('Did something!')
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ The `script` component is the JavaScript code that will be executed for the test. The `result` component will contain the return value of the evaluated script. The `script` can accept string or heredoc formats:
157
+
158
+ ```ruby
159
+ let(:script) do
160
+ <<~JS
161
+ CustomCode.doSomething();
162
+ JS
163
+ end
164
+ ```
165
+
166
+ Do note that **the `script` component can execute one statement only**. If multiple statements are needed, consider wrapping them in a function and invoking that function in the `script` component.
167
+
168
+ ```ruby
169
+ let(:script) do
170
+ <<~JS
171
+ (() => {
172
+ const value1 = CustomCode.getValue1();
173
+ const value2 = CustomCode.getValue2();
174
+ return CustomCode.combineValues(value1, value2);
175
+ })();
176
+ JS
177
+ end
178
+ ```
179
+
180
+ The above will successfully execute the three statements and return the value in `result`. However, this can become a problem if the JavaScript code being tested relies on waiting for each statement to complete. In such cases, it is recommended to use an array instead in `script`:
181
+
182
+ ```ruby
183
+ let(:script) do
184
+ [
185
+ <<~JS,
186
+ CustomCode.initialize();
187
+ CustomCode.doSomething();
188
+ JS
189
+ 'CustomCode.openModal()',
190
+ 'CustomCode.closeModal()'
191
+ ]
192
+ end
193
+ ```
194
+
195
+ In such cases where `script` is an array, the `result` component will contain the return value of the **last statement only**.
196
+
197
+
198
+ ## Integration Testing
199
+
200
+ Refer to [Cuprite](https://github.com/rubycdp/cuprite) and [Capybara](https://github.com/teamcapybara/capybara). Use them as normally in integration tests.
201
+
202
+ ## JavaScript Coverage Reports
203
+
204
+ After the tests (successful, failed or cancelled), coverage reports will be generated in `coverage/javascript` by default.
205
+
206
+ ## Contributing
207
+
208
+ 1. Fork the repository.
209
+ 2. Create a new branch for your feature or bugfix.
210
+ 3. Make your changes.
211
+ 5. Submit a pull request describing your changes.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module IntegrationTestsRails
6
+ module Generators
7
+ # Generator responsible for setting up the Rails project with necessary tools to make integration testing possible.
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ desc 'Initialize project for integration testing.'
12
+
13
+ def install_node_dependencies
14
+ unless system('which yarn > /dev/null 2>&1')
15
+ say 'Yarn is not installed. Please install Yarn first: https://yarnpkg.com/getting-started/install', :red
16
+ exit 1
17
+ end
18
+
19
+ say 'Installing Istanbul...', :green
20
+ run 'yarn add --dev istanbul-lib-instrument istanbul-lib-coverage istanbul-lib-report istanbul-reports'
21
+ run 'yarn install'
22
+ end
23
+
24
+ def copy_tests_controller
25
+ template 'tests_controller.rb', 'spec/support/features/tests_controller.rb'
26
+ end
27
+
28
+ def add_route
29
+ route 'resources(:tests, only: :index) if Rails.env.test?'
30
+ end
31
+
32
+ def update_gitignore
33
+ gitignore_path = '.gitignore'
34
+ lines_to_add = ['node_modules/', 'coverage/']
35
+
36
+ return unless File.exist?(gitignore_path)
37
+
38
+ content = File.read(gitignore_path)
39
+ lines_to_add.each do |line|
40
+ if content.include?(line)
41
+ say "'#{line}' already exists in .gitignore", :blue
42
+ else
43
+ append_to_file gitignore_path, "\n#{line}\n"
44
+ say "Added '#{line}' to .gitignore", :green
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This provides a minimal page that loads your JavaScript
4
+ class TestsController < ActionController::Base
5
+ def index
6
+ render inline: <<~HTML.squish
7
+ <!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <meta name="turbo-visit-control" content="reload">
13
+ <%%= csrf_meta_tags %>
14
+ <%%= csp_meta_tag %>
15
+ <%%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
16
+ <%%= stylesheet_link_tag 'custom', "data-turbo-track": "reload" %>
17
+ <%%= javascript_importmap_tags %>
18
+ <!-- If there are JavaScript libraries not globally available, include them here for testing.-->
19
+ <!-- E.g. The block below shows how to import a JavaScript module and attach it to the window object. -->
20
+ <!-- The file is located in app/javascripts/libs/my_library.js -->
21
+ <!--
22
+ <script type="module">
23
+ import MyLibrary from 'libs/my_library';
24
+ window.MyLibrary = MyLibrary;
25
+ </script>
26
+ -->
27
+ </head>
28
+ <body>
29
+ <!-- Include JavaScript libraries here instead if they need to be loaded much later. -->
30
+ <!-- E.g. The line below loads a JavaScript file located in app/assets/javascripts/plugins/vendor.min.js -->
31
+ <%%#= javascript_include_tag 'plugins/vendor.min' %>
32
+ </body>
33
+ </html>
34
+ HTML
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntegrationTestsRails
4
+ module Capybara
5
+ # Adds helpers to enable unit testing.
6
+ module Helper
7
+ extend RSpec::SharedContext
8
+
9
+ let(:result) do
10
+ case script
11
+ when Array
12
+ script.map { |cmd| page.evaluate_script(cmd) }.last
13
+ when String
14
+ page.evaluate_script(script)
15
+ end
16
+ end
17
+
18
+ let(:script) { nil }
19
+
20
+ before do
21
+ visit tests_path
22
+ result
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntegrationTestsRails
4
+ module Capybara
5
+ # Configure Capybara to use local Chrome browser via Cuprite.
6
+ module Local
7
+ class << self
8
+ def setup
9
+ config = IntegrationTestsRails.configuration
10
+
11
+ ::Capybara.default_max_wait_time = config.wait_time
12
+ ::Capybara.server = :puma, { Silent: !Util.verbose? }
13
+
14
+ register_driver
15
+ Util.log 'Local Chrome Mode: Server is running locally'
16
+ end
17
+
18
+ private
19
+
20
+ def register_driver
21
+ config = IntegrationTestsRails.configuration
22
+
23
+ ::Capybara.register_driver(:cuprite) do |app|
24
+ options = {
25
+ window_size: config.window_size,
26
+ browser_options: {
27
+ 'no-sandbox' => nil,
28
+ 'disable-dev-shm-usage' => nil,
29
+ 'disable-web-security' => nil,
30
+ 'disable-gpu' => nil
31
+ }
32
+ }
33
+
34
+ Util.log 'Registered Cuprite driver using local configuration'
35
+ ::Capybara::Cuprite::Driver.new(app, options)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntegrationTestsRails
4
+ module Capybara
5
+ # Configure Capybara to use remote Chrome browser via Cuprite.
6
+ module Remote
7
+ class << self
8
+ def setup
9
+ config = IntegrationTestsRails.configuration
10
+ server_host = config.server_host
11
+ server_port = config.server_port
12
+
13
+ ::Capybara.server_host = server_host
14
+ ::Capybara.server_port = server_port
15
+ ::Capybara.default_max_wait_time = config.wait_time
16
+ ::Capybara.app_host = "http://localhost:#{server_port}"
17
+ ::Capybara.server = :puma, {
18
+ Silent: !Util.verbose?,
19
+ Host: server_host,
20
+ Port: server_port,
21
+ Threads: config.puma_threads
22
+ }
23
+
24
+ register_driver
25
+ Util.log "Remote Chrome Mode: Test server bound to #{server_host}:#{server_port}"
26
+ end
27
+
28
+ private
29
+
30
+ def register_driver
31
+ config = IntegrationTestsRails.configuration
32
+
33
+ ::Capybara.register_driver(:cuprite) do |app|
34
+ timeout = config.timeout
35
+ options = {
36
+ window_size: config.window_size,
37
+ url: config.chrome_url,
38
+ timeout: timeout,
39
+ process_timeout: timeout
40
+ }
41
+
42
+ Util.log 'Registered Cuprite driver using remote configuration'
43
+ ::Capybara::Cuprite::Driver.new(app, options)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers'
4
+
5
+ module IntegrationTestsRails
6
+ module Capybara
7
+ # Utilities for Capybara setup and configuration are found here.
8
+ module Util
9
+ class << self
10
+ def configure_webmock
11
+ WebMock.disable_net_connect!(allow_localhost: true)
12
+ log 'WebMock configured to allow localhost connections'
13
+ end
14
+
15
+ def ensure_server_ready(context)
16
+ return if @server_ready
17
+
18
+ config = IntegrationTestsRails.configuration
19
+ log "Waiting for server on #{::Capybara.app_host.presence || 'localhost'} to start..."
20
+
21
+ server_retries = config.max_server_retries
22
+ server_retries.times do |attempt|
23
+ context.visit('/400')
24
+ @server_ready = true
25
+ log 'Server is ready!'
26
+ break
27
+ rescue StandardError
28
+ log "Server not ready (attempt #{attempt + 1}/#{server_retries})."
29
+ end
30
+ log "Server did not start after #{server_retries} attempts..." unless @server_ready
31
+ end
32
+
33
+ def configure_rspec
34
+ RSpec.configure do |config|
35
+ config.before(:each, type: :feature) do
36
+ ::Capybara.current_driver = ::Capybara.javascript_driver
37
+ IntegrationTestsRails::Capybara::Util.ensure_server_ready(self)
38
+ end
39
+
40
+ config.include(Helper, type: :feature, unit: true)
41
+ end
42
+ end
43
+
44
+ def verbose?
45
+ IntegrationTestsRails.configuration.verbose
46
+ end
47
+
48
+ def log(message)
49
+ puts "[CAPYBARA] #{message}" if verbose?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/cuprite'
4
+ require_relative 'capybara/util'
5
+ require_relative 'capybara/remote'
6
+ require_relative 'capybara/local'
7
+
8
+ module IntegrationTestsRails
9
+ # This contains the Capybara setup and configuration.
10
+ module Capybara
11
+ class << self
12
+ def setup
13
+ config = IntegrationTestsRails.configuration
14
+
15
+ ::Capybara.javascript_driver = :cuprite
16
+
17
+ if config.remote
18
+ Remote.setup
19
+ else
20
+ Local.setup
21
+ end
22
+
23
+ Util.configure_rspec
24
+ Util.configure_webmock
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntegrationTestsRails
4
+ # Configuration class for this gem to modify adjustable settings for Capybara, Cuprite and Istanbul.
5
+ class Configuration
6
+ attr_accessor :source_dir, :output_dir, :backup_dir, :coverage_path, :wait_time, :remote, :chrome_url,
7
+ :verbose, :timeout, :server_host, :server_port, :puma_threads, :window_size, :max_server_retries
8
+
9
+ def initialize
10
+ @backup_dir = 'tmp/js_backup'
11
+ @chrome_url = nil
12
+ @coverage_path = 'coverage/nyc'
13
+ @max_server_retries = 1000
14
+ @output_dir = 'tmp/instrumented_js'
15
+ @puma_threads = '1:1'
16
+ @remote = false
17
+ @server_host = '0.0.0.0' # rubocop:disable Style/IpAddresses
18
+ @server_port = nil
19
+ @source_dir = 'app/javascript'
20
+ @timeout = 30
21
+ @verbose = false
22
+ @wait_time = 5
23
+ @window_size = [1920, 1080]
24
+ end
25
+
26
+ def source_path
27
+ Rails.root.join(source_dir)
28
+ end
29
+
30
+ def output_path
31
+ Rails.root.join(output_dir)
32
+ end
33
+
34
+ def backup_path
35
+ Rails.root.join(backup_dir)
36
+ end
37
+
38
+ def coverage_dir
39
+ Rails.root.join(coverage_path)
40
+ end
41
+
42
+ def coverage_file
43
+ coverage_dir.join('coverage.json')
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'pathname'
6
+ require 'shellwords'
7
+
8
+ module IntegrationTestsRails
9
+ module Istanbul
10
+ # Collects reports and manages instrumented files.
11
+ module Collector
12
+ class << self
13
+ def setup
14
+ config = IntegrationTestsRails.configuration
15
+
16
+ # Instrument files
17
+ Instrumenter.instrument_all
18
+
19
+ # Backup and replace original files
20
+ backup_and_replace_files
21
+
22
+ # Clean previous coverage data
23
+ coverage_dir = config.coverage_dir
24
+ FileUtils.rm_rf(coverage_dir)
25
+ FileUtils.mkdir_p(coverage_dir)
26
+ end
27
+
28
+ def collect(page)
29
+ coverage_data = page.evaluate_script('window.__coverage__')
30
+ save_coverage_snapshot(coverage_data) if coverage_data.present?
31
+ rescue StandardError => e
32
+ Util.log "Coverage collection failed: #{e.message}"
33
+ end
34
+
35
+ def generate_report
36
+ # Generate report using Node.js (will merge all snapshot files)
37
+ report_script = build_report_script
38
+ output = `node -e #{Shellwords.escape(report_script)} 2>&1`.strip
39
+
40
+ # Parse and display coverage summary
41
+ return if output.blank?
42
+
43
+ begin
44
+ data = JSON.parse(output)
45
+ puts "\nJavaScript Coverage: #{data['covered']} / #{data['total']} LOC (#{data['pct']}%) covered."
46
+ puts "Coverage report: coverage/javascript/index.html\n"
47
+ rescue JSON::ParserError
48
+ puts output
49
+ end
50
+ end
51
+
52
+ def restore_original_files
53
+ config = IntegrationTestsRails.configuration
54
+
55
+ backup_dir = config.backup_path
56
+ source_dir = config.source_path
57
+
58
+ return unless Dir.exist?(backup_dir)
59
+
60
+ FileUtils.rm_rf(source_dir)
61
+ FileUtils.cp_r(backup_dir, source_dir)
62
+ Util.log '✓ Restored original JavaScript files'
63
+ end
64
+
65
+ private
66
+
67
+ def backup_and_replace_files
68
+ config = IntegrationTestsRails.configuration
69
+ source_path = config.source_path
70
+ backup_path = config.backup_path
71
+ output_path = config.output_path
72
+
73
+ # Backup originals
74
+ FileUtils.rm_rf(backup_path)
75
+ FileUtils.cp_r(source_path, backup_path)
76
+
77
+ # Replace with instrumented
78
+ Dir.glob(output_path.join('**/*.js')).each do |instrumented_file|
79
+ relative_path = Pathname.new(instrumented_file).relative_path_from(output_path)
80
+ target_file = source_path.join(relative_path)
81
+ FileUtils.cp(instrumented_file, target_file)
82
+ end
83
+ end
84
+
85
+ def save_coverage_snapshot(coverage_data)
86
+ config = IntegrationTestsRails.configuration
87
+
88
+ snapshot_file = config.coverage_dir.join("js-#{Time.now.to_f.to_s.tr('.', '-')}.json")
89
+ File.write(snapshot_file, JSON.pretty_generate(coverage_data))
90
+ end
91
+
92
+ def build_report_script
93
+ config = IntegrationTestsRails.configuration
94
+
95
+ <<~JS
96
+ const libCoverage = require('istanbul-lib-coverage');
97
+ const libReport = require('istanbul-lib-report');
98
+ const reports = require('istanbul-reports');
99
+ const fs = require('fs');
100
+ const path = require('path');
101
+
102
+ const coverageDir = '#{config.coverage_dir}';
103
+ const files = fs.readdirSync(coverageDir).filter(f => f.startsWith('js-') && f.endsWith('.json'));
104
+
105
+ const coverageMap = libCoverage.createCoverageMap();
106
+
107
+ files.forEach(file => {
108
+ const filePath = path.join(coverageDir, file);
109
+ const coverage = JSON.parse(fs.readFileSync(filePath, 'utf8'));
110
+ coverageMap.merge(coverage);
111
+ });
112
+
113
+ // Add all instrumented files (even those with 0% coverage)
114
+ function findJsFiles(dir, fileList = []) {
115
+ const items = fs.readdirSync(dir);
116
+ items.forEach(item => {
117
+ const fullPath = path.join(dir, item);
118
+ const stat = fs.statSync(fullPath);
119
+ if (stat.isDirectory()) {
120
+ findJsFiles(fullPath, fileList);
121
+ } else if (item.endsWith('.js')) {
122
+ fileList.push(fullPath);
123
+ }
124
+ });
125
+ return fileList;
126
+ }
127
+
128
+ const instrumentedDir = '#{config.output_path}';
129
+ const instrumentedFiles = findJsFiles(instrumentedDir);
130
+
131
+ instrumentedFiles.forEach(instrumentedFile => {
132
+ const relativePath = path.relative(instrumentedDir, instrumentedFile);
133
+ const originalFile = path.join('#{config.source_path}', relativePath);
134
+
135
+ if (coverageMap.data[originalFile]) return;
136
+
137
+ try {
138
+ const code = fs.readFileSync(instrumentedFile, 'utf8');
139
+ const match = code.match(/var coverageData = (\\{[\\s\\S]+?\\});/);
140
+
141
+ if (match && match[1]) {
142
+ const coverageData = eval('(' + match[1] + ')');
143
+ coverageData.path = originalFile;
144
+ coverageMap.addFileCoverage(coverageData);
145
+ }
146
+ } catch(e) {
147
+ // Skip files that can't be parsed
148
+ }
149
+ });
150
+
151
+ const context = libReport.createContext({
152
+ dir: 'coverage/javascript',
153
+ coverageMap: coverageMap
154
+ });
155
+
156
+ ['html', 'lcov', 'cobertura'].forEach(reportType => {
157
+ const report = reports.create(reportType, {});
158
+ report.execute(context);
159
+ });
160
+
161
+ const summary = coverageMap.getCoverageSummary();
162
+ console.log(JSON.stringify({
163
+ covered: summary.lines.covered,
164
+ total: summary.lines.total,
165
+ pct: summary.lines.pct.toFixed(2)
166
+ }));
167
+ JS
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'pathname'
6
+ require 'shellwords'
7
+
8
+ module IntegrationTestsRails
9
+ module Istanbul
10
+ # Instruments JavaScript files for code coverage using Istanbul.
11
+ module Instrumenter
12
+ class << self
13
+ def instrument_all
14
+ config = IntegrationTestsRails.configuration
15
+ output_path = config.output_path
16
+
17
+ # Clean output directory
18
+ FileUtils.rm_rf(output_path)
19
+ FileUtils.mkdir_p(output_path)
20
+
21
+ # Find all JS files
22
+ js_files = Dir.glob(config.source_path.join('**/*.js'))
23
+ Util.log "Instrumenting #{js_files.length} JavaScript files..."
24
+
25
+ js_files.each do |file|
26
+ instrument_file(file)
27
+ end
28
+
29
+ Util.log '✓ Instrumented files created'
30
+ Util.log '=== Istanbul Instrumentation Complete ==='
31
+ end
32
+
33
+ def instrument_file(file)
34
+ config = IntegrationTestsRails.configuration
35
+
36
+ relative_path = Pathname.new(file).relative_path_from(config.source_path)
37
+ output_file = config.output_path.join(relative_path)
38
+
39
+ FileUtils.mkdir_p(output_file.dirname)
40
+
41
+ # Use Node.js to instrument the file
42
+ code = File.read(file)
43
+ escaped_code = JSON.generate(code)
44
+ escaped_file = JSON.generate(file.to_s)
45
+
46
+ js_command = <<~JS
47
+ const { createInstrumenter } = require('istanbul-lib-instrument');
48
+ const instrumenter = createInstrumenter({ esModules: true, compact: false });
49
+ const code = #{escaped_code};
50
+ const filename = #{escaped_file};
51
+ console.log(instrumenter.instrumentSync(code, filename));
52
+ JS
53
+
54
+ instrumented = `node -e #{Shellwords.escape(js_command)}`.strip
55
+
56
+ if $CHILD_STATUS.success?
57
+ File.write(output_file, instrumented)
58
+ else
59
+ warn "Failed to instrument #{relative_path}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntegrationTestsRails
4
+ module Istanbul
5
+ # Utilities for Istanbul setup and configuration are found here.
6
+ module Util
7
+ class << self
8
+ def configure_rspec
9
+ RSpec.configure do |config|
10
+ config.before(:suite) do
11
+ Instrumenter.instrument_all
12
+ Collector.setup
13
+ end
14
+
15
+ config.after(:each, type: :feature) do
16
+ Collector.collect(page)
17
+ end
18
+ end
19
+ end
20
+
21
+ def log(message)
22
+ return unless verbose?
23
+
24
+ puts "[ISTANBUL] #{message}"
25
+ end
26
+
27
+ private
28
+
29
+ def verbose?
30
+ IntegrationTestsRails.configuration&.verbose
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'istanbul/instrumenter'
4
+ require_relative 'istanbul/collector'
5
+ require_relative 'istanbul/util'
6
+
7
+ module IntegrationTestsRails
8
+ # This contains the Istanbul setup and configuration.
9
+ module Istanbul
10
+ class << self
11
+ def setup
12
+ Util.configure_rspec
13
+ end
14
+ end
15
+
16
+ # Ensure cleanup at exit, either success, failure or cancellation.
17
+ at_exit do
18
+ Collector.generate_report
19
+ Collector.restore_original_files
20
+ rescue StandardError => e
21
+ warn "Istanbul cleanup failed: #{e.message}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntegrationTestsRails
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'integration_tests_rails/version'
4
+ require_relative 'integration_tests_rails/configuration'
5
+ require_relative 'integration_tests_rails/istanbul'
6
+ require_relative 'integration_tests_rails/capybara'
7
+
8
+ # The main module for the IntegrationTestsRails gem.
9
+ module IntegrationTestsRails
10
+ class << self
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield(configuration)
17
+ end
18
+
19
+ def reset_configuration!
20
+ @configuration = Configuration.new
21
+ end
22
+
23
+ # Convenience method to set up everything at once
24
+ def setup
25
+ yield(configuration) if block_given?
26
+
27
+ Capybara.setup
28
+ Istanbul.setup
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: integration-tests-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tien
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: capybara
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: cuprite
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec-rails
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: webmock
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ email:
83
+ - tieeeeen1994@gmail.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - MIT-LICENSE
89
+ - README.md
90
+ - lib/generators/integration_tests_rails/install_generator.rb
91
+ - lib/generators/integration_tests_rails/templates/tests_controller.rb
92
+ - lib/integration_tests_rails.rb
93
+ - lib/integration_tests_rails/capybara.rb
94
+ - lib/integration_tests_rails/capybara/helpers.rb
95
+ - lib/integration_tests_rails/capybara/local.rb
96
+ - lib/integration_tests_rails/capybara/remote.rb
97
+ - lib/integration_tests_rails/capybara/util.rb
98
+ - lib/integration_tests_rails/configuration.rb
99
+ - lib/integration_tests_rails/istanbul.rb
100
+ - lib/integration_tests_rails/istanbul/collector.rb
101
+ - lib/integration_tests_rails/istanbul/instrumenter.rb
102
+ - lib/integration_tests_rails/istanbul/util.rb
103
+ - lib/integration_tests_rails/version.rb
104
+ homepage: https://github.com/tieeeeen1994/integration-tests-rails
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ homepage_uri: https://github.com/tieeeeen1994/integration-tests-rails
109
+ source_code_uri: https://github.com/tieeeeen1994/integration-tests-rails
110
+ rubygems_mfa_required: 'true'
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 3.4.0
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 3.6.9
126
+ specification_version: 4
127
+ summary: Integration Testing for Rails applications using Istanbul, Cuprite, Capybara
128
+ and RSpec specifically.
129
+ test_files: []