panda-core 0.11.0 → 0.12.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -0
  3. data/app/assets/tailwind/application.css +5 -0
  4. data/app/models/panda/core/user.rb +20 -11
  5. data/config/brakeman.ignore +51 -0
  6. data/db/migrate/20250809000001_create_panda_core_users.rb +1 -1
  7. data/db/migrate/20251203100000_rename_is_admin_to_admin_in_panda_core_users.rb +18 -0
  8. data/lib/panda/core/configuration.rb +4 -0
  9. data/lib/panda/core/engine/autoload_config.rb +9 -10
  10. data/lib/panda/core/engine/omniauth_config.rb +82 -36
  11. data/lib/panda/core/engine/route_config.rb +37 -0
  12. data/lib/panda/core/engine.rb +46 -41
  13. data/lib/panda/core/middleware.rb +146 -0
  14. data/lib/panda/core/testing/rails_helper.rb +17 -8
  15. data/lib/panda/core/testing/support/authentication_helpers.rb +6 -6
  16. data/lib/panda/core/testing/support/authentication_test_helpers.rb +2 -12
  17. data/lib/panda/core/testing/support/system/browser_console_logger.rb +1 -2
  18. data/lib/panda/core/testing/support/system/chrome_path.rb +38 -0
  19. data/lib/panda/core/testing/support/system/cuprite_helpers.rb +79 -0
  20. data/lib/panda/core/testing/support/system/cuprite_setup.rb +61 -66
  21. data/lib/panda/core/testing/support/system/system_test_helpers.rb +11 -11
  22. data/lib/panda/core/version.rb +1 -1
  23. data/lib/tasks/panda/core/users.rake +3 -3
  24. data/lib/tasks/panda/shared.rake +31 -5
  25. data/public/panda-core-assets/favicons/browserconfig.xml +1 -1
  26. data/public/panda-core-assets/favicons/site.webmanifest +1 -1
  27. data/public/panda-core-assets/panda-core-0.11.0.css +2 -0
  28. data/public/panda-core-assets/panda-core.css +2 -2
  29. metadata +8 -8
  30. data/lib/panda/core/engine/middleware_config.rb +0 -17
  31. data/lib/panda/core/testing/support/system/better_system_tests.rb +0 -180
  32. data/lib/panda/core/testing/support/system/capybara_config.rb +0 -64
  33. data/lib/panda/core/testing/support/system/ci_capybara_config.rb +0 -77
  34. data/public/panda-core-assets/panda-core-0.10.6.css +0 -2
  35. data/public/panda-core-assets/panda-core-0.10.7.css +0 -2
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+
5
+ module Rails
6
+ module Configuration
7
+ # Add a tiny compatibility layer so the proxy can be inspected and reset in tests.
8
+ class MiddlewareStackProxy
9
+ include Enumerable
10
+
11
+ def clear
12
+ operations.clear
13
+ delete_operations.clear
14
+ self
15
+ end
16
+
17
+ def each(&block)
18
+ to_stack.each { |mw| block.call(wrap_middleware(mw)) }
19
+ end
20
+
21
+ def to_a
22
+ map(&:itself)
23
+ end
24
+
25
+ def [](index)
26
+ to_a[index]
27
+ end
28
+
29
+ def first
30
+ to_a.first
31
+ end
32
+
33
+ def last
34
+ to_a.last
35
+ end
36
+
37
+ def size
38
+ to_a.size
39
+ end
40
+
41
+ private
42
+
43
+ def to_stack
44
+ ActionDispatch::MiddlewareStack.new.tap do |stack|
45
+ merge_into(stack)
46
+ end
47
+ end
48
+
49
+ def wrap_middleware(middleware)
50
+ args, kwargs = split_args_and_kwargs(middleware.args)
51
+ MiddlewareEntry.new(middleware.klass, args, kwargs, middleware.block)
52
+ end
53
+
54
+ def split_args_and_kwargs(args)
55
+ return [args, {}] unless args.last.is_a?(Hash)
56
+
57
+ [args[0...-1], args.last]
58
+ end
59
+
60
+ MiddlewareEntry = Struct.new(:klass, :args, :kwargs, :block)
61
+ end
62
+ end
63
+ end
64
+
65
+ module Panda
66
+ module Core
67
+ module Middleware
68
+ EXECUTOR = "ActionDispatch::Executor"
69
+
70
+ def self.use(app, klass, *args, **kwargs, &block)
71
+ app.config.middleware.use(klass, *args, **kwargs, &block)
72
+ end
73
+
74
+ def self.insert_before(app, priority_targets, klass, *args, **kwargs, &block)
75
+ stack = build_stack(app)
76
+ target = resolve_target(stack, priority_targets, :insert_before)
77
+ insertion_point = target || fallback_before(stack)
78
+ app.config.middleware.insert_before(insertion_point, klass, *args, **kwargs, &block)
79
+ end
80
+
81
+ def self.insert_after(app, priority_targets, klass, *args, **kwargs, &block)
82
+ stack = build_stack(app)
83
+ target = resolve_target(stack, priority_targets, :insert_after)
84
+ insertion_point = target || fallback_after(stack)
85
+ app.config.middleware.insert_after(insertion_point, klass, *args, **kwargs, &block)
86
+ end
87
+
88
+ #
89
+ # Helpers
90
+ #
91
+ def self.resolve_target(app_or_stack, priority_targets, _operation)
92
+ stack = normalize_stack(app_or_stack)
93
+ priority_targets.find { |candidate| middleware_exists?(stack, candidate) }
94
+ end
95
+ private_class_method :resolve_target
96
+
97
+ def self.middleware_exists?(app_or_stack, candidate)
98
+ stack = normalize_stack(app_or_stack)
99
+ stack.any? { |mw| middleware_matches?(mw.klass, candidate) }
100
+ end
101
+ private_class_method :middleware_exists?
102
+
103
+ def self.middleware_matches?(klass, candidate)
104
+ klass == candidate || klass.name == candidate.to_s
105
+ end
106
+ private_class_method :middleware_matches?
107
+
108
+ def self.fallback_before(stack)
109
+ executor_index = index_for(stack, EXECUTOR)
110
+ executor_index || 0
111
+ end
112
+ private_class_method :fallback_before
113
+
114
+ def self.fallback_after(stack)
115
+ executor_index = index_for(stack, EXECUTOR)
116
+ return executor_index + 1 if executor_index
117
+
118
+ stack_size = stack.size
119
+ stack_size.zero? ? 0 : stack_size - 1
120
+ end
121
+ private_class_method :fallback_after
122
+
123
+ def self.index_for(stack, target)
124
+ stack.each_with_index do |middleware, idx|
125
+ return idx if middleware_matches?(middleware.klass, target)
126
+ end
127
+ nil
128
+ end
129
+ private_class_method :index_for
130
+
131
+ def self.build_stack(app)
132
+ app.config.middleware.to_a
133
+ end
134
+ private_class_method :build_stack
135
+
136
+ def self.normalize_stack(app_or_stack)
137
+ if app_or_stack.respond_to?(:config) && app_or_stack.config.respond_to?(:middleware)
138
+ build_stack(app_or_stack)
139
+ else
140
+ app_or_stack
141
+ end
142
+ end
143
+ private_class_method :normalize_stack
144
+ end
145
+ end
146
+ end
@@ -104,11 +104,6 @@ RSpec.configure do |config|
104
104
  ActionMailer::Base.logger = nil if defined?(ActionMailer)
