@gogocat/data-bind 1.11.0 → 2.0.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/.editorconfig +14 -14
- package/.vscode/launch.json +12 -12
- package/CONFIGURATION.md +294 -0
- package/REACTIVE_MODE.md +553 -0
- package/README.md +266 -829
- package/babel.config.json +30 -0
- package/dist/js/_escape.d.ts +14 -0
- package/dist/js/_escape.d.ts.map +1 -0
- package/dist/js/applyBinding.d.ts +11 -0
- package/dist/js/applyBinding.d.ts.map +1 -0
- package/dist/js/attrBinding.d.ts +12 -0
- package/dist/js/attrBinding.d.ts.map +1 -0
- package/dist/js/binder.d.ts +67 -0
- package/dist/js/binder.d.ts.map +1 -0
- package/dist/js/changeBinding.d.ts +19 -0
- package/dist/js/changeBinding.d.ts.map +1 -0
- package/dist/js/commentWrapper.d.ts +39 -0
- package/dist/js/commentWrapper.d.ts.map +1 -0
- package/dist/js/config.d.ts +55 -0
- package/dist/js/config.d.ts.map +1 -0
- package/dist/js/createBindingOption.d.ts +32 -0
- package/dist/js/createBindingOption.d.ts.map +1 -0
- package/dist/js/createEventBinding.d.ts +10 -0
- package/dist/js/createEventBinding.d.ts.map +1 -0
- package/dist/js/cssBinding.d.ts +15 -0
- package/dist/js/cssBinding.d.ts.map +1 -0
- package/dist/js/dataBind.js +2772 -2519
- package/dist/js/dataBind.min.js +8 -1
- package/dist/js/dataBind.min.js.map +1 -1
- package/dist/js/domWalker.d.ts +9 -0
- package/dist/js/domWalker.d.ts.map +1 -0
- package/dist/js/forOfBinding.d.ts +12 -0
- package/dist/js/forOfBinding.d.ts.map +1 -0
- package/dist/js/hoverBinding.d.ts +13 -0
- package/dist/js/hoverBinding.d.ts.map +1 -0
- package/dist/js/ifBinding.d.ts +12 -0
- package/dist/js/ifBinding.d.ts.map +1 -0
- package/dist/js/index.d.ts +10 -0
- package/dist/js/index.d.ts.map +1 -0
- package/dist/js/modelBinding.d.ts +12 -0
- package/dist/js/modelBinding.d.ts.map +1 -0
- package/dist/js/postProcess.d.ts +3 -0
- package/dist/js/postProcess.d.ts.map +1 -0
- package/dist/js/pubSub.d.ts +11 -0
- package/dist/js/pubSub.d.ts.map +1 -0
- package/dist/js/reactiveProxy.d.ts +28 -0
- package/dist/js/reactiveProxy.d.ts.map +1 -0
- package/dist/js/renderForOfBinding.d.ts +8 -0
- package/dist/js/renderForOfBinding.d.ts.map +1 -0
- package/dist/js/renderIfBinding.d.ts +22 -0
- package/dist/js/renderIfBinding.d.ts.map +1 -0
- package/dist/js/renderIteration.d.ts +16 -0
- package/dist/js/renderIteration.d.ts.map +1 -0
- package/dist/js/renderTemplate.d.ts +14 -0
- package/dist/js/renderTemplate.d.ts.map +1 -0
- package/dist/js/renderTemplatesBinding.d.ts +19 -0
- package/dist/js/renderTemplatesBinding.d.ts.map +1 -0
- package/dist/js/showBinding.d.ts +13 -0
- package/dist/js/showBinding.d.ts.map +1 -0
- package/dist/js/switchBinding.d.ts +13 -0
- package/dist/js/switchBinding.d.ts.map +1 -0
- package/dist/js/textBinding.d.ts +13 -0
- package/dist/js/textBinding.d.ts.map +1 -0
- package/dist/js/types/_escape.d.ts +14 -0
- package/dist/js/types/_escape.d.ts.map +1 -0
- package/dist/js/types/applyBinding.d.ts +11 -0
- package/dist/js/types/applyBinding.d.ts.map +1 -0
- package/dist/js/types/attrBinding.d.ts +12 -0
- package/dist/js/types/attrBinding.d.ts.map +1 -0
- package/dist/js/types/binder.d.ts +67 -0
- package/dist/js/types/binder.d.ts.map +1 -0
- package/dist/js/types/changeBinding.d.ts +19 -0
- package/dist/js/types/changeBinding.d.ts.map +1 -0
- package/dist/js/types/commentWrapper.d.ts +39 -0
- package/dist/js/types/commentWrapper.d.ts.map +1 -0
- package/dist/js/types/config.d.ts +55 -0
- package/dist/js/types/config.d.ts.map +1 -0
- package/dist/js/types/createBindingOption.d.ts +32 -0
- package/dist/js/types/createBindingOption.d.ts.map +1 -0
- package/dist/js/types/createEventBinding.d.ts +10 -0
- package/dist/js/types/createEventBinding.d.ts.map +1 -0
- package/dist/js/types/cssBinding.d.ts +15 -0
- package/dist/js/types/cssBinding.d.ts.map +1 -0
- package/dist/js/types/domWalker.d.ts +9 -0
- package/dist/js/types/domWalker.d.ts.map +1 -0
- package/dist/js/types/forOfBinding.d.ts +12 -0
- package/dist/js/types/forOfBinding.d.ts.map +1 -0
- package/dist/js/types/hoverBinding.d.ts +13 -0
- package/dist/js/types/hoverBinding.d.ts.map +1 -0
- package/dist/js/types/ifBinding.d.ts +12 -0
- package/dist/js/types/ifBinding.d.ts.map +1 -0
- package/dist/js/types/index.d.ts +10 -0
- package/dist/js/types/index.d.ts.map +1 -0
- package/dist/js/types/modelBinding.d.ts +12 -0
- package/dist/js/types/modelBinding.d.ts.map +1 -0
- package/dist/js/types/postProcess.d.ts +3 -0
- package/dist/js/types/postProcess.d.ts.map +1 -0
- package/dist/js/types/pubSub.d.ts +11 -0
- package/dist/js/types/pubSub.d.ts.map +1 -0
- package/dist/js/types/reactiveProxy.d.ts +28 -0
- package/dist/js/types/reactiveProxy.d.ts.map +1 -0
- package/dist/js/types/renderForOfBinding.d.ts +8 -0
- package/dist/js/types/renderForOfBinding.d.ts.map +1 -0
- package/dist/js/types/renderIfBinding.d.ts +22 -0
- package/dist/js/types/renderIfBinding.d.ts.map +1 -0
- package/dist/js/types/renderIteration.d.ts +16 -0
- package/dist/js/types/renderIteration.d.ts.map +1 -0
- package/dist/js/types/renderTemplate.d.ts +14 -0
- package/dist/js/types/renderTemplate.d.ts.map +1 -0
- package/dist/js/types/renderTemplatesBinding.d.ts +19 -0
- package/dist/js/types/renderTemplatesBinding.d.ts.map +1 -0
- package/dist/js/types/showBinding.d.ts +13 -0
- package/dist/js/types/showBinding.d.ts.map +1 -0
- package/dist/js/types/switchBinding.d.ts +13 -0
- package/dist/js/types/switchBinding.d.ts.map +1 -0
- package/dist/js/types/textBinding.d.ts +13 -0
- package/dist/js/types/textBinding.d.ts.map +1 -0
- package/dist/js/types/types.d.ts +111 -0
- package/dist/js/types/types.d.ts.map +1 -0
- package/dist/js/types/util.d.ts +119 -0
- package/dist/js/types/util.d.ts.map +1 -0
- package/dist/js/types.d.ts +111 -0
- package/dist/js/types.d.ts.map +1 -0
- package/dist/js/util.d.ts +119 -0
- package/dist/js/util.d.ts.map +1 -0
- package/eslint.config.js +124 -0
- package/examples/DBMONSTER_COMPARISON.md +123 -0
- package/examples/afterRenderDemo.html +119 -0
- package/examples/bootstrap/css/animate.css +1579 -1579
- package/examples/bootstrap/css/bootstrap.min.css +6 -6
- package/examples/bootstrap/css/homeservices.css +378 -390
- package/examples/bootstrap/css/open-iconic.css +511 -511
- package/examples/bootstrap/fonts/open-iconic.svg +543 -543
- package/examples/bootstrap/js/compMessageDialog.js +20 -19
- package/examples/bootstrap/js/compSearchBar.js +12 -19
- package/examples/bootstrap/js/compSearchResults.js +50 -46
- package/examples/bootstrap/js/featureAdsResult.json +65 -65
- package/examples/bootstrap/js/searchResult.json +57 -57
- package/examples/bootstrap.html +343 -332
- package/examples/css/baseTodo.css +141 -141
- package/examples/css/dbMonsterStyles.css +27 -27
- package/examples/css/indexTodo.css +374 -374
- package/examples/dbmonsterForOfReactive.html +40 -0
- package/examples/dbmonsterReact.html +19 -0
- package/examples/forOfBindingSimpleDebug.html +45 -0
- package/examples/form.html +20 -4
- package/examples/globalConfig.html +131 -0
- package/examples/js/afterRenderDemo.js +190 -0
- package/examples/js/appTodo.js +46 -46
- package/examples/js/attrBindingDemo.js +2 -2
- package/examples/js/dbMonApp.js +24 -26
- package/examples/js/dbMonAppReact.jsx +79 -0
- package/examples/js/dbMonAppReactive.js +28 -0
- package/examples/js/fiberDemo.js +4 -4
- package/examples/js/filtersDemo.js +8 -8
- package/examples/js/forOfDemo.js +7 -9
- package/examples/js/forOfDemoComplex.js +44 -17
- package/examples/js/form.js +44 -12
- package/examples/js/globalConfig.js +117 -0
- package/examples/js/ifBindingDemo.js +16 -16
- package/examples/js/reactiveDemo.js +119 -0
- package/examples/js/switchBindingDemo.js +8 -8
- package/examples/react-dbmonster/dist/bundle.js +43 -0
- package/examples/react-dbmonster/package-lock.json +537 -0
- package/examples/react-dbmonster/package.json +16 -0
- package/examples/react-dbmonster/src/index.jsx +80 -0
- package/examples/reactiveDemo.html +127 -0
- package/examples/refreshRateTest.html +75 -75
- package/index.html +841 -0
- package/package.json +31 -34
- package/rollup.config.js +79 -36
- package/src/{_escape.js → _escape.ts} +19 -17
- package/src/applyBinding.ts +179 -0
- package/src/{attrBinding.js → attrBinding.ts} +14 -13
- package/src/binder.ts +289 -0
- package/src/changeBinding.ts +93 -0
- package/src/{commentWrapper.js → commentWrapper.ts} +33 -30
- package/src/config.ts +107 -0
- package/src/createBindingOption.ts +91 -0
- package/src/createEventBinding.ts +88 -0
- package/src/{cssBinding.js → cssBinding.ts} +13 -11
- package/src/{domWalker.js → domWalker.ts} +44 -30
- package/src/{forOfBinding.js → forOfBinding.ts} +4 -3
- package/src/hoverBinding.ts +84 -0
- package/src/{ifBinding.js → ifBinding.ts} +14 -12
- package/src/index.ts +53 -0
- package/src/{modelBinding.js → modelBinding.ts} +11 -9
- package/src/postProcess.ts +22 -0
- package/src/{pubSub.js → pubSub.ts} +24 -15
- package/src/reactiveProxy.ts +285 -0
- package/src/{renderForOfBinding.js → renderForOfBinding.ts} +55 -33
- package/src/{renderIfBinding.js → renderIfBinding.ts} +45 -20
- package/src/renderIteration.ts +53 -0
- package/src/renderTemplate.ts +165 -0
- package/src/renderTemplatesBinding.ts +73 -0
- package/src/{showBinding.js → showBinding.ts} +4 -3
- package/src/{switchBinding.js → switchBinding.ts} +18 -15
- package/src/{textBinding.js → textBinding.ts} +5 -4
- package/src/types.ts +124 -0
- package/src/util.ts +810 -0
- package/test/css/reporter.css +9 -9
- package/test/fixtures/dataBindBootstrap.html +2 -2
- package/test/fixtures/formBindings.html +9 -1
- package/test/globals.d.ts +19 -0
- package/test/helpers/testHelper.js +46 -11
- package/test/mocks/featureAdsResult.json +65 -65
- package/test/mocks/searchResult.json +57 -57
- package/test/specs/{attrBinding.spec.js → attrBinding.spec.ts} +103 -106
- package/test/specs/{binder.spec.js → binder.spec.ts} +29 -27
- package/test/specs/blurBinding.spec.ts +60 -0
- package/test/specs/chainableUse.spec.ts +125 -0
- package/test/specs/clickBinding.spec.ts +194 -0
- package/test/specs/{cssBinding.spec.js → cssBinding.spec.ts} +72 -79
- package/test/specs/{dataBindBootstrap.spec.js → dataBindBootstrap.spec.ts} +332 -313
- package/test/specs/{filter.spec.js → filter.spec.ts} +75 -76
- package/test/specs/{forOfBinding.spec.js → forOfBinding.spec.ts} +208 -219
- package/test/specs/formBinding.spec.ts +272 -0
- package/test/specs/ifBinding.spec.ts +165 -0
- package/test/specs/{nestedComponent.spec.js → nestedComponent.spec.ts} +88 -88
- package/test/specs/reactiveProxy.spec.ts +465 -0
- package/test/specs/{showBinding.spec.js → showBinding.spec.ts} +148 -149
- package/test/specs/{switchBinding.spec.js → switchBinding.spec.ts} +172 -173
- package/test/specs/templateBinding.spec.ts +273 -0
- package/test/specs/{textBinding.spec.js → textBinding.spec.ts} +47 -48
- package/test/tsconfig.json +31 -0
- package/test-output.txt +200 -0
- package/test-reactive.html +224 -0
- package/tsconfig.json +28 -0
- package/vendors/lodash.custom.js +4577 -4577
- package/vendors/lodash.custom.min.js +45 -45
- package/vitest.config.js +27 -0
- package/.eslintrc.js +0 -1
- package/.grunt/grunt-contrib-jasmine/boot.js +0 -161
- package/.grunt/grunt-contrib-jasmine/dist/js/dataBind.js +0 -9
- package/.grunt/grunt-contrib-jasmine/grunt-template-jasmine-istanbul/reporter.js +0 -23
- package/.grunt/grunt-contrib-jasmine/jasmine-html.js +0 -853
- package/.grunt/grunt-contrib-jasmine/jasmine.css +0 -271
- package/.grunt/grunt-contrib-jasmine/jasmine.js +0 -9761
- package/.grunt/grunt-contrib-jasmine/jasmine_favicon.png +0 -0
- package/.grunt/grunt-contrib-jasmine/json2.js +0 -489
- package/.grunt/grunt-contrib-jasmine/reporter.js +0 -107
- package/coverage/coverage.json +0 -1
- package/coverage/lcov/lcov-report/base.css +0 -213
- package/coverage/lcov/lcov-report/index.html +0 -93
- package/coverage/lcov/lcov-report/js/dataBind.js.html +0 -6596
- package/coverage/lcov/lcov-report/js/index.html +0 -93
- package/coverage/lcov/lcov-report/prettify.css +0 -1
- package/coverage/lcov/lcov-report/prettify.js +0 -1
- package/coverage/lcov/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov/lcov-report/sorter.js +0 -158
- package/coverage/lcov/lcov.info +0 -1991
- package/eslintrc.json +0 -40
- package/examples/bootstrap/js/bootstrap.min.js +0 -6
- package/examples/bootstrap/js/popper.min.js +0 -5
- package/examples/bootstrap/js/searchSuggestion.js +0 -58
- package/examples/bootstrap/js/typeahead.jquery.js +0 -1538
- package/gruntfile.js +0 -92
- package/gulpfile.js +0 -32
- package/src/binder.js +0 -422
- package/src/changeBinding.js +0 -57
- package/src/config.js +0 -65
- package/src/createBindingOption.js +0 -66
- package/src/createEventBinding.js +0 -46
- package/src/eventSystem.js +0 -46
- package/src/hoverBinding.js +0 -57
- package/src/index.js +0 -26
- package/src/renderTemplate.js +0 -128
- package/src/util.js +0 -648
- package/test/specs/blurBinding.spec.js +0 -57
- package/test/specs/formBinding.spec.js +0 -292
- package/test/specs/ifBinding.spec.js +0 -169
- package/test/specs/templateBinding.spec.js +0 -117
- package/vendors/jasmine-jquery.js +0 -841
- package/vendors/jquery-3.2.1.min.js +0 -4
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
2
|
+
import {waitFor} from '@testing-library/dom';
|
|
3
|
+
|
|
4
|
+
describe('Given form-component initised', () => {
|
|
5
|
+
const getElementAttributesObj = function ($el: Element) {
|
|
6
|
+
const obj: any = {};
|
|
7
|
+
Array.from($el.attributes).forEach((attr) => {
|
|
8
|
+
if (attr.specified) {
|
|
9
|
+
obj[attr.name] = attr.value;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
return obj;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const formComponentVM = {
|
|
16
|
+
title: 'form component title',
|
|
17
|
+
description: 'form component description',
|
|
18
|
+
carName: 'saab',
|
|
19
|
+
showContent: false,
|
|
20
|
+
markAllCompleted: false,
|
|
21
|
+
gender: 'female',
|
|
22
|
+
testDate: '2017-12-25',
|
|
23
|
+
testRange: '1',
|
|
24
|
+
message: 'This is message from viewModel',
|
|
25
|
+
taskName: '',
|
|
26
|
+
showTaskNameInput: false,
|
|
27
|
+
testAttr: {
|
|
28
|
+
id: 'testId',
|
|
29
|
+
rel: 'testRel',
|
|
30
|
+
class: 'show',
|
|
31
|
+
},
|
|
32
|
+
onAddTask(_e: Event, _$element: any, newValue: any, _oldValue: any) {
|
|
33
|
+
this.taskName = newValue;
|
|
34
|
+
this.updateView();
|
|
35
|
+
},
|
|
36
|
+
onMarkAllCompleted(_e: Event, _$element: any, _newValue: any, _oldValue: any) {
|
|
37
|
+
this.markAllCompleted = true;
|
|
38
|
+
this.updateView();
|
|
39
|
+
},
|
|
40
|
+
onSelected(e: Event, $element: any, newValue: any, oldValue: any) {
|
|
41
|
+
expect(newValue).not.toBe(oldValue);
|
|
42
|
+
},
|
|
43
|
+
onGenderChanged(e: Event, $element: any, newValue: any, oldValue: any) {
|
|
44
|
+
expect(newValue).not.toBe(oldValue);
|
|
45
|
+
},
|
|
46
|
+
onTestDateChanged(e: Event, $element: any, newValue: any, oldValue: any) {
|
|
47
|
+
expect(newValue).not.toBe(oldValue);
|
|
48
|
+
},
|
|
49
|
+
onTestRangeChanged(e: Event, $element: any, newValue: any, oldValue: any) {
|
|
50
|
+
expect(newValue).not.toBe(oldValue);
|
|
51
|
+
this.updateView();
|
|
52
|
+
},
|
|
53
|
+
onTestRangeInputChange(e: Event, $element: any, newValue: any, oldValue: any) {
|
|
54
|
+
expect(newValue).not.toBe(oldValue);
|
|
55
|
+
this.updateView();
|
|
56
|
+
},
|
|
57
|
+
onMessageChanged(e: Event, $element: any, newValue: any, oldValue: any) {
|
|
58
|
+
expect(newValue).not.toBe(oldValue);
|
|
59
|
+
this.updateView();
|
|
60
|
+
},
|
|
61
|
+
onEditTask(_e: Event, _$element: any) {
|
|
62
|
+
this.showTaskNameInput = true;
|
|
63
|
+
this.updateView();
|
|
64
|
+
},
|
|
65
|
+
onFocusEditTask(e: Event, $element: any) {
|
|
66
|
+
expect($element.id).toBe('taskName');
|
|
67
|
+
},
|
|
68
|
+
onBlurEditTask(e: Event, $element: any) {
|
|
69
|
+
expect($element.id).toBe('taskName');
|
|
70
|
+
},
|
|
71
|
+
onTestFormSubmit(e: Event, _$element: any, _formData: any) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
},
|
|
74
|
+
updateView() {
|
|
75
|
+
namespace.formComponentApp.render();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const namespace: any = {};
|
|
79
|
+
|
|
80
|
+
beforeEach(async () => {
|
|
81
|
+
loadFixture('test/fixtures/formBindings.html');
|
|
82
|
+
namespace.formComponentApp = dataBind.init(document.querySelector('[data-bind-comp="form-component"]'), formComponentVM);
|
|
83
|
+
await namespace.formComponentApp.render();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
// clean up app
|
|
88
|
+
delete namespace.formComponentApp;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('Then each bond input element should updated according to viewModel', async () => {
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(document.querySelector('#test-form-title')!.textContent).toBe(formComponentVM.title);
|
|
94
|
+
expect(document.querySelector('#test-form-description')!.textContent).toBe(formComponentVM.description);
|
|
95
|
+
expect((document.querySelector('#test-form-description') as HTMLElement)!.style.display !== 'none').toBe(formComponentVM.showContent);
|
|
96
|
+
expect((document.querySelector('#toggle-all') as HTMLInputElement).checked).toBe(formComponentVM.markAllCompleted);
|
|
97
|
+
expect((document.querySelector('input[name="gender"]:checked') as HTMLInputElement).value).toBe(formComponentVM.gender);
|
|
98
|
+
expect((document.querySelector('#carName') as HTMLSelectElement).value).toBe(formComponentVM.carName);
|
|
99
|
+
expect((document.querySelector('#testDate') as HTMLInputElement).value).toBe(formComponentVM.testDate);
|
|
100
|
+
expect((document.querySelector('#testRange') as HTMLInputElement).value).toBe(formComponentVM.testRange);
|
|
101
|
+
expect(document.querySelector('#testRangeLabel')!.textContent).toBe((document.querySelector('#testRange') as HTMLInputElement).value);
|
|
102
|
+
expect((document.querySelector('#taskName') as HTMLInputElement).value).toBe(formComponentVM.taskName);
|
|
103
|
+
expect((document.querySelector('#taskName') as HTMLElement)!.style.display !== 'none').toBe(formComponentVM.showTaskNameInput);
|
|
104
|
+
}, {timeout: 500});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
it('When change #new-todo input value then viewModel should have updated', async () => {
|
|
109
|
+
const task1 = 'new test task';
|
|
110
|
+
|
|
111
|
+
// Wait for element to be ready
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
const $newTodo = document.getElementById('new-todo');
|
|
114
|
+
expect($newTodo).not.toBeNull();
|
|
115
|
+
}, {timeout: 500});
|
|
116
|
+
|
|
117
|
+
const $newTodo = document.getElementById('new-todo') as HTMLInputElement;
|
|
118
|
+
const evt = document.createEvent('HTMLEvents');
|
|
119
|
+
|
|
120
|
+
evt.initEvent('change', true, true);
|
|
121
|
+
$newTodo.value = task1;
|
|
122
|
+
$newTodo.dispatchEvent(evt);
|
|
123
|
+
|
|
124
|
+
// defer to check after async render
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(formComponentVM.taskName).toBe(task1);
|
|
127
|
+
expect((document.querySelector('#taskName') as HTMLInputElement).value).toBe(formComponentVM.taskName);
|
|
128
|
+
}, {timeout: 500});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('When #toggle-all checked then viewModel should have updated', async () => {
|
|
132
|
+
const $toggleAll = document.getElementById('toggle-all') as HTMLInputElement;
|
|
133
|
+
const evt = document.createEvent('HTMLEvents');
|
|
134
|
+
|
|
135
|
+
evt.initEvent('change', true, true);
|
|
136
|
+
$toggleAll.checked = true;
|
|
137
|
+
$toggleAll.dispatchEvent(evt);
|
|
138
|
+
|
|
139
|
+
// defer to check after async render
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(formComponentVM.markAllCompleted).toBe(true);
|
|
142
|
+
}, {timeout: 500});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('When #carName dropdwon changed then viewModel should have updated', async () => {
|
|
146
|
+
const newCarName = 'volvo';
|
|
147
|
+
const $carName = document.getElementById('carName') as HTMLSelectElement;
|
|
148
|
+
const evt = document.createEvent('HTMLEvents');
|
|
149
|
+
|
|
150
|
+
evt.initEvent('change', true, true);
|
|
151
|
+
$carName.value = newCarName;
|
|
152
|
+
$carName.dispatchEvent(evt);
|
|
153
|
+
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(formComponentVM.carName).toBe(newCarName);
|
|
156
|
+
}, {timeout: 500});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('When #radioMale changed then viewModel should have updated', async () => {
|
|
160
|
+
const newGender = 'male';
|
|
161
|
+
const radioMale = document.getElementById('radioMale') as HTMLInputElement;
|
|
162
|
+
const evt = document.createEvent('HTMLEvents');
|
|
163
|
+
|
|
164
|
+
evt.initEvent('change', true, true);
|
|
165
|
+
radioMale.checked = true;
|
|
166
|
+
radioMale.dispatchEvent(evt);
|
|
167
|
+
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(formComponentVM.gender).toBe(newGender);
|
|
170
|
+
}, {timeout: 500});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('When #testDate date input changed then viewModel should have updated', async () => {
|
|
174
|
+
const newTestDate = '2017-12-31';
|
|
175
|
+
const $testDate = document.getElementById('testDate') as HTMLInputElement;
|
|
176
|
+
const evt = document.createEvent('HTMLEvents');
|
|
177
|
+
|
|
178
|
+
evt.initEvent('change', true, true);
|
|
179
|
+
$testDate.value = newTestDate;
|
|
180
|
+
$testDate.dispatchEvent(evt);
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(formComponentVM.testDate).toBe(newTestDate);
|
|
184
|
+
}, {timeout: 500});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('When #testRange range input changed then viewModel should have updated', async () => {
|
|
188
|
+
const newTestRange = '3';
|
|
189
|
+
const $testRange = document.getElementById('testRange') as HTMLInputElement;
|
|
190
|
+
const evt = document.createEvent('HTMLEvents');
|
|
191
|
+
|
|
192
|
+
evt.initEvent('change', true, true);
|
|
193
|
+
$testRange.value = newTestRange;
|
|
194
|
+
$testRange.dispatchEvent(evt);
|
|
195
|
+
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(formComponentVM.testRange).toBe(newTestRange);
|
|
198
|
+
expect(document.querySelector('#testRangeLabel')!.textContent).toBe(newTestRange);
|
|
199
|
+
}, {timeout: 500});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('When #testRange range input trigger onInput changed then viewModel should have updated', async () => {
|
|
203
|
+
const newTestRange = '4';
|
|
204
|
+
const $testRange = document.getElementById('testRange') as HTMLInputElement;
|
|
205
|
+
const evt = document.createEvent('HTMLEvents');
|
|
206
|
+
|
|
207
|
+
evt.initEvent('input', true, true);
|
|
208
|
+
$testRange.value = newTestRange;
|
|
209
|
+
$testRange.dispatchEvent(evt);
|
|
210
|
+
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(formComponentVM.testRange).toBe(newTestRange);
|
|
213
|
+
expect(document.querySelector('#testRangeLabel')!.textContent).toBe(newTestRange);
|
|
214
|
+
}, {timeout: 500});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('When #message range input changed with xss html then viewModel data should have escaped value and updated', async () => {
|
|
218
|
+
const newMessage = '<img src=x onerror=alert(1)>';
|
|
219
|
+
const escapedMessage = '<img src=x onerror=alert(1)>';
|
|
220
|
+
const $message = document.getElementById('message') as HTMLTextAreaElement;
|
|
221
|
+
const evt = document.createEvent('HTMLEvents');
|
|
222
|
+
|
|
223
|
+
evt.initEvent('change', true, true);
|
|
224
|
+
$message.value = newMessage;
|
|
225
|
+
$message.dispatchEvent(evt);
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(formComponentVM.message).toBe(escapedMessage);
|
|
229
|
+
}, {timeout: 500});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('When #labelTaskeName label double clicked then viewModel should have updated and #taskName should show', async () => {
|
|
233
|
+
const $taskName = document.getElementById('taskName') as HTMLInputElement;
|
|
234
|
+
|
|
235
|
+
await waitFor(() => {
|
|
236
|
+
expect((document.querySelector('#taskName') as HTMLElement)!.style.display !== 'none').toBe(formComponentVM.showTaskNameInput);
|
|
237
|
+
}, {timeout: 500});
|
|
238
|
+
|
|
239
|
+
$taskName.focus();
|
|
240
|
+
$taskName.blur();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('Element with attribute binding', () => {
|
|
244
|
+
it('should display attribute from viewModel', async () => {
|
|
245
|
+
const $el = document.querySelector('[data-bind-attr="testAttr"]')!;
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
const attrObj = getElementAttributesObj($el);
|
|
248
|
+
Object.keys(formComponentVM.testAttr).forEach(k => {
|
|
249
|
+
expect(attrObj[k]).toBe((formComponentVM.testAttr as any)[k]);
|
|
250
|
+
});
|
|
251
|
+
}, {timeout: 500});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should update attribute according to viewModel', async () => {
|
|
255
|
+
const $el = document.querySelector('[data-bind-attr="testAttr"]')!;
|
|
256
|
+
// Use component.viewModel for reactive updates (reactive mode is default)
|
|
257
|
+
namespace.formComponentApp.viewModel.testAttr = {
|
|
258
|
+
id: '8888',
|
|
259
|
+
class: 'hidden',
|
|
260
|
+
} as any;
|
|
261
|
+
// No need to call updateView() - reactive mode triggers automatic render
|
|
262
|
+
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
const attrObj = getElementAttributesObj($el);
|
|
265
|
+
Object.keys(namespace.formComponentApp.viewModel.testAttr).forEach(k => {
|
|
266
|
+
expect(attrObj[k]).toBe((namespace.formComponentApp.viewModel.testAttr as any)[k]);
|
|
267
|
+
});
|
|
268
|
+
expect(attrObj.rel).toBeUndefined();
|
|
269
|
+
}, {timeout: 500});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
2
|
+
import {waitFor} from '@testing-library/dom';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('Given [data-bind-comp="if-component"] inited', () => {
|
|
6
|
+
const namespace: any = {};
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
loadFixture('test/fixtures/ifBinding.html');
|
|
10
|
+
|
|
11
|
+
namespace.viewModel = {
|
|
12
|
+
renderIntro: true,
|
|
13
|
+
heading: 'Test data-if-binding',
|
|
14
|
+
description: 'This is intro text',
|
|
15
|
+
story: {
|
|
16
|
+
title: 'Hansel and Gretel',
|
|
17
|
+
description:
|
|
18
|
+
'"Hansel and Gretel" (also known as Hansel and Grettel, Hansel and Grethel, or Little Brother and Little Sister) is a well-known fairy tale of German origin.',
|
|
19
|
+
link: 'https://www.google.com.au/search?q=Hansel+and+Gretel',
|
|
20
|
+
},
|
|
21
|
+
viewModelPropFn($data: any) {
|
|
22
|
+
return typeof $data.viewModelPropFn === 'function';
|
|
23
|
+
},
|
|
24
|
+
undefinedViewModelPropFn(_$data: any) {
|
|
25
|
+
return;
|
|
26
|
+
},
|
|
27
|
+
setStroylinkAttr(_$data: any) {
|
|
28
|
+
return {
|
|
29
|
+
href: this.story.link,
|
|
30
|
+
title: this.story.title,
|
|
31
|
+
target: '_blank',
|
|
32
|
+
rel: 'noopener noreferrer',
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
onStoryClick(e: Event, _$el: any) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
},
|
|
38
|
+
updateView(opt?: any) {
|
|
39
|
+
this.APP.render(opt);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
namespace.myIfComponent = dataBind.init(document.querySelector('[data-bind-comp="if-component"]'), namespace.viewModel);
|
|
44
|
+
|
|
45
|
+
await namespace.myIfComponent.render();
|
|
46
|
+
|
|
47
|
+
// vitest spies
|
|
48
|
+
vi.spyOn(namespace.viewModel, 'onStoryClick');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
// clean up all app/components
|
|
53
|
+
for (const prop in namespace) {
|
|
54
|
+
if (Object.prototype.hasOwnProperty.call(namespace, prop)) {
|
|
55
|
+
delete namespace[prop];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('Then [data-bind-comp="myIfComponent"] should have render', async () => {
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(document.querySelector('#intro-heading')!.textContent).toBe(namespace.viewModel.heading);
|
|
63
|
+
expect(document.querySelector('#intro-description')!.textContent).toBe(namespace.viewModel.description);
|
|
64
|
+
}, {timeout: 500});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('Then render if-binding elements with comment tag wrap around', async () => {
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
const introOpenCommentWrap = document.getElementById('intro')!.previousSibling;
|
|
70
|
+
const introCloseCommentWrap = document.getElementById('intro')!.nextSibling;
|
|
71
|
+
|
|
72
|
+
expect(introOpenCommentWrap!.nodeType).toBe(8);
|
|
73
|
+
expect(introCloseCommentWrap!.nodeType).toBe(8);
|
|
74
|
+
expect(introOpenCommentWrap!.textContent).toContain('data-if');
|
|
75
|
+
expect(introCloseCommentWrap!.textContent).toContain('data-if');
|
|
76
|
+
}, {timeout: 500});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
it('should not render #story ', async () => {
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(document.querySelector('#story')).toBe(null);
|
|
83
|
+
}, {timeout: 500});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
it('should not render #testPropFn ', async () => {
|
|
88
|
+
await waitFor(() => {
|
|
89
|
+
expect(document.getElementById('testPropFn')).not.toBe(null);
|
|
90
|
+
}, {timeout: 500});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
it('should not render #testUnDefiniedProp ', async () => {
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(document.getElementById('testUnDefiniedProp')).toBe(null);
|
|
97
|
+
}, {timeout: 500});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
it('should render inverse negated boolean block', async () => {
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(document.getElementById('NotTestUnDefiniedProp')).not.toBe(null);
|
|
104
|
+
}, {timeout: 500});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('When update viewModel renderIntro to false', () => {
|
|
108
|
+
it('should render story and remove intro', async () => {
|
|
109
|
+
namespace.viewModel.renderIntro = false;
|
|
110
|
+
await namespace.myIfComponent.render();
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(document.querySelector('#story')).not.toBe(null);
|
|
114
|
+
expect(document.querySelector('#intro')).toBe(null);
|
|
115
|
+
expect(document.querySelector('#storyIntroHeading')!.textContent).toBe(namespace.viewModel.story.title);
|
|
116
|
+
expect(document.querySelector('#storyDescription')!.textContent).toBe(namespace.viewModel.story.description);
|
|
117
|
+
expect(document.querySelector('#storyLink')!.getAttribute('href')).toBe(namespace.viewModel.story.link);
|
|
118
|
+
expect(document.querySelector('#storyLink')!.getAttribute('title')).toBe(namespace.viewModel.story.title);
|
|
119
|
+
expect(document.querySelector('#storyLink')!.getAttribute('target')).toBe('_blank');
|
|
120
|
+
expect(document.querySelector('#storyLink')!.getAttribute('rel')).toBe('noopener noreferrer');
|
|
121
|
+
|
|
122
|
+
const evt = document.createEvent('HTMLEvents');
|
|
123
|
+
evt.initEvent('click', true, true);
|
|
124
|
+
|
|
125
|
+
const $searchInput = document.getElementById('storyLink')!;
|
|
126
|
+
$searchInput.dispatchEvent(evt);
|
|
127
|
+
|
|
128
|
+
expect(namespace.viewModel.onStoryClick).toHaveBeenCalled();
|
|
129
|
+
namespace.viewModel.onStoryClick.mockClear();
|
|
130
|
+
}, {timeout: 500});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('When update viewModel renderIntro to true', () => {
|
|
135
|
+
it('should render intro and remove story', async () => {
|
|
136
|
+
namespace.viewModel.renderIntro = true;
|
|
137
|
+
await namespace.myIfComponent.render();
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(document.querySelector('#story')).toBe(null);
|
|
141
|
+
expect(document.querySelector('#intro')).not.toBe(null);
|
|
142
|
+
}, {timeout: 500});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('When update viewModel renderIntro to false again', () => {
|
|
147
|
+
it('should render story and event handler rebind', async () => {
|
|
148
|
+
namespace.viewModel.renderIntro = false;
|
|
149
|
+
await namespace.myIfComponent.render();
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
const $storyLink = document.getElementById('storyLink')!;
|
|
153
|
+
const evt = document.createEvent('HTMLEvents');
|
|
154
|
+
evt.initEvent('click', true, true);
|
|
155
|
+
|
|
156
|
+
expect(document.querySelector('#story')).not.toBe(null);
|
|
157
|
+
expect(document.querySelector('#intro')).toBe(null);
|
|
158
|
+
|
|
159
|
+
$storyLink.dispatchEvent(evt);
|
|
160
|
+
expect(namespace.viewModel.onStoryClick).toHaveBeenCalled();
|
|
161
|
+
namespace.viewModel.onStoryClick.mockClear();
|
|
162
|
+
}, {timeout: 500});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
describe
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
expect(
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
});
|
|
1
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
2
|
+
import {waitFor} from '@testing-library/dom';
|
|
3
|
+
|
|
4
|
+
describe('When nested data-bind-comp initised', () => {
|
|
5
|
+
const namespace: any = {};
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
loadFixture('test/fixtures/nestedComponents.html');
|
|
9
|
+
|
|
10
|
+
namespace.parentComponentVM = {
|
|
11
|
+
title: 'parent component title',
|
|
12
|
+
description: 'parent component description',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
namespace.childComponentVM = {
|
|
16
|
+
title: 'child component title',
|
|
17
|
+
description: 'child component description',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
namespace.grandChildComponentVM = {
|
|
21
|
+
title: 'grand child component title',
|
|
22
|
+
description: 'grand child component description',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
namespace.slibingChildComponentVM = {
|
|
26
|
+
title: 'slibing child component title',
|
|
27
|
+
description: 'slibing child component description',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const parentComponent = dataBind.init(document.querySelector('[data-bind-comp="parent-component"]'), namespace.parentComponentVM);
|
|
31
|
+
const childComponent = dataBind.init(document.querySelector('[data-bind-comp="child-component"]'), namespace.childComponentVM);
|
|
32
|
+
const grandChildComponent = dataBind.init(
|
|
33
|
+
document.querySelector('[data-bind-comp="grand-child-component"]'),
|
|
34
|
+
namespace.grandChildComponentVM,
|
|
35
|
+
);
|
|
36
|
+
const slibingChildComponent = dataBind.init(
|
|
37
|
+
document.querySelector('[data-bind-comp="slibing-child-component"]'),
|
|
38
|
+
namespace.slibingChildComponentVM,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
parentComponent.render();
|
|
42
|
+
childComponent.render();
|
|
43
|
+
grandChildComponent.render();
|
|
44
|
+
slibingChildComponent.render();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// clean up all app/components
|
|
49
|
+
for (const prop in namespace) {
|
|
50
|
+
if (Object.prototype.hasOwnProperty.call(namespace, prop)) {
|
|
51
|
+
delete namespace[prop];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('Then #parent-component-title and #parent-component-description should render according parentComponentVM', async () => {
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(document.querySelector('#parent-component-title')!.textContent).toBe(namespace.parentComponentVM.title);
|
|
59
|
+
expect(document.querySelector('#parent-component-description')!.textContent).toBe(namespace.parentComponentVM.description);
|
|
60
|
+
}, {timeout: 500});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
it('Then #child-component-title and #child-component-description should render according childComponentVM', async () => {
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(document.querySelector('#child-component-title')!.textContent).toBe(namespace.childComponentVM.title);
|
|
67
|
+
expect(document.querySelector('#child-component-description')!.textContent).toBe(namespace.childComponentVM.description);
|
|
68
|
+
}, {timeout: 500});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
it('Then #grand-child-component-title and #grand-child-component-description should render according grandChildComponentVM', async () => {
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(document.querySelector('#grand-child-component-title')!.textContent).toBe(namespace.grandChildComponentVM.title);
|
|
75
|
+
expect(document.querySelector('#grand-child-component-description')!.textContent).toBe(namespace.grandChildComponentVM.description);
|
|
76
|
+
}, {timeout: 500});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
it('Then #slibing-child-component-title and #slibing-child-component-description should render according slibingChildComponentVM', async () => {
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(document.querySelector('#slibing-child-component-title')!.textContent).toBe(namespace.slibingChildComponentVM.title);
|
|
83
|
+
expect(document.querySelector('#slibing-child-component-description')!.textContent).toBe(
|
|
84
|
+
namespace.slibingChildComponentVM.description,
|
|
85
|
+
);
|
|
86
|
+
}, {timeout: 500});
|
|
87
|
+
});
|
|
88
|
+
});
|