Skip to content

Commit

Permalink
fix flakey test (maybe) and try one method of introducing integrating…
Browse files Browse the repository at this point in the history
… fixes from adams path
  • Loading branch information
oscarduignan committed Dec 5, 2024
1 parent afe4dc4 commit 7cef20f
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 51 deletions.
23 changes: 16 additions & 7 deletions lib/browser-tests/puppeteer-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function withHmrcStylesAndScripts(body) {
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<link rel="stylesheet" href="/assets/hmrc-frontend-${version}.min.css">
<link rel="stylesheet" href="/assets/accessible-autocomplete-${version}.css">
${preloadGovukFonts}
</head>
<body class="govuk-template__body">
Expand All @@ -49,19 +50,27 @@ export function withHmrcStylesAndScripts(body) {
</script>
${body}
<script src="/assets/hmrc-frontend-${version}.min.js" type="module"></script>
<script src="/assets/accessible-autocomplete-${version}.js" type="module"></script>
</body>
</html>
`;
}

export async function render(page, body, options) {
await page.setRequestInterception(true);
page.once('request', (req) => {
req.respond({ contentType: 'text/html', body });
page.setRequestInterception(false);
});
await page.goto('http://localhost:3000/', options); // if ever flaky, waitUntil networkidle0 (js loaded)
await page.evaluateHandle('document.fonts.ready');
await page.bringToFront();
const interceptPageRender = (req) => {
if (req.url() === 'http://localhost:3000/') {
return req.respond({ contentType: 'text/html', body });
}
return Promise.resolve().then(() => req.continue()).catch(() => {});
};
page.on('request', interceptPageRender);
try {
await page.goto('http://localhost:3000/', options); // if ever flaky, waitUntil networkidle0 (js loaded)
await page.bringToFront();
} finally {
page.off('request', interceptPageRender);
await page.setRequestInterception(false);
}
return page;
}
12 changes: 12 additions & 0 deletions src/accessible-autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ $govuk-include-default-font-face: false;
.autocomplete__hint {
@include govuk-font($size: 19);
}

// the following is a bit more targeted, wouldn't work in ie11, and might be a bit brittle

.govuk-form-group--error div:has(+ .govuk-select--error[data-module='hmrc-accessible-autocomplete']) > .autocomplete__wrapper .autocomplete__input {
border-color: #d4351c;
}

// the following is more compatible, and probably unlikely to style unintended stuff

//.govuk-form-group--error .autocomplete__wrapper .autocomplete__input {
// border-color: #d4351c;
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import {
delay,
render,
withHmrcStylesAndScripts,
} from '../../../lib/browser-tests/puppeteer-helpers';

const adamsPolyfill = `
// Note - updated to work with the HMRC Frontend implementation
// https://github.com/hmrc/play-frontend-hmrc#adding-accessible-autocomplete-css-and-javascript
if (typeof HMRCAccessibleAutocomplete != 'undefined' && document.querySelector('[data-module="hmrc-accessible-autocomplete"]') != null) {
var originalSelect = document.querySelector('[data-module="hmrc-accessible-autocomplete"]');
// load autocomplete - now handled by the HMRC component wrapper in Twirl
// accessibleAutocomplete.enhanceSelectElement({
// selectElement: originalSelect,
// showAllValues: true
// });
// =====================================================
// Polyfill autocomplete once loaded
// =====================================================
var checkForLoad = setInterval(checkForAutocompleteLoad, 50);
var parentForm = upTo(originalSelect, 'form');
function polyfillAutocomplete(){
var combo = parentForm.querySelector('[role="combobox"]');
// =====================================================
// Update autocomplete once loaded with fallback's aria attributes
// Ensures hint and error are read out before usage instructions
// =====================================================
if(originalSelect && originalSelect.getAttribute('aria-describedby') > ""){
if(parentForm){
if(combo){
combo.setAttribute('aria-describedby', originalSelect.getAttribute('aria-describedby') + ' ' + combo.getAttribute('aria-describedby'));
}
}
}
// =====================================================
// Update autocomplete once loaded with error styling if needed
// This won't work if the autocomplete css is loaded after the frontend library css because
// the autocomplete's border will override the error class's border (they are both the same specificity)
// but we can use the class assigned to build a more specific rule
// =====================================================
setErrorClass();
function setErrorClass(){
if(originalSelect && originalSelect.classList.contains("govuk-select--error")){
if(parentForm){
if(combo){
combo.classList.add("govuk-input--error");
// Also set up an event listener to check for changes to input so we know when to repeat the copy
combo.addEventListener('focus', function(){setErrorClass()});
combo.addEventListener('blur', function(){setErrorClass()});
combo.addEventListener('change', function(){setErrorClass()});
}
}
}
}
// =====================================================
// Ensure when user replaces valid answer with a non-valid answer, then valid answer is not retained
// =====================================================
var holdSubmit = true;
parentForm.addEventListener('submit', function(e){
if(holdSubmit){
e.preventDefault()
if(originalSelect.querySelectorAll('[selected]').length > 0 || originalSelect.value > ""){
var resetSelect = false;
if(originalSelect.value){
if(combo.value != originalSelect.querySelector('option[value="' + originalSelect.value +'"]').text){
resetSelect = true;
}
}
if(resetSelect){
originalSelect.value = "";
if(originalSelect.querySelectorAll('[selected]').length > 0){
originalSelect.querySelectorAll('[selected]')[0].removeAttribute('selected');
}
}
}
holdSubmit = false;
//parentForm.submit();
HTMLFormElement.prototype.submit.call(parentForm); // because submit buttons have id of "submit" which masks the form's natural form.submit() function
}
})
}
function checkForAutocompleteLoad(){
if(parentForm.querySelector('[role="combobox"]')){
clearInterval(checkForLoad)
polyfillAutocomplete();
}
}
}
// Find first ancestor of el with tagName
// or undefined if not found
function upTo(el, tagName) {
tagName = tagName.toLowerCase();
while (el && el.parentNode) {
el = el.parentNode;
if (el.tagName && el.tagName.toLowerCase() == tagName) {
return el;
}
}
// Many DOM methods return null if they don't
// find the element they are searching for
// It would be OK to omit the following and just
// return undefined
return null;
}
`;

describe('Patched accessible autocomplete', () => {
describe('has an aria-describedby attribute linking to the hint and error associated with the original select', () => {
it('should add hint and error before the usage instructions', async () => {
await render(page, withHmrcStylesAndScripts(`
<div class="govuk-form-group govuk-form-group--error">
<label class="govuk-label" for="location">
Choose location
</label>
<div id="location-hint" class="govuk-hint">
This can be different to where you went before
</div>
<p id="location-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> Select a location
</p>
<select class="govuk-select govuk-select--error" id="location" name="location" aria-describedby="location-hint location-error" data-module="hmrc-accessible-autocomplete">
<option value="choose" selected>Choose location</option>
<option value="eastmidlands">East Midlands</option>
<option value="eastofengland">East of England</option>
<option value="london">London</option>
<option value="northeast">North East</option>
<option value="northwest">North West</option>
<option value="southeast">South East</option>
<option value="southwest">South West</option>
<option value="westmidlands">West Midlands</option>
<option value="yorkshire">Yorkshire and the Humber</option>
</select>
</div>
`));

const element = await page.$('#location');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
const ariaDescribedBy = await element.evaluate((el) => el.getAttribute('aria-describedby'));

expect(tagName).not.toBe('select'); // or select element was not enhanced to be an autocomplete component
expect(ariaDescribedBy).toBe('location-hint location-error location__assistiveHint');
});

it('should not be possible for them to be added twice if page is still using adams patch', async () => {
await render(page, withHmrcStylesAndScripts(`
<form>
<div class="govuk-form-group govuk-form-group--error">
<label class="govuk-label" for="location">
Choose location
</label>
<div id="location-hint" class="govuk-hint">
This can be different to where you went before
</div>
<p id="location-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> Select a location
</p>
<select class="govuk-select govuk-select--error" id="location" name="location" aria-describedby="location-hint location-error" data-module="hmrc-accessible-autocomplete">
<option value="choose" selected>Choose location</option>
<option value="eastmidlands">East Midlands</option>
<option value="eastofengland">East of England</option>
<option value="london">London</option>
<option value="northeast">North East</option>
<option value="northwest">North West</option>
<option value="southeast">South East</option>
<option value="southwest">South West</option>
<option value="westmidlands">West Midlands</option>
<option value="yorkshire">Yorkshire and the Humber</option>
</select>
</div>
</form>
`));

await page.evaluate(adamsPolyfill);
await delay(100); // because it takes ~50ms for adam's polyfill to apply

const element = await page.$('#location');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
const ariaDescribedBy = await element.evaluate((el) => el.getAttribute('aria-describedby'));

expect(tagName).not.toBe('select'); // or select element was not enhanced to be an autocomplete component
expect(ariaDescribedBy).toBe('location-hint location-error location__assistiveHint');
});
});
describe('original select has an error', () => {
it('should have the border colour of a gov.uk input with errors', async () => {
await render(page, withHmrcStylesAndScripts(`
<div class="govuk-form-group govuk-form-group--error">
<label class="govuk-label" for="location">
Choose location
</label>
<div id="location-hint" class="govuk-hint">
This can be different to where you went before
</div>
<p id="location-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> Select a location
</p>
<select class="govuk-select govuk-select--error" id="location" name="location" aria-describedby="location-hint location-error" data-module="hmrc-accessible-autocomplete">
<option value="choose" selected>Choose location</option>
<option value="eastmidlands">East Midlands</option>
<option value="eastofengland">East of England</option>
<option value="london">London</option>
<option value="northeast">North East</option>
<option value="northwest">North West</option>
<option value="southeast">South East</option>
<option value="southwest">South West</option>
<option value="westmidlands">West Midlands</option>
<option value="yorkshire">Yorkshire and the Humber</option>
</select>
</div>
`));

const element = await page.$('#location');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
const borderColor = await element.evaluate((el) => getComputedStyle(el).getPropertyValue("border-color"));

// await jestPuppeteer.debug();

expect(tagName).not.toBe('select'); // or select element was not enhanced to be an autocomplete component
expect(borderColor).toBe('rgb(212, 53, 28)');
});
});
});
34 changes: 32 additions & 2 deletions src/components/accessible-autocomplete/accessible-autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@ function AccessibleAutoComplete($module, window, document) {

AccessibleAutoComplete.prototype.init = function init() {
if (this.$module) {
const selectElement = this.$module;
const showAllValues = (this.$module.getAttribute('data-show-all-values') === 'true');
const autoselect = (this.$module.getAttribute('data-auto-select') === 'true');
const defaultValue = this.$module.getAttribute('data-default-value');
const minLength = this.$module.getAttribute('data-min-length');

const configurationOptions = {
selectElement: this.$module,
selectElement,
showAllValues,
autoselect,
defaultValue,
minLength,
};

const language = this.$module.getAttribute('data-language') || 'en';
const language = selectElement.getAttribute('data-language') || 'en';

if (language === 'cy') {
configurationOptions.tAssistiveHint = () => 'Pan fydd canlyniadau awtogwblhau ar gael, defnyddiwch y saethau i fyny ac i lawr i’w hadolygu a phwyswch y fysell ’enter’ i’w dewis.'
Expand All @@ -34,7 +35,36 @@ AccessibleAutoComplete.prototype.init = function init() {
};
}

const selectElementOriginalId = selectElement.id;
const selectElementAriaDescribedBy = selectElement.getAttribute('aria-describedby');

window.HMRCAccessibleAutocomplete.enhanceSelectElement(configurationOptions);

const autocompleteElement = document.getElementById(selectElementOriginalId);
const autocompleteElementAriaDescribedBy = autocompleteElement && autocompleteElement.getAttribute('aria-describedby');

const autocompleteElementMissingAriaDescribedAttrs = (
autocompleteElement
&& autocompleteElement.tagName !== 'select'
&& autocompleteElementAriaDescribedBy
&& selectElementAriaDescribedBy
&& !autocompleteElementAriaDescribedBy.includes(selectElementAriaDescribedBy)
);
if (autocompleteElementMissingAriaDescribedAttrs) {
// if there is a hint and/or error then the autocomplete element
// needs to be aria-describedby these, which it isn't be default
// we need to check if it hasn't already been done to avoid
autocompleteElement.setAttribute(
'aria-describedby',
`${selectElementAriaDescribedBy} ${autocompleteElementAriaDescribedBy}`,
);
// and in case page is still using adam's patch, this should stop
// the select elements aria described by being added to the
// autocomplete element twice when that runs (though unsure if a
// screen reader would actually announce the elements twice if same
// element was listed twice in the aria-describedby attribute)
selectElement.setAttribute('aria-describedby', '');
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { examplePreview } from '../../../lib/url-helpers';
describe('/components/account-menu', () => {
const defaultAccountMenu = examplePreview('account-menu/default');

async function displayStyle(selector) {
function displayStyle(selector) {
return page.$eval(selector, (el) => window.getComputedStyle(el).display);
}

Expand Down
1 change: 1 addition & 0 deletions src/components/back-link-helper/example.njk
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<script type="text/javascript">
Object.defineProperty(document, "referrer", {get : function(){ return window.location.href; }});
</script>

{{ govukBackLink({
attributes: {
"data-module": "hmrc-back-link"
Expand Down
Loading

0 comments on commit 7cef20f

Please sign in to comment.