Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/karma.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
tunnelName: ${{ env.SAUCE_TUNNEL_ID }}

- run: yarn sauce:ci
- run: ENABLE_EXPERIMENTAL_SIGNALS=1 yarn sauce:ci
- run: DISABLE_SYNTHETIC=1 yarn sauce:ci
- run: LEGACY_BROWSERS=1 yarn sauce:ci
- run: FORCE_NATIVE_SHADOW_MODE_FOR_TEST=1 yarn sauce:ci
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/integration-karma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This set of environment variables applies to the `start` and `test` commands:
- **`API_VERSION=<version>`:** API version to use when compiling.
- **`DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER=1`:** Disable synthetic shadow in the compiler itself.
- **`DISABLE_STATIC_CONTENT_OPTIMIZATION=1`:** Disable static content optimization by setting `enableStaticContentOptimization` to `false`.
- **`ENABLE_EXPERIMENTAL_SIGNALS=1`:** Enables tests for experimental signals protocol.

## Examples

Expand Down
2 changes: 2 additions & 0 deletions packages/@lwc/integration-karma/scripts/karma-plugins/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL,
NODE_ENV_FOR_TEST,
API_VERSION,
ENABLE_EXPERIMENTAL_SIGNALS,
} = require('../shared/options');

const DIST_DIR = path.resolve(__dirname, '../../dist');
Expand All @@ -45,6 +46,7 @@ function createEnvFile() {
NATIVE_SHADOW_ROOT_DEFINED: typeof ShadowRoot !== 'undefined',
SYNTHETIC_SHADOW_ENABLED: ${SYNTHETIC_SHADOW_ENABLED},
ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL: ${ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL},
ENABLE_EXPERIMENTAL_SIGNALS: ${ENABLE_EXPERIMENTAL_SIGNALS},
LWC_VERSION: ${JSON.stringify(LWC_VERSION)},
API_VERSION: ${JSON.stringify(API_VERSION)}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/@lwc/integration-karma/scripts/shared/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const NODE_ENV_FOR_TEST = process.env.NODE_ENV_FOR_TEST;
const API_VERSION = process.env.API_VERSION
? parseInt(process.env.API_VERSION, 10)
: HIGHEST_API_VERSION;
const ENABLE_EXPERIMENTAL_SIGNALS = Boolean(process.env.ENABLE_EXPERIMENTAL_SIGNALS);

module.exports = {
// Test configuration
Expand All @@ -40,6 +41,7 @@ module.exports = {
DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER,
DISABLE_STATIC_CONTENT_OPTIMIZATION,
SYNTHETIC_SHADOW_ENABLED: !DISABLE_SYNTHETIC,
ENABLE_EXPERIMENTAL_SIGNALS,
API_VERSION,
GREP: process.env.GREP,
COVERAGE: Boolean(process.env.COVERAGE),
Expand Down
220 changes: 220 additions & 0 deletions packages/@lwc/integration-karma/test/signal/protocol/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { createElement, setFeatureFlagForTest } from 'lwc';
import Reactive from 'x/reactive';
import NonReactive from 'x/nonReactive';
import Container from 'x/container';
import Parent from 'x/parent';
import Child from 'x/child';
import DuplicateSignalOnTemplate from 'x/duplicateSignalOnTemplate';
import List from 'x/list';
import { Signal } from 'x/signal';

if (process.env.ENABLE_EXPERIMENTAL_SIGNALS) {
describe('signal protocol', () => {
beforeAll(() => {
setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true);
});

afterAll(() => {
setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false);
});

describe('lwc engine subscribes template re-render callback when signal is bound to an LWC and used on a template', () => {
[
{
testName:
'contains a getter that references a bound signal (.value on template)',
flag: 'showGetterSignal',
},
{
testName: 'contains a getter that references a bound signal value',
flag: 'showOnlyUsingSignalNotValue',
},
{
testName: 'contains a signal with @api annotation (.value on template)',
flag: 'showApiSignal',
},
{
testName: 'contains a signal with @track annotation (.value on template)',
flag: 'showTrackedSignal',
},
{
testName:
'contains an observed field referencing a signal (.value on template)',
flag: 'showObservedFieldSignal',
},
{
testName:
'contains a direct reference to a signal (not .value) in the template',
flag: 'showOnlyUsingSignalNotValue',
},
].forEach(({ testName, flag }) => {
// Test all ways of binding signal to an LWC + template that cause re-rendering
it(testName, async () => {
const elm = createElement('x-reactive', { is: Reactive });
document.body.appendChild(elm);
await Promise.resolve();

expect(elm.getSignalSubscriberCount()).toBe(0);
elm[flag] = true;
await Promise.resolve();

// the engine will automatically subscribe the re-render callback
expect(elm.getSignalSubscriberCount()).toBe(1);
});
});
});

it('lwc engine should automatically unsubscribe the re-render callback if signal is not used on a template', async () => {
const elm = createElement('x-reactive', { is: Reactive });
elm.showObservedFieldSignal = true;
document.body.appendChild(elm);
await Promise.resolve();

expect(elm.getSignalSubscriberCount()).toBe(1);
elm.showObservedFieldSignal = false;
await Promise.resolve();

expect(elm.getSignalSubscriberCount()).toBe(0);
document.body.removeChild(elm);
});

it('lwc engine does not subscribe re-render callback if signal is not used on a template', async () => {
const elm = createElement('x-non-reactive', { is: NonReactive });
document.body.appendChild(elm);
await Promise.resolve();

expect(elm.getSignalSubscriberCount()).toBe(0);
});

it('only the components referencing a signal should re-render', async () => {
const container = createElement('x-container', { is: Container });
const signalElm = createElement('x-signal-elm', { is: Child });
const signal = new Signal('initial value');
signalElm.signal = signal;
container.appendChild(signalElm);
document.body.appendChild(container);

await Promise.resolve();

expect(container.renderCount).toBe(1);
expect(signalElm.renderCount).toBe(1);
expect(signal.getSubscriberCount()).toBe(1);

signal.value = 'updated value';
await Promise.resolve();

expect(container.renderCount).toBe(1);
expect(signalElm.renderCount).toBe(2);
expect(signal.getSubscriberCount()).toBe(1);
});

it('only subscribes the re-render callback a single time when signal is referenced multiple times on a template', async () => {
const elm = createElement('x-duplicate-signals-on-template', {
is: DuplicateSignalOnTemplate,
});
document.body.appendChild(elm);
await Promise.resolve();

expect(elm.renderCount).toBe(1);
expect(elm.getSignalSubscriberCount()).toBe(1);
expect(elm.getSignalRemovedSubscriberCount()).toBe(0);

elm.updateSignalValue();
await Promise.resolve();

expect(elm.renderCount).toBe(2);
expect(elm.getSignalSubscriberCount()).toBe(1);
expect(elm.getSignalRemovedSubscriberCount()).toBe(1);
});

it('only subscribes re-render callback a single time when signal is referenced multiple times in a list', async () => {
const elm = createElement('x-list', { is: List });
const signal = new Signal('initial value');
elm.signal = signal;
document.body.appendChild(elm);
await Promise.resolve();

expect(signal.getSubscriberCount()).toBe(1);
expect(signal.getRemovedSubscriberCount()).toBe(0);

document.body.removeChild(elm);
await Promise.resolve();

expect(signal.getSubscriberCount()).toBe(0);
expect(signal.getRemovedSubscriberCount()).toBe(1);
});

it('unsubscribes when element is removed from the dom', async () => {
const elm = createElement('x-child', { is: Child });
const signal = new Signal('initial value');
elm.signal = signal;
document.body.appendChild(elm);
await Promise.resolve();

expect(signal.getSubscriberCount()).toBe(1);
expect(signal.getRemovedSubscriberCount()).toBe(0);

document.body.removeChild(elm);
await Promise.resolve();

expect(signal.getSubscriberCount()).toBe(0);
expect(signal.getRemovedSubscriberCount()).toBe(1);
});

it('on template re-render unsubscribes all components where signal is not present on the template', async () => {
const elm = createElement('x-parent', { is: Parent });
elm.showChild = true;

document.body.appendChild(elm);
await Promise.resolve();

// subscribed both parent and child
// as long as parent contains reference to the signal, even if it's just to pass it to a child
// it will be subscribed.
expect(elm.getSignalSubscriberCount()).toBe(2);
expect(elm.getSignalRemovedSubscriberCount()).toBe(0);

elm.showChild = false;
await Promise.resolve();

// The signal is not being used on the parent template anymore so it will be removed
expect(elm.getSignalSubscriberCount()).toBe(0);
expect(elm.getSignalRemovedSubscriberCount()).toBe(2);
});

it('does not subscribe if the signal shape is incorrect', async () => {
const elm = createElement('x-child', { is: Child });
const subscribe = jasmine.createSpy();
// Note the signals property is value's' and not value
const signal = { values: 'initial value', subscribe };
elm.signal = signal;
document.body.appendChild(elm);
await Promise.resolve();

expect(subscribe).not.toHaveBeenCalled();
});
});
} else {
describe('ENABLE_EXPERIMENTAL_SIGNALS not set', () => {
beforeAll(() => {
setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false);
});

it('does not subscribe or unsubscribe if feature flag is disabled', async () => {
const elm = createElement('x-child', { is: Child });
const signal = new Signal('initial value');
elm.signal = signal;
document.body.appendChild(elm);
await Promise.resolve();

expect(signal.getSubscriberCount()).toBe(0);
expect(signal.getRemovedSubscriberCount()).toBe(0);

document.body.removeChild(elm);
await Promise.resolve();

expect(signal.getSubscriberCount()).toBe(0);
expect(signal.getRemovedSubscriberCount()).toBe(0);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
{signal.value}
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { LightningElement, api } from 'lwc';

export default class extends LightningElement {
@api renderCount = 0;
@api signal;

renderedCallback() {
this.renderCount++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<slot></slot>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LightningElement, api } from 'lwc';

export default class extends LightningElement {
@api renderCount = 0;

renderedCallback() {
this.renderCount++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
{signal.value}
{signal.value}
{signal.value}
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LightningElement, api } from 'lwc';
import { Signal } from 'x/signal';

export default class extends LightningElement {
signal = new Signal('initial value');
@api renderCount = 0;

renderedCallback() {
this.renderCount++;
}

@api
getSignalSubscriberCount() {
return this.signal.getSubscriberCount();
}

@api
getSignalRemovedSubscriberCount() {
return this.signal.getRemovedSubscriberCount();
}

@api
updateSignalValue() {
this.signal.value = 'updated value';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<template for:each={items} for:item="item">
<p key={item}>{signal.value}</p>
</template>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { LightningElement, api } from 'lwc';

export default class extends LightningElement {
@api signal;
items = [1, 2, 3, 4, 5, 6];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<span>{apiSignalValue}</span>
<span>{trackSignalValue}</span>
<span>{observedFieldSignalValue}</span>
<span>{externalSignalValueGetter}</span>
<span>{observedFieldBoundSignalValue}</span>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LightningElement, api, track } from 'lwc';
import { Signal } from 'x/signal';

const signal = new Signal('initial value');

export default class extends LightningElement {
// Note that this signal is bound but it's never referenced on the template
_signal = signal;
@api apiSignalValue = signal.value;
@track trackSignalValue = signal.value;
observedFieldExternalSignalValue = signal.value;
observedFieldBoundSignalValue = this._signal.value;

get externalSignalValueGetter() {
return signal.value;
}

@api
getSignalSubscriberCount() {
return signal.getSubscriberCount();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<x-child lwc:if={showChild} signal={signal}></x-child>
</template>
Loading