@internetarchive/ia-item-navigator 0.0.0-a10 → 0.0.0-a12
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.
- package/demo/app-root.ts +6 -4
- package/dist/demo/app-root.d.ts +0 -1
- package/dist/demo/app-root.js +1 -3
- package/dist/demo/app-root.js.map +1 -1
- package/dist/src/interfaces/nav-controller-interface.d.ts +9 -0
- package/dist/src/interfaces/nav-controller-interface.js.map +1 -1
- package/dist/src/item-inspector/item-inspector.d.ts +0 -47
- package/dist/src/item-inspector/item-inspector.js +253 -271
- package/dist/src/item-inspector/item-inspector.js.map +1 -1
- package/dist/src/item-navigator.d.ts +14 -7
- package/dist/src/item-navigator.js +60 -30
- package/dist/src/item-navigator.js.map +1 -1
- package/dist/src/no-theater-available.d.ts +9 -0
- package/dist/src/no-theater-available.js +79 -0
- package/dist/src/no-theater-available.js.map +1 -0
- package/dist/test/book-nav-stub.d.ts +17 -0
- package/dist/test/book-nav-stub.js +42 -0
- package/dist/test/book-nav-stub.js.map +1 -0
- package/dist/test/ia-item-navigator.test.d.ts +1 -0
- package/dist/test/ia-item-navigator.test.js +146 -114
- package/dist/test/ia-item-navigator.test.js.map +1 -1
- package/dist/test/ia-stub.d.ts +22 -0
- package/dist/test/ia-stub.js +34 -3
- package/dist/test/ia-stub.js.map +1 -1
- package/dist/test/no-theater-available.test.d.ts +1 -0
- package/dist/test/no-theater-available.test.js +27 -0
- package/dist/test/no-theater-available.test.js.map +1 -0
- package/package.json +3 -2
- package/src/interfaces/nav-controller-interface.ts +13 -0
- package/src/item-navigator.ts +69 -36
- package/src/no-theater-available.ts +87 -0
- package/test/book-nav-stub.ts +35 -0
- package/test/ia-item-navigator.test.ts +191 -143
- package/test/ia-stub.ts +78 -2
- package/test/no-theater-available.test.ts +32 -0
- package/src/item-inspector/item-inspector.ts +0 -296
@@ -1,129 +1,113 @@
|
|
1
1
|
/* eslint-disable camelcase */
|
2
|
-
/* eslint-disable import/no-duplicates */
|
3
2
|
import { html, fixture, expect } from '@open-wc/testing';
|
4
3
|
import Sinon from 'sinon';
|
5
|
-
// import { MetadataResponse, Metadata, File, Review, SpeechMusicASREntry } from '@internetarchive/search-service';
|
6
|
-
import {
|
7
|
-
SharedResizeObserver,
|
8
|
-
// SharedResizeObserverInterface
|
9
|
-
} from '@internetarchive/shared-resize-observer';
|
10
4
|
|
11
|
-
|
5
|
+
import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
|
6
|
+
import { IntNavController } from '../src/interfaces/nav-controller-interface';
|
12
7
|
import { ItemNavigator } from '../src/item-navigator';
|
13
8
|
import '../src/item-navigator';
|
14
|
-
// import { IaItemInspector } from '../src/item-inspector/item-inspector';
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
// this.created = 1;
|
20
|
-
// this.d1 = 'hello';
|
21
|
-
// this.d2 = 'boop';
|
22
|
-
// this.dir = 'whee';
|
23
|
-
// this.files = [];
|
24
|
-
// this.files_count = 0;
|
25
|
-
// this.item_last_updated = 2020;
|
26
|
-
// this.item_size = 111;
|
27
|
-
// this.metadata = { identifier: 'foo' } as Metadata;
|
28
|
-
// this.server = 'foo-server';
|
29
|
-
// this.uniq = 2;
|
30
|
-
// this.workable_servers = ['abc'];
|
31
|
-
// }
|
32
|
-
|
33
|
-
// rawResponse: any;
|
34
|
-
|
35
|
-
// created: number;
|
36
|
-
|
37
|
-
// d1: string;
|
38
|
-
|
39
|
-
// d2: string;
|
40
|
-
|
41
|
-
// dir: string;
|
42
|
-
|
43
|
-
// files: File[];
|
44
|
-
|
45
|
-
// files_count: number;
|
46
|
-
|
47
|
-
// item_last_updated: number;
|
48
|
-
|
49
|
-
// item_size: number;
|
50
|
-
|
51
|
-
// metadata: Metadata;
|
52
|
-
|
53
|
-
// server: string;
|
54
|
-
|
55
|
-
// uniq: number;
|
56
|
-
|
57
|
-
// workable_servers: string[];
|
58
|
-
|
59
|
-
// speech_vs_music_asr?: SpeechMusicASREntry[] | undefined;
|
60
|
-
|
61
|
-
// reviews?: Review[] | undefined;
|
62
|
-
// };
|
10
|
+
import '../test/book-nav-stub';
|
11
|
+
import { ItemStub, menuProvider, shortcut } from '../test/ia-stub';
|
12
|
+
import { IntManageSideMenuEvent } from '../src/interfaces/event-interfaces';
|
63
13
|
|
64
14
|
afterEach(() => {
|
65
15
|
Sinon.restore();
|
66
16
|
});
|
67
17
|
|
68
18
|
describe('ItemNavigator', () => {
|
69
|
-
describe('
|
70
|
-
it('shows
|
19
|
+
describe('Theaters', () => {
|
20
|
+
it('shows <ia-no-theater-available> by default', async () => {
|
71
21
|
const el = await fixture<ItemNavigator>(
|
72
|
-
html`<ia-item-navigator></ia-item-navigator>`
|
22
|
+
html`<ia-item-navigator .item=${new ItemStub()}></ia-item-navigator>`
|
73
23
|
);
|
74
|
-
expect(el.shadowRoot?.querySelector('ia-
|
24
|
+
expect(el.shadowRoot?.querySelector('ia-no-theater-available')).to.exist;
|
25
|
+
});
|
26
|
+
|
27
|
+
it('shows <book-navigator> if `this.itemType = "bookreader"`', async () => {
|
28
|
+
const el = await fixture<ItemNavigator>(
|
29
|
+
html`<ia-item-navigator
|
30
|
+
.itemType=${`bookreader`}
|
31
|
+
.item=${new ItemStub()}
|
32
|
+
></ia-item-navigator>`
|
33
|
+
);
|
34
|
+
|
35
|
+
await el.updateComplete;
|
36
|
+
|
37
|
+
el.toggleMenu();
|
38
|
+
await el.updateComplete;
|
39
|
+
|
40
|
+
const bookNavigator = el.shadowRoot?.querySelector(
|
41
|
+
'book-navigator'
|
42
|
+
) as IntNavController;
|
43
|
+
await bookNavigator.updateComplete;
|
44
|
+
|
45
|
+
console.log('132234234324324324');
|
46
|
+
// TODO: add BookNavigator type & import via @internetarchive/bookreader
|
47
|
+
// For now, let's check that the BookNavigator element and its properties exist w/ stub
|
48
|
+
expect(bookNavigator).to.exist;
|
49
|
+
expect(bookNavigator?.modal).to.exist;
|
50
|
+
expect(bookNavigator?.baseHost).to.exist;
|
51
|
+
expect(bookNavigator?.book).to.exist;
|
52
|
+
expect(bookNavigator?.signedIn).to.be.null;
|
53
|
+
expect(bookNavigator?.sharedObserver).to.exist;
|
54
|
+
expect(bookNavigator?.sideMenuOpen).to.exist;
|
75
55
|
});
|
76
|
-
|
56
|
+
});
|
57
|
+
describe('`el.loaded`', () => {
|
58
|
+
it('toggles the spinning loader', async () => {
|
77
59
|
const el = await fixture<ItemNavigator>(
|
78
60
|
html`<ia-item-navigator></ia-item-navigator>`
|
79
61
|
);
|
80
|
-
|
81
|
-
expect(
|
82
|
-
expect(mainTheaterSection?.classList.contains('hide')).to.be.true;
|
83
|
-
expect(el.loaded).to.be.false;
|
84
|
-
expect(el.hasAttribute('loaded')).to.equal(false);
|
62
|
+
expect(el.loaded).to.be.null; // initial load
|
63
|
+
expect(el.shadowRoot?.querySelector('ia-itemnav-loader')).to.exist;
|
85
64
|
});
|
86
|
-
it('
|
65
|
+
it('hides reader section if `!loaded`', async () => {
|
87
66
|
const el = await fixture<ItemNavigator>(
|
88
67
|
html`<ia-item-navigator></ia-item-navigator>`
|
89
68
|
);
|
90
|
-
|
91
|
-
|
69
|
+
|
70
|
+
expect(
|
71
|
+
el.shadowRoot?.querySelector('#reader')?.getAttribute('class')
|
72
|
+
).to.contain('hide');
|
73
|
+
});
|
74
|
+
it('shows reader when `loaded` ', async () => {
|
75
|
+
const el = await fixture<ItemNavigator>(
|
76
|
+
html`<ia-item-navigator .item=${new ItemStub()}></ia-item-navigator>`
|
77
|
+
);
|
92
78
|
|
93
79
|
const mainTheaterSection = el.shadowRoot?.querySelector('#reader');
|
94
80
|
expect(mainTheaterSection?.classList.contains('hide')).to.be.false;
|
95
81
|
expect(el.loaded).to.be.true;
|
96
82
|
// `loaded` property is reflected as DOM attribute
|
97
83
|
expect(el.hasAttribute('loaded')).to.equal(true);
|
84
|
+
expect(el.shadowRoot?.querySelector('ia-no-theater-available')).to.exist;
|
98
85
|
});
|
86
|
+
it('listens to `@loadingStateUpdated` to update `loaded`', async () => {
|
87
|
+
const el = await fixture<ItemNavigator>(
|
88
|
+
html`<ia-item-navigator></ia-item-navigator>`
|
89
|
+
);
|
99
90
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
// expect(el?.item).to.not.be.undefined;
|
110
|
-
|
111
|
-
// const mainTheaterSection = el.shadowRoot?.querySelector('#reader');
|
112
|
-
|
113
|
-
// const contentController = mainTheaterSection?.querySelector('ia-item-inspector');
|
114
|
-
|
115
|
-
// expect(contentController).to.equal(32324);
|
116
|
-
|
117
|
-
// // const loadingEvent = new CustomEvent('loadingStateUpdated', { detail: { loaded: true }}) as IntLoadingStateUpdatedEvent;
|
118
|
-
// // contentController?.emitLoadingStatusUpdate(true);
|
91
|
+
await el.updateComplete;
|
92
|
+
const spy = Sinon.spy();
|
93
|
+
el.loadingStateUpdated = spy;
|
94
|
+
el.loaded = false;
|
95
|
+
await el.updateComplete;
|
96
|
+
// check base properties
|
97
|
+
expect(el.loaded).to.equal(false);
|
98
|
+
expect(el.item).to.be.undefined;
|
119
99
|
|
120
|
-
|
121
|
-
|
100
|
+
// hydrate item
|
101
|
+
el.item = new ItemStub();
|
102
|
+
await el.updateComplete;
|
122
103
|
|
123
|
-
|
104
|
+
// spy fires
|
105
|
+
expect(spy.called).to.equal(true);
|
106
|
+
expect(spy.callCount).to.equal(1);
|
107
|
+
});
|
124
108
|
});
|
125
109
|
|
126
|
-
describe('
|
110
|
+
describe('`el.sharedObserver`', () => {
|
127
111
|
it('can create one', async () => {
|
128
112
|
const el = await fixture<ItemNavigator>(
|
129
113
|
html`<ia-item-navigator></ia-item-navigator>`
|
@@ -146,7 +130,7 @@ describe('ItemNavigator', () => {
|
|
146
130
|
});
|
147
131
|
});
|
148
132
|
|
149
|
-
describe('
|
133
|
+
describe('`el.modal`', () => {
|
150
134
|
it('can create one', async () => {
|
151
135
|
const el = await fixture<ItemNavigator>(
|
152
136
|
html`<ia-item-navigator></ia-item-navigator>`
|
@@ -171,59 +155,123 @@ describe('ItemNavigator', () => {
|
|
171
155
|
});
|
172
156
|
});
|
173
157
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
158
|
+
describe('full browser window immersion "fullscreen"', () => {
|
159
|
+
it('creates reflected attribute `viewportinfullscreen`', async () => {
|
160
|
+
/** to help with external styling adjustmnents */
|
161
|
+
const el = await fixture<ItemNavigator>(
|
162
|
+
html`<ia-item-navigator></ia-item-navigator>`
|
163
|
+
);
|
164
|
+
expect(el.getAttribute('viewportinfullscreen')).to.be.null;
|
165
|
+
|
166
|
+
el.viewportInFullscreen = true;
|
167
|
+
await el.updateComplete;
|
168
|
+
|
169
|
+
expect(el.getAttribute('viewportinfullscreen')).to.exist;
|
170
|
+
});
|
171
|
+
});
|
179
172
|
|
180
|
-
|
181
|
-
|
173
|
+
/* Side menu & shortcuts tests */
|
174
|
+
describe('el.menuOpened', () => {
|
175
|
+
it('toggles side menu open', async () => {
|
176
|
+
const el = await fixture<ItemNavigator>(
|
177
|
+
html`<ia-item-navigator .item=${new ItemStub()}></ia-item-navigator>`
|
178
|
+
);
|
182
179
|
|
183
|
-
|
184
|
-
|
180
|
+
el.menuContents = [menuProvider];
|
181
|
+
await el.updateComplete;
|
185
182
|
|
186
|
-
|
187
|
-
// const el = await fixture<YourWebComponent>(
|
188
|
-
// html`<your-webcomponent></your-webcomponent>`
|
189
|
-
// );
|
183
|
+
const nav = el.shadowRoot?.querySelector('nav');
|
190
184
|
|
191
|
-
|
192
|
-
|
193
|
-
|
185
|
+
expect(nav?.querySelector('#menu')).to.exist;
|
186
|
+
// side menu starts closed
|
187
|
+
expect(el.menuOpened).to.be.false;
|
188
|
+
expect(nav?.querySelector('#menu')?.getAttribute('class')).to.contain(
|
189
|
+
'hidden'
|
190
|
+
);
|
191
|
+
|
192
|
+
// let's open menu
|
193
|
+
el.toggleMenu();
|
194
|
+
await el.updateComplete;
|
195
|
+
|
196
|
+
expect(el.menuOpened).to.be.true;
|
197
|
+
expect(nav?.querySelector('#menu')?.getAttribute('class')).to.not.contain(
|
198
|
+
'hidden'
|
199
|
+
);
|
200
|
+
});
|
201
|
+
|
202
|
+
it('opens menu shortcut with `@manageSideMenuEvents`', async () => {
|
203
|
+
const el = await fixture<ItemNavigator>(
|
204
|
+
html`<ia-item-navigator .item=${new ItemStub()}></ia-item-navigator>`
|
205
|
+
);
|
206
|
+
const detail = {
|
207
|
+
menuId: 'fullscreen',
|
208
|
+
action: 'open',
|
209
|
+
};
|
210
|
+
|
211
|
+
el.menuContents = [menuProvider];
|
212
|
+
await el.updateComplete;
|
213
|
+
const frame = el.shadowRoot?.querySelector('#frame');
|
214
|
+
// default menu open behavior is to side menu open, not overlay
|
215
|
+
expect(frame?.getAttribute('class')).to.contain('shift');
|
216
|
+
|
217
|
+
expect(el.menuOpened).to.be.false;
|
218
|
+
expect(el.openMenu).to.be.empty;
|
219
|
+
expect(frame?.getAttribute('class')).to.not.contain('open');
|
220
|
+
|
221
|
+
const event = new CustomEvent('updateSideMenu', {
|
222
|
+
detail,
|
223
|
+
}) as IntManageSideMenuEvent;
|
224
|
+
el.manageSideMenuEvents(event);
|
225
|
+
await el.updateComplete;
|
226
|
+
|
227
|
+
expect(el.shouldRenderMenu).to.be.true;
|
228
|
+
expect(el.menuOpened).to.be.true;
|
229
|
+
expect(el.openMenu).to.equal(detail.menuId);
|
230
|
+
|
231
|
+
expect(frame?.getAttribute('class')).to.contain('open');
|
232
|
+
});
|
233
|
+
});
|
234
|
+
|
235
|
+
describe('el.menuContents', () => {
|
236
|
+
it('draws side menu when populated', async () => {
|
237
|
+
const el = await fixture<ItemNavigator>(
|
238
|
+
html`<ia-item-navigator .item=${new ItemStub()}></ia-item-navigator>`
|
239
|
+
);
|
240
|
+
|
241
|
+
el.menuContents = [menuProvider];
|
242
|
+
await el.updateComplete;
|
243
|
+
expect(el.menuContents.length).to.exist;
|
244
|
+
expect(el.shouldRenderMenu).to.be.true;
|
245
|
+
|
246
|
+
const nav = el.shadowRoot?.querySelector('nav');
|
247
|
+
expect(nav).to.exist;
|
194
248
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
// html`<your-webcomponent></your-webcomponent>`
|
225
|
-
// );
|
226
|
-
|
227
|
-
// await expect(el).shadowDom.to.be.accessible();
|
228
|
-
// });
|
229
|
-
// });
|
249
|
+
const menuSlider = nav?.querySelector('ia-menu-slider');
|
250
|
+
expect(menuSlider).to.exist;
|
251
|
+
expect(menuSlider?.getAttribute('manuallyhandleclose')).to.exist;
|
252
|
+
expect(menuSlider?.getAttribute('open')).to.exist;
|
253
|
+
});
|
254
|
+
});
|
255
|
+
|
256
|
+
describe('`el.menuShortcuts`', () => {
|
257
|
+
it('displays shortcut & toggle side menu button', async () => {
|
258
|
+
const el = await fixture<ItemNavigator>(
|
259
|
+
html`<ia-item-navigator .item=${new ItemStub()}></ia-item-navigator>`
|
260
|
+
);
|
261
|
+
|
262
|
+
el.menuContents = [menuProvider];
|
263
|
+
el.menuShortcuts = [shortcut];
|
264
|
+
await el.updateComplete;
|
265
|
+
|
266
|
+
const nav = el.shadowRoot?.querySelector('nav');
|
267
|
+
|
268
|
+
expect(el.menuShortcuts.length).to.exist;
|
269
|
+
expect(nav).to.exist;
|
270
|
+
expect(nav?.querySelector('.shortcuts')).to.exist;
|
271
|
+
expect(
|
272
|
+
nav?.querySelector('.shortcuts')?.querySelector('i.fullscreen-test')
|
273
|
+
).to.exist;
|
274
|
+
expect(nav?.querySelector('.toggle-menu')).to.exist;
|
275
|
+
});
|
276
|
+
});
|
277
|
+
});
|
package/test/ia-stub.ts
CHANGED
@@ -1,3 +1,79 @@
|
|
1
|
-
|
1
|
+
/* eslint-disable camelcase */
|
2
|
+
import {
|
3
|
+
MetadataResponse,
|
4
|
+
Metadata,
|
5
|
+
File,
|
6
|
+
Review,
|
7
|
+
SpeechMusicASREntry,
|
8
|
+
} from '@internetarchive/search-service';
|
9
|
+
import { html } from 'lit-html';
|
10
|
+
import {
|
11
|
+
IntMenuShortcut,
|
12
|
+
IntMenuProvider,
|
13
|
+
} from '../src/interfaces/menu-interfaces';
|
2
14
|
|
3
|
-
|
15
|
+
export class ItemStub implements MetadataResponse {
|
16
|
+
constructor() {
|
17
|
+
this.rawResponse = '';
|
18
|
+
this.created = 1;
|
19
|
+
this.d1 = 'hello';
|
20
|
+
this.d2 = 'boop';
|
21
|
+
this.dir = 'whee';
|
22
|
+
this.files = [];
|
23
|
+
this.files_count = 0;
|
24
|
+
this.item_last_updated = 2020;
|
25
|
+
this.item_size = 111;
|
26
|
+
this.metadata = { identifier: 'foo' } as Metadata;
|
27
|
+
this.server = 'foo-server';
|
28
|
+
this.uniq = 2;
|
29
|
+
this.workable_servers = ['abc'];
|
30
|
+
}
|
31
|
+
|
32
|
+
rawResponse: any;
|
33
|
+
|
34
|
+
created: number;
|
35
|
+
|
36
|
+
d1: string;
|
37
|
+
|
38
|
+
d2: string;
|
39
|
+
|
40
|
+
dir: string;
|
41
|
+
|
42
|
+
files: File[];
|
43
|
+
|
44
|
+
files_count: number;
|
45
|
+
|
46
|
+
item_last_updated: number;
|
47
|
+
|
48
|
+
item_size: number;
|
49
|
+
|
50
|
+
metadata: Metadata;
|
51
|
+
|
52
|
+
server: string;
|
53
|
+
|
54
|
+
uniq: number;
|
55
|
+
|
56
|
+
workable_servers: string[];
|
57
|
+
|
58
|
+
speech_vs_music_asr?: SpeechMusicASREntry[] | undefined;
|
59
|
+
|
60
|
+
reviews?: Review[] | undefined;
|
61
|
+
}
|
62
|
+
|
63
|
+
export const shortcut = {
|
64
|
+
id: 'fullscreen',
|
65
|
+
icon: html`<i class="fas fullscreen-test"></i>`,
|
66
|
+
} as IntMenuShortcut;
|
67
|
+
|
68
|
+
export const menuProvider = {
|
69
|
+
...shortcut,
|
70
|
+
label: 'foo',
|
71
|
+
menuDetails: html`<div>foo</div>`,
|
72
|
+
selected: true,
|
73
|
+
followable: false,
|
74
|
+
href: 'https://archive.foo',
|
75
|
+
item: new ItemStub(),
|
76
|
+
baseHost: 'https://archive.foo',
|
77
|
+
subPrefix: 'bar',
|
78
|
+
updated: () => {},
|
79
|
+
} as IntMenuProvider;
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { html, fixture, expect } from '@open-wc/testing';
|
2
|
+
import { IANoTheaterAvailable } from '../src/no-theater-available';
|
3
|
+
import '../src/no-theater-available';
|
4
|
+
|
5
|
+
describe('IANoTheaterAvailable', () => {
|
6
|
+
it('Fires `loadingStateUpdated` on identifier change', async () => {
|
7
|
+
const el = await fixture<IANoTheaterAvailable>(
|
8
|
+
html`<ia-no-theater-available
|
9
|
+
.identifier=${`foo`}
|
10
|
+
></ia-no-theater-available>`
|
11
|
+
);
|
12
|
+
let eventFired = false;
|
13
|
+
el.addEventListener('loadingStateUpdated', () => {
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
15
|
+
eventFired = true;
|
16
|
+
});
|
17
|
+
expect(eventFired).to.be.false;
|
18
|
+
|
19
|
+
el.identifier = 'bar';
|
20
|
+
await el.updateComplete;
|
21
|
+
expect(eventFired).to.be.true;
|
22
|
+
});
|
23
|
+
it('Has link to item download page', async () => {
|
24
|
+
const el = await fixture<IANoTheaterAvailable>(
|
25
|
+
html`<ia-no-theater-available
|
26
|
+
.identifier=${`foo`}
|
27
|
+
></ia-no-theater-available>`
|
28
|
+
);
|
29
|
+
expect(el.downloadUrl).to.equal('/download/foo');
|
30
|
+
expect(el?.shadowRoot?.querySelector('a')?.href).to.contain(el.downloadUrl);
|
31
|
+
});
|
32
|
+
});
|