@internetarchive/radio-player 0.0.3-a4 → 0.0.3

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.
Files changed (34) hide show
  1. package/.eslintrc +37 -0
  2. package/.gitignore +11 -0
  3. package/.storybook/.babelrc +15 -0
  4. package/.storybook/addons.js +7 -0
  5. package/.storybook/config.js +11 -0
  6. package/.storybook/webpack.config.js +5 -0
  7. package/demo/full-text-search-service.ts +32 -0
  8. package/demo/index.html +53 -0
  9. package/demo/radio-player-controller.ts +237 -0
  10. package/karma.bs.config.js +16 -0
  11. package/karma.conf.js +66 -0
  12. package/package-lock.json +51719 -0
  13. package/package.json +5 -5
  14. package/stories/index.stories.js +40 -0
  15. package/stories/transcript.js +683 -0
  16. package/test/assets/arrow.mp3 +0 -0
  17. package/test/music-zone.test.js +13 -0
  18. package/test/promised-sleep.js +3 -0
  19. package/test/radio-player-config.test.js +42 -0
  20. package/test/radio-player.test.js +1072 -0
  21. package/test/sample_transcript.js +1493 -0
  22. package/test/search-handler/search-backends/full-text-sample-response-brackets.js +60 -0
  23. package/test/search-handler/search-backends/full-text-sample-response-em.js +60 -0
  24. package/test/search-handler/search-backends/full-text-search-backend.test.js +74 -0
  25. package/test/search-handler/search-backends/full-text-search-response.test.js +14 -0
  26. package/test/search-handler/search-backends/local-search-backend.test.js +39 -0
  27. package/test/search-handler/search-handler.test.js +550 -0
  28. package/test/search-handler/search-helper.test.js +63 -0
  29. package/test/search-handler/search-model.test.js +56 -0
  30. package/test/search-handler/transcript-index.test.js +66 -0
  31. package/test/search-results-switcher.test.js +97 -0
  32. package/tsconfig.build.json +7 -0
  33. package/tsconfig.json +21 -0
  34. package/yarn.lock +14450 -0
