Salesforceアプリケーション開発をする際のメモ

# 1. 目的

- 忘れた時に見る
- 検索した際に古い書き方がトップの方に出てきて間違えてしまうことがあったのを防ぎたい

# 2. 参考

- lwc-recipes
  https://github.com/trailheadapps/lwc-recipes
- Lightning Web Components Developer Guide
  https://developer.salesforce.com/docs/platform/lwc/guide

# 3. メモ

- 検証に使用しているデモリポジトリ
  https://github.com/ShigeoTejima/salesforce-1gp-demo


## Lightning Web Component - implementation

### refreshApexで正常の場合、エラーの場合の挙動を定義する

```javascript
refreshApex(this.wiredProperty)
    .then(() => {
      // 正常の場合の挙動を定義する
    })
    .catch(error => {
      // 正常の場合の挙動を定義する
    }
);
```
or
```javascript
async someFunction() {
    try {
        await refreshApex(this.wiredProperty);
      // 正常の場合の挙動を定義する
    } catch (error) {
      // 正常の場合の挙動を定義する
    }
}
```

## Lightning Web Component - test

### 基本的な記述

- `flushPromises`を使用して、コンポーネントの反映を行う

```javascript:__tests_/demo.test.js
describe('c-demo', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }

        jest.clearAllMocks();
    });

    async function flushPromises() {
        return Promise.resolve();
    }

    describe('list of demos', () => {
        it('when data exists', async () => {
            const element = createElement('c-demo', {
                is: Demo
            });
            document.body.appendChild(element);

            // given:
            // - モック定義やテストのための事前操作を記述する
            const mockGetDemos = [
                { "id": 1, "name": "Foo", "description": "Foo Description" },
                { "id": 2, "name": "Bar" }
            ];
            getDemos.emit(mockGetDemos);

            // when:
            // - コンポーネントを更新する
            await flushPromises();

            // then:
            // - 確認対象を検証する
            const datatableElement = element.shadowRoot.querySelector('lightning-datatable');

            expect(datatableElement.data.length).toBe(2);
            expect(datatableElement.data).toEqual([
                { "id": 1, "name": "Foo", "description": "Foo Description" },
                { "id": 2, "name": "Bar" }
            ])
        });

}
```

### `@wire`されたApexメソッドのテスト

- 以下のような実装があるとする
```javascript:demo.js
// ~ 省略 ~
import getDemos from '@salesforce/apex/DemoController.getDemos';

export default class Demo_page extends LightningElement {
    @wire(getDemos)
    demos;

// ~ 省略 ~
```
- Apexメソッドのモック定義
  - `createApexTestWireAdapter`を使用する
```javascript:__tests__/demo.test.js
// ~ 省略 ~
import getDemos from '@salesforce/apex/DemoController.getDemos';

jest.mock(
    '@salesforce/apex/DemoController.getDemos',
    () => {
        const {
            createApexTestWireAdapter
        } = require('@salesforce/sfdx-lwc-jest');
        return {
            default: createApexTestWireAdapter(jest.fn())
        };
    },
    { virtual: true }
);

    describe('demos', () => {

// ~ 省略 ~

```

- Apexメソッドがデータを返す場合
```javascript:__tests__/demo.test.js
    it('when data exists', async () => {

// ~ 省略 ~

        const mockGetDemos = [
            { "id": 1, "name": "Foo", "description": "Foo Description" },
            { "id": 2, "name": "Bar" }
        ];
        getDemos.emit(mockGetDemos);

// ~ 省略 ~

    }

```

- Apexメソッドがエラーとなる場合
```javascript:__tests__/demo.test.js
    it('when returned error to get data', async () => {

// ~ 省略 ~

        getDemos.error();

// ~ 省略 ~

    }

```

### `dispatch`された`ShowToastEvent`のテスト

- 例として、`wire`されたApexメソッドがエラーの場合に、トーストを表示する
```javascript:demo.js
    @wire(getDemos)
    getDemos(value) {
        this.demos = value;
        const { data, error } = value;
        if (error) {
            this.dispatchErrorToast();
        } else if (data) {
            // nop
        }
    }

    dispatchErrorToast() {
        this.dispatchEvent(
            new ShowToastEvent({
                title: 'Error loading Demo',
                message: 'fail to fetch records of demo',
                variant: 'error'
            })
        );
    }
```

```javascript:__tests_/demo.test.js
import { ShowToastEventName } from 'lightning/platformShowToastEvent';

// ~ 省略 ~

describe('c-demo', () => {

// ~ 省略 ~

        it('when returned error to get data', async () => {
            const element = createElement('c-demo-page', {
                is: Demo_page
            });
            document.body.appendChild(element);

            // given:
            // - toast eventにモックされたハンドラーを定義する
            const toastHandler = jest.fn();
            element.addEventListener(ShowToastEventName, toastHandler);

            // - wireされたApexメソッドでエラーを発生させる
            getDemos.error();

            // when:
            // - コンポーネントを反映させる
            await flushPromises();

            // then:
            const datatableElement = element.shadowRoot.querySelector('lightning-datatable');

            expect(datatableElement).toBe(null);

            // - toast eventに定義したモックされたハンドラーに対して検証を行う
            expect(toastHandler).toHaveBeenCalled();
            expect(toastHandler).toHaveBeenCalledTimes(1);
        });
    });

```

- `ShowToastEventName`、`ShowToastEvent`のモジュールを作成する
　　- `trailheadapps/lwc-recipes`の以下のソースをそのまま利用した
　　- https://github.com/trailheadapps/lwc-recipes/blob/main/force-app/test/jest-mocks/lightning/platformShowToastEvent.js
```javascript:force-app/test/jest-mocks/lightning/platformShowToastEvent.js
/**
 * For the original lightning/platformShowToastEvent mock that comes by default with
 * @salesforce/sfdx-lwc-jest, see:
 * https://github.com/salesforce/sfdx-lwc-jest/blob/master/src/lightning-stubs/platformShowToastEvent/platformShowToastEvent.js
 */

export const ShowToastEventName = 'lightning__showtoast';

export class ShowToastEvent extends CustomEvent {
    constructor(toast) {
        super(ShowToastEventName, {
            composed: true,
            cancelable: true,
            bubbles: true,
            detail: toast
        });
    }
}
```

- `jest.config.js`でモジュール定義する
  - `moduleNameMapper`の箇所で、`lightning/platformShowToastEvent`を定義する
```javascript:jest.config.js
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');

module.exports = {
    ...jestConfig,
    modulePathIgnorePatterns: ['<rootDir>/.localdevserver'],
    moduleNameMapper: {
        '^lightning/platformShowToastEvent$': '<rootDir>/force-app/test/jest-mocks/lightning/platformShowToastEvent'
    }
};
```