@excom/quark-sheet 0.1.0
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/.rush/temp/chunked-rush-logs/quark-sheet.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark-sheet.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark-sheet.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/index.css +5 -0
- package/index.ts +17 -0
- package/package.json +46 -0
- package/quark-sheet.ts +182 -0
- package/rush-logs/quark-sheet.apply-exports.cache.log +1 -0
- package/rush-logs/quark-sheet.apply-exports.log +1 -0
- package/rush-logs/quark-sheet.build_docs.cache.log +1 -0
- package/rush-logs/quark-sheet.build_docs.log +1 -0
- package/rush-logs/quark-sheet.build_package-metas.cache.log +1 -0
- package/rush-logs/quark-sheet.build_package-metas.log +1 -0
- package/src/quark-sheet.css +8 -0
- package/support/custom-elements.json +220 -0
- package/support/demos/provider.html +13 -0
- package/support/demos/simple.html +14 -0
- package/support/dist-docs/quark-sheet.md +147 -0
- package/support/docs/README.md +53 -0
- package/support/package-meta.json +185 -0
- package/support/tests/__snapshots__/provider.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/simple.view.test.ts.snap +23 -0
- package/support/tests/provider.view.test.ts +42 -0
- package/support/tests/quark-sheet.test.ts +377 -0
- package/support/tests/simple.view.test.ts +33 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { invokeCommand } from "@excom/neutron";
|
|
2
|
+
import "../../index";
|
|
3
|
+
import {
|
|
4
|
+
afterEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
fixture,
|
|
8
|
+
it,
|
|
9
|
+
spyFetch,
|
|
10
|
+
wait,
|
|
11
|
+
waitForEvent,
|
|
12
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
13
|
+
import { KitLogger } from "@excom/kit-logger";
|
|
14
|
+
|
|
15
|
+
const vi = (globalThis as any).vi;
|
|
16
|
+
|
|
17
|
+
const quarkSrc = `main[data-test="abc"] span { data-test: "def"; }`;
|
|
18
|
+
|
|
19
|
+
describe("quark-sheet", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.innerHTML = "";
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("defines the quark-sheet custom element", () => {
|
|
26
|
+
expect(customElements.get("quark-sheet")).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("builds quark from inline text content on connect", () => {
|
|
30
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
31
|
+
`<quark-sheet>${quarkSrc}</quark-sheet>`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(el).dom.to.equalTag(
|
|
35
|
+
`<quark-sheet is-success></quark-sheet>`,
|
|
36
|
+
);
|
|
37
|
+
expect(el.quarkInstance).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("does not build quark when text content is empty", () => {
|
|
41
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
42
|
+
`<quark-sheet></quark-sheet>`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(el.isSuccess).toBeFalsy();
|
|
46
|
+
expect(el.quarkInstance).toBeFalsy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("fetches and builds quark from srcUrl", async () => {
|
|
50
|
+
const fetchSpy = vi
|
|
51
|
+
.spyOn(globalThis, "fetch")
|
|
52
|
+
.mockImplementation(() =>
|
|
53
|
+
Promise.resolve({
|
|
54
|
+
text: () => Promise.resolve(quarkSrc),
|
|
55
|
+
} as Response),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
59
|
+
`<quark-sheet src-url="/public/my-sheet"></quark-sheet>`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(el).dom.to.equalTag(
|
|
63
|
+
`<quark-sheet is-loading src-url="/public/my-sheet"></quark-sheet>`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await waitForEvent(el, "quark-sheet-success");
|
|
67
|
+
|
|
68
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
69
|
+
"/public/my-sheet",
|
|
70
|
+
expect.any(Object),
|
|
71
|
+
);
|
|
72
|
+
expect(el).dom.to.equalTag(
|
|
73
|
+
`<quark-sheet is-success src-url="/public/my-sheet"></quark-sheet>`,
|
|
74
|
+
);
|
|
75
|
+
expect(el.quarkInstance).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("re-fetches on the --reload command, bypassing the shared text cache", async () => {
|
|
79
|
+
let calls = 0;
|
|
80
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(() => {
|
|
81
|
+
calls++;
|
|
82
|
+
return Promise.resolve({
|
|
83
|
+
text: () => Promise.resolve(quarkSrc),
|
|
84
|
+
} as Response);
|
|
85
|
+
});
|
|
86
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
87
|
+
`<quark-sheet src-url="/public/reload-sheet"></quark-sheet>`,
|
|
88
|
+
);
|
|
89
|
+
await waitForEvent(el, "quark-sheet-success");
|
|
90
|
+
const first = el.quarkInstance;
|
|
91
|
+
expect(calls).toBe(1);
|
|
92
|
+
|
|
93
|
+
invokeCommand(el, "--reload");
|
|
94
|
+
// the handler runs in a microtask after the dispatch
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
expect(el.isLoading).toBe(true);
|
|
97
|
+
await waitForEvent(el, "quark-sheet-success");
|
|
98
|
+
expect(calls).toBe(2);
|
|
99
|
+
expect(el.quarkInstance).not.toBe(first);
|
|
100
|
+
expect(el).dom.to.equalTag(
|
|
101
|
+
`<quark-sheet is-success src-url="/public/reload-sheet"></quark-sheet>`,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// without src-url the command is a no-op
|
|
105
|
+
const inline = fixture<HTMLQuarkSheetElement>(
|
|
106
|
+
`<quark-sheet>${quarkSrc}</quark-sheet>`,
|
|
107
|
+
);
|
|
108
|
+
invokeCommand(inline, "--reload");
|
|
109
|
+
await wait(0);
|
|
110
|
+
expect(inline.isLoading).toBeFalsy();
|
|
111
|
+
expect(calls).toBe(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("unregisters previous quark when srcUrl changes", async () => {
|
|
115
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
116
|
+
`<quark-sheet>${quarkSrc}</quark-sheet>`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const qi = el.quarkInstance!;
|
|
120
|
+
vi.spyOn(qi, "unregister");
|
|
121
|
+
|
|
122
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(() =>
|
|
123
|
+
Promise.resolve({
|
|
124
|
+
text: () =>
|
|
125
|
+
Promise.resolve(quarkSrc + " extra-rule { autofocus: ''; }"),
|
|
126
|
+
} as Response),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
el.textContent = "";
|
|
130
|
+
el.srcUrl = "/public/new-sheet";
|
|
131
|
+
|
|
132
|
+
await waitForEvent(el, "quark-sheet-success");
|
|
133
|
+
|
|
134
|
+
expect(qi.unregister).toHaveBeenCalled();
|
|
135
|
+
expect(el.quarkInstance).not.toBe(qi);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("sets error state when src fetch rejects", async () => {
|
|
139
|
+
KitLogger.suppress();
|
|
140
|
+
|
|
141
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(() =>
|
|
142
|
+
Promise.reject(new Error("network error")),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
146
|
+
`<quark-sheet src-url="/bad-url"></quark-sheet>`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await waitForEvent(el, "quark-sheet-error");
|
|
150
|
+
|
|
151
|
+
KitLogger.unsuppress();
|
|
152
|
+
|
|
153
|
+
expect(el).dom.to.equalTag(
|
|
154
|
+
`<quark-sheet is-error src-url="/bad-url"></quark-sheet>`,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("emits quark-sheet-loading when loading starts", async () => {
|
|
159
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
160
|
+
() => new Promise(() => {}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
164
|
+
`<quark-sheet></quark-sheet>`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const loadingPromise = waitForEvent(el, "quark-sheet-loading", () => {
|
|
168
|
+
el.srcUrl = "/slow-url";
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await loadingPromise;
|
|
172
|
+
|
|
173
|
+
expect(el.isLoading).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("re-registers quark when reconnected", () => {
|
|
177
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
178
|
+
`<quark-sheet>${quarkSrc}</quark-sheet>`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const qi = el.quarkInstance!;
|
|
182
|
+
vi.spyOn(qi, "register");
|
|
183
|
+
|
|
184
|
+
el.remove();
|
|
185
|
+
document.body.appendChild(el);
|
|
186
|
+
|
|
187
|
+
expect(qi.register).toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("unregisters quark on disconnect", async () => {
|
|
191
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
192
|
+
`<quark-sheet>${quarkSrc}</quark-sheet>`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const qi = el.quarkInstance!;
|
|
196
|
+
vi.spyOn(qi, "unregister");
|
|
197
|
+
|
|
198
|
+
el.remove();
|
|
199
|
+
await wait(0);
|
|
200
|
+
|
|
201
|
+
expect(qi.unregister).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("scopes rules to the host by default (backwards compatible)", async () => {
|
|
205
|
+
const root = fixture<HTMLElement>(
|
|
206
|
+
`<div>
|
|
207
|
+
<section>
|
|
208
|
+
<quark-sheet>[bind-x] { data-hit: ""; }</quark-sheet>
|
|
209
|
+
<span bind-x></span>
|
|
210
|
+
</section>
|
|
211
|
+
<aside><span bind-x></span></aside>
|
|
212
|
+
</div>`,
|
|
213
|
+
);
|
|
214
|
+
const el = root.querySelector("quark-sheet") as HTMLQuarkSheetElement;
|
|
215
|
+
await wait(0);
|
|
216
|
+
await wait(0);
|
|
217
|
+
|
|
218
|
+
expect(el.quarkInstance!.options.isScoped).toBe(true);
|
|
219
|
+
expect(
|
|
220
|
+
root.querySelector("section [bind-x]")?.hasAttribute("data-hit"),
|
|
221
|
+
).toBe(true);
|
|
222
|
+
expect(root.querySelector("aside [bind-x]")?.hasAttribute("data-hit")).toBe(
|
|
223
|
+
false,
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("is-global runs top-level rules in the root context", async () => {
|
|
228
|
+
const root = fixture<HTMLElement>(
|
|
229
|
+
`<div>
|
|
230
|
+
<section>
|
|
231
|
+
<quark-sheet is-global>[bind-x] { data-hit: ""; }</quark-sheet>
|
|
232
|
+
<span bind-x></span>
|
|
233
|
+
</section>
|
|
234
|
+
<aside><span bind-x></span></aside>
|
|
235
|
+
</div>`,
|
|
236
|
+
);
|
|
237
|
+
const el = root.querySelector("quark-sheet") as HTMLQuarkSheetElement;
|
|
238
|
+
await wait(0);
|
|
239
|
+
await wait(0);
|
|
240
|
+
|
|
241
|
+
expect(el.quarkInstance!.options.isScoped).toBe(false);
|
|
242
|
+
expect(root.querySelectorAll("[bind-x][data-hit]")).toHaveLength(2);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("quark mutates desired elements", async () => {
|
|
246
|
+
// Sheet + target must share a parent: Quark scopes rules to
|
|
247
|
+
// the `@scope` wrapper anchored at the sheet's host.
|
|
248
|
+
const root = fixture<HTMLElement>(
|
|
249
|
+
`<section>
|
|
250
|
+
<quark-sheet>${quarkSrc}</quark-sheet>
|
|
251
|
+
<main data-test="abc">
|
|
252
|
+
<span></span>
|
|
253
|
+
</main>
|
|
254
|
+
</section>`,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Attribute paints are scheduled on the next macrotask.
|
|
258
|
+
await wait(0);
|
|
259
|
+
expect(root.querySelector("span")?.getAttribute("data-test")).toBe("def");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("sets error state and emits quark-sheet-error when inline text fails to parse", async () => {
|
|
263
|
+
KitLogger.suppress();
|
|
264
|
+
const el = document.createElement("quark-sheet") as HTMLQuarkSheetElement;
|
|
265
|
+
el.textContent = "a { b: ?; }";
|
|
266
|
+
try {
|
|
267
|
+
await waitForEvent(el, "quark-sheet-error", () =>
|
|
268
|
+
document.body.appendChild(el),
|
|
269
|
+
);
|
|
270
|
+
} finally {
|
|
271
|
+
KitLogger.unsuppress();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
expect(el).dom.to.equalTag(`<quark-sheet is-error></quark-sheet>`);
|
|
275
|
+
expect(el.isSuccess).toBeFalsy();
|
|
276
|
+
expect(el.quarkInstance).toBeFalsy();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("sets error state when fetched text fails to parse", async () => {
|
|
280
|
+
KitLogger.suppress();
|
|
281
|
+
spyFetch({ status: 200, body: "a { b: ?; }" });
|
|
282
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
283
|
+
`<quark-sheet src-url="/public/broken-sheet"></quark-sheet>`,
|
|
284
|
+
);
|
|
285
|
+
try {
|
|
286
|
+
await waitForEvent(el, "quark-sheet-error");
|
|
287
|
+
} finally {
|
|
288
|
+
KitLogger.unsuppress();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
expect(el).dom.to.equalTag(
|
|
292
|
+
`<quark-sheet is-error src-url="/public/broken-sheet"></quark-sheet>`,
|
|
293
|
+
);
|
|
294
|
+
expect(el.srcText).toBe("a { b: ?; }");
|
|
295
|
+
expect(el.srcPromise).toBeFalsy();
|
|
296
|
+
expect(el.quarkInstance).toBeFalsy();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("rebuilds and re-registers quark when is-global toggles", () => {
|
|
300
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
301
|
+
`<quark-sheet>${quarkSrc}</quark-sheet>`,
|
|
302
|
+
);
|
|
303
|
+
const scoped = el.quarkInstance!;
|
|
304
|
+
expect(scoped.options.isScoped).toBe(true);
|
|
305
|
+
vi.spyOn(scoped, "unregister");
|
|
306
|
+
|
|
307
|
+
el.isGlobal = true;
|
|
308
|
+
|
|
309
|
+
const global = el.quarkInstance!;
|
|
310
|
+
expect(scoped.unregister).toHaveBeenCalledTimes(1);
|
|
311
|
+
expect(global).not.toBe(scoped);
|
|
312
|
+
expect(global.options.isScoped).toBe(false);
|
|
313
|
+
expect(global.isRegistered).toBe(true);
|
|
314
|
+
expect(el).dom.to.equalTag(
|
|
315
|
+
`<quark-sheet is-global is-success></quark-sheet>`,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
el.isGlobal = false;
|
|
319
|
+
|
|
320
|
+
expect(global.isRegistered).toBe(false);
|
|
321
|
+
expect(el.quarkInstance).not.toBe(global);
|
|
322
|
+
expect(el.quarkInstance!.options.isScoped).toBe(true);
|
|
323
|
+
expect(el.quarkInstance!.isRegistered).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("does not build quark when is-global toggles without sheet text", () => {
|
|
327
|
+
const el = fixture<HTMLQuarkSheetElement>(`<quark-sheet></quark-sheet>`);
|
|
328
|
+
|
|
329
|
+
el.isGlobal = true;
|
|
330
|
+
|
|
331
|
+
expect(el.quarkInstance).toBeFalsy();
|
|
332
|
+
expect(el.isSuccess).toBeFalsy();
|
|
333
|
+
expect(el.isError).toBeFalsy();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("defers building quark while a fetch is pending", async () => {
|
|
337
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
338
|
+
() => new Promise(() => {}),
|
|
339
|
+
);
|
|
340
|
+
const el = fixture<HTMLQuarkSheetElement>(
|
|
341
|
+
`<quark-sheet src-url="/public/never-resolves"></quark-sheet>`,
|
|
342
|
+
);
|
|
343
|
+
expect(el.isLoading).toBe(true);
|
|
344
|
+
|
|
345
|
+
el.srcText = quarkSrc;
|
|
346
|
+
await wait(0);
|
|
347
|
+
|
|
348
|
+
expect(el.quarkInstance).toBeFalsy();
|
|
349
|
+
expect(el).dom.to.equalTag(
|
|
350
|
+
`<quark-sheet is-loading src-url="/public/never-resolves"></quark-sheet>`,
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("re-registers the same quark against the new parent when moved", () => {
|
|
355
|
+
const root = fixture<HTMLElement>(
|
|
356
|
+
`<div>
|
|
357
|
+
<section data-host="a"><quark-sheet>${quarkSrc}</quark-sheet></section>
|
|
358
|
+
<section data-host="b"></section>
|
|
359
|
+
</div>`,
|
|
360
|
+
);
|
|
361
|
+
const el = root.querySelector("quark-sheet") as HTMLQuarkSheetElement;
|
|
362
|
+
const hostA = root.querySelector('[data-host="a"]')!;
|
|
363
|
+
const hostB = root.querySelector('[data-host="b"]')!;
|
|
364
|
+
const qi = el.quarkInstance!;
|
|
365
|
+
expect(qi.host.deref()).toBe(hostA);
|
|
366
|
+
vi.spyOn(qi, "unregister");
|
|
367
|
+
vi.spyOn(qi, "register");
|
|
368
|
+
|
|
369
|
+
hostB.appendChild(el);
|
|
370
|
+
|
|
371
|
+
expect(qi.unregister).toHaveBeenCalledTimes(1);
|
|
372
|
+
expect(qi.register).toHaveBeenCalledTimes(1);
|
|
373
|
+
expect(el.quarkInstance).toBe(qi);
|
|
374
|
+
expect(qi.isRegistered).toBe(true);
|
|
375
|
+
expect(qi.host.deref()).toBe(hostB);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import "../../index";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
8
|
+
import {
|
|
9
|
+
expectComplexity,
|
|
10
|
+
flush,
|
|
11
|
+
measureComplexity,
|
|
12
|
+
mountView,
|
|
13
|
+
readDemo,
|
|
14
|
+
} from "@excom/quark/support/tests/view-helpers";
|
|
15
|
+
|
|
16
|
+
describe("simple view", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
document.body.innerHTML = "";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("labels the details panel open and closed", async () => {
|
|
22
|
+
const { root, quark } = await mountView(readDemo(import.meta.url, "simple"));
|
|
23
|
+
const details = root.querySelector("details")!;
|
|
24
|
+
expect(details.querySelector("summary")?.textContent).toBe("Panel Closed");
|
|
25
|
+
const meter = measureComplexity(quark!);
|
|
26
|
+
details.setAttribute("open", "");
|
|
27
|
+
await flush();
|
|
28
|
+
const budget = meter.take();
|
|
29
|
+
meter.stop();
|
|
30
|
+
expect(details.querySelector("summary")?.textContent).toBe("Panel Open");
|
|
31
|
+
expectComplexity(budget);
|
|
32
|
+
});
|
|
33
|
+
});
|