-
Creating Web Component
1.1 basic component
1.2 components inputs by attributes
1.3 components inputs/outputs by events\ -
Testing
2.1 first test
2.2 snapshot
2.3 test with test doubles
2.4 playwright\
In new .ts file :
- Create template either
const template = `<template><div>Hello !</div></template>`
/!\ this one cannot be changed after the first rendering
or
const template = document.createElement('template');
function createTemplate(name: string): string {
return `<div>Hello ${name} !</div>`
}
- Create component by Extending CustomHTMLElement
export class MyComponent extends CustomHTMLElement {
constructor() {
super()
/* Attach template to shadow DOM */
this.attachShadow({ mode: 'open' }).appendChild(
template.content.cloneNode(true),
)
}
/* connectedCallback() is automatically called once on first rendering */
connectedCallback() {
/* Create template with variable */
const name = 'Maxime'
const newTemplate = createTemplate(name)
this.renderComponent(newTemplate)
}
}
- Define your new tag
/* Define tag with 'arl' prefix */
customElements.define('arl-my-component', MyComponent)
/!\ prefix is mandatory, prevent collision of tag names
- Where you want to use it In HTML template
<arl-my-component></arl-my-component>
In TS file
import './src/components/MyComponent/MyComponent'
@see example
in HTLM
<arl-footer is-user-logged-in="false"></arl-footer>
/!\ attributes must be in lowercase
in TS
class MyComponent extends CustomHTMLElement {
//...
static get observedAttributes() {
return ['is-user-logged-in']
}
// called an attribute value of the tag changes
attributeChangedCallback(
attributeName: string,
_oldValue: string,
newValue: string,
) {
//attributeName = 'is-user-logged-in' (if this is the attribute that just changed)
//newValue = 'false' (in this example)
//oldValue is the previous value
}
}
@see example
In HTML
<body>
<custom-element-imperative></custom-element-imperative>
</body>
output : to send data out of a component
window.dispatchEvent(
new CustomEvent('MY_EVENT', { detail: { payload: 'Toto' } }),
)
input : to get data from outside another component
window.addEventListener(
'MY_EVENT',
callback((event: CustomEvent) => console.log(event.detail.payload)),
)
in myComponent.spec.ts
//import your component
import { MyComponent } from './my-component'
it('title should be the right thing', () => {
//given
const component = new MyComponent()
//when
// call the connectedCallback to mimic the rendering of the component (=init)
component.connectedCallback()
//then
//get dom element
const title = component.shadowRoot?.querySelector('.title')?.textContent
//assertion
expect(title).toEqual('the right thing')
})
in myComponent.spec.ts
const component = new MyComponent()
component.connectedCallback()
expect(component.shadowRoot?.querySelector('#component-id')).toMatchSnapshot()
-
The first time you run the test, it creates a file with the name of your test file that is the snapshot template of the component in "snapshots" folder,
-
next runs, this automatically generated file will be used to compare the snapshot of the component with the one generated by the test
SPY
const spyDispatchEvent = jest.spyOn(window, 'dispatchEvent');
const expectedParameters = (spyDispatchEvent.mock.calls[nthCall][nthParameter] as CustomEvent).detail /* nthCall: represent the nth call that we want to watch, nthParameter represent the nth parameter sent that we want to watch */
expect(expectedBetChoice).toEqual({ ... })
STUB
const stub = { key: 'value' }
jest.mock('../../services', () => ({
fetchGameOdds: jest.fn().mockResolvedValue(stub),
}))
const browser: Browser = await chromium.launch({
headless: true,
slowMo: 0,
devtools: false,
})
const context: BrowserContext = await browser.newContext()
const page: Page = await context.newPage()
await myComponent.isBlockDisplayed('tag or CSS selectors')
await input.type('100')
await input.press('Backspace')
await myComponent.getSummaryContent()
import { StorybookControls } from '../../models'
import './footer'
export default {
title: 'Components/Footer',
argTypes: {
isUserConnected: {
control: 'boolean',
defaultValue: false,
},
},
}
type ArgTypes = {
isUserConnected: StorybookControls
}
export const Default = (argTypes: ArgTypes) =>
`<arl-footer is-user-logged-in="${argTypes.isUserConnected}"></arl-footer>`