# BSDS Components Core Package

This is the core dependency for our framework-compatible packages:
- [**@wwtdev/bsds-components-vue2**](https://www.npmjs.com/package/@wwtdev/bsds-components-vue2)
- [**@wwtdev/bsds-components-vue3**](https://www.npmjs.com/package/@wwtdev/bsds-components-vue3)

## Component & Unit Testing

The following packages have been installed for component / unit testing:
- [Stencil Core Testing](https://stenciljs.com/docs/unit-testing) - Testing library included with Stencil.
- [Jest](https://jestjs.io/) - JavaScript testing framework.
- [@testing-library/dom](https://testing-library.com/docs/dom-testing-library/intro/) - DOM Testing Library which provides various utilities for querying the DOM.
- [Faker](https://fakerjs.dev/) - Helps generate fake (but realistic) data for testing.

### Component vs Unit Tests

There are a couple key differences between Component and Unit testing. For the majority of this component library, we will be writing Component tests. The Vue documentation site actually does a very good job of explaining the differences ([Unit Testing](https://vuejs.org/guide/scaling-up/testing.html#unit-testing) vs [Component Testing](https://vuejs.org/guide/scaling-up/testing.html#component-testing)), however, here are some key differences for quick reference:

*Unit Testing*
> These should primarily test isolated "units" of business code (functions, composable, etc.). Typically written for utility functions, custom hooks, or composables, but not actual Web components.

*Component Testing*
> These tests focus on a component's props, events, slots, styles, classes, hooks, etc. Primarily write tests which set/update props, fire events, etc. which should update the presentation of the component. Then, check the outputted component code.

## Run Unit Tests

Runs the full unit test suite.

`npm run test`

## Watch Unit Tests

Runs and watches for changes on the full unit test suite.

`npm run test:watch`

## Run Unit Tests with Coverage

Runs the full unit test suite and outputs a coverage report. This report can be seen both in the console output and as an HTML page at _coverage/lcov-report/index.html_.

`npm run test:coverage`

## Run Specific Unit Test(s)

> There's a slight [issue](https://github.com/ionic-team/stencil/pull/3444) with Stencil v2.13.0 that won't let Jest CLI arguments pass through properly, so these instructions will change when/if our Stencil version is upgraded.

Add `-- t [test name pattern]` to any test command (except coverage) to run for a particular file pattern.
- `npm run test -- t [test name pattern]`
- `npm run test:watch -- t [test name pattern]`

For coverage, just add `-- [test name pattern]`.
- `npm run test:coverage -- [test name pattern]`

Examples:
- `npm run test -- t bs-button.spec.tsx` runs just `bs-button.spec.tsx`.
- `npm run test:watch -- t bs-banner` runs just `bs-banner.spec.tsx` and watches for changes.
- `npm run test -- t profile` runs all tests with "profile" in the name .(`bs-profile-img.spec.tsx`, `bs-profile-layout.spec.tsx`, `bs-profile.spec.tsx`, `bs-profile-details.spec.tsx`).
- `npm run test:coverage -- bs-banner` runs coverage report for `bs-banner.spec.tsx`.

## Testing Tips & Tricks

### Test Structure
- Suggest using the `describe()` function from Jest to structure the test in a meaningful way. For example, an overall `describe('Component Tests')` for containing all the tests then another `describe('functionName()')` for each function.
- `it()` from Jest should be used to describe the test input/output. For example, `it('should do this when this and that are set')`.
- `beforeEach()` and `afterEach()` can be used for setting up/cleaning up everything within a `describe()` section.

### Mounting for Component Tests
- Mount components using `newSpecPage()`. For example:
```javascript
const page = await newSpecPage({
  components: [BsBadge],
  html: `<bs-badge color="blue">Some Content</bs-badge>`,
});
```
- `const component = page.root;` to get the root component.
- Components can be queried like any HTML element. For example:
```javascript
const component = page.root.querySelector('.bs-toast') as HTMLElement;

expect(component.getAttribute('data-shown')).toBe('');
```
- Dynamically updating props/state values is possible by using `page.rootInstance`. For example, updating the `isVisible` state can be done by `page.rootInstance.isVisible = true`.
- `await page.waitForChanges()` for waiting for state / props updates to propagate.

### What to Test
- Primarily test rendered component for various things such as attributes, slot content, event handling, etc.
  - Test an attribute: `expect(component.getAttribute('data-shown')).toBe('Some Val');`.
  - Test text content: `expect(component.textContent).toBe('Some Text');`.
- Testing prop values being placed on the DOM adds no real value unless those values are dynamically shown. For example, `:data-text="textBtn ? '' : undefined"`. In this example we would write two tests. One for each branch of the condition.
- Make sure to hit as many branches as possible, edge cases, etc. in other component logic. Check coverage report to see what may be missed.
- Also consider that sometimes a test would not add any meaningful value. For example, writing a unit test for a getter. While this would improve coverage numbers, the test itself adds no value.

### Event Testing
- `@testing-library/dom` adds a very useful function for testing events: `fireEvent`.
- Here's a sample usage a Jest mock and firing an event to test something:
```javascript
const evMock = jest.fn();
element.addEventListener('mouseenter', evMock);

fireEvent.mouseEnter(page.root);

expect(evMock).toHaveBeenCalled();
```
- Firing a custom event:
``` javascript
const ev = new CustomEvent('oncustomname', { detail: 'some event detail' })
fireEvent(page.root, ev)
```

### Miscellaneous Tips
- Use `faker` from `@faker-js/faker` to create realistic test data.
- Jest provides various ways to handle faking timers if a component uses `setTimeout()`. Be sure to `jest.clearAllTimers()` after each test since this can mess with the async component mounting.
```javascript
// Fake-out the timer system for these tests so it's more predictable
beforeEach(() => jest.useFakeTimers());

// Clean up timers in the system
afterEach(() => jest.clearAllTimers());

it('should test some timer features', () => {
  // mount and do some updates to trigger the timer
  jest.advanceTimersByTime(300);
  // expect something to have happened
});
```
- `jest.useFakeTimers()` will not work when a test itself is `async` (e.g. `it('should test something', async () => { // test })`). In this case, here are a couple ways to test async code:
  - One-liner using `Promise` which waits for a 500ms timer:
    ```javascript
      it('should run some async code', async () => {
        ...
        // trigger code here
        ...
        // wait 500ms
        await new Promise(res => setTimeout(res, 500))
        ...
        // expect some result here
        expect('actual').toBe('expected')
      })
    ```
  - Using a `done` argument to the test case which tells the test when it is finished:
    ```javascript
      it('should run some async code', async (done) => {
        ...
        // trigger code here
        ...
        setTimeout(() => {
          // expect some result after 500ms
          expect('actual').toBe('expected')
          ...
          // call done when finished
          done()
        }, 500)
      })
    ```
