@internetarchive/bookreader 5.0.0-90 → 5.0.0-92

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 (54) hide show
  1. package/BookReader/BookReader.js +1 -1
  2. package/BookReader/BookReader.js.map +1 -1
  3. package/BookReader/ia-bookreader-bundle.js +2 -2
  4. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  5. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  6. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  7. package/BookReader/plugins/plugin.autoplay.js +1 -1
  8. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  9. package/BookReader/plugins/plugin.chapters.js +2 -2
  10. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  11. package/BookReader/plugins/plugin.iiif.js +1 -1
  12. package/BookReader/plugins/plugin.iiif.js.map +1 -1
  13. package/BookReader/plugins/plugin.resume.js +1 -1
  14. package/BookReader/plugins/plugin.resume.js.map +1 -1
  15. package/BookReader/plugins/plugin.search.js +1 -1
  16. package/BookReader/plugins/plugin.search.js.map +1 -1
  17. package/BookReader/plugins/plugin.text_selection.js +1 -1
  18. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  19. package/BookReader/plugins/plugin.tts.js +1 -1
  20. package/BookReader/plugins/plugin.tts.js.map +1 -1
  21. package/BookReaderDemo/IADemoBr.js +29 -1
  22. package/BookReaderDemo/ia-multiple-volumes-manifest.js +0 -1
  23. package/CHANGELOG.md +28 -0
  24. package/README.md +1 -1
  25. package/package.json +1 -1
  26. package/src/BookNavigator/book-navigator.js +5 -2
  27. package/src/BookNavigator/search/search-provider.js +13 -7
  28. package/src/BookNavigator/sharing.js +1 -1
  29. package/src/BookReader/BookModel.js +5 -4
  30. package/src/BookReader/Toolbar/Toolbar.js +5 -0
  31. package/src/BookReader/options.js +10 -6
  32. package/src/BookReader.js +49 -23
  33. package/src/BookReaderPlugin.js +8 -0
  34. package/src/plugins/plugin.chapters.js +220 -157
  35. package/src/plugins/plugin.text_selection.js +19 -1
  36. package/src/plugins/search/plugin.search.js +330 -376
  37. package/src/plugins/search/view.js +13 -9
  38. package/src/plugins/tts/WebTTSEngine.js +67 -41
  39. package/src/plugins/tts/plugin.tts.js +1 -3
  40. package/src/plugins/tts/utils.js +13 -0
  41. package/src/util/browserSniffing.js +11 -1
  42. package/tests/e2e/helpers/mockSearch.js +1 -1
  43. package/tests/jest/BookNavigator/book-navigator.test.js +8 -3
  44. package/tests/jest/BookNavigator/search/search-provider.test.js +16 -4
  45. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
  46. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +70 -0
  47. package/tests/jest/BookReader.test.js +26 -1
  48. package/tests/jest/plugins/plugin.chapters.test.js +56 -58
  49. package/tests/jest/plugins/search/plugin.search.test.js +17 -42
  50. package/tests/jest/plugins/search/plugin.search.view.test.js +10 -18
  51. package/tests/jest/plugins/tts/WebTTSEngine.test.js +18 -12
  52. package/tests/jest/plugins/url/plugin.url.test.js +1 -1
  53. package/tests/jest/util/browserSniffing.test.js +9 -3
  54. package/tests/jest/utils.js +4 -1
@@ -1,7 +1,7 @@
1
1
  import sinon from "sinon";
2
2
 
3
- import BookReader from "@/src/BookReader.js";
4
- import "@/src/plugins/plugin.chapters.js";
3
+ import "@/src/BookReader.js";
4
+ import {ChaptersPlugin} from "@/src/plugins/plugin.chapters.js";
5
5
  import { BookModel } from "@/src/BookReader/BookModel";
6
6
  import { deepCopy } from "../utils";
7
7
  /** @typedef {import('@/src/plugins/plugin.chapters').TocEntry} TocEntry */
@@ -68,50 +68,44 @@ afterEach(() => {
68
68
  sinon.restore();
69
69
  });
70
70
 