105
105
  end
106
106
 
107
- # Suppress Rails command output during generator tests
108
- config.before(:each, type: :generator) do
109
- allow(Rails::Command).to receive(:invoke).and_return(true)
110
- end
111
-
112
107
  # Force all examples to run
113
108
  config.filter_run_including({})
114
109
  config.run_all_when_everything_filtered = true
@@ -169,14 +164,28 @@ RSpec.configure do |config|
169
164
  OmniAuth.config.test_mode = true if defined?(OmniAuth)
170
165
 
171
166
  # DatabaseCleaner configuration
167
+ config.around(:each) do |example|
168
+ DatabaseCleaner.strategy = (example.metadata[:type] == :system) ? :truncation : :transaction
169
+ DatabaseCleaner.cleaning do
170
+ example.run
171
+ end
172
+ end
173
+
174
+ config.before(:each, type: :system) do
175
+ driven_by :panda_cuprite
176
+ end
177
+
178
+ config.use_transactional_fixtures = false
179
+
172
180
  config.before(:suite) do
173
181
  # Allow DATABASE_URL in CI environment
174
- if ENV["DATABASE_URL"]
182
+ if ENV["DATABASE_URL"] &&
183
+ ENV["SKIP_DB_CLEAN_WITH_DATABASE_URL"].nil? &&
184
+ ENV["ACT"] != "true"
175
185
  DatabaseCleaner.allow_remote_database_url = true
