diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index bf7d828eb8..384537b9ab 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -9,6 +9,7 @@ //= link components/map/map-test.geojson //= link components/map/maplibre-gl-csp-worker.js //= link views/travel-advice.js +//= link lib/asset-manager-session.js //= link static-error-pages.js diff --git a/app/assets/javascripts/lib/asset-manager-session.js b/app/assets/javascripts/lib/asset-manager-session.js new file mode 100644 index 0000000000..dbddec92d6 --- /dev/null +++ b/app/assets/javascripts/lib/asset-manager-session.js @@ -0,0 +1,33 @@ +/* Warms up the Asset Manager draft-assets session cookie via a single + * placeholder image request, then reloads any draft images already on + * the page once that request has settled (success or failure). + */ +/* istanbul ignore next */ +window.GOVUK = window.GOVUK || {}; + +(function (root) { + function draftAssetImages () { + return [...document.images].filter((image) => + image.src.includes('assets.') // currently, asset urls in draft preview point to their live link: 'assets.xyz' instead of 'draft-assets.xyz'. Created a backlog item for this: https://gov-uk.atlassian.net/browse/WHIT-4008. + ) + } + + function reloadDraftImages (draftAssets) { + draftAssets.forEach((image) => { + // re-setting src tries to fetch the image again, with the hope that a prior success is just served from cache + image.setAttribute('src', image.getAttribute('src')) + }) + } + + root.GOVUK.warmUpAssetManagerSession = function (placeholderAssetUrl) { + const draftAssets = draftAssetImages() + // fewer than 2 images means there's no concurrent-request race to fix, so nothing to warm up + if (draftAssets.length < 2) return + + const reload = () => reloadDraftImages(draftAssets) + const placeholder = new Image() + placeholder.onload = reload + placeholder.onerror = reload + placeholder.src = placeholderAssetUrl + } +}(window)) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index a916665a49..b7a978744b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -24,4 +24,5 @@ <%= yield %> + <%= render "shared/asset_manager_session" %> <% end %> diff --git a/app/views/layouts/full_width.html.erb b/app/views/layouts/full_width.html.erb index e04d2aa1a4..58b0be7822 100644 --- a/app/views/layouts/full_width.html.erb +++ b/app/views/layouts/full_width.html.erb @@ -14,4 +14,5 @@ <%= yield %> + <%= render "shared/asset_manager_session" %> <% end %> diff --git a/app/views/layouts/header_content_sidebar.html.erb b/app/views/layouts/header_content_sidebar.html.erb index 64220aa479..37c6498a86 100644 --- a/app/views/layouts/header_content_sidebar.html.erb +++ b/app/views/layouts/header_content_sidebar.html.erb @@ -60,5 +60,6 @@ <% end %> + <%= render "shared/asset_manager_session" %> <% end %> diff --git a/app/views/layouts/header_sidebar_content.html.erb b/app/views/layouts/header_sidebar_content.html.erb index d686f56961..20646969ea 100644 --- a/app/views/layouts/header_sidebar_content.html.erb +++ b/app/views/layouts/header_sidebar_content.html.erb @@ -34,5 +34,6 @@ + <%= render "shared/asset_manager_session" %> <% end %> diff --git a/app/views/layouts/manual.html.erb b/app/views/layouts/manual.html.erb index 922f0f5a96..09f9ffb045 100644 --- a/app/views/layouts/manual.html.erb +++ b/app/views/layouts/manual.html.erb @@ -17,5 +17,6 @@ <%= yield %> + <%= render "shared/asset_manager_session" %> <% end %> diff --git a/app/views/shared/_asset_manager_session.html.erb b/app/views/shared/_asset_manager_session.html.erb new file mode 100644 index 0000000000..6dfef79d48 --- /dev/null +++ b/app/views/shared/_asset_manager_session.html.erb @@ -0,0 +1,18 @@ +<% if draft_host? %> + <% + host = if GovukEnvironment.current == "staging" + "staging.publishing.service.gov.uk" + elsif GovukEnvironment.current == "integration" + "integration.publishing.service.gov.uk" + else + "publishing.service.gov.uk" + end + placeholder_asset_url = "https://draft-assets.#{host}/media/5e59279b86650c53b2cefbfe/placeholder.jpg" + %> + <%= javascript_include_tag "lib/asset-manager-session.js", integrity: false %> + <%= javascript_tag nonce: true do %> + window.addEventListener("load", function () { + GOVUK.warmUpAssetManagerSession('<%= placeholder_asset_url %>') + }) + <% end %> +<% end %> diff --git a/spec/javascripts/lib/asset-manager-session.spec.js b/spec/javascripts/lib/asset-manager-session.spec.js new file mode 100644 index 0000000000..4d15720fae --- /dev/null +++ b/spec/javascripts/lib/asset-manager-session.spec.js @@ -0,0 +1,90 @@ +describe('GOVUK.warmUpAssetManagerSession', function () { + let originalImage + + const addImage = (src) => { + const img = document.createElement('img') + img.src = src + document.body.appendChild(img) + return img + } + + const trackReloads = (image) => { + const reloadedUrls = [] + const originalSetAttribute = image.setAttribute.bind(image) + image.setAttribute = (name, value) => { + if (name === 'src') reloadedUrls.push(value) + originalSetAttribute(name, value) + } + return reloadedUrls + } + + const stubImage = ({ succeeds }) => { + class FakeImage { + get src () { + return this._src + } + + set src (value) { + this._src = value + succeeds + ? this.onload && this.onload() + : this.onerror && this.onerror() + } + } + window.Image = FakeImage + } + + beforeEach(function () { + originalImage = window.Image + }) + + afterEach(function () { + window.Image = originalImage + document.querySelectorAll('img').forEach((img) => img.remove()) + }) + + it('does nothing when fewer than two draft images are on the page', function () { + addImage('https://draft-assets.test.gov.uk/media/1/one.jpg') + stubImage({ succeeds: true }) + + GOVUK.warmUpAssetManagerSession('https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg') + + expect(document.images[0].src).toEqual( + 'https://draft-assets.test.gov.uk/media/1/one.jpg' + ) + }) + + it('retries all draft images once the placeholder request succeeds', function () { + const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg') + const second = addImage('https://draft-assets.test.gov.uk/media/2/two.jpg?foo=bar') + const firstReloads = trackReloads(first) + const secondReloads = trackReloads(second) + stubImage({ succeeds: true }) + + GOVUK.warmUpAssetManagerSession('https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg') + + expect(firstReloads).toEqual(['https://draft-assets.test.gov.uk/media/1/one.jpg']) + expect(secondReloads).toEqual(['https://draft-assets.test.gov.uk/media/2/two.jpg?foo=bar']) + }) + + it('also retries all draft images when the placeholder request errors', function () { + const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg') + addImage('https://draft-assets.test.gov.uk/media/2/two.jpg') + const firstReloads = trackReloads(first) + stubImage({ succeeds: false }) + + GOVUK.warmUpAssetManagerSession('https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg') + + expect(firstReloads).toEqual(['https://draft-assets.test.gov.uk/media/1/one.jpg']) + }) + + it('ignores non-asset-manager images when counting/retrying', function () { + const other = addImage('https://static.test.gov.uk/media/1/one.jpg') + addImage('https://draft-assets.test.gov.uk/media/2/two.jpg') + stubImage({ succeeds: true }) + + GOVUK.warmUpAssetManagerSession('https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg') + + expect(other.src).toEqual('https://static.test.gov.uk/media/1/one.jpg') + }) +})