govuk_publishing_components 43.3.0 → 43.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/govuk_publishing_components/icon-autocomplete-search-suggestion.svg +4 -0
  3. data/app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js +123 -0
  4. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +5 -4
  5. data/app/assets/stylesheets/govuk_publishing_components/components/_phase-banner.scss +0 -2
  6. data/app/assets/stylesheets/govuk_publishing_components/components/_search-with-autocomplete.scss +200 -0
  7. data/app/views/govuk_publishing_components/components/_layout_header.html.erb +16 -40
  8. data/app/views/govuk_publishing_components/components/_search.html.erb +16 -14
  9. data/app/views/govuk_publishing_components/components/_search_with_autocomplete.html.erb +27 -0
  10. data/app/views/govuk_publishing_components/components/docs/layout_header.yml +0 -41
  11. data/app/views/govuk_publishing_components/components/docs/phase_banner.yml +4 -0
  12. data/app/views/govuk_publishing_components/components/docs/search_with_autocomplete.yml +61 -0
  13. data/lib/govuk_publishing_components/version.rb +1 -1
  14. data/node_modules/accessible-autocomplete/CHANGELOG.md +390 -0
  15. data/node_modules/accessible-autocomplete/CODEOWNERS +2 -0
  16. data/node_modules/accessible-autocomplete/CONTRIBUTING.md +161 -0
  17. data/node_modules/accessible-autocomplete/LICENSE.txt +20 -0
  18. data/node_modules/accessible-autocomplete/Procfile +1 -0
  19. data/node_modules/accessible-autocomplete/README.md +490 -0
  20. data/node_modules/accessible-autocomplete/accessibility-criteria.md +42 -0
  21. data/node_modules/accessible-autocomplete/app.json +15 -0
  22. data/node_modules/accessible-autocomplete/babel.config.js +29 -0
  23. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css +3 -0
  24. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css.map +1 -0
  25. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js +2 -0
  26. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js.map +1 -0
  27. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js +2 -0
  28. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js.map +1 -0
  29. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js +2 -0
  30. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js.map +1 -0
  31. data/node_modules/accessible-autocomplete/examples/form-single.html +379 -0
  32. data/node_modules/accessible-autocomplete/examples/form.html +673 -0
  33. data/node_modules/accessible-autocomplete/examples/index.html +738 -0
  34. data/node_modules/accessible-autocomplete/examples/preact/index.html +346 -0
  35. data/node_modules/accessible-autocomplete/examples/react/index.html +347 -0
  36. data/node_modules/accessible-autocomplete/package.json +93 -0
  37. data/node_modules/accessible-autocomplete/postcss.config.js +16 -0
  38. data/node_modules/accessible-autocomplete/preact.js +1 -0
  39. data/node_modules/accessible-autocomplete/react.js +1 -0
  40. data/node_modules/accessible-autocomplete/scripts/check-staged.mjs +16 -0
  41. data/node_modules/accessible-autocomplete/src/autocomplete.css +167 -0
  42. data/node_modules/accessible-autocomplete/src/autocomplete.js +608 -0
  43. data/node_modules/accessible-autocomplete/src/dropdown-arrow-down.js +11 -0
  44. data/node_modules/accessible-autocomplete/src/status.js +125 -0
  45. data/node_modules/accessible-autocomplete/src/wrapper.js +60 -0
  46. data/node_modules/accessible-autocomplete/test/functional/dropdown-arrow-down.js +46 -0
  47. data/node_modules/accessible-autocomplete/test/functional/index.js +793 -0
  48. data/node_modules/accessible-autocomplete/test/functional/wrapper.js +339 -0
  49. data/node_modules/accessible-autocomplete/test/integration/index.js +309 -0
  50. data/node_modules/accessible-autocomplete/test/karma.config.js +46 -0
  51. data/node_modules/accessible-autocomplete/test/wdio.config.js +123 -0
  52. data/node_modules/accessible-autocomplete/webpack.config.mjs +244 -0
  53. metadata +46 -2