186
+ DatabaseCleaner.clean_with(:truncation)
176
187
  end
177
188
 
178
- DatabaseCleaner.clean_with :truncation
179
-
180
189
  # Hook for gems to add custom suite setup
181
190
  # Gems can define Panda::Testing.before_suite_hook and it will be called here
182
191
  if defined?(Panda::Testing) && Panda::Testing.respond_to?(:before_suite_hook)
@@ -4,18 +4,18 @@ module Panda
4
4
  module Core
5
5
  module AuthenticationHelpers
6
6
  # Create test users with fixed IDs for consistent fixture references
7
- def create_admin_user
7
+ def create_admin_user(attributes = {})
8
8
  Panda::Core::User.find_or_create_by!(id: "8f481fcb-d9c8-55d7-ba17-5ea5d9ed8b7a") do |user|
9
- user.email = "admin@test.example.com"
10
- user.name = "Admin User"
9
+ user.email = attributes[:email] || "admin@test.example.com"
10
+ user.name = attributes[:name] || "Admin User"
11
11
  user.admin = true
12
12
  end
13
13
  end
14
14
 
15
- def create_regular_user
15
+ def create_regular_user(attributes = {})
16
16
  Panda::Core::User.find_or_create_by!(id: "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d") do |user|
17
- user.email = "user@test.example.com"
18
- user.name = "Regular User"
17
+ user.email = attributes[:email] || "user@test.example.com"
18
+ user.name = attributes[:name] || "Regular User"
19
19
  user.admin = false
20
20
  end
21
21
  end
@@ -22,12 +22,7 @@ module Panda
22
22
  user.email = attributes[:email] || "admin@example.com"
23
23
  user.name = attributes[:name] || "Admin User"
24
24
  user.image_url = attributes[:image_url] || default_image_url
25
- # Use is_admin for the actual column, but support both for compatibility
26
- if user.respond_to?(:is_admin=)
27
- user.is_admin = attributes.fetch(:admin, true)
28
- elsif user.respond_to?(:admin=)
29
- user.admin = attributes.fetch(:admin, true)
30
- end
25
+ user.admin = attributes[:admin] || true
31
26
  # Only set OAuth fields if they exist on the model
32
27
  user.uid = attributes[:uid] || "admin_oauth_uid_123" if user.respond_to?(:uid=)
33
28
  user.provider = attributes[:provider] || "google_oauth2" if user.respond_to?(:provider=)
@@ -42,12 +37,7 @@ module Panda
42
37
  user.email = attributes[:email] || "user@example.com"
43
38
  user.name = attributes[:name] || "Regular User"
44
39
  user.image_url = attributes[:image_url] || default_image_url(dark: true)
45
- # Use is_admin for the actual column, but support both for compatibility
46
- if user.respond_to?(:is_admin=)
47
- user.is_admin = attributes.fetch(:admin, false)
48
- elsif user.respond_to?(:admin=)
49
- user.admin = attributes.fetch(:admin, false)
50
- end
40
+ user.admin = attributes[:admin] || false
51
41
  # Only set OAuth fields if they exist on the model
52
42
  user.uid = attributes[:uid] || "user_oauth_uid_456" if user.respond_to?(:uid=)
53
43
  user.provider = attributes[:provider] || "google_oauth2" if user.respond_to?(:provider=)
@@ -9,8 +9,7 @@ module BrowserConsoleLogger
9
9
 
10
10
  if respond_to?(:page) && page.driver.is_a?(Capybara::Cuprite::Driver)
11
11
  begin
12
- # Access the console logger via the CupriteSetup module
13
- console_logger = Panda::Core::Testing::CupriteSetup.console_logger
12
+ console_logger = Panda::Core::Testing::CupriteSetup.console_logger if defined?(Panda::Core::Testing::CupriteSetup)
14
13
 
15
14
  unless console_logger
16
15
  puts "\n⚠️ Console logger not available"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Testing
