From 5499cd60f91f77233208c93260801e1bc7f7b25c Mon Sep 17 00:00:00 2001 From: Chris Contolini Date: Fri, 22 Mar 2024 11:46:00 -0400 Subject: [PATCH 1/6] Add TCCP htmx extensions to handle cache busting and filter tracking Adds an htmx extension that adds an `htmx` query param to URLs immediately before htmx makes a request and then removes it before pushing it to the browser's history. This allows endpoints fetched by htmx via AJAX to have a URL that is different from their host page's URL, reducing CDN caching mistaken identity bugs. Also adds an htmx extension store's the filter page's pathname to web storage so that our breadcrumbs can later retrive it. --- cfgov/tccp/jinja2/tccp/cards.html | 1 + .../jinja2/tccp/includes/filter_form.html | 2 +- cfgov/unprocessed/apps/tccp/js/htmx.js | 53 +++++++++++++++++++ cfgov/unprocessed/apps/tccp/js/index.js | 7 +-- esbuild/scripts.js | 1 + 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 cfgov/unprocessed/apps/tccp/js/htmx.js diff --git a/cfgov/tccp/jinja2/tccp/cards.html b/cfgov/tccp/jinja2/tccp/cards.html index 4ed45ec43b4..93da9710734 100644 --- a/cfgov/tccp/jinja2/tccp/cards.html +++ b/cfgov/tccp/jinja2/tccp/cards.html @@ -11,6 +11,7 @@ {{ super() }} + {% endblock %} {% block css %} diff --git a/cfgov/tccp/jinja2/tccp/includes/filter_form.html b/cfgov/tccp/jinja2/tccp/includes/filter_form.html index 5a0f1c0c7a4..9ab6078f8bc 100644 --- a/cfgov/tccp/jinja2/tccp/includes/filter_form.html +++ b/cfgov/tccp/jinja2/tccp/includes/filter_form.html @@ -68,7 +68,7 @@ hx-swap="show:none" hx-indicator=".htmx-container" hx-target=".htmx-results" - hx-push-url="false"> + hx-replace-url="true">
{{ render_form_fields(form) }} diff --git a/cfgov/unprocessed/apps/tccp/js/htmx.js b/cfgov/unprocessed/apps/tccp/js/htmx.js new file mode 100644 index 00000000000..4603fa8fe49 --- /dev/null +++ b/cfgov/unprocessed/apps/tccp/js/htmx.js @@ -0,0 +1,53 @@ +import htmx from 'htmx.org'; + +import webStorageProxy from '../../../js/modules/util/web-storage-proxy'; + +/** + * htmx extension that adds an `htmx=true` query parameter + * to URLs immediately before htmx makes a request and then + * removes it before pushing it to the browser's history. + * This allows endpoints fetched by htmx via AJAX to have + * a URL that is different from their host page's URL, + * reducing CDN caching mistaken identity bugs. + * + * There are other ways to handle htmx cache busting, + * including a `getCacheBusterParam` config option and + * using a `Vary: HX-Request` HTTP header, but we've + * found them to be unreliable with our infrastructure. + * + * See https://htmx.org/docs/#caching + * See https://htmx.org/extensions/ + * See https://htmx.org/events/#htmx:configRequest + * See https://htmx.org/events/#htmx:beforeHistoryUpdate + */ +htmx.defineExtension('htmx-url-param', { + onEvent: function (name, event) { + if (name === 'htmx:configRequest') { + event.detail.parameters.htmx = 'true'; + } + if (name === 'htmx:beforeHistoryUpdate') { + event.detail.history.path = event.detail.history.path.replace( + /&?htmx=true/, + '', + ); + } + }, +}); + +/** + * htmx extension that stores the page's pathname in web + * storage whenever it's updated + * See https://htmx.org/extensions/ + * See https://htmx.org/events/#htmx:replacedInHistory + */ +htmx.defineExtension('store-tccp-filter-path', { + onEvent: function (name, event) { + if (name === 'htmx:replacedInHistory') { + webStorageProxy.setItem('tccp-filter-path', event.detail.path); + } + }, +}); + +// Add htmx extensions to the dom and initialize them +document.body.setAttribute('hx-ext', 'htmx-url-param, store-tccp-filter-path'); +htmx.process(document.body); diff --git a/cfgov/unprocessed/apps/tccp/js/index.js b/cfgov/unprocessed/apps/tccp/js/index.js index 202cf8b0d1b..57040188d11 100644 --- a/cfgov/unprocessed/apps/tccp/js/index.js +++ b/cfgov/unprocessed/apps/tccp/js/index.js @@ -1,13 +1,14 @@ -import htmx from 'htmx.org'; import { attach } from '@cfpb/cfpb-atomic-component'; -// See https://htmx.org/docs/#caching -htmx.config.getCacheBusterParam = true; +import webStorageProxy from '../../../js/modules/util/web-storage-proxy'; /** * Initialize some things. */ function init() { + // Store the card filter query params to web storage for our breadcrumbs + webStorageProxy.setItem('tccp-filter-path', window.location.pathname); + // Attach "show more" click handler attach('show-more', 'click', handleShowMore); } diff --git a/esbuild/scripts.js b/esbuild/scripts.js index 41c9e01f933..7d704937ef5 100644 --- a/esbuild/scripts.js +++ b/esbuild/scripts.js @@ -43,6 +43,7 @@ const jsPaths = [ `${apps}/teachers-digital-platform/js/index.js`, `${apps}/filing-instruction-guide/js/fig-init.js`, `${apps}/tccp/js/index.js`, + `${apps}/tccp/js/htmx.js`, ]; /** From e98ac7973b3ed0a8c1397f70ca16568713ab36c9 Mon Sep 17 00:00:00 2001 From: Chris Contolini Date: Fri, 22 Mar 2024 13:06:07 -0400 Subject: [PATCH 2/6] Add TCCP Cypress E2E tests --- .../credit-cards/explore-cards-helpers.cy.js | 63 +++++++++++++++++++ .../credit-cards/explore-cards.cy.js | 48 ++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 test/cypress/integration/consumer-tools/credit-cards/explore-cards-helpers.cy.js create mode 100644 test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js diff --git a/test/cypress/integration/consumer-tools/credit-cards/explore-cards-helpers.cy.js b/test/cypress/integration/consumer-tools/credit-cards/explore-cards-helpers.cy.js new file mode 100644 index 00000000000..581e3c56cd0 --- /dev/null +++ b/test/cypress/integration/consumer-tools/credit-cards/explore-cards-helpers.cy.js @@ -0,0 +1,63 @@ +export class ExploreCreditCards { + openLandingPage() { + cy.visit('/consumer-tools/credit-cards/explore-cards/'); + } + + openResultsPage(filterParams) { + const url = + '/consumer-tools/credit-cards/explore-cards/cards?' + + new URLSearchParams(filterParams).toString(); + cy.visit(url); + } + + selectCreditTier(tier) { + cy.get('select[name=credit_tier]').select(tier); + } + + selectLocation(location) { + cy.get('select[name=location]').select(location); + } + + selectSituation(situation) { + cy.get('input[name=situations]').check(situation, { force: true }); + } + + clickSubmitButton() { + cy.get('button').contains('See cards for your situation').click(); + } + + openFilterExpandable() { + cy.get('.o-filterable-list-controls button.o-expandable_header').click(); + } + + clickShowMoreButton() { + cy.get('button') + .contains('Show more results with higher interest rates') + .click(); + } + + getNumberResults() { + return new Promise((resolve) => { + return cy + .get('.htmx-container') + .not('.htmx-request') + .get('.o-filterable-list-results .m-notification') + .then((el) => resolve(Number(el.text().replace(/[^0-9]/g, '')))); + }); + } + + getNumberVisibleResults() { + return new Promise((resolve) => { + return cy + .get('.htmx-container') + .not('.htmx-request') + .get('.o-filterable-list-results table tr') + .filter(':visible') + .then((el) => resolve(el.length)); + }); + } + + selectCheckboxFilter(name, value) { + cy.get(`input[name=${name}]`).check(value, { force: true }); + } +} diff --git a/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js b/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js new file mode 100644 index 00000000000..2a7349d0e86 --- /dev/null +++ b/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js @@ -0,0 +1,48 @@ +import { ExploreCreditCards } from './explore-cards-helpers.cy.js'; + +const exploreCards = new ExploreCreditCards(); + +describe('Explore credit cards landing page', () => { + it('should show results tailored to the selected situation', () => { + exploreCards.openLandingPage(); + + exploreCards.selectLocation('FL'); + exploreCards.selectSituation('Earn rewards'); + exploreCards.clickSubmitButton(); + + exploreCards.openFilterExpandable(); + + cy.get('#id_rewards input').should('be.checked'); + }); +}); + +describe('Explore credit cards results page', () => { + it('should update results when user changes filters', () => { + exploreCards.openResultsPage(); + + exploreCards.getNumberResults().then((oldNumResults) => { + exploreCards.selectCheckboxFilter('rewards', 'Cashback rewards'); + exploreCards.getNumberResults().then((newNumResults) => { + expect(newNumResults).to.be.lt(oldNumResults); + }); + }); + }); + it('should show additional results when "Show more" button is clicked', () => { + exploreCards.openResultsPage(); + + exploreCards.getNumberVisibleResults().then((oldNumResults) => { + exploreCards.clickShowMoreButton(); + exploreCards.getNumberVisibleResults().then((newNumResults) => { + expect(oldNumResults).to.be.lt(newNumResults); + }); + }); + }); + it('should link to card detail pages', () => { + exploreCards.openResultsPage(); + + cy.get('td[data-label="Credit card"] a').first().click(); + + cy.get('h1').contains('Customize for your situation').should('not.exist'); + cy.get('h2').contains('Application requirements').should('exist'); + }); +}); From 7ff1324c5970a4e4c158dcddefcf5fcb2eaed651 Mon Sep 17 00:00:00 2001 From: Chris Contolini Date: Fri, 22 Mar 2024 14:38:50 -0400 Subject: [PATCH 3/6] Consolidate all TCCP pathname storage logic in htmx module --- cfgov/unprocessed/apps/tccp/js/htmx.js | 8 +++++++- cfgov/unprocessed/apps/tccp/js/index.js | 4 ---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cfgov/unprocessed/apps/tccp/js/htmx.js b/cfgov/unprocessed/apps/tccp/js/htmx.js index 4603fa8fe49..491a84bed37 100644 --- a/cfgov/unprocessed/apps/tccp/js/htmx.js +++ b/cfgov/unprocessed/apps/tccp/js/htmx.js @@ -35,7 +35,7 @@ htmx.defineExtension('htmx-url-param', { }); /** - * htmx extension that stores the page's pathname in web + * htmx extension that stores the page's path in web * storage whenever it's updated * See https://htmx.org/extensions/ * See https://htmx.org/events/#htmx:replacedInHistory @@ -48,6 +48,12 @@ htmx.defineExtension('store-tccp-filter-path', { }, }); +// Store the path on page load before htmx has started up +webStorageProxy.setItem( + 'tccp-filter-path', + window.location.pathname + window.location.search, +); + // Add htmx extensions to the dom and initialize them document.body.setAttribute('hx-ext', 'htmx-url-param, store-tccp-filter-path'); htmx.process(document.body); diff --git a/cfgov/unprocessed/apps/tccp/js/index.js b/cfgov/unprocessed/apps/tccp/js/index.js index 57040188d11..76fe38049ee 100644 --- a/cfgov/unprocessed/apps/tccp/js/index.js +++ b/cfgov/unprocessed/apps/tccp/js/index.js @@ -1,13 +1,9 @@ import { attach } from '@cfpb/cfpb-atomic-component'; -import webStorageProxy from '../../../js/modules/util/web-storage-proxy'; - /** * Initialize some things. */ function init() { - // Store the card filter query params to web storage for our breadcrumbs - webStorageProxy.setItem('tccp-filter-path', window.location.pathname); // Attach "show more" click handler attach('show-more', 'click', handleShowMore); } From 02a43c379a3398518ec10af581e5141eb4a2fb13 Mon Sep 17 00:00:00 2001 From: Chris Contolini Date: Fri, 22 Mar 2024 15:12:26 -0400 Subject: [PATCH 4/6] Disable card detail test until add we add dummy data --- .../consumer-tools/credit-cards/explore-cards.cy.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js b/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js index 2a7349d0e86..7ff7897f466 100644 --- a/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js +++ b/test/cypress/integration/consumer-tools/credit-cards/explore-cards.cy.js @@ -37,7 +37,8 @@ describe('Explore credit cards results page', () => { }); }); }); - it('should link to card detail pages', () => { + // Disabling this test until we add card test data + xit('should link to card detail pages', () => { exploreCards.openResultsPage(); cy.get('td[data-label="Credit card"] a').first().click(); From 9f805f2d8ffc51e9e93bac6d9f101de0b9dd8e96 Mon Sep 17 00:00:00 2001 From: Chris Contolini Date: Fri, 22 Mar 2024 15:17:53 -0400 Subject: [PATCH 5/6] Update cfgov/unprocessed/apps/tccp/js/htmx.js Co-authored-by: Andy Chosak --- cfgov/unprocessed/apps/tccp/js/htmx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfgov/unprocessed/apps/tccp/js/htmx.js b/cfgov/unprocessed/apps/tccp/js/htmx.js index 491a84bed37..5740bd55bf3 100644 --- a/cfgov/unprocessed/apps/tccp/js/htmx.js +++ b/cfgov/unprocessed/apps/tccp/js/htmx.js @@ -26,8 +26,8 @@ htmx.defineExtension('htmx-url-param', { event.detail.parameters.htmx = 'true'; } if (name === 'htmx:beforeHistoryUpdate') { - event.detail.history.path = event.detail.history.path.replace( - /&?htmx=true/, + event.detail.history.path = event.detail.history.path.replaceAll( + /(&htmx=|(?<=\?)htmx=)true/g, '', ); } From c7160506b755636c5006d9cc0555d9774ef4d817 Mon Sep 17 00:00:00 2001 From: Chris Contolini Date: Fri, 22 Mar 2024 16:21:45 -0400 Subject: [PATCH 6/6] Disable htmx history on TCCP pages --- cfgov/unprocessed/apps/tccp/js/htmx.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cfgov/unprocessed/apps/tccp/js/htmx.js b/cfgov/unprocessed/apps/tccp/js/htmx.js index 5740bd55bf3..902be5d4d84 100644 --- a/cfgov/unprocessed/apps/tccp/js/htmx.js +++ b/cfgov/unprocessed/apps/tccp/js/htmx.js @@ -54,6 +54,14 @@ webStorageProxy.setItem( window.location.pathname + window.location.search, ); +// Disable htmx localStorage cache. We've found CFPB pages are +// large enough that htmx hits the localStorage limit pretty +// quickly and throws harmless-but-annoying `historyCacheError` +// console errors. +// See https://htmx.org/attributes/hx-history/ +// See https://htmx.org/events/#htmx:historyCacheError +document.body.setAttribute('hx-history', 'false'); + // Add htmx extensions to the dom and initialize them document.body.setAttribute('hx-ext', 'htmx-url-param, store-tccp-filter-path'); htmx.process(document.body);