active_storage-async_variants 0.8.0 → 0.9.0
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/CHANGELOG.md +6 -0
- data/README.md +5 -48
- data/Rakefile +1 -20
- data/config/routes.rb +0 -17
- data/gemfiles/rails_7.2.gemfile +0 -4
- data/gemfiles/rails_8.0.gemfile +0 -4
- data/gemfiles/rails_8.1.gemfile +0 -4
- data/lib/active_storage/async_variants/version.rb +1 -1
- data/lib/active_storage/async_variants.rb +3 -35
- metadata +2 -47
- data/app/assets/javascripts/progress-bar.js +0 -347
- data/app/assets/javascripts/retry.js +0 -11
- data/app/assets/stylesheets/progress.css +0 -35
- data/app/assets/stylesheets/retry.css +0 -98
- data/app/controllers/active_storage/async_variants/states_controller.rb +0 -71
- data/app/views/active_storage/async_variants/states/_failed.html.erb +0 -44
- data/app/views/active_storage/async_variants/states/_pending.html.erb +0 -1
- data/app/views/active_storage/async_variants/states/_processed.html.erb +0 -5
- data/app/views/active_storage/async_variants/states/_processing.html.erb +0 -15
- data/app/views/active_storage/async_variants/states/show.html.erb +0 -10
- data/features/retry_flow.feature +0 -20
- data/features/retry_visibility.feature +0 -23
- data/features/state_rendering.feature +0 -40
- data/features/step_definitions/dummy_steps.rb +0 -144
- data/features/support/env.rb +0 -28
- data/lib/active_storage/async_variants/asset_tag_helper_extension.rb +0 -59
- data/lib/active_storage/async_variants/helper.rb +0 -80
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Feature: image_tag with async: true emits the right markup per variant state
|
|
2
|
-
|
|
3
|
-
Scenario: A processed variant emits a plain <img> -- no turbo-frame, no chrome
|
|
4
|
-
Given a user with an attached avatar
|
|
5
|
-
And the avatar's :thumb_proc variant is in processed state
|
|
6
|
-
When I visit the avatar page for the :thumb_proc variant
|
|
7
|
-
Then the page should NOT contain a turbo-frame
|
|
8
|
-
|
|
9
|
-
Scenario: An unprocessed variant emits a turbo-frame in processing state
|
|
10
|
-
Given a user with an attached avatar
|
|
11
|
-
And the avatar's :thumb_proc variant is in processing state
|
|
12
|
-
When I visit the avatar page for the :thumb_proc variant
|
|
13
|
-
Then the page should contain a turbo-frame
|
|
14
|
-
And the frame should render the processing state
|
|
15
|
-
|
|
16
|
-
Scenario: A processing variant renders an indeterminate progress bar before any heartbeat
|
|
17
|
-
Given a user with an attached avatar
|
|
18
|
-
And the avatar's :thumb_proc variant is in processing state
|
|
19
|
-
When I visit the avatar page for the :thumb_proc variant
|
|
20
|
-
Then the page should contain a turbo-frame
|
|
21
|
-
And the frame should render a progress bar
|
|
22
|
-
And the progress bar should be indeterminate
|
|
23
|
-
And the placeholder image should reserve layout
|
|
24
|
-
|
|
25
|
-
Scenario: A processing variant with reported progress renders a determinate progress bar
|
|
26
|
-
Given a user with an attached avatar
|
|
27
|
-
And the avatar's :thumb_proc variant is in processing state
|
|
28
|
-
And the avatar's :thumb_proc variant has reported 42% progress
|
|
29
|
-
When I visit the avatar page for the :thumb_proc variant
|
|
30
|
-
Then the page should contain a turbo-frame
|
|
31
|
-
And the progress bar should show 42%
|
|
32
|
-
|
|
33
|
-
Scenario: A failed variant renders the failed partial inside the frame
|
|
34
|
-
Given a user with an attached avatar
|
|
35
|
-
And the retry affordance is visible to everyone
|
|
36
|
-
And the avatar's :thumb_proc variant is in failed state with error "boom"
|
|
37
|
-
When I visit the avatar page for the :thumb_proc variant
|
|
38
|
-
Then the page should contain a turbo-frame
|
|
39
|
-
And the frame should render the failed state
|
|
40
|
-
And the frame should render an error progress bar
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Given /^a user with an attached avatar$/ do
|
|
4
|
-
@user = User.create!
|
|
5
|
-
attach_avatar_to(@user)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
Given /^the avatar's :(\w+) variant is in (pending|processing|processed|failed) state(?: with error "([^"]+)")?$/ do |variant_name, state, error|
|
|
9
|
-
variant = @user.avatar.variant(variant_name.to_sym)
|
|
10
|
-
if state == "processed"
|
|
11
|
-
simulate_processed_variant(variant)
|
|
12
|
-
else
|
|
13
|
-
create_variant_record(variant, state: state, error: error)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
Given /^the avatar's :(\w+) variant has reported (\d+)% progress$/ do |variant_name, percent|
|
|
18
|
-
variant = @user.avatar.variant(variant_name.to_sym)
|
|
19
|
-
record = variant.blob.variant_records.find_by(variation_digest: variant.variation.digest)
|
|
20
|
-
record.update!(progress: percent.to_i)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
Then /^the frame should render a progress bar$/ do
|
|
24
|
-
expect(page).to have_css("turbo-frame progress-bar", visible: :all)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
Then /^the frame should render an error progress bar$/ do
|
|
28
|
-
expect(page).to have_css("turbo-frame progress-bar[error]", visible: :all)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
Then /^the progress bar should be indeterminate$/ do
|
|
32
|
-
expect(page).to have_css("turbo-frame progress-bar:not([percent])", visible: :all)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
Then /^the progress bar should show (\d+)%$/ do |percent|
|
|
36
|
-
expect(page).to have_css(%(turbo-frame progress-bar[percent="#{percent}"]), visible: :all)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Default (visible-only) matcher: passes only because the placeholder is hidden
|
|
40
|
-
# via opacity (not visibility/display), so Capybara still sees it.
|
|
41
|
-
Then /^the placeholder image should reserve layout$/ do
|
|
42
|
-
expect(page).to have_css("turbo-frame .async-variant-processing img")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Given /^the retry affordance is visible to everyone$/ do
|
|
46
|
-
ActiveStorage::AsyncVariants.retry_visible_if { true }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
Given /^the retry affordance is hidden$/ do
|
|
50
|
-
ActiveStorage::AsyncVariants.retry_visible_if { false }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
Given /^the retry affordance requires a signed-in user$/ do
|
|
54
|
-
ActiveStorage::AsyncVariants.retry_visible_if { current_user.present? }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
When /^I sign in as the (admin|user)$/ do |_kind|
|
|
58
|
-
visit "/session/#{@user.id}"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
When /^I visit the avatar page for the :(\w+) variant$/ do |variant_name|
|
|
62
|
-
visit "/avatars/#{@user.id}/#{variant_name}"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
Then /^the page should contain a turbo-frame$/ do
|
|
66
|
-
expect(page).to have_css("turbo-frame")
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
Then /^the page should NOT contain a turbo-frame$/ do
|
|
70
|
-
expect(page).to have_no_css("turbo-frame")
|
|
71
|
-
expect(page).to have_css("img")
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
Then /^the frame should render the (failed|processing|processed) state$/ do |state|
|
|
75
|
-
expect(page).to have_css("turbo-frame .async-variant-#{state}")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
Then /^the retry affordance should be visible$/ do
|
|
79
|
-
expect(page).to have_css("async-variant-retry", visible: :all)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
Then /^the retry affordance should NOT be visible$/ do
|
|
83
|
-
expect(page).to have_no_css("async-variant-retry", visible: :all)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
Then /^the page should not have navigated away$/ do
|
|
87
|
-
expect(page).to have_css("h1#page-marker")
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
Then /^the failure dialog should be open$/ do
|
|
91
|
-
expect(page).to have_css("async-variant-retry", visible: :all)
|
|
92
|
-
open = page.evaluate_script(<<~JS)
|
|
93
|
-
(() => {
|
|
94
|
-
const host = document.querySelector("async-variant-retry")
|
|
95
|
-
if (!host || !host.shadowRoot) return false
|
|
96
|
-
const dialog = host.shadowRoot.querySelector("dialog")
|
|
97
|
-
return dialog ? dialog.open : false
|
|
98
|
-
})()
|
|
99
|
-
JS
|
|
100
|
-
expect(open).to eq(true)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
Then /^the failure dialog should contain "([^"]+)"$/ do |text|
|
|
104
|
-
contains = page.evaluate_script(<<~JS)
|
|
105
|
-
(() => {
|
|
106
|
-
const host = document.querySelector("async-variant-retry")
|
|
107
|
-
if (!host || !host.shadowRoot) return false
|
|
108
|
-
return host.shadowRoot.textContent.includes(#{text.inspect})
|
|
109
|
-
})()
|
|
110
|
-
JS
|
|
111
|
-
expect(contains).to eq(true)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Wait for Turbo to swap the frame and our shim to attach the shadow root.
|
|
115
|
-
def wait_for_shadow!
|
|
116
|
-
Timeout.timeout(5) do
|
|
117
|
-
loop do
|
|
118
|
-
attached = page.evaluate_script("!!document.querySelector('async-variant-retry')?.shadowRoot")
|
|
119
|
-
break if attached
|
|
120
|
-
sleep 0.05
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
When /^I click the retry opener$/ do
|
|
126
|
-
wait_for_shadow!
|
|
127
|
-
page.evaluate_script(<<~JS)
|
|
128
|
-
document.querySelector("async-variant-retry").shadowRoot.querySelector(".opener").click()
|
|
129
|
-
JS
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
When /^I click "Retry processing"$/ do
|
|
133
|
-
wait_for_shadow!
|
|
134
|
-
page.evaluate_script(<<~JS)
|
|
135
|
-
document.querySelector("async-variant-retry").shadowRoot.querySelector("footer button").click()
|
|
136
|
-
JS
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
When /^I close the failure dialog$/ do
|
|
140
|
-
wait_for_shadow!
|
|
141
|
-
page.evaluate_script(<<~JS)
|
|
142
|
-
document.querySelector("async-variant-retry").shadowRoot.querySelector("dialog > button[type=button]").click()
|
|
143
|
-
JS
|
|
144
|
-
end
|
data/features/support/env.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
ENV["RAILS_ENV"] ||= "test"
|
|
4
|
-
require File.expand_path("../../spec/dummy/config/environment", __dir__)
|
|
5
|
-
|
|
6
|
-
require "capybara"
|
|
7
|
-
require "capybara/cucumber"
|
|
8
|
-
require "capybara/cuprite"
|
|
9
|
-
require_relative "../../spec/support/schema"
|
|
10
|
-
|
|
11
|
-
DummySchema.load!
|
|
12
|
-
|
|
13
|
-
Capybara.register_driver(:cuprite) do |app|
|
|
14
|
-
Capybara::Cuprite::Driver.new(app, window_size: [1200, 900], timeout: 10, process_timeout: 20, js_errors: true)
|
|
15
|
-
end
|
|
16
|
-
Capybara.app = Rails.application
|
|
17
|
-
Capybara.javascript_driver = :cuprite
|
|
18
|
-
Capybara.default_driver = :cuprite
|
|
19
|
-
Capybara.server = :puma, { Silent: true }
|
|
20
|
-
|
|
21
|
-
ActionController::Base.allow_forgery_protection = false
|
|
22
|
-
|
|
23
|
-
# Test adapter just records enqueued jobs -- we don't want them to actually run
|
|
24
|
-
# during browser tests (they'd call real transformers and modify variant_records
|
|
25
|
-
# behind the test's back). The retry step seeds the new state explicitly.
|
|
26
|
-
ActiveJob::Base.queue_adapter = :test
|
|
27
|
-
|
|
28
|
-
Before { DummySchema.cleanup! }
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveStorage
|
|
4
|
-
module AsyncVariants
|
|
5
|
-
# Prepended onto ActionView::Helpers::AssetTagHelper. Adds `async:` and
|
|
6
|
-
# `direct:` options to image_tag/video_tag; routes unprocessed variants
|
|
7
|
-
# through a <turbo-frame> whose body lives in StatesController#show.
|
|
8
|
-
module AssetTagHelperExtension
|
|
9
|
-
include ActiveStorage::AsyncVariants::Helper
|
|
10
|
-
|
|
11
|
-
def image_tag(source, options = {})
|
|
12
|
-
async_variant_tag(:image, [source], options) do |urls, opts|
|
|
13
|
-
super(urls.first, opts)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def video_tag(*sources)
|
|
18
|
-
options = sources.extract_options!
|
|
19
|
-
async_variant_tag(:video, sources, options) do |urls, opts|
|
|
20
|
-
super(*urls, opts)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def async_variant_tag(kind, sources, options)
|
|
27
|
-
options = options.symbolize_keys
|
|
28
|
-
async = options.delete(:async)
|
|
29
|
-
direct = options.delete(:direct)
|
|
30
|
-
return yield(sources, options) if !async && !direct
|
|
31
|
-
|
|
32
|
-
variant, *rest = sources
|
|
33
|
-
assert_async_variant!(variant)
|
|
34
|
-
|
|
35
|
-
if async && !async_variant_processed_inline?(variant)
|
|
36
|
-
async_variant_turbo_frame(variant, kind:, direct:, html_options: options)
|
|
37
|
-
else
|
|
38
|
-
yield [async_variant_resolved_src(variant, direct:), *rest], options
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def assert_async_variant!(source)
|
|
43
|
-
unless source.is_a?(ActiveStorage::VariantWithRecord) || source.is_a?(ActiveStorage::Preview)
|
|
44
|
-
raise ArgumentError, "image_tag/video_tag with async:/direct: requires an ActiveStorage::VariantWithRecord or Preview, got #{source.class}"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def async_variant_turbo_frame(variant, kind:, direct:, html_options:)
|
|
49
|
-
content_tag(
|
|
50
|
-
:"turbo-frame",
|
|
51
|
-
"",
|
|
52
|
-
id: async_variant_frame_id(variant),
|
|
53
|
-
src: async_variant_frame_src(variant, kind: kind, direct: direct, html_options: html_options),
|
|
54
|
-
refresh: "morph",
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveStorage
|
|
4
|
-
module AsyncVariants
|
|
5
|
-
# View/controller helper methods. Included in the AssetTagHelperExtension
|
|
6
|
-
# (so image_tag/video_tag can use them) and added to StatesController via
|
|
7
|
-
# `helper ActiveStorage::AsyncVariants::Helper` (so the state partials can
|
|
8
|
-
# use them).
|
|
9
|
-
module Helper
|
|
10
|
-
# Invisible box reserving layout for the progress bar. The image src is a
|
|
11
|
-
# tiny gem GIF whose URL ends in the media filename (no original fetched);
|
|
12
|
-
# the video placeholder carries no <source>.
|
|
13
|
-
def async_variant_placeholder_tag(variant, html_options = {}, kind: :image)
|
|
14
|
-
opts = async_variant_box_dimensions(variant)
|
|
15
|
-
.merge(html_options.symbolize_keys.except(:src, :controls, :autoplay, :preload, :poster))
|
|
16
|
-
if kind == :video
|
|
17
|
-
content_tag(:video, "", opts)
|
|
18
|
-
else
|
|
19
|
-
src = Rails.application.routes.url_helpers.async_variant_placeholder_path(variant.blob.filename.to_s)
|
|
20
|
-
image_tag(src, opts)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def async_variant_box_dimensions(variant)
|
|
25
|
-
resize = variant.variation.transformations
|
|
26
|
-
.values_at(:resize_to_limit, :resize_to_fit, :resize_to_fill)
|
|
27
|
-
.compact.first
|
|
28
|
-
resize.is_a?(Array) ? { width: resize[0], height: resize[1] } : {}
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# In test with non-bucket-backed services, the gem defers to vanilla
|
|
32
|
-
# ActiveStorage (synchronous vips transform) -- inline rendering keeps
|
|
33
|
-
# those environments simple. Otherwise, only inline a normal <img> when
|
|
34
|
-
# the variant has reached the processed terminal state.
|
|
35
|
-
def async_variant_processed_inline?(variant)
|
|
36
|
-
!variant.blob.bucket_backed? || variant.async_state == "processed"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def async_variant_resolved_src(variant, direct:)
|
|
40
|
-
if direct && variant.async_state == "processed"
|
|
41
|
-
async_variant_direct_url(variant)
|
|
42
|
-
else
|
|
43
|
-
variant.processed if variant.blob.bucket_backed?
|
|
44
|
-
async_variant_representation_path(variant)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def async_variant_frame_id(variant)
|
|
49
|
-
digest = variant.variation.digest.gsub(/[^a-zA-Z0-9_-]/, "")
|
|
50
|
-
"async-variant-#{variant.blob.id}-#{digest}"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def async_variant_frame_src(variant, kind:, direct:, html_options: {})
|
|
54
|
-
async_variant_state_path(
|
|
55
|
-
signed_blob_id: variant.blob.signed_id,
|
|
56
|
-
variation_key: variant.variation.key,
|
|
57
|
-
kind:,
|
|
58
|
-
direct:,
|
|
59
|
-
opts: html_options.slice(*PASS_THROUGH_HTML_OPTIONS),
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def async_variant_direct_url(variant)
|
|
64
|
-
if cdn = ActiveStorage::AsyncVariants.cdn_host
|
|
65
|
-
"#{cdn}/#{variant.key}"
|
|
66
|
-
else
|
|
67
|
-
variant.image.url
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def async_variant_representation_path(variant)
|
|
72
|
-
Rails.application.routes.url_helpers.rails_blob_representation_path(
|
|
73
|
-
variant.blob.signed_id,
|
|
74
|
-
variant.variation.key,
|
|
75
|
-
variant.blob.filename.to_s,
|
|
76
|
-
)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|