6
+ module Support
7
+ module System
8
+ module ChromePath
9
+ def self.resolve
10
+ @resolved ||= begin
11
+ candidates = [
12
+ # Linux (Debian/Ubuntu + your CI image)
13
+ "/usr/bin/chromium",
14
+ "/usr/bin/chromium-browser",
15
+ "/usr/bin/google-chrome",
16
+ "/opt/google/chrome/google-chrome",
17
+ "/opt/google/chrome/chrome",
18
+
19
+ # macOS Homebrew paths (Chromium)
20
+ "/opt/homebrew/bin/chromium",
21
+ "/usr/local/bin/chromium",
22
+
23
+ # macOS Google Chrome
24
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
25
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
26
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
27
+ ]
28
+
29
+ candidates.find { |path| File.executable?(path) } ||
30
+ raise("Could not find a Chrome/Chromium binary on this system")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  # Helper methods for Cuprite-based system tests
4
6
  #
5
7
  # This module provides utility methods for working with Cuprite in system tests:
@@ -16,6 +18,83 @@ module Panda
16
18
  module Core
17
19
  module Testing
18
20
  module CupriteHelpers
21
+ # Resolve the configured Capybara artifacts directory
22
+ def capybara_artifacts_dir
23
+ Pathname.new(Capybara.save_path || Rails.root.join("tmp/capybara"))
24
+ end
25
+
26
+ # Save a PNG screenshot for the current page.
27
+ #
28
+ def save_screenshot!(name = nil)
29
+ name ||= example.metadata[:full_description].parameterize
30
+ path = capybara_artifacts_dir.join("#{name}.png")
31
+
32
+ FileUtils.mkdir_p(File.dirname(path))
33
+ page.save_screenshot(path, full: true) # rubocop:disable Lint/Debugger
34
+ puts "📸 Saved screenshot: #{path}"
35
+
36
+ path
37
+ end
38
+
39
+ #
40
+ # Record a small MP4 video of the test — uses Cuprite's Chrome DevTools API
41
+ #
42
+ def record_video!(name = nil, seconds: 3)
43
+ name ||= example.metadata[:full_description].parameterize
44
+ path = capybara_artifacts_dir.join("#{name}.mp4")
45
+
46
+ FileUtils.mkdir_p(File.dirname(path))
47
+
48
+ session = page.driver.browser
49
+ client = session.client
50
+
51
+ # Enable screencast
52
+ client.command("Page.startScreencast", format: "png", quality: 80, maxWidth: 1280, maxHeight: 800)
53
+
54
+ frames = []
55
+
56
+ start = Time.now
57
+ while Time.now - start < seconds
58
+ message = client.listen
59
+ if message["method"] == "Page.screencastFrame"
60
+ frames << message["params"]["data"]
61
+ client.command("Page.screencastFrameAck", sessionId: message["params"]["sessionId"])
62
+ end
63
+ end
64
+
65
+ # Stop
66
+ client.command("Page.stopScreencast")
67
+
68
+ # Convert frames to MP4 using ffmpeg
69
+ Dir.mktmpdir do |dir|
70
+ png_dir = File.join(dir, "frames")
71
+ FileUtils.mkdir_p(png_dir)
72
+
73
+ frames.each_with_index do |data, i|
74
+ File.binwrite(File.join(png_dir, "frame-%05d.png" % i), Base64.decode64(data))
75
+ end
76
+
77
+ system <<~CMD
78
+ ffmpeg -y -framerate 8 -pattern_type glob -i '#{png_dir}/*.png' -c:v libx264 -pix_fmt yuv420p '#{path}'
79
+ CMD
80
+ end
81
+
82
+ puts "🎥 Saved video: #{path}"
83
+ path
84
+ rescue => e
85
+ puts "⚠️ Failed to record video: #{e.message}"
86
+ nil
87
+ end
88
+
89
+ def save_html!(name = nil)
90
+ name ||= example.metadata[:full_description].parameterize
91
+ path = capybara_artifacts_dir.join("#{name}.html")
92
+ FileUtils.mkdir_p(File.dirname(path))
93
+ File.write(path, page.html)
94
+ puts "📝 Saved HTML snapshot: #{path}"
95
+ path
96
+ end
97
+
19
98
  # Ensure page is loaded and stable before interacting
