panda-core 0.8.0 → 0.8.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/app/builders/panda/core/form_builder.rb +20 -8
- data/app/helpers/panda/core/asset_helper.rb +14 -22
- data/lib/panda/core/engine/importmap_config.rb +4 -13
- data/lib/panda/core/testing/rails_helper.rb +8 -1
- data/lib/panda/core/testing/support/system/capybara_setup.rb +62 -0
- data/lib/panda/core/testing/support/system/cuprite_setup.rb +115 -0
- data/lib/panda/core/testing/support/system/database_connection_helpers.rb +23 -0
- data/lib/panda/core/testing/support/system/system_test_helpers.rb +233 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +3 -0
- data/lib/tasks/assets.rake +2 -2
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5104fbc6c2a10dd53ae172533723569d644f2e1af250ffb6893f77c736e62bf7
|
|
4
|
+
data.tar.gz: 0ecf42c34b259b6c9905d13d1da0d4858751c86725a7530a4b3ae64c79aa7c4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ec14592b0c46a9098b044dadeaefb49f06a25c5b27db7b47b51b3cd814927f8b8e7e6e2292745cdda595c628a736c351ad81d785d9d47d78a17ee571ecd6a4a
|
|
7
|
+
data.tar.gz: 96238145f24d276e5443354dd4fbb58925553ef941bc5ff1f0466401202598618d99153167f655ae1f85e8aa7c5b2c47ea27add530ee9a46515039bb824a3c2c
|
|
@@ -11,6 +11,9 @@ module Panda
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def text_field(attribute, options = {})
|
|
14
|
+
# Extract custom label if provided
|
|
15
|
+
custom_label = options.delete(:label)
|
|
16
|
+
|
|
14
17
|
# Add disabled/readonly styling
|
|
15
18
|
field_classes = if options[:readonly] || options[:disabled]
|
|
16
19
|
readonly_input_styles
|
|
@@ -20,7 +23,7 @@ module Panda
|
|
|
20
23
|
|
|
21
24
|
if options.dig(:data, :prefix)
|
|
22
25
|
content_tag :div, class: container_styles do
|
|
23
|
-
label(attribute) + meta_text(options) +
|
|
26
|
+
label(attribute, custom_label) + meta_text(options) +
|
|
24
27
|
content_tag(:div, class: "flex flex-grow") do
|
|
25
28
|
content_tag(:span,
|
|
26
29
|
class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") do
|
|
@@ -31,7 +34,7 @@ module Panda
|
|
|
31
34
|
end
|
|
32
35
|
else
|
|
33
36
|
content_tag :div, class: container_styles do
|
|
34
|
-
label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: field_classes)) + error_message(attribute)
|
|
37
|
+
label(attribute, custom_label) + meta_text(options) + super(attribute, options.reverse_merge(class: field_classes)) + error_message(attribute)
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
|
@@ -49,8 +52,11 @@ module Panda
|
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def text_area(method, options = {})
|
|
55
|
+
# Extract custom label if provided
|
|
56
|
+
custom_label = options.delete(:label)
|
|
57
|
+
|
|
52
58
|
content_tag :div, class: container_styles do
|
|
53
|
-
label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
|
|
59
|
+
label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
|
|
54
60
|
end
|
|
55
61
|
end
|
|
56
62
|
|
|
@@ -61,8 +67,11 @@ module Panda
|
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
def select(method, choices = nil, options = {}, html_options = {})
|
|
70
|
+
# Extract custom label if provided
|
|
71
|
+
custom_label = options.delete(:label)
|
|
72
|
+
|
|
64
73
|
content_tag :div, class: container_styles do
|
|
65
|
-
label(method) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg + error_message(method)
|
|
74
|
+
label(method, custom_label) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg + error_message(method)
|
|
66
75
|
end
|
|
67
76
|
end
|
|
68
77
|
|
|
@@ -84,6 +93,9 @@ module Panda
|
|
|
84
93
|
end
|
|
85
94
|
|
|
86
95
|
def file_field(method, options = {})
|
|
96
|
+
# Extract custom label if provided
|
|
97
|
+
custom_label = options.delete(:label)
|
|
98
|
+
|
|
87
99
|
# Check if cropper is requested
|
|
88
100
|
with_cropper = options.delete(:with_cropper)
|
|
89
101
|
|
|
@@ -99,7 +111,7 @@ module Panda
|
|
|
99
111
|
field_id = "#{object_name}_#{method}"
|
|
100
112
|
|
|
101
113
|
content_tag :div, class: container_styles do
|
|
102
|
-
label(method) +
|
|
114
|
+
label(method, custom_label) +
|
|
103
115
|
meta_text(options) +
|
|
104
116
|
# Cropper stylesheet
|
|
105
117
|
@template.content_tag(:link, nil, rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css") +
|
|
@@ -153,9 +165,9 @@ module Panda
|
|
|
153
165
|
elsif simple_mode
|
|
154
166
|
# Simple file input with basic styling
|
|
155
167
|
content_tag :div, class: container_styles do
|
|
156
|
-
label(method) +
|
|
168
|
+
label(method, custom_label) +
|
|
157
169
|
meta_text(options) +
|
|
158
|
-
super(method, options.reverse_merge(class: "file:rounded file:border-0 file:text-sm file:bg-white file:text-gray-500 hover:file:bg-gray-50 bg-white px-2.5 hover:bg-gray-50"
|
|
170
|
+
super(method, options.reverse_merge(class: "file:rounded file:border-0 file:text-sm file:bg-white file:text-gray-500 hover:file:bg-gray-50 bg-white px-2.5 hover:bg-gray-50 #{input_styles}"))
|
|
159
171
|
end
|
|
160
172
|
else
|
|
161
173
|
# Fancy drag-and-drop UI
|
|
@@ -166,7 +178,7 @@ module Panda
|
|
|
166
178
|
field_id = "#{object_name}_#{method}"
|
|
167
179
|
|
|
168
180
|
content_tag :div, class: "#{container_styles} col-span-full", data: {controller: "file-upload"} do
|
|
169
|
-
label(method) +
|
|
181
|
+
label(method, custom_label) +
|
|
170
182
|
meta_text(options) +
|
|
171
183
|
content_tag(:div, class: "mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10 dark:border-white/25 transition-colors", data: {file_upload_target: "dropzone"}) do
|
|
172
184
|
content_tag(:div, class: "text-center") do
|
|
@@ -22,29 +22,21 @@ module Panda
|
|
|
22
22
|
javascript_include_tag(js_url, type: "module")
|
|
23
23
|
end
|
|
24
24
|
else
|
|
25
|
-
# Development mode -
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"panda/core/controllers/toggle_controller": "/panda/core/controllers/toggle_controller.js",
|
|
40
|
-
"panda/core/controllers/theme_form_controller": "/panda/core/controllers/theme_form_controller.js"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
</script>
|
|
44
|
-
<script type="module" src="/panda/core/application.js"></script>
|
|
45
|
-
<script type="module" src="/panda/core/controllers/index.js"></script>
|
|
25
|
+
# Development mode - Use the engine's importmap (loaded in initializer)
|
|
26
|
+
# This keeps the engine's JavaScript separate from the app's importmap
|
|
27
|
+
# Build the importmap JSON manually since paths are already absolute
|
|
28
|
+
imports = {}
|
|
29
|
+
Panda::Core.importmap.instance_variable_get(:@packages).each do |name, package|
|
|
30
|
+
imports[name] = package[:path]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
importmap_json = JSON.generate({"imports" => imports})
|
|
34
|
+
|
|
35
|
+
<<~HTML.html_safe
|
|
36
|
+
<script type="importmap">#{importmap_json}</script>
|
|
37
|
+
<script type="module">import "panda/core/application"</script>
|
|
38
|
+
<script type="module">import "panda/core/controllers/index"</script>
|
|
46
39
|
HTML
|
|
47
|
-
importmap_html.html_safe
|
|
48
40
|
end
|
|
49
41
|
end
|
|
50
42
|
|
|
@@ -8,20 +8,11 @@ module Panda
|
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
9
|
|
|
10
10
|
included do
|
|
11
|
-
#
|
|
11
|
+
# Load the engine's importmap
|
|
12
|
+
# This keeps the engine's JavaScript separate from the app's importmap
|
|
12
13
|
initializer "panda_core.importmap", before: "importmap" do |app|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
app.config.importmap.paths = app.config.importmap.paths.dup if app.config.importmap.paths.frozen?
|
|
16
|
-
|
|
17
|
-
# Add our paths
|
|
18
|
-
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
19
|
-
|
|
20
|
-
# Handle cache sweepers similarly
|
|
21
|
-
if app.config.importmap.cache_sweepers.frozen?
|
|
22
|
-
app.config.importmap.cache_sweepers = app.config.importmap.cache_sweepers.dup
|
|
23
|
-
end
|
|
24
|
-
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
14
|
+
Panda::Core.importmap = Importmap::Map.new.tap do |map|
|
|
15
|
+
map.draw(Panda::Core::Engine.root.join("config/importmap.rb"))
|
|
25
16
|
end
|
|
26
17
|
end
|
|
27
18
|
end
|
|
@@ -31,7 +31,14 @@ end
|
|
|
31
31
|
# Load all support files from panda-core
|
|
32
32
|
# Files are now in lib/panda/core/testing/support/ to be included in the published gem
|
|
33
33
|
support_path = File.expand_path("../support", __FILE__)
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
# Load system test infrastructure first (Capybara, Cuprite, helpers)
|
|
36
|
+
system_test_files = Dir[File.join(support_path, "system/**/*.rb")].sort
|
|
37
|
+
system_test_files.each { |f| require f }
|
|
38
|
+
|
|
39
|
+
# Load other support files
|
|
40
|
+
other_support_files = Dir[File.join(support_path, "**/*.rb")].sort.reject { |f| f.include?("/system/") }
|
|
41
|
+
other_support_files.each { |f| require f }
|
|
35
42
|
|
|
36
43
|
RSpec.configure do |config|
|
|
37
44
|
# Include panda-core route helpers by default
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared Capybara configuration for all Panda gems
|
|
4
|
+
# This provides standard Capybara setup with sensible defaults
|
|
5
|
+
|
|
6
|
+
# Increase wait time for CI environments where asset loading is slower
|
|
7
|
+
Capybara.default_max_wait_time = ENV["CI"].present? ? 10 : 5
|
|
8
|
+
|
|
9
|
+
# Normalize whitespaces when using `has_text?` and similar matchers,
|
|
10
|
+
# i.e., ignore newlines, trailing spaces, etc.
|
|
11
|
+
# That makes tests less dependent on slight UI changes.
|
|
12
|
+
Capybara.default_normalize_ws = true
|
|
13
|
+
|
|
14
|
+
# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.).
|
|
15
|
+
# It could be useful to be able to configure this path from the outside (e.g., on CI).
|
|
16
|
+
Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara")
|
|
17
|
+
|
|
18
|
+
# Disable animation so we're not waiting for it
|
|
19
|
+
Capybara.disable_animation = true
|
|
20
|
+
|
|
21
|
+
# See SystemTestHelpers#take_screenshot
|
|
22
|
+
# This allows us to track which session was last used for proper screenshot naming
|
|
23
|
+
Capybara.singleton_class.prepend(Module.new do
|
|
24
|
+
attr_accessor :last_used_session
|
|
25
|
+
|
|
26
|
+
def using_session(name, &block)
|
|
27
|
+
self.last_used_session = name
|
|
28
|
+
super
|
|
29
|
+
ensure
|
|
30
|
+
self.last_used_session = nil
|
|
31
|
+
end
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
# Configure server host and port
|
|
35
|
+
Capybara.server_host = "127.0.0.1"
|
|
36
|
+
Capybara.server_port = ENV["CAPYBARA_PORT"]&.to_i # Let Capybara choose if not specified
|
|
37
|
+
|
|
38
|
+
# Configure Puma server with explicit options
|
|
39
|
+
# Use single-threaded mode to share database connection with tests
|
|
40
|
+
Capybara.register_server :puma do |app, port, host|
|
|
41
|
+
require "rack/handler/puma"
|
|
42
|
+
Rack::Handler::Puma.run(app, Port: port, Host: host, Silent: true, Threads: "1:1")
|
|
43
|
+
end
|
|
44
|
+
Capybara.server = :puma
|
|
45
|
+
|
|
46
|
+
# Do not set app_host here - let Capybara determine it from the server
|
|
47
|
+
# This avoids conflicts between what's configured and what's actually running
|
|
48
|
+
|
|
49
|
+
# RSpec configuration for Capybara
|
|
50
|
+
RSpec.configure do |config|
|
|
51
|
+
# Save screenshots on system test failures
|
|
52
|
+
config.after(:each, type: :system) do |example|
|
|
53
|
+
if example.exception
|
|
54
|
+
timestamp = Time.now.strftime("%Y-%m-%d-%H%M%S")
|
|
55
|
+
"tmp/capybara/failures/#{example.full_description.parameterize}_#{timestamp}"
|
|
56
|
+
|
|
57
|
+
# Screenshots are saved automatically by Capybara, but we could save additional artifacts here
|
|
58
|
+
# save_page("#{filename_base}.html")
|
|
59
|
+
# save_screenshot("#{filename_base}.png", full: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ferrum"
|
|
4
|
+
require "capybara/cuprite"
|
|
5
|
+
|
|
6
|
+
# Shared Cuprite driver configuration for all Panda gems
|
|
7
|
+
# This provides standard Cuprite setup with sensible defaults that work across gems
|
|
8
|
+
#
|
|
9
|
+
# Features:
|
|
10
|
+
# - :cuprite driver for standard desktop testing
|
|
11
|
+
# - :cuprite_mobile driver for mobile viewport testing
|
|
12
|
+
# - JavaScript error reporting enabled by default (js_errors: true)
|
|
13
|
+
# - CI-optimized browser options
|
|
14
|
+
# - Environment-based configuration (HEADLESS, INSPECTOR, SLOWMO)
|
|
15
|
+
|
|
16
|
+
module Panda
|
|
17
|
+
module Core
|
|
18
|
+
module Testing
|
|
19
|
+
module CupriteSetup
|
|
20
|
+
# Base Cuprite options shared across all drivers
|
|
21
|
+
def self.base_options
|
|
22
|
+
{
|
|
23
|
+
window_size: [1440, 1000],
|
|
24
|
+
inspector: ENV["INSPECTOR"].in?(%w[y 1 yes true]),
|
|
25
|
+
headless: !ENV["HEADLESS"].in?(%w[n 0 no false]),
|
|
26
|
+
slowmo: ENV["SLOWMO"]&.to_f || 0,
|
|
27
|
+
timeout: 30,
|
|
28
|
+
js_errors: true, # IMPORTANT: Report JavaScript errors as test failures
|
|
29
|
+
ignore_default_browser_options: false,
|
|
30
|
+
process_timeout: 10,
|
|
31
|
+
wait_for_network_idle: false, # Don't wait for all network requests
|
|
32
|
+
pending_connection_errors: false, # Don't fail on pending external connections
|
|
33
|
+
browser_options: {
|
|
34
|
+
"no-sandbox": nil,
|
|
35
|
+
"disable-gpu": nil,
|
|
36
|
+
"disable-dev-shm-usage": nil,
|
|
37
|
+
"disable-background-networking": nil,
|
|
38
|
+
"disable-default-apps": nil,
|
|
39
|
+
"disable-extensions": nil,
|
|
40
|
+
"disable-sync": nil,
|
|
41
|
+
"disable-translate": nil,
|
|
42
|
+
"no-first-run": nil,
|
|
43
|
+
"ignore-certificate-errors": nil,
|
|
44
|
+
"allow-insecure-localhost": nil,
|
|
45
|
+
"enable-features": "NetworkService,NetworkServiceInProcess",
|
|
46
|
+
"disable-blink-features": "AutomationControlled"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Additional options for CI environments
|
|
52
|
+
def self.ci_browser_options
|
|
53
|
+
{
|
|
54
|
+
"disable-web-security": nil,
|
|
55
|
+
"allow-file-access-from-files": nil,
|
|
56
|
+
"allow-file-access": nil
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Configure standard desktop driver
|
|
61
|
+
def self.register_desktop_driver
|
|
62
|
+
options = base_options.dup
|
|
63
|
+
|
|
64
|
+
# Add CI-specific options
|
|
65
|
+
if ENV["GITHUB_ACTIONS"] == "true"
|
|
66
|
+
options[:browser_options].merge!(ci_browser_options)
|
|
67
|
+
|
|
68
|
+
puts "\n🔍 Cuprite Configuration (Desktop):"
|
|
69
|
+
puts " Debug mode: #{ENV["DEBUG"]}"
|
|
70
|
+
puts " Headless: #{options[:headless]}"
|
|
71
|
+
puts " JS Errors: #{options[:js_errors]}"
|
|
72
|
+
puts " Browser options: #{options[:browser_options].keys.join(", ")}"
|
|
73
|
+
puts ""
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Capybara.register_driver :cuprite do |app|
|
|
77
|
+
Capybara::Cuprite::Driver.new(app, **options)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Configure mobile viewport driver
|
|
82
|
+
def self.register_mobile_driver
|
|
83
|
+
options = base_options.dup
|
|
84
|
+
options[:window_size] = [375, 667] # iPhone SE size
|
|
85
|
+
|
|
86
|
+
if ENV["GITHUB_ACTIONS"] == "true"
|
|
87
|
+
options[:browser_options].merge!(ci_browser_options)
|
|
88
|
+
|
|
89
|
+
puts "\n🔍 Cuprite Configuration (Mobile):"
|
|
90
|
+
puts " Window size: #{options[:window_size]}"
|
|
91
|
+
puts " JS Errors: #{options[:js_errors]}"
|
|
92
|
+
puts ""
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Capybara.register_driver :cuprite_mobile do |app|
|
|
96
|
+
Capybara::Cuprite::Driver.new(app, **options)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Register all drivers
|
|
101
|
+
def self.setup!
|
|
102
|
+
register_desktop_driver
|
|
103
|
+
register_mobile_driver
|
|
104
|
+
|
|
105
|
+
# Set default drivers
|
|
106
|
+
Capybara.default_driver = :cuprite
|
|
107
|
+
Capybara.javascript_driver = :cuprite
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Auto-setup when required
|
|
115
|
+
Panda::Core::Testing::CupriteSetup.setup!
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Share database connection between test thread and server thread
|
|
4
|
+
# This allows the Puma server to see uncommitted transaction data from fixtures
|
|
5
|
+
class ActiveRecord::Base
|
|
6
|
+
mattr_accessor :shared_connection
|
|
7
|
+
@@shared_connection = nil
|
|
8
|
+
|
|
9
|
+
def self.connection
|
|
10
|
+
@@shared_connection || retrieve_connection
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Set up shared connection before system tests
|
|
15
|
+
RSpec.configure do |config|
|
|
16
|
+
config.before(:each, type: :system) do
|
|
17
|
+
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
config.after(:each, type: :system) do
|
|
21
|
+
ActiveRecord::Base.shared_connection = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generic system test helpers for Cuprite-based testing
|
|
4
|
+
# These methods work for any Rails application using Cuprite
|
|
5
|
+
|
|
6
|
+
module Panda
|
|
7
|
+
module Core
|
|
8
|
+
module Testing
|
|
9
|
+
module SystemTestHelpers
|
|
10
|
+
# Make failure screenshots compatible with multi-session setup
|
|
11
|
+
def take_screenshot
|
|
12
|
+
return super unless Capybara.last_used_session
|
|
13
|
+
|
|
14
|
+
Capybara.using_session(Capybara.last_used_session) { super }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Ensure page is loaded and stable before interacting
|
|
18
|
+
def ensure_page_loaded
|
|
19
|
+
# Check if we're on about:blank
|
|
20
|
+
current_url = begin
|
|
21
|
+
page.current_url
|
|
22
|
+
rescue
|
|
23
|
+
"unknown"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if current_url.include?("about:blank")
|
|
27
|
+
puts "[CI] Page is on about:blank, skipping recovery to avoid loops" if ENV["GITHUB_ACTIONS"]
|
|
28
|
+
return false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Wait for page to be ready
|
|
32
|
+
wait_for_ready_state
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Wait for document ready state
|
|
37
|
+
def wait_for_ready_state
|
|
38
|
+
Timeout.timeout(5) do
|
|
39
|
+
loop do
|
|
40
|
+
ready = page.evaluate_script("document.readyState")
|
|
41
|
+
break if ready == "complete"
|
|
42
|
+
|
|
43
|
+
sleep 0.1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
rescue Timeout::Error
|
|
47
|
+
puts "[CI] Timeout waiting for document ready state" if ENV["GITHUB_ACTIONS"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Waits for a specific selector to be present and visible
|
|
51
|
+
# @param selector [String] CSS selector to wait for
|
|
52
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 5)
|
|
53
|
+
# @return [Boolean] true if element is found, false if timeout occurs
|
|
54
|
+
def wait_for_selector(selector, timeout: 5)
|
|
55
|
+
start_time = Time.now
|
|
56
|
+
while Time.now - start_time < timeout
|
|
57
|
+
return true if page.has_css?(selector, visible: true)
|
|
58
|
+
|
|
59
|
+
sleep 0.1
|
|
60
|
+
end
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Waits for a specific text to be present on the page
|
|
65
|
+
# @param text [String] Text to wait for
|
|
66
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 5)
|
|
67
|
+
# @return [Boolean] true if text is found, false if timeout occurs
|
|
68
|
+
def wait_for_text(text, timeout: 5)
|
|
69
|
+
start_time = Time.now
|
|
70
|
+
while Time.now - start_time < timeout
|
|
71
|
+
return true if page.has_text?(text)
|
|
72
|
+
|
|
73
|
+
sleep 0.1
|
|
74
|
+
end
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Waits for network requests to complete
|
|
79
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 5)
|
|
80
|
+
# @return [Boolean] true if network is idle, false if timeout occurs
|
|
81
|
+
def wait_for_network_idle(timeout: 5)
|
|
82
|
+
page.driver.wait_for_network_idle(timeout: timeout)
|
|
83
|
+
true
|
|
84
|
+
rescue => e
|
|
85
|
+
puts "[CI] Network idle timeout: #{e.message}" if ENV["GITHUB_ACTIONS"]
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Waits for JavaScript to modify the DOM
|
|
90
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 5)
|
|
91
|
+
# @return [Boolean] true if mutation occurred, false if timeout occurs
|
|
92
|
+
def wait_for_dom_mutation(timeout: 5)
|
|
93
|
+
start_time = Time.now
|
|
94
|
+
initial_dom = page.html
|
|
95
|
+
while Time.now - start_time < timeout
|
|
96
|
+
return true if page.html != initial_dom
|
|
97
|
+
|
|
98
|
+
sleep 0.1
|
|
99
|
+
end
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Drop #pause anywhere in a test to stop the execution
|
|
104
|
+
# Useful when you want to check out the contents of a web page in the middle of a test
|
|
105
|
+
# running in a headful mode
|
|
106
|
+
def pause
|
|
107
|
+
page.driver.pause
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Drop #browser_debug anywhere in a test to open a Chrome inspector and pause the execution
|
|
111
|
+
# Usage: browser_debug(binding)
|
|
112
|
+
def browser_debug(*)
|
|
113
|
+
page.driver.debug
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Allows sending a list of CSS selectors to be clicked on in the correct order (no delay)
|
|
117
|
+
# Useful where you need to trigger e.g. a blur event on an input field
|
|
118
|
+
def click_on_selectors(*css_selectors)
|
|
119
|
+
css_selectors.each do |selector|
|
|
120
|
+
find(selector).click
|
|
121
|
+
sleep 0.1 # Add a small delay to allow JavaScript to run
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Wait for a field to have a specific value
|
|
126
|
+
# @param field_name [String] The field name or label
|
|
127
|
+
# @param value [String] The expected value
|
|
128
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 5)
|
|
129
|
+
def wait_for_field_value(field_name, value, timeout: 5)
|
|
130
|
+
start_time = Time.now
|
|
131
|
+
while Time.now - start_time < timeout
|
|
132
|
+
return true if page.has_field?(field_name, with: value)
|
|
133
|
+
|
|
134
|
+
sleep 0.1
|
|
135
|
+
end
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Configure RSpec to use these helpers
|
|
144
|
+
RSpec.configure do |config|
|
|
145
|
+
config.include Panda::Core::Testing::SystemTestHelpers, type: :system
|
|
146
|
+
|
|
147
|
+
# Make URLs in mailers contain the correct server host
|
|
148
|
+
# This is required for testing links in emails (e.g., via capybara-email)
|
|
149
|
+
config.around(:each, type: :system) do |ex|
|
|
150
|
+
was_host = Rails.application.default_url_options[:host]
|
|
151
|
+
Rails.application.default_url_options[:host] = Capybara.server_host
|
|
152
|
+
ex.run
|
|
153
|
+
Rails.application.default_url_options[:host] = was_host
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Make sure this hook runs before others
|
|
157
|
+
# Means you don't have to set js: true in every system spec
|
|
158
|
+
config.prepend_before(:each, type: :system) do
|
|
159
|
+
driven_by :cuprite
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# CI-specific error handling
|
|
163
|
+
config.around(:each, type: :system) do |example|
|
|
164
|
+
if ENV["GITHUB_ACTIONS"] == "true"
|
|
165
|
+
begin
|
|
166
|
+
example.run
|
|
167
|
+
rescue => e
|
|
168
|
+
puts "[CI] Test error detected: #{e.class} - #{e.message}"
|
|
169
|
+
puts "[CI] Current URL: #{begin
|
|
170
|
+
page.current_url
|
|
171
|
+
rescue
|
|
172
|
+
"unknown"
|
|
173
|
+
end}"
|
|
174
|
+
raise e
|
|
175
|
+
end
|
|
176
|
+
else
|
|
177
|
+
example.run
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Enhanced screenshot capture on failure
|
|
182
|
+
config.after(:each, type: :system) do |example|
|
|
183
|
+
next unless example.exception
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
# Wait for any pending JavaScript to complete
|
|
187
|
+
begin
|
|
188
|
+
page.driver.wait_for_network_idle
|
|
189
|
+
rescue
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
sleep 0.5 # Wait for DOM to be ready
|
|
194
|
+
|
|
195
|
+
# Get comprehensive page info
|
|
196
|
+
page_html = begin
|
|
197
|
+
page.html
|
|
198
|
+
rescue
|
|
199
|
+
"<html><body>Error loading page</body></html>"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
page_title = begin
|
|
203
|
+
page.title
|
|
204
|
+
rescue
|
|
205
|
+
"N/A"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Warn about minimal page content
|
|
209
|
+
if page_html.length < 100
|
|
210
|
+
puts "Warning: Page content appears minimal (#{page_html.length} chars) when taking screenshot"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Use Capybara's save_screenshot method
|
|
214
|
+
screenshot_path = Capybara.save_screenshot
|
|
215
|
+
if screenshot_path
|
|
216
|
+
puts "Screenshot saved to: #{screenshot_path}"
|
|
217
|
+
puts "Page title: #{page_title}"
|
|
218
|
+
puts "Page content length: #{page_html.length} characters"
|
|
219
|
+
|
|
220
|
+
# Save page HTML for debugging in CI
|
|
221
|
+
if ENV["GITHUB_ACTIONS"]
|
|
222
|
+
html_debug_path = screenshot_path.gsub(".png", ".html")
|
|
223
|
+
File.write(html_debug_path, page_html)
|
|
224
|
+
puts "Page HTML saved to: #{html_debug_path}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
rescue => e
|
|
228
|
+
puts "Failed to capture screenshot: #{e.message}"
|
|
229
|
+
puts "Exception class: #{example.exception.class}"
|
|
230
|
+
puts "Exception message: #{example.exception.message}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
data/lib/panda/core/version.rb
CHANGED
data/lib/panda/core.rb
CHANGED
data/lib/tasks/assets.rake
CHANGED
|
@@ -59,7 +59,7 @@ namespace :panda do
|
|
|
59
59
|
|
|
60
60
|
if system(cmd)
|
|
61
61
|
puts "✅ CSS compiled: #{output_file} (#{File.size(output_file)} bytes)"
|
|
62
|
-
puts "📦 Included content from: #{Panda::Core::ModuleRegistry.registered_modules.join(
|
|
62
|
+
puts "📦 Included content from: #{Panda::Core::ModuleRegistry.registered_modules.join(", ")}" if Panda::Core::ModuleRegistry.registered_modules.any?
|
|
63
63
|
else
|
|
64
64
|
puts "❌ CSS compilation failed"
|
|
65
65
|
exit 1
|
|
@@ -82,7 +82,7 @@ namespace :panda do
|
|
|
82
82
|
|
|
83
83
|
if system(cmd)
|
|
84
84
|
puts "✅ CSS compiled: #{versioned_file} (#{File.size(versioned_file)} bytes)"
|
|
85
|
-
puts "📦 Included content from: #{Panda::Core::ModuleRegistry.registered_modules.join(
|
|
85
|
+
puts "📦 Included content from: #{Panda::Core::ModuleRegistry.registered_modules.join(", ")}" if Panda::Core::ModuleRegistry.registered_modules.any?
|
|
86
86
|
|
|
87
87
|
# Create/update unversioned symlink
|
|
88
88
|
symlink = output_dir.join("panda-core.css")
|
metadata
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: panda-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Otaina Limited
|
|
8
8
|
- James Inman
|
|
9
|
+
autorequire:
|
|
9
10
|
bindir: bin
|
|
10
11
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
+
date: 2025-11-12 00:00:00.000000000 Z
|
|
12
13
|
dependencies:
|
|
13
14
|
- !ruby/object:Gem::Dependency
|
|
14
15
|
name: image_processing
|
|
@@ -504,6 +505,10 @@ files:
|
|
|
504
505
|
- lib/panda/core/testing/support/omniauth_setup.rb
|
|
505
506
|
- lib/panda/core/testing/support/service_stubs.rb
|
|
506
507
|
- lib/panda/core/testing/support/setup.rb
|
|
508
|
+
- lib/panda/core/testing/support/system/capybara_setup.rb
|
|
509
|
+
- lib/panda/core/testing/support/system/cuprite_setup.rb
|
|
510
|
+
- lib/panda/core/testing/support/system/database_connection_helpers.rb
|
|
511
|
+
- lib/panda/core/testing/support/system/system_test_helpers.rb
|
|
507
512
|
- lib/panda/core/version.rb
|
|
508
513
|
- lib/tasks/assets.rake
|
|
509
514
|
- lib/tasks/panda/core/migrations.rake
|
|
@@ -520,6 +525,7 @@ metadata:
|
|
|
520
525
|
homepage_uri: https://github.com/tastybamboo/panda-core
|
|
521
526
|
source_code_uri: https://github.com/tastybamboo/panda-core
|
|
522
527
|
changelog_uri: https://github.com/tastybamboo/panda-core/blob/main/CHANGELOG.md
|
|
528
|
+
post_install_message:
|
|
523
529
|
rdoc_options: []
|
|
524
530
|
require_paths:
|
|
525
531
|
- lib
|
|
@@ -534,7 +540,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
534
540
|
- !ruby/object:Gem::Version
|
|
535
541
|
version: '0'
|
|
536
542
|
requirements: []
|
|
537
|
-
rubygems_version: 3.
|
|
543
|
+
rubygems_version: 3.5.22
|
|
544
|
+
signing_key:
|
|
538
545
|
specification_version: 4
|
|
539
546
|
summary: Core libraries and development tools for Tasty Bamboo projects
|
|
540
547
|
test_files: []
|