-
Notifications
You must be signed in to change notification settings - Fork 399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: signals test cases #3962
Merged
Merged
feat: signals test cases #3962
Changes from 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
770670f
chore: initial set of test cases for signals implementation
jmsjtu 4439e74
chore: revert playground changes
jmsjtu 81e4a1a
Merge branch 'jtu/signals' of github.com:salesforce/lwc into jtu/sign…
jmsjtu e4d348f
Merge branch 'jtu/signals' of github.com:salesforce/lwc into jtu/sign…
jmsjtu 1c736a7
chore: move unit tests to @lwc/signals package
jmsjtu a81647d
Merge branch 'jtu/signals' of github.com:salesforce/lwc into jtu/sign…
jmsjtu e161656
chore: update config for signals feature flag
jmsjtu a278dda
chore: update tests
jmsjtu 8019c24
Merge branch 'jtu/signals' of github.com:salesforce/lwc into jtu/sign…
jmsjtu d140646
Merge branch 'jtu/signals' of github.com:salesforce/lwc into jtu/sign…
jmsjtu 1013936
chore: update based on pr feedback
jmsjtu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
packages/@lwc/engine-core/src/libs/signal/__tests__/index.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* Copyright (c) 2023, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: MIT | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
*/ | ||
|
||
import { Signal } from './signal'; | ||
|
||
describe('signal protocol', () => { | ||
it('should be able to retrieve value', () => { | ||
const s = new Signal(1); | ||
expect(s.value).toBe(1); | ||
}); | ||
|
||
it('should be able to subscribe to signal', () => { | ||
const s = new Signal(); | ||
expect('subscribe' in s).toBe(true); | ||
expect(typeof s.subscribe).toBe('function'); | ||
const onUpdate = jest.fn(); | ||
expect(() => s.subscribe(onUpdate)).not.toThrow(); | ||
}); | ||
|
||
it('should be able to notify subscribers', () => { | ||
const s = new Signal(); | ||
const onUpdate = jest.fn(); | ||
s.subscribe(onUpdate); | ||
s.value = 1; | ||
expect(onUpdate).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('subscribe should return an unsubscribe function', () => { | ||
const s = new Signal(); | ||
const onUpdate = jest.fn(); | ||
const unsubscribe = s.subscribe(onUpdate); | ||
expect(typeof unsubscribe).toBe('function'); | ||
}); | ||
|
||
it('should not notify once unsubscribed', () => { | ||
const s = new Signal(0); | ||
const onUpdate1 = jest.fn(); | ||
const onUpdate2 = jest.fn(); | ||
const unsubscribe1 = s.subscribe(onUpdate1); | ||
const unsubscribe2 = s.subscribe(onUpdate2); | ||
|
||
s.value = 1; | ||
expect(onUpdate1).toHaveBeenCalledTimes(1); | ||
expect(onUpdate2).toHaveBeenCalledTimes(1); | ||
|
||
unsubscribe1(); | ||
|
||
s.value = 2; | ||
expect(onUpdate1).toHaveBeenCalledTimes(1); | ||
expect(onUpdate2).toHaveBeenCalledTimes(2); | ||
|
||
unsubscribe2(); | ||
|
||
s.value = 3; | ||
expect(onUpdate1).toHaveBeenCalledTimes(1); | ||
expect(onUpdate2).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('SignalBaseClass does not subscribe duplicate OnUpdate callback functions', () => { | ||
const s = new Signal(0); | ||
const onUpdate = jest.fn(); | ||
s.subscribe(onUpdate); | ||
s.subscribe(onUpdate); | ||
|
||
s.value = 1; | ||
expect(onUpdate).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should be able to reference other signals in subscription', () => { | ||
const s1 = new Signal(0); | ||
const s2 = new Signal(1); | ||
const s3 = new Signal(1); | ||
|
||
s2.subscribe(() => (s1.value = s2.value + s3.value)); | ||
s3.subscribe(() => (s1.value = s2.value + s3.value)); | ||
|
||
s2.value = 2; | ||
expect(s1.value).toBe(3); | ||
|
||
s3.value = 3; | ||
expect(s1.value).toBe(5); | ||
}); | ||
}); |
26 changes: 26 additions & 0 deletions
26
packages/@lwc/engine-core/src/libs/signal/__tests__/signal.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* | ||
* Copyright (c) 2023, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: MIT | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
*/ | ||
|
||
import { SignalBaseClass } from '../index'; | ||
|
||
export class Signal extends SignalBaseClass<any> { | ||
_value; | ||
|
||
constructor(initialValue?: any) { | ||
super(); | ||
this._value = initialValue; | ||
} | ||
|
||
set value(newValue) { | ||
this._value = newValue; | ||
this.notify(); | ||
} | ||
|
||
get value() { | ||
return this._value; | ||
} | ||
} |
95 changes: 95 additions & 0 deletions
95
packages/@lwc/integration-karma/test/signal/protocol/index.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { createElement } from 'lwc'; | ||
import Reactive from 'x/reactive'; | ||
import NonReactive from 'x/nonReactive'; | ||
import Parent from 'x/parent'; | ||
import Child from 'x/child'; | ||
import { Signal } from 'x/signal'; | ||
|
||
describe('signal protocol', () => { | ||
describe('lwc engine subscribes template re-render callback when signal is bound to an LWC and used on a template', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests validate when the framework will attempt to subscribe to a signal used in an LWC. |
||
[ | ||
{ | ||
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 the 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: Parent }); | ||
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); | ||
|
||
signal.value = 'updated value'; | ||
|
||
await Promise.resolve(); | ||
|
||
expect(signal.getSubscriberCount()).toBe(1); | ||
expect(container.renderCount).toBe(1); | ||
expect(signalElm.renderCount).toBe(2); | ||
}); | ||
}); |
3 changes: 3 additions & 0 deletions
3
packages/@lwc/integration-karma/test/signal/protocol/x/child/child.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<template> | ||
{signal.value} | ||
</template> |
14 changes: 14 additions & 0 deletions
14
packages/@lwc/integration-karma/test/signal/protocol/x/child/child.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { LightningElement, api } from 'lwc'; | ||
|
||
export default class extends LightningElement { | ||
@api renderCount = 0; | ||
@api signal; | ||
|
||
constructor() { | ||
super(); | ||
} | ||
|
||
renderedCallback() { | ||
this.renderCount++; | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
packages/@lwc/integration-karma/test/signal/protocol/x/nonReactive/nonReactive.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
26 changes: 26 additions & 0 deletions
26
packages/@lwc/integration-karma/test/signal/protocol/x/nonReactive/nonReactive.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
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; | ||
// Note in the tests we use createElement outside of the LWC engine and therefore no template is | ||
// actively rendering which is why this does not get automatically registered. | ||
observedFieldBoundSignalValue = this._signal.value; | ||
|
||
get externalSignalValueGetter() { | ||
// Note that the value of the signal is not bound to the class and will therefore not be | ||
// automatically subscribed to the re-render. | ||
return signal.value; | ||
} | ||
|
||
@api | ||
getSignalSubscriberCount() { | ||
return signal.getSubscriberCount(); | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/@lwc/integration-karma/test/signal/protocol/x/parent/parent.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<template> | ||
<slot></slot> | ||
</template> |
9 changes: 9 additions & 0 deletions
9
packages/@lwc/integration-karma/test/signal/protocol/x/parent/parent.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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++; | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
packages/@lwc/integration-karma/test/signal/protocol/x/reactive/reactive.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<template> | ||
<span lwc:if={showApiSignal}>{apiSignal.value}</span> | ||
<span lwc:if={showGetterSignal}>{getterSignalField.value}</span> | ||
<span lwc:if={showTrackedSignal}>{trackSignal.value}</span> | ||
<span lwc:if={showObservedFieldSignal}>{observedFieldSignal.value}</span> | ||
<span lwc:if={showOnlyUsingSignalNotValue}>{observedFieldSignal}</span> | ||
<span lwc:if={showGetterSignalValue}>{getterSignalFieldValue}</span> | ||
</template> |
32 changes: 32 additions & 0 deletions
32
packages/@lwc/integration-karma/test/signal/protocol/x/reactive/reactive.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { LightningElement, api, track } from 'lwc'; | ||
import { Signal } from 'x/signal'; | ||
|
||
const signal = new Signal('initial value'); | ||
|
||
export default class extends LightningElement { | ||
@api showApiSignal = false; | ||
@api showGetterSignal = false; | ||
@api showGetterSignalValue = false; | ||
@api showTrackedSignal = false; | ||
@api showObservedFieldSignal = false; | ||
@api showOnlyUsingSignalNotValue = false; | ||
|
||
@api apiSignal = signal; | ||
@track trackSignal = signal; | ||
|
||
observedFieldSignal = signal; | ||
|
||
get getterSignalField() { | ||
// this works because the signal is bound to the LWC | ||
return this.observedFieldSignal; | ||
} | ||
|
||
get getterSignalFieldValue() { | ||
return this.observedFieldSignal.value; | ||
} | ||
|
||
@api | ||
getSignalSubscriberCount() { | ||
return signal.getSubscriberCount(); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/@lwc/integration-karma/test/signal/protocol/x/signal/signal.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { SignalBaseClass } from 'lwc'; | ||
|
||
export class Signal extends SignalBaseClass { | ||
constructor(initialValue) { | ||
super(); | ||
this._value = initialValue; | ||
} | ||
|
||
set value(newValue) { | ||
this._value = newValue; | ||
this.notify(); | ||
} | ||
|
||
get value() { | ||
return this._value; | ||
} | ||
|
||
getSubscriberCount() { | ||
return this.subscribers.size; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created these tests as a basic sanity check for behavior we're expecting from a signals implementation.
The idea is for anyone planning to implement the interface to understand how the framework expects it to behave.