@@ -0,0 +1,1072 @@
1
+ import {
2
+ html, fixture, expect, oneEvent, elementUpdated
3
+ } from '@open-wc/testing';
4
+
5
+ import promisedSleep from './promised-sleep';
6
+
7
+ import '../lib/src/radio-player';
8
+ import { AudioSource, TranscriptEntryConfig } from '../index';
9
+ import RadioPlayerConfig from '../lib/src/models/radio-player-config';
10
+ import sampleTranscript from './sample_transcript';
11
+ import { TranscriptConfig } from '@internetarchive/transcript-view';
12
+ import { SearchHandler } from '../lib/src/search-handler/search-handler';
13
+ import { LocalSearchBackend } from '../lib/src/search-handler/search-backends/local-search-backend/local-search-backend';
14
+ import { TranscriptIndex } from '../lib/src/search-handler/transcript-index';
15
+
16
+ describe('Radio Player', () => {
17
+ describe('Configuration', () => {
18
+ it('has no configuration', async () => {
19
+ const el = await fixture(html`
20
+ <radio-player></radio-player>
21
+ `);
22
+
23
+ expect(el.config).to.equal(undefined);
24
+ });
25
+
26
+ it('can be configured with a title and date', async () => {
27
+ const config = new RadioPlayerConfig('foo-title', 'bar-date', '', undefined, []);
28
+
29
+ const el = await fixture(html`
30
+ <radio-player .config=${config}></radio-player>
31
+ `);
32
+
33
+ const titleDisplay = el.shadowRoot.querySelector('.title');
34
+ const dateDisplay = el.shadowRoot.querySelector('.date');
35
+
36
+ expect(titleDisplay.innerText).to.equal('foo-title');
37
+ expect(dateDisplay.innerText).to.equal('bar-date');
38
+ });
39
+
40
+ it('does renders the waveform if one is passed in', async () => {
41
+ const config = new RadioPlayerConfig('foo-title', 'bar-date', '', 'waveform.png', []);
42
+
43
+ const el = await fixture(html`
44
+ <radio-player .config=${config}></radio-player>
45
+ `);
46
+
47
+ expect(el.shadowRoot.querySelectorAll('waveform-progress').length).to.equal(1);
48
+ });
49
+
50
+ it('does not render the waveform if one is not passed in', async () => {
51
+ const config = new RadioPlayerConfig('foo-title', 'bar-date', '', undefined, []);
52
+
53
+ const el = await fixture(html`
54
+ <radio-player .config=${config}></radio-player>
55
+ `);
56
+
57
+ expect(el.shadowRoot.querySelectorAll('waveform-progress').length).to.equal(0);
58
+ });
59
+ });
60
+
61
+ describe('Setup', () => {
62
+ it('renders all of the major sub components', async () => {
63
+ const el = await fixture(html`
64
+ <radio-player></radio-player>
65
+ `);
66
+
67
+ const { shadowRoot } = el;
68
+
69
+ expect(shadowRoot.querySelectorAll('transcript-view').length).to.equal(1);
70
+ expect(shadowRoot.querySelectorAll('.collection-logo').length).to.equal(1);
71
+ expect(shadowRoot.querySelectorAll('.title-date').length).to.equal(1);
72
+ expect(shadowRoot.querySelectorAll('audio-element').length).to.equal(1);
73
+ expect(shadowRoot.querySelectorAll('playback-controls').length).to.equal(1);
74
+ expect(shadowRoot.querySelectorAll('scrubber-bar').length).to.equal(1);
75
+ });
76
+ });
77
+
78
+ describe('Playback Functionality', () => {
79
+ it('can play audio', async () => {
80
+ const audioMp3 = new AudioSource('./assets/arrow.mp3', 'audio/mpeg');
81
+ const audioSources = [audioMp3];
82
+
83
+ const config = new RadioPlayerConfig('foo-title', 'bar-date', '', '', audioSources);
84
+
85
+ const el = await fixture(html`
86
+ <radio-player .config=${config}></radio-player>
87
+ `);
88
+
89
+ // we have to do this in a setTimeout so the event listener below has a chance to listen
90
+ setTimeout(() => { el.play(); });
91
+ const response = await oneEvent(el, 'playbackStarted');
92
+ expect(response).to.exist;
93
+ });
94
+
95
+ it('can pause audio', async () => {
96
+ const audioMp3 = new AudioSource('./assets/arrow.mp3', 'audio/mpeg');
97
+ const audioSources = [audioMp3];
98
+
99
+ const config = new RadioPlayerConfig('foo-title', 'bar-date', '', '', audioSources);
100
+
101
+ const el = await fixture(html`
102
+ <radio-player .config=${config}></radio-player>
103
+ `);
104
+
105
+ el.play();
106
+
107
+ // we have to do this in a setTimeout so the event listener below has a chance to listen
108
+ setTimeout(() => { el.pause(); });
109
+ const response = await oneEvent(el, 'playbackPaused');
110
+ expect(response).to.exist;
111
+ });
112
+
113
+ it('seeks the audio element back by 10 seconds when the backButtonHandler is called', async () => {
114
+ const el = await fixture(html`
115
+ <radio-player></radio-player>
116
+ `);
117
+
118
+ let seekTime = 0;
119
+ const audioElement = el.shadowRoot.querySelector('audio-element');
120
+ audioElement.seekBy = (time) => { seekTime = time; };
121
+ el.backButtonHandler();
122
+ expect(seekTime).to.equal(-10);
123
+ });
124
+
125
+ it('seeks the audio element forward by 10 seconds when the forwardButtonHandler is called', async () => {
126
+ const el = await fixture(html`
127
+ <radio-player></radio-player>
128
+ `);
129
+
130
+ let seekTime = 0;
131
+ const audioElement = el.shadowRoot.querySelector('audio-element');
132
+ audioElement.seekBy = (time) => { seekTime = time; };
133
+ el.forwardButtonHandler();
134
+ expect(seekTime).to.equal(10);
135
+ });
136
+
137
+ it('handles play / pause button correctly', async () => {
138
+ const el = await fixture(html`
139
+ <radio-player></radio-player>
140
+ `);
141
+
142
+ let playCalled = false;
143
+ let pauseCalled = false;
144
+
145
+ const audioElement = el.shadowRoot.querySelector('audio-element');
146
+ audioElement.play = () => { playCalled = true; };
147
+ audioElement.pause = () => { pauseCalled = true; };
148
+
149
+ expect(el.isPlaying).to.equal(false);
150
+ el.playPauseButtonHandler();
151
+
152
+ expect(el.isPlaying).to.equal(true);
153
+ expect(playCalled).to.equal(true);
154
+ expect(pauseCalled).to.equal(false);
155
+
156
+ el.playPauseButtonHandler();
157
+
158
+ expect(el.isPlaying).to.equal(false);
159
+ expect(pauseCalled).to.equal(true);
160
+ });
161
+ });
162
+
163
+ describe('Searching', () => {
164
+ class MockSearchHandler {
165
+ search(query) {
166
+ this.searchCalled = true;
167
+ return new Promise(resolve => resolve());
168
+ }
169
+
170
+ constructor() {
171
+ this.searchCalled = false;
172
+ }
173
+ }
174
+
175
+ it('shows and hides the search results switcher', async () => {
176
+ const el = await fixture(html`
177
+ <radio-player></radio-player>
178
+ `);
179
+
180
+ const searchResultsSwitcher = el.shadowRoot.querySelector('search-results-switcher');
181
+ expect(searchResultsSwitcher.classList.contains('hidden')).to.equal(true);
182
+
183
+ el.shouldShowSearchResultSwitcher = true;
184
+ await promisedSleep(100);
185
+ expect(searchResultsSwitcher.classList.contains('hidden')).to.equal(false);
186
+ });
187
+
188
+ it('shows and hides the no search results message', async () => {
189
+ const el = await fixture(html`
190
+ <radio-player></radio-player>
191
+ `);
192
+
193
+ const searchResultsSwitcher = el.shadowRoot.querySelector('.no-search-results-message');
194
+ expect(searchResultsSwitcher.classList.contains('hidden')).to.equal(true);
195
+
196
+ el.shouldShowNoSearchResultMessage = true;
197
+ await promisedSleep(100);
198
+ expect(searchResultsSwitcher.classList.contains('hidden')).to.equal(false);
199
+ });
200
+
201
+ it('updates the search term when updateSearchTerm event is emitted', async () => {
202
+ const el = await fixture(html`
203
+ <radio-player></radio-player>
204
+ `);
205
+
206
+ const searchBar = el.shadowRoot.querySelector('expandable-search-bar');
207
+ const inputField = searchBar.shadowRoot.querySelector('input[type=text]');
208
+ inputField.value = 'foo';
209
+ const keyUpEvent = new KeyboardEvent('keyup');
210
+ inputField.dispatchEvent(keyUpEvent);
211
+ expect(el.searchTerm).to.equal('foo');
212
+ });
213
+
214
+ it('executes the search when the enter key is pressed', async () => {
215
+ const el = await fixture(html`
216
+ <radio-player></radio-player>
217
+ `);
218
+
219
+ const searchHandler = new MockSearchHandler();
220
+
221
+ el.searchHandler = searchHandler;
222
+
223
+ const searchBar = el.shadowRoot.querySelector('expandable-search-bar');
224
+ const inputField = searchBar.shadowRoot.querySelector('input[type=text]');
225
+ inputField.value = 'foo';
226
+
227
+ const event = new KeyboardEvent('keyup', { key: 'Enter' });
228
+ inputField.dispatchEvent(event);
229
+
230
+ expect(searchHandler.searchCalled).to.be.true;
231
+ });
232
+
233
+ it('does not execute search when enter pressed if there is no value in the return', async () => {
234
+ const el = await fixture(html`
235
+ <radio-player></radio-player>
236
+ `);
237
+
238
+ const searchHandler = new MockSearchHandler();
239
+
240
+ el.searchHandler = searchHandler;
241
+
242
+ const searchBar = el.shadowRoot.querySelector('expandable-search-bar');
243
+ const inputField = searchBar.shadowRoot.querySelector('input[type=text]');
244
+ inputField.value = '';
245
+
246
+ const event = new KeyboardEvent('keyup', { key: 'Enter' });
247
+ inputField.dispatchEvent(event);
248
+
249
+ expect(searchHandler.searchCalled).to.be.false;
250
+ });
251
+
252
+ it('sets the search transcript to undefined if there is no search handler', async () => {
253
+ const el = await fixture(html`
254
+ <radio-player></radio-player>
255
+ `);
256
+ const mockSearchResultsTranscript = new TranscriptConfig([]);
257
+ el.searchResultsTranscript = mockSearchResultsTranscript;
258
+ expect(el.searchResultsTranscript).to.not.be.undefined;
259
+ el.executeSearch('foo bar');
260
+ expect(el.searchResultsTranscript).to.be.undefined;
261
+ });
262
+
263
+ it('sets the search transcript to undefined if the search term is less than 2 characters', async () => {
264
+ const el = await fixture(html`
265
+ <radio-player></radio-player>
266
+ `);
267
+ const searchHandler = new MockSearchHandler();
268
+ el.searchHandler = searchHandler;
269
+ const mockSearchResultsTranscript = new TranscriptConfig([]);
270
+ el.searchResultsTranscript = mockSearchResultsTranscript;
271
+ el.executeSearch('f');
272
+ expect(el.searchResultsTranscript).to.be.undefined;
273
+ });
274
+
275
+ it('does not update the search term if updateSearchTerm event does not contain needed info', async () => {
276
+ const el = await fixture(html`
277
+ <radio-player></radio-player>
278
+ `);
279
+
280
+ el.searchTerm = 'boop';
281
+
282
+ const event = new CustomEvent('foo', { });
283
+ el.updateSearchTerm(event);
284
+ expect(el.searchTerm).to.equal('boop');
285
+
286
+ const event2 = new CustomEvent('foo', { detail: { } });
287
+ el.updateSearchTerm(event2);
288
+ expect(el.searchTerm).to.equal('boop');
289
+ });
290
+
291
+
292
+ it('can clear searches properly', async () => {
293
+ const el = await fixture(html`
294
+ <radio-player searchTerm='foo search'></radio-player>
295
+ `);
296
+
297
+ const transcriptView = el.shadowRoot.querySelector('transcript-view');
298
+ const searchResultsSwitcher = el.shadowRoot.querySelector('search-results-switcher');
299
+
300
+ expect(el.searchTerm).to.equal('foo search');
301
+
302
+ el.searchCleared();
303
+ expect(el.searchTerm).to.equal('');
304
+ expect(el.searchResultsTranscript).to.equal(undefined);
305
+ expect(searchResultsSwitcher.currentResultIndex).to.equal(0);
306
+ expect(transcriptView.selectedSearchResultIndex).to.equal(0);
307
+ });
308
+
309
+ it('retrieves no search results if there is no transcript config', async () => {
310
+ const el = await fixture(html`
311
+ <radio-player>
312
+ </radio-player>
313
+ `);
314
+
315
+ expect(el.searchResults.length).to.equal(0);
316
+ });
317
+
318
+ it('properly updates the search results switcher when there are search results', async () => {
319
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
320
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
321
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
322
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
323
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'foo', false);
324
+ const entries = [entry1, entry2, entry3, entry4, entry5];
325
+
326
+ const transcriptConfig = new TranscriptConfig(entries);
327
+ const transcriptIndex = new TranscriptIndex(transcriptConfig);
328
+ const searchBackend = new LocalSearchBackend(transcriptIndex);
329
+ const searchHandler = new SearchHandler(searchBackend, transcriptIndex);
330
+
331
+ const el = await fixture(html`
332
+ <radio-player
333
+ .transcriptConfig=${transcriptConfig}
334
+ .searchHandler=${searchHandler}>
335
+ </radio-player>
336
+ `);
337
+
338
+ const searchResultsSwitcher = el.shadowRoot.querySelector('search-results-switcher');
339
+
340
+ el.executeSearch('foo');
341
+
342
+ await promisedSleep(100);
343
+
344
+ expect(el.shouldShowSearchResultSwitcher).to.equal(true);
345
+ expect(searchResultsSwitcher.numberOfResults).to.equal(2);
346
+ });
347
+
348
+
349
+ it('properly updates the search results switcher when there are no search results', async () => {
350
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
351
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
352
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
353
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
354
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
355
+ const entries = [entry1, entry2, entry3, entry4, entry5];
356
+
357
+ const transcriptConfig = new TranscriptConfig(entries);
358
+
359
+ const el = await fixture(html`
360
+ <radio-player
361
+ .transcriptConfig=${transcriptConfig}>
362
+ </radio-player>
363
+ `);
364
+
365
+ el.searchTerm = 'foo';
366
+ el.updateSearchResultSwitcher();
367
+
368
+ expect(el.shouldShowNoSearchResultMessage).to.equal(true);
369
+ expect(el.shouldShowSearchResultSwitcher).to.equal(false);
370
+ });
371
+ });
372
+
373
+ describe('Interactions', () => {
374
+ it('updates `playbackRate` when `changePlaybackRate` callback is triggered', async () => {
375
+ const el = await fixture(html`
376
+ <radio-player></radio-player>
377
+ `);
378
+
379
+ const event = new CustomEvent('foo', { detail: { playbackRate: 1.5 } });
380
+ el.changePlaybackRate(event);
381
+ expect(el.playbackRate).to.equal(1.5);
382
+ });
383
+
384
+ it('does not update `playbackRate` when `changePlaybackRate` is not passed a proper event', async () => {
385
+ const el = await fixture(html`
386
+ <radio-player></radio-player>
387
+ `);
388
+
389
+ const event = new CustomEvent('foo', { });
390
+ el.changePlaybackRate(event);
391
+ expect(el.playbackRate).to.equal(1);
392
+
393
+ const event2 = new CustomEvent('foo', { detail: { } });
394
+ el.changePlaybackRate(event2);
395
+ expect(el.playbackRate).to.equal(1);
396
+ });
397
+
398
+ it('updates `volume` when `volumeChanged` callback is triggered', async () => {
399
+ const el = await fixture(html`
400
+ <radio-player></radio-player>
401
+ `);
402
+
403
+ const event = new CustomEvent('foo', { detail: { volume: 75 } });
404
+ el.volumeChanged(event);
405
+ expect(el.volume).to.equal(75);
406
+ });
407
+
408
+ it('does not update `volume` if `volumeChanged` event is not properly structured', async () => {
409
+ const el = await fixture(html`
410
+ <radio-player></radio-player>
411
+ `);
412
+
413
+ el.volume = 23;
414
+
415
+ const event = new CustomEvent('foo', { });
416
+ el.volumeChanged(event);
417
+ expect(el.volume).to.equal(23);
418
+
419
+ const event2 = new CustomEvent('foo', { detail: { } });
420
+ el.volumeChanged(event2);
421
+ expect(el.volume).to.equal(23);
422
+ });
423
+
424
+ it('updates `duration` when `handleDurationChange` callback is triggered', async () => {
425
+ const el = await fixture(html`
426
+ <radio-player></radio-player>
427
+ `);
428
+
429
+ const event = new CustomEvent('foo', { detail: { duration: 75 } });
430
+ el.handleDurationChange(event);
431
+ expect(el.duration).to.equal(75);
432
+ });
433
+
434
+ it('does not update `duration` if `handleDurationChange` event is not properly structured', async () => {
435
+ const el = await fixture(html`
436
+ <radio-player></radio-player>
437
+ `);
438
+
439
+ el.duration = 23;
440
+
441
+ const event = new CustomEvent('foo', { });
442
+ el.handleDurationChange(event);
443
+ expect(el.duration).to.equal(23);
444
+
445
+ const event2 = new CustomEvent('foo', { detail: { } });
446
+ el.handleDurationChange(event2);
447
+ expect(el.duration).to.equal(23);
448
+ });
449
+
450
+ it('updates `currentTime` when `handleTimeChange` callback is triggered', async () => {
451
+ const el = await fixture(html`
452
+ <radio-player></radio-player>
453
+ `);
454
+
455
+ const event = new CustomEvent('foo', { detail: { currentTime: 75 } });
456
+ el.handleTimeChange(event);
457
+ expect(el.currentTime).to.equal(75);
458
+ });
459
+
460
+ it('does not update `currentTime` if `handleTimeChange` event is not properly structured', async () => {
461
+ const el = await fixture(html`
462
+ <radio-player></radio-player>
463
+ `);
464
+
465
+ el.currentTime = 15;
466
+
467
+ const event = new CustomEvent('foo', { });
468
+ el.handleTimeChange(event);
469
+ expect(el.currentTime).to.equal(15);
470
+
471
+ const event2 = new CustomEvent('foo', { detail: { } });
472
+ el.handleTimeChange(event2);
473
+ expect(el.currentTime).to.equal(15);
474
+ });
475
+
476
+ it('updates `currentTime` when `valueChangedFromScrub` callback is triggered', async () => {
477
+ const el = await fixture(html`
478
+ <radio-player></radio-player>
479
+ `);
480
+
481
+ el.duration = 100;
482
+
483
+ const event = new CustomEvent('foo', { detail: { value: 75 } });
484
+ el.valueChangedFromScrub(event);
485
+ expect(el.currentTime).to.equal(75);
486
+ });
487
+
488
+ it('does not update `currentTime` if `valueChangedFromScrub` event is not properly structured', async () => {
489
+ const el = await fixture(html`
490
+ <radio-player></radio-player>
491
+ `);
492
+
493
+ el.duration = 100;
494
+ el.currentTime = 15;
495
+
496
+ const event = new CustomEvent('foo', { });
497
+ el.valueChangedFromScrub(event);
498
+ expect(el.currentTime).to.equal(15);
499
+
500
+ const event2 = new CustomEvent('foo', { detail: { } });
501
+ el.valueChangedFromScrub(event2);
502
+ expect(el.currentTime).to.equal(15);
503
+ });
504
+ });
505
+
506
+ describe('Transcript Interactions', () => {
507
+ it('updates `currentTime` when `transcriptEntrySelected` callback is triggered', async () => {
508
+ const el = await fixture(html`
509
+ <radio-player></radio-player>
510
+ `);
511
+
512
+ const event = new CustomEvent('foo', { detail: { entry: { start: 75 } } });
513
+ el.transcriptEntrySelected(event);
514
+ expect(el.currentTime).to.equal(75);
515
+ });
516
+
517
+ it('does not update `currentTime` if `transcriptEntrySelected` event is not properly structured', async () => {
518
+ const el = await fixture(html`
519
+ <radio-player></radio-player>
520
+ `);
521
+
522
+ el.currentTime = 15;
523
+
524
+ const event = new CustomEvent('foo', { });
525
+ el.transcriptEntrySelected(event);
526
+ expect(el.currentTime).to.equal(15);
527
+
528
+ const event2 = new CustomEvent('foo', { detail: { } });
529
+ el.transcriptEntrySelected(event2);
530
+ expect(el.currentTime).to.equal(15);
531
+
532
+ const event3 = new CustomEvent('foo', { detail: { entry: {} } });
533
+ el.transcriptEntrySelected(event3);
534
+ expect(el.currentTime).to.equal(15);
535
+ });
536
+
537
+ it('updates the transcriptView result index and scrolls when `searchResultIndexChanged` callback is triggered', async () => {
538
+ const el = await fixture(html`
539
+ <radio-player></radio-player>
540
+ `);
541
+
542
+ let scrollCalled = false;
543
+
544
+ const transcriptView = el.shadowRoot.querySelector('transcript-view');
545
+ transcriptView.scrollToSelectedSearchResult = () => { scrollCalled = true; };
546
+
547
+ const event = new CustomEvent('foo', { detail: { searchResultIndex: 3 } });
548
+ el.searchResultIndexChanged(event);
549
+ expect(transcriptView.selectedSearchResultIndex).to.equal(3);
550
+ expect(scrollCalled).to.equal(true);
551
+ });
552
+
553
+ it('does not update the searchResultIndexChanged if event does not contain needed info', async () => {
554
+ const el = await fixture(html`
555
+ <radio-player></radio-player>
556
+ `);
557
+ const transcriptView = el.shadowRoot.querySelector('transcript-view');
558
+ transcriptView.selectedSearchResultIndex = 13;
559
+
560
+ const event = new CustomEvent('foo', { });
561
+ el.searchResultIndexChanged(event);
562
+ expect(transcriptView.selectedSearchResultIndex).to.equal(13);
563
+
564
+ const event2 = new CustomEvent('foo', { detail: { } });
565
+ el.searchResultIndexChanged(event);
566
+ expect(transcriptView.selectedSearchResultIndex).to.equal(13);
567
+ });
568
+
569
+
570
+ it('updates the transcriptView result index and scrolls when `searchResultIndexChanged` callback is triggered', async () => {
571
+ const el = await fixture(html`
572
+ <radio-player></radio-player>
573
+ `);
574
+
575
+ let scrollCalled = false;
576
+
577
+ const transcriptView = el.shadowRoot.querySelector('transcript-view');
578
+ transcriptView.scrollToSelectedSearchResult = () => { scrollCalled = true; };
579
+
580
+ const event = new CustomEvent('foo', { detail: { searchResultIndex: 3 } });
581
+ el.searchResultIndexChanged(event);
582
+ expect(transcriptView.selectedSearchResultIndex).to.equal(3);
583
+ expect(scrollCalled).to.equal(true);
584
+ });
585
+
586
+ it('does not update transcriptView when search result index changed if transcript view does not exist', async () => {
587
+ const el = await fixture(html`
588
+ <radio-player></radio-player>
589
+ `);
590
+
591
+ let scrollCalled = false;
592
+
593
+ const transcriptView = el.shadowRoot.querySelector('transcript-view');
594
+ transcriptView.remove();
595
+
596
+ transcriptView.scrollToSelectedSearchResult = () => { scrollCalled = true; };
597
+
598
+ const event = new CustomEvent('foo', { detail: { searchResultIndex: 3 } });
599
+ el.searchResultIndexChanged(event);
600
+ expect(scrollCalled).to.equal(false);
601
+ });
602
+
603
+ it('handles transcript entry selection correctly', async () => {
604
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
605
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
606
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
607
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
608
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
609
+ const entries = [entry1, entry2, entry3, entry4, entry5];
610
+
611
+ const transcriptConfig = new TranscriptConfig(entries);
612
+
613
+ const el = await fixture(html`
614
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
615
+ `);
616
+ const audioElement = el.shadowRoot.querySelector('audio-element');
617
+
618
+ let seekedTime = 0;
619
+ let playCalled = false;
620
+
621
+ audioElement.seekTo = (time) => {
622
+ seekedTime = time;
623
+ };
624
+
625
+ audioElement.play = () => {
626
+ playCalled = true;
627
+ };
628
+
629
+ el.duration = 100;
630
+
631
+ const event = new CustomEvent('foo', { detail: { entry: entry3 } });
632
+
633
+ setTimeout(() => { el.transcriptEntrySelected(event); });
634
+ const response = await oneEvent(el, 'transcriptEntrySelected');
635
+
636
+ expect(response.detail.newTime).to.equal(37);
637
+ expect(seekedTime).to.equal(37);
638
+ expect(playCalled).to.equal(true);
639
+ expect(el.currentTime).to.equal(37);
640
+ });
641
+ });
642
+
643
+ describe('Scrubber Bar', () => {
644
+ it('generates proper scrubber bar section markers', async () => {
645
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
646
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
647
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
648
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
649
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
650
+ const entries = [entry1, entry2, entry3, entry4, entry5];
651
+
652
+ const transcriptConfig = new TranscriptConfig(entries);
653
+
654
+ const el = await fixture(html`
655
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
656
+ `);
657
+
658
+ el.duration = 100;
659
+
660
+ const expected = [0, 18, 37, 57, 74, 100];
661
+
662
+ el.scrubberBarMarkerPercentages.forEach((element, index) => {
663
+ // we need to round the values because sometimes the percentage calculations
664
+ // come out to 56.9999999
665
+ expect(Math.round(element)).to.equal(expected[index]);
666
+ });
667
+ });
668
+
669
+ it('seeks to the next section when the nextSectionButtonHandler is called', async () => {
670
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
671
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
672
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
673
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
674
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
675
+ const entries = [entry1, entry2, entry3, entry4, entry5];
676
+
677
+ const transcriptConfig = new TranscriptConfig(entries);
678
+
679
+ const el = await fixture(html`
680
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
681
+ `);
682
+ const audioElement = el.shadowRoot.querySelector('audio-element');
683
+
684
+ let seekedTime = 0;
685
+
686
+ audioElement.seekTo = (time) => {
687
+ seekedTime = time;
688
+ };
689
+
690
+ el.duration = 100;
691
+ el.percentComplete = 35;
692
+
693
+ el.nextSectionButtonHandler();
694
+
695
+ expect(seekedTime).to.equal(37.1);
696
+ });
697
+
698
+ it('seeks to the beginning of the section when the previousSectionButtonHandler is called', async () => {
699
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
700
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
701
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
702
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
703
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
704
+ const entries = [entry1, entry2, entry3, entry4, entry5];
705
+
706
+ const transcriptConfig = new TranscriptConfig(entries);
707
+
708
+ const el = await fixture(html`
709
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
710
+ `);
711
+ const audioElement = el.shadowRoot.querySelector('audio-element');
712
+
713
+ let seekedTime = 0;
714
+
715
+ audioElement.seekTo = (time) => {
716
+ seekedTime = time;
717
+ };
718
+
719
+ el.duration = 100;
720
+ el.percentComplete = 35;
721
+
722
+ el.prevSectionButtonHandler();
723
+
724
+ expect(seekedTime).to.equal(17.9);
725
+ });
726
+
727
+ it('handles audio scrubbing correctly', async () => {
728
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
729
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
730
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
731
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
732
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
733
+ const entries = [entry1, entry2, entry3, entry4, entry5];
734
+
735
+ const transcriptConfig = new TranscriptConfig(entries);
736
+
737
+ const el = await fixture(html`
738
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
739
+ `);
740
+ const audioElement = el.shadowRoot.querySelector('audio-element');
741
+
742
+ let seekedTime = 0;
743
+
744
+ audioElement.seekTo = (time) => {
745
+ seekedTime = time;
746
+ };
747
+
748
+ el.duration = 100;
749
+
750
+ const event = new CustomEvent('foo', { detail: { value: 73 } });
751
+
752
+ setTimeout(() => { el.valueChangedFromScrub(event); });
753
+ const response = await oneEvent(el, 'timeChangedFromScrub');
754
+
755
+ expect(response.detail.newTime).to.equal(73);
756
+ expect(seekedTime).to.equal(73);
757
+ expect(el.currentTime).to.equal(73);
758
+ expect(el.percentComplete).to.equal(73);
759
+ });
760
+ });
761
+
762
+ describe('Music Zone Handling', () => {
763
+ it('skips music zone properly', async () => {
764
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
765
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
766
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
767
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
768
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
769
+ const entries = [entry1, entry2, entry3, entry4, entry5];
770
+
771
+ const transcriptConfig = new TranscriptConfig(entries);
772
+
773
+ const el = await fixture(html`
774
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
775
+ `);
776
+ const audioElement = el.shadowRoot.querySelector('audio-element');
777
+
778
+ let seekedTime = 0;
779
+
780
+ audioElement.seekTo = (time) => {
781
+ seekedTime = time;
782
+ };
783
+
784
+ el.duration = 100;
785
+ el.currentTime = 59;
786
+ el.skipMusicZone();
787
+ expect(seekedTime).to.equal(74.1);
788
+ });
789
+
790
+ it('does not skip music zone if not currently in one', async () => {
791
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
792
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
793
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
794
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
795
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
796
+ const entries = [entry1, entry2, entry3, entry4, entry5];
797
+
798
+ const transcriptConfig = new TranscriptConfig(entries);
799
+
800
+ const el = await fixture(html`
801
+ <radio-player .transcriptConfig=${transcriptConfig}></radio-player>
802
+ `);
803
+ const audioElement = el.shadowRoot.querySelector('audio-element');
804
+
805
+ let seekedTime = -1;
806
+
807
+ audioElement.seekTo = (time) => {
808
+ seekedTime = time;
809
+ };
810
+
811
+ el.duration = 100;
812
+ el.currentTime = 39;
813
+ el.skipMusicZone();
814
+
815
+ // seekTo is never called because we're not in a music zone
816
+ expect(seekedTime).to.equal(-1);
817
+ expect(el.currentTime).to.equal(39);
818
+ });
819
+
820
+ it('calls `skipMusicZone` if music skipping is enabled', async () => {
821
+ const entry1 = new TranscriptEntryConfig(1, 1, 17, 'foo', false);
822
+ const entry2 = new TranscriptEntryConfig(1, 18, 37, '', true);
823
+ const entry3 = new TranscriptEntryConfig(1, 37, 56, 'bar', false);
824
+ const entry4 = new TranscriptEntryConfig(1, 57, 74, 'baz', true);
825
+ const entry5 = new TranscriptEntryConfig(1, 75, 100, 'baz', false);
826
+ const entries = [entry1, entry2, entry3, entry4, entry5];
827
+
828
+ const transcriptConfig = new TranscriptConfig(entries);
829
+
830
+ const el = await fixture(html`
831
+ <radio-player
832
+ .transcriptConfig=${transcriptConfig}
833
+ skipMusicSections="true">
834
+ </radio-player>
835
+ `);
836
+
837
+ let skipCalled = false;
838
+
839
+ el.skipMusicZone = () => {
840
+ skipCalled = true;
841
+ };
842
+
843
+ el.duration = 100;
844
+ el.currentTime = 39;
845
+
846
+ await promisedSleep(50);
847
+
848
+ expect(skipCalled).to.equal(true);
849
+ });
850
+ });
851
+
852
+ describe('Quick Search Entries', () => {
853
+ it('properly generates QuickSearchEntry objects from config', async () => {
854
+ const config = new RadioPlayerConfig('foo-title', 'bar-date', '', '', [], ['foo', 'bar', 'baz']);
855
+
856
+ const el = await fixture(html`
857
+ <radio-player
858
+ .config=${config}>
859
+ </radio-player>
860
+ `);
861
+
862
+ const { quickSearches } = el;
863
+
864
+ expect(quickSearches[0].displayText).to.equal('foo');
865
+ expect(quickSearches[1].displayText).to.equal('bar');
866
+ expect(quickSearches[2].displayText).to.equal('baz');
867
+ });
868
+ });
869
+
870
+ describe('Events', () => {
871
+ it('emits playbackRateChanged event', async () => {
872
+ const el = await fixture(html`
873
+ <radio-player></radio-player>
874
+ `);
875
+
876
+ const event = new CustomEvent('foo', {
877
+ detail: { playbackRate: 1.25 },
878
+ });
879
+ setTimeout(() => { el.changePlaybackRate(event); });
880
+ const response = await oneEvent(el, 'playbackRateChanged');
881
+ expect(response.detail.playbackRate).to.equal(1.25);
882
+ });
883
+
884
+ it('emits volumeChanged event', async () => {
885
+ const el = await fixture(html`
886
+ <radio-player></radio-player>
887
+ `);
888
+
889
+ const event = new CustomEvent('foo', {
890
+ detail: { volume: 0.5 },
891
+ });
892
+ setTimeout(() => { el.volumeChanged(event); });
893
+ const response = await oneEvent(el, 'volumeChanged');
894
+ expect(response.detail.volume).to.equal(0.5);
895
+ });
896
+
897
+ it('emits jumpBackButtonPressed event', async () => {
898
+ const el = await fixture(html`
899
+ <radio-player></radio-player>
900
+ `);
901
+
902
+ setTimeout(() => { el.backButtonHandler(); });
903
+ const response = await oneEvent(el, 'jumpBackButtonPressed');
904
+ expect(response).to.exist;
905
+ });
906
+
907
+ it('emits jumpForwardButtonPressed event', async () => {
908
+ const el = await fixture(html`
909
+ <radio-player></radio-player>
910
+ `);
911
+
912
+ setTimeout(() => { el.forwardButtonHandler(); });
913
+ const response = await oneEvent(el, 'jumpForwardButtonPressed');
914
+ expect(response).to.exist;
915
+ });
916
+
917
+ it('emits playPauseButtonPressed event', async () => {
918
+ const el = await fixture(html`
919
+ <radio-player></radio-player>
920
+ `);
921
+
922
+ setTimeout(() => { el.playPauseButtonHandler(); });
923
+ const response = await oneEvent(el, 'playPauseButtonPressed');
924
+ expect(response.detail.isPlaying).to.equal(true);
925
+ });
926
+
927
+ it('emits nextSectionButtonPressed event', async () => {
928
+ const el = await fixture(html`
929
+ <radio-player></radio-player>
930
+ `);
931
+
932
+ setTimeout(() => { el.nextSectionButtonHandler(); });
933
+ const response = await oneEvent(el, 'nextSectionButtonPressed');
934
+ expect(response).to.exist;
935
+ });
936
+
937
+ it('emits prevSectionButtonPressed event', async () => {
938
+ const el = await fixture(html`
939
+ <radio-player></radio-player>
940
+ `);
941
+
942
+ // needed to be able to press the prev section button
943
+ el.percentComplete = 35;
944
+
945
+ setTimeout(() => { el.prevSectionButtonHandler(); });
946
+ const response = await oneEvent(el, 'prevSectionButtonPressed');
947
+ expect(response).to.exist;
948
+ });
949
+
950
+ it('emits currentTimeChanged event', async () => {
951
+ const el = await fixture(html`
952
+ <radio-player></radio-player>
953
+ `);
954
+
955
+ setTimeout(() => { el.currentTime = 5; });
956
+ const response = await oneEvent(el, 'currentTimeChanged');
957
+ expect(response.detail.currentTime).to.equal(5);
958
+ });
959
+
960
+ it('emits searchCleared event', async () => {
961
+ const el = await fixture(html`
962
+ <radio-player></radio-player>
963
+ `);
964
+
965
+ setTimeout(() => { el.searchCleared(); });
966
+ const response = await oneEvent(el, 'searchCleared');
967
+ expect(response).to.exist;
968
+ });
969
+
970
+ it('emits searchTermChanged event', async () => {
971
+ const el = await fixture(html`
972
+ <radio-player></radio-player>
973
+ `);
974
+
975
+ const event = new CustomEvent('foo', {
976
+ detail: { value: 'boop' },
977
+ });
978
+ setTimeout(() => { el.updateSearchTerm(event); });
979
+ const response = await oneEvent(el, 'searchTermChanged');
980
+ expect(response.detail.searchTerm).to.equal('boop');
981
+ });
982
+
983
+ it('emits playbackPaused event', async () => {
984
+ const el = await fixture(html`
985
+ <radio-player></radio-player>
986
+ `);
987
+
988
+ setTimeout(() => { el.playbackPaused(); });
989
+ const response = await oneEvent(el, 'playbackPaused');
990
+ expect(response).to.exist;
991
+ });
992
+
993
+ it('emits playbackStarted event', async () => {
994
+ const el = await fixture(html`
995
+ <radio-player></radio-player>
996
+ `);
997
+
998
+ setTimeout(() => { el.playbackStarted(); });
999
+ const response = await oneEvent(el, 'playbackStarted');
1000
+ expect(response).to.exist;
1001
+ });
1002
+
1003
+ it('emits canplay event', async () => {
1004
+ const el = await fixture(html`
1005
+ <radio-player></radio-player>
1006
+ `);
1007
+
1008
+ setTimeout(() => { el.canplay(); });
1009
+ const response = await oneEvent(el, 'canplay');
1010
+ expect(response).to.exist;
1011
+ });
1012
+
1013
+ it('emits timeChangedFromScrub event', async () => {
1014
+ const el = await fixture(html`
1015
+ <radio-player></radio-player>
1016
+ `);
1017
+
1018
+ el.duration = 100;
1019
+
1020
+ const event = new CustomEvent('foo', {
1021
+ detail: { value: 35 },
1022
+ });
1023
+ setTimeout(() => { el.valueChangedFromScrub(event); });
1024
+ const response = await oneEvent(el, 'timeChangedFromScrub');
1025
+ expect(response.detail.newTime).to.equal(35);
1026
+ });
1027
+
1028
+ it('emits transcriptEntrySelected event', async () => {
1029
+ const el = await fixture(html`
1030
+ <radio-player></radio-player>
1031
+ `);
1032
+
1033
+ const event = new CustomEvent('foo', {
1034
+ detail: { entry: { start: 35 } },
1035
+ });
1036
+ setTimeout(() => { el.transcriptEntrySelected(event); });
1037
+ const response = await oneEvent(el, 'transcriptEntrySelected');
1038
+ expect(response.detail.newTime).to.equal(35);
1039
+ });
1040
+
1041
+ it('emits highlightedSearchResultChanged event', async () => {
1042
+ const el = await fixture(html`
1043
+ <radio-player></radio-player>
1044
+ `);
1045
+
1046
+ const event = new CustomEvent('foo', {
1047
+ detail: { searchResultIndex: 3 },
1048
+ });
1049
+ setTimeout(() => { el.searchResultIndexChanged(event); });
1050
+ const response = await oneEvent(el, 'highlightedSearchResultChanged');
1051
+ expect(response.detail.searchResultIndex).to.equal(3);
1052
+ });
1053
+
1054
+ it('emits searchExecuted event', async () => {
1055
+ const el = await fixture(html`
1056
+ <radio-player></radio-player>
1057
+ `);
1058
+
1059
+ class MockSearchHandler {
1060
+ search() {
1061
+ return new Promise(resolve => resolve());
1062
+ }
1063
+ }
1064
+ const searchHandler = new MockSearchHandler();
1065
+ el.searchHandler = searchHandler;
1066
+
1067
+ setTimeout(() => { el.executeSearch('foo'); });
1068
+ const response = await oneEvent(el, 'searchExecuted');
1069
+ expect(response).to.exist;
1070
+ });
1071
+ });
1072
+ });