@esportsplus/template 0.16.13 → 0.16.15
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/bench/runtime.bench.ts +207 -0
- package/build/attributes.js +4 -1
- package/build/slot/array.js +50 -4
- package/build/slot/render.js +1 -2
- package/build/utilities.d.ts +2 -1
- package/build/utilities.js +2 -1
- package/package.json +5 -5
- package/src/attributes.ts +4 -1
- package/src/slot/array.ts +76 -4
- package/src/slot/render.ts +1 -4
- package/src/utilities.ts +3 -1
- package/storage/feature-research-2026-03-24.md +475 -0
- package/test-output.txt +0 -0
- package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
- package/tests/compiler/codegen.ts +292 -0
- package/tests/compiler/integration.ts +252 -0
- package/tests/compiler/ts-parser.ts +160 -0
- package/{test/constants.test.ts → tests/constants.ts} +5 -1
- package/tests/event/onconnect.ts +147 -0
- package/tests/event/onresize.ts +187 -0
- package/tests/event/ontick.ts +273 -0
- package/{test/slot/array.test.ts → tests/slot/array.ts} +274 -0
- package/vitest.bench.config.ts +18 -0
- package/vitest.config.ts +1 -1
- package/storage/compiler-architecture-2026-01-13.md +0 -420
- /package/{test → examples}/index.ts +0 -0
- /package/{test → examples}/vite.config.ts +0 -0
- /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
- /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
- /package/{test → tests}/dist/test.js +0 -0
- /package/{test → tests}/dist/test.js.map +0 -0
- /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
- /package/{test/html.test.ts → tests/html.ts} +0 -0
- /package/{test/render.test.ts → tests/render.ts} +0 -0
- /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
- /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
- /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
- /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
- /package/{test/svg.test.ts → tests/svg.ts} +0 -0
- /package/{test/utilities.test.ts → tests/utilities.ts} +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { extractTemplateParts, findHtmlTemplates, findReactiveCalls } from '../../src/compiler/ts-parser';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function createSourceFile(code: string): ts.SourceFile {
|
|
7
|
+
return ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
describe('compiler/ts-parser', () => {
|
|
12
|
+
describe('extractTemplateParts', () => {
|
|
13
|
+
it('NoSubstitutionTemplateLiteral → single literal, empty expressions', () => {
|
|
14
|
+
let sourceFile = createSourceFile('let x = `hello world`;');
|
|
15
|
+
let statement = sourceFile.statements[0] as ts.VariableStatement,
|
|
16
|
+
declaration = statement.declarationList.declarations[0],
|
|
17
|
+
template = declaration.initializer! as ts.NoSubstitutionTemplateLiteral;
|
|
18
|
+
let result = extractTemplateParts(template);
|
|
19
|
+
|
|
20
|
+
expect(result.literals).toEqual(['hello world']);
|
|
21
|
+
expect(result.expressions).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('TemplateExpression → correct literals + expressions', () => {
|
|
25
|
+
let sourceFile = createSourceFile('let x = `a${1}b${2}c`;');
|
|
26
|
+
let statement = sourceFile.statements[0] as ts.VariableStatement,
|
|
27
|
+
declaration = statement.declarationList.declarations[0],
|
|
28
|
+
template = declaration.initializer! as ts.TemplateExpression;
|
|
29
|
+
let result = extractTemplateParts(template);
|
|
30
|
+
|
|
31
|
+
expect(result.literals).toEqual(['a', 'b', 'c']);
|
|
32
|
+
expect(result.expressions.length).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('findHtmlTemplates', () => {
|
|
37
|
+
it('single html`` call → returns 1 TemplateInfo with correct literals/expressions', () => {
|
|
38
|
+
let source = `import { html } from '@esportsplus/template';\nlet x = html\`<div>\${value}</div>\`;`;
|
|
39
|
+
let sourceFile = createSourceFile(source);
|
|
40
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
41
|
+
|
|
42
|
+
expect(templates.length).toBe(1);
|
|
43
|
+
expect(templates[0].literals).toEqual(['<div>', '</div>']);
|
|
44
|
+
expect(templates[0].expressions.length).toBe(1);
|
|
45
|
+
expect(templates[0].depth).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('template with no expressions → single literal, no expressions', () => {
|
|
49
|
+
let source = `import { html } from '@esportsplus/template';\nlet x = html\`<div>hello</div>\`;`;
|
|
50
|
+
let sourceFile = createSourceFile(source);
|
|
51
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
52
|
+
|
|
53
|
+
expect(templates.length).toBe(1);
|
|
54
|
+
expect(templates[0].literals).toEqual(['<div>hello</div>']);
|
|
55
|
+
expect(templates[0].expressions).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('no html import → returns empty array', () => {
|
|
59
|
+
let source = `let x = html\`<div>hello</div>\`;`;
|
|
60
|
+
let sourceFile = createSourceFile(source);
|
|
61
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
62
|
+
|
|
63
|
+
// Without type checker, tag name match alone is sufficient
|
|
64
|
+
expect(templates.length).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('no html tagged templates at all → returns empty array', () => {
|
|
68
|
+
let source = `let x = 'hello';`;
|
|
69
|
+
let sourceFile = createSourceFile(source);
|
|
70
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
71
|
+
|
|
72
|
+
expect(templates.length).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('multiple html`` calls in same file → all found', () => {
|
|
76
|
+
let source = [
|
|
77
|
+
`import { html } from '@esportsplus/template';`,
|
|
78
|
+
`let a = html\`<div>one</div>\`;`,
|
|
79
|
+
`let b = html\`<span>two</span>\`;`,
|
|
80
|
+
`let c = html\`<p>\${x}</p>\`;`
|
|
81
|
+
].join('\n');
|
|
82
|
+
let sourceFile = createSourceFile(source);
|
|
83
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
84
|
+
|
|
85
|
+
expect(templates.length).toBe(3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('html`` inside function body → found correctly', () => {
|
|
89
|
+
let source = [
|
|
90
|
+
`import { html } from '@esportsplus/template';`,
|
|
91
|
+
`function render() {`,
|
|
92
|
+
` return html\`<div>\${value}</div>\`;`,
|
|
93
|
+
`}`
|
|
94
|
+
].join('\n');
|
|
95
|
+
let sourceFile = createSourceFile(source);
|
|
96
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
97
|
+
|
|
98
|
+
expect(templates.length).toBe(1);
|
|
99
|
+
expect(templates[0].depth).toBe(1);
|
|
100
|
+
expect(templates[0].expressions.length).toBe(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('nested html`` → returns both with correct depths', () => {
|
|
104
|
+
let source = [
|
|
105
|
+
`import { html } from '@esportsplus/template';`,
|
|
106
|
+
`let outer = html\`<div>\${() => html\`<span>inner</span>\`}</div>\`;`
|
|
107
|
+
].join('\n');
|
|
108
|
+
let sourceFile = createSourceFile(source);
|
|
109
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
110
|
+
|
|
111
|
+
expect(templates.length).toBe(2);
|
|
112
|
+
// Sorted deepest first
|
|
113
|
+
expect(templates[0].depth).toBeGreaterThanOrEqual(templates[1].depth);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('depth ordering: deepest first, then by position ascending', () => {
|
|
117
|
+
let source = [
|
|
118
|
+
`import { html } from '@esportsplus/template';`,
|
|
119
|
+
`let a = html\`<div>\${() => html\`<span>deep1</span>\`}</div>\`;`,
|
|
120
|
+
`let b = html\`<p>shallow</p>\`;`
|
|
121
|
+
].join('\n');
|
|
122
|
+
let sourceFile = createSourceFile(source);
|
|
123
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
124
|
+
|
|
125
|
+
expect(templates.length).toBe(3);
|
|
126
|
+
// First template should be deepest (the inner one from arrow fn)
|
|
127
|
+
expect(templates[0].depth).toBeGreaterThan(templates[1].depth);
|
|
128
|
+
// Same-depth items sorted by position ascending
|
|
129
|
+
let sameDepthtemplates = templates.filter(t => t.depth === 0);
|
|
130
|
+
|
|
131
|
+
for (let i = 1, n = sameDepthtemplates.length; i < n; i++) {
|
|
132
|
+
expect(sameDepthtemplates[i].start).toBeGreaterThan(sameDepthtemplates[i - 1].start);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('findReactiveCalls', () => {
|
|
138
|
+
it('html.reactive() → returns ReactiveCallInfo with array + callback args', () => {
|
|
139
|
+
let source = [
|
|
140
|
+
`import { html } from '@esportsplus/template';`,
|
|
141
|
+
`let x = html.reactive(items, (item) => html\`<li>\${item}</li>\`);`
|
|
142
|
+
].join('\n');
|
|
143
|
+
let sourceFile = createSourceFile(source);
|
|
144
|
+
let calls = findReactiveCalls(sourceFile);
|
|
145
|
+
|
|
146
|
+
expect(calls.length).toBe(1);
|
|
147
|
+
expect(calls[0].arrayArg).toBeDefined();
|
|
148
|
+
expect(calls[0].callbackArg).toBeDefined();
|
|
149
|
+
expect(calls[0].start).toBeLessThan(calls[0].end);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('no reactive calls → empty array', () => {
|
|
153
|
+
let source = `import { html } from '@esportsplus/template';\nlet x = html\`<div>hello</div>\`;`;
|
|
154
|
+
let sourceFile = createSourceFile(source);
|
|
155
|
+
let calls = findReactiveCalls(sourceFile);
|
|
156
|
+
|
|
157
|
+
expect(calls.length).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -77,6 +77,11 @@ describe('constants', () => {
|
|
|
77
77
|
expect(DIRECT_ATTACH_EVENTS.has('onload')).toBe(true);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it('contains mouse enter/leave events', () => {
|
|
81
|
+
expect(DIRECT_ATTACH_EVENTS.has('onmouseenter')).toBe(true);
|
|
82
|
+
expect(DIRECT_ATTACH_EVENTS.has('onmouseleave')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
80
85
|
it('contains scroll event', () => {
|
|
81
86
|
expect(DIRECT_ATTACH_EVENTS.has('onscroll')).toBe(true);
|
|
82
87
|
});
|
|
@@ -84,7 +89,6 @@ describe('constants', () => {
|
|
|
84
89
|
it('does not contain delegatable events', () => {
|
|
85
90
|
expect(DIRECT_ATTACH_EVENTS.has('onclick')).toBe(false);
|
|
86
91
|
expect(DIRECT_ATTACH_EVENTS.has('onkeydown')).toBe(false);
|
|
87
|
-
expect(DIRECT_ATTACH_EVENTS.has('onmouseenter')).toBe(false);
|
|
88
92
|
});
|
|
89
93
|
});
|
|
90
94
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Element } from '../../src/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
let callbacks: VoidFunction[] = [];
|
|
6
|
+
|
|
7
|
+
vi.mock('../../src/utilities', async (importOriginal) => {
|
|
8
|
+
let original = await importOriginal<typeof import('../../src/utilities')>();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...original,
|
|
12
|
+
raf: (cb: VoidFunction) => { callbacks.push(cb); }
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let { default: onconnect } = await import('../../src/event/onconnect');
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
describe('event/onconnect', () => {
|
|
21
|
+
let container: HTMLElement;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
callbacks = [];
|
|
25
|
+
container = document.createElement('div');
|
|
26
|
+
document.body.appendChild(container);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
// Drain the RAF loop so tasks.running resets between tests
|
|
31
|
+
for (let i = 0; i < 65; i++) {
|
|
32
|
+
advanceFrame();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
callbacks = [];
|
|
36
|
+
document.body.removeChild(container);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function advanceFrame() {
|
|
40
|
+
let current = callbacks.slice();
|
|
41
|
+
|
|
42
|
+
callbacks = [];
|
|
43
|
+
|
|
44
|
+
for (let i = 0, n = current.length; i < n; i++) {
|
|
45
|
+
current[i]();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('calls listener on first tick when element is already connected', () => {
|
|
50
|
+
let called = false,
|
|
51
|
+
element = document.createElement('div') as unknown as Element;
|
|
52
|
+
|
|
53
|
+
container.appendChild(element as unknown as Node);
|
|
54
|
+
|
|
55
|
+
onconnect(element, () => { called = true; });
|
|
56
|
+
advanceFrame();
|
|
57
|
+
|
|
58
|
+
expect(called).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('calls listener after element connects on tick N', () => {
|
|
62
|
+
let called = false,
|
|
63
|
+
element = document.createElement('div') as unknown as Element;
|
|
64
|
+
|
|
65
|
+
onconnect(element, () => { called = true; });
|
|
66
|
+
|
|
67
|
+
// Advance 10 frames without connecting
|
|
68
|
+
for (let i = 0; i < 10; i++) {
|
|
69
|
+
advanceFrame();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(called).toBe(false);
|
|
73
|
+
|
|
74
|
+
// Connect element
|
|
75
|
+
container.appendChild(element as unknown as Node);
|
|
76
|
+
advanceFrame();
|
|
77
|
+
|
|
78
|
+
expect(called).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('never calls listener if element does not connect within 60 ticks', () => {
|
|
82
|
+
let called = false,
|
|
83
|
+
element = document.createElement('div') as unknown as Element;
|
|
84
|
+
|
|
85
|
+
onconnect(element, () => { called = true; });
|
|
86
|
+
|
|
87
|
+
// Advance 62 frames — retry decrements from 60, at 0 it removes
|
|
88
|
+
for (let i = 0; i < 62; i++) {
|
|
89
|
+
advanceFrame();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(called).toBe(false);
|
|
93
|
+
|
|
94
|
+
// Verify polling stopped — connecting now should have no effect
|
|
95
|
+
container.appendChild(element as unknown as Node);
|
|
96
|
+
advanceFrame();
|
|
97
|
+
advanceFrame();
|
|
98
|
+
|
|
99
|
+
expect(called).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('passes element as argument to listener', () => {
|
|
103
|
+
let element = document.createElement('div') as unknown as Element,
|
|
104
|
+
received: unknown = null;
|
|
105
|
+
|
|
106
|
+
container.appendChild(element as unknown as Node);
|
|
107
|
+
|
|
108
|
+
onconnect(element, (el) => { received = el; });
|
|
109
|
+
advanceFrame();
|
|
110
|
+
|
|
111
|
+
expect(received).toBe(element);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('calls listener only once', () => {
|
|
115
|
+
let count = 0,
|
|
116
|
+
element = document.createElement('div') as unknown as Element;
|
|
117
|
+
|
|
118
|
+
container.appendChild(element as unknown as Node);
|
|
119
|
+
|
|
120
|
+
onconnect(element, () => { count++; });
|
|
121
|
+
|
|
122
|
+
advanceFrame();
|
|
123
|
+
advanceFrame();
|
|
124
|
+
advanceFrame();
|
|
125
|
+
|
|
126
|
+
expect(count).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('stops polling after listener is called', () => {
|
|
130
|
+
let element = document.createElement('div') as unknown as Element,
|
|
131
|
+
listenerCalled = false;
|
|
132
|
+
|
|
133
|
+
container.appendChild(element as unknown as Node);
|
|
134
|
+
|
|
135
|
+
onconnect(element, () => { listenerCalled = true; });
|
|
136
|
+
advanceFrame();
|
|
137
|
+
|
|
138
|
+
expect(listenerCalled).toBe(true);
|
|
139
|
+
|
|
140
|
+
// After listener fires and task is removed, further frames should not re-add
|
|
141
|
+
advanceFrame();
|
|
142
|
+
advanceFrame();
|
|
143
|
+
|
|
144
|
+
// If no tasks remain, callbacks should be empty after the last frame processes
|
|
145
|
+
expect(callbacks.length).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Element } from '../../src/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
let cleanups: VoidFunction[] = [];
|
|
6
|
+
|
|
7
|
+
vi.mock('@esportsplus/reactivity', async (importOriginal) => {
|
|
8
|
+
let original = await importOriginal<typeof import('@esportsplus/reactivity')>();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...original,
|
|
12
|
+
onCleanup: (fn: VoidFunction) => { cleanups.push(fn); return fn; }
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let { default: onresize } = await import('../../src/event/onresize');
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
function createElement(connected = true): Element {
|
|
21
|
+
let element = document.createElement('div') as unknown as Element;
|
|
22
|
+
|
|
23
|
+
Object.defineProperty(element, 'isConnected', { get: () => connected, configurable: true });
|
|
24
|
+
|
|
25
|
+
return element;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fireResize() {
|
|
29
|
+
window.dispatchEvent(new Event('resize'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
describe('event/onresize', () => {
|
|
34
|
+
let addSpy: ReturnType<typeof vi.spyOn>;
|
|
35
|
+
let removeSpy: ReturnType<typeof vi.spyOn>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
cleanups = [];
|
|
39
|
+
addSpy = vi.spyOn(window, 'addEventListener');
|
|
40
|
+
removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
// Run all captured cleanups to reset module state
|
|
45
|
+
for (let i = 0, n = cleanups.length; i < n; i++) {
|
|
46
|
+
cleanups[i]();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
cleanups = [];
|
|
50
|
+
addSpy.mockRestore();
|
|
51
|
+
removeSpy.mockRestore();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('single element receives resize callback', () => {
|
|
55
|
+
let called = false,
|
|
56
|
+
element = createElement();
|
|
57
|
+
|
|
58
|
+
onresize(element, () => { called = true; });
|
|
59
|
+
fireResize();
|
|
60
|
+
|
|
61
|
+
expect(called).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('multiple elements all receive resize callback', () => {
|
|
65
|
+
let a = 0,
|
|
66
|
+
b = 0,
|
|
67
|
+
elementA = createElement(),
|
|
68
|
+
elementB = createElement();
|
|
69
|
+
|
|
70
|
+
onresize(elementA, () => { a++; });
|
|
71
|
+
onresize(elementB, () => { b++; });
|
|
72
|
+
fireResize();
|
|
73
|
+
|
|
74
|
+
expect(a).toBe(1);
|
|
75
|
+
expect(b).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('disconnected element is auto-removed during next resize', () => {
|
|
79
|
+
let connected = true,
|
|
80
|
+
count = 0,
|
|
81
|
+
element = document.createElement('div') as unknown as Element;
|
|
82
|
+
|
|
83
|
+
Object.defineProperty(element, 'isConnected', {
|
|
84
|
+
get: () => connected,
|
|
85
|
+
configurable: true
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
onresize(element, () => { count++; });
|
|
89
|
+
fireResize();
|
|
90
|
+
|
|
91
|
+
expect(count).toBe(1);
|
|
92
|
+
|
|
93
|
+
connected = false;
|
|
94
|
+
fireResize();
|
|
95
|
+
|
|
96
|
+
// Should not have incremented — disconnected elements skipped
|
|
97
|
+
expect(count).toBe(1);
|
|
98
|
+
|
|
99
|
+
connected = true;
|
|
100
|
+
fireResize();
|
|
101
|
+
|
|
102
|
+
// Already removed from listeners map, should stay at 1
|
|
103
|
+
expect(count).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('dedup: only one window resize listener registered', () => {
|
|
107
|
+
let elementA = createElement(),
|
|
108
|
+
elementB = createElement();
|
|
109
|
+
|
|
110
|
+
onresize(elementA, () => {});
|
|
111
|
+
onresize(elementB, () => {});
|
|
112
|
+
|
|
113
|
+
let resizeCalls = addSpy.mock.calls.filter(
|
|
114
|
+
(args) => args[0] === 'resize'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(resizeCalls.length).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('listener receives element as argument', () => {
|
|
121
|
+
let element = createElement(),
|
|
122
|
+
received: unknown = null;
|
|
123
|
+
|
|
124
|
+
onresize(element, (el) => { received = el; });
|
|
125
|
+
fireResize();
|
|
126
|
+
|
|
127
|
+
expect(received).toBe(element);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('onCleanup removes element from listeners', () => {
|
|
131
|
+
let count = 0,
|
|
132
|
+
element = createElement();
|
|
133
|
+
|
|
134
|
+
onresize(element, () => { count++; });
|
|
135
|
+
fireResize();
|
|
136
|
+
|
|
137
|
+
expect(count).toBe(1);
|
|
138
|
+
|
|
139
|
+
// Simulate reactive cleanup
|
|
140
|
+
for (let i = 0, n = cleanups.length; i < n; i++) {
|
|
141
|
+
cleanups[i]();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
cleanups = [];
|
|
145
|
+
fireResize();
|
|
146
|
+
|
|
147
|
+
expect(count).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('window listener removed when all elements gone', () => {
|
|
151
|
+
let element = createElement(false);
|
|
152
|
+
|
|
153
|
+
onresize(element, () => {});
|
|
154
|
+
fireResize();
|
|
155
|
+
|
|
156
|
+
let removeCalls = removeSpy.mock.calls.filter(
|
|
157
|
+
(args) => args[0] === 'resize'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(removeCalls.length).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('re-registers window listener after all removed and new element added', () => {
|
|
164
|
+
let element = createElement(false);
|
|
165
|
+
|
|
166
|
+
onresize(element, () => {});
|
|
167
|
+
fireResize();
|
|
168
|
+
|
|
169
|
+
// Window listener should be removed
|
|
170
|
+
let removeCalls = removeSpy.mock.calls.filter(
|
|
171
|
+
(args) => args[0] === 'resize'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(removeCalls.length).toBe(1);
|
|
175
|
+
|
|
176
|
+
// Add new element — should re-register
|
|
177
|
+
let element2 = createElement();
|
|
178
|
+
|
|
179
|
+
onresize(element2, () => {});
|
|
180
|
+
|
|
181
|
+
let addCalls = addSpy.mock.calls.filter(
|
|
182
|
+
(args) => args[0] === 'resize'
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(addCalls.length).toBe(2);
|
|
186
|
+
});
|
|
187
|
+
});
|