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.
- checksums.yaml +4 -4
- data/README.md +15 -0
- data/app/assets/tailwind/application.css +5 -0
- data/app/models/panda/core/user.rb +20 -11
- data/config/brakeman.ignore +51 -0
- data/db/migrate/20250809000001_create_panda_core_users.rb +1 -1
- data/db/migrate/20251203100000_rename_is_admin_to_admin_in_panda_core_users.rb +18 -0
- data/lib/panda/core/configuration.rb +4 -0
- data/lib/panda/core/engine/autoload_config.rb +9 -10
- data/lib/panda/core/engine/omniauth_config.rb +82 -36
- data/lib/panda/core/engine/route_config.rb +37 -0
- data/lib/panda/core/engine.rb +46 -41
- data/lib/panda/core/middleware.rb +146 -0
- data/lib/panda/core/testing/rails_helper.rb +17 -8
- data/lib/panda/core/testing/support/authentication_helpers.rb +6 -6
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +2 -12
- data/lib/panda/core/testing/support/system/browser_console_logger.rb +1 -2
- data/lib/panda/core/testing/support/system/chrome_path.rb +38 -0
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +79 -0
- data/lib/panda/core/testing/support/system/cuprite_setup.rb +61 -66
- data/lib/panda/core/testing/support/system/system_test_helpers.rb +11 -11
- data/lib/panda/core/version.rb +1 -1
- data/lib/tasks/panda/core/users.rake +3 -3
- data/lib/tasks/panda/shared.rake +31 -5
- data/public/panda-core-assets/favicons/browserconfig.xml +1 -1
- data/public/panda-core-assets/favicons/site.webmanifest +1 -1
- data/public/panda-core-assets/panda-core-0.11.0.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- metadata +8 -8
- data/lib/panda/core/engine/middleware_config.rb +0 -17
- data/lib/panda/core/testing/support/system/better_system_tests.rb +0 -180
- data/lib/panda/core/testing/support/system/capybara_config.rb +0 -64
- data/lib/panda/core/testing/support/system/ci_capybara_config.rb +0 -77
- data/public/panda-core-assets/panda-core-0.10.6.css +0 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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:
|
|
40
|
-
js_errors: true,
|
|
42
|
+
timeout: cuprite_timeout,
|
|
43
|
+
js_errors: true,
|
|
41
44
|
ignore_default_browser_options: false,
|
|
42
|
-
process_timeout:
|
|
43
|
-
wait_for_network_idle: false,
|
|
44
|
-
pending_connection_errors:
|
|
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":
|
|
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"
|
|
68
|
-
"allow-file-access-from-files"
|
|
69
|
-
"allow-file-access"
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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}"
|
data/lib/panda/core/version.rb
CHANGED
|
@@ -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!(
|
|
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!(
|
|
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
|
-
|
|
112
|
+
admin: true
|
|
113
113
|
)
|
|
114
114
|
puts "✓ Created admin user '#{user.email}'"
|
|
115
115
|
puts " Name: #{user.name}" if user.respond_to?(:name)
|