20
99
  def ensure_page_loaded
21
100
  # Check if we're on about:blank and need to reload
@@ -1,54 +1,55 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "ferrum"
4
2
  require "capybara/cuprite"
3
+ require "tmpdir"
5
4
  require_relative "ferrum_console_logger"
6
-
7
- # Shared Cuprite driver configuration for all Panda gems
8
- # This provides standard Cuprite setup with sensible defaults that work across gems
9
- #
10
- # Features:
11
- # - :cuprite driver for standard desktop testing
12
- # - :cuprite_mobile driver for mobile viewport testing
13
- # - JavaScript error reporting enabled by default (js_errors: true)
14
- # - CI-optimized browser options
15
- # - Environment-based configuration (HEADLESS, INSPECTOR, SLOWMO)
5
+ require_relative "chrome_path"
16
6
 
17
7
  module Panda
18
8
  module Core
19
9
  module Testing
20
10
  module CupriteSetup
21
- # Class variable to store the console logger instance
22
- # This allows tests to access console logs after they run
23
11
  @console_logger = nil
24
12
 
25
13
  class << self
26
14
  attr_accessor :console_logger
27
15
  end
28
16
 
29
- # Base Cuprite options shared across all drivers
30
17
  def self.base_options
31
- default_timeout = 2
32
- default_process_timeout = 2
18
+ ci_default_timeout = 30
19
+ ci_default_process_timeout = 120
20
+ default_timeout = ENV["CI"] ? ci_default_timeout : 5
21
+ default_process_timeout = ENV["CI"] ? ci_default_process_timeout : 10
22
+
23
+ cuprite_timeout = ENV["CUPRITE_TIMEOUT"]&.to_i ||
24
+ ENV["FERRUM_TIMEOUT"]&.to_i ||
25
+ default_timeout
26
+
27
+ process_timeout_value = ENV["CUPRITE_PROCESS_TIMEOUT"]&.to_i ||
28
+ ENV["FERRUM_PROCESS_TIMEOUT"]&.to_i ||
29
+ default_process_timeout
30
+
31
+ puts "[Cuprite Config] timeout = #{cuprite_timeout}, process_timeout = #{process_timeout_value}" if ENV["CI"] || ENV["DEBUG"]
32
+ puts "[Cuprite Config] ENV: CUPRITE_TIMEOUT=#{ENV["CUPRITE_TIMEOUT"].inspect}, CUPRITE_PROCESS_TIMEOUT=#{ENV["CUPRITE_PROCESS_TIMEOUT"].inspect}" if ENV["CI"] || ENV["DEBUG"]
33
+
34
+ browser_path = ENV["BROWSER_PATH"] || Panda::Core::Testing::Support::System::ChromePath.resolve
33
35
 
34
36
  {
37
+ browser_path: browser_path,
35
38
  window_size: [1440, 1000],
36
39
  inspector: ENV["INSPECTOR"].in?(%w[y 1 yes true]),
37
40
  headless: !ENV["HEADLESS"].in?(%w[n 0 no false]),
38
41
  slowmo: ENV["SLOWMO"]&.to_f || 0,
39
- timeout: ENV["CUPRITE_TIMEOUT"]&.to_i || default_timeout,
40
- js_errors: true, # IMPORTANT: Report JavaScript errors as test failures
42
+ timeout: cuprite_timeout,
43
+ js_errors: true,
41
44
  ignore_default_browser_options: false,
42
- process_timeout: ENV["CUPRITE_PROCESS_TIMEOUT"]&.to_i || default_process_timeout,
43
- wait_for_network_idle: false, # Don't wait for all network requests
44
- pending_connection_errors: false, # Don't fail on pending external connections
45
+ process_timeout: process_timeout_value,
46
+ wait_for_network_idle: false,
47
+ pending_connection_errors: ENV["CI"] != "true",
48
+ max_conns: 1,
49
+ restart_if: {crashes: true, attempts: 2},
45
50
  browser_options: {
46
51
  "no-sandbox": nil,
47
52
  "disable-gpu": nil,
48
- "disable-dev-shm-usage": nil,
49
- "disable-background-networking": nil,
50
- "disable-default-apps": nil,
51
- "disable-extensions": nil,
52
53
  "disable-sync": nil,
53
54
  "disable-translate": nil,
54
55
  "no-first-run": nil,
@@ -56,71 +57,65 @@ module Panda
56
57
  "allow-insecure-localhost": nil,
57
58
  "enable-features": "NetworkService,NetworkServiceInProcess",
58
59
  "disable-blink-features": "AutomationControlled",
59
- "no-dbus": true
60
+ "no-dbus": nil,
61
+ "log-level": ENV["CI"] ? "0" : "3"
60
62
  }
61
63
  }
