@plannotator/web-highlighter 0.8.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/.cursor/environment.json +6 -0
- package/.eslintrc.js +250 -0
- package/.prettierrc +9 -0
- package/.travis.yml +17 -0
- package/CHANGELOG.md +220 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/README.zh_CN.md +367 -0
- package/config/base.config.js +25 -0
- package/config/base.example.config.js +38 -0
- package/config/paths.js +22 -0
- package/config/server.config.js +17 -0
- package/config/webpack.config.dev.js +18 -0
- package/config/webpack.config.example.js +20 -0
- package/config/webpack.config.prod.js +28 -0
- package/dist/data/cache.d.ts +13 -0
- package/dist/index.d.ts +58 -0
- package/dist/model/range/dom.d.ts +6 -0
- package/dist/model/range/index.d.ts +20 -0
- package/dist/model/range/selection.d.ts +14 -0
- package/dist/model/source/dom.d.ts +23 -0
- package/dist/model/source/index.d.ts +18 -0
- package/dist/painter/dom.d.ts +22 -0
- package/dist/painter/index.d.ts +19 -0
- package/dist/painter/style.d.ts +1 -0
- package/dist/types/index.d.ts +102 -0
- package/dist/util/camel.d.ts +5 -0
- package/dist/util/const.d.ts +41 -0
- package/dist/util/deferred.d.ts +9 -0
- package/dist/util/dom.d.ts +32 -0
- package/dist/util/event.emitter.d.ts +13 -0
- package/dist/util/hook.d.ts +15 -0
- package/dist/util/interaction.d.ts +6 -0
- package/dist/util/is.mobile.d.ts +5 -0
- package/dist/util/tool.d.ts +4 -0
- package/dist/util/uuid.d.ts +4 -0
- package/dist/web-highlighter.min.js +3 -0
- package/dist/web-highlighter.min.js.map +1 -0
- package/docs/ADVANCE.md +113 -0
- package/docs/ADVANCE.zh_CN.md +111 -0
- package/docs/img/create-flow.jpg +0 -0
- package/docs/img/create-flow.zh_CN.jpg +0 -0
- package/docs/img/logo.png +0 -0
- package/docs/img/remove-flow.jpg +0 -0
- package/docs/img/remove-flow.zh_CN.jpg +0 -0
- package/docs/img/sample.gif +0 -0
- package/example/index.css +2 -0
- package/example/index.js +214 -0
- package/example/local.store.js +72 -0
- package/example/my.css +119 -0
- package/example/tpl.html +59 -0
- package/package.json +103 -0
- package/script/build.js +17 -0
- package/script/convet-md.js +25 -0
- package/script/dev.js +22 -0
- package/src/data/cache.ts +57 -0
- package/src/index.ts +285 -0
- package/src/model/range/dom.ts +94 -0
- package/src/model/range/index.ts +88 -0
- package/src/model/range/selection.ts +55 -0
- package/src/model/source/dom.ts +66 -0
- package/src/model/source/index.ts +54 -0
- package/src/painter/dom.ts +345 -0
- package/src/painter/index.ts +199 -0
- package/src/painter/style.ts +21 -0
- package/src/types/index.ts +118 -0
- package/src/util/camel.ts +6 -0
- package/src/util/const.ts +54 -0
- package/src/util/deferred.ts +37 -0
- package/src/util/dom.ts +155 -0
- package/src/util/event.emitter.ts +45 -0
- package/src/util/hook.ts +52 -0
- package/src/util/interaction.ts +20 -0
- package/src/util/is.mobile.ts +7 -0
- package/src/util/tool.ts +14 -0
- package/src/util/uuid.ts +10 -0
- package/test/api.spec.ts +555 -0
- package/test/event.spec.ts +284 -0
- package/test/fixtures/broken.json +32 -0
- package/test/fixtures/index.html +11 -0
- package/test/fixtures/source.json +47 -0
- package/test/hook.spec.ts +244 -0
- package/test/integrate.spec.ts +48 -0
- package/test/mobile.spec.ts +87 -0
- package/test/option.spec.ts +212 -0
- package/test/util.spec.ts +244 -0
- package/test-newlines.html +226 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import jsdomGlobal from 'jsdom-global';
|
|
5
|
+
import jsdom from 'jsdom';
|
|
6
|
+
import Highlighter from '../src/index';
|
|
7
|
+
import { getDefaultOptions } from '../src/util/const';
|
|
8
|
+
import getInteraction from '../src/util/interaction';
|
|
9
|
+
|
|
10
|
+
describe('Highlighter on mobiles', function () {
|
|
11
|
+
this.timeout(50000);
|
|
12
|
+
|
|
13
|
+
let highlighter: Highlighter;
|
|
14
|
+
let cleanup;
|
|
15
|
+
let wrapSelector: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const html = readFileSync(resolve(__dirname, 'fixtures', 'index.html'), 'utf-8');
|
|
19
|
+
cleanup = jsdomGlobal('', {
|
|
20
|
+
resources: new jsdom.ResourceLoader({
|
|
21
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
document.body.innerHTML = html;
|
|
25
|
+
highlighter = new Highlighter();
|
|
26
|
+
wrapSelector = getDefaultOptions().wrapTag;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('#run', () => {
|
|
30
|
+
it('should highlight automatically after the user\'s interaction', () => {
|
|
31
|
+
expect(getInteraction().PointerEnd).to.be.equal('touchend');
|
|
32
|
+
|
|
33
|
+
highlighter.run();
|
|
34
|
+
|
|
35
|
+
const range = document.createRange();
|
|
36
|
+
const $p = document.querySelectorAll('p')[0];
|
|
37
|
+
range.setStart($p.childNodes[0], 0);
|
|
38
|
+
range.setEnd($p.childNodes[0], 17);
|
|
39
|
+
window.getSelection().addRange(range);
|
|
40
|
+
|
|
41
|
+
const content = range.toString();
|
|
42
|
+
const e = new TouchEvent('touchend', {
|
|
43
|
+
view: window,
|
|
44
|
+
bubbles: true,
|
|
45
|
+
cancelable: true
|
|
46
|
+
});
|
|
47
|
+
document.body.dispatchEvent(e);
|
|
48
|
+
|
|
49
|
+
const $w = document.querySelectorAll('p')[0].querySelector(wrapSelector);
|
|
50
|
+
expect($w.textContent).to.be.equal(content);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('#dispose', () => {
|
|
55
|
+
it('should not highlight automatically after calling .dispose', () => {
|
|
56
|
+
highlighter.run();
|
|
57
|
+
highlighter.dispose();
|
|
58
|
+
|
|
59
|
+
const range = document.createRange();
|
|
60
|
+
const $p = document.querySelectorAll('p')[0];
|
|
61
|
+
range.setStart($p.childNodes[0], 0);
|
|
62
|
+
range.setEnd($p.childNodes[0], 17);
|
|
63
|
+
window.getSelection().addRange(range);
|
|
64
|
+
|
|
65
|
+
const e = new TouchEvent('touchend', {
|
|
66
|
+
view: window,
|
|
67
|
+
bubbles: true,
|
|
68
|
+
cancelable: true
|
|
69
|
+
});
|
|
70
|
+
document.body.dispatchEvent(e);
|
|
71
|
+
|
|
72
|
+
const $w = document.querySelectorAll('p')[0].querySelectorAll(wrapSelector);
|
|
73
|
+
expect($w.length).to.be.equal(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should remove all highlights after calling .dispose', () => {
|
|
77
|
+
highlighter.dispose();
|
|
78
|
+
|
|
79
|
+
const $w = document.querySelectorAll(wrapSelector);
|
|
80
|
+
expect($w.length).to.be.equal(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
cleanup();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import jsdomGlobal from 'jsdom-global';
|
|
5
|
+
import Highlighter from '../src/index';
|
|
6
|
+
import { getDefaultOptions } from '../src/util/const';
|
|
7
|
+
|
|
8
|
+
describe('Highlighter Options', function () {
|
|
9
|
+
this.timeout(50000);
|
|
10
|
+
|
|
11
|
+
let cleanup: Function;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
const html = readFileSync(resolve(__dirname, 'fixtures', 'index.html'), 'utf-8');
|
|
15
|
+
cleanup = jsdomGlobal();
|
|
16
|
+
document.body.innerHTML = html;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('#$root', () => {
|
|
20
|
+
it('should highlight text inside $root', () => {
|
|
21
|
+
const $root = document.querySelectorAll('p')[0];
|
|
22
|
+
const highlighter = new Highlighter({ $root });
|
|
23
|
+
highlighter.removeAll();
|
|
24
|
+
|
|
25
|
+
expect(highlighter.getDoms()).lengthOf(0);
|
|
26
|
+
|
|
27
|
+
const range = document.createRange();
|
|
28
|
+
range.setStart($root.childNodes[0], 0);
|
|
29
|
+
range.setEnd($root.childNodes[0], 17);
|
|
30
|
+
highlighter.fromRange(range);
|
|
31
|
+
|
|
32
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should not highlight text outside $root', () => {
|
|
36
|
+
const $root = document.querySelectorAll('p')[0];
|
|
37
|
+
const highlighter = new Highlighter({ $root });
|
|
38
|
+
highlighter.removeAll();
|
|
39
|
+
expect(highlighter.getDoms()).lengthOf(0);
|
|
40
|
+
|
|
41
|
+
const range = document.createRange();
|
|
42
|
+
const $p = document.querySelectorAll('p')[1];
|
|
43
|
+
range.setStart($p.childNodes[0], 0);
|
|
44
|
+
range.setEnd($p.childNodes[0], 17);
|
|
45
|
+
highlighter.fromRange(range);
|
|
46
|
+
|
|
47
|
+
expect(highlighter.getDoms()).lengthOf(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('#exceptSelectors', () => {
|
|
52
|
+
|
|
53
|
+
it('should skip nodes because of the tag selector filters', () => {
|
|
54
|
+
const highlighter = new Highlighter({
|
|
55
|
+
exceptSelectors: ['p']
|
|
56
|
+
});
|
|
57
|
+
highlighter.removeAll();
|
|
58
|
+
|
|
59
|
+
const $p = document.querySelectorAll('p')[0];
|
|
60
|
+
const range = document.createRange();
|
|
61
|
+
range.setStart($p.childNodes[0], 0);
|
|
62
|
+
range.setEnd($p.childNodes[0], 17);
|
|
63
|
+
highlighter.fromRange(range);
|
|
64
|
+
|
|
65
|
+
expect(highlighter.getDoms()).lengthOf(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should skip nodes because of the className selector filters', () => {
|
|
69
|
+
const highlighter = new Highlighter({
|
|
70
|
+
exceptSelectors: ['.first-p']
|
|
71
|
+
});
|
|
72
|
+
highlighter.removeAll();
|
|
73
|
+
|
|
74
|
+
const $p = document.querySelectorAll('p')[0];
|
|
75
|
+
const range = document.createRange();
|
|
76
|
+
range.setStart($p.childNodes[0], 0);
|
|
77
|
+
range.setEnd($p.childNodes[0], 17);
|
|
78
|
+
highlighter.fromRange(range);
|
|
79
|
+
|
|
80
|
+
expect(highlighter.getDoms()).lengthOf(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should skip nodes because of the id selector filters', () => {
|
|
84
|
+
const highlighter = new Highlighter({
|
|
85
|
+
exceptSelectors: ['#js-first-p']
|
|
86
|
+
});
|
|
87
|
+
highlighter.removeAll();
|
|
88
|
+
|
|
89
|
+
const $p = document.querySelectorAll('p')[0];
|
|
90
|
+
const range = document.createRange();
|
|
91
|
+
range.setStart($p.childNodes[0], 0);
|
|
92
|
+
range.setEnd($p.childNodes[0], 17);
|
|
93
|
+
highlighter.fromRange(range);
|
|
94
|
+
|
|
95
|
+
expect(highlighter.getDoms()).lengthOf(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should not skip when no filter matches', () => {
|
|
99
|
+
const highlighter = new Highlighter({
|
|
100
|
+
exceptSelectors: ['.no-match']
|
|
101
|
+
});
|
|
102
|
+
highlighter.removeAll();
|
|
103
|
+
|
|
104
|
+
const $p = document.querySelectorAll('p')[0];
|
|
105
|
+
const range = document.createRange();
|
|
106
|
+
range.setStart($p.childNodes[0], 0);
|
|
107
|
+
range.setEnd($p.childNodes[0], 17);
|
|
108
|
+
highlighter.fromRange(range);
|
|
109
|
+
|
|
110
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('#wrapTag', () => {
|
|
115
|
+
it('should use default tag for wrapping node when there\'s no config', () => {
|
|
116
|
+
const wrapTag = getDefaultOptions().wrapTag;
|
|
117
|
+
const highlighter = new Highlighter();
|
|
118
|
+
highlighter.removeAll();
|
|
119
|
+
|
|
120
|
+
const $p = document.querySelectorAll('p')[0];
|
|
121
|
+
const range = document.createRange();
|
|
122
|
+
range.setStart($p.childNodes[0], 0);
|
|
123
|
+
range.setEnd($p.childNodes[0], 17);
|
|
124
|
+
highlighter.fromRange(range);
|
|
125
|
+
|
|
126
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
127
|
+
const useCorrectTag = highlighter
|
|
128
|
+
.getDoms()
|
|
129
|
+
.every(n => n.tagName.toUpperCase() === wrapTag.toUpperCase());
|
|
130
|
+
expect(useCorrectTag).to.be.true;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should use customized tag for wrapping nodes', () => {
|
|
134
|
+
const wrapTag = getDefaultOptions().wrapTag === 'b' ? 'i' : 'b';
|
|
135
|
+
const highlighter = new Highlighter({ wrapTag });
|
|
136
|
+
highlighter.removeAll();
|
|
137
|
+
|
|
138
|
+
const $p = document.querySelectorAll('p')[0];
|
|
139
|
+
const range = document.createRange();
|
|
140
|
+
range.setStart($p.childNodes[0], 0);
|
|
141
|
+
range.setEnd($p.childNodes[0], 17);
|
|
142
|
+
highlighter.fromRange(range);
|
|
143
|
+
|
|
144
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
145
|
+
const useCorrectTag = highlighter
|
|
146
|
+
.getDoms()
|
|
147
|
+
.every(n => n.tagName.toUpperCase() === wrapTag.toUpperCase());
|
|
148
|
+
expect(useCorrectTag).to.be.true;
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('#style.className', () => {
|
|
153
|
+
it('should use default className for wrapping nodes when there\'s no config', () => {
|
|
154
|
+
const defaultClassName = getDefaultOptions().style.className;
|
|
155
|
+
const highlighter = new Highlighter();
|
|
156
|
+
highlighter.removeAll();
|
|
157
|
+
|
|
158
|
+
const $p = document.querySelectorAll('p')[0];
|
|
159
|
+
const range = document.createRange();
|
|
160
|
+
range.setStart($p.childNodes[0], 0);
|
|
161
|
+
range.setEnd($p.childNodes[0], 17);
|
|
162
|
+
highlighter.fromRange(range);
|
|
163
|
+
|
|
164
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
165
|
+
expect(highlighter.getDoms().every(n => n.classList.contains(defaultClassName))).to.be.true;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should use customized className for wrapping nodes', () => {
|
|
169
|
+
const className = 'test-class-config';
|
|
170
|
+
const defaultClassName = getDefaultOptions().style.className;
|
|
171
|
+
const highlighter = new Highlighter({
|
|
172
|
+
style: { className }
|
|
173
|
+
});
|
|
174
|
+
highlighter.removeAll();
|
|
175
|
+
|
|
176
|
+
const $p = document.querySelectorAll('p')[0];
|
|
177
|
+
const range = document.createRange();
|
|
178
|
+
range.setStart($p.childNodes[0], 0);
|
|
179
|
+
range.setEnd($p.childNodes[0], 17);
|
|
180
|
+
highlighter.fromRange(range);
|
|
181
|
+
|
|
182
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
183
|
+
expect(highlighter.getDoms().every(n => n.classList.contains(className))).to.be.true;
|
|
184
|
+
expect(highlighter.getDoms().some(n => n.classList.contains(defaultClassName))).to.be.false;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should use support an className array for wrapping nodes', () => {
|
|
188
|
+
const className = ['test-class-config-1', 'test-class-config-2'];
|
|
189
|
+
const defaultClassName = getDefaultOptions().style.className;
|
|
190
|
+
const highlighter = new Highlighter({
|
|
191
|
+
style: { className }
|
|
192
|
+
});
|
|
193
|
+
highlighter.removeAll();
|
|
194
|
+
|
|
195
|
+
const $p = document.querySelectorAll('p')[0];
|
|
196
|
+
const range = document.createRange();
|
|
197
|
+
range.setStart($p.childNodes[0], 0);
|
|
198
|
+
range.setEnd($p.childNodes[0], 17);
|
|
199
|
+
highlighter.fromRange(range);
|
|
200
|
+
|
|
201
|
+
expect(highlighter.getDoms()).lengthOf.gt(0);
|
|
202
|
+
expect(highlighter.getDoms().every(n =>
|
|
203
|
+
n.classList.contains(className[0]) && n.classList.contains(className[1])
|
|
204
|
+
)).to.be.true;
|
|
205
|
+
expect(highlighter.getDoms().some(n => n.classList.contains(defaultClassName))).to.be.false;
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
afterEach(() => {
|
|
210
|
+
cleanup();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import jsdomGlobal from 'jsdom-global';
|
|
3
|
+
import HighlightSource from '../src/model/source/index';
|
|
4
|
+
import sources from './fixtures/source.json';
|
|
5
|
+
import Cache from '../src/data/cache';
|
|
6
|
+
import Hook from '../src/util/hook';
|
|
7
|
+
import EventEmitter from '../src/util/event.emitter';
|
|
8
|
+
import { initDefaultStylesheet } from '../src/painter/style';
|
|
9
|
+
import { addEventListener } from '../src/util/dom';
|
|
10
|
+
import sinon from 'sinon';
|
|
11
|
+
|
|
12
|
+
describe('Else Utils', function () {
|
|
13
|
+
this.timeout(50000);
|
|
14
|
+
|
|
15
|
+
describe('Cache', () => {
|
|
16
|
+
let cache: Cache;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
cache = new Cache();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should work correctly when saving source', () => {
|
|
23
|
+
const s = sources[0];
|
|
24
|
+
const highlightSource = new HighlightSource(s.startMeta, s.endMeta, s.text, s.id);
|
|
25
|
+
cache.save(highlightSource);
|
|
26
|
+
expect(cache.getAll()).lengthOf(1);
|
|
27
|
+
expect(cache.getAll()[0]).to.be.equal(highlightSource);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should throw error when set data', () => {
|
|
31
|
+
const s = sources[0];
|
|
32
|
+
const highlightSource = new HighlightSource(s.startMeta, s.endMeta, s.text, s.id);
|
|
33
|
+
cache.save(highlightSource);
|
|
34
|
+
expect(() => cache.data = [highlightSource]).to.throw();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should be the same when using .data and .getAll', () => {
|
|
38
|
+
const s = sources[0];
|
|
39
|
+
const highlightSource = new HighlightSource(s.startMeta, s.endMeta, s.text, s.id);
|
|
40
|
+
cache.save(highlightSource);
|
|
41
|
+
expect(cache.getAll()).lengthOf(cache.data.length);
|
|
42
|
+
expect(cache.getAll().every(c => cache.data.indexOf(c) > -1)).to.be.true;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should support an array as the argument', () => {
|
|
46
|
+
const highlightSources = sources.map(s => new HighlightSource(s.startMeta, s.endMeta, s.text, s.id));
|
|
47
|
+
cache.save(highlightSources);
|
|
48
|
+
expect(cache.getAll()).lengthOf(highlightSources.length);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return the correct data by .get', () => {
|
|
52
|
+
const s = sources[0];
|
|
53
|
+
const highlightSource = new HighlightSource(s.startMeta, s.endMeta, s.text, s.id);
|
|
54
|
+
cache.save(highlightSource);
|
|
55
|
+
expect(cache.get(s.id)).to.be.equal(highlightSource);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Style', () => {
|
|
60
|
+
it('should not generate duplicate stylesheet', () => {
|
|
61
|
+
const cleanup = jsdomGlobal();
|
|
62
|
+
expect(document.querySelectorAll('style')).lengthOf(0);
|
|
63
|
+
initDefaultStylesheet();
|
|
64
|
+
expect(document.querySelectorAll('style')).lengthOf(1);
|
|
65
|
+
initDefaultStylesheet();
|
|
66
|
+
expect(document.querySelectorAll('style')).lengthOf(1);
|
|
67
|
+
cleanup();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Hook', () => {
|
|
72
|
+
it('should have the correct name', () => {
|
|
73
|
+
const hook = new Hook('test');
|
|
74
|
+
expect(hook.name).to.be.equal('test');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should be empty before tapping anything', () => {
|
|
78
|
+
const hook = new Hook();
|
|
79
|
+
expect(hook.isEmpty()).to.be.true;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should call only once for a tapped function', () => {
|
|
83
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
84
|
+
const spy2: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
85
|
+
const hook = new Hook();
|
|
86
|
+
hook.tap(spy);
|
|
87
|
+
hook.tap(spy2);
|
|
88
|
+
hook.call();
|
|
89
|
+
|
|
90
|
+
expect(spy.calledOnce).to.be.true;
|
|
91
|
+
expect(spy2.calledOnce).to.be.true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should call tapped functions in the correct order', () => {
|
|
95
|
+
const seq = [];
|
|
96
|
+
const first = () => seq.push(1);
|
|
97
|
+
const second = () => seq.push(2);
|
|
98
|
+
const third = () => seq.push(3);
|
|
99
|
+
const hook = new Hook();
|
|
100
|
+
hook.tap(first);
|
|
101
|
+
hook.tap(second);
|
|
102
|
+
hook.tap(third);
|
|
103
|
+
hook.call();
|
|
104
|
+
|
|
105
|
+
expect(seq).to.deep.equal([1, 2, 3]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return a valid removing function when calling .tap', () => {
|
|
109
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
110
|
+
const hook = new Hook();
|
|
111
|
+
const remove = hook.tap(spy);
|
|
112
|
+
remove();
|
|
113
|
+
hook.call();
|
|
114
|
+
|
|
115
|
+
expect(hook.isEmpty()).to.be.true;
|
|
116
|
+
expect(spy.callCount).to.be.equal(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should not tap the same function twice', () => {
|
|
120
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
121
|
+
const hook = new Hook();
|
|
122
|
+
hook.tap(spy);
|
|
123
|
+
hook.tap(spy);
|
|
124
|
+
hook.call();
|
|
125
|
+
|
|
126
|
+
expect(spy.calledOnce).to.be.true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should remove the tapped functions by calling .remove', () => {
|
|
130
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
131
|
+
const hook = new Hook();
|
|
132
|
+
hook.tap(spy);
|
|
133
|
+
hook.remove(spy);
|
|
134
|
+
hook.call();
|
|
135
|
+
|
|
136
|
+
expect(hook.isEmpty()).to.be.true;
|
|
137
|
+
expect(spy.callCount).to.be.equal(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should get the correct parameters when calling .call', () => {
|
|
141
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
142
|
+
const spy2: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
143
|
+
const hook = new Hook();
|
|
144
|
+
const p1 = 'test';
|
|
145
|
+
const p2 = [1];
|
|
146
|
+
hook.tap(spy);
|
|
147
|
+
hook.tap(spy2);
|
|
148
|
+
hook.call(p1, p2);
|
|
149
|
+
|
|
150
|
+
expect(spy.args[0][0]).to.be.equal(p1);
|
|
151
|
+
expect(spy2.args[0][0]).to.be.equal(p1);
|
|
152
|
+
expect(spy.args[0][1]).to.be.equal(p2);
|
|
153
|
+
expect(spy2.args[0][1]).to.be.equal(p2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should use the last tapped function\s return as the returned value', () => {
|
|
157
|
+
const hook = new Hook();
|
|
158
|
+
hook.tap(() => 1);
|
|
159
|
+
hook.tap(() => 2);
|
|
160
|
+
hook.tap(() => 3);
|
|
161
|
+
expect(hook.call()).to.be.equal(3);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should not throw error when removing a non-exist function', () => {
|
|
165
|
+
const hook = new Hook();
|
|
166
|
+
expect(() => hook.remove(() => {})).not.to.be.throw();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('EventEmitter', () => {
|
|
171
|
+
it('should call the callback when event emit', () => {
|
|
172
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
173
|
+
const event = new EventEmitter();
|
|
174
|
+
event.on('test', spy);
|
|
175
|
+
event.emit('test');
|
|
176
|
+
|
|
177
|
+
expect(spy.calledOnce).to.be.true;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should not call the callback when another event emit', () => {
|
|
181
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
182
|
+
const event = new EventEmitter();
|
|
183
|
+
event.on('test', spy);
|
|
184
|
+
event.emit('test-2');
|
|
185
|
+
|
|
186
|
+
expect(spy.callCount).to.be.equal(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should not call the callback after calling .off', () => {
|
|
190
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
191
|
+
const event = new EventEmitter();
|
|
192
|
+
event.on('test', spy);
|
|
193
|
+
event.off('test', spy);
|
|
194
|
+
event.emit('test');
|
|
195
|
+
|
|
196
|
+
expect(spy.callCount).to.be.equal(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should not have effects when event name is not exist', () => {
|
|
200
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
201
|
+
const event = new EventEmitter();
|
|
202
|
+
event.on('test', spy);
|
|
203
|
+
event.off('test-1', spy);
|
|
204
|
+
event.emit('test');
|
|
205
|
+
|
|
206
|
+
expect(spy.calledOnce).to.be.true;
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('DOM addEventListener', () => {
|
|
211
|
+
it('should add listener correctly', () => {
|
|
212
|
+
const cleanup = jsdomGlobal('<button>test</button>');
|
|
213
|
+
|
|
214
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
215
|
+
const dom = document.querySelector('button');
|
|
216
|
+
addEventListener(dom, 'click', spy);
|
|
217
|
+
dom.dispatchEvent(new MouseEvent('click', {
|
|
218
|
+
view: window,
|
|
219
|
+
bubbles: true,
|
|
220
|
+
cancelable: true
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
expect(spy.calledOnce).to.be.true;
|
|
224
|
+
cleanup();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should remove listener correctly', () => {
|
|
228
|
+
const cleanup = jsdomGlobal('<button>test</button>');
|
|
229
|
+
|
|
230
|
+
const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
|
|
231
|
+
const dom = document.querySelector('button');
|
|
232
|
+
const remove = addEventListener(dom, 'click', spy);
|
|
233
|
+
remove();
|
|
234
|
+
dom.dispatchEvent(new MouseEvent('click', {
|
|
235
|
+
view: window,
|
|
236
|
+
bubbles: true,
|
|
237
|
+
cancelable: true
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
expect(spy.callCount).to.be.equal(0);
|
|
241
|
+
cleanup();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|