{/* prettier-ignore */}
{/*
Copyright 2023 Comcast Cable Communications Management, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

SPDX-License-Identifier: Apache-2.0
\*/}

import { Meta } from '@storybook/blocks';

<Meta title="Docs / Unit Testing / Test Utils / makeCreateComponent " />

# makeCreateComponent

Returns a function, referred to as a `createComponent` function, which is used to generate an instance of a component to run unit test cases against.

## Arguments

`makeCreateComponent(component, defaultConfig, defaultOptions)`

| argument       | type          | default   | description                                                                                                                |
| -------------- | ------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- |
| component      | lng.Component | undefined | component to render an instance of                                                                                         |
| defaultConfig  | object        | {}        | properties that should be applied to the component by default                                                              |
| defaultOptions | object        | {}        | default options used when creating the component instance (these options are passed to the `TestRenderer.create` function) |

### `createComponent`

Returns an array of 2 elements:

1. an instance of the component to be tested against (generated by `testRenderer.getInstance()`)
2. a [testRenderer](/story/docs-unit-testing-test-renderer-create--page#testrenderer-functions) object generated using the `component` argument and a merged object created from `defaultOptions` and `options`

#### Arguments

| argument | type   | default | description                                                                                                                                                                |
| -------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| config   | object | {}      | properties that should be applied to the component. This object is spread over the `defaultConfig` object.                                                                 |
| options  | object | {}      | options used when creating the component instance (these options are passed to the `TestRenderer.create` function). This object is spread over the `defaultOptions` object |

<br />

##### Example: Generating a `makeComponent` function and test component

```js
import lng from '@lightningjs/core';
import { makeCreateComponent } from '@lightningjs/ui-components-test-utils';

// Lightning component to write unit tests for
class Example extends lng.Component {
  static _template() {
    return {
      Example: {
        text: { text: '' }
      }
    };
  }

  set exampleProp(value) {
    this._exampleProp = value;
    this.tag('Example').text.text = value;
  }

  get exampleProp() {
    return this._exampleProp;
  }
}

// unit tests for Example component

// generate a createComponent function to use in tests
const defaultConfig = { exampleProp: 'foo' };
const defaultOptions = { focused: false };
const createExampleComponent = makeCreateComponent(
  Example,
  defaultConfig,
  defaultOptions
);

// Ex. An instance of the component rendered with the properties defined in the defaultConfig and defaultOptions object
it('should render the exampleProp value as text', () => {
  const [example] = createExampleComponent();

  expect(example.exampleProp).toBe('foo');
  expect(example.tag('Example').text.text).toBe('foo');
  expect(example.hasFocus()).toBe(false);
});

// Ex. An instance of the component rendered with the properties defined in the config object
it('should support updating the text', () => {
  // overwrite the value of exampleProp defined in defaultConfig
  const [example] = createExampleComponent({
    exampleProp: 'bar'
  });

  expect(example.exampleProp).toBe('bar');
  expect(example.tag('Example').text.text).toBe('bar');
});

// Ex. An instance of the component rendered with options defined in the options object
it('should support rendering the component in a focused state', () => {
  // overwrite the value of focused defined in defaultOptions
  const [example] = createExampleComponent({}, { focused: true });

  expect(example.hasFocus()).toBe(true);
});

// Ex. Using a function provided by the testRenderer object (focus)
it('should allow focusing on the component', () => {
  const [example, testRenderer] = createExampleComponent();

  // focused was set to false in defaultOptions, so the componet will initially render without focus
  expect(example.hasFocus()).toBe(false);

  testRenderer.focus();

  expect(example.hasFocus()).toBe(true);
});
```

### spyOnMethods

A utility which enables running component methods asynchronously.

The `options` argument passed to the `createComponent` function accepts a field called `spyOnMethods`.
The value for `spyOnMethods` can be an array of method names to generate promises from. When one of those methods is invoked in the component it's associated promise will resolve, then the actual method will be invoked. The promises generated from the passed in methods are given the name `_${methodName}SpyPromise`.

<br />

##### Example: Spying on component methods and awaiting their execution before making assertions on their side effects

```js
class MethodSpiesExample extends Base {
  static get __componentName() {
    return 'MethodSpiesExample';
  }

  static _template() {
    return {
      Title: {
        type: TextBox
      },
      Subtitle: {
        type: TextBox,
        y: 100
      }
    };
  }

  static get tags() {
    return ['Title', 'Subtitle'];
  }

  static get properties() {
    return ['title', 'subtitle'];
  }

  _construct() {
    super._construct();
    this._title = 'title';
    this._subtitle = 'subtitle';
  }

  _update() {
    this._updateTitle();
    this._updateSubtitle();
  }

  _updateTitle() {
    this._Title.content = this.title;
  }
  _updateSubtitle() {
    this._Subtitle.content = this.subtitle;
  }
}

const createComponent = makeCreateComponent(MethodSpies);

it('should update the title and subtitle', async () => {
  const [methodSpiesComponent, testRenderer] = createComponent(
    {},
    { spyOnMethods: ['_update'] }
  );

  // wait for _update to be called on the initial render before proceeding
  await methodSpiesComponent.__updateSpyPromise;

  expect(methodSpiesComponent._Title.content).toBe('title');
  expect(methodSpiesComponent._Subtitle.content).toBe('subtitle');

  const newTitle = 'new title';
  const newSubtitle = 'new subtitle';
  methodSpiesComponent.patch({
    title: newTitle,
    subtitle: newSubtitle
  });

  // wait for _update to be called in response to values in the properties array changing
  await methodSpiesComponent.__updateSpyPromise;

  expect(methodSpiesComponent._Title.content).toBe(newTitle);
  expect(methodSpiesComponent._Subtitle.content).toBe(newSubtitle);
});
```

**Note: `spyOnMethods` can only be set in in the `options` object of `createComponent`. It can not be assigned to the `defaultOptions` object in `makeCreateComponent`**
