thunderous 2.3.12 → 2.3.13
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/dist/__test__/custom-element.test.js +48 -0
- package/dist/__test__/registry.test.js +60 -0
- package/dist/__test__/render.test.js +48 -0
- package/dist/__test__/server-side.test.js +196 -0
- package/dist/__test__/signals.test.js +247 -0
- package/dist/__test__/utilities.test.js +8 -0
- package/dist/constants.js +13 -0
- package/dist/custom-element.js +391 -0
- package/dist/index.cjs +45 -12
- package/dist/index.js +5 -1007
- package/dist/registry.js +95 -0
- package/dist/render.js +350 -0
- package/dist/server-side.js +136 -0
- package/dist/signals.js +115 -0
- package/dist/utilities.js +9 -0
- package/package.json +6 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
import { customElement } from '../custom-element';
|
|
4
|
+
import { html } from '../render';
|
|
5
|
+
import { createRegistry } from '../registry';
|
|
6
|
+
import { NOOP } from '../utilities';
|
|
7
|
+
await describe('customElement', async () => {
|
|
8
|
+
await it('does not throw on the server', () => {
|
|
9
|
+
assert.doesNotThrow(() => customElement(() => html `<div></div>`));
|
|
10
|
+
});
|
|
11
|
+
await it('returns an element result class', () => {
|
|
12
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
13
|
+
assert.ok(MyElement);
|
|
14
|
+
const keys = Object.keys(MyElement);
|
|
15
|
+
assert(keys.every((key) => ['define', 'register', 'eject'].includes(key)));
|
|
16
|
+
});
|
|
17
|
+
await it('supports scoped registries', () => {
|
|
18
|
+
const registry = createRegistry({ scoped: true });
|
|
19
|
+
assert.doesNotThrow(() => customElement(() => html `<div></div>`, { shadowRootOptions: { registry } }));
|
|
20
|
+
});
|
|
21
|
+
await it('returns self for chaining', () => {
|
|
22
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
23
|
+
const registry = createRegistry();
|
|
24
|
+
assert.strictEqual(MyElement.define('my-element'), MyElement);
|
|
25
|
+
assert.strictEqual(MyElement.register(registry), MyElement);
|
|
26
|
+
});
|
|
27
|
+
await it('registers the element with a registry', () => {
|
|
28
|
+
const registry = createRegistry();
|
|
29
|
+
const MyElement = customElement(() => html `<div></div>`)
|
|
30
|
+
.register(registry)
|
|
31
|
+
.define('my-element');
|
|
32
|
+
assert.strictEqual(registry.getTagName(MyElement), 'MY-ELEMENT');
|
|
33
|
+
});
|
|
34
|
+
await it('logs an error when registering after defining in a scoped registry', (testContext) => {
|
|
35
|
+
testContext.mock.method(console, 'error', NOOP);
|
|
36
|
+
const errorMock = console.error.mock;
|
|
37
|
+
const registry = createRegistry({ scoped: true });
|
|
38
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
39
|
+
MyElement.define('my-element');
|
|
40
|
+
MyElement.register(registry);
|
|
41
|
+
assert.strictEqual(errorMock.callCount(), 1);
|
|
42
|
+
assert.strictEqual(errorMock.calls[0].arguments[0], 'Must call `register()` before `define()` for scoped registries.');
|
|
43
|
+
});
|
|
44
|
+
await it('throws an error when ejecting on the server', () => {
|
|
45
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
46
|
+
assert.throws(() => MyElement.eject());
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
import { createRegistry } from '../registry';
|
|
4
|
+
import { customElement } from '../custom-element';
|
|
5
|
+
import { html } from '../render';
|
|
6
|
+
import { NOOP } from '../utilities';
|
|
7
|
+
await describe('createRegistry', async () => {
|
|
8
|
+
await it('creates a global registry', () => {
|
|
9
|
+
const registry = createRegistry();
|
|
10
|
+
assert.ok(registry);
|
|
11
|
+
assert.strictEqual(registry.scoped, false);
|
|
12
|
+
});
|
|
13
|
+
await it('creates a scoped registry', () => {
|
|
14
|
+
const registry = createRegistry({ scoped: true });
|
|
15
|
+
assert.ok(registry);
|
|
16
|
+
assert.strictEqual(registry.scoped, true);
|
|
17
|
+
});
|
|
18
|
+
await it('defines a custom element', () => {
|
|
19
|
+
const registry = createRegistry();
|
|
20
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
21
|
+
assert.doesNotThrow(() => registry.define('my-element', MyElement));
|
|
22
|
+
});
|
|
23
|
+
await it('warns about duplicate custom elements', (testContext) => {
|
|
24
|
+
testContext.mock.method(console, 'warn', NOOP);
|
|
25
|
+
const warnMock = console.warn.mock;
|
|
26
|
+
const registry = createRegistry();
|
|
27
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
28
|
+
const MyElement2 = customElement(() => html `<div></div>`);
|
|
29
|
+
registry.define('my-element', MyElement);
|
|
30
|
+
registry.define('my-element', MyElement2);
|
|
31
|
+
assert.strictEqual(warnMock.callCount(), 1);
|
|
32
|
+
assert.strictEqual(warnMock.calls[0].arguments[0], 'Custom element tag name "MY-ELEMENT" was already defined. Skipping...');
|
|
33
|
+
registry.define('my-element-2', MyElement);
|
|
34
|
+
assert.strictEqual(warnMock.callCount(), 2);
|
|
35
|
+
assert.strictEqual(warnMock.calls[1].arguments[0], 'MY-ELEMENT-2 was already defined. Skipping...');
|
|
36
|
+
});
|
|
37
|
+
await it('gets the tag name of a custom element', () => {
|
|
38
|
+
const registry = createRegistry();
|
|
39
|
+
const tagName = 'my-element';
|
|
40
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
41
|
+
registry.define(tagName, MyElement);
|
|
42
|
+
const result = registry.getTagName(MyElement);
|
|
43
|
+
assert.strictEqual(result, tagName.toUpperCase());
|
|
44
|
+
});
|
|
45
|
+
await it('gets all tag names defined in the registry', () => {
|
|
46
|
+
const registry = createRegistry();
|
|
47
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
48
|
+
const MyElement2 = customElement(() => html `<div></div>`);
|
|
49
|
+
registry.define('my-element', MyElement);
|
|
50
|
+
registry.define('my-element-2', MyElement2);
|
|
51
|
+
const result = registry.getAllTagNames();
|
|
52
|
+
assert.deepStrictEqual(result, ['MY-ELEMENT', 'MY-ELEMENT-2']);
|
|
53
|
+
});
|
|
54
|
+
await it('throws an error if ejected on the server', () => {
|
|
55
|
+
const registry = createRegistry();
|
|
56
|
+
const MyElement = customElement(() => html `<div></div>`);
|
|
57
|
+
registry.define('my-element', MyElement);
|
|
58
|
+
assert.throws(() => registry.eject(), { message: 'Cannot eject a registry on the server.' });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
import { html, css } from '../render';
|
|
4
|
+
import { NOOP } from '../utilities';
|
|
5
|
+
await describe('html', async () => {
|
|
6
|
+
await it('renders a simple string', () => {
|
|
7
|
+
const result = html `<div></div>`;
|
|
8
|
+
assert.strictEqual(result, '<div></div>');
|
|
9
|
+
});
|
|
10
|
+
await it('renders a string with interpolated values', () => {
|
|
11
|
+
const result = html `<div>${'Hello, world!'} ${1} ${true}</div>`;
|
|
12
|
+
assert.strictEqual(result, '<div>Hello, world! 1 true</div>');
|
|
13
|
+
});
|
|
14
|
+
await it('renders a string with signals', () => {
|
|
15
|
+
const mockGetter = () => 'Hello, world!';
|
|
16
|
+
const result = html `<div>${mockGetter}</div>`;
|
|
17
|
+
assert.strictEqual(result, '<div>Hello, world!</div>');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
await describe('css', async () => {
|
|
21
|
+
await it('renders a simple string', () => {
|
|
22
|
+
// prettier-ignore
|
|
23
|
+
const result = css `div { color: red; }`;
|
|
24
|
+
assert.strictEqual(result, 'div { color: red; }');
|
|
25
|
+
});
|
|
26
|
+
await it('renders a string with interpolated values', () => {
|
|
27
|
+
// prettier-ignore
|
|
28
|
+
const result = css `div { --str: ${'str'}; --num: ${1}; --bool: ${true}; }`;
|
|
29
|
+
assert.strictEqual(result, 'div { --str: str; --num: 1; --bool: true; }');
|
|
30
|
+
});
|
|
31
|
+
await it('logs an error if a non-primitive value is interpolated', (testContext) => {
|
|
32
|
+
testContext.mock.method(console, 'error', NOOP);
|
|
33
|
+
const errorMock = console.error.mock;
|
|
34
|
+
const obj = {};
|
|
35
|
+
// prettier-ignore
|
|
36
|
+
const result = css `div { --obj: ${obj}; }`;
|
|
37
|
+
assert.strictEqual(result, 'div { --obj: ; }');
|
|
38
|
+
assert.strictEqual(errorMock.callCount(), 1);
|
|
39
|
+
assert.strictEqual(errorMock.calls[0].arguments[0], 'Objects are not valid in CSS values. Received:');
|
|
40
|
+
assert.strictEqual(errorMock.calls[0].arguments[1], obj);
|
|
41
|
+
});
|
|
42
|
+
await it('renders a string with signals', () => {
|
|
43
|
+
const mockGetter = () => 'red';
|
|
44
|
+
// prettier-ignore
|
|
45
|
+
const result = css `div { color: ${mockGetter}; }`;
|
|
46
|
+
assert.strictEqual(result, 'div { color: red; }');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { clientOnlyCallback, getServerRenderArgs, insertTemplates, onServerDefine, serverCss, serverDefine, serverDefineFns, wrapTemplate, } from '../server-side';
|
|
2
|
+
import { describe, mock, it } from 'node:test';
|
|
3
|
+
import assert from 'assert';
|
|
4
|
+
import { createRegistry } from '../registry';
|
|
5
|
+
import { DEFAULT_RENDER_OPTIONS } from '../constants';
|
|
6
|
+
import { NOOP } from '../utilities';
|
|
7
|
+
import { customElement } from '../custom-element';
|
|
8
|
+
import { html } from '../render';
|
|
9
|
+
const stripWhitespace = (template) => template.trim().replace(/\s\s+/g, ' ');
|
|
10
|
+
await describe('getServerRenderArgs', async () => {
|
|
11
|
+
await it('throws on the server when accessing client-only properties', () => {
|
|
12
|
+
const args = getServerRenderArgs('my-element-1');
|
|
13
|
+
assert.throws(() => args.elementRef.children, {
|
|
14
|
+
message: 'The `elementRef` property is not available on the server.',
|
|
15
|
+
});
|
|
16
|
+
assert.throws(() => args.root.children, { message: 'The `root` property is not available on the server.' });
|
|
17
|
+
assert.throws(() => args.internals.ariaChecked, {
|
|
18
|
+
message: 'The `internals` property is not available on the server.',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
await it('returns nothing from customCallback on the server', () => {
|
|
22
|
+
const args = getServerRenderArgs('my-element-2');
|
|
23
|
+
assert.strictEqual(args.customCallback(NOOP), '');
|
|
24
|
+
});
|
|
25
|
+
await it('tracks CSS strings on the server using adoptStyleSheet', () => {
|
|
26
|
+
const args = getServerRenderArgs('my-element-3');
|
|
27
|
+
// build the expected result
|
|
28
|
+
const cssStr = ':host { color: red; }';
|
|
29
|
+
const expectedServerCss = new Map();
|
|
30
|
+
expectedServerCss.set('my-element-3', [cssStr]);
|
|
31
|
+
// @ts-expect-error // this will be a string on the server.
|
|
32
|
+
args.adoptStyleSheet(':host { color: red; }');
|
|
33
|
+
assert.deepStrictEqual(serverCss, expectedServerCss);
|
|
34
|
+
});
|
|
35
|
+
await it('tracks CSS strings on the server with registries using adoptStyleSheet', () => {
|
|
36
|
+
const registry = createRegistry();
|
|
37
|
+
const args = getServerRenderArgs('my-element-4', registry);
|
|
38
|
+
// build the expected result
|
|
39
|
+
const cssStr1 = ':host { color: blue; }';
|
|
40
|
+
const expectedServerCss = new Map();
|
|
41
|
+
expectedServerCss.set('my-element-4', [cssStr1]);
|
|
42
|
+
// @ts-expect-error // this will be a string on the server.
|
|
43
|
+
args.adoptStyleSheet(cssStr1);
|
|
44
|
+
assert.deepStrictEqual(registry.__serverCss, expectedServerCss);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
await describe('wrapTemplate', async () => {
|
|
48
|
+
await it('wraps the render result in a template tag', () => {
|
|
49
|
+
const template = stripWhitespace(wrapTemplate({
|
|
50
|
+
tagName: 'my-element-5',
|
|
51
|
+
serverRender: () => 'Hello, world!',
|
|
52
|
+
options: DEFAULT_RENDER_OPTIONS,
|
|
53
|
+
}));
|
|
54
|
+
const expectedTemplate = stripWhitespace(/* html */ `
|
|
55
|
+
<template
|
|
56
|
+
shadowrootmode="closed"
|
|
57
|
+
shadowrootdelegatesfocus="false"
|
|
58
|
+
shadowrootclonable="false"
|
|
59
|
+
shadowrootserializable="false"
|
|
60
|
+
>
|
|
61
|
+
Hello, world!
|
|
62
|
+
</template>
|
|
63
|
+
`);
|
|
64
|
+
assert.strictEqual(template, expectedTemplate);
|
|
65
|
+
});
|
|
66
|
+
await it('wraps the render result in a template tag with CSS', () => {
|
|
67
|
+
const args = getServerRenderArgs('my-element-6');
|
|
68
|
+
// @ts-expect-error // this will be a string on the server.
|
|
69
|
+
args.adoptStyleSheet(':host { color: green; }');
|
|
70
|
+
const template = stripWhitespace(wrapTemplate({
|
|
71
|
+
tagName: 'my-element-6',
|
|
72
|
+
serverRender: () => 'Hello, world!',
|
|
73
|
+
options: DEFAULT_RENDER_OPTIONS,
|
|
74
|
+
}));
|
|
75
|
+
const expectedTemplate = stripWhitespace(/* html */ `
|
|
76
|
+
<template
|
|
77
|
+
shadowrootmode="closed"
|
|
78
|
+
shadowrootdelegatesfocus="false"
|
|
79
|
+
shadowrootclonable="false"
|
|
80
|
+
shadowrootserializable="false"
|
|
81
|
+
>
|
|
82
|
+
<style>:host { color: green; }</style>Hello, world!
|
|
83
|
+
</template>
|
|
84
|
+
`);
|
|
85
|
+
assert.strictEqual(template, expectedTemplate);
|
|
86
|
+
});
|
|
87
|
+
await it('wraps the render result in a template tag when shadow root is not attached', () => {
|
|
88
|
+
const template = stripWhitespace(wrapTemplate({
|
|
89
|
+
tagName: 'my-element-7',
|
|
90
|
+
serverRender: () => 'Hello, world!',
|
|
91
|
+
options: {
|
|
92
|
+
...DEFAULT_RENDER_OPTIONS,
|
|
93
|
+
attachShadow: false,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
const expectedTemplate = 'Hello, world!';
|
|
97
|
+
assert.strictEqual(template, expectedTemplate);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
await describe('insertTemplates', async () => {
|
|
101
|
+
await it('inserts the template into the input string', () => {
|
|
102
|
+
const inputString = /* html */ `<my-element-7></my-element-7>`;
|
|
103
|
+
const template = /* html */ `<div>Hello, world!</div>`;
|
|
104
|
+
const result = stripWhitespace(insertTemplates('my-element-7', template, inputString));
|
|
105
|
+
const expectedResult = stripWhitespace(/* html */ `
|
|
106
|
+
<my-element-7><div>Hello, world!</div></my-element-7>
|
|
107
|
+
`);
|
|
108
|
+
assert.strictEqual(result, expectedResult);
|
|
109
|
+
});
|
|
110
|
+
await it('inserts the template into the input string and parses attribute references', () => {
|
|
111
|
+
const inputString = /* html */ `<my-element-8 test="Hello, world!"></my-element-8>`;
|
|
112
|
+
const template = /* html */ `<div>{{attr:test}}</div>`;
|
|
113
|
+
const result = stripWhitespace(insertTemplates('my-element-8', template, inputString));
|
|
114
|
+
const expectedResult = stripWhitespace(/* html */ `
|
|
115
|
+
<my-element-8 test="Hello, world!"><div>Hello, world!</div></my-element-8>
|
|
116
|
+
`);
|
|
117
|
+
assert.strictEqual(result, expectedResult);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
await describe('onServerDefine', async () => {
|
|
121
|
+
await it('adds the function to the set', () => {
|
|
122
|
+
const fn = NOOP;
|
|
123
|
+
onServerDefine(fn);
|
|
124
|
+
assert.strictEqual(serverDefineFns.size, 1);
|
|
125
|
+
assert.strictEqual(serverDefineFns.has(fn), true);
|
|
126
|
+
serverDefineFns.clear();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
await describe('serverDefine', async () => {
|
|
130
|
+
await it('calls the serverDefineFns with the result of serverRender', () => {
|
|
131
|
+
const fn = mock.fn((tagName, template) => {
|
|
132
|
+
assert.strictEqual(tagName, 'my-element-9');
|
|
133
|
+
assert.strictEqual(template, 'Hello, world!');
|
|
134
|
+
});
|
|
135
|
+
onServerDefine(fn);
|
|
136
|
+
serverDefine({
|
|
137
|
+
tagName: 'my-element-9',
|
|
138
|
+
serverRender: () => 'Hello, world!',
|
|
139
|
+
options: {
|
|
140
|
+
...DEFAULT_RENDER_OPTIONS,
|
|
141
|
+
attachShadow: false,
|
|
142
|
+
},
|
|
143
|
+
elementResult: customElement(() => html `<div></div>`),
|
|
144
|
+
});
|
|
145
|
+
assert.strictEqual(fn.mock.calls.length, 1);
|
|
146
|
+
assert.strictEqual(fn.mock.calls[0].arguments[0], 'my-element-9');
|
|
147
|
+
serverDefineFns.clear();
|
|
148
|
+
});
|
|
149
|
+
await it('sets the server render options on the parent registry', () => {
|
|
150
|
+
const parentRegistry = createRegistry();
|
|
151
|
+
const serverRender = () => 'Hello, world!';
|
|
152
|
+
serverDefine({
|
|
153
|
+
tagName: 'my-element-10',
|
|
154
|
+
serverRender,
|
|
155
|
+
options: DEFAULT_RENDER_OPTIONS,
|
|
156
|
+
parentRegistry,
|
|
157
|
+
elementResult: customElement(() => html `<div></div>`),
|
|
158
|
+
});
|
|
159
|
+
const expectedServerRenderOpts = new Map([
|
|
160
|
+
['my-element-10', { serverRender, ...DEFAULT_RENDER_OPTIONS }],
|
|
161
|
+
]);
|
|
162
|
+
assert.strictEqual(parentRegistry.__serverRenderOpts.size, 1);
|
|
163
|
+
assert.deepStrictEqual(parentRegistry.__serverRenderOpts, expectedServerRenderOpts);
|
|
164
|
+
});
|
|
165
|
+
await it('renders scoped registries correctly', () => {
|
|
166
|
+
const scopedRegistry = createRegistry({ scoped: true });
|
|
167
|
+
onServerDefine((tagName, template) => {
|
|
168
|
+
assert.strictEqual(tagName, 'my-element-12');
|
|
169
|
+
assert.strictEqual(template, '<my-element-11>inner</my-element-11>');
|
|
170
|
+
});
|
|
171
|
+
serverDefine({
|
|
172
|
+
tagName: 'my-element-11',
|
|
173
|
+
serverRender: () => 'inner',
|
|
174
|
+
options: { ...DEFAULT_RENDER_OPTIONS, attachShadow: false },
|
|
175
|
+
parentRegistry: scopedRegistry,
|
|
176
|
+
elementResult: customElement(() => html `<div></div>`),
|
|
177
|
+
});
|
|
178
|
+
serverDefine({
|
|
179
|
+
tagName: 'my-element-12',
|
|
180
|
+
serverRender: () => '<my-element-11></my-element-11>',
|
|
181
|
+
options: { ...DEFAULT_RENDER_OPTIONS, attachShadow: false },
|
|
182
|
+
scopedRegistry,
|
|
183
|
+
elementResult: customElement(() => html `<div></div>`),
|
|
184
|
+
});
|
|
185
|
+
serverDefineFns.clear();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
await describe('clientOnlyCallback', async () => {
|
|
189
|
+
await it('does nothing on the server when called directly', () => {
|
|
190
|
+
let runCount = 0;
|
|
191
|
+
clientOnlyCallback(() => {
|
|
192
|
+
runCount++;
|
|
193
|
+
});
|
|
194
|
+
assert.strictEqual(runCount, 0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createEffect, createSignal, derived } from '../signals';
|
|
2
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
3
|
+
import assert from 'assert';
|
|
4
|
+
import { NOOP } from '../utilities';
|
|
5
|
+
await describe('createSignal', async () => {
|
|
6
|
+
await it('sets the initial value', () => {
|
|
7
|
+
const [count] = createSignal(0);
|
|
8
|
+
assert.strictEqual(count(), 0);
|
|
9
|
+
});
|
|
10
|
+
await it('sets a new value', () => {
|
|
11
|
+
const [count, setCount] = createSignal(0);
|
|
12
|
+
setCount(1);
|
|
13
|
+
assert.strictEqual(count(), 1);
|
|
14
|
+
});
|
|
15
|
+
await it('does not recalculate for equal primitives', () => {
|
|
16
|
+
const [count, setCount] = createSignal(0);
|
|
17
|
+
let runCount = 0;
|
|
18
|
+
createEffect(() => {
|
|
19
|
+
count();
|
|
20
|
+
runCount++;
|
|
21
|
+
});
|
|
22
|
+
setCount(0);
|
|
23
|
+
assert.strictEqual(runCount, 1);
|
|
24
|
+
});
|
|
25
|
+
await it('does not recalculate for complex data', () => {
|
|
26
|
+
const [count, setCount] = createSignal({ value: 0 });
|
|
27
|
+
let runCount = 0;
|
|
28
|
+
createEffect(() => {
|
|
29
|
+
count();
|
|
30
|
+
runCount++;
|
|
31
|
+
});
|
|
32
|
+
setCount({ value: 0 });
|
|
33
|
+
assert.strictEqual(runCount, 1);
|
|
34
|
+
});
|
|
35
|
+
await it('runs in debug mode', async (testContext) => {
|
|
36
|
+
await it('adds the label when the signal is created with one', async () => {
|
|
37
|
+
testContext.mock.method(console, 'log', NOOP);
|
|
38
|
+
const logMock = console.log.mock;
|
|
39
|
+
beforeEach(() => logMock.resetCalls());
|
|
40
|
+
const [count, setCount] = createSignal(0, { debugMode: true, label: 'count' });
|
|
41
|
+
await it('does not log when the signal is initially created', () => {
|
|
42
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
43
|
+
});
|
|
44
|
+
await it('logs when the signal getter is run', async () => {
|
|
45
|
+
count();
|
|
46
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
47
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
48
|
+
'Signal retrieved:',
|
|
49
|
+
{ value: 0, subscribers: new Set(), label: '(count)' },
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
await it('logs when the signal setter is run', async () => {
|
|
53
|
+
setCount(1);
|
|
54
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
55
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
56
|
+
'Signal set:',
|
|
57
|
+
{ oldValue: 0, newValue: 1, subscribers: new Set(), label: '(count)' },
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
await it('uses "anonymous signal" when the signal is created without a label', async () => {
|
|
62
|
+
testContext.mock.method(console, 'log', NOOP);
|
|
63
|
+
const logMock = console.log.mock;
|
|
64
|
+
beforeEach(() => logMock.resetCalls());
|
|
65
|
+
const [count, setCount] = createSignal(0, { debugMode: true });
|
|
66
|
+
await it('does not log when the signal is initially created', () => {
|
|
67
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
68
|
+
});
|
|
69
|
+
await it('logs when the signal getter is run', () => {
|
|
70
|
+
count();
|
|
71
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
72
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
73
|
+
'Signal retrieved:',
|
|
74
|
+
{ value: 0, subscribers: new Set(), label: 'anonymous signal' },
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
await it('logs when the signal setter is run', () => {
|
|
78
|
+
setCount(1);
|
|
79
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
80
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
81
|
+
'Signal set:',
|
|
82
|
+
{ oldValue: 0, newValue: 1, subscribers: new Set(), label: 'anonymous signal' },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
await it('does not log if debugMode is false', async () => {
|
|
87
|
+
testContext.mock.method(console, 'log', NOOP);
|
|
88
|
+
const logMock = console.log.mock;
|
|
89
|
+
beforeEach(() => logMock.resetCalls());
|
|
90
|
+
const [count, setCount] = createSignal(0, { debugMode: false, label: 'count' });
|
|
91
|
+
await it('does not log when the signal is initially created', () => {
|
|
92
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
93
|
+
});
|
|
94
|
+
await it('does not log when the signal getter is run', () => {
|
|
95
|
+
count();
|
|
96
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
97
|
+
});
|
|
98
|
+
await it('does not log when the signal setter is run', () => {
|
|
99
|
+
setCount(1);
|
|
100
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
await it('adds getter and setter labels in addition to the overall signal label', async () => {
|
|
104
|
+
testContext.mock.method(console, 'log', NOOP);
|
|
105
|
+
const logMock = console.log.mock;
|
|
106
|
+
beforeEach(() => logMock.resetCalls());
|
|
107
|
+
const [count, setCount] = createSignal(0, { debugMode: true, label: 'count' });
|
|
108
|
+
await it('does not log when the signal is initially created', () => {
|
|
109
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
110
|
+
});
|
|
111
|
+
await it('logs when the signal getter is run with a label', () => {
|
|
112
|
+
count({ debugMode: true, label: 'getter' });
|
|
113
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
114
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
115
|
+
'Signal retrieved:',
|
|
116
|
+
{ value: 0, subscribers: new Set(), label: '(count) getter' },
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
await it('logs when the signal setter is run with a label', () => {
|
|
120
|
+
setCount(1, { debugMode: true, label: 'setter' });
|
|
121
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
122
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
123
|
+
'Signal set:',
|
|
124
|
+
{ oldValue: 0, newValue: 1, subscribers: new Set(), label: '(count) setter' },
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
await it('adds getter and setter labels instead of the overall signal label', async () => {
|
|
129
|
+
testContext.mock.method(console, 'log', NOOP);
|
|
130
|
+
const logMock = console.log.mock;
|
|
131
|
+
beforeEach(() => logMock.resetCalls());
|
|
132
|
+
const [count, setCount] = createSignal(0);
|
|
133
|
+
await it('does not log when the signal is initially created', () => {
|
|
134
|
+
assert.strictEqual(logMock.callCount(), 0);
|
|
135
|
+
});
|
|
136
|
+
await it('logs when the signal getter is run with a label', () => {
|
|
137
|
+
count({ debugMode: true, label: 'getter' });
|
|
138
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
139
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
140
|
+
'Signal retrieved:',
|
|
141
|
+
{ value: 0, subscribers: new Set(), label: 'getter' },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
await it('logs when the signal setter is run with a label', () => {
|
|
145
|
+
setCount(1, { debugMode: true, label: 'setter' });
|
|
146
|
+
assert.strictEqual(logMock.callCount(), 1);
|
|
147
|
+
assert.deepStrictEqual(logMock.calls[0].arguments, [
|
|
148
|
+
'Signal set:',
|
|
149
|
+
{ oldValue: 0, newValue: 1, subscribers: new Set(), label: 'setter' },
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
await it('handles errors in subscribers', async (testContext) => {
|
|
154
|
+
testContext.mock.method(console, 'error', NOOP);
|
|
155
|
+
const errorMock = console.error.mock;
|
|
156
|
+
const [count, setCount] = createSignal(0);
|
|
157
|
+
const error = new Error('Test error');
|
|
158
|
+
createEffect(() => {
|
|
159
|
+
if (count() === 1) {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
setCount(1);
|
|
164
|
+
assert.strictEqual(errorMock.callCount(), 1);
|
|
165
|
+
assert.deepStrictEqual(errorMock.calls[0].arguments, [
|
|
166
|
+
'Error in subscriber:',
|
|
167
|
+
{ error, oldValue: 0, newValue: 1, fn: errorMock.calls[0].arguments[1].fn },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
await describe('createEffect', async () => {
|
|
173
|
+
await it('runs immediately', () => {
|
|
174
|
+
const [count] = createSignal(0);
|
|
175
|
+
let result;
|
|
176
|
+
createEffect(() => {
|
|
177
|
+
result = count();
|
|
178
|
+
});
|
|
179
|
+
assert.strictEqual(result, 0);
|
|
180
|
+
});
|
|
181
|
+
await it('runs when signals change', () => {
|
|
182
|
+
const [count, setCount] = createSignal(0);
|
|
183
|
+
let result;
|
|
184
|
+
createEffect(() => {
|
|
185
|
+
result = count();
|
|
186
|
+
});
|
|
187
|
+
setCount(1);
|
|
188
|
+
assert.strictEqual(result, 1);
|
|
189
|
+
});
|
|
190
|
+
await it('handles multiple subscribers', () => {
|
|
191
|
+
const [count, setCount] = createSignal(0);
|
|
192
|
+
let result1;
|
|
193
|
+
let result2;
|
|
194
|
+
createEffect(() => {
|
|
195
|
+
result1 = count();
|
|
196
|
+
});
|
|
197
|
+
createEffect(() => {
|
|
198
|
+
result2 = count();
|
|
199
|
+
});
|
|
200
|
+
setCount(1);
|
|
201
|
+
assert.strictEqual(result1, 1);
|
|
202
|
+
assert.strictEqual(result2, 1);
|
|
203
|
+
});
|
|
204
|
+
await it('handles errors in effects', (testContext) => {
|
|
205
|
+
testContext.mock.method(console, 'error', NOOP);
|
|
206
|
+
const errorMock = console.error.mock;
|
|
207
|
+
const error = new Error('Test error');
|
|
208
|
+
createEffect(() => {
|
|
209
|
+
throw error;
|
|
210
|
+
});
|
|
211
|
+
assert.strictEqual(errorMock.callCount(), 1);
|
|
212
|
+
assert.deepStrictEqual(errorMock.calls[0].arguments, [
|
|
213
|
+
'Error in effect:',
|
|
214
|
+
{ error, fn: errorMock.calls[0].arguments[1].fn },
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
await describe('derived', async () => {
|
|
219
|
+
await it('calculates the value immediately', () => {
|
|
220
|
+
const [count] = createSignal(1);
|
|
221
|
+
const doubled = derived(() => count() * 2);
|
|
222
|
+
assert.strictEqual(doubled(), 2);
|
|
223
|
+
});
|
|
224
|
+
await it('recalculates the value upon updating', () => {
|
|
225
|
+
const [count, setCount] = createSignal(1);
|
|
226
|
+
const doubled = derived(() => count() * 2);
|
|
227
|
+
setCount(2);
|
|
228
|
+
assert.strictEqual(doubled(), 4);
|
|
229
|
+
});
|
|
230
|
+
await it('handles errors in derived signals', (testContext) => {
|
|
231
|
+
testContext.mock.method(console, 'error', NOOP);
|
|
232
|
+
const errorMock = console.error.mock;
|
|
233
|
+
const error = new Error('Test error');
|
|
234
|
+
const [count, setCount] = createSignal(1);
|
|
235
|
+
derived(() => {
|
|
236
|
+
if (count() === 2) {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
setCount(2);
|
|
241
|
+
assert.strictEqual(errorMock.callCount(), 1);
|
|
242
|
+
assert.deepStrictEqual(errorMock.calls[0].arguments, [
|
|
243
|
+
'Error in derived signal:',
|
|
244
|
+
{ error, fn: errorMock.calls[0].arguments[1].fn },
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_RENDER_OPTIONS = {
|
|
2
|
+
formAssociated: false,
|
|
3
|
+
observedAttributes: [],
|
|
4
|
+
attributesAsProperties: [],
|
|
5
|
+
attachShadow: true,
|
|
6
|
+
shadowRootOptions: {
|
|
7
|
+
mode: 'closed',
|
|
8
|
+
delegatesFocus: false,
|
|
9
|
+
clonable: false,
|
|
10
|
+
serializable: false,
|
|
11
|
+
slotAssignment: 'named',
|
|
12
|
+
},
|
|
13
|
+
};
|