Skip to content

Commit

Permalink
Merge pull request #8266 from cfpb/htmx-cache
Browse files Browse the repository at this point in the history
Add TCCP htmx extensions to handle cache busting and filter tracking
  • Loading branch information
chosak authored Mar 25, 2024
2 parents ee2323d + c716050 commit 98f07a6
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 5 deletions.
1 change: 1 addition & 0 deletions cfgov/tccp/jinja2/tccp/cards.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
{{ super() }}
<script src="{{ static( 'js/routes/on-demand/filterable-list-controls.js' ) }}"></script>
<script src="{{ static( 'apps/tccp/js/index.js' ) }}"></script>
<script src="{{ static( 'apps/tccp/js/htmx.js' ) }}"></script>
{% endblock %}

{% block css %}
Expand Down
2 changes: 1 addition & 1 deletion cfgov/tccp/jinja2/tccp/includes/filter_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
hx-swap="show:none"
hx-indicator=".htmx-container"
hx-target=".htmx-results"
hx-push-url="false">
hx-replace-url="true">
<div class="content-l">

{{ render_form_fields(form) }}
Expand Down
67 changes: 67 additions & 0 deletions cfgov/unprocessed/apps/tccp/js/htmx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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.replaceAll(
/(&htmx=|(?<=\?)htmx=)true/g,
'',
);
}
},
});

/**
* 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
*/
htmx.defineExtension('store-tccp-filter-path', {
onEvent: function (name, event) {
if (name === 'htmx:replacedInHistory') {
webStorageProxy.setItem('tccp-filter-path', event.detail.path);
}
},
});

// Store the path on page load before htmx has started up
webStorageProxy.setItem(
'tccp-filter-path',
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);
5 changes: 1 addition & 4 deletions cfgov/unprocessed/apps/tccp/js/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import htmx from 'htmx.org';
import { attach } from '@cfpb/cfpb-atomic-component';

// See https://htmx.org/docs/#caching
htmx.config.getCacheBusterParam = true;

/**
* Initialize some things.
*/
function init() {
// Attach "show more" click handler
attach('show-more', 'click', handleShowMore);
}

Expand Down
1 change: 1 addition & 0 deletions esbuild/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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);
});
});
});
// 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();

cy.get('h1').contains('Customize for your situation').should('not.exist');
cy.get('h2').contains('Application requirements').should('exist');
});
});

0 comments on commit 98f07a6

Please sign in to comment.