├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── activeadmin_assets.gemspec ├── bin ├── console ├── setup └── tailwindcss ├── lib ├── activeadmin_assets.rb └── activeadmin_assets │ ├── compatibility_check.rb │ ├── middleware.rb │ ├── path.rb │ ├── railtie.rb │ ├── url_patch.rb │ └── version.rb ├── spec ├── activeadmin_assets │ ├── compatibility_check_spec.rb │ ├── middleware_spec.rb │ ├── path_spec.rb │ ├── url_patch_spec.rb │ └── version_spec.rb ├── screenshots │ ├── linux │ │ ├── dashboard.png │ │ ├── resource_form.png │ │ ├── resource_index.png │ │ └── resource_show.png │ └── macos │ │ ├── dashboard.png │ │ ├── resource_form.png │ │ ├── resource_index.png │ │ └── resource_show.png ├── spec_helper.rb ├── support │ ├── capybara_setup.rb │ └── coverage_setup.rb ├── system │ ├── css_spec.rb │ └── js_spec.rb └── templates │ ├── app_template.rb │ ├── dashboard_template.rb │ └── user_resource_template.rb └── tasks ├── benchmark.rake ├── css.rake ├── css ├── entrypoint.css └── tailwind.config.js ├── generate_spec_app.rake └── js.rake /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.1.0' 18 | - '3.4.0' 19 | 20 | steps: 21 | - uses: awalsh128/cache-apt-pkgs-action@latest 22 | with: 23 | packages: libvips libglib2.0-0 libglib2.0-dev libwebp-dev libvips42 libpng-dev 24 | version: tests-v1 25 | - uses: nanasess/setup-chromedriver@v2 26 | - uses: actions/checkout@v4 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true 32 | - name: Run the default task 33 | run: bundle exec rake 34 | - uses: codecov/codecov-action@v4.0.1 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | - uses: actions/upload-artifact@v4 38 | if: always() 39 | with: 40 | name: screenshots-${{ matrix.ruby }} 41 | retention-days: 1 42 | path: 'spec/screenshots' 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /lib/activeadmin_assets/assets 10 | .rspec_status 11 | Gemfile.lock 12 | spec/dummy 13 | spec/screenshots/**/*diff.png 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [1.0.2] - 2024-09-17 4 | 5 | - Don't raise if inexistent asset is accessed, just log a warning 6 | 7 | ## [1.0.1] - 2024-07-14 8 | 9 | - Actually include assets in gem 😅 10 | 11 | ## [1.0.0] - 2024-07-14 12 | 13 | - Initial release (yanked) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in activeadmin_assets.gemspec 6 | gemspec 7 | 8 | group :development, :test do 9 | gem 'activeadmin', '~> 4.0.0.beta12' 10 | gem 'benchmark-ips' 11 | gem 'capybara', '~> 3.0' 12 | gem 'capybara-screenshot-diff', git: 'https://github.com/donv/capybara-screenshot-diff', require: 'capybara_screenshot_diff/rspec' 13 | gem 'chunky_png' 14 | gem 'csv' # needed for activeadmin, standalone on Ruby >= 3.4 15 | gem 'debug' 16 | gem 'puma', '~> 6.0' 17 | gem 'rake', '~> 13.2' 18 | gem 'rspec-rails', '~> 6.0' 19 | gem 'ruby-vips' 20 | gem 'selenium-webdriver', '~> 4.22' 21 | gem 'simplecov-cobertura', require: false 22 | gem 'sqlite3', '~> 2.0' 23 | gem 'tailwindcss-rails', '~> 2.6', require: false 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Janosch Müller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/activeadmin_assets.svg)](http://badge.fury.io/rb/activeadmin_assets) 2 | [![Build Status](https://github.com/jaynetics/activeadmin_assets/actions/workflows/main.yml/badge.svg)](https://github.com/jaynetics/activeadmin_assets/actions) 3 | [![Coverage](https://codecov.io/github/jaynetics/activeadmin_assets/graph/badge.svg?token=7fCHVrCeFv)](https://codecov.io/github/jaynetics/activeadmin_assets) 4 | 5 | # ActiveAdminAssets 6 | 7 | This gem is for you if you want to be able to run [ActiveAdmin](https://github.com/activeadmin/activeadmin) v4+ without any asset setup, e.g.: 8 | 9 | - no `cssbundling-rails` or `tailwindcss-rails` 10 | - no `sprockets` or `propshaft` 11 | - no `assets:precompile` or similar build steps 12 | 13 | Like the asset gems of old, it includes static copies of all required assets and injects them automatically. 14 | 15 | ## Caveats 16 | 17 | - This will prevent you from customizing ActiveAdmin's tailwind config, making theming more hacky. 18 | - This will prevent you from using tailwind classes that are not used by ActiveAdmin itself. 19 | - This might break with ActiveAdmin updates, though I don't consider it likely so its not version-locked yet. 20 | 21 | ## Installation 22 | 23 | Add `activeadmin_assets` to your Gemfile. 24 | 25 | ## Usage 26 | 27 | That's it 😁. If you want, you can configure the path to serve static assets from: 28 | 29 | ```ruby 30 | ActiveAdminAssets.path = '/x/admin-assets' # default: '/active_admin_assets' 31 | ``` 32 | 33 | ## Contributing 34 | 35 | Bug reports and pull requests are welcome on GitHub at https://github.com/jaynetics/activeadmin_assets. 36 | 37 | ## License 38 | 39 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | Dir['tasks/**/*.rake'].each { |file| load(file) } 7 | 8 | RSpec::Core::RakeTask.new(:spec) 9 | 10 | task default: [:css, :js, :generate_spec_app, :spec] 11 | 12 | # ensure fresh assets are included when packaging the gem 13 | task build: %i[css js] 14 | -------------------------------------------------------------------------------- /activeadmin_assets.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/activeadmin_assets/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'activeadmin_assets' 7 | spec.version = ActiveAdminAssets::VERSION 8 | spec.authors = ['Janosch Müller'] 9 | spec.email = ['janosch84@gmail.com'] 10 | 11 | spec.summary = 'Run ActiveAdmin v4 without asset setup.' 12 | spec.homepage = 'https://github.com/jaynetics/activeadmin_assets' 13 | spec.license = 'MIT' 14 | spec.required_ruby_version = '>= 3.1.0' 15 | 16 | spec.metadata['homepage_uri'] = spec.homepage 17 | spec.metadata['source_code_uri'] = spec.homepage 18 | spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md" 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | gemspec = File.basename(__FILE__) 23 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 24 | ls.readlines("\x0", chomp: true).reject do |f| 25 | (f == gemspec) || f.start_with?(*%w[bin/ spec/ tasks/ .git .github Gemfile]) 26 | end 27 | end 28 | spec.files += Dir['lib/activeadmin_assets/assets/**/*.{br,css,gz,js,map}'] 29 | spec.require_paths = ['lib'] 30 | 31 | spec.add_dependency 'activeadmin', '>= 4.0.0.beta7', '< 5.0.0' 32 | spec.add_dependency 'importmap-rails', '>= 2.0.0' 33 | end 34 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "activeadmin_assets" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/tailwindcss: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'tailwindcss' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("tailwindcss-rails", "tailwindcss") 28 | -------------------------------------------------------------------------------- /lib/activeadmin_assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir[File.join(__dir__, "activeadmin_assets", "*.rb")].each { |f| require f } 4 | -------------------------------------------------------------------------------- /lib/activeadmin_assets/compatibility_check.rb: -------------------------------------------------------------------------------- 1 | module ActiveAdminAssets 2 | # Checks if custom/manual asset provisioning is in place. 3 | # To be more thorough, this could also check for usage of new tailwind classes 4 | # in any admin controllers or partials, though that might be a bit expensive. 5 | module CompatibilityCheck 6 | def self.call(app) 7 | return unless %w[development test].include?(ENV['RAILS_ENV']) 8 | 9 | customizations = Dir[app.root.join('app/assets/{builds,stylesheets}/active_admin.*')] 10 | customizations.any? and warn ""\ 11 | "The activeadmin_assets gem is not compatible with providing core "\ 12 | "activeadmin assets yourself. Please remove either the gem "\ 13 | "or your custom files: #{customizations}" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/activeadmin_assets/middleware.rb: -------------------------------------------------------------------------------- 1 | module ActiveAdminAssets 2 | # Catches active admin asset requests and serves them from the gem. 3 | class Middleware 4 | def initialize(app, *) 5 | @app = app 6 | @regexp = /\A#{ActiveAdminAssets.path}(.+)/ 7 | end 8 | 9 | def call(env) 10 | serve(env[Rack::PATH_INFO]) || @app.call(env) 11 | end 12 | 13 | def serve(path) 14 | return unless asset_path = path[@regexp, 1] 15 | 16 | static_path = File.join(__dir__, 'assets', "#{asset_path}.gz") 17 | send_data(static_path) 18 | end 19 | 20 | # This could be made more efficient with sendfile, 21 | # perhaps after checking config.action_dispatch.x_sendfile_header 22 | def send_data(path) 23 | data = File.read(path) 24 | headers = { 25 | 'cache-control' => 'public, max-age=86400', 26 | 'content-encoding' => 'gzip', 27 | 'content-length' => data.bytesize.to_s, 28 | 'content-type' => path['.css'] ? 'text/css' : 'text/javascript', 29 | } 30 | [200, headers, [data]] 31 | rescue Errno::ENOENT => e 32 | Rails.logger.warn("ActiveAdminAssets::Middleware: #{e.class} #{e.message}") 33 | nil 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/activeadmin_assets/path.rb: -------------------------------------------------------------------------------- 1 | require 'active_admin/version' 2 | 3 | module ActiveAdminAssets 4 | # Add VERSIONs to invalidate browser caches on gem updates. 5 | def self.path 6 | "#{@path || '/active_admin_assets'}/#{ActiveAdmin::VERSION}/#{VERSION}/" 7 | end 8 | 9 | def self.path=(arg) 10 | @path = arg && !arg.empty? && "/#{arg.gsub(%r{\A/|/\z}, '')}" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/activeadmin_assets/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | require 'rails/engine/railties' 3 | 4 | module ActiveAdminAssets 5 | class Railtie < ::Rails::Railtie 6 | initializer 'activeadmin_assets.initializer' do |app| 7 | ActiveAdminAssets::CompatibilityCheck.call(app) 8 | app.middleware.insert(0, ActiveAdminAssets::Middleware) 9 | end 10 | 11 | ActiveSupport.on_load(:active_admin_controller) do 12 | ActiveAdmin::LayoutHelper.prepend(ActiveAdminAssets::URLPatch) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/activeadmin_assets/url_patch.rb: -------------------------------------------------------------------------------- 1 | module ActiveAdminAssets 2 | # Modify asset URLs for active admins assets: 3 | # - prepend special path (includes VERSIONs to bust caches on gem updates) 4 | # - prevent sprockets or propshaft from adding digests to the asset URLs 5 | module URLPatch 6 | def stylesheet_link_tag(path, ...) 7 | return super unless active_admin_asset?(path) 8 | 9 | tag.link(rel: :stylesheet, href: path_to_asset("#{path.chomp('.css')}.css")) 10 | end 11 | 12 | # this is called by importmap-rails, too 13 | def path_to_asset(path, ...) 14 | return super unless active_admin_asset?(path) 15 | 16 | path.sub(%r{\A/?}, ActiveAdminAssets.path) 17 | end 18 | 19 | def active_admin_asset?(path) 20 | %r{\A/?(?:active_admin|flowbite|rails_ujs_esm)(?:[/.]|\z)}.match?(path) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/activeadmin_assets/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveAdminAssets 4 | VERSION = '1.0.2' 5 | end 6 | -------------------------------------------------------------------------------- /spec/activeadmin_assets/compatibility_check_spec.rb: -------------------------------------------------------------------------------- 1 | describe ActiveAdminAssets::CompatibilityCheck do 2 | it 'warns if setup is found that is not compatible with ActiveAdminAssets' do 3 | # the dummy app has active_admin.css, generated by activeadmin:install 4 | expect { described_class.call(Rails.application) }.to output(/not compatible/).to_stderr 5 | end 6 | 7 | it 'does nothing outside development/test' do 8 | original_env = ENV['RAILS_ENV'] 9 | ENV['RAILS_ENV'] = 'superproduction' 10 | expect { described_class.call(Rails.application) }.not_to output.to_stderr 11 | ensure 12 | ENV['RAILS_ENV'] = original_env 13 | end 14 | 15 | it 'does nothing if no incompatible setup is found' do 16 | app = instance_double(Rails::Application, root: Pathname.new(__dir__)) 17 | expect { described_class.call(app) }.not_to output.to_stderr 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/activeadmin_assets/middleware_spec.rb: -------------------------------------------------------------------------------- 1 | describe ActiveAdminAssets::Middleware do 2 | it 'logs a warning when an asset is not found' do 3 | env = { Rack::PATH_INFO => "#{ActiveAdminAssets.path}/unknown" } 4 | app = ->* { [ 404, {}, 'nada'] } 5 | 6 | expect(Rails.logger).to receive(:warn) 7 | expect(described_class.new(app).call(env)).to eq [404, {}, 'nada'] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/activeadmin_assets/path_spec.rb: -------------------------------------------------------------------------------- 1 | describe ActiveAdminAssets, '.path' do 2 | before do 3 | stub_const('ActiveAdmin::VERSION', 'v1') 4 | stub_const('ActiveAdminAssets::VERSION', 'v2') 5 | end 6 | 7 | it 'includes version numbers' do 8 | expect(ActiveAdminAssets.path).to eq('/active_admin_assets/v1/v2/') 9 | end 10 | 11 | it 'can be changed, keeping the format and version numbers' do 12 | { 13 | 'a' => '/a/v1/v2/', 14 | '/b' => '/b/v1/v2/', 15 | 'c/' => '/c/v1/v2/', 16 | '/d/' => '/d/v1/v2/', 17 | 'e/f' => '/e/f/v1/v2/', 18 | '/g/h' => '/g/h/v1/v2/', 19 | '/i/j/' => '/i/j/v1/v2/', 20 | '' => '/active_admin_assets/v1/v2/', 21 | nil => '/active_admin_assets/v1/v2/', 22 | }.each do |setting, expected_result| 23 | ActiveAdminAssets.path = setting 24 | expect(ActiveAdminAssets.path).to eq(expected_result) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/activeadmin_assets/url_patch_spec.rb: -------------------------------------------------------------------------------- 1 | describe ActiveAdminAssets::URLPatch do 2 | it 'applies to the active_admin stylesheet' do 3 | result = render { stylesheet_link_tag 'active_admin' } 4 | expect(result).to start_with('') 26 | ensure 27 | ActiveAdmin.importmap.packages.delete('my_own_thing') 28 | end 29 | 30 | def render(&block) 31 | context = Class.new(ActionController::Base) 32 | context.helper(described_class) 33 | context.helpers.instance_exec(&block) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/activeadmin_assets/version_spec.rb: -------------------------------------------------------------------------------- 1 | describe ActiveAdminAssets, '::VERSION' do 2 | it 'exists' do 3 | expect(ActiveAdminAssets::VERSION).to match(/\d/) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/screenshots/linux/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/linux/dashboard.png -------------------------------------------------------------------------------- /spec/screenshots/linux/resource_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/linux/resource_form.png -------------------------------------------------------------------------------- /spec/screenshots/linux/resource_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/linux/resource_index.png -------------------------------------------------------------------------------- /spec/screenshots/linux/resource_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/linux/resource_show.png -------------------------------------------------------------------------------- /spec/screenshots/macos/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/macos/dashboard.png -------------------------------------------------------------------------------- /spec/screenshots/macos/resource_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/macos/resource_form.png -------------------------------------------------------------------------------- /spec/screenshots/macos/resource_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/macos/resource_index.png -------------------------------------------------------------------------------- /spec/screenshots/macos/resource_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaynetics/activeadmin_assets/84eecc0c765ecba04d2f2fc68133c015f24f81a0/spec/screenshots/macos/resource_show.png -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'support/coverage_setup' 4 | 5 | ENV['RAILS_ENV'] ||= 'test' 6 | require File.expand_path('dummy/config/environment', __dir__) 7 | require 'rspec/rails' 8 | require_relative 'support/capybara_setup' 9 | 10 | RSpec.configure do |config| 11 | config.use_transactional_fixtures = true 12 | config.infer_spec_type_from_file_location! 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/capybara_setup.rb: -------------------------------------------------------------------------------- 1 | require 'selenium/webdriver' 2 | 3 | Capybara.server = :puma, { Silent: true } 4 | 5 | Capybara.register_driver :customized_chrome do |app| 6 | version = Capybara::Selenium::Driver.load_selenium 7 | options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options 8 | options = Selenium::WebDriver::Chrome::Options.new.tap do |opts| 9 | opts.add_argument('--headless') # note: screenshot size differs if not headless 10 | # https://github.com/SeleniumHQ/selenium/issues/14453 11 | opts.add_argument('--disable-search-engine-choice-screen') 12 | # prevent different screenshot resolution on retina devices 13 | opts.add_argument('--force-device-scale-factor=1') 14 | # various fixes for GH actions 15 | opts.add_argument('--disable-dev-shm-usage') 16 | opts.add_argument('--disable-extensions') 17 | opts.add_argument('--disable-infobars') 18 | opts.add_argument('--disable-notifications') 19 | opts.add_argument('--disable-site-isolation-trials') 20 | opts.add_argument('--remote-debugging-pipe') 21 | end 22 | # prevent top info bar about automation which changes height by 139px, grrr 23 | options.exclude_switches = [*options.exclude_switches, 'enable-automation'] 24 | $selenium_driver = Capybara::Selenium::Driver.new(app, **{ browser: :chrome, options_key => options }) 25 | end 26 | 27 | Capybara::Screenshot.add_os_path = true 28 | Capybara::Screenshot.save_path = File.expand_path('../screenshots', __dir__) 29 | Capybara::Screenshot::Diff.driver = :vips 30 | Capybara::Screenshot::Diff.fail_if_new = ENV['CI'] 31 | Capybara::Screenshot::Diff.tolerance = 0.05 32 | 33 | RSpec.configure do |config| 34 | config.before(:each, type: :system) { driven_by :customized_chrome } 35 | config.include ::CapybaraScreenshotDiff::DSL 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/coverage_setup.rb: -------------------------------------------------------------------------------- 1 | return unless ENV['CI'] || ARGV.grep(/spec\.rb/).empty? # skip if running individual specs 2 | 3 | require 'simplecov' 4 | require 'simplecov-cobertura' 5 | 6 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter if ENV['CI'] 7 | SimpleCov.start do 8 | add_filter 'spec' 9 | enable_coverage :branch 10 | primary_coverage :branch 11 | end 12 | SimpleCov.minimum_coverage line: 100, branch: 100 13 | -------------------------------------------------------------------------------- /spec/system/css_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe ActiveAdminAssets, 'CSS' do 4 | let(:user) { User.create!(id: 1, first_name: 'Stable', last_name: 'String') } 5 | before { prevent_fluctuating_version_text } 6 | 7 | it 'has dashboard css' do 8 | visit admin_root_path 9 | expect_stable_screenshot('dashboard') 10 | end 11 | 12 | it 'has index css' do 13 | user # put in DB 14 | visit admin_users_path 15 | expect_stable_screenshot('resource_index') 16 | end 17 | 18 | it 'has show css' do 19 | visit admin_user_path(user) 20 | expect_stable_screenshot('resource_show') 21 | end 22 | 23 | it 'has form css' do 24 | visit edit_admin_user_path(user) 25 | expect_stable_screenshot('resource_form') 26 | end 27 | 28 | def expect_stable_screenshot(name) 29 | # standardize resolution / viewport size (1400x600) 30 | required_window_size = execute_script <<~JS 31 | return [outerWidth - innerWidth + 1400, outerHeight - innerHeight + 600]; 32 | JS 33 | page.current_window.resize_to(*required_window_size) 34 | 35 | # standardize light/dark mode 36 | $selenium_driver.browser.execute_cdp( 37 | "Emulation.setEmulatedMedia", 38 | features: [{ "name": "prefers-color-scheme", "value": "light" }], 39 | ) 40 | 41 | sleep 0.1 42 | 43 | # run capybara-screenshot-diff 44 | expect(page).to match_screenshot(name) 45 | end 46 | 47 | def prevent_fluctuating_version_text 48 | allow(I18n).to receive(:t).and_wrap_original do |m, *args, **kw, &blk| 49 | args[0] == 'active_admin.powered_by' ? 'test' : m.call(*args, **kw, &blk) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/system/js_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe ActiveAdminAssets, 'JS' do 4 | it 'has confirm dialog js (ujs)', type: :system do 5 | user = User.create! 6 | visit admin_user_path(user) 7 | 8 | js_alert_text = accept_confirm { click_link 'Delete User' } 9 | sleep 0.1 # wait for delayed delete request 10 | 11 | expect(js_alert_text).to include('Are you sure') 12 | expect(User.count).to eq(0) 13 | end 14 | 15 | it 'has menu js (flowbite)' do 16 | visit admin_root_path 17 | 18 | # use mobile size so sidebar menu is initially hidden 19 | page.current_window.resize_to(400, 600) 20 | expect(page).not_to have_text('Stuff') 21 | 22 | # toggle sidebar menu (this uses flowbite JS) 23 | find('[data-drawer-target="main-menu"]').click 24 | expect(page).to have_text('Stuff') 25 | 26 | # expand nested menu (this is custom AA JS - features/main_menu.js) 27 | expect(page).not_to have_text('Useroos') 28 | click_on 'Stuff' 29 | expect(page).to have_text('Useroos') 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/templates/app_template.rb: -------------------------------------------------------------------------------- 1 | # template for dummy rails app used in specs 2 | 3 | gem 'activeadmin_assets', path: __dir__ + '/../../' 4 | gem 'activeadmin', '>= 4.0.0.beta7', '< 5.0.0' 5 | gem 'csv' 6 | 7 | # https://github.com/activeadmin/activeadmin/pull/7235#issuecomment-1000823435 8 | insert_into_file 'config/environments/test.rb', 'false # ', after: /config.eager_load *=/ 9 | 10 | generate 'model', 'User first_name:string last_name:string admin:boolean --no-test-framework --no-timestamps' 11 | generate 'active_admin:install --skip-users' 12 | 13 | insert_into_file 'config/initializers/active_admin.rb', <<-RUBY, after: "|config|\n" 14 | config.load_paths = [File.join(Rails.root, 'app/lib/aa')] 15 | RUBY 16 | 17 | file 'app/lib/aa/users.rb', File.read(__dir__ + '/user_resource_template.rb') 18 | 19 | file 'app/lib/aa/dashboard.rb', File.read(__dir__ + '/dashboard_template.rb') 20 | 21 | route 'root "application#index"' 22 | 23 | rake 'db:migrate db:test:prepare' 24 | -------------------------------------------------------------------------------- /spec/templates/dashboard_template.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register_page "Dashboard" do 2 | menu priority: 1, label: 'foo' 3 | 4 | content title: 'bar' do 5 | panel('baz') { 'qux' } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/templates/user_resource_template.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register User do 2 | menu label: 'Useroos', parent: 'Stuff' 3 | actions :all 4 | config.filters = false 5 | end 6 | -------------------------------------------------------------------------------- /tasks/benchmark.rake: -------------------------------------------------------------------------------- 1 | desc 'Run benchmarks' 2 | task :benchmark do 3 | require 'benchmark/ips' 4 | require 'activeadmin_assets' 5 | require 'action_dispatch' 6 | 7 | puts 'How much does the ActiveAdminAssets::Middleware slow down '\ 8 | 'non-asset requests compared to ActionDispatch::Static?' 9 | 10 | app = ->(_) {} 11 | env = { "PATH_INFO" => "/index.html", "REQUEST_METHOD" => "GET" } 12 | m1 = ActiveAdminAssets::Middleware.new(app) 13 | m2 = ActionDispatch::Static.new(app, 'some_path') 14 | 15 | Benchmark.ips do |x| 16 | x.report('ActiveAdminAssets::Middleware') { m1.call(env) } 17 | x.report('ActionDispatch::Static') { m2.call(env) } 18 | x.compare! 19 | end 20 | # => ActiveAdminAssets::Middleware: 4072263.3 i/s 21 | # => ActionDispatch::Static: 155206.9 i/s - 26.24x slower 22 | end 23 | -------------------------------------------------------------------------------- /tasks/css.rake: -------------------------------------------------------------------------------- 1 | desc 'Build Active Admin stylesheets' 2 | task :css do 3 | dest = "#{__dir__}/../lib/activeadmin_assets/assets/active_admin.css" 4 | 5 | sh "#{__dir__}/../bin/tailwindcss", 6 | '-c', "#{__dir__}/css/tailwind.config.js", 7 | '-i', "#{__dir__}/css/entrypoint.css", 8 | '-o', dest 9 | 10 | sh 'gzip', '-f', dest 11 | end 12 | -------------------------------------------------------------------------------- /tasks/css/entrypoint.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tasks/css/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const execSync = require('child_process').execSync 2 | const activeAdminPath = execSync('bundle show activeadmin', { encoding: 'utf-8' }).trim() 3 | 4 | module.exports = { 5 | content: [ 6 | `${activeAdminPath}/vendor/javascript/flowbite.js`, 7 | `${activeAdminPath}/plugin.js`, 8 | `${activeAdminPath}/app/views/**/*.{arb,erb,html,rb}`, 9 | ], 10 | darkMode: "class", 11 | plugins: [ 12 | require(`${activeAdminPath}/plugin.js`) 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tasks/generate_spec_app.rake: -------------------------------------------------------------------------------- 1 | desc 'Generate a dummy rails app for testing' 2 | task :generate_spec_app do 3 | sh 'rm -rf spec/dummy' 4 | sh *%w[ 5 | rails new spec/dummy 6 | --template=spec/templates/app_template.rb 7 | --skip-action-cable 8 | --skip-action-mailbox 9 | --skip-action-text 10 | --skip-active-job 11 | --skip-active-storage 12 | --skip-asset-pipeline 13 | --skip-bootsnap 14 | --skip-bundle 15 | --skip-dev-gems 16 | --skip-docker 17 | --skip-git 18 | --skip-hotwire 19 | --skip-javascript 20 | --skip-jbuilder 21 | --skip-keeps 22 | --skip-listen 23 | --skip-spring 24 | --skip-system-test 25 | --skip-test 26 | --skip-turbolinks 27 | --skip-webpack 28 | ] 29 | end 30 | -------------------------------------------------------------------------------- /tasks/js.rake: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | # In theory, we could simply use the JS files included in the activeadmin gem 4 | # and serve them, but adding them to activeadmin_assets allows us to treat JS 5 | # the same as CSS, making for a simpler URLPatch and Middleware. 6 | desc 'Copy Active Admin javascript' 7 | task :js do 8 | active_admin_path = `bundle show activeadmin`.chomp 9 | Dir["#{active_admin_path}/{app,vendor}/javascript/**/*.js"].each do |file| 10 | relative_path = file.split(%r{(?:app|vendor)/javascript/}).last 11 | dest = "#{__dir__}/../lib/activeadmin_assets/assets/#{relative_path}" 12 | FileUtils.mkdir_p(File.dirname(dest)) 13 | FileUtils.cp(file, dest) 14 | sh 'gzip', '-f', dest 15 | end 16 | end 17 | --------------------------------------------------------------------------------