-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(atomic): add tests to binding decorators (#4911)
* Add tests to various binding decorators * Move `fetchBindings` into separate file * fix small bugs on the decorators https://coveord.atlassian.net/browse/KIT-3901 --------- Co-authored-by: Frederic Beaudoin <[email protected]>
- Loading branch information
1 parent
e3599bd
commit 12c4aac
Showing
11 changed files
with
545 additions
and
41 deletions.
There are no files selected for viewing
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
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,152 @@ | ||
import {type Controller} from '@coveo/headless'; | ||
import {LitElement} from 'lit'; | ||
import {customElement} from 'lit/decorators.js'; | ||
import {vi} from 'vitest'; | ||
import type {Bindings} from '../components/search/atomic-search-interface/interfaces'; | ||
import {bindStateToController} from './bind-state'; | ||
import type {InitializableComponent} from './types'; | ||
|
||
class MockController implements Controller { | ||
state = {}; | ||
subscribe = vi.fn((callback) => { | ||
this.callback = callback; | ||
return () => {}; | ||
}); | ||
|
||
callback?: () => void; | ||
|
||
updateState(newState: {}) { | ||
this.state = newState; | ||
this.callback?.(); | ||
} | ||
} | ||
|
||
describe('@bindStateToController decorator', () => { | ||
const onUpdateCallbackMethodSpy = vi.fn(); | ||
let element: InitializableComponent<Bindings> & LitElement; | ||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>; | ||
let controller: MockController; | ||
|
||
@customElement('missing-initialize') | ||
class MissingInitialize extends LitElement { | ||
controller!: MockController; | ||
@bindStateToController('controller') | ||
controllerState!: MockController['state']; | ||
} | ||
|
||
@customElement('missing-property') | ||
class MissingProperty extends LitElement { | ||
// @ts-expect-error - invalid property | ||
@bindStateToController('invalidProperty') | ||
controllerState!: MockController['state']; | ||
initialize() {} | ||
} | ||
|
||
@customElement('test-element') | ||
class TestElement extends LitElement { | ||
controller!: MockController; | ||
@bindStateToController('controller') | ||
controllerState!: MockController['state']; | ||
initialize() { | ||
this.controller = controller; | ||
} | ||
} | ||
|
||
@customElement('test-element-callback') | ||
class TestElementCallback extends LitElement { | ||
controller!: MockController; | ||
@bindStateToController('controller', { | ||
onUpdateCallbackMethod: 'onUpdateCallbackMethod', | ||
}) | ||
controllerState!: MockController['state']; | ||
onUpdateCallbackMethod = onUpdateCallbackMethodSpy; | ||
initialize() { | ||
this.controller = controller; | ||
} | ||
} | ||
|
||
@customElement('test-element-no-callback') | ||
class TestElementNoCallback extends LitElement { | ||
controller!: MockController; | ||
@bindStateToController('controller', { | ||
onUpdateCallbackMethod: 'nonExistentMethod', | ||
}) | ||
controllerState!: MockController['state']; | ||
|
||
initialize() { | ||
this.controller = controller; | ||
} | ||
} | ||
|
||
const setupElement = async <T extends LitElement>(tag = 'test-element') => { | ||
element = document.createElement(tag) as InitializableComponent<Bindings> & | ||
T; | ||
document.body.appendChild(element); | ||
await element.updateComplete; | ||
}; | ||
|
||
const teardownElement = () => { | ||
document.body.removeChild(element); | ||
}; | ||
|
||
beforeEach(async () => { | ||
consoleErrorSpy = vi.spyOn(console, 'error'); | ||
controller = new MockController(); | ||
await await setupElement<TestElement>(); | ||
}); | ||
|
||
afterEach(() => { | ||
teardownElement(); | ||
consoleErrorSpy.mockRestore(); | ||
}); | ||
|
||
it('it should not disturb the render life cycle', async () => { | ||
element.initialize!(); | ||
expect(element.hasUpdated).toBe(true); | ||
}); | ||
|
||
it('it should not log an error to the console when the "initialize" method and the "controller" property are defined', () => { | ||
element.initialize!(); | ||
expect(consoleErrorSpy).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('it should log an error to the console when "controller" property is not defined', async () => { | ||
await setupElement<MissingProperty>('missing-property'); | ||
element.initialize!(); | ||
expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
'invalidProperty property is not defined on component', | ||
element | ||
); | ||
}); | ||
|
||
it('it should log an error to the console when the "initialize" method is not defined', async () => { | ||
await setupElement<MissingInitialize>('missing-initialize'); | ||
element.initialize!(); | ||
expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
'ControllerState: The "initialize" method has to be defined and instantiate a controller for the property controller', | ||
element | ||
); | ||
}); | ||
|
||
it('should subscribe to the controller', async () => { | ||
element.initialize!(); | ||
expect(controller.subscribe).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should call the onUpdateCallbackMethod if specified', async () => { | ||
await setupElement<TestElementCallback>('test-element-callback'); | ||
element.initialize!(); | ||
controller.updateState({value: 'updated state'}); | ||
expect(onUpdateCallbackMethodSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should log an error if the onUpdateCallbackMethod is not defined', async () => { | ||
await setupElement<TestElementNoCallback>('test-element-no-callback'); | ||
element.initialize!(); | ||
controller.updateState({value: 'updated state'}); | ||
expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
'ControllerState: The onUpdateCallbackMethod property "nonExistentMethod" is not defined', | ||
element | ||
); | ||
}); | ||
}); |
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
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,89 @@ | ||
import { | ||
buildSearchEngine, | ||
getSampleSearchEngineConfiguration, | ||
} from '@coveo/headless'; | ||
import {LitElement, html} from 'lit'; | ||
import {customElement, state} from 'lit/decorators.js'; | ||
import {vi} from 'vitest'; | ||
import type {Bindings} from '../components/search/atomic-search-interface/interfaces'; | ||
import {bindingGuard} from './binding-guard'; | ||
|
||
describe('@bindingGuard decorator', () => { | ||
let element: TestElement; | ||
const renderSpy = vi.fn(); | ||
const bindings = { | ||
engine: buildSearchEngine({ | ||
configuration: getSampleSearchEngineConfiguration(), | ||
}), | ||
} as Bindings; | ||
|
||
@customElement('test-element') | ||
class TestElement extends LitElement { | ||
@state() bindings!: Bindings; | ||
@bindingGuard() | ||
public render() { | ||
renderSpy(); | ||
return html`<div>Content to render when bindings are present</div>`; | ||
} | ||
} | ||
|
||
const setupElement = async () => { | ||
element = document.createElement('test-element') as TestElement; | ||
document.body.appendChild(element); | ||
await element.updateComplete; | ||
}; | ||
|
||
const teardownElement = () => { | ||
document.body.removeChild(element); | ||
}; | ||
|
||
beforeEach(async () => { | ||
await setupElement(); | ||
}); | ||
|
||
afterEach(() => { | ||
teardownElement(); | ||
renderSpy.mockRestore(); | ||
}); | ||
|
||
it('should render the original content when bindings are present', async () => { | ||
element.bindings = bindings; | ||
await element.updateComplete; | ||
|
||
expect(element.shadowRoot?.textContent).toContain( | ||
'Content to render when bindings are present' | ||
); | ||
|
||
expect(renderSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should render nothing when bindings are not present', async () => { | ||
// @ts-expect-error - testing invalid binding | ||
element.bindings = undefined as Bindings; | ||
await element.updateComplete; | ||
|
||
expect(element.shadowRoot?.textContent).toBe(''); | ||
expect(renderSpy).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should throw an error if used on a property', () => { | ||
expect(() => { | ||
// @ts-expect-error - unused class | ||
class _ { | ||
// @ts-expect-error - invalid usage | ||
@bindingGuard() myProp?: string; | ||
} | ||
}).toThrow('@bindingGuard decorator can only be used on render method'); | ||
}); | ||
|
||
it('should throw an error if used on a method other than render', () => { | ||
expect(() => { | ||
// @ts-expect-error - unused class | ||
class _ { | ||
// @ts-expect-error - invalid usage | ||
@bindingGuard() | ||
public someMethod() {} | ||
} | ||
}).toThrow('@bindingGuard decorator can only be used on render method'); | ||
}); | ||
}); |
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
Oops, something went wrong.