Skip to content

Commit 3c78f17

Browse files
authored
feat: signals test cases (#3962)
1 parent 4ec0439 commit 3c78f17

File tree

32 files changed

+651
-0
lines changed

32 files changed

+651
-0
lines changed

.github/workflows/karma.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ jobs:
5959
- run: API_VERSION=58 DISABLE_SYNTHETIC=1 yarn sauce:ci
6060
- run: API_VERSION=59 yarn sauce:ci
6161
- run: API_VERSION=59 DISABLE_SYNTHETIC=1 yarn sauce:ci
62+
- run: ENABLE_EXPERIMENTAL_SIGNALS=1 yarn sauce:ci
63+
- run: ENABLE_EXPERIMENTAL_SIGNALS=1 DISABLE_SYNTHETIC=1 yarn sauce:ci
6264

6365
- name: Upload coverage results
6466
uses: actions/upload-artifact@v3

packages/@lwc/integration-karma/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ This set of environment variables applies to the `start` and `test` commands:
4040
- **`API_VERSION=<version>`:** API version to use when compiling.
4141
- **`DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER=1`:** Disable synthetic shadow in the compiler itself.
4242
- **`DISABLE_STATIC_CONTENT_OPTIMIZATION=1`:** Disable static content optimization by setting `enableStaticContentOptimization` to `false`.
43+
- **`ENABLE_EXPERIMENTAL_SIGNALS=1`:** Enables tests for experimental signals protocol.
4344

4445
## Examples
4546

packages/@lwc/integration-karma/scripts/karma-plugins/env.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const {
2222
ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION,
2323
NODE_ENV_FOR_TEST,
2424
API_VERSION,
25+
ENABLE_EXPERIMENTAL_SIGNALS,
2526
FORCE_LWC_V5_COMPILER_FOR_TEST,
2627
} = require('../shared/options');
2728

@@ -47,6 +48,7 @@ function createEnvFile() {
4748
NATIVE_SHADOW_ROOT_DEFINED: typeof ShadowRoot !== 'undefined',
4849
SYNTHETIC_SHADOW_ENABLED: ${SYNTHETIC_SHADOW_ENABLED},
4950
ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL: ${ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL},
51+
ENABLE_EXPERIMENTAL_SIGNALS: ${ENABLE_EXPERIMENTAL_SIGNALS},
5052
ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION: ${ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION},
5153
LWC_VERSION: ${JSON.stringify(LWC_VERSION)},
5254
API_VERSION: ${JSON.stringify(API_VERSION)},

packages/@lwc/integration-karma/scripts/shared/options.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const NODE_ENV_FOR_TEST = process.env.NODE_ENV_FOR_TEST;
3333
const API_VERSION = process.env.API_VERSION
3434
? parseInt(process.env.API_VERSION, 10)
3535
: HIGHEST_API_VERSION;
36+
const ENABLE_EXPERIMENTAL_SIGNALS = Boolean(process.env.ENABLE_EXPERIMENTAL_SIGNALS);
3637

3738
// TODO [#3974]: remove temporary logic to support v5 compiler + v6+ engine
3839
const FORCE_LWC_V5_COMPILER_FOR_TEST = Boolean(process.env.FORCE_LWC_V5_COMPILER_FOR_TEST);
@@ -46,6 +47,7 @@ module.exports = {
4647
DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER,
4748
DISABLE_STATIC_CONTENT_OPTIMIZATION,
4849
SYNTHETIC_SHADOW_ENABLED: !DISABLE_SYNTHETIC,
50+
ENABLE_EXPERIMENTAL_SIGNALS,
4951
API_VERSION,
5052
FORCE_LWC_V5_COMPILER_FOR_TEST,
5153
ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION,
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { createElement, setFeatureFlagForTest } from 'lwc';
2+
import Reactive from 'x/reactive';
3+
import NonReactive from 'x/nonReactive';
4+
import Container from 'x/container';
5+
import Parent from 'x/parent';
6+
import Child from 'x/child';
7+
import DuplicateSignalOnTemplate from 'x/duplicateSignalOnTemplate';
8+
import List from 'x/list';
9+
10+
// Note for testing purposes the signal implementation uses LWC module resolution to simplify things.
11+
// In production the signal will come from a 3rd party library.
12+
import { Signal } from 'x/signal';
13+
14+
if (process.env.ENABLE_EXPERIMENTAL_SIGNALS) {
15+
describe('signal protocol', () => {
16+
beforeAll(() => {
17+
setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true);
18+
});
19+
20+
afterAll(() => {
21+
setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false);
22+
});
23+
24+
describe('lwc engine subscribes template re-render callback when signal is bound to an LWC and used on a template', () => {
25+
[
26+
{
27+
testName:
28+
'contains a getter that references a bound signal (.value on template)',
29+
flag: 'showGetterSignal',
30+
},
31+
{
32+
testName: 'contains a getter that references a bound signal value',
33+
flag: 'showOnlyUsingSignalNotValue',
34+
},
35+
{
36+
testName: 'contains a signal with @api annotation (.value on template)',
37+
flag: 'showApiSignal',
38+
},
39+
{
40+
testName: 'contains a signal with @track annotation (.value on template)',
41+
flag: 'showTrackedSignal',
42+
},
43+
{
44+
testName:
45+
'contains an observed field referencing a signal (.value on template)',
46+
flag: 'showObservedFieldSignal',
47+
},
48+
{
49+
testName:
50+
'contains a direct reference to a signal (not .value) in the template',
51+
flag: 'showOnlyUsingSignalNotValue',
52+
},
53+
].forEach(({ testName, flag }) => {
54+
// Test all ways of binding signal to an LWC + template that cause re-rendering
55+
it(testName, async () => {
56+
const elm = createElement('x-reactive', { is: Reactive });
57+
document.body.appendChild(elm);
58+
await Promise.resolve();
59+
60+
expect(elm.getSignalSubscriberCount()).toBe(0);
61+
elm[flag] = true;
62+
await Promise.resolve();
63+
64+
// the engine will automatically subscribe the re-render callback
65+
expect(elm.getSignalSubscriberCount()).toBe(1);
66+
});
67+
});
68+
});
69+
70+
it('lwc engine should automatically unsubscribe the re-render callback if signal is not used on a template', async () => {
71+
const elm = createElement('x-reactive', { is: Reactive });
72+
elm.showObservedFieldSignal = true;
73+
document.body.appendChild(elm);
74+
await Promise.resolve();
75+
76+
expect(elm.getSignalSubscriberCount()).toBe(1);
77+
elm.showObservedFieldSignal = false;
78+
await Promise.resolve();
79+
80+
expect(elm.getSignalSubscriberCount()).toBe(0);
81+
document.body.removeChild(elm);
82+
});
83+
84+
it('lwc engine does not subscribe re-render callback if signal is not used on a template', async () => {
85+
const elm = createElement('x-non-reactive', { is: NonReactive });
86+
document.body.appendChild(elm);
87+
await Promise.resolve();
88+
89+
expect(elm.getSignalSubscriberCount()).toBe(0);
90+
});
91+
92+
it('only the components referencing a signal should re-render', async () => {
93+
const container = createElement('x-container', { is: Container });
94+
const signalElm = createElement('x-signal-elm', { is: Child });
95+
const signal = new Signal('initial value');
96+
signalElm.signal = signal;
97+
container.appendChild(signalElm);
98+
document.body.appendChild(container);
99+
100+
await Promise.resolve();
101+
102+
expect(container.renderCount).toBe(1);
103+
expect(signalElm.renderCount).toBe(1);
104+
expect(signal.getSubscriberCount()).toBe(1);
105+
106+
signal.value = 'updated value';
107+
await Promise.resolve();
108+
109+
expect(container.renderCount).toBe(1);
110+
expect(signalElm.renderCount).toBe(2);
111+
expect(signal.getSubscriberCount()).toBe(1);
112+
});
113+
114+
it('only subscribes the re-render callback a single time when signal is referenced multiple times on a template', async () => {
115+
const elm = createElement('x-duplicate-signals-on-template', {
116+
is: DuplicateSignalOnTemplate,
117+
});
118+
document.body.appendChild(elm);
119+
await Promise.resolve();
120+
121+
expect(elm.renderCount).toBe(1);
122+
expect(elm.getSignalSubscriberCount()).toBe(1);
123+
expect(elm.getSignalRemovedSubscriberCount()).toBe(0);
124+
125+
elm.updateSignalValue();
126+
await Promise.resolve();
127+
128+
expect(elm.renderCount).toBe(2);
129+
expect(elm.getSignalSubscriberCount()).toBe(1);
130+
expect(elm.getSignalRemovedSubscriberCount()).toBe(1);
131+
});
132+
133+
it('only subscribes re-render callback a single time when signal is referenced multiple times in a list', async () => {
134+
const elm = createElement('x-list', { is: List });
135+
const signal = new Signal('initial value');
136+
elm.signal = signal;
137+
document.body.appendChild(elm);
138+
await Promise.resolve();
139+
140+
expect(signal.getSubscriberCount()).toBe(1);
141+
expect(signal.getRemovedSubscriberCount()).toBe(0);
142+
143+
document.body.removeChild(elm);
144+
await Promise.resolve();
145+
146+
expect(signal.getSubscriberCount()).toBe(0);
147+
expect(signal.getRemovedSubscriberCount()).toBe(1);
148+
});
149+
150+
it('unsubscribes when element is removed from the dom', async () => {
151+
const elm = createElement('x-child', { is: Child });
152+
const signal = new Signal('initial value');
153+
elm.signal = signal;
154+
document.body.appendChild(elm);
155+
await Promise.resolve();
156+
157+
expect(signal.getSubscriberCount()).toBe(1);
158+
expect(signal.getRemovedSubscriberCount()).toBe(0);
159+
160+
document.body.removeChild(elm);
161+
await Promise.resolve();
162+
163+
expect(signal.getSubscriberCount()).toBe(0);
164+
expect(signal.getRemovedSubscriberCount()).toBe(1);
165+
});
166+
167+
it('on template re-render unsubscribes all components where signal is not present on the template', async () => {
168+
const elm = createElement('x-parent', { is: Parent });
169+
elm.showChild = true;
170+
171+
document.body.appendChild(elm);
172+
await Promise.resolve();
173+
174+
// subscribed both parent and child
175+
// as long as parent contains reference to the signal, even if it's just to pass it to a child
176+
// it will be subscribed.
177+
expect(elm.getSignalSubscriberCount()).toBe(2);
178+
expect(elm.getSignalRemovedSubscriberCount()).toBe(0);
179+
180+
elm.showChild = false;
181+
await Promise.resolve();
182+
183+
// The signal is not being used on the parent template anymore so it will be removed
184+
expect(elm.getSignalSubscriberCount()).toBe(0);
185+
expect(elm.getSignalRemovedSubscriberCount()).toBe(2);
186+
});
187+
188+
it('does not subscribe if the signal shape is incorrect', async () => {
189+
const elm = createElement('x-child', { is: Child });
190+
const subscribe = jasmine.createSpy();
191+
// Note the signals property is value's' and not value
192+
const signal = { values: 'initial value', subscribe };
193+
elm.signal = signal;
194+
document.body.appendChild(elm);
195+
await Promise.resolve();
196+
197+
expect(subscribe).not.toHaveBeenCalled();
198+
});
199+
});
200+
} else {
201+
describe('ENABLE_EXPERIMENTAL_SIGNALS not set', () => {
202+
beforeAll(() => {
203+
setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false);
204+
});
205+
206+
it('does not subscribe or unsubscribe if feature flag is disabled', async () => {
207+
const elm = createElement('x-child', { is: Child });
208+
const signal = new Signal('initial value');
209+
elm.signal = signal;
210+
document.body.appendChild(elm);
211+
await Promise.resolve();
212+
213+
expect(signal.getSubscriberCount()).toBe(0);
214+
expect(signal.getRemovedSubscriberCount()).toBe(0);
215+
216+
document.body.removeChild(elm);
217+
await Promise.resolve();
218+
219+
expect(signal.getSubscriberCount()).toBe(0);
220+
expect(signal.getRemovedSubscriberCount()).toBe(0);
221+
});
222+
});
223+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
{signal.value}
3+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
@api renderCount = 0;
5+
@api signal;
6+
7+
renderedCallback() {
8+
this.renderCount++;
9+
}
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<slot></slot>
3+
</template>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
@api renderCount = 0;
5+
6+
renderedCallback() {
7+
this.renderCount++;
8+
}
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
{signal.value}
3+
{signal.value}
4+
{signal.value}
5+
</template>

0 commit comments

Comments
 (0)