71
- describe("BRChaptersPlugin", () => {
71
+ describe("ChaptersPlugin", () => {
72
72
  beforeEach(() => {
73
73
  sinon.stub(BookModel.prototype, "getPageIndex").callsFake((str) =>
74
74
  parseFloat(str),
75
75
  );
76
76
  });
77
77
 
78
- describe("_chaptersInit", () => {
79
- test("does not render when no open library record", async () => {
80
- const fakeBR = {
81
- options: {},
82
- getOpenLibraryRecord: async () => null,
83
- _chaptersRender: sinon.stub(),
84
- };
85
- await BookReader.prototype._chapterInit.call(fakeBR);
86
- expect(fakeBR._chaptersRender.callCount).toBe(0);
78
+ describe("init", () => {
79
+ test("does not render when open library has no record", async () => {
80
+ const p = new ChaptersPlugin({ options: { vars: {} } });
81
+ sinon.stub(p, "getOpenLibraryRecord").resolves(null);
82
+ sinon.spy(p, "_render");
83
+ await p.init();
84
+ expect(p._render.callCount).toBe(0);
87
85
  });
88
86
 
89
- test("does not render when open library record with no TOC", async () => {
90
- const fakeBR = {
91
- options: {},
92
- getOpenLibraryRecord: async () => ({ key: "/books/OL1M" }),
93
- _chaptersRender: sinon.stub(),
94
- };
95
- await BookReader.prototype._chapterInit.call(fakeBR);
96
- expect(fakeBR._chaptersRender.callCount).toBe(0);
87
+ test("does not render when open library record has no TOC", async () => {
88
+ const p = new ChaptersPlugin({ options: { vars: {} } });
89
+ sinon.stub(p, "getOpenLibraryRecord").resolves({ key: "/books/OL1M" });
90
+ sinon.spy(p, "_render");
91
+ await p.init();
92
+ expect(p._render.callCount).toBe(0);
97
93
  });
98
94
 
99
95
  test("renders if valid TOC on open library", async () => {
100
96
  const fakeBR = {
101
- options: {},
97
+ options: { vars: {} },
102
98
  bind: sinon.stub(),
103
- book: {
104
- getPageIndex: (str) => parseFloat(str),
105
- },
106
- getOpenLibraryRecord: async () => ({
107
- "title": "The Adventures of Sherlock Holmes",
108
- "table_of_contents": deepCopy(SAMPLE_TOC_OPTION),
109
- "ocaid": "adventureofsherl0000unse",
110
- }),
111
- _chaptersRender: sinon.stub(),
112
99
  };
113
- await BookReader.prototype._chapterInit.call(fakeBR);
114
- expect(fakeBR._chaptersRender.callCount).toBe(1);
100
+ const p = new ChaptersPlugin(fakeBR);
101
+ sinon.stub(p, "getOpenLibraryRecord").resolves({
102
+ "title": "The Adventures of Sherlock Holmes",
103
+ "table_of_contents": deepCopy(SAMPLE_TOC_OPTION),
104
+ "ocaid": "adventureofsherl0000unse",
105
+ });
106
+ sinon.stub(p, "_render");
107
+ await p.init();
108
+ expect(p._render.callCount).toBe(1);
115
109
  });
116
110
 
117
111
  test("does not fetch open library record if table of contents in options", async () => {
@@ -120,12 +114,13 @@ describe("BRChaptersPlugin", () => {
120
114
  table_of_contents: deepCopy(SAMPLE_TOC_UNDEF),
121
115
  },
122
116
  bind: sinon.stub(),
123
- getOpenLibraryRecord: sinon.stub(),
124
- _chaptersRender: sinon.stub(),
125
117
  };
126
- await BookReader.prototype._chapterInit.call(fakeBR);
127
- expect(fakeBR.getOpenLibraryRecord.callCount).toBe(0);
128
- expect(fakeBR._chaptersRender.callCount).toBe(1);
118
+ const p = new ChaptersPlugin(fakeBR);
119
+ sinon.stub(p, "getOpenLibraryRecord");
120
+ sinon.stub(p, "_render");
121
+ await p.init();
122
+ expect(p.getOpenLibraryRecord.callCount).toBe(0);
123
+ expect(p._render.callCount).toBe(1);
129
124
  });
130
125
 
131
126
  test("converts leafs and pagenums to page index", async () => {
@@ -141,55 +136,58 @@ describe("BRChaptersPlugin", () => {
141
136
  leafNumToIndex: (leaf) => leaf + 1,
142
137
  getPageIndex: (str) => parseFloat(str),
143
138
  },
144
- _chaptersRender: sinon.stub(),
145
139
  };
146
- await BookReader.prototype._chapterInit.call(fakeBR);
147
- expect(fakeBR._chaptersRender.callCount).toBe(1);
148
- expect(fakeBR._tocEntries[0].pageIndex).toBe(1);
149
- expect(fakeBR._tocEntries[1].pageIndex).toBe(17);
140
+ const p = new ChaptersPlugin(fakeBR);
141
+ sinon.stub(p, "_render");
142
+ await p.init();
143
+ expect(p._render.callCount).toBe(1);
144
+ expect(p._tocEntries[0].pageIndex).toBe(1);
145
+ expect(p._tocEntries[1].pageIndex).toBe(17);
150
146
  });
151
147
  });
152
148
 
153
- describe('_chaptersRender', () => {
149
+ describe('_render', () => {
154
150
  test('renders markers and panel', () => {
155
151
  const fakeBR = {
156
- _tocEntries: SAMPLE_TOC,
157
- _chaptersRenderMarker: sinon.stub(),
158
152
  shell: {
159
153
  menuProviders: {},
160
154
  addMenuShortcut: sinon.stub(),
161
155
  updateMenuContents: sinon.stub(),
162
156
  },
163
157
  };
164
- BookReader.prototype._chaptersRender.call(fakeBR);
158
+ const p = new ChaptersPlugin(fakeBR);
159
+ sinon.stub(p, '_renderMarker');
160
+ p._tocEntries = deepCopy(SAMPLE_TOC);
161
+ p._render();
165
162
  expect(fakeBR.shell.menuProviders['chapters']).toBeTruthy();
166
163
  expect(fakeBR.shell.addMenuShortcut.callCount).toBe(1);
167
164
  expect(fakeBR.shell.updateMenuContents.callCount).toBe(1);
168
- expect(fakeBR._chaptersRenderMarker.callCount).toBeGreaterThan(1);
165
+ expect(p._renderMarker.callCount).toBeGreaterThan(1);
169
166
  });
170
167
  });
171
168
 
172
- describe('_chaptersUpdateCurrent', () => {
169
+ describe('_updateCurrent', () => {
173
170
  test('highlights the current chapter', () => {
174
171
  const fakeBR = {
175
172
  mode: 2,
176
173
  firstIndex: 16,
177
174
  displayedIndices: [16, 17],
178
- _tocEntries: SAMPLE_TOC,
179
- _chaptersPanel: {
180
- currentChapter: null,
181
- },
182
175
  };
183
- BookReader.prototype._chaptersUpdateCurrent.call(fakeBR);
184
- expect(fakeBR._chaptersPanel.currentChapter).toEqual(SAMPLE_TOC[1]);
176
+ const p = new ChaptersPlugin(fakeBR);
177
+ p._tocEntries = deepCopy(SAMPLE_TOC);
178
+ p._chaptersPanel = {
179
+ currentChapter: null,
180
+ };
181
+ p._updateCurrent();
182
+ expect(p._chaptersPanel.currentChapter).toEqual(SAMPLE_TOC[1]);
185
183
 
186
184
  fakeBR.mode = 1;
187
- BookReader.prototype._chaptersUpdateCurrent.call(fakeBR);
188
- expect(fakeBR._chaptersPanel.currentChapter).toEqual(SAMPLE_TOC[0]);
185
+ p._updateCurrent();
186
+ expect(p._chaptersPanel.currentChapter).toEqual(SAMPLE_TOC[0]);
189
187
 
190
188
  fakeBR.firstIndex = 0;
191
- BookReader.prototype._chaptersUpdateCurrent.call(fakeBR);
192
- expect(fakeBR._chaptersPanel.currentChapter).toBeUndefined();
189
+ p._updateCurrent();
190
+ expect(p._chaptersPanel.currentChapter).toBeUndefined();
193
191
  });
194
192
  });
195
193
  });
@@ -5,6 +5,7 @@ import { DUMMY_RESULTS } from './utils.js';
5
5
 
6
6
  jest.mock('@/src/plugins/search/view.js');
7
7
 
8
+ /** @type {BookReader} */
8
9
  let br;
9
10
  const namespace = 'BookReader:';
10
11
  const triggeredEvents = () => {
@@ -23,10 +24,14 @@ beforeEach(() => {
23
24
 
24
25
  $.fn.trigger = jest.fn();
25
26
  document.body.innerHTML = '<div id="BookReader">';
26
- br = new BookReader();
27
+ br = new BookReader({
28
+ server: 'foo.bar.com',
29
+ bookPath: '/13/items/foo/foobar',
30
+ subPrefix: '/foobar',
31
+ });
27
32
  br.initToolbar = jest.fn();
28
33
  br.showProgressPopup = jest.fn();
29
- br.searchXHR = jest.fn();
34
+ br._plugins.search.searchXHR = jest.fn();
30
35
  });
31
36
 
32
37
  afterEach(() => {
@@ -34,54 +39,24 @@ afterEach(() => {
34
39
  });
35
40
 
36
41
  describe('Plugin: Search', () => {
37
- test('has option flag', () => {
38
- expect(BookReader.defaultOptions.enableSearch).toEqual(true);
39
- });
40
-
41
- test('has added BR property: server', () => {
42
- expect(br).toHaveProperty('server');
43
- expect(br.server).toBeTruthy();
44
- });
45
-
46
- test('has added BR property: bookId', () => {
47
- expect(br).toHaveProperty('bookId');
48
- expect(br.bookId).toBeFalsy();
49
- });
50
-
51
- test('has added BR property: subPrefix', () => {
52
- expect(br).toHaveProperty('subPrefix');
53
- expect(br.subPrefix).toBeFalsy();
54
- });
55
-
56
- test('has added BR property: bookPath', () => {
57
- expect(br).toHaveProperty('bookPath');
58
- expect(br.bookPath).toBeFalsy();
59
- });
60
-
61
- test('has added BR property: searchInsideUrl', () => {
62
- expect(br).toHaveProperty('searchInsideUrl');
63
- expect(br.searchInsideUrl).toBeTruthy();
64
- });
65
-
66
- test('has added BR property: initialSearchTerm', () => {
67
- expect(br.options).toHaveProperty('initialSearchTerm');
68
- expect(br.options.initialSearchTerm).toBeFalsy();
69
- });
70
-
71
42
  test('On init, it loads the toolbar', () => {
72
43
  br.init();
73
44
  expect(br.initToolbar).toHaveBeenCalled();
74
45
  });
75
46
 
47
+ test('Constructs SearchView', () => {
48
+ expect(br._plugins.search.searchView).toBeDefined();
49
+ });
50
+
76
51
  test('On init, it will run a search if given `options.initialSearchTerm`', () => {
77
- br.search = jest.fn();
78
- br.options.initialSearchTerm = 'foo';
52
+ br._plugins.search.search = jest.fn();
53
+ br.options.plugins.search.initialSearchTerm = 'foo';
79
54
  br.init();
80
55
 
81
- expect(br.search).toHaveBeenCalled();
82
- expect(br.search.mock.calls[0][1])
56
+ expect(br._plugins.search.search).toHaveBeenCalled();
57
+ expect(br._plugins.search.search.mock.calls[0][1])
83
58
  .toHaveProperty('goToFirstResult', true);
84
- expect(br.search.mock.calls[0][1])
59
+ expect(br._plugins.search.search.mock.calls[0][1])
85
60
  .toHaveProperty('suppressFragmentChange', false);
86
61
  });
87
62
 
@@ -100,7 +75,7 @@ describe('Plugin: Search', () => {
100
75
  test('SearchStarted event fires and should go to first result', () => {
101
76
  br.init();
102
77
  br.search('foo', { goToFirstResult: true});
103
- expect(br.options.goToFirstResult).toBeTruthy();
78
+ expect(br._plugins.search.options.goToFirstResult).toBeTruthy();
104
79
  });
105
80
 
106
81
  test('SearchCallback event fires when AJAX search returns results', async () => {
@@ -1,9 +1,10 @@
1
-
1
+ // @ts-check
2
2
  import BookReader from '@/src/BookReader.js';
3
3
  import '@/src/plugins/search/plugin.search.js';
4
4
  import { marshallSearchResults } from '@/src/plugins/search/utils.js';
5
5
  import '@/src/plugins/search/view.js';
6
6
 
7
+ /** @type {BookReader} */
7
8
  let br;
8
9
  const namespace = 'BookReader:';
9
10
  const results = {
@@ -71,31 +72,22 @@ afterEach(() => {
71
72
  });
72
73
 
73
74
  describe('View: Plugin: Search', () => {
74
- test('When search runs, the view gets created.', () => {
75
- br.search = jest.fn();
76
- br.options.initialSearchTerm = 'foo';
77
- br.init();
78
-
79
- expect(br.searchView).toBeDefined();
80
- expect(br.searchView.handleSearchCallback).toBeDefined();
81
- });
82
-
83
75
  describe("Search results navigation bar", () => {
84
76
  test('Search Results callback creates the results nav', () => {
85
77
  br.init();
86
78
  const event = new CustomEvent(`${namespace}SearchCallback`);
87
79
  const options = { goToFirstResult: false };
88
80
 
89
- expect(br.searchView.dom.searchNavigation).toBeUndefined();
81
+ expect(br._plugins.search.searchView.dom.searchNavigation).toBeUndefined();
90
82
 
91
- br.searchView.handleSearchCallback(event, { results, options});
92
- expect(br.searchView.dom.searchNavigation).toBeDefined();
83
+ br._plugins.search.searchView.handleSearchCallback(event, { results, options});
84
+ expect(br._plugins.search.searchView.dom.searchNavigation).toBeDefined();
93
85
  });
94
86
  test('has controls', () => {
95
87
  br.init();
96
88
  const event = new CustomEvent(`${namespace}SearchCallback`);
97
89
  const options = { goToFirstResult: false };
98
- br.searchView.handleSearchCallback(event, { results, options});
90
+ br._plugins.search.searchView.handleSearchCallback(event, { results, options});
99
91
 
100
92
  const searchResultsNav = document.querySelector('.BRsearch-navigation');
101
93
  expect(searchResultsNav).toBeDefined();
@@ -110,9 +102,9 @@ describe('View: Plugin: Search', () => {
110
102
  br.init();
111
103
  const event = new CustomEvent(`${namespace}SearchCallback`);
112
104
  const options = { goToFirstResult: false };
113
- br.searchView.handleSearchCallback(event, { results: resultWithScript, options });
105
+ br._plugins.search.searchView.handleSearchCallback(event, { results: resultWithScript, options });
114
106
 
115
- expect(br.searchView.dom.searchNavigation.parent().html()).not.toContain('<script>alert(1);</script>');
107
+ expect(br._plugins.search.searchView.dom.searchNavigation.parent().html()).not.toContain('<script>alert(1);</script>');
116
108
  });
117
109
 
118
110
  describe('Click events handlers', () => {
@@ -122,7 +114,7 @@ describe('View: Plugin: Search', () => {
122
114
  br.trigger = (eventName) => eventNameTriggered = eventName;
123
115
 
124
116
  expect(eventNameTriggered).toBeFalsy();
125
- br.searchView.toggleSidebar();
117
+ br._plugins.search.searchView.toggleSidebar();
126
118
  expect(eventNameTriggered).toEqual('ToggleSearchMenu');
127
119
  });
128
120
  it('triggers custom event when closing navbar', () => {
@@ -131,7 +123,7 @@ describe('View: Plugin: Search', () => {
131
123
  br.trigger = (eventName) => eventNameTriggered = eventName;
132
124
 
133
125
  expect(eventNameTriggered).toBeFalsy();
134
- br.searchView.clearSearchFieldAndResults();
126
+ br._plugins.search.searchView.clearSearchFieldAndResults();
135
127
  expect(eventNameTriggered).toEqual('SearchResultsCleared');
136
128
  });
137
129
  });
@@ -93,51 +93,57 @@ describe('WebTTSSound', () => {
93
93
  });
94
94
 
95
95
  describe('_chromePausingBugFix', () => {
96
+ /** @type {sinon.SinonFakeTimers} */
97
+ let clock = null;
98
+
99
+ beforeEach(() => {
100
+ clock = sinon.useFakeTimers();
101
+ });
102
+
103
+ afterEach(() => {
104
+ clock.restore();
105
+ });
106
+
96
107
  test('if speech less than 15s, nothing special', async () => {
97
- const clock = sinon.useFakeTimers();
98
108
  const sound = new WebTTSSound('hello world foo bar');
99
109
  sound.load();
100
110
  sound.play();
101
111
  sound._chromePausingBugFix();
102
112
  clock.tick(10000);
103
113
  sound.utterance.dispatchEvent('end', {});
104
- clock.restore();
105
114
  await afterEventLoop();
106
115
  expect(speechSynthesis.pause.callCount).toBe(0);
107
116
  });
108
117
 
109
118
  test('if speech greater than 15s, pause called', async () => {
110
- const clock = sinon.useFakeTimers();
111
119
  const sound = new WebTTSSound('foo bah');
112
120
  sound.load();
113
121
  sound.play();
114
122
  sound._chromePausingBugFix();
115
123
  clock.tick(20000);
116
- clock.restore();
117
124
 
118
125
  await afterEventLoop();
119
126
  expect(speechSynthesis.pause.callCount).toBe(1);
120
127
  });
121
128
 
122
- test('on pause reloads if timed out', async () => {
123
- const clock = sinon.useFakeTimers();
129
+ test('on pause, stops sound if timed out', async () => {
124
130
  const sound = new WebTTSSound('foo bah');
125
131
  sound.load();
126
132
  sound.play();
133
+ sound.stop = sinon.stub();
127
134
  sound._chromePausingBugFix();
128
- sound.pause();
129
- clock.tick(2000);
130
- clock.restore();
135
+ clock.tick(5000);
136
+ sound.utterance.dispatchEvent('pause', {});
137
+ await afterEventLoop();
138
+ clock.tick(15000);
131
139
 
132
140
  await afterEventLoop();
133
- expect(speechSynthesis.cancel.callCount).toBe(1);
141
+ expect(sound.stop.callCount).toBe(1);
134
142
  });
135
143
  });
136
144
 
137
145
  test('fire pause if browser does not do it', async () => {
138
146
  const clock = sinon.useFakeTimers();
139
- const languageGetter = jest.spyOn(window.navigator, 'userAgent', 'get');
140
- languageGetter.mockReturnValue('firefox android');
141
147
  const sound = new WebTTSSound('foo bah');
142
148
  sound.load();
143
149
  sound.play();
@@ -145,7 +145,7 @@ describe('Plugin: URL controller', () => {
145
145
  search: 'foo',
146
146
  }));
147
147
  BookReader.prototype.search = jest.fn();
148
- br.options.initialSearchTerm = 'foo';
148
+ br.options.plugins.search = { initialSearchTerm: 'foo' };
149
149
  br.options.urlMode = 'history';
150
150
  br.init();
151
151
  br.urlUpdateFragment();
@@ -1,5 +1,5 @@
1
1
  import {
2
- isChrome, isFirefox, isSafari,
2
+ isChrome, isEdge, isFirefox, isSafari,
3
3
  } from '@/src/util/browserSniffing.js';
4
4
 
5
5
  const TESTS = [
@@ -19,7 +19,13 @@ const TESTS = [
19
19
  name: 'Edge on Windows 10',
20
20
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362',
21
21
  vendor: '',
22
- machingFn: null,
22
+ machingFn: isEdge,
23
+ },
24
+ {
25
+ name: 'Edge on Windows 11',
26
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0',
27
+ vendor: 'Google Inc.',
28
+ machingFn: isEdge,
23
29
  },
24
30
  {
25
31
  name: 'IE11 on Windows 10',
@@ -47,7 +53,7 @@ const TESTS = [
47
53
  },
48
54
  ];
49
55
 
50
- for (const fn of [isChrome, isFirefox, isSafari]) {
56
+ for (const fn of [isChrome, isEdge, isFirefox, isSafari]) {
51
57
  describe(fn.name, () => {
52
58
  for (const { name, userAgent, vendor, machingFn } of TESTS) {
53
59
  test(name, () => expect(fn(userAgent, vendor)).toBe(machingFn == fn));
@@ -1,3 +1,6 @@
1
+ // Keep a copy of this, since it can be overridden by sinon timers.
2
+ const _realTimeout = setTimeout;
3
+
1
4
  /**
2
5
  * Resolves after all enqueued callbacks in the event loop have resolved.
3
6
  * @return {Promise}
@@ -5,7 +8,7 @@
5
8
  export function afterEventLoop() {
6
9
  // Waiting 0 seconds essentially lets us run at the end of the event
7
10
  // loop (i.e. after any promises which aren't _actually_ async have finished)
8
- return new Promise(res => setTimeout(res, 0));
11
+ return new Promise(res => _realTimeout(res, 0));
9
12
  }
10
13
 
11
14
  /**