@internetarchive/bookreader 5.0.0-70-a3 → 5.0.0-70

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. package/BookReader/BookReader.js +1 -1
  2. package/BookReader/BookReader.js.map +1 -1
  3. package/BookReader/ia-bookreader-bundle.js +293 -586
  4. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  5. package/BookReader/plugins/plugin.chapters.js +2 -2
  6. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  7. package/BookReader/plugins/plugin.tts.js +1 -1
  8. package/BookReader/plugins/plugin.tts.js.map +1 -1
  9. package/BookReader/plugins/plugin.url.js +1 -1
  10. package/BookReader/plugins/plugin.url.js.map +1 -1
  11. package/BookReaderDemo/demo-internetarchive.html +1 -1
  12. package/CHANGELOG.md +4 -0
  13. package/package.json +4 -2
  14. package/src/BookNavigator/assets/icon_sort_asc.js +5 -0
  15. package/src/BookNavigator/assets/icon_sort_desc.js +5 -0
  16. package/src/BookNavigator/assets/icon_sort_neutral.js +5 -0
  17. package/src/BookNavigator/assets/icon_volumes.js +11 -0
  18. package/src/BookNavigator/book-navigator.js +2 -2
  19. package/src/BookNavigator/sharing.js +5 -5
  20. package/src/BookNavigator/volumes/volumes-provider.js +111 -0
  21. package/src/BookNavigator/volumes/volumes.js +188 -0
  22. package/src/BookReader.js +4 -5
  23. package/src/ia-bookreader/ia-bookreader.js +6 -6
  24. package/src/plugins/tts/AbstractTTSEngine.js +22 -3
  25. package/src/plugins/url/UrlPlugin.js +5 -7
  26. package/tests/e2e/models/Navigation.js +1 -1
  27. package/tests/jest/BookNavigator/book-navigator.test.js +2 -2
  28. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
  29. package/tests/jest/BookNavigator/volumes/volumes-provider.test.js +121 -17
  30. package/tests/jest/BookNavigator/volumes/volumes.test.js +97 -0
  31. package/tests/jest/plugins/tts/AbstractTTSEngine.test.js +20 -0
  32. package/tests/jest/plugins/url/UrlPlugin.test.js +8 -0
  33. package/src/BookNavigator/viewable-files.js +0 -95
@@ -1,10 +1,9 @@
1
- import { fixtureCleanup, fixtureSync } from '@open-wc/testing-helpers';
1
+ import { fixture, fixtureCleanup, fixtureSync } from '@open-wc/testing-helpers';
2
2
  import sinon from 'sinon';
3
- import ViewableFilesProvider from '@/src/BookNavigator/viewable-files';
3
+ import volumesProvider from '@/src/BookNavigator/volumes/volumes-provider';
4
4
 
