Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ define([
this._bindSubmit();
}
$(this.options.addToCartButtonSelector).prop('disabled', false);
this._on(window, {

/**
* Reset button text and disabled state when the page is restored from the BFCache,
* as the button may be frozen mid-AJAX ("Adding...") or in the post-success state ("Added").
*/
'pageshow': function (event) {
if (event.originalEvent.persisted) {
var addToCartButtonTextDefault = this.options.addToCartButtonTextDefault || $t('Add to Cart'),
addToCartButton = $(this.options.addToCartButtonSelector);

addToCartButton.removeClass(this.options.addToCartButtonDisabledClass);
addToCartButton.find('span').text(addToCartButtonTextDefault);
addToCartButton.prop('title', addToCartButtonTextDefault);
}
}
});
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ define([
'use strict';

return function (config, element) {
$(window).on('pageshow', function (event) {
if (event.originalEvent.persisted) {
$(element).attr('disabled', false);
}
});

$(element).on('click', function (event) {
var cart = customerData.get('cart'),
customer = customerData.get('customer');
Expand Down
12 changes: 12 additions & 0 deletions app/code/Magento/Checkout/view/frontend/web/js/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ define([
};

this._on(this.element, events);
this._on(window, {

/**
* Re-enable the checkout button when the page is restored from the BFCache,
* as it is disabled on click to prevent double-submission during redirect.
*/
'pageshow': function (event) {
if (event.originalEvent.persisted) {
$(this.options.button.checkout).prop('disabled', false);
}
}
});
this._calcHeight();
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ define([
customerData.reload(['cart'], false);
}

$(window).on('pageshow', function (event) {
if (event.originalEvent.persisted) {
addToCartCalls = 0;
self.isLoading(false);
self.closeMinicart();
}
});

return this._super();
},
//jscs:enable requireCamelCaseOrUpperCaseIdentifiers
Expand Down
2 changes: 1 addition & 1 deletion app/code/Magento/Cms/Controller/Noroute/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function execute()
if ($resultPage) {
$resultPage->setStatusHeader(404, '1.1', 'Not Found');
$resultPage->setHeader('Status', '404 File not found');
$resultPage->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0', true);
$resultPage->setHeader('Cache-Control', 'no-cache, must-revalidate, max-age=0', true);
return $resultPage;
} else {
/** @var \Magento\Framework\Controller\Result\Forward $resultForward */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ define([
this.zipInput = $(this.options.selectors.zip, this.element);
this.countrySelect = $(this.options.selectors.country, this.element);

this._on(window, {
'pageshow': function (event) {
if (event.originalEvent.persisted) {
$(this.options.selectors.button, this.element).attr('disabled', false);
}
}
});

this.element.validation({

/**
Expand Down
35 changes: 35 additions & 0 deletions app/code/Magento/Customer/view/frontend/web/js/customer-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,41 @@ define([
customerData.onAjaxComplete(xhr.responseJSON, settings);
});

/**
* Re-run section staleness checks when the page is restored from the BFCache so that
* the cart counter and other section-driven UI reflect any changes made while the page
* was cached (e.g. adding to cart on another page).
*
* Also compares the PHP-rendered login state (baked into the page at render time) with the
* current state in localStorage. If they differ the user logged in or out on another page
* while this one was cached, so a full reload is needed to get fresh server-rendered HTML.
*/
$(window).on('pageshow', function (event) {
var backendLoggedIn, cacheStorage, customerSection, frontendLoggedIn;

if (event.originalEvent.persisted) {
customerData.init();

backendLoggedIn = Boolean(parseInt(options.isLoggedIn, 10) || 0);
frontendLoggedIn = false;

try {
cacheStorage = localStorage.getItem('mage-cache-storage');

if (cacheStorage) {
customerSection = JSON.parse(cacheStorage).customer || null;
frontendLoggedIn = Boolean(customerSection && customerSection.firstname);
}
} catch (e) {
// Ignore JSON parse errors
}

if (frontendLoggedIn !== backendLoggedIn) {
window.location.reload();
}
}
});

/**
* Events listener
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ define([
customerData.reload([], false);
}
});

$(window).on('pageshow', function (event) {
if (event.originalEvent.persisted && !customer().firstname) {
customerData.reload([], false);
}
});
}
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ define([
loginAction.registerLoginCallback(function () {
self.isLoading(false);
});

$(window).on('pageshow', function (event) {
if (event.originalEvent.persisted) {
self.isLoading(false);
}
});
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ define([
*/
_create: function () {
this.element.on('submit', $.proxy(this._showLoader, this));
this._on(window, {
'pageshow': function (event) {
if (event.originalEvent.persisted) {
this.element.find(this.options.pleaseWaitLoader).hide().end()
.find(this.options.placeOrderSubmit).prop('disabled', false).css('opacity', 1);
}
}
});
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ define([

/** @inheritdoc */
initialize: function (config, element) {
var self = this;

this._super();
this.element = element;
$(element).on('change', $.proxy(this.updateSignUpStatus, this));
this.updateSignUpStatus();

$(window).on('pageshow', function (event) {
if (event.originalEvent.persisted) {
$(self.submitButton).prop('disabled', false);
}
});
},

/**
Expand Down
159 changes: 159 additions & 0 deletions app/code/Magento/PageCache/Test/Mftf/Test/BFCacheCompatibilityTest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright 2024 Adobe
* All Rights Reserved.
*/
-->
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">

<!--
Verify that the Cache-Control response header on storefront pages does not contain
the no-store directive. Pages with no-store are ineligible for the browser
Back/Forward Cache (BFCache), so removing it is a prerequisite for BFCache support.
-->
<test name="StorefrontBFCacheNoCacheControlNoStoreHeaderTest">
<annotations>
<features value="PageCache"/>
<stories value="BFCache Compatibility"/>
<title value="Storefront page Cache-Control header must not contain no-store"/>
<description value="Verifies that the Cache-Control response header on a storefront page does not include no-store, which would prevent the browser from storing the page in the Back/Forward Cache."/>
<severity value="CRITICAL"/>
<group value="bfcache"/>
</annotations>

<amOnPage url="/" stepKey="goToStorefront"/>
<waitForPageLoad stepKey="waitForPageLoad"/>
<executeJS function="return (function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', window.location.href, false);
xhr.send(null);
return xhr.getResponseHeader('Cache-Control') || '';
})()" stepKey="cacheControlHeader"/>
<assertStringNotContainsString stepKey="assertNoStoreAbsent">
<expectedResult type="string">no-store</expectedResult>
<actualResult type="variable">cacheControlHeader</actualResult>
</assertStringNotContainsString>
</test>

<!--
Verify that the add-to-cart button is reset to its default enabled state when a
page is restored from the BFCache. The button can be frozen in a disabled
"Adding..." state if the browser caches the page during an in-flight AJAX request.
-->
<test name="StorefrontBFCacheAddToCartButtonResetOnPageShowTest">
<annotations>
<features value="Catalog"/>
<stories value="BFCache Compatibility"/>
<title value="Add-to-cart button is reset to default state on BFCache page restore"/>
<description value="Simulates a product page being frozen in the BFCache while the add-to-cart button was disabled mid-AJAX. Verifies the button is re-enabled and shows the default label after a simulated BFCache restore."/>
<severity value="MAJOR"/>
<group value="bfcache"/>
</annotations>
<before>
<createData entity="SimpleProduct2" stepKey="createProduct"/>
</before>
<after>
<deleteData createDataKey="createProduct" stepKey="deleteProduct"/>
</after>

<amOnPage url="$createProduct.custom_attributes[url_key]$.html" stepKey="goToProductPage"/>
<waitForPageLoad stepKey="waitForProductPageLoad"/>
<waitForElementVisible selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="waitForAddToCartButton"/>

<!-- Simulate the button state frozen mid-AJAX as if cached by BFCache while adding -->
<executeJS function="
var btn = document.querySelector('{{StorefrontProductPageSection.addToCartBtn}}');
if (btn) {
btn.classList.add('disabled');
var span = btn.querySelector('span');
if (span) { span.textContent = 'Adding...'; }
}
" stepKey="freezeButtonState"/>
<seeElement selector="{{StorefrontProductPageSection.addToCartBtn}}.disabled" stepKey="assertButtonDisabledBeforeRestore"/>

<!-- Simulate a BFCache page restore by dispatching a persisted pageshow event -->
<executeJS function="window.dispatchEvent(new PageTransitionEvent('pageshow', {persisted: true, bubbles: false}))" stepKey="simulateBFCacheRestore"/>
<wait time="1" stepKey="waitForPageShowHandlers"/>

<dontSeeElement selector="{{StorefrontProductPageSection.addToCartBtn}}.disabled" stepKey="assertButtonEnabledAfterRestore"/>
<see userInput="Add to Cart" selector="{{StorefrontProductPageSection.addToCartBtn}} span" stepKey="assertButtonLabelReset"/>
</test>

<!--
Verify that the minicart dropdown is closed when a page is restored from the BFCache.
If the user had the minicart open before navigating away, the BFCache snapshot
preserves that open state; the pageshow handler must close it.
-->
<test name="StorefrontBFCacheMinicartClosedOnPageShowTest">
<annotations>
<features value="Checkout"/>
<stories value="BFCache Compatibility"/>
<title value="Minicart dropdown is closed on BFCache page restore"/>
<description value="Opens the minicart dropdown then simulates a BFCache page restore. Verifies the dropdown is automatically closed so users do not see a stale open cart."/>
<severity value="MAJOR"/>
<group value="bfcache"/>
</annotations>
<before>
<createData entity="SimpleProduct2" stepKey="createProduct"/>
</before>
<after>
<deleteData createDataKey="createProduct" stepKey="deleteProduct"/>
</after>

<amOnPage url="$createProduct.custom_attributes[url_key]$.html" stepKey="goToProductPage"/>
<waitForPageLoad stepKey="waitForProductPageLoad"/>

<!-- Add a product to cart so the minicart has content and can be opened -->
<actionGroup ref="StorefrontProductPageAddSimpleProductToCartActionGroup" stepKey="addProductToCart"/>

<!-- Open the minicart dropdown -->
<click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMinicart"/>
<waitForElementVisible selector="{{StorefrontMinicartSection.blockMiniCart}}" stepKey="waitForMinicartOpen"/>
<seeElement selector="{{StorefrontMinicartSection.blockMiniCart}}" stepKey="assertMinicartOpenBeforeRestore"/>

<!-- Simulate a BFCache page restore -->
<executeJS function="window.dispatchEvent(new PageTransitionEvent('pageshow', {persisted: true, bubbles: false}))" stepKey="simulateBFCacheRestore"/>
<wait time="1" stepKey="waitForPageShowHandlers"/>

<waitForElementNotVisible selector="{{StorefrontMinicartSection.blockMiniCart}}" stepKey="assertMinicartClosedAfterRestore"/>
</test>

<!--
Verify that flash messages shown before a navigation are cleared when the page is
restored from the BFCache. Without this, a success or error message triggered by a
previous action would be re-displayed every time the user navigates back.
-->
<test name="StorefrontBFCacheFlashMessagesClearedOnPageShowTest">
<annotations>
<features value="Theme"/>
<stories value="BFCache Compatibility"/>
<title value="Flash messages are cleared on BFCache page restore"/>
<description value="Adds a product to cart to produce a success message, then simulates a BFCache page restore. Verifies the message is not shown again after the restore."/>
<severity value="MAJOR"/>
<group value="bfcache"/>
</annotations>
<before>
<createData entity="SimpleProduct2" stepKey="createProduct"/>
</before>
<after>
<deleteData createDataKey="createProduct" stepKey="deleteProduct"/>
</after>

<amOnPage url="$createProduct.custom_attributes[url_key]$.html" stepKey="goToProductPage"/>
<waitForPageLoad stepKey="waitForProductPageLoad"/>

<!-- Add the product to cart to trigger a success flash message -->
<actionGroup ref="StorefrontProductPageAddSimpleProductToCartActionGroup" stepKey="addProductToCart"/>
<waitForElementVisible selector="div.message-success" stepKey="waitForSuccessMessage"/>
<seeElement selector="div.message-success" stepKey="assertMessageVisibleBeforeRestore"/>

<!-- Simulate a BFCache page restore -->
<executeJS function="window.dispatchEvent(new PageTransitionEvent('pageshow', {persisted: true, bubbles: false}))" stepKey="simulateBFCacheRestore"/>
<wait time="1" stepKey="waitForPageShowHandlers"/>

<dontSeeElement selector="div.message-success" stepKey="assertMessageClearedAfterRestore"/>
</test>

</tests>
6 changes: 2 additions & 4 deletions app/code/Magento/PageCache/etc/varnish4.vcl
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,7 @@ sub vcl_backend_response {

# If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
if (beresp.ttl <= 0s ||
beresp.http.Surrogate-control ~ "no-store" ||
(!beresp.http.Surrogate-Control &&
beresp.http.Cache-Control ~ "no-cache|no-store") ||
beresp.http.Cache-Control ~ "no-cache" ||
beresp.http.Vary == "*") {
# Mark as Hit-For-Pass for the next 2 minutes
set beresp.ttl = 120s;
Expand Down Expand Up @@ -217,7 +215,7 @@ sub vcl_deliver {
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") {
set resp.http.Pragma = "no-cache";
set resp.http.Expires = "-1";
set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
set resp.http.Cache-Control = "no-cache, must-revalidate, max-age=0";
}

if (!resp.http.X-Magento-Debug) {
Expand Down
6 changes: 2 additions & 4 deletions app/code/Magento/PageCache/etc/varnish5.vcl
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ sub vcl_backend_response {

# If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
if (beresp.ttl <= 0s ||
beresp.http.Surrogate-control ~ "no-store" ||
(!beresp.http.Surrogate-Control &&
beresp.http.Cache-Control ~ "no-cache|no-store") ||
beresp.http.Cache-Control ~ "no-cache" ||
beresp.http.Vary == "*") {
# Mark as Hit-For-Pass for the next 2 minutes
set beresp.ttl = 120s;
Expand Down Expand Up @@ -214,7 +212,7 @@ sub vcl_deliver {
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") {
set resp.http.Pragma = "no-cache";
set resp.http.Expires = "-1";
set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
set resp.http.Cache-Control = "no-cache, must-revalidate, max-age=0";
}

if (!resp.http.X-Magento-Debug) {
Expand Down
Loading