panda-core 0.11.0 → 0.12.5
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 +196 -109
- data/app/builders/panda/core/form_builder.rb +9 -9
- data/app/components/panda/core/UI/button.rb +4 -4
- data/app/components/panda/core/admin/button_component.rb +2 -2
- data/app/components/panda/core/admin/form_input_component.rb +2 -2
- data/app/components/panda/core/admin/form_select_component.rb +2 -2
- data/app/components/panda/core/admin/panel_component.rb +1 -1
- data/app/components/panda/core/admin/statistics_component.rb +3 -3
- data/app/components/panda/core/admin/tab_bar_component.rb +3 -3
- data/app/components/panda/core/admin/table_component.rb +3 -3
- data/app/controllers/panda/core/admin/sessions_controller.rb +4 -1
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +1 -1
- data/app/models/panda/core/user.rb +23 -14
- data/app/views/layouts/panda/core/admin.html.erb +10 -1
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +3 -3
- data/app/views/panda/core/admin/sessions/new.html.erb +2 -3
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +5 -5
- 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/asset_loader.rb +1 -1
- data/lib/panda/core/configuration.rb +4 -0
- data/lib/panda/core/engine/admin_controller_config.rb +6 -2
- data/lib/panda/core/engine/autoload_config.rb +9 -10
- data/lib/panda/core/engine/omniauth_config.rb +92 -34
- 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/module_registry.rb +21 -13
- 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/panda/core.rb +11 -0
- 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-0.12.2.css +2 -0
- data/public/panda-core-assets/panda-core-0.12.3.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- metadata +10 -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
data/lib/panda/core/engine.rb
CHANGED
|
@@ -1,82 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require "rails
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
4
|
require "omniauth"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# Issue: https://github.com/cookpad/omniauth-rails_csrf_protection/issues/23
|
|
9
|
-
# This can be removed once the gem is updated or Rails 8.2 is released
|
|
10
|
-
#
|
|
11
|
-
# We suppress the warning by temporarily redirecting stderr since
|
|
12
|
-
# ActiveSupport::Deprecation.silence was removed in Rails 8.1
|
|
13
|
-
original_stderr = $stderr
|
|
14
|
-
$stderr = StringIO.new
|
|
15
|
-
begin
|
|
16
|
-
require "omniauth/rails_csrf_protection"
|
|
17
|
-
ensure
|
|
18
|
-
$stderr = original_stderr
|
|
19
|
-
end
|
|
6
|
+
require "panda/core/middleware"
|
|
7
|
+
require "panda/core/module_registry"
|
|
20
8
|
|
|
21
|
-
#
|
|
9
|
+
# Shared engine mixins
|
|
22
10
|
require_relative "shared/inflections_config"
|
|
23
11
|
require_relative "shared/generator_config"
|
|
24
12
|
|
|
25
|
-
#
|
|
13
|
+
# Engine mixins
|
|
26
14
|
require_relative "engine/autoload_config"
|
|
27
|
-
require_relative "engine/middleware_config"
|
|
28
15
|
require_relative "engine/importmap_config"
|
|
29
16
|
require_relative "engine/omniauth_config"
|
|
30
17
|
require_relative "engine/phlex_config"
|
|
31
18
|
require_relative "engine/admin_controller_config"
|
|
32
|
-
|
|
33
|
-
# Load module registry
|
|
34
|
-
require_relative "module_registry"
|
|
19
|
+
require_relative "engine/route_config"
|
|
35
20
|
|
|
36
21
|
module Panda
|
|
37
22
|
module Core
|
|
38
23
|
class Engine < ::Rails::Engine
|
|
39
24
|
isolate_namespace Panda::Core
|
|
40
25
|
|
|
41
|
-
#
|
|
26
|
+
#
|
|
27
|
+
# Include shared behaviours
|
|
28
|
+
#
|
|
42
29
|
include Shared::InflectionsConfig
|
|
43
30
|
include Shared::GeneratorConfig
|
|
44
31
|
|
|
45
|
-
#
|
|
32
|
+
#
|
|
33
|
+
# Include engine-level concerns
|
|
34
|
+
#
|
|
46
35
|
include AutoloadConfig
|
|
47
|
-
include MiddlewareConfig
|
|
48
36
|
include ImportmapConfig
|
|
49
37
|
include OmniauthConfig
|
|
50
38
|
include PhlexConfig
|
|
51
39
|
include AdminControllerConfig
|
|
40
|
+
include RouteConfig
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
#
|
|
43
|
+
# Misc configuration point
|
|
44
|
+
#
|
|
45
|
+
initializer "panda_core.configuration" do
|
|
46
|
+
# Intentionally quiet — used as a stable anchor point
|
|
55
47
|
end
|
|
56
48
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
initializer "
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
#
|
|
50
|
+
# Static asset handling for:
|
|
51
|
+
# /panda-core-assets
|
|
52
|
+
#
|
|
53
|
+
initializer "panda_core.static_assets" do |app|
|
|
54
|
+
Panda::Core::Middleware.use(
|
|
55
|
+
app,
|
|
56
|
+
Rack::Static,
|
|
64
57
|
urls: ["/panda-core-assets"],
|
|
65
58
|
root: Panda::Core::Engine.root.join("public"),
|
|
66
59
|
header_rules: [
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
[
|
|
61
|
+
:all,
|
|
62
|
+
{
|
|
63
|
+
"Cache-Control" =>
|
|
64
|
+
Rails.env.development? ?
|
|
65
|
+
"no-cache, no-store, must-revalidate" :
|
|
66
|
+
"public, max-age=31536000"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
69
|
]
|
|
70
|
+
)
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
Panda::Core::Middleware.use(
|
|
73
|
+
app,
|
|
74
|
+
Panda::Core::ModuleRegistry::JavaScriptMiddleware
|
|
75
|
+
)
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
#
|
|
81
|
+
#
|
|
82
|
+
# Register engine with ModuleRegistry
|
|
83
|
+
#
|
|
80
84
|
Panda::Core::ModuleRegistry.register(
|
|
81
85
|
gem_name: "panda-core",
|
|
82
86
|
engine: "Panda::Core::Engine",
|
|
@@ -85,6 +89,7 @@ Panda::Core::ModuleRegistry.register(
|
|
|
85
89
|
components: "app/components/panda/core/**/*.rb",
|
|
86
90
|
helpers: "app/helpers/panda/core/**/*.rb",
|
|
87
91
|
views: "app/views/panda/core/**/*.erb",
|
|
92
|
+
layouts: "app/views/layouts/panda/core/**/*.erb",
|
|
88
93
|
javascripts: "app/assets/javascript/panda/core/**/*.js"
|
|
89
94
|
}
|
|
90
95
|
)
|
|
@@ -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
|
|
@@ -343,6 +343,10 @@ module Panda
|
|
|
343
343
|
private
|
|
344
344
|
|
|
345
345
|
def find_javascript_file(relative_path)
|
|
346
|
+
# Build list of paths to try - include .js extension fallback
|
|
347
|
+
paths_to_try = [relative_path]
|
|
348
|
+
paths_to_try << "#{relative_path}.js" unless relative_path.end_with?(".js")
|
|
349
|
+
|
|
346
350
|
# Check each registered module's JavaScript directory
|
|
347
351
|
ModuleRegistry.modules.each do |gem_name, info|
|
|
348
352
|
next unless ModuleRegistry.send(:engine_available?, info[:engine])
|
|
@@ -350,24 +354,28 @@ module Panda
|
|
|
350
354
|
root = ModuleRegistry.send(:engine_root, info[:engine])
|
|
351
355
|
next unless root
|
|
352
356
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
357
|
+
paths_to_try.each do |path|
|
|
358
|
+
# Check in app/javascript/panda/ (primary location)
|
|
359
|
+
candidate = root.join("app/javascript/panda", path)
|
|
360
|
+
return candidate.to_s if candidate.exist? && candidate.file?
|
|
356
361
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
# Fallback to public/panda/ (for CI environments where assets are copied)
|
|
363
|
+
public_candidate = root.join("public/panda", path)
|
|
364
|
+
return public_candidate.to_s if public_candidate.exist? && public_candidate.file?
|
|
365
|
+
end
|
|
360
366
|
end
|
|
361
367
|
|
|
362
368
|
# Also check Rails.root if available (for dummy apps in CI)
|
|
363
369
|
if defined?(Rails.root)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
paths_to_try.each do |path|
|
|
371
|
+
# Check app/javascript/panda/ in Rails.root
|
|
372
|
+
rails_candidate = Rails.root.join("app/javascript/panda", path)
|
|
373
|
+
return rails_candidate.to_s if rails_candidate.exist? && rails_candidate.file?
|
|
374
|
+
|
|
375
|
+
# Fallback to public/panda/ in Rails.root
|
|
376
|
+
rails_public_candidate = Rails.root.join("public/panda", path)
|
|
377
|
+
return rails_public_candidate.to_s if rails_public_candidate.exist? && rails_public_candidate.file?
|
|
378
|
+
end
|
|
371
379
|
end
|
|
372
380
|
|
|
373
381
|
nil
|
|
@@ -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
|