@internetarchive/histogram-date-range 0.1.8-alpha.4 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,595 +1,595 @@
1
- import { html, fixture, expect, oneEvent, aTimeout } from '@open-wc/testing';
2
-
3
- import { HistogramDateRange } from '../src/histogram-date-range';
4
- import '../src/histogram-date-range';
5
-
6
- const SLIDER_WIDTH = 10;
7
- const WIDTH = 200;
8
-
9
- const subject = html`
10
- <histogram-date-range
11
- width="${WIDTH}"
12
- tooltipWidth="140"
13
- height="50"
14
- dateFormat="M/D/YYYY"
15
- minDate="1900"
16
- maxDate="12/4/2020"
17
- bins="[33, 1, 100]"
18
- >
19
- </histogram-date-range>
20
- `;
21
-
22
- async function createCustomElementInHTMLContainer(): Promise<HistogramDateRange> {
23
- document.head.insertAdjacentHTML(
24
- 'beforeend',
25
- `<style>
26
- html {
27
- font-size:10px;
28
- }
29
- .container {
30
- width: 400px;
31
- height: 400px;
32
- display: flex;
33
- background: #FFF6E1;
34
- justify-content: center;
35
- align-items: center;
36
- }
37
- </style>`
38
- );
39
- // https://open-wc.org/docs/testing/helpers/#customize-the-fixture-container
40
- const parentNode = document.createElement('div');
41
- parentNode.setAttribute('class', 'container');
42
- return fixture<HistogramDateRange>(subject, { parentNode });
43
- }
44
-
45
- describe('HistogramDateRange', () => {
46
- it('shows scaled histogram bars when provided with data', async () => {
47
- const el = await createCustomElementInHTMLContainer();
48
- const bars = (el.shadowRoot?.querySelectorAll(
49
- '.bar'
50
- ) as unknown) as SVGRectElement[];
51
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
52
-
53
- expect(heights).to.eql([38, 7, 50]);
54
- });
55
-
56
- it('changes the position of the sliders and standardizes date format when dates are entered', async () => {
57
- const el = await createCustomElementInHTMLContainer();
58
-
59
- /* -------------------------- minimum (left) slider ------------------------- */
60
- expect(el.minSliderX).to.eq(SLIDER_WIDTH);
61
- const minDateInput = el.shadowRoot?.querySelector(
62
- '#date-min'
63
- ) as HTMLInputElement;
64
-
65
- const pressEnterEvent = new KeyboardEvent('keyup', {
66
- key: 'Enter',
67
- });
68
-
69
- // valid min date
70
- minDateInput.value = '1950';
71
- minDateInput.dispatchEvent(pressEnterEvent);
72
-
73
- expect(Math.floor(el.minSliderX)).to.eq(84);
74
- expect(el.minSelectedDate).to.eq('1/1/1950'); // set to correct format
75
-
76
- // attempt to set date earlier than first item
77
- minDateInput.value = '10/1/1850';
78
- minDateInput.dispatchEvent(new Event('blur'));
79
-
80
- expect(Math.floor(el.minSliderX)).to.eq(SLIDER_WIDTH); // leftmost valid position
81
- // allow date value less than slider range
82
- expect(el.minSelectedDate).to.eq('10/1/1850');
83
-
84
- /* -------------------------- maximum (right) slider ------------------------- */
85
- expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
86
- const maxDateInput = el.shadowRoot?.querySelector(
87
- '#date-max'
88
- ) as HTMLInputElement;
89
-
90
- // set valid max date
91
- maxDateInput.value = '3/12/1975';
92
- maxDateInput.dispatchEvent(pressEnterEvent);
93
-
94
- expect(Math.floor(el.maxSliderX)).to.eq(121);
95
- expect(maxDateInput.value).to.eq('3/12/1975');
96
-
97
- // attempt to set date later than last item
98
- maxDateInput.value = '12/31/2199';
99
- maxDateInput.dispatchEvent(new Event('blur'));
100
- await el.updateComplete;
101
-
102
- expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH); // rightmost valid position
103
- // allow date value greater than slider range
104
- expect(maxDateInput.value).to.eq('12/31/2199');
105
- });
106
-
107
- it('handles invalid date inputs', async () => {
108
- const el = await createCustomElementInHTMLContainer();
109
-
110
- /* -------------------------- minimum (left) slider ------------------------- */
111
- const minDateInput = el.shadowRoot?.querySelector(
112
- '#date-min'
113
- ) as HTMLInputElement;
114
-
115
- minDateInput.value = '5/17/1961';
116
- minDateInput.dispatchEvent(new Event('blur'));
117
- await el.updateComplete;
118
-
119
- expect(Math.floor(el.minSliderX)).to.eq(101);
120
- expect(minDateInput.value).to.eq('5/17/1961');
121
-
122
- // enter invalid value
123
- minDateInput.value = 'invalid';
124
- minDateInput.dispatchEvent(new Event('blur'));
125
- await el.updateComplete;
126
-
127
- expect(Math.floor(el.minSliderX)).to.eq(101); // does not move
128
- expect(minDateInput.value).to.eq('5/17/1961'); // resets back to previous date
129
-
130
- /* -------------------------- maximum (right) slider ------------------------- */
131
- const maxDateInput = el.shadowRoot?.querySelector(
132
- '#date-max'
133
- ) as HTMLInputElement;
134
-
135
- // initial values
136
- expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
137
- expect(maxDateInput.value).to.eq('12/4/2020');
138
-
139
- // enter invalid value
140
- maxDateInput.value = 'Abc 12, 1YYY';
141
- maxDateInput.dispatchEvent(new Event('blur'));
142
- await el.updateComplete;
143
-
144
- expect(Math.floor(el.maxSliderX)).to.eq(WIDTH - SLIDER_WIDTH); // does not move
145
- expect(maxDateInput.value).to.eq('12/4/2020'); // resets back to previous date
146
- });
147
-
148
- it('updates the date inputs when the sliders are moved', async () => {
149
- const el = await createCustomElementInHTMLContainer();
150
-
151
- /* -------------------------- minimum (left) slider ------------------------- */
152
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
153
- const container = el.shadowRoot?.querySelector(
154
- '#container'
155
- ) as HTMLDivElement;
156
- const minDateInput = el.shadowRoot?.querySelector(
157
- '#date-min'
158
- ) as HTMLInputElement;
159
-
160
- // initial state
161
- expect(minSlider.getBoundingClientRect().x).to.eq(108);
162
- expect(Array.from(minSlider.classList).join(' ')).to.eq('draggable');
163
-
164
- // pointer down
165
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
166
- await el.updateComplete;
167
-
168
- // cursor changes to 'grab'
169
- const classList = minSlider.classList;
170
- expect(classList.contains('draggable')).to.be.true;
171
- expect(classList.contains('dragging')).to.be.true;
172
-
173
- // slide to right
174
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
175
- await el.updateComplete;
176
-
177
- // slider has moved
178
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(168);
179
- // min date is updated
180
- expect(minDateInput.value).to.eq('4/23/1940');
181
-
182
- // stop dragging
183
- window.dispatchEvent(new PointerEvent('pointerup'));
184
- await el.updateComplete;
185
-
186
- // cursor returns to normal
187
- expect(Array.from(container.classList)).not.to.include('dragging');
188
-
189
- /* -------------------------- maximum (right) slider ------------------------- */
190
- const maxSlider = el.shadowRoot?.querySelector('#slider-max') as SVGElement;
191
- const maxDateInput = el.shadowRoot?.querySelector(
192
- '#date-max'
193
- ) as HTMLInputElement;
194
-
195
- // initial state
196
- expect(maxSlider.getBoundingClientRect().x).to.eq(298);
197
-
198
- // slide to left
199
- maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 195 }));
200
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 160 }));
201
- await el.updateComplete;
202
-
203
- // slider has moved
204
- expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268);
205
- // max date is updated
206
- expect(maxDateInput.value).to.eq('10/8/2000');
207
- await el.updateComplete;
208
-
209
- // try to slide min slider past max slider
210
- minSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 62 }));
211
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 190 }));
212
- await el.updateComplete;
213
-
214
- // slider moves all the way to meet the right slider
215
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(258);
216
-
217
- // try to slide max slider past min slider
218
- maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 120 }));
219
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 50 }));
220
- await el.updateComplete;
221
- expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268); // max slider didn't move
222
- });
223
-
224
- it("emits a custom event when the element's date range changes", async () => {
225
- const el = await createCustomElementInHTMLContainer();
226
- el.updateDelay = 30; // set debounce delay of 30ms
227
-
228
- const minDateInput = el.shadowRoot?.querySelector(
229
- '#date-min'
230
- ) as HTMLInputElement;
231
- const updateEventPromise = oneEvent(el, 'histogramDateRangeUpdated');
232
-
233
- // simulate typing a new value into input
234
- minDateInput.value = '1955';
235
- minDateInput.dispatchEvent(new Event('blur'));
236
-
237
- // will wait longer than debounce delay
238
- const { detail } = await updateEventPromise;
239
- // verify that event is emitted
240
- expect(detail.minDate).to.equal('1/1/1955');
241
- expect(detail.maxDate).to.equal('12/4/2020');
242
-
243
- let eventCount = 0;
244
- el.addEventListener('histogramDateRangeUpdated', () => (eventCount += 1));
245
-
246
- // events are not sent if no change since the last event that was sent
247
- minDateInput.value = '1955';
248
- minDateInput.dispatchEvent(new Event('blur'));
249
- await aTimeout(60); // wait longer than debounce delay
250
- expect(eventCount).to.equal(0);
251
-
252
- const updateEventPromise2 = oneEvent(el, 'histogramDateRangeUpdated');
253
-
254
- // with the debounce, multiple quick changes only result in one event sent
255
- minDateInput.value = '1965';
256
- minDateInput.dispatchEvent(new Event('blur'));
257
- await aTimeout(10); // wait less than the debounce delay
258
-
259
- minDateInput.dispatchEvent(new Event('focus'));
260
- minDateInput.value = '1975';
261
- minDateInput.dispatchEvent(new Event('blur'));
262
- await aTimeout(10);
263
-
264
- minDateInput.dispatchEvent(new Event('focus'));
265
- minDateInput.value = '1985';
266
- minDateInput.dispatchEvent(new Event('blur'));
267
- await aTimeout(10);
268
-
269
- const event2 = await updateEventPromise2;
270
- expect(event2.detail.minDate).to.equal('1/1/1985');
271
- expect(eventCount).to.equal(1); // only one event was fired
272
- });
273
-
274
- it('shows/hides tooltip when hovering over (or pointing at) a bar', async () => {
275
- const el = await createCustomElementInHTMLContainer();
276
- // include a number which will require commas (1,000,000)
277
- el.bins = [1000000, 1, 100];
278
- await aTimeout(10);
279
- const bars = (el.shadowRoot?.querySelectorAll(
280
- '.bar'
281
- ) as unknown) as SVGRectElement[];
282
- const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
283
- expect(tooltip.innerText).to.eq('');
284
-
285
- // hover
286
- bars[0].dispatchEvent(new PointerEvent('pointerenter'));
287
- await el.updateComplete;
288
- expect(tooltip.innerText).to.match(
289
- /^1,000,000 items\n1\/1\/1900 - 4\/23\/1940/
290
- );
291
- expect(getComputedStyle(tooltip).display).to.eq('block');
292
-
293
- // leave
294
- bars[0].dispatchEvent(new PointerEvent('pointerleave'));
295
- await el.updateComplete;
296
- expect(getComputedStyle(tooltip).display).to.eq('none');
297
- expect(tooltip.innerText).to.eq('');
298
-
299
- // ensure singular item is not pluralized
300
- bars[1].dispatchEvent(new PointerEvent('pointerenter'));
301
- await el.updateComplete;
302
- expect(tooltip.innerText).to.match(/^1 item\n4\/23\/1940 - 8\/13\/1980/);
303
- });
304
-
305
- it('does not show tooltip while dragging', async () => {
306
- const el = await createCustomElementInHTMLContainer();
307
- const bars = (el.shadowRoot?.querySelectorAll(
308
- '.bar'
309
- ) as unknown) as SVGRectElement[];
310
- const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
311
- expect(tooltip.innerText).to.eq('');
312
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
313
-
314
- // pointer down and slide right
315
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
316
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100 }));
317
- await el.updateComplete;
318
-
319
- // hover over bar
320
- bars[0].dispatchEvent(new PointerEvent('pointerenter'));
321
- await el.updateComplete;
322
- // tooltip display is suppressed while dragging
323
- expect(tooltip.style.display).to.eq('');
324
- });
325
-
326
- it('passes the a11y audit', async () => {
327
- await fixture<HistogramDateRange>(subject).then(el =>
328
- expect(el).shadowDom.to.be.accessible()
329
- );
330
- });
331
-
332
- it('allows range to be pre-selected', async () => {
333
- const el = await fixture<HistogramDateRange>(
334
- html`
335
- <histogram-date-range
336
- minDate="1900"
337
- maxDate="Dec 4, 2020"
338
- minSelectedDate="2012"
339
- maxSelectedDate="2019"
340
- bins="[33, 1, 100]"
341
- >
342
- </histogram-date-range>
343
- `
344
- );
345
- const minDateInput = el.shadowRoot?.querySelector(
346
- '#date-min'
347
- ) as HTMLInputElement;
348
- expect(minDateInput.value).to.eq('2012');
349
-
350
- const maxDateInput = el.shadowRoot?.querySelector(
351
- '#date-max'
352
- ) as HTMLInputElement;
353
- expect(maxDateInput.value).to.eq('2019');
354
- });
355
-
356
- it('extends the selected range when the histogram is clicked outside of the current range', async () => {
357
- const el = await fixture<HistogramDateRange>(
358
- html`
359
- <histogram-date-range
360
- minDate="1900"
361
- maxDate="2020"
362
- minSelectedDate="1950"
363
- maxSelectedDate="1955"
364
- bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
365
- >
366
- </histogram-date-range>
367
- `
368
- );
369
-
370
- const leftBarToClick = Array.from(
371
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
372
- )[1]; // click on second bar to the left
373
-
374
- leftBarToClick.dispatchEvent(new Event('click'));
375
- await el.updateComplete;
376
- expect(el.minSelectedDate).to.eq('1910'); // range was extended to left
377
-
378
- const rightBarToClick = Array.from(
379
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
380
- )[8]; // click on second bar from the right
381
-
382
- rightBarToClick.dispatchEvent(new Event('click'));
383
- expect(el.maxSelectedDate).to.eq('1998'); // range was extended to right
384
- });
385
-
386
- it('narrows the selected range when the histogram is clicked inside of the current range', async () => {
387
- const el = await fixture<HistogramDateRange>(
388
- html`
389
- <histogram-date-range
390
- minDate="1900"
391
- maxDate="2020"
392
- minSelectedDate="1900"
393
- maxSelectedDate="2020"
394
- bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
395
- >
396
- </histogram-date-range>
397
- `
398
- );
399
-
400
- ///////////////////////////////////////////////
401
- // NB: the slider nearest the clicked bar moves
402
- ///////////////////////////////////////////////
403
-
404
- const leftBarToClick = Array.from(
405
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
406
- )[3]; // click on fourth bar to the left
407
-
408
- leftBarToClick.dispatchEvent(new Event('click'));
409
- expect(el.minSelectedDate).to.eq('1932'); // range was extended to the right
410
-
411
- const rightBarToClick = Array.from(
412
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
413
- )[8]; // click on second bar from the right
414
-
415
- rightBarToClick.dispatchEvent(new Event('click'));
416
- expect(el.maxSelectedDate).to.eq('1998'); // range was extended to the left
417
- });
418
-
419
- it('handles invalid pre-selected range by defaulting to overall max and min', async () => {
420
- const el = await fixture<HistogramDateRange>(
421
- html`
422
- <histogram-date-range
423
- minDate="1900"
424
- maxDate="2020"
425
- minSelectedDate="2000xyz"
426
- maxSelectedDate="5000"
427
- bins="[33, 1, 100]"
428
- >
429
- </histogram-date-range>
430
- `
431
- );
432
- const minDateInput = el.shadowRoot?.querySelector(
433
- '#date-min'
434
- ) as HTMLInputElement;
435
- // malformed min date defaults to overall min
436
- expect(minDateInput.value).to.eq('1900');
437
-
438
- const maxDateInput = el.shadowRoot?.querySelector(
439
- '#date-max'
440
- ) as HTMLInputElement;
441
- // well-formed max date is allowed
442
- expect(maxDateInput.value).to.eq('5000');
443
- });
444
-
445
- it('handles year values less than 1000 by overriding date format to just display year', async () => {
446
- const el = await fixture<HistogramDateRange>(
447
- html`
448
- <histogram-date-range
449
- dateFormat="M/D/YYYY"
450
- minDate="-2000"
451
- maxDate="2000"
452
- minSelectedDate="-500"
453
- maxSelectedDate="500"
454
- bins="[33, 1, 100]"
455
- >
456
- </histogram-date-range>
457
- `
458
- );
459
- const minDateInput = el.shadowRoot?.querySelector(
460
- '#date-min'
461
- ) as HTMLInputElement;
462
- expect(minDateInput.value).to.eq('-500');
463
-
464
- const maxDateInput = el.shadowRoot?.querySelector(
465
- '#date-max'
466
- ) as HTMLInputElement;
467
- expect(maxDateInput.value).to.eq('500');
468
- });
469
-
470
- it('handles missing data', async () => {
471
- let el = await fixture<HistogramDateRange>(
472
- html`<histogram-date-range>
473
- minDate="1900" maxDate="2020" bins=""
474
- </histogram-date-range>`
475
- );
476
- expect(el.shadowRoot?.innerHTML).to.contain('no data');
477
- el = await fixture<HistogramDateRange>(
478
- html`<histogram-date-range
479
- minDate="1900"
480
- maxDate="2020"
481
- bins="[]"
482
- missingDataMessage="no data available"
483
- ></histogram-date-range>`
484
- );
485
- expect(el.shadowRoot?.innerHTML).to.contain('no data available');
486
- });
487
-
488
- it('correctly displays data consisting of a single bin', async () => {
489
- const el = await fixture<HistogramDateRange>(
490
- html`
491
- <histogram-date-range minDate="2020" maxDate="2020" bins="[50]">
492
- </histogram-date-range>
493
- `
494
- );
495
- const bars = (el.shadowRoot?.querySelectorAll(
496
- '.bar'
497
- ) as unknown) as SVGRectElement[];
498
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
499
- expect(heights).to.eql([157]);
500
- });
501
-
502
- it('correctly displays small diff between max and min values', async () => {
503
- const el = await fixture<HistogramDateRange>(
504
- html`
505
- <histogram-date-range bins="[1519,2643,1880,2041,1638,1441]">
506
- </histogram-date-range>
507
- `
508
- );
509
- const bars = (el.shadowRoot?.querySelectorAll(
510
- '.bar'
511
- ) as unknown) as SVGRectElement[];
512
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
513
- expect(heights).to.eql([37, 40, 38, 38, 37, 36]);
514
- });
515
-
516
- it('has a disabled state', async () => {
517
- const el = await fixture<HistogramDateRange>(
518
- html`
519
- <histogram-date-range
520
- minDate="1900"
521
- maxDate="2020"
522
- disabled
523
- bins="[33, 1, 100]"
524
- >
525
- </histogram-date-range>
526
- `
527
- );
528
- expect(
529
- el.shadowRoot
530
- ?.querySelector('.inner-container')
531
- ?.classList.contains('disabled')
532
- ).to.eq(true);
533
-
534
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
535
-
536
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8); // initial state
537
-
538
- // attempt to slide to right
539
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
540
- await el.updateComplete;
541
-
542
- // cursor is not draggable if disabled
543
- expect(Array.from(minSlider.classList).join(' ')).to.eq('');
544
-
545
- // attempt to slide to right
546
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
547
- await el.updateComplete;
548
-
549
- // slider does not moved if element disabled
550
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8);
551
- });
552
-
553
- it('has a loading state with an activity indicator', async () => {
554
- const el = await fixture<HistogramDateRange>(
555
- html`
556
- <histogram-date-range
557
- minDate="1900"
558
- maxDate="2020"
559
- loading
560
- bins="[33, 1, 100]"
561
- >
562
- </histogram-date-range>
563
- `
564
- );
565
- expect(
566
- el.shadowRoot
567
- ?.querySelector('ia-activity-indicator')
568
- ?.attributes?.getNamedItem('mode')?.value
569
- ).to.eq('processing');
570
- });
571
-
572
- it('can use LitElement bound properties', async () => {
573
- const el = await fixture<HistogramDateRange>(
574
- html`
575
- <histogram-date-range
576
- .minDate=${1900}
577
- .maxDate=${'Dec 4, 2020'}
578
- .minSelectedDate=${2012}
579
- .maxSelectedDate=${2019}
580
- .bins=${[33, 1, 100]}
581
- >
582
- </histogram-date-range>
583
- `
584
- );
585
- const minDateInput = el.shadowRoot?.querySelector(
586
- '#date-min'
587
- ) as HTMLInputElement;
588
- expect(minDateInput.value).to.eq('2012');
589
-
590
- const maxDateInput = el.shadowRoot?.querySelector(
591
- '#date-max'
592
- ) as HTMLInputElement;
593
- expect(maxDateInput.value).to.eq('2019');
594
- });
595
- });
1
+ import { html, fixture, expect, oneEvent, aTimeout } from '@open-wc/testing';
2
+
3
+ import { HistogramDateRange } from '../src/histogram-date-range';
4
+ import '../src/histogram-date-range';
5
+
6
+ const SLIDER_WIDTH = 10;
7
+ const WIDTH = 200;
8
+
9
+ const subject = html`
10
+ <histogram-date-range
11
+ width="${WIDTH}"
12
+ tooltipWidth="140"
13
+ height="50"
14
+ dateFormat="M/D/YYYY"
15
+ minDate="1900"
16
+ maxDate="12/4/2020"
17
+ bins="[33, 1, 100]"
18
+ >
19
+ </histogram-date-range>
20
+ `;
21
+
22
+ async function createCustomElementInHTMLContainer(): Promise<HistogramDateRange> {
23
+ document.head.insertAdjacentHTML(
24
+ 'beforeend',
25
+ `<style>
26
+ html {
27
+ font-size:10px;
28
+ }
29
+ .container {
30
+ width: 400px;
31
+ height: 400px;
32
+ display: flex;
33
+ background: #FFF6E1;
34
+ justify-content: center;
35
+ align-items: center;
36
+ }
37
+ </style>`
38
+ );
39
+ // https://open-wc.org/docs/testing/helpers/#customize-the-fixture-container
40
+ const parentNode = document.createElement('div');
41
+ parentNode.setAttribute('class', 'container');
42
+ return fixture<HistogramDateRange>(subject, { parentNode });
43
+ }
44
+
45
+ describe('HistogramDateRange', () => {
46
+ it('shows scaled histogram bars when provided with data', async () => {
47
+ const el = await createCustomElementInHTMLContainer();
48
+ const bars = el.shadowRoot?.querySelectorAll(
49
+ '.bar'
50
+ ) as unknown as SVGRectElement[];
51
+ const heights = Array.from(bars).map(b => b.height.baseVal.value);
52
+
53
+ expect(heights).to.eql([38, 7, 50]);
54
+ });
55
+
56
+ it('changes the position of the sliders and standardizes date format when dates are entered', async () => {
57
+ const el = await createCustomElementInHTMLContainer();
58
+
59
+ /* -------------------------- minimum (left) slider ------------------------- */
60
+ expect(el.minSliderX).to.eq(SLIDER_WIDTH);
61
+ const minDateInput = el.shadowRoot?.querySelector(
62
+ '#date-min'
63
+ ) as HTMLInputElement;
64
+
65
+ const pressEnterEvent = new KeyboardEvent('keyup', {
66
+ key: 'Enter',
67
+ });
68
+
69
+ // valid min date
70
+ minDateInput.value = '1950';
71
+ minDateInput.dispatchEvent(pressEnterEvent);
72
+
73
+ expect(Math.floor(el.minSliderX)).to.eq(84);
74
+ expect(el.minSelectedDate).to.eq('1/1/1950'); // set to correct format
75
+
76
+ // attempt to set date earlier than first item
77
+ minDateInput.value = '10/1/1850';
78
+ minDateInput.dispatchEvent(new Event('blur'));
79
+
80
+ expect(Math.floor(el.minSliderX)).to.eq(SLIDER_WIDTH); // leftmost valid position
81
+ // allow date value less than slider range
82
+ expect(el.minSelectedDate).to.eq('10/1/1850');
83
+
84
+ /* -------------------------- maximum (right) slider ------------------------- */
85
+ expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
86
+ const maxDateInput = el.shadowRoot?.querySelector(
87
+ '#date-max'
88
+ ) as HTMLInputElement;
89
+
90
+ // set valid max date
91
+ maxDateInput.value = '3/12/1975';
92
+ maxDateInput.dispatchEvent(pressEnterEvent);
93
+
94
+ expect(Math.floor(el.maxSliderX)).to.eq(121);
95
+ expect(maxDateInput.value).to.eq('3/12/1975');
96
+
97
+ // attempt to set date later than last item
98
+ maxDateInput.value = '12/31/2199';
99
+ maxDateInput.dispatchEvent(new Event('blur'));
100
+ await el.updateComplete;
101
+
102
+ expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH); // rightmost valid position
103
+ // allow date value greater than slider range
104
+ expect(maxDateInput.value).to.eq('12/31/2199');
105
+ });
106
+
107
+ it('handles invalid date inputs', async () => {
108
+ const el = await createCustomElementInHTMLContainer();
109
+
110
+ /* -------------------------- minimum (left) slider ------------------------- */
111
+ const minDateInput = el.shadowRoot?.querySelector(
112
+ '#date-min'
113
+ ) as HTMLInputElement;
114
+
115
+ minDateInput.value = '5/17/1961';
116
+ minDateInput.dispatchEvent(new Event('blur'));
117
+ await el.updateComplete;
118
+
119
+ expect(Math.floor(el.minSliderX)).to.eq(101);
120
+ expect(minDateInput.value).to.eq('5/17/1961');
121
+
122
+ // enter invalid value
123
+ minDateInput.value = 'invalid';
124
+ minDateInput.dispatchEvent(new Event('blur'));
125
+ await el.updateComplete;
126
+
127
+ expect(Math.floor(el.minSliderX)).to.eq(101); // does not move
128
+ expect(minDateInput.value).to.eq('5/17/1961'); // resets back to previous date
129
+
130
+ /* -------------------------- maximum (right) slider ------------------------- */
131
+ const maxDateInput = el.shadowRoot?.querySelector(
132
+ '#date-max'
133
+ ) as HTMLInputElement;
134
+
135
+ // initial values
136
+ expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
137
+ expect(maxDateInput.value).to.eq('12/4/2020');
138
+
139
+ // enter invalid value
140
+ maxDateInput.value = 'Abc 12, 1YYY';
141
+ maxDateInput.dispatchEvent(new Event('blur'));
142
+ await el.updateComplete;
143
+
144
+ expect(Math.floor(el.maxSliderX)).to.eq(WIDTH - SLIDER_WIDTH); // does not move
145
+ expect(maxDateInput.value).to.eq('12/4/2020'); // resets back to previous date
146
+ });
147
+
148
+ it('updates the date inputs when the sliders are moved', async () => {
149
+ const el = await createCustomElementInHTMLContainer();
150
+
151
+ /* -------------------------- minimum (left) slider ------------------------- */
152
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
153
+ const container = el.shadowRoot?.querySelector(
154
+ '#container'
155
+ ) as HTMLDivElement;
156
+ const minDateInput = el.shadowRoot?.querySelector(
157
+ '#date-min'
158
+ ) as HTMLInputElement;
159
+
160
+ // initial state
161
+ expect(minSlider.getBoundingClientRect().x).to.eq(108);
162
+ expect(Array.from(minSlider.classList).join(' ')).to.eq('draggable');
163
+
164
+ // pointer down
165
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
166
+ await el.updateComplete;
167
+
168
+ // cursor changes to 'grab'
169
+ const classList = minSlider.classList;
170
+ expect(classList.contains('draggable')).to.be.true;
171
+ expect(classList.contains('dragging')).to.be.true;
172
+
173
+ // slide to right
174
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 60 }));
175
+ await el.updateComplete;
176
+
177
+ // slider has moved
178
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(168);
179
+ // min date is updated
180
+ expect(minDateInput.value).to.eq('4/23/1940');
181
+
182
+ // stop dragging
183
+ window.dispatchEvent(new PointerEvent('pointerup'));
184
+ await el.updateComplete;
185
+
186
+ // cursor returns to normal
187
+ expect(Array.from(container.classList)).not.to.include('dragging');
188
+
189
+ /* -------------------------- maximum (right) slider ------------------------- */
190
+ const maxSlider = el.shadowRoot?.querySelector('#slider-max') as SVGElement;
191
+ const maxDateInput = el.shadowRoot?.querySelector(
192
+ '#date-max'
193
+ ) as HTMLInputElement;
194
+
195
+ // initial state
196
+ expect(maxSlider.getBoundingClientRect().x).to.eq(298);
197
+
198
+ // slide to left
199
+ maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 195 }));
200
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 165 }));
201
+ await el.updateComplete;
202
+
203
+ // slider has moved
204
+ expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268);
205
+ // max date is updated
206
+ expect(maxDateInput.value).to.eq('10/8/2000');
207
+ await el.updateComplete;
208
+
209
+ // try to slide min slider past max slider
210
+ minSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 62 }));
211
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 190 }));
212
+ await el.updateComplete;
213
+
214
+ // slider moves all the way to meet the right slider
215
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(258);
216
+
217
+ // try to slide max slider past min slider
218
+ maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 120 }));
219
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 50 }));
220
+ await el.updateComplete;
221
+ expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268); // max slider didn't move
222
+ });
223
+
224
+ it("emits a custom event when the element's date range changes", async () => {
225
+ const el = await createCustomElementInHTMLContainer();
226
+ el.updateDelay = 30; // set debounce delay of 30ms
227
+
228
+ const minDateInput = el.shadowRoot?.querySelector(
229
+ '#date-min'
230
+ ) as HTMLInputElement;
231
+ const updateEventPromise = oneEvent(el, 'histogramDateRangeUpdated');
232
+
233
+ // simulate typing a new value into input
234
+ minDateInput.value = '1955';
235
+ minDateInput.dispatchEvent(new Event('blur'));
236
+
237
+ // will wait longer than debounce delay
238
+ const { detail } = await updateEventPromise;
239
+ // verify that event is emitted
240
+ expect(detail.minDate).to.equal('1/1/1955');
241
+ expect(detail.maxDate).to.equal('12/4/2020');
242
+
243
+ let eventCount = 0;
244
+ el.addEventListener('histogramDateRangeUpdated', () => (eventCount += 1));
245
+
246
+ // events are not sent if no change since the last event that was sent
247
+ minDateInput.value = '1955';
248
+ minDateInput.dispatchEvent(new Event('blur'));
249
+ await aTimeout(60); // wait longer than debounce delay
250
+ expect(eventCount).to.equal(0);
251
+
252
+ const updateEventPromise2 = oneEvent(el, 'histogramDateRangeUpdated');
253
+
254
+ // with the debounce, multiple quick changes only result in one event sent
255
+ minDateInput.value = '1965';
256
+ minDateInput.dispatchEvent(new Event('blur'));
257
+ await aTimeout(10); // wait less than the debounce delay
258
+
259
+ minDateInput.dispatchEvent(new Event('focus'));
260
+ minDateInput.value = '1975';
261
+ minDateInput.dispatchEvent(new Event('blur'));
262
+ await aTimeout(10);
263
+
264
+ minDateInput.dispatchEvent(new Event('focus'));
265
+ minDateInput.value = '1985';
266
+ minDateInput.dispatchEvent(new Event('blur'));
267
+ await aTimeout(10);
268
+
269
+ const event2 = await updateEventPromise2;
270
+ expect(event2.detail.minDate).to.equal('1/1/1985');
271
+ expect(eventCount).to.equal(1); // only one event was fired
272
+ });
273
+
274
+ it('shows/hides tooltip when hovering over (or pointing at) a bar', async () => {
275
+ const el = await createCustomElementInHTMLContainer();
276
+ // include a number which will require commas (1,000,000)
277
+ el.bins = [1000000, 1, 100];
278
+ await aTimeout(10);
279
+ const bars = el.shadowRoot?.querySelectorAll(
280
+ '.bar'
281
+ ) as unknown as SVGRectElement[];
282
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
283
+ expect(tooltip.innerText).to.eq('');
284
+
285
+ // hover
286
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
287
+ await el.updateComplete;
288
+ expect(tooltip.innerText).to.match(
289
+ /^1,000,000 items\n1\/1\/1900 - 4\/23\/1940/
290
+ );
291
+ expect(getComputedStyle(tooltip).display).to.eq('block');
292
+
293
+ // leave
294
+ bars[0].dispatchEvent(new PointerEvent('pointerleave'));
295
+ await el.updateComplete;
296
+ expect(getComputedStyle(tooltip).display).to.eq('none');
297
+ expect(tooltip.innerText).to.eq('');
298
+
299
+ // ensure singular item is not pluralized
300
+ bars[1].dispatchEvent(new PointerEvent('pointerenter'));
301
+ await el.updateComplete;
302
+ expect(tooltip.innerText).to.match(/^1 item\n4\/23\/1940 - 8\/13\/1980/);
303
+ });
304
+
305
+ it('does not show tooltip while dragging', async () => {
306
+ const el = await createCustomElementInHTMLContainer();
307
+ const bars = el.shadowRoot?.querySelectorAll(
308
+ '.bar'
309
+ ) as unknown as SVGRectElement[];
310
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
311
+ expect(tooltip.innerText).to.eq('');
312
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
313
+
314
+ // pointer down and slide right
315
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
316
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100 }));
317
+ await el.updateComplete;
318
+
319
+ // hover over bar
320
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
321
+ await el.updateComplete;
322
+ // tooltip display is suppressed while dragging
323
+ expect(tooltip.style.display).to.eq('');
324
+ });
325
+
326
+ it('passes the a11y audit', async () => {
327
+ await fixture<HistogramDateRange>(subject).then(el =>
328
+ expect(el).shadowDom.to.be.accessible()
329
+ );
330
+ });
331
+
332
+ it('allows range to be pre-selected', async () => {
333
+ const el = await fixture<HistogramDateRange>(
334
+ html`
335
+ <histogram-date-range
336
+ minDate="1900"
337
+ maxDate="Dec 4, 2020"
338
+ minSelectedDate="2012"
339
+ maxSelectedDate="2019"
340
+ bins="[33, 1, 100]"
341
+ >
342
+ </histogram-date-range>
343
+ `
344
+ );
345
+ const minDateInput = el.shadowRoot?.querySelector(
346
+ '#date-min'
347
+ ) as HTMLInputElement;
348
+ expect(minDateInput.value).to.eq('2012');
349
+
350
+ const maxDateInput = el.shadowRoot?.querySelector(
351
+ '#date-max'
352
+ ) as HTMLInputElement;
353
+ expect(maxDateInput.value).to.eq('2019');
354
+ });
355
+
356
+ it('extends the selected range when the histogram is clicked outside of the current range', async () => {
357
+ const el = await fixture<HistogramDateRange>(
358
+ html`
359
+ <histogram-date-range
360
+ minDate="1900"
361
+ maxDate="2020"
362
+ minSelectedDate="1950"
363
+ maxSelectedDate="1955"
364
+ bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
365
+ >
366
+ </histogram-date-range>
367
+ `
368
+ );
369
+
370
+ const leftBarToClick = Array.from(
371
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
372
+ )[1]; // click on second bar to the left
373
+
374
+ leftBarToClick.dispatchEvent(new Event('click'));
375
+ await el.updateComplete;
376
+ expect(el.minSelectedDate).to.eq('1910'); // range was extended to left
377
+
378
+ const rightBarToClick = Array.from(
379
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
380
+ )[8]; // click on second bar from the right
381
+
382
+ rightBarToClick.dispatchEvent(new Event('click'));
383
+ expect(el.maxSelectedDate).to.eq('1998'); // range was extended to right
384
+ });
385
+
386
+ it('narrows the selected range when the histogram is clicked inside of the current range', async () => {
387
+ const el = await fixture<HistogramDateRange>(
388
+ html`
389
+ <histogram-date-range
390
+ minDate="1900"
391
+ maxDate="2020"
392
+ minSelectedDate="1900"
393
+ maxSelectedDate="2020"
394
+ bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
395
+ >
396
+ </histogram-date-range>
397
+ `
398
+ );
399
+
400
+ ///////////////////////////////////////////////
401
+ // NB: the slider nearest the clicked bar moves
402
+ ///////////////////////////////////////////////
403
+
404
+ const leftBarToClick = Array.from(
405
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
406
+ )[3]; // click on fourth bar to the left
407
+
408
+ leftBarToClick.dispatchEvent(new Event('click'));
409
+ expect(el.minSelectedDate).to.eq('1932'); // range was extended to the right
410
+
411
+ const rightBarToClick = Array.from(
412
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
413
+ )[8]; // click on second bar from the right
414
+
415
+ rightBarToClick.dispatchEvent(new Event('click'));
416
+ expect(el.maxSelectedDate).to.eq('1998'); // range was extended to the left
417
+ });
418
+
419
+ it('handles invalid pre-selected range by defaulting to overall max and min', async () => {
420
+ const el = await fixture<HistogramDateRange>(
421
+ html`
422
+ <histogram-date-range
423
+ minDate="1900"
424
+ maxDate="2020"
425
+ minSelectedDate="2000xyz"
426
+ maxSelectedDate="5000"
427
+ bins="[33, 1, 100]"
428
+ >
429
+ </histogram-date-range>
430
+ `
431
+ );
432
+ const minDateInput = el.shadowRoot?.querySelector(
433
+ '#date-min'
434
+ ) as HTMLInputElement;
435
+ // malformed min date defaults to overall min
436
+ expect(minDateInput.value).to.eq('1900');
437
+
438
+ const maxDateInput = el.shadowRoot?.querySelector(
439
+ '#date-max'
440
+ ) as HTMLInputElement;
441
+ // well-formed max date is allowed
442
+ expect(maxDateInput.value).to.eq('5000');
443
+ });
444
+
445
+ it('handles year values less than 1000 by overriding date format to just display year', async () => {
446
+ const el = await fixture<HistogramDateRange>(
447
+ html`
448
+ <histogram-date-range
449
+ dateFormat="M/D/YYYY"
450
+ minDate="-2000"
451
+ maxDate="2000"
452
+ minSelectedDate="-500"
453
+ maxSelectedDate="500"
454
+ bins="[33, 1, 100]"
455
+ >
456
+ </histogram-date-range>
457
+ `
458
+ );
459
+ const minDateInput = el.shadowRoot?.querySelector(
460
+ '#date-min'
461
+ ) as HTMLInputElement;
462
+ expect(minDateInput.value).to.eq('-500');
463
+
464
+ const maxDateInput = el.shadowRoot?.querySelector(
465
+ '#date-max'
466
+ ) as HTMLInputElement;
467
+ expect(maxDateInput.value).to.eq('500');
468
+ });
469
+
470
+ it('handles missing data', async () => {
471
+ let el = await fixture<HistogramDateRange>(
472
+ html`<histogram-date-range>
473
+ minDate="1900" maxDate="2020" bins=""
474
+ </histogram-date-range>`
475
+ );
476
+ expect(el.shadowRoot?.innerHTML).to.contain('no data');
477
+ el = await fixture<HistogramDateRange>(
478
+ html`<histogram-date-range
479
+ minDate="1900"
480
+ maxDate="2020"
481
+ bins="[]"
482
+ missingDataMessage="no data available"
483
+ ></histogram-date-range>`
484
+ );
485
+ expect(el.shadowRoot?.innerHTML).to.contain('no data available');
486
+ });
487
+
488
+ it('correctly displays data consisting of a single bin', async () => {
489
+ const el = await fixture<HistogramDateRange>(
490
+ html`
491
+ <histogram-date-range minDate="2020" maxDate="2020" bins="[50]">
492
+ </histogram-date-range>
493
+ `
494
+ );
495
+ const bars = el.shadowRoot?.querySelectorAll(
496
+ '.bar'
497
+ ) as unknown as SVGRectElement[];
498
+ const heights = Array.from(bars).map(b => b.height.baseVal.value);
499
+ expect(heights).to.eql([157]);
500
+ });
501
+
502
+ it('correctly displays small diff between max and min values', async () => {
503
+ const el = await fixture<HistogramDateRange>(
504
+ html`
505
+ <histogram-date-range bins="[1519,2643,1880,2041,1638,1441]">
506
+ </histogram-date-range>
507
+ `
508
+ );
509
+ const bars = el.shadowRoot?.querySelectorAll(
510
+ '.bar'
511
+ ) as unknown as SVGRectElement[];
512
+ const heights = Array.from(bars).map(b => b.height.baseVal.value);
513
+ expect(heights).to.eql([37, 40, 38, 38, 37, 36]);
514
+ });
515
+
516
+ it('has a disabled state', async () => {
517
+ const el = await fixture<HistogramDateRange>(
518
+ html`
519
+ <histogram-date-range
520
+ minDate="1900"
521
+ maxDate="2020"
522
+ disabled
523
+ bins="[33, 1, 100]"
524
+ >
525
+ </histogram-date-range>
526
+ `
527
+ );
528
+ expect(
529
+ el.shadowRoot
530
+ ?.querySelector('.inner-container')
531
+ ?.classList.contains('disabled')
532
+ ).to.eq(true);
533
+
534
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
535
+
536
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8); // initial state
537
+
538
+ // attempt to slide to right
539
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
540
+ await el.updateComplete;
541
+
542
+ // cursor is not draggable if disabled
543
+ expect(Array.from(minSlider.classList).join(' ')).to.eq('');
544
+
545
+ // attempt to slide to right
546
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
547
+ await el.updateComplete;
548
+
549
+ // slider does not moved if element disabled
550
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8);
551
+ });
552
+
553
+ it('has a loading state with an activity indicator', async () => {
554
+ const el = await fixture<HistogramDateRange>(
555
+ html`
556
+ <histogram-date-range
557
+ minDate="1900"
558
+ maxDate="2020"
559
+ loading
560
+ bins="[33, 1, 100]"
561
+ >
562
+ </histogram-date-range>
563
+ `
564
+ );
565
+ expect(
566
+ el.shadowRoot
567
+ ?.querySelector('ia-activity-indicator')
568
+ ?.attributes?.getNamedItem('mode')?.value
569
+ ).to.eq('processing');
570
+ });
571
+
572
+ it('can use LitElement bound properties', async () => {
573
+ const el = await fixture<HistogramDateRange>(
574
+ html`
575
+ <histogram-date-range
576
+ .minDate=${1900}
577
+ .maxDate=${'Dec 4, 2020'}
578
+ .minSelectedDate=${2012}
579
+ .maxSelectedDate=${2019}
580
+ .bins=${[33, 1, 100]}
581
+ >
582
+ </histogram-date-range>
583
+ `
584
+ );
585
+ const minDateInput = el.shadowRoot?.querySelector(
586
+ '#date-min'
587
+ ) as HTMLInputElement;
588
+ expect(minDateInput.value).to.eq('2012');
589
+
590
+ const maxDateInput = el.shadowRoot?.querySelector(
591
+ '#date-max'
592
+ ) as HTMLInputElement;
593
+ expect(maxDateInput.value).to.eq('2019');
594
+ });
595
+ });