62
64
  end
63
65
 
64
- # Additional options for CI environments
65
66
  def self.ci_browser_options
66
67
  {
67
- "disable-web-security": nil,
68
- "allow-file-access-from-files": nil,
69
- "allow-file-access": nil
68
+ "disable-web-security" => nil,
69
+ "allow-file-access-from-files" => nil,
70
+ "allow-file-access" => nil,
71
+ "disable-dev-shm-usage" => nil, # Sets shared memory in /tmp; don't use if lots of /dev/shm space
72
+ "no-sandbox" => nil
70
73
  }
71
74
  end
72
75
 
73
- # Configure standard desktop driver
74
- def self.register_desktop_driver
76
+ def self.register_panda_cuprite_driver(name: :panda_cuprite, window_size: [1280, 720])
75
77
  options = base_options.dup
78
+ options[:window_size] = window_size
79
+ options[:browser_options] = options[:browser_options].dup
80
+ options[:browser_options]["user-data-dir"] = Dir.mktmpdir("cuprite-profile")
76
81
 
77
- # Add CI-specific options
78
- if ENV["GITHUB_ACTIONS"] == "true"
82
+ if ENV["CI"] == "true" # Covers both act and GitHub Actions
79
83
  options[:browser_options].merge!(ci_browser_options)
80
-
81
- # Ensure CI uses xvfb to run the browser
82
- options[:browser_options][:xvfb] = true
83
84
  end
84
85
 
85
- # Create console logger for capturing browser console messages
86
- self.console_logger = Panda::Core::Testing::Support::System::FerrumConsoleLogger.new
87
- options[:logger] = console_logger
88
-
89
- Capybara.register_driver :cuprite do |app|
90
- Capybara::Cuprite::Driver.new(app, **options)
91
- end
92
- end
93
-
94
- # Configure mobile viewport driver
95
- def self.register_mobile_driver
96
- options = base_options.dup
97
- options[:window_size] = [375, 667] # iPhone SE size
98
-
99
- if ENV["GITHUB_ACTIONS"] == "true"
100
- options[:browser_options].merge!(ci_browser_options)
101
- end
102
-
103
- # Use the same console logger instance for mobile driver
104
86
  options[:logger] = console_logger if console_logger
105
87
 
106
- Capybara.register_driver :cuprite_mobile do |app|
88
+ Capybara.register_driver name do |app|
107
89
  Capybara::Cuprite::Driver.new(app, **options)
108
90
  end
109
91
  end
110
92
 
111
- # Register all drivers
112
93
  def self.setup!
113
- register_desktop_driver
114
- register_mobile_driver
115
-
116
- # Set default drivers
117
- Capybara.default_driver = :cuprite
118
- Capybara.javascript_driver = :cuprite
94
+ register_panda_cuprite_driver(name: :panda_cuprite, window_size: [1280, 720])
95
+ register_panda_cuprite_driver(name: :panda_cuprite_mobile, window_size: [375, 667])
96
+ Capybara.default_driver = :panda_cuprite
97
+ Capybara.javascript_driver = :panda_cuprite
119
98
  end
120
99
  end
121
100
  end
122
101
  end
123
102
  end
124
103
 
125
- # Auto-setup when required
104
+ Capybara.default_max_wait_time = ENV.fetch("CAPYBARA_MAX_WAIT_TIME", "5").to_i
105
+ Capybara.raise_server_errors = true
106
+
107
+ # Server selection (overridable via env)
108
+ server_name = ENV.fetch("CAPYBARA_SERVER", "puma").to_sym
109
+ Capybara.server = server_name
110
+ Capybara.server_host = ENV.fetch("CAPYBARA_SERVER_HOST", "127.0.0.1")
111
+ # Allow dynamic port by leaving blank
112
+ port_env = ENV["CAPYBARA_PORT"]
113
+ Capybara.server_port = (port_env && !port_env.empty?) ? port_env.to_i : nil
114
+ app_host_env = ENV["CAPYBARA_APP_HOST"]
115
+ Capybara.app_host = app_host_env unless app_host_env.to_s.empty?
116
+ Capybara.always_include_port = !Capybara.app_host.nil?
117
+
118
+ HEADLESS = !(ENV["HEADFUL"] == "true")
119
+
120
+ # Register the drivers and setup the options
126
121
  Panda::Core::Testing::CupriteSetup.setup!
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "cuprite_helpers"
4
- require_relative "better_system_tests"
5
4
 
