diff --git a/packages/web-components/IMPLEMENTATION_NOTES.md b/packages/web-components/IMPLEMENTATION_NOTES.md index 16bf97b9176..a7fc3931a87 100644 --- a/packages/web-components/IMPLEMENTATION_NOTES.md +++ b/packages/web-components/IMPLEMENTATION_NOTES.md @@ -27,6 +27,18 @@ - [Masthead search](#masthead-search) - [TypeScript type definition](#typescript-type-definition) - [Vendor directory](#vendor-directory) +- [Unit tests](#unit-tests) + - [Test setup](#test-setup) + - [Writing tests](#writing-tests) + - [Rendering a template](#rendering-a-template) + - [Waiting for template to be updated](#waiting-for-template-to-be-updated) + - [Changing a public property](#changing-a-public-property) + - [Changing a private property](#changing-a-private-property) + - [Simulating a user action](#simulating-a-user-action) + - [Inspecting a DOM element or a component property](#inspecting-a-dom-element-or-a-component-property) + - [Seeing if a particular method has been called or a particular event has been fired](#seeing-if-a-particular-method-has-been-called-or-a-particular-event-has-been-fired) + - [Defining mocks](#defining-mocks) + - [Restoring state](#restoring-state) - [RTL support](#rtl-support) @@ -279,6 +291,86 @@ The `import`s of `@carbon/ibmdotcom-services-store` code in `@carbon/ibmdotcom-w import { loadLanguage, setLanguage } from '../../internal/vendor/@carbon/ibmdotcom-services-store/actions/localeAPI'; ``` +## Unit tests + +Unit tests for `@carbon/ibmdotcom-web-components` uses [Karma](http://karma-runner.github.io/latest/index.html), so that we can inspect the DOM to debug unit tests. `@carbon/ibmdotcom-web-components` uses [Jasmine](https://jasmine.github.io) for test definition and assertion, with a [custom matcher for snapshots](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/tests/utils/snapshot.js). + +Please refer to [our developer documentation](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/docs/contributing-to-web-components.md#unit-test-coverage) to see how to run the tests. + +### Test setup + +The Karma configuration can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/tests/karma.conf.js). + +`@carbon/ibmdotcom-web-components` uses [`karma-webpack`](https://www.npmjs.com/package/karma-webpack) to resolve ECMAScript modules in tests, and uses [`karma-snapshot`](https://www.npmjs.com/package/karma-snapshot) to save/load the snapshots. + +### Writing tests + +Most unit tests consists of the following parts: + +1. Render a template +1. Wait for the template to be rendered +1. (Optional) Do either of: + + 1. Change a public property or a private property + 1. Simulate a user action + +1. (Optional) Wait for the template to be updated upon above change +1. Do one of below to see if above yields to the desired result: + + 1. Inspect the DOM content + 2. Inspect a component property + 3. See if a particular method has been called or a particular event has been fired + +#### Rendering a template + +We use [`lit-html`](https://lit-html.polymer-project.org), the underlying template library of `lit-element`, for that purpose. In some cases, we reuse templates from [Storybook CSF](https://storybook.js.org/docs/react/api/csf), [the test for ``](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/leadspace-block/__tests__/leadspace-block.test.ts#L12-L30) is an example. + +#### Waiting for template to be updated + +`lit-html` renders template asynchronously, using [microtask](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model), so that rendering template can be done in batch for multiple changes in template variables. That means the change in template is not reflected to the DOM right away. + +Creating a promise and waiting for it ensures that the subsequent code, e.g. assertions, run after pre-scheduled microtasks are done. We do it by: + +```javascript +await Promise.resolve(); +``` + +If multiple `lit-html` templates are in play for tests, multiple template update cycles (microtasks) may be in play. For example, rendering `` in a test template involves three update cycles, one is of test template, one is of `` that registers its `slotchange` event handler, one is of `` again the updates the template upon `slotchange` event. In such case we need to wait for microtask to be done multiple times. + +#### Changing a public property + +Changing a public property in test allows us to see if the template is rendered correctly and DOM is updated correctly, upon change in public property. To do that, we can simply grab the DOM node of the component and change public propery of the DOM node. An example can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L73). + +#### Changing a private property + +Changing a private property in test allows us to quickly set up the test environment for particular case, in an isolated manner. For example, `` has a [private property that reflect the space between cards](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/carousel.ts#L46) that is updated by `slotchange` event handler as well as by `ResizeObserver` callback. Directly updating such private property allows us to quickly [test our code to see if the component behaves correctly upon change in such private property](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L63-L65). + +#### Simulating a user action + +There are a couple of common ways to simulate a user action. The easiest one is using [`HTMLElement.click()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). Another one is creating a custom event and running [`.dispatchEvent()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent). An example of the latter can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/footer/__tests__/footer-nav-group.test.ts#L102-L103). + +#### Inspecting a DOM element or a component property + +We can simply grab a component DOM to see whether its property or shadow DOM content is correct. An example of the former can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L204-L206). An example of the latter can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L127-L129). + +#### Seeing if a particular method has been called or a particular event has been fired + +To see if a particular method has been called or a particular event has been fired, we use [spies](https://jasmine.github.io/2.0/introduction#section-Spies). An example can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/expressive-modal/__tests__/expressive-modal.test.ts#L121-L127). + +### Defining mocks + +The core notion of unit tests is running tests in an isolated environment, so that minimal factors affect whether the tests pass or fail. [Mocks](https://en.wikipedia.org/wiki/Mock_object) are very useful to achieve that. + +An example of setting mocks via Jasmine API can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/lightbox-media-viewer/__tests__/lightbox-media-viewer-container.test.ts#L25-L39). An example of manually setting mocks can be found [here](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L35-L44). + +### Restoring state + +It's important for each tests to ensure the test environment is re-set back to the original state after the test runs, no matter whether the test passes or fails. Lack of such discipline often causes flaky tests. + +The common way to ensure that is [`beforeAll()`](https://jasmine.github.io/api/3.5/global.html#beforeAll)/[`beforeEach()`](https://jasmine.github.io/api/3.5/global.html#beforeEach)/[`afterEach()`](https://jasmine.github.io/api/3.5/global.html#afterEach)/[`afterAll()`](https://jasmine.github.io/api/3.5/global.html#afterAll). For example, most tests [render an empty template](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L318) after test is finished. Tests using mocks (except one set by Jasmine API) must [reset them](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/carousel/__tests__/carousel.test.ts#L324-L325) after test is finished. + +We have [our own utility](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/tests/utils/event-manager.ts) to clean-up event handlers after tests. With that utilitity, we can attach events by `events.on(element, eventName, eventHandler)` ([example](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/expressive-modal/__tests__/expressive-modal.test.ts#L232)) and clean them up by `events.reset()` ([example](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/blob/v1.15.0/packages/web-components/src/components/expressive-modal/__tests__/expressive-modal.test.ts#L264)). + ## RTL support While [CSS logical properties and values](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) will allow us to support LTR and RTL in one codebase, there are several key properties that are not yet supported by some browsers, for example, [`inset-inline-start`](https://developer.mozilla.org/en-US/docs/Web/CSS/inset-inline-start).