@@ -0,0 +1,339 @@
1
+ /* global before, beforeEach, after, describe, it */
2
+
3
+ import { expect } from 'chai'
4
+ import accessibleAutocomplete from '../../src/wrapper'
5
+
6
+ const DEFAULT_OPTIONS = {
7
+ '': 'Select',
8
+ fr: 'France',
9
+ de: 'Germany',
10
+ gb: 'United Kingdom of Great Britain & Northern Ireland'
11
+ }
12
+
13
+ const injectSelectToEnhanceIntoDOM = (element, settings) => {
14
+ settings = settings || {}
15
+ settings.options = settings.options || DEFAULT_OPTIONS
16
+ settings.id = settings.id !== undefined ? settings.id : 'location-picker-id'
17
+ settings.name = settings.name !== undefined ? settings.name : 'location-picker-name'
18
+ const $select = document.createElement('select')
19
+ if (settings.id) {
20
+ $select.id = settings.id
21
+ }
22
+ if (settings.name) {
23
+ $select.name = settings.name
24
+ }
25
+
26
+ Object.keys(settings.options)
27
+ .map(optionKey => {
28
+ const option = document.createElement('option')
29
+ option.value = optionKey
30
+ option.text = settings.options[optionKey]
31
+ option.selected = (settings.selected === optionKey)
32
+ return option
33
+ })
34
+ .forEach(option => $select.appendChild(option))
35
+
36
+ element.appendChild($select)
37
+
38
+ return $select
39
+ }
40
+
41
+ describe('Wrapper', () => {
42
+ let scratch
43
+ before(() => {
44
+ scratch = document.createElement('div');
45
+ (document.body || document.documentElement).appendChild(scratch)
46
+ })
47
+
48
+ beforeEach(() => {
49
+ scratch.innerHTML = ''
50
+ })
51
+
52
+ after(() => {
53
+ scratch.parentNode.removeChild(scratch)
54
+ scratch = null
55
+ })
56
+
57
+ it('throws an error when called on nonexistent element', () => {
58
+ expect(
59
+ accessibleAutocomplete.bind(null, {
60
+ element: document.querySelector('#nothing-container'),
61
+ id: 'scratch',
62
+ source: () => {}
63
+ })
64
+ ).to.throw('element is not defined')
65
+ })
66
+
67
+ it('throws an error when called without an id ', () => {
68
+ expect(
69
+ accessibleAutocomplete.bind(null, {
70
+ element: scratch,
71
+ source: () => {}
72
+ })
73
+ ).to.throw('id is not defined')
74
+ })
75
+
76
+ it('throws an error when called without a source', () => {
77
+ expect(
78
+ accessibleAutocomplete.bind(null, {
79
+ element: scratch,
80
+ id: 'scratch'
81
+ })
82
+ ).to.throw('source is not defined')
83
+ })
84
+
85
+ it('throws an error when called on nonexistent selectElement', () => {
86
+ expect(
87
+ accessibleAutocomplete.enhanceSelectElement.bind(null, {
88
+ selectElement: document.querySelector('#nothing')
89
+ })
90
+ ).to.throw('selectElement is not defined')
91
+ })
92
+
93
+ it('can enhance a select element', () => {
94
+ const select = injectSelectToEnhanceIntoDOM(scratch)
95
+ const id = select.id
96
+
97
+ accessibleAutocomplete.enhanceSelectElement({
98
+ selectElement: select
99
+ })
100
+
101
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
102
+ expect(autocompleteInstances.length).to.equal(1)
103
+
104
+ const autocompleteInstance = autocompleteInstances[0]
105
+
106
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
107
+ expect(autocompleteInput.tagName.toLowerCase()).to.equal('input')
108
+ expect(autocompleteInput.id).to.equal(id)
109
+ })
110
+
111
+ it('uses the defaultValue setting to populate the input field if no option is selected', () => {
112
+ const select = injectSelectToEnhanceIntoDOM(scratch, { selected: '' })
113
+ accessibleAutocomplete.enhanceSelectElement({
114
+ defaultValue: '',
115
+ selectElement: select
116
+ })
117
+
118
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
119
+ const autocompleteInstance = autocompleteInstances[0]
120
+
121
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
122
+ expect(autocompleteInput.value).to.equal('')
123
+ })
124
+
125
+ it('uses the option label as the default input element value if an option is selected', () => {
126
+ const select = injectSelectToEnhanceIntoDOM(scratch, { selected: 'de' })
127
+ accessibleAutocomplete.enhanceSelectElement({
128
+ defaultValue: '',
129
+ selectElement: select
130
+ })
131
+
132
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
133
+ const autocompleteInstance = autocompleteInstances[0]
134
+
135
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
136
+ expect(autocompleteInput.value).to.equal('Germany')
137
+ })
138
+
139
+ it('gives the autocomplete element a blank name attribute by default', () => {
140
+ const select = injectSelectToEnhanceIntoDOM(scratch)
141
+
142
+ accessibleAutocomplete.enhanceSelectElement({
143
+ selectElement: select
144
+ })
145
+
146
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
147
+
148
+ const autocompleteInstance = autocompleteInstances[0]
149
+
150
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
151
+ expect(autocompleteInput.name).to.equal('')
152
+ })
153
+
154
+ it('can define a name for the autocomplete element', () => {
155
+ const select = injectSelectToEnhanceIntoDOM(scratch)
156
+
157
+ accessibleAutocomplete.enhanceSelectElement({
158
+ name: 'location-picker-autocomplete',
159
+ selectElement: select
160
+ })
161
+
162
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
163
+
164
+ const autocompleteInstance = autocompleteInstances[0]
165
+
166
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
167
+ expect(autocompleteInput.name).to.equal('location-picker-autocomplete')
168
+ })
169
+
170
+ it('does not include "null" options in autocomplete', (done) => {
171
+ const select = injectSelectToEnhanceIntoDOM(scratch)
172
+
173
+ accessibleAutocomplete.enhanceSelectElement({
174
+ selectElement: select
175
+ })
176
+
177
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
178
+ const autocompleteInstance = autocompleteInstances[0]
179
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
180
+
181
+ // Using setTimeouts here since changes in values take a while to reflect in lists
182
+ autocompleteInput.value = 'e'
183
+ setTimeout(() => {
184
+ const autocompleteOptions = autocompleteInstance.querySelectorAll('.autocomplete__option')
185
+ expect(autocompleteOptions.length).to.equal(3)
186
+ expect([].map.call(autocompleteOptions, o => o.textContent)).not.to.contain('Select')
187
+ done()
188
+ }, 250)
189
+ })
190
+
191
+ it('includes "null" options in autocomplete if `preserveNullOptions` flag is true', (done) => {
192
+ const select = injectSelectToEnhanceIntoDOM(scratch)
193
+
194
+ accessibleAutocomplete.enhanceSelectElement({
195
+ preserveNullOptions: true,
196
+ selectElement: select
197
+ })
198
+
199
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
200
+ const autocompleteInstance = autocompleteInstances[0]
201
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
202
+
203
+ // Using setTimeouts here since changes in values take a while to reflect in lists
204
+ autocompleteInput.value = 'e'
205
+ setTimeout(() => {
206
+ const autocompleteOptions = autocompleteInstance.querySelectorAll('.autocomplete__option')
207
+ expect(autocompleteOptions.length).to.equal(4)
208
+ expect([].map.call(autocompleteOptions, o => o.textContent)).to.contain('Select')
209
+ done()
210
+ }, 250)
211
+ })
212
+
213
+ it('has all options when typing', (done) => {
214
+ const select = injectSelectToEnhanceIntoDOM(scratch)
215
+
216
+ accessibleAutocomplete.enhanceSelectElement({
217
+ selectElement: select
218
+ })
219
+
220
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
221
+ const autocompleteInstance = autocompleteInstances[0]
222
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
223
+ const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
224
+
225
+ // Using setTimeouts here since changes in values take a while to reflect in lists
226
+ autocompleteInput.value = 'Fran'
227
+ setTimeout(() => {
228
+ expect(autocompleteOption.textContent).to.equal('France')
229
+ autocompleteInput.value = 'Ger'
230
+ setTimeout(() => {
231
+ expect(autocompleteOption.textContent).to.equal('Germany')
232
+ autocompleteInput.value = 'United'
233
+ setTimeout(() => {
234
+ const autocompleteHint = autocompleteInstance.querySelector('.autocomplete__hint')
235
+ expect(autocompleteOption.textContent).to.equal('United Kingdom of Great Britain & Northern Ireland')
236
+ expect(autocompleteHint.value).to.equal('United Kingdom of Great Britain & Northern Ireland')
237
+ done()
238
+ }, 250)
239
+ }, 250)
240
+ }, 250)
241
+ })
242
+
243
+ it('includes aria attributes on each option, to indicate position within the full set of list item elements', (done) => {
244
+ const select = injectSelectToEnhanceIntoDOM(scratch)
245
+
246
+ accessibleAutocomplete.enhanceSelectElement({
247
+ selectElement: select
248
+ })
249
+
250
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
251
+ const autocompleteInstance = autocompleteInstances[0]
252
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
253
+ autocompleteInput.value = 'e'
254
+ setTimeout(() => {
255
+ const autocompleteOptions = autocompleteInstance.querySelectorAll('.autocomplete__option')
256
+ expect(autocompleteOptions.length).to.equal(3)
257
+ expect(autocompleteOptions[0].getAttribute('aria-posinset')).to.equal('1')
258
+ expect(autocompleteOptions[0].getAttribute('aria-setsize')).to.equal('3')
259
+ expect(autocompleteOptions[1].getAttribute('aria-posinset')).to.equal('2')
260
+ expect(autocompleteOptions[1].getAttribute('aria-setsize')).to.equal('3')
261
+ expect(autocompleteOptions[2].getAttribute('aria-posinset')).to.equal('3')
262
+ expect(autocompleteOptions[2].getAttribute('aria-setsize')).to.equal('3')
263
+ done()
264
+ }, 250)
265
+ })
266
+
267
+ it('includes an explicit position suffix on each list item option when iOS is detected', (done) => {
268
+ Object.defineProperty(global.navigator, 'userAgent', { value: 'iPhone AppleWebKit', configurable: true })
269
+
270
+ const select = injectSelectToEnhanceIntoDOM(scratch)
271
+
272
+ accessibleAutocomplete.enhanceSelectElement({
273
+ selectElement: select
274
+ })
275
+
276
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
277
+ const autocompleteInstance = autocompleteInstances[0]
278
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
279
+ const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
280
+
281
+ autocompleteInput.value = 'Fran'
282
+ setTimeout(() => {
283
+ expect(autocompleteOption.textContent).to.equal('France 1 of 1')
284
+ const iosSuffixSpan = autocompleteOption.querySelector('#location-picker-id__option-suffix--0')
285
+ expect(iosSuffixSpan.textContent).to.equal(' 1 of 1')
286
+ done()
287
+ }, 250)
288
+ })
289
+
290
+ it('does not include a position suffix on each list item option, when iOS is not detected', (done) => {
291
+ Object.defineProperty(global.navigator, 'userAgent', { value: 'definitely not an iDevice', configurable: true })
292
+
293
+ const select = injectSelectToEnhanceIntoDOM(scratch)
294
+
295
+ accessibleAutocomplete.enhanceSelectElement({
296
+ selectElement: select
297
+ })
298
+
299
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
300
+ const autocompleteInstance = autocompleteInstances[0]
301
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
302
+ const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
303
+
304
+ autocompleteInput.value = 'Fran'
305
+ setTimeout(() => {
306
+ expect(autocompleteOption.textContent).to.equal('France')
307
+ const iosSuffixSpan = autocompleteOption.querySelector('#location-picker-id__option-suffix--0')
308
+ expect(iosSuffixSpan).to.equal(null)
309
+ done()
310
+ }, 250)
311
+ })
312
+
313
+ it('onConfirm updates original select', (done) => {
314
+ const select = injectSelectToEnhanceIntoDOM(scratch, { selected: 'de' })
315
+
316
+ accessibleAutocomplete.enhanceSelectElement({
317
+ selectElement: select
318
+ })
319
+
320
+ const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
321
+ const autocompleteInstance = autocompleteInstances[0]
322
+ const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
323
+ const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
324
+
325
+ // Check the initial value of the original selectElement
326
+ expect(select.value).to.equal('de')
327
+ // Using setTimeouts here since changes in values take a while to reflect in lists
328
+ autocompleteInput.value = 'United'
329
+ setTimeout(() => {
330
+ expect(autocompleteOption.textContent).to.equal('United Kingdom of Great Britain & Northern Ireland')
331
+ autocompleteOption.click()
332
+ expect(select.value).to.equal('gb')
333
+ setTimeout(() => {
334
+ expect(autocompleteInput.value).to.equal('United Kingdom of Great Britain & Northern Ireland')
335
+ done()
336
+ }, 250)
337
+ }, 250)
338
+ })
339
+ })
@@ -0,0 +1,309 @@
1
+ /* global afterEach, beforeEach, describe, it */
2
+
3
+ const { mkdir } = require('fs/promises')
4
+ const { dirname, join } = require('path')
5
+ const { cwd } = require('process')
6
+ const { $, browser, expect } = require('@wdio/globals')
7
+ const { Key } = require('webdriverio')
8
+
9
+ const { browserName, browserVersion } = browser.capabilities
10
+ const isChrome = browserName === 'chrome'
11
+ // const isFireFox = browserName === 'firefox'
12
+ const isIE = browserName === 'internet explorer'
13
+ const liveRegionWaitTimeMillis = 10000
14
+
15
+ const basicExample = () => {
16
+ describe('basic example', function () {
17
+ const input = 'input#autocomplete-default'
18
+
19
+ let $input
20
+ let $menu
21
+
22
+ beforeEach(async () => {
23
+ $input = await $(input)
24
+ $menu = await $(`${input} + ul`)
25
+ })
26
+
27
+ it('should show the input', async () => {
28
+ await $input.waitForExist()
29
+ expect(await $input.isDisplayed()).toEqual(true)
30
+ })
31
+
32
+ it('should allow focusing the input', async () => {
33
+ await $input.click()
34
+ expect(await $input.isFocused()).toEqual(true)
35
+ })
36
+
37
+ it('should display suggestions', async () => {
38
+ await $input.click()
39
+ await $input.setValue('ita')
40
+ await $menu.waitForDisplayed()
41
+ expect(await $menu.isDisplayed()).toEqual(true)
42
+ })
43
+
44
+ // These tests are flakey when run through Saucelabs so we only run them
45
+ // in Chrome
46
+ if (isChrome) {
47
+ it('should announce status changes using two alternately updated aria live regions', async () => {
48
+ const $regionA = await $('#autocomplete-default__status--A')
49
+ const $regionB = await $('#autocomplete-default__status--B')
50
+
51
+ expect(await $regionA.getText()).toEqual('')
52
+ expect(await $regionB.getText()).toEqual('')
53
+
54
+ await $input.click()
55
+ await $input.setValue('a')
56
+
57
+ // We can't tell which region will be used first, so we have to
58
+ // await all region status text for at least one non-empty value
59
+ const waitForText = ($elements) => async () =>
60
+ Promise.all($elements.map(($element) => $element.getText()))
61
+ .then((values) => values.some(Boolean))
62
+
63
+ await browser.waitUntil(
64
+ waitForText([$regionA, $regionB]),
65
+ liveRegionWaitTimeMillis,
66
+ `expected the first aria live region to be populated within ${liveRegionWaitTimeMillis} milliseconds`
67
+ )
68
+
69
+ if (await $regionA.getText()) {
70
+ await $input.addValue('s')
71
+ await browser.waitUntil(
72
+ waitForText([$regionA, $regionB]),
73
+ liveRegionWaitTimeMillis,
74
+ `expected the first aria live region to be cleared, and the second to be populated within ${liveRegionWaitTimeMillis} milliseconds`
75
+ )
76
+
77
+ await $input.addValue('h')
78
+ await browser.waitUntil(
79
+ waitForText([$regionA, $regionB]),
80
+ liveRegionWaitTimeMillis,
81
+ `expected the first aria live region to be populated, and the second to be cleared within ${liveRegionWaitTimeMillis} milliseconds`
82
+ )
83
+ } else {
84
+ await $input.addValue('s')
85
+ await browser.waitUntil(
86
+ waitForText([$regionA, $regionB]),
87
+ liveRegionWaitTimeMillis,
88
+ `expected the first aria live region to be populated, and the second to be cleared within ${liveRegionWaitTimeMillis} milliseconds`
89
+ )
90
+
91
+ await $input.addValue('h')
92
+ await browser.waitUntil(
93
+ waitForText([$regionA, $regionB]),
94
+ liveRegionWaitTimeMillis,
95
+ `expected the first aria live region to be cleared, and the second to be populated within ${liveRegionWaitTimeMillis} milliseconds`
96
+ )
97
+ }
98
+ })
99
+ }
100
+
101
+ it('should set aria-selected to true on selected option and false on other options', async () => {
102
+ await $input.click()
103
+ await $input.setValue('ita')
104
+
105
+ const $option1 = await $(`${input} + ul li:nth-child(1)`)
106
+ const $option2 = await $(`${input} + ul li:nth-child(2)`)
107
+
108
+ await browser.keys([Key.ArrowDown])
109
+ expect(await $option1.getAttribute('aria-selected')).toEqual('true')
110
+ expect(await $option2.getAttribute('aria-selected')).toEqual('false')
111
+ await browser.keys([Key.ArrowDown])
112
+ expect(await $option1.getAttribute('aria-selected')).toEqual('false')
113
+ expect(await $option2.getAttribute('aria-selected')).toEqual('true')
114
+ })
115
+
116
+ describe('keyboard use', () => {
117
+ it('should allow typing', async () => {
118
+ await $input.click()
119
+ await $input.addValue('ita')
120
+ expect(await $input.getValue()).toEqual('ita')
121
+ })
122
+
123
+ it('should allow selecting an option', async () => {
124
+ await $input.click()
125
+ await $input.setValue('ita')
126
+
127
+ const $option1 = await $(`${input} + ul li:nth-child(1)`)
128
+ const $option2 = await $(`${input} + ul li:nth-child(2)`)
129
+
130
+ await browser.keys([Key.ArrowDown])
131
+ expect(await $input.isFocused()).toEqual(false)
132
+ expect(await $option1.isFocused()).toEqual(true)
133
+ await browser.keys([Key.ArrowDown])
134
+ expect(await $menu.isDisplayed()).toEqual(true)
135
+ expect(await $input.getValue()).toEqual('ita')
136
+ expect(await $option1.isFocused()).toEqual(false)
137
+ expect(await $option2.isFocused()).toEqual(true)
138
+ })
139
+
140
+ it('should allow confirming an option', async () => {
141
+ await $input.click()
142
+ await $input.setValue('ita')
143
+ await browser.keys([Key.ArrowDown, Key.Enter])
144
+ await browser.waitUntil(async () => await $input.getValue() !== 'ita')
145
+ expect(await $input.isFocused()).toEqual(true)
146
+ expect(await $input.getValue()).toEqual('Italy')
147
+ })
148
+
149
+ it('should redirect keypresses on an option to input', async () => {
150
+ if (!isIE) {
151
+ await $input.click()
152
+ await $input.setValue('ita')
153
+
154
+ const $option1 = await $(`${input} + ul li:nth-child(1)`)
155
+
156
+ await browser.keys([Key.ArrowDown])
157
+ expect(await $input.isFocused()).toEqual(false)
158
+ expect(await $option1.isFocused()).toEqual(true)
159
+ await $option1.addValue('l')
160
+ expect(await $input.isFocused()).toEqual(true)
161
+ expect(await $input.getValue()).toEqual('ital')
162
+ } else {
163
+ // FIXME: This feature does not work correctly on IE 11
164
+ }
165
+ })
166
+ })
167
+
168
+ describe('mouse use', () => {
169
+ it('should allow confirming an option', async () => {
170
+ await $input.click()
171
+ await $input.setValue('ita')
172
+
173
+ const $option1 = await $(`${input} + ul li:nth-child(1)`)
174
+ await $option1.click()
175
+
176
+ expect(await $input.isFocused()).toEqual(true)
177
+ expect(await $input.getValue()).toEqual('Italy')
178
+ })
179
+ })
180
+ })
181
+ }
182
+
183
+ const customTemplatesExample = () => {
184
+ describe('custom templates example', function () {
185
+ const input = 'input#autocomplete-customTemplates'
186
+
187
+ let $input
188
+
189
+ beforeEach(async () => {
190
+ $input = await $(input)
191
+
192
+ await $input.setValue('') // Prevent autofilling, IE likes to do this.
193
+ })
194
+
195
+ describe('mouse use', () => {
196
+ it('should allow confirming an option by clicking on child elements', async () => {
197
+ await $input.setValue('uni')
198
+
199
+ const $option1InnerElement = await $(`${input} + ul li:nth-child(1) strong`)
200
+
201
+ if (isIE) {
202
+ // FIXME: This feature works correctly on IE but testing it doesn't seem to work.
203
+ return
204
+ }
205
+
206
+ try {
207
+ await $option1InnerElement.click()
208
+ } catch (error) {
209
+ // In some cases (mainly ChromeDriver) the automation protocol won't
210
+ // allow clicking span elements. In this case we just skip the test.
211
+ if (error.toString().match(/Other element would receive the click/)) {
212
+ return
213
+ } else {
214
+ throw error
215
+ }
216
+ }
217
+
218
+ expect(await $input.isFocused()).toEqual(true)
219
+ expect(await $input.getValue()).toEqual('United Kingdom')
220
+ })
221
+ })
222
+ })
223
+ }
224
+
225
+ const classesPropsExamples = () => {
226
+ describe('classes properties', () => {
227
+ it('should set `inputClasses` on both hint and input', async () => {
228
+ const $input = await $('input#autocomplete-inputClasses')
229
+ await expect($input).toHaveElementClass('app-input')
230
+
231
+ // Trigger the display of the hint
232
+ await $input.setValue('fra')
233
+
234
+ const $hint = $input.parentElement().$('.autocomplete__hint')
235
+ await expect($hint).toBeDisplayed()
236
+ await expect($hint).toHaveElementClass('app-input')
237
+ })
238
+
239
+ it('should set `hintClasses` on the hint', async () => {
240
+ const $input = await $('input#autocomplete-hintClasses')
241
+
242
+ // Trigger the display of the hint
243
+ await $input.setValue('fra')
244
+
245
+ const $hint = $input.parentElement().$('.autocomplete__hint')
246
+ await expect($hint).toBeDisplayed()
247
+ await expect($hint).toHaveElementClass('app-hint')
248
+ })
249
+ })
250
+ }
251
+
252
+ const takeScreenshotsIfFail = () => {
253
+ afterEach(async function () {
254
+ const testFailed = this.currentTest.state === 'failed'
255
+ if (testFailed) {
256
+ const timestamp = +new Date()
257
+ const browserVariant = isIE ? `ie${browserVersion}` : browserName
258
+ const testTitle = this.currentTest.title.replace(/\W/g, '-')
259
+ const filename = join(cwd(), `screenshots/${timestamp}-${browserVariant}-${testTitle}.png`)
260
+ await mkdir(dirname(filename), { recursive: true })
261
+ await browser.saveScreenshot(filename)
262
+ console.log(`Test failed, created: ${filename}`)
263
+ }
264
+ })
265
+ }
266
+
267
+ describe('Accessible Autocomplete', () => {
268
+ beforeEach(async () => {
269
+ await browser.url('/')
270
+ })
271
+
272
+ it('should have the right title', async () => {
273
+ expect(await browser.getTitle()).toEqual('Accessible Autocomplete examples')
274
+ })
275
+
276
+ basicExample()
277
+ customTemplatesExample()
278
+ classesPropsExamples()
279
+
280
+ takeScreenshotsIfFail()
281
+ })
282
+
283
+ describe('Accessible Autocomplete Preact', () => {
284
+ beforeEach(async () => {
285
+ await browser.url('/preact')
286
+ })
287
+
288
+ it('should have the right title', async () => {
289
+ expect(await browser.getTitle()).toEqual('Accessible Autocomplete Preact examples')
290
+ })
291
+
292
+ basicExample()
293
+
294
+ takeScreenshotsIfFail()
295
+ })
296
+
297
+ describe('Accessible Autocomplete React', () => {
298
+ beforeEach(async () => {
299
+ await browser.url('/react')
300
+ })
301
+
302
+ it('should have the right title', async () => {
303
+ expect(await browser.getTitle()).toEqual('Accessible Autocomplete React examples')
304
+ })
305
+
306
+ basicExample()
307
+
308
+ takeScreenshotsIfFail()
309
+ })
@@ -0,0 +1,46 @@
1
+ require('@babel/register')({
2
+ rootMode: 'upward'
3
+ })
4
+
5
+ const puppeteer = require('puppeteer')
6
+ const webpackConfig = require('../webpack.config.mjs')
7
+
8
+ // Use Chrome headless
9
+ process.env.CHROME_BIN = puppeteer.executablePath()
10
+
11
+ module.exports = function (config) {
12
+ config.set({
13
+ basePath: '../',
14
+ frameworks: ['mocha', 'webpack'],
15
+ reporters: ['mocha'],
16
+
17
+ browsers: ['ChromeHeadless'],
18
+
19
+ files: [
20
+ 'test/functional/**/*.js'
21
+ ],
22
+
23
+ preprocessors: {
24
+ 'test/**/*.js': ['webpack'],
25
+ 'src/**/*.js': ['webpack'],
26
+ '**/*.js': ['sourcemap']
27
+ },
28
+
29
+ webpack: {
30
+ // Use standalone webpack config [0] rather
31
+ // than Preact [1] or React [2] configs
32
+ ...webpackConfig.default[0],
33
+
34
+ // Use Karma managed test entry points
35
+ entry: undefined,
36
+
37
+ // Use Karma default `os.tmpdir()` output
38
+ output: undefined,
39
+
40
+ // Suppress webpack performance warnings due to
41
+ // Karma chunked output and inline source maps
42
+ performance: { hints: false },
43
+ stats: { preset: 'errors-only' }
44
+ }
45
+ })
46
+ }