6
5
  # Generic system test helpers for Cuprite-based testing
7
6
  # These methods work for any Rails application using Cuprite
@@ -11,7 +10,6 @@ module Panda
11
10
  module Testing
12
11
  module SystemTestHelpers
13
12
  include CupriteHelpers
14
- include BetterSystemTests
15
13
  end
16
14
  end
17
15
  end
@@ -21,6 +19,13 @@ end
21
19
  RSpec.configure do |config|
22
20
  config.include Panda::Core::Testing::SystemTestHelpers, type: :system
23
21
 
22
+ tmp = ENV["TMPDIR"]
23
+ if tmp && File.world_writable?(tmp)
24
+ config.before(:suite) do
25
+ warn "⚠️ TMPDIR is world-writable: #{tmp}"
26
+ end
27
+ end
28
+
24
29
  # Make urls in mailers contain the correct server host
25
30
  config.around(:each, type: :system) do |ex|
26
31
  was_host = Rails.application.default_url_options[:host]
@@ -102,7 +107,9 @@ RSpec.configure do |config|
102
107
  nil
103
108
  end
104
109
 
105
- # sleep 0.5
110
+ name = example.metadata[:full_description].parameterize
111
+
112
+ save_html!(name)
106
113
 
107
114
  # Get comprehensive page info
108
115
  page_html = begin
@@ -132,20 +139,13 @@ RSpec.configure do |config|
132
139
  end
133
140
 
134
141
  # Use Capybara's save_screenshot method
135
- screenshot_path = Capybara.save_screenshot
142
+ screenshot_path = save_screenshot!(name)
136
143
  if screenshot_path
137
144
  puts "Screenshot saved to: #{screenshot_path}"
138
145
  puts "Page title: #{page_title}" if page_title.present?
139
146
  puts "Current URL: #{current_url}" if current_url.present?
140
147
  puts "Current path: #{current_path}" if current_path.present?
141
148
  puts "Page content length: #{page_html.length} characters"
142
-
143
- # Save page HTML for debugging in CI
144
- if ENV["GITHUB_ACTIONS"] && page_html.present?
145
- html_debug_path = screenshot_path.gsub(".png", ".html")
146
- File.write(html_debug_path, page_html)
147
- puts "Page HTML saved to: #{html_debug_path}"
148
- end
149
149
  end
150
150
  rescue => e
151
151
  puts "Failed to capture screenshot: #{e.message}"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Panda
4
4
  module Core
5
- VERSION = "0.11.0"
5
+ VERSION = "0.12.2"
6
6
  end
7
7
  end
@@ -45,7 +45,7 @@ namespace :panda do
45
45
  if user.admin?
46
46
  puts "User '#{user.email}' is already an admin"
47
47
  else
48
- user.update!(is_admin: true)
48
+ user.update!(admin: true)
49
49
  puts "✓ Granted admin privileges to '#{user.email}'"
50
50
  end
51
51
  end
@@ -75,7 +75,7 @@ namespace :panda do
75
75
  exit 1
76
76
  end
77
77
 
78
- user.update!(is_admin: false)
78
+ user.update!(admin: false)
79
79
  puts "✓ Revoked admin privileges from '#{user.email}'"
80
80
  else
81
81
  puts "User '#{user.email}' is not an admin"
@@ -109,7 +109,7 @@ namespace :panda do
109
109
  user = Panda::Core::User.create!(
110
110
  email: email.downcase,
111
111
  name: name,
112
- is_admin: true
112
+ admin: true
113
113
  )
114
114
  puts "✓ Created admin user '#{user.email}'"
115
115
  puts " Name: #{user.name}" if user.respond_to?(:name)