5
5
  const brOptions = {
6
6
  "options": {
7
- "subPrefix": 'special-subprefix',
8
7
  "enableMultipleBooks": true,
9
8
  "multipleBooksList": {
10
9
  "by_subprefix": {
@@ -40,11 +39,10 @@ afterEach(() => {
40
39
  });
41
40
 
42
41
  describe('Volumes Provider', () => {
43
- test('initiating & sorting', () => {
42
+ test('constructor', () => {
44
43
  const onProviderChange = sinon.fake();
45
-
46
44
  const baseHost = "https://archive.org";
47
- const provider = new ViewableFilesProvider({
45
+ const provider = new volumesProvider({
48
46
  baseHost,
49
47
  bookreader: brOptions,
50
48
  onProviderChange
@@ -57,24 +55,130 @@ describe('Volumes Provider', () => {
57
55
  expect(provider.id).toEqual('volumes');
58
56
  expect(provider.icon).toBeDefined();
59
57
  expect(fixtureSync(provider.icon).tagName).toEqual('svg');
60
- expect(provider.sortOrderBy).toEqual('default');
61
-
62
58
  expect(provider.label).toEqual(`Viewable files (${volumeCount})`);
63
59
  expect(provider.viewableFiles).toBeDefined();
64
60
  expect(provider.viewableFiles.length).toEqual(3);
65
- expect(provider.volumeCount).toEqual(3);
66
61
 
67
- expect(provider.component.baseHost).toEqual(baseHost);
68
- expect(provider.component.fileList).toEqual(provider.viewableFiles);
69
- expect(provider.component.subPrefix).toEqual(brOptions.options.subPrefix);
62
+ expect(provider.component.hostUrl).toBeDefined();
63
+ expect(provider.component.hostUrl).toEqual(baseHost);
64
+ expect(provider.component).toBeDefined();
65
+ });
66
+
67
+ test('sorting cycles - render sort actionButton', async () => {
68
+ const onProviderChange = sinon.fake();
69
+ const baseHost = "https://archive.org";
70
+ const provider = new volumesProvider({
71
+ baseHost,
72
+ bookreader: brOptions,
73
+ onProviderChange
74
+ });
75
+
76
+ expect(provider.sortOrderBy).toEqual("default");
77
+
78
+ provider.sortVolumes("title_asc");
79
+ expect(provider.sortOrderBy).toEqual("title_asc");
80
+ expect(fixtureSync(provider.sortButton).outerHTML).toContain("sort-by asc-icon");
81
+
82
+ provider.sortVolumes("title_desc");
83
+ expect(provider.sortOrderBy).toEqual("title_desc");
84
+ expect(fixtureSync(provider.sortButton).outerHTML).toContain("sort-by desc-icon");
85
+
86
+ provider.sortVolumes("default");
87
+ expect(provider.sortOrderBy).toEqual("default");
88
+ expect(fixtureSync(provider.sortButton).outerHTML).toContain("sort-by neutral-icon");
89
+ });
90
+
91
+ test('sort volumes in initial order', async () => {
92
+ const onProviderChange = sinon.fake();
93
+ const baseHost = "https://archive.org";
94
+ const provider = new volumesProvider({
95
+ baseHost,
96
+ bookreader: brOptions,
97
+ onProviderChange
98
+ });
99
+
100
+ const parsedFiles = brOptions.options.multipleBooksList.by_subprefix;
101
+ const files = Object.keys(parsedFiles).map(item => parsedFiles[item]).sort((a, b) => a.orig_sort - b.orig_sort);
102
+ const origSortTitles = files.map(item => item.title);
70
103
 
104
+ provider.sortVolumes("default");
105
+
106
+ expect(provider.sortOrderBy).toEqual("default");
71
107
  expect(provider.actionButton).toBeDefined();
72
- expect(provider.actionButton).toEqual(provider.sortFilesComponent);
73
- expect(provider.actionButton.fileListRaw).toEqual(provider.viewableFiles);
74
108
 
75
- const callbackSpy = sinon.spy(provider, 'handleFileListSorted');
76
- provider.actionButton.sortVolumes('title_asc');
109
+ const providerFileTitles = provider.viewableFiles.map(item => item.title);
110
+ // use `.eql` for "lose equality" in order to deeply compare values.
111
+ expect(providerFileTitles).toEqual([...origSortTitles]);
112
+ });
77
113
 
78
- expect(callbackSpy.callCount).toEqual(1);
114
+ test('sort volumes in ascending title order', async () => {
115
+ const onProviderChange = sinon.fake();
116
+ const baseHost = "https://archive.org";
117
+ const provider = new volumesProvider({
118
+ baseHost,
119
+ bookreader: brOptions,
120
+ onProviderChange
121
+ });
122
+
123
+ const parsedFiles = brOptions.options.multipleBooksList.by_subprefix;
124
+ const files = Object.keys(parsedFiles).map(item => parsedFiles[item]);
125
+ const ascendingTitles = files.map(item => item.title).sort((a, b) => a.localeCompare(b));
126
+
127
+ provider.sortVolumes("title_asc");
128
+
129
+ expect(provider.sortOrderBy).toEqual("title_asc");
130
+ expect(provider.actionButton).toBeDefined();
131
+
132
+ const providerFileTitles = provider.viewableFiles.map(item => item.title);
133
+ // use `.eql` for "lose equality" in order to deeply compare values.
134
+ expect(providerFileTitles).toEqual([...ascendingTitles]);
135
+ });
136
+
137
+ test('sort volumes in descending title order', async () => {
138
+ const onProviderChange = sinon.fake();
139
+ const baseHost = "https://archive.org";
140
+ const provider = new volumesProvider({
141
+ baseHost,
142
+ bookreader: brOptions,
143
+ onProviderChange
144
+ });
145
+ provider.isSortAscending = false;
146
+
147
+ const parsedFiles = brOptions.options.multipleBooksList.by_subprefix;
148
+ const files = Object.keys(parsedFiles).map(item => parsedFiles[item]);
149
+ const descendingTitles = files.map(item => item.title).sort((a, b) => b.localeCompare(a));
150
+
151
+ provider.sortVolumes("title_desc");
152
+
153
+ expect(provider.sortOrderBy).toEqual("title_desc");
154
+ expect(provider.actionButton).toBeDefined();
155
+
156
+ const providerFileTitles = provider.viewableFiles.map(item => item.title);
157
+ // use `.eql` for "lose equality" in order to deeply compare values.
158
+ expect(providerFileTitles).toEqual([...descendingTitles]);
159
+ });
160
+
161
+ describe('Sorting icons', () => {
162
+ test('has 3 icons', async () => {
163
+ const onProviderChange = sinon.fake();
164
+ const baseHost = "https://archive.org";
165
+ const provider = new volumesProvider({
166
+ baseHost,
167
+ bookreader: brOptions,
168
+ onProviderChange
169
+ });
170
+ provider.sortOrderBy = 'default';
171
+
172
+ const origSortButton = await fixture(provider.sortButton);
173
+ expect(origSortButton.classList.contains('neutral-icon')).toBeTruthy();
174
+
175
+ provider.sortOrderBy = 'title_asc';
176
+ const ascButton = await fixture(provider.sortButton);
177
+ expect(ascButton.classList.contains('asc-icon')).toBeTruthy();
178
+
179
+ provider.sortOrderBy = 'title_desc';
180
+ const descButton = await fixture(provider.sortButton);
181
+ expect(descButton.classList.contains('desc-icon')).toBeTruthy();
182
+ });
79
183
  });
80
184
  });
@@ -0,0 +1,97 @@
1
+ import {
2
+ html,
3
+ fixture,
4
+ fixtureCleanup,
5
+ } from '@open-wc/testing-helpers';
6
+ import sinon from 'sinon';
7
+ import '@/src/BookNavigator/volumes/volumes.js';
8
+
9
+
10
+ const brOptions = {
11
+ "options": {
12
+ "enableMultipleBooks": true,
13
+ "multipleBooksList": {
14
+ "by_subprefix": {
15
+ "/details/SubBookTest": {
16
+ "url_path": "/details/SubBookTest",
17
+ "file_subprefix": "book1/GPORFP",
18
+ "orig_sort": 0,
19
+ "title": "book1/GPORFP.pdf",
20
+ "file_source": "/book1/GPORFP_jp2.zip"
21
+ },
22
+ "/details/SubBookTest/subdir/book2/brewster_kahle_internet_archive": {
23
+ "url_path": "/details/SubBookTest/subdir/book2/brewster_kahle_internet_archive",
24
+ "file_subprefix": "subdir/book2/brewster_kahle_internet_archive",
25
+ "orig_sort": 1,
26
+ "title": "subdir/book2/brewster_kahle_internet_archive.pdf",
27
+ "file_source": "/subdir/book2/brewster_kahle_internet_archive_jp2.zip"
28
+ },
29
+ "/details/SubBookTest/subdir/subsubdir/book3/Rfp008011ResponseInternetArchive-without-resume": {
30
+ "url_path": "/details/SubBookTest/subdir/subsubdir/book3/Rfp008011ResponseInternetArchive-without-resume",
31
+ "file_subprefix": "subdir/subsubdir/book3/Rfp008011ResponseInternetArchive-without-resume",
32
+ "orig_sort": 2,
33
+ "title": "subdir/subsubdir/book3/Rfp008011ResponseInternetArchive-without-resume.pdf",
34
+ "file_source": "/subdir/subsubdir/book3/Rfp008011ResponseInternetArchive-without-resume_jp2.zip"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ };
40
+
41
+ const container = (brOptions, prefix) => (
42
+ html`
43
+ <viewable-files .viewableFiles=${brOptions} .hostUrl="https://archive.org" .subPrefix=${prefix}></viewable-files>
44
+ `
45
+ );
46
+
47
+ beforeEach(() => {
48
+ const body = document.querySelector('body');
49
+ const brHook = document.createElement('div');
50
+ brHook.setAttribute('id', 'BookReader');
51
+ body.appendChild(brHook);
52
+ });
53
+
54
+ afterEach(() => {
55
+ sinon.restore();
56
+ fixtureCleanup();
57
+ });
58
+
59
+ describe('<viewable-files>', () => {
60
+ test('sets default properties', async () => {
61
+ const files = brOptions.options.multipleBooksList?.by_subprefix;
62
+ const viewableFiles = Object.keys(files).map(item => files[item]);
63
+ const el = await fixture(container(viewableFiles));
64
+ await el.updateComplete;
65
+
66
+ expect(el.viewableFiles).toEqual(viewableFiles);
67
+ expect(el.viewableFiles.length).toEqual(3);
68
+ expect(el.shadowRoot.querySelectorAll("ul li").length).toEqual(3);
69
+
70
+ expect(el.shadowRoot.querySelector(".item-title").textContent).toContain(`${viewableFiles[0].title}`);
71
+ });
72
+
73
+ test('render empty volumes', async () => {
74
+ const viewableFiles = [];
75
+ const el = await fixture(container(viewableFiles));
76
+ await el.updateComplete;
77
+
78
+ expect(el.viewableFiles).toEqual(viewableFiles);
79
+ expect(el.viewableFiles.length).toEqual(0);
80
+ expect(el.shadowRoot.childElementCount).not.toEqual(0);
81
+ });
82
+
83
+ test('render active volume item set as first viewable item ', async () => {
84
+ const files = brOptions.options.multipleBooksList?.by_subprefix;
85
+ const viewableFiles = Object.keys(files).map(item => files[item]);
86
+ const prefix = viewableFiles[0].file_subprefix;
87
+
88
+ const el = await fixture(container(viewableFiles, prefix));
89
+ await el.updateComplete;
90
+
91
+ expect(el.viewableFiles).toEqual(viewableFiles);
92
+ expect(el.viewableFiles.length).toEqual(3);
93
+
94
+ expect(el.shadowRoot.querySelectorAll("ul li div")[1].className).toEqual("content active");
95
+ });
96
+
97
+ });
@@ -111,6 +111,26 @@ for (const dummyVoice of [dummyVoiceHyphens, dummyVoiceUnderscores]) {
111
111
 
112
112
  expect(getBestBookVoice(voices, 'en', ['en-CA', 'en'])).toBe(voices[0]);
113
113
  });
114
+
115
+ test('choose stored language from localStorage', () => {
116
+ const voices = [
117
+ dummyVoice({lang: "en-US", voiceURI: "English US", default: true}),
118
+ dummyVoice({lang: "en-GB", voiceURI: "English GB"}),
119
+ dummyVoice({lang: "en-CA", voiceURI: "English CA"}),
120
+ ];
121
+ class DummyEngine extends AbstractTTSEngine {
122
+ getVoices() { return voices; }
123
+ }
124
+ const ttsEngine = new DummyEngine({...DUMMY_TTS_ENGINE_OPTS, bookLanguage: 'en'});
125
+ // simulates setting default voice on tts startup
126
+ ttsEngine.updateBestVoice();
127
+ // simulates user choosing a voice that matches the bookLanguage
128
+ // voice will be stored in localStorage
129
+ ttsEngine.setVoice(voices[2].voiceURI);
130
+
131
+ // expecting the voice to be selected by getMatchingStoredVoice and returned as best voice
132
+ expect(getBestBookVoice(voices, 'en', [])).toBe(voices[2]);
133
+ });
114
134
  });
115
135
  }
116
136
 
@@ -19,6 +19,10 @@ describe('UrlPlugin tests', () => {
19
19
  expect(urlPlugin.urlStateToUrlString(urlStateWithQueries)).toBe(expectedUrlFromStateWithQueries);
20
20
  });
21
21
 
22
+ test('encodes page number', () => {
23
+ expect(urlPlugin.urlStateToUrlString({ page: '12/46' })).toBe(`page/12%2F46`);
24
+ });
25
+
22
26
  test('urlStateToUrlString with unknown states in schema', () => {
23
27
  const urlState = { page: 'n7', mode: '1up' };
24
28
  const urlStateWithQueries = { page: 'n7', mode: '1up', q: 'hello', viewer: 'theater', sortBy: 'title_asc' };
@@ -47,6 +51,10 @@ describe('UrlPlugin tests', () => {
47
51
  expect(urlPlugin.urlStringToUrlState(url1)).toEqual({page: 'n7', mode: '1up'});
48
52
  });
49
53
 
54
+ test('decodes page number', () => {
55
+ expect(urlPlugin.urlStringToUrlState('/page/12%2F46')).toStrictEqual({ page: '12/46' });
56
+ });
57
+
50
58
  test('urlStringToUrlState with deprecated_for', () => {
51
59
  const url = '/page/n7/mode/2up/search/hello';
52
60
 
@@ -1,95 +0,0 @@
1
- import { html } from 'lit';
2
-
3
- import { viewableFilesIcon } from '@internetarchive/ia-item-navigator/dist/src/menus/viewable-files';
4
- import '@internetarchive/ia-item-navigator/dist/src/menus/viewable-files';
5
-
6
- /**
7
- * * @typedef { 'title_asc' | 'title_desc' | 'default'} SortTypesT
8
- */
9
- const sortTypes = {
10
- title_asc: 'title_asc',
11
- title_desc: 'title_desc',
12
- default: 'default'
13
- };
14
- export default class ViewableFilesProvider {
15
- /**
16
- * @param {import('../BookReader').default} bookreader
17
- */
18
- constructor({ baseHost, bookreader, onProviderChange }) {
19
- /** @type {import('../BookReader').default} */
20
- this.bookreader = bookreader;
21
- this.onProviderChange = onProviderChange;
22
- this.baseHost = baseHost;
23
-
24
- const files = bookreader.options.multipleBooksList.by_subprefix;
25
- this.viewableFiles = Object.keys(files).map(item => files[item]);
26
- this.volumeCount = Object.keys(files).length;
27
-
28
- this.id = "volumes";
29
- this.label = `Viewable files (${this.volumeCount})`;
30
- this.icon = html`${viewableFilesIcon}`;
31
- this.sortOrderBy = sortTypes.default;
32
-
33
- this.component = document.createElement("iaux-in-viewable-files-panel");
34
- this.component.addSortToUrl = true;
35
- this.component.subPrefix = bookreader.options.subPrefix || "";
36
- this.component.baseHost = baseHost;
37
- this.component.fileList = [...this.viewableFiles];
38
-
39
- this.sortFilesComponent = document.createElement("iaux-in-sort-files-button");
40
- this.sortFilesComponent.fileListRaw = this.viewableFiles;
41
- this.sortFilesComponent.addEventListener('fileListSorted', (e) => this.handleFileListSorted(e));
42
- this.actionButton = this.sortFilesComponent;
43
-
44
- // get sort state from query param
45
- if (this.bookreader.urlPlugin) {
46
- this.bookreader.urlPlugin.pullFromAddressBar();
47
-
48
- const urlSortValue = this.bookreader.urlPlugin.getUrlParam('sort');
49
- if (urlSortValue === sortTypes.title_asc || urlSortValue === sortTypes.title_desc) {
50
- this.sortOrderBy = urlSortValue;
51
- }
52
- }
53
-
54
- this.sortFilesComponent.sortVolumes(this.sortOrderBy);
55
-
56
- this.onProviderChange(this.bookreader);
57
- }
58
-
59
- /** @param { SortTypesT } sortType */
60
- async handleFileListSorted(event) {
61
- const { sortType, sortedFiles } = event.detail;
62
-
63
- this.viewableFiles = sortedFiles;
64
- this.sortOrderBy = sortType;
65
-
66
- // update the component
67
- this.component.fileList = [...this.viewableFiles];
68
- await this.component.updateComplete;
69
-
70
- if (this.bookreader.urlPlugin) {
71
- this.bookreader.urlPlugin.pullFromAddressBar();
72
- if (this.sortOrderBy !== sortTypes.default) {
73
- this.bookreader.urlPlugin.setUrlParam('sort', this.sortOrderBy);
74
- } else {
75
- this.bookreader.urlPlugin.removeUrlParam('sort');
76
- }
77
- }
78
-
79
- this.onProviderChange(this.bookreader);
80
-
81
- this.multipleFilesClicked(this.sortOrderBy);
82
- }
83
-
84
- /**
85
- * @param { SortTypesT } orderBy
86
- */
87
- multipleFilesClicked(orderBy) {
88
- window.archive_analytics?.send_event(
89
- 'BookReader',
90
- `VolumesSort|${orderBy}`,
91
- window.location.path,
92
- );
93
- }
94
-
95
- }