From ed827848717bce9f1f9da0f663a121f8db3d8b63 Mon Sep 17 00:00:00 2001 From: Romaric Date: Wed, 28 Sep 2022 14:28:34 +0100 Subject: [PATCH 01/14] Refactor character count message generation Introduce a function focused on formatting the message to help with testing --- .../character-count/character-count.mjs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index a60d90aa7e..c84954f01a 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -278,20 +278,16 @@ CharacterCount.prototype.count = function (text) { * @returns {String} Status message */ CharacterCount.prototype.getCountMessage = function () { - var $textarea = this.$textarea - var config = this.config - var remainingNumber = this.maxLength - this.count($textarea.value) + var remainingNumber = this.maxLength - this.count(this.$textarea.value) - var charVerb = 'remaining' - var charNoun = 'character' - var displayNumber = remainingNumber - if (config.maxwords) { - charNoun = 'word' - } - charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's') + var countType = this.config.maxwords ? 'words' : 'characters' + return this.formatCountMessage(remainingNumber, countType) +} - charVerb = (remainingNumber < 0) ? 'too many' : 'remaining' - displayNumber = Math.abs(remainingNumber) +CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) { + var charVerb = (remainingNumber < 0) ? 'too many' : 'remaining' + var displayNumber = Math.abs(remainingNumber) + var charNoun = displayNumber === 1 ? countType.substring(0, countType.length - 1) : countType return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb } From fcd3ec1357d49579885a3e2b8a1fb66711b440de Mon Sep 17 00:00:00 2001 From: Romaric Date: Wed, 28 Sep 2022 14:51:27 +0100 Subject: [PATCH 02/14] Add unit test for count message formatting --- .../character-count.unit.test.mjs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/govuk/components/character-count/character-count.unit.test.mjs diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs new file mode 100644 index 0000000000..1b1bdbb37b --- /dev/null +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -0,0 +1,47 @@ +/** + * @jest-environment jsdom + */ + +import CharacterCount from './character-count.mjs' + +describe('CharacterCount', () => { + describe('formatCountMessage', () => { + let component + beforeAll(() => { + // The component won't initialise if we don't pass it an element + component = new CharacterCount(document.createElement('div')) + }) + + it('formats singular remaining characters', () => { + expect(component.formatCountMessage(1, 'characters')).toEqual('You have 1 character remaining') + }) + it('formats plural remaining characters', () => { + expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') + }) + it('formats singular exceeding characters', () => { + expect(component.formatCountMessage(-1, 'characters')).toEqual('You have 1 character too many') + }) + it('formats plural exceeding characters', () => { + expect(component.formatCountMessage(-10, 'characters')).toEqual('You have 10 characters too many') + }) + it('formats character limit being met', () => { + expect(component.formatCountMessage(0, 'characters')).toEqual('You have 0 characters remaining') + }) + + it('formats singular remaining words', () => { + expect(component.formatCountMessage(1, 'words')).toEqual('You have 1 word remaining') + }) + it('formats plural remaining words', () => { + expect(component.formatCountMessage(10, 'words')).toEqual('You have 10 words remaining') + }) + it('formats singular exceeding words', () => { + expect(component.formatCountMessage(-1, 'words')).toEqual('You have 1 word too many') + }) + it('formats plural exceeding words', () => { + expect(component.formatCountMessage(-10, 'words')).toEqual('You have 10 words too many') + }) + it('formats word limit being met', () => { + expect(component.formatCountMessage(0, 'words')).toEqual('You have 0 words remaining') + }) + }) +}) From 9e8e2d1296796655cb48c9436b9f155b264fdb10 Mon Sep 17 00:00:00 2001 From: Romaric Date: Wed, 28 Sep 2022 15:05:56 +0100 Subject: [PATCH 03/14] Use I18n for formatting count message --- .../character-count/character-count.mjs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index c84954f01a..35be8e425d 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -2,7 +2,8 @@ import '../../vendor/polyfills/Date/now.mjs' import '../../vendor/polyfills/Function/prototype/bind.mjs' import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normalisation import '../../vendor/polyfills/Element/prototype/classList.mjs' -import { mergeConfigs, normaliseDataset } from '../../common.mjs' +import { extractConfigByNamespace, mergeConfigs, normaliseDataset } from '../../common.mjs' +import { I18n } from '../../i18n.mjs' /** * JavaScript enhancements for the CharacterCount component @@ -31,7 +32,21 @@ function CharacterCount ($module, config) { } var defaultConfig = { - threshold: 0 + threshold: 0, + i18n: { + // Characters + charactersUnderLimitZero: 'You have %{count} characters remaining', + charactersUnderLimitOne: 'You have %{count} character remaining', + charactersUnderLimitOther: 'You have %{count} characters remaining', + charactersOverLimitOne: 'You have %{count} character too many', + charactersOverLimitOther: 'You have %{count} characters too many', + // Words + wordsUnderLimitOne: 'You have %{count} word remaining', + wordsUnderLimitOther: 'You have %{count} words remaining', + wordsAtLimitOther: 'You have 0 words remaining', + wordsOverLimitOne: 'You have %{count} word too many', + wordsOverLimitOther: 'You have %{count} words too many' + } } // Read config set using dataset ('data-' values) @@ -58,6 +73,8 @@ function CharacterCount ($module, config) { datasetConfig ) + this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n')) + // Determine the limit attribute (characters or words) if (this.config.maxwords) { this.maxLength = this.config.maxwords @@ -285,11 +302,10 @@ CharacterCount.prototype.getCountMessage = function () { } CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) { - var charVerb = (remainingNumber < 0) ? 'too many' : 'remaining' - var displayNumber = Math.abs(remainingNumber) - var charNoun = displayNumber === 1 ? countType.substring(0, countType.length - 1) : countType + var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit' + var translationKey = countType + translationKeySuffix - return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb + return this.i18n.t(translationKey, { count: Math.abs(remainingNumber) }) } /** From 04008109b8bc32c39780c191fba95da304cc0cda Mon Sep 17 00:00:00 2001 From: Romaric Date: Wed, 28 Sep 2022 15:47:41 +0100 Subject: [PATCH 04/14] Add test for passing different translations --- .../character-count.unit.test.mjs | 115 +++++++++++++----- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index 1b1bdbb37b..7940233abd 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -6,42 +6,91 @@ import CharacterCount from './character-count.mjs' describe('CharacterCount', () => { describe('formatCountMessage', () => { - let component - beforeAll(() => { - // The component won't initialise if we don't pass it an element - component = new CharacterCount(document.createElement('div')) - }) + describe('default configuration', () => { + let component + beforeAll(() => { + // The component won't initialise if we don't pass it an element + component = new CharacterCount(createElement('div')) + }) - it('formats singular remaining characters', () => { - expect(component.formatCountMessage(1, 'characters')).toEqual('You have 1 character remaining') - }) - it('formats plural remaining characters', () => { - expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') - }) - it('formats singular exceeding characters', () => { - expect(component.formatCountMessage(-1, 'characters')).toEqual('You have 1 character too many') - }) - it('formats plural exceeding characters', () => { - expect(component.formatCountMessage(-10, 'characters')).toEqual('You have 10 characters too many') - }) - it('formats character limit being met', () => { - expect(component.formatCountMessage(0, 'characters')).toEqual('You have 0 characters remaining') - }) + it('formats singular remaining characters', () => { + expect(component.formatCountMessage(1, 'characters')).toEqual('You have 1 character remaining') + }) + it('formats plural remaining characters', () => { + expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') + }) + it('formats singular exceeding characters', () => { + expect(component.formatCountMessage(-1, 'characters')).toEqual('You have 1 character too many') + }) + it('formats plural exceeding characters', () => { + expect(component.formatCountMessage(-10, 'characters')).toEqual('You have 10 characters too many') + }) + it('formats character limit being met', () => { + expect(component.formatCountMessage(0, 'characters')).toEqual('You have 0 characters remaining') + }) - it('formats singular remaining words', () => { - expect(component.formatCountMessage(1, 'words')).toEqual('You have 1 word remaining') - }) - it('formats plural remaining words', () => { - expect(component.formatCountMessage(10, 'words')).toEqual('You have 10 words remaining') - }) - it('formats singular exceeding words', () => { - expect(component.formatCountMessage(-1, 'words')).toEqual('You have 1 word too many') + it('formats singular remaining words', () => { + expect(component.formatCountMessage(1, 'words')).toEqual('You have 1 word remaining') + }) + it('formats plural remaining words', () => { + expect(component.formatCountMessage(10, 'words')).toEqual('You have 10 words remaining') + }) + it('formats singular exceeding words', () => { + expect(component.formatCountMessage(-1, 'words')).toEqual('You have 1 word too many') + }) + it('formats plural exceeding words', () => { + expect(component.formatCountMessage(-10, 'words')).toEqual('You have 10 words too many') + }) + it('formats word limit being met', () => { + expect(component.formatCountMessage(0, 'words')).toEqual('You have 0 words remaining') + }) }) - it('formats plural exceeding words', () => { - expect(component.formatCountMessage(-10, 'words')).toEqual('You have 10 words too many') - }) - it('formats word limit being met', () => { - expect(component.formatCountMessage(0, 'words')).toEqual('You have 0 words remaining') + + describe('i18n', () => { + describe('JavaScript configuration', () => { + it('overrides the default configuration', () => { + const component = new CharacterCount(createElement('div'), { + i18n: { charactersUnderLimitOne: 'Custom text. Count: %{count}' }, + 'i18n.charactersOverLimitOther': 'Different custom text. Count: %{count}' + }) + expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') + expect(component.formatCountMessage(-10, 'characters')).toEqual('Different custom text. Count: 10') + // Other keys remain untouched + expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') + }) + }) + + describe('Data attribute configuration', () => { + it('overrides the default configuration', () => { + const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) + const component = new CharacterCount($div) + expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') + // Other keys remain untouched + expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') + }) + + it('overrides the JavaScript configuration', () => { + const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) + const component = new CharacterCount($div, { + i18n: { + charactersUnderLimitOne: 'Different custom text. Count: %{count}' + } + }) + expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') + // Other keys remain untouched + expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') + }) + }) }) }) }) + +function createElement (tagName, { ...attributes } = {}) { + const el = document.createElement(tagName) + + Object.entries(attributes).forEach(([attributeName, attributeValue]) => { + el.setAttribute(attributeName, attributeValue) + }) + + return el +} From 71b7a3bac9945227390bb96b10b2474266602edf Mon Sep 17 00:00:00 2001 From: Romaric Date: Wed, 28 Sep 2022 17:07:37 +0100 Subject: [PATCH 05/14] Add tests for formatting of numbers in CharacterCount Provided by the I18n object --- .../character-count/character-count.mjs | 2 +- .../character-count.unit.test.mjs | 66 +++++++++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index 35be8e425d..93eff08478 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -73,7 +73,7 @@ function CharacterCount ($module, config) { datasetConfig ) - this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n')) + this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), { locale: this.config.i18nLocale }) // Determine the limit attribute (characters or words) if (this.config.maxwords) { diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index 7940233abd..783ebb41ab 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -44,11 +44,16 @@ describe('CharacterCount', () => { it('formats word limit being met', () => { expect(component.formatCountMessage(0, 'words')).toEqual('You have 0 words remaining') }) + + it('formats the count', () => { + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10,000 words remaining') + expect(component.formatCountMessage(-10000, 'words')).toEqual('You have 10,000 words too many') + }) }) describe('i18n', () => { describe('JavaScript configuration', () => { - it('overrides the default configuration', () => { + it('overrides the default translation keys', () => { const component = new CharacterCount(createElement('div'), { i18n: { charactersUnderLimitOne: 'Custom text. Count: %{count}' }, 'i18n.charactersOverLimitOther': 'Different custom text. Count: %{count}' @@ -58,10 +63,17 @@ describe('CharacterCount', () => { // Other keys remain untouched expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) + + it('overrides the default locale', () => { + const component = new CharacterCount(createElement('div'), { + i18nLocale: 'de' + }) + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') + }) }) describe('Data attribute configuration', () => { - it('overrides the default configuration', () => { + it('overrides the default translation keys', () => { const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) const component = new CharacterCount($div) expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') @@ -69,23 +81,53 @@ describe('CharacterCount', () => { expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) - it('overrides the JavaScript configuration', () => { - const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) - const component = new CharacterCount($div, { - i18n: { - charactersUnderLimitOne: 'Different custom text. Count: %{count}' - } + it('overrides the default locale', () => { + const component = new CharacterCount(createElement('div', { + 'data-i18n-locale': 'de' + })) + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') + }) + + describe('precedence over JavaScript configuration', () => { + it('overrides translation keys', () => { + const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) + const component = new CharacterCount($div, { + i18n: { + charactersUnderLimitOne: 'Different custom text. Count: %{count}' + } + }) + expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') + // Other keys remain untouched + expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') + }) + + it('overrides the default locale', () => { + const $div = createElement('div', { + 'data-i18n-locale': 'de' // Dot as thousand separator + }) + const component = new CharacterCount($div, { + i18nLocale: 'fr' // Space as thousand separator + }) + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') }) - expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') - // Other keys remain untouched - expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) }) }) }) }) -function createElement (tagName, { ...attributes } = {}) { +/** + * Creates an element with the given attributes + * + * TODO: Extract to a dom-helpers.mjs file if necessary to share + * TODO: Extend to allow passing props and children if necessary + * TODO: Make available to components once support for older browsers is dropped (add tests) + * + * @param {String} tagName + * @param {Object} attributes + * @returns HTMLElement + */ +function createElement (tagName, attributes = {}) { const el = document.createElement(tagName) Object.entries(attributes).forEach(([attributeName, attributeValue]) => { From 88fcbc3a3b92bf6ce824c1819824cfb3dbcf05b9 Mon Sep 17 00:00:00 2001 From: Romaric Date: Thu, 29 Sep 2022 12:24:18 +0100 Subject: [PATCH 06/14] Add separate translation key when limit is met --- .../components/character-count/character-count.mjs | 11 +++++++---- .../character-count/character-count.unit.test.mjs | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index 93eff08478..78651b6471 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -35,15 +35,15 @@ function CharacterCount ($module, config) { threshold: 0, i18n: { // Characters - charactersUnderLimitZero: 'You have %{count} characters remaining', charactersUnderLimitOne: 'You have %{count} character remaining', charactersUnderLimitOther: 'You have %{count} characters remaining', + charactersAtLimit: 'You have no characters remaining', charactersOverLimitOne: 'You have %{count} character too many', charactersOverLimitOther: 'You have %{count} characters too many', // Words wordsUnderLimitOne: 'You have %{count} word remaining', wordsUnderLimitOther: 'You have %{count} words remaining', - wordsAtLimitOther: 'You have 0 words remaining', + wordsAtLimit: 'You have no words remaining', wordsOverLimitOne: 'You have %{count} word too many', wordsOverLimitOther: 'You have %{count} words too many' } @@ -302,10 +302,13 @@ CharacterCount.prototype.getCountMessage = function () { } CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) { + if (remainingNumber === 0) { + return this.i18n.t(countType + 'AtLimit') + } + var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit' - var translationKey = countType + translationKeySuffix - return this.i18n.t(translationKey, { count: Math.abs(remainingNumber) }) + return this.i18n.t(countType + translationKeySuffix, { count: Math.abs(remainingNumber) }) } /** diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index 783ebb41ab..1e90a44f6c 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -26,7 +26,7 @@ describe('CharacterCount', () => { expect(component.formatCountMessage(-10, 'characters')).toEqual('You have 10 characters too many') }) it('formats character limit being met', () => { - expect(component.formatCountMessage(0, 'characters')).toEqual('You have 0 characters remaining') + expect(component.formatCountMessage(0, 'characters')).toEqual('You have no characters remaining') }) it('formats singular remaining words', () => { @@ -42,7 +42,7 @@ describe('CharacterCount', () => { expect(component.formatCountMessage(-10, 'words')).toEqual('You have 10 words too many') }) it('formats word limit being met', () => { - expect(component.formatCountMessage(0, 'words')).toEqual('You have 0 words remaining') + expect(component.formatCountMessage(0, 'words')).toEqual('You have no words remaining') }) it('formats the count', () => { From 66d8a1b13495d7e2066f33b689bd33c87779c6e5 Mon Sep 17 00:00:00 2001 From: Romaric Date: Thu, 29 Sep 2022 13:01:40 +0100 Subject: [PATCH 07/14] Default character count locale to closest 'lang' attribute Commit is a bit bulky for what sounds a simple thing, but it's mostly behind the scene logistics: - extracting the helper for creating dom elements in its own file - introducing a function to get the value of an attribute from the closest parent with it --- lib/dom-helpers.mjs | 19 +++++++ src/govuk/common.mjs | 14 +++++ src/govuk/common.unit.test.mjs | 31 ++++++++++- .../character-count/character-count.mjs | 8 ++- .../character-count.unit.test.mjs | 52 +++++++++++-------- 5 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 lib/dom-helpers.mjs diff --git a/lib/dom-helpers.mjs b/lib/dom-helpers.mjs new file mode 100644 index 0000000000..a36f66781f --- /dev/null +++ b/lib/dom-helpers.mjs @@ -0,0 +1,19 @@ +/** + * Creates an element with the given attributes + * + * TODO: Extend to allow passing props and children if necessary + * TODO: Make available to components once support for older browsers is dropped (add tests) + * + * @param {String} tagName + * @param {Object} attributes + * @returns HTMLElement + */ +export function createElement (tagName, attributes = {}) { + const el = document.createElement(tagName) + + Object.entries(attributes).forEach(([attributeName, attributeValue]) => { + el.setAttribute(attributeName, attributeValue) + }) + + return el +} diff --git a/src/govuk/common.mjs b/src/govuk/common.mjs index 22f23ddd53..abe0925bbd 100644 --- a/src/govuk/common.mjs +++ b/src/govuk/common.mjs @@ -1,5 +1,6 @@ import './vendor/polyfills/Element/prototype/dataset.mjs' import './vendor/polyfills/String/prototype/trim.mjs' +import './vendor/polyfills/Element/prototype/closest.mjs' /** * TODO: Ideally this would be a NodeList.prototype.forEach polyfill @@ -186,3 +187,16 @@ export function normaliseDataset (dataset) { return out } + +/** + * Returns the value of the `lang` attribute closest to the given element (including itself) + * + * @param {HTMLElement} $element - The element to start walking the DOM tree up + * @returns {String|undefined} + */ +export function closestAttributeValue ($element, attributeName) { + var closestElementWithAttribute = $element.closest('[' + attributeName + ']') + if (closestElementWithAttribute) { + return closestElementWithAttribute.getAttribute(attributeName) + } +} diff --git a/src/govuk/common.unit.test.mjs b/src/govuk/common.unit.test.mjs index 83338d5e7c..c28f63d90e 100644 --- a/src/govuk/common.unit.test.mjs +++ b/src/govuk/common.unit.test.mjs @@ -2,7 +2,8 @@ * @jest-environment jsdom */ -import { mergeConfigs, extractConfigByNamespace, normaliseString, normaliseDataset } from './common.mjs' +import { createElement } from '../../lib/dom-helpers.mjs' +import { mergeConfigs, extractConfigByNamespace, normaliseString, normaliseDataset, closestAttributeValue } from './common.mjs' // TODO: Write unit tests for `nodeListForEach` and `generateUniqueID` @@ -216,4 +217,32 @@ describe('Common JS utilities', () => { }) }) }) + + describe('closestAttributeValue', () => { + it('returns the value of the attribute if on the element', () => { + const $element = createElement('div', { lang: 'en-GB' }) + + expect(closestAttributeValue($element, 'lang')).toEqual('en-GB') + }) + + it('returns the value of the closest parent with the attribute if it exists', () => { + const $greatGrandParent = createElement('div', { lang: 'cy-GB' }) // To check that we take the value up + const $grandparent = createElement('div', { lang: 'en-GB' }) + const $parent = createElement('div') // To check that we walk up the tree + const $element = createElement('div') + $greatGrandParent.appendChild($grandparent) + $grandparent.appendChild($parent) + $parent.appendChild($element) + + expect(closestAttributeValue($element, 'lang')).toEqual('en-GB') + }) + + it('returns undefined if neither the element or a parent have the attribute', () => { + const $parent = createElement('div') + const $element = createElement('div') + $parent.appendChild($element) + + expect(closestAttributeValue($element, 'lang')).toBeUndefined() + }) + }) }) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index 78651b6471..c6d68f0a44 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -2,7 +2,7 @@ import '../../vendor/polyfills/Date/now.mjs' import '../../vendor/polyfills/Function/prototype/bind.mjs' import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normalisation import '../../vendor/polyfills/Element/prototype/classList.mjs' -import { extractConfigByNamespace, mergeConfigs, normaliseDataset } from '../../common.mjs' +import { closestAttributeValue, extractConfigByNamespace, mergeConfigs, normaliseDataset } from '../../common.mjs' import { I18n } from '../../i18n.mjs' /** @@ -73,7 +73,11 @@ function CharacterCount ($module, config) { datasetConfig ) - this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), { locale: this.config.i18nLocale }) + console.log(this.$module) + this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), { + // Read the fallback if necessary rather than have it set in the defaults + locale: this.config.i18nLocale || closestAttributeValue($module, 'lang') + }) // Determine the limit attribute (characters or words) if (this.config.maxwords) { diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index 1e90a44f6c..02ef8d308b 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -2,6 +2,7 @@ * @jest-environment jsdom */ +import { createElement } from '../../../../lib/dom-helpers.mjs' import CharacterCount from './character-count.mjs' describe('CharacterCount', () => { @@ -72,6 +73,26 @@ describe('CharacterCount', () => { }) }) + describe('lang attribute configuration', () => { + it('overrides the locale when set on the element', () => { + const $div = createElement('div', { + lang: 'de' + }) + const component = new CharacterCount($div) + + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') + }) + + it('overrides the locale when set on an ancestor', () => { + const $parent = createElement('div', { lang: 'de' }) + const $div = createElement('div') + $parent.appendChild($div) + const component = new CharacterCount($div) + + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') + }) + }) + describe('Data attribute configuration', () => { it('overrides the default translation keys', () => { const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) @@ -110,29 +131,18 @@ describe('CharacterCount', () => { }) expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') }) + + it('overrides the locale in lang attribute', () => { + const $div = createElement('div', { + 'data-i18n-locale': 'de', + lang: 'fr' + }) + const component = new CharacterCount($div) + + expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') + }) }) }) }) }) }) - -/** - * Creates an element with the given attributes - * - * TODO: Extract to a dom-helpers.mjs file if necessary to share - * TODO: Extend to allow passing props and children if necessary - * TODO: Make available to components once support for older browsers is dropped (add tests) - * - * @param {String} tagName - * @param {Object} attributes - * @returns HTMLElement - */ -function createElement (tagName, attributes = {}) { - const el = document.createElement(tagName) - - Object.entries(attributes).forEach(([attributeName, attributeValue]) => { - el.setAttribute(attributeName, attributeValue) - }) - - return el -} From 74d3b94af2e155e83145a881ba365bbad24a2992 Mon Sep 17 00:00:00 2001 From: Romaric Date: Thu, 29 Sep 2022 18:27:10 +0100 Subject: [PATCH 08/14] Update JSDoc --- lib/dom-helpers.mjs | 3 -- src/govuk/common.mjs | 3 +- .../character-count/character-count.mjs | 29 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/dom-helpers.mjs b/lib/dom-helpers.mjs index a36f66781f..56c8f4dffb 100644 --- a/lib/dom-helpers.mjs +++ b/lib/dom-helpers.mjs @@ -1,9 +1,6 @@ /** * Creates an element with the given attributes * - * TODO: Extend to allow passing props and children if necessary - * TODO: Make available to components once support for older browsers is dropped (add tests) - * * @param {String} tagName * @param {Object} attributes * @returns HTMLElement diff --git a/src/govuk/common.mjs b/src/govuk/common.mjs index abe0925bbd..ff3c537e87 100644 --- a/src/govuk/common.mjs +++ b/src/govuk/common.mjs @@ -189,9 +189,10 @@ export function normaliseDataset (dataset) { } /** - * Returns the value of the `lang` attribute closest to the given element (including itself) + * Returns the value of the given attribute closest to the given element (including itself) * * @param {HTMLElement} $element - The element to start walking the DOM tree up + * @param {String} attributeName - The name of the attribute * @returns {String|undefined} */ export function closestAttributeValue ($element, attributeName) { diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index c6d68f0a44..b733cb574c 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -25,6 +25,27 @@ import { I18n } from '../../i18n.mjs' * @param {Number} [config.threshold=0] - The percentage value of the limit at * which point the count message is displayed. If this attribute is set, the * count message will be hidden by default. + * @param {Object} [config.i18n] + * @param {String} [config.i18n.charactersUnderLimitOne="You have %{count} character remaining"] + * Message notifying users they're 1 character under the limit + * @param {String} [config.i18n.charactersUnderLimitOther="You have %{count} characters remaining"] + * Message notifying users they're any number of characters under the limit + * @param {String} [config.i18n.charactersAtLimit="You have no characters remaining"] + * Message notifying users they've reached the limit number of characters + * @param {String} [config.i18n.charactersOverLimitOne="You have %{count} character too many"] + * Message notifying users they're 1 character over the limit + * @param {String} [config.i18n.charactersOverLimitOther="You have %{count} characters too many"] + * Message notifying users they're any number of characters over the limit + * @param {String} [config.i18n.wordsUnderLimitOne="You have %{count} word remaining"] + * Message notifying users they're 1 word under the limit + * @param {String} [config.i18n.wordsUnderLimitOther="You have %{count} words remaining"] + * Message notifying users they're any number of words under the limit + * @param {String} [config.i18n.wordsAtLimit="You have no words remaining"] + * Message notifying users they've reached the limit number of words + * @param {String} [config.i18n.wordsOverLimitOne="You have %{count} word too many"] + * Message notifying users they're 1 word over the limit + * @param {String} [config.i18n.wordsOverLimitOther="You have %{count} words too many"] + * Message notifying users they're any number of words over the limit */ function CharacterCount ($module, config) { if (!$module) { @@ -305,6 +326,14 @@ CharacterCount.prototype.getCountMessage = function () { return this.formatCountMessage(remainingNumber, countType) } +/** + * Formats the message shown to users according to what's counted + * and how many remain + * + * @param {Number} remainingNumber - The number of words/characaters remaining + * @param {String} countType - "words" or "characters" + * @returns String + */ CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) { if (remainingNumber === 0) { return this.i18n.t(countType + 'AtLimit') From 5fe3fc5fa28abf34464dc7fff85b258f90cddaf2 Mon Sep 17 00:00:00 2001 From: Romaric Date: Mon, 3 Oct 2022 15:21:20 +0100 Subject: [PATCH 09/14] Rewrite test checking the closestAttributeValue reads up the DOM tree Make the HTML structure more apparent than creating a series of elements manually --- lib/dom-helpers.mjs | 14 ++++++++++++++ src/govuk/common.unit.test.mjs | 19 +++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/dom-helpers.mjs b/lib/dom-helpers.mjs index 56c8f4dffb..3d11748bc3 100644 --- a/lib/dom-helpers.mjs +++ b/lib/dom-helpers.mjs @@ -14,3 +14,17 @@ export function createElement (tagName, attributes = {}) { return el } + +/** + * Creates a DocumentFragment from the given HTML + * + * Allows to quickly scaffold parts of a DOM tree for testing + * + * @param {String} html - The HTML to turn into a DOM tree + * @returns DocumentFragment + */ +export function createFragmentFromHTML (html) { + const template = document.createElement('template') + template.innerHTML = html + return template.content.cloneNode(true) +} diff --git a/src/govuk/common.unit.test.mjs b/src/govuk/common.unit.test.mjs index c28f63d90e..cfe3053556 100644 --- a/src/govuk/common.unit.test.mjs +++ b/src/govuk/common.unit.test.mjs @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import { createElement } from '../../lib/dom-helpers.mjs' +import { createElement, createFragmentFromHTML } from '../../lib/dom-helpers.mjs' import { mergeConfigs, extractConfigByNamespace, normaliseString, normaliseDataset, closestAttributeValue } from './common.mjs' // TODO: Write unit tests for `nodeListForEach` and `generateUniqueID` @@ -226,13 +226,16 @@ describe('Common JS utilities', () => { }) it('returns the value of the closest parent with the attribute if it exists', () => { - const $greatGrandParent = createElement('div', { lang: 'cy-GB' }) // To check that we take the value up - const $grandparent = createElement('div', { lang: 'en-GB' }) - const $parent = createElement('div') // To check that we walk up the tree - const $element = createElement('div') - $greatGrandParent.appendChild($grandparent) - $grandparent.appendChild($parent) - $parent.appendChild($element) + const dom = createFragmentFromHTML(` +
+
+
+
+
+
+
+ `) + const $element = dom.querySelector('.target') expect(closestAttributeValue($element, 'lang')).toEqual('en-GB') }) From 152c56288502d90944a4e9c68cb996dd2b81a126 Mon Sep 17 00:00:00 2001 From: Romaric Date: Mon, 3 Oct 2022 15:39:24 +0100 Subject: [PATCH 10/14] Remove configuration of locale via JavaScript --- .../character-count/character-count.mjs | 3 +- .../character-count.unit.test.mjs | 34 ------------------- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index b733cb574c..47e48af2f3 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -94,10 +94,9 @@ function CharacterCount ($module, config) { datasetConfig ) - console.log(this.$module) this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), { // Read the fallback if necessary rather than have it set in the defaults - locale: this.config.i18nLocale || closestAttributeValue($module, 'lang') + locale: closestAttributeValue($module, 'lang') }) // Determine the limit attribute (characters or words) diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index 02ef8d308b..fd8a5a3a53 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -64,13 +64,6 @@ describe('CharacterCount', () => { // Other keys remain untouched expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) - - it('overrides the default locale', () => { - const component = new CharacterCount(createElement('div'), { - i18nLocale: 'de' - }) - expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') - }) }) describe('lang attribute configuration', () => { @@ -102,13 +95,6 @@ describe('CharacterCount', () => { expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) - it('overrides the default locale', () => { - const component = new CharacterCount(createElement('div', { - 'data-i18n-locale': 'de' - })) - expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') - }) - describe('precedence over JavaScript configuration', () => { it('overrides translation keys', () => { const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) @@ -121,26 +107,6 @@ describe('CharacterCount', () => { // Other keys remain untouched expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) - - it('overrides the default locale', () => { - const $div = createElement('div', { - 'data-i18n-locale': 'de' // Dot as thousand separator - }) - const component = new CharacterCount($div, { - i18nLocale: 'fr' // Space as thousand separator - }) - expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') - }) - - it('overrides the locale in lang attribute', () => { - const $div = createElement('div', { - 'data-i18n-locale': 'de', - lang: 'fr' - }) - const component = new CharacterCount($div) - - expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') - }) }) }) }) From 10d6bc21a5bb260dbf2dd4b08ff3e8542a96324c Mon Sep 17 00:00:00 2001 From: Romaric Date: Mon, 3 Oct 2022 16:06:36 +0100 Subject: [PATCH 11/14] Tidy up character count translation tests --- .../character-count.unit.test.mjs | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index fd8a5a3a53..ebef39a2d2 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -14,39 +14,26 @@ describe('CharacterCount', () => { component = new CharacterCount(createElement('div')) }) - it('formats singular remaining characters', () => { - expect(component.formatCountMessage(1, 'characters')).toEqual('You have 1 character remaining') - }) - it('formats plural remaining characters', () => { - expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') - }) - it('formats singular exceeding characters', () => { - expect(component.formatCountMessage(-1, 'characters')).toEqual('You have 1 character too many') - }) - it('formats plural exceeding characters', () => { - expect(component.formatCountMessage(-10, 'characters')).toEqual('You have 10 characters too many') - }) - it('formats character limit being met', () => { - expect(component.formatCountMessage(0, 'characters')).toEqual('You have no characters remaining') - }) - - it('formats singular remaining words', () => { - expect(component.formatCountMessage(1, 'words')).toEqual('You have 1 word remaining') - }) - it('formats plural remaining words', () => { - expect(component.formatCountMessage(10, 'words')).toEqual('You have 10 words remaining') - }) - it('formats singular exceeding words', () => { - expect(component.formatCountMessage(-1, 'words')).toEqual('You have 1 word too many') - }) - it('formats plural exceeding words', () => { - expect(component.formatCountMessage(-10, 'words')).toEqual('You have 10 words too many') - }) - it('formats word limit being met', () => { - expect(component.formatCountMessage(0, 'words')).toEqual('You have no words remaining') - }) + const cases = [ + { number: 1, type: 'characters', expected: 'You have 1 character remaining' }, + { number: 10, type: 'characters', expected: 'You have 10 characters remaining' }, + { number: -1, type: 'characters', expected: 'You have 1 character too many' }, + { number: -10, type: 'characters', expected: 'You have 10 characters too many' }, + { number: 0, type: 'characters', expected: 'You have no characters remaining' }, + { number: 1, type: 'words', expected: 'You have 1 word remaining' }, + { number: 10, type: 'words', expected: 'You have 10 words remaining' }, + { number: -1, type: 'words', expected: 'You have 1 word too many' }, + { number: -10, type: 'words', expected: 'You have 10 words too many' }, + { number: 0, type: 'words', expected: 'You have no words remaining' } + ] + it.each(cases)( + 'picks the relevant translation for $number $type', + function test ({ number, type, expected }) { + expect(component.formatCountMessage(number, type)).toEqual(expected) + } + ) - it('formats the count', () => { + it('formats the number inserted in the message', () => { expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10,000 words remaining') expect(component.formatCountMessage(-10000, 'words')).toEqual('You have 10,000 words too many') }) From a2a4934e3a5b0c6717865f51b5244ff4bee6bc9a Mon Sep 17 00:00:00 2001 From: Romaric Date: Mon, 3 Oct 2022 16:21:07 +0100 Subject: [PATCH 12/14] Revert translation string for when limit is met Updates the testing to use the JavaScript configuration --- .../components/character-count/character-count.mjs | 4 ++-- .../character-count/character-count.unit.test.mjs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index 47e48af2f3..335b936138 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -58,13 +58,13 @@ function CharacterCount ($module, config) { // Characters charactersUnderLimitOne: 'You have %{count} character remaining', charactersUnderLimitOther: 'You have %{count} characters remaining', - charactersAtLimit: 'You have no characters remaining', + charactersAtLimit: 'You have 0 characters remaining', charactersOverLimitOne: 'You have %{count} character too many', charactersOverLimitOther: 'You have %{count} characters too many', // Words wordsUnderLimitOne: 'You have %{count} word remaining', wordsUnderLimitOther: 'You have %{count} words remaining', - wordsAtLimit: 'You have no words remaining', + wordsAtLimit: 'You have 0 words remaining', wordsOverLimitOne: 'You have %{count} word too many', wordsOverLimitOther: 'You have %{count} words too many' } diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index ebef39a2d2..371b83249f 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -19,12 +19,12 @@ describe('CharacterCount', () => { { number: 10, type: 'characters', expected: 'You have 10 characters remaining' }, { number: -1, type: 'characters', expected: 'You have 1 character too many' }, { number: -10, type: 'characters', expected: 'You have 10 characters too many' }, - { number: 0, type: 'characters', expected: 'You have no characters remaining' }, + { number: 0, type: 'characters', expected: 'You have 0 characters remaining' }, { number: 1, type: 'words', expected: 'You have 1 word remaining' }, { number: 10, type: 'words', expected: 'You have 10 words remaining' }, { number: -1, type: 'words', expected: 'You have 1 word too many' }, { number: -10, type: 'words', expected: 'You have 10 words too many' }, - { number: 0, type: 'words', expected: 'You have no words remaining' } + { number: 0, type: 'words', expected: 'You have 0 words remaining' } ] it.each(cases)( 'picks the relevant translation for $number $type', @@ -51,6 +51,15 @@ describe('CharacterCount', () => { // Other keys remain untouched expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') }) + + it('uses specific keys for when limit is reached', () => { + const component = new CharacterCount(createElement('div'), { + i18n: { charactersAtLimit: 'Custom text.' }, + 'i18n.wordsAtLimit': 'Different custom text.' + }) + expect(component.formatCountMessage(0, 'characters')).toEqual('Custom text.') + expect(component.formatCountMessage(0, 'words')).toEqual('Different custom text.') + }) }) describe('lang attribute configuration', () => { From 4dcdeae8c776fdc1eb6d75b1871e4739fceac327 Mon Sep 17 00:00:00 2001 From: Romaric Date: Mon, 10 Oct 2022 09:39:07 +0100 Subject: [PATCH 13/14] Remove dom-helpers Waiting for further discussion about abstraction in https://github.com/alphagov/govuk-frontend/issues/2894 --- lib/dom-helpers.mjs | 30 ------------------ src/govuk/common.unit.test.mjs | 14 +++++---- .../character-count.unit.test.mjs | 31 ++++++++++++------- 3 files changed, 28 insertions(+), 47 deletions(-) delete mode 100644 lib/dom-helpers.mjs diff --git a/lib/dom-helpers.mjs b/lib/dom-helpers.mjs deleted file mode 100644 index 3d11748bc3..0000000000 --- a/lib/dom-helpers.mjs +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Creates an element with the given attributes - * - * @param {String} tagName - * @param {Object} attributes - * @returns HTMLElement - */ -export function createElement (tagName, attributes = {}) { - const el = document.createElement(tagName) - - Object.entries(attributes).forEach(([attributeName, attributeValue]) => { - el.setAttribute(attributeName, attributeValue) - }) - - return el -} - -/** - * Creates a DocumentFragment from the given HTML - * - * Allows to quickly scaffold parts of a DOM tree for testing - * - * @param {String} html - The HTML to turn into a DOM tree - * @returns DocumentFragment - */ -export function createFragmentFromHTML (html) { - const template = document.createElement('template') - template.innerHTML = html - return template.content.cloneNode(true) -} diff --git a/src/govuk/common.unit.test.mjs b/src/govuk/common.unit.test.mjs index cfe3053556..5c0ce893e1 100644 --- a/src/govuk/common.unit.test.mjs +++ b/src/govuk/common.unit.test.mjs @@ -2,7 +2,6 @@ * @jest-environment jsdom */ -import { createElement, createFragmentFromHTML } from '../../lib/dom-helpers.mjs' import { mergeConfigs, extractConfigByNamespace, normaliseString, normaliseDataset, closestAttributeValue } from './common.mjs' // TODO: Write unit tests for `nodeListForEach` and `generateUniqueID` @@ -220,13 +219,15 @@ describe('Common JS utilities', () => { describe('closestAttributeValue', () => { it('returns the value of the attribute if on the element', () => { - const $element = createElement('div', { lang: 'en-GB' }) + const $element = document.createElement('div') + $element.setAttribute('lang', 'en-GB') expect(closestAttributeValue($element, 'lang')).toEqual('en-GB') }) it('returns the value of the closest parent with the attribute if it exists', () => { - const dom = createFragmentFromHTML(` + const template = document.createElement('template') + template.innerHTML = `
@@ -234,15 +235,16 @@ describe('Common JS utilities', () => {
- `) + ` + const dom = template.content.cloneNode(true) const $element = dom.querySelector('.target') expect(closestAttributeValue($element, 'lang')).toEqual('en-GB') }) it('returns undefined if neither the element or a parent have the attribute', () => { - const $parent = createElement('div') - const $element = createElement('div') + const $parent = document.createElement('div') + const $element = document.createElement('div') $parent.appendChild($element) expect(closestAttributeValue($element, 'lang')).toBeUndefined() diff --git a/src/govuk/components/character-count/character-count.unit.test.mjs b/src/govuk/components/character-count/character-count.unit.test.mjs index 371b83249f..82a788d2c5 100644 --- a/src/govuk/components/character-count/character-count.unit.test.mjs +++ b/src/govuk/components/character-count/character-count.unit.test.mjs @@ -2,7 +2,6 @@ * @jest-environment jsdom */ -import { createElement } from '../../../../lib/dom-helpers.mjs' import CharacterCount from './character-count.mjs' describe('CharacterCount', () => { @@ -11,7 +10,7 @@ describe('CharacterCount', () => { let component beforeAll(() => { // The component won't initialise if we don't pass it an element - component = new CharacterCount(createElement('div')) + component = new CharacterCount(document.createElement('div')) }) const cases = [ @@ -42,10 +41,11 @@ describe('CharacterCount', () => { describe('i18n', () => { describe('JavaScript configuration', () => { it('overrides the default translation keys', () => { - const component = new CharacterCount(createElement('div'), { + const component = new CharacterCount(document.createElement('div'), { i18n: { charactersUnderLimitOne: 'Custom text. Count: %{count}' }, 'i18n.charactersOverLimitOther': 'Different custom text. Count: %{count}' }) + expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') expect(component.formatCountMessage(-10, 'characters')).toEqual('Different custom text. Count: 10') // Other keys remain untouched @@ -53,10 +53,11 @@ describe('CharacterCount', () => { }) it('uses specific keys for when limit is reached', () => { - const component = new CharacterCount(createElement('div'), { + const component = new CharacterCount(document.createElement('div'), { i18n: { charactersAtLimit: 'Custom text.' }, 'i18n.wordsAtLimit': 'Different custom text.' }) + expect(component.formatCountMessage(0, 'characters')).toEqual('Custom text.') expect(component.formatCountMessage(0, 'words')).toEqual('Different custom text.') }) @@ -64,18 +65,21 @@ describe('CharacterCount', () => { describe('lang attribute configuration', () => { it('overrides the locale when set on the element', () => { - const $div = createElement('div', { - lang: 'de' - }) + const $div = document.createElement('div') + $div.setAttribute('lang', 'de') + const component = new CharacterCount($div) expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') }) it('overrides the locale when set on an ancestor', () => { - const $parent = createElement('div', { lang: 'de' }) - const $div = createElement('div') + const $parent = document.createElement('div') + $parent.setAttribute('lang', 'de') + + const $div = document.createElement('div') $parent.appendChild($div) + const component = new CharacterCount($div) expect(component.formatCountMessage(10000, 'words')).toEqual('You have 10.000 words remaining') @@ -84,8 +88,11 @@ describe('CharacterCount', () => { describe('Data attribute configuration', () => { it('overrides the default translation keys', () => { - const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) + const $div = document.createElement('div') + $div.setAttribute('data-i18n.characters-under-limit-one', 'Custom text. Count: %{count}') + const component = new CharacterCount($div) + expect(component.formatCountMessage(1, 'characters')).toEqual('Custom text. Count: 1') // Other keys remain untouched expect(component.formatCountMessage(10, 'characters')).toEqual('You have 10 characters remaining') @@ -93,7 +100,9 @@ describe('CharacterCount', () => { describe('precedence over JavaScript configuration', () => { it('overrides translation keys', () => { - const $div = createElement('div', { 'data-i18n.characters-under-limit-one': 'Custom text. Count: %{count}' }) + const $div = document.createElement('div') + $div.setAttribute('data-i18n.characters-under-limit-one', 'Custom text. Count: %{count}') + const component = new CharacterCount($div, { i18n: { charactersUnderLimitOne: 'Different custom text. Count: %{count}' From a5ed6065c2039584724b33043abbaaac41f29dbb Mon Sep 17 00:00:00 2001 From: Romaric Pascal Date: Tue, 11 Oct 2022 15:38:56 +0100 Subject: [PATCH 14/14] Fix out of sync values in JSDoc Co-authored-by: Colin Rotherham --- src/govuk/components/character-count/character-count.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index 335b936138..1fb5ac3b17 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -30,7 +30,7 @@ import { I18n } from '../../i18n.mjs' * Message notifying users they're 1 character under the limit * @param {String} [config.i18n.charactersUnderLimitOther="You have %{count} characters remaining"] * Message notifying users they're any number of characters under the limit - * @param {String} [config.i18n.charactersAtLimit="You have no characters remaining"] + * @param {String} [config.i18n.charactersAtLimit="You have 0 characters remaining"] * Message notifying users they've reached the limit number of characters * @param {String} [config.i18n.charactersOverLimitOne="You have %{count} character too many"] * Message notifying users they're 1 character over the limit @@ -40,7 +40,7 @@ import { I18n } from '../../i18n.mjs' * Message notifying users they're 1 word under the limit * @param {String} [config.i18n.wordsUnderLimitOther="You have %{count} words remaining"] * Message notifying users they're any number of words under the limit - * @param {String} [config.i18n.wordsAtLimit="You have no words remaining"] + * @param {String} [config.i18n.wordsAtLimit="You have 0 words remaining"] * Message notifying users they've reached the limit number of words * @param {String} [config.i18n.wordsOverLimitOne="You have %{count} word too many"] * Message notifying users they're 1 word over the limit