CypressのComponentテストでUAのテストを行う

Cypressでユーザーエージェント(以降、UAと記載)による条件分岐のテストを行う場合、
E2Eテストであれば[userAgent](https://docs.cypress.io/guides/references/configuration#Browser)の設定がありますが、
Componentテストの場合は上記の設定は効かず、
どのようにテストを書くのか非常に困ったのでまとめます。

# いきなり結論

`__defineGetter__`で`window.navigator`の`userAgent`を直接書き換えることで解決できます。

```jsx:test.cy.jsx
import Target from './Component'

before(() => {
    cy.mount(
        <Target>
            <div data-cy="children">test</div>
        </Target>
    )

    const chromeUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
    // UAをchrome用の値に変更
    window.navigator.__defineGetter__(
        'userAgent',
        () => chromeUA,
    )
})
it('chromeの場合、childrenのみ表示すること', () => {
    // エラーメッセージが表示されないこと
    cy.get('[data-cy="エラーメッセージ"]')
        .should('not.be.exist')

    // childrenが表示されること
    cy.get('[data-cy="children"]').should('be.visible')
})
```

以降はそれなりに細かく解説を書いてきます。

# 対象読者

- CypressでComponentテストを書いている人
- UAの分岐処理をテストしたい人

コンポーネントの実装はReactで書きますが、  
テスト自体はCypressで動かすため、[テストの作成](#テストの作成)以降はVueなどでも問題ないはず。

# 実行環境

本記事は、以下の環境で実行を行いました。

- macOS - Ventura
- Node - 18.9.0
- Vite - 4.3.8
- React - 18.2.0
- Cypress - 12.12.0
- Safari - 16.5
- Chrome - 114.0.5735.90

# 事前準備

Cypress等のセットアップはすでに完了している前提で進めます。

UAからブラウザを判断する処理をイチから書くのは骨が折れるので、  
今回は[UAParser.js](https://github.com/faisalman/ua-parser-js)を使います。

## UAParser.jsのインストール

`npm i ua-parser-js`

## Safariの場合エラーを出すコンポーネントの作成

UAParser.jsが返すブラウザ名は[ここ](https://github.com/faisalman/ua-parser-js#-getbrowseridata)に定義されています。

今回はSafariの利用を制限する想定で、
`browser.name`に`Safari`を含む場合に分岐するよう処理を書いています。

```jsx:Component.jsx
import { useState, useEffect } from 'react'
import { UAParser } from 'ua-parser-js'

const SafariError = () =>  (
    <p data-cy='エラーメッセージ'>
        Safariでのアクセスは禁止されています。
    </p>
)

export default ({ children }) => {
    const [isSafari, setIsSafari] = useState()

    useEffect(() => {
        // ブラウザのUAを取得
        const ua = window.navigator.userAgent
        // UAParseに渡してUAを扱いやすいように整形する
        const { browser } = UAParser(ua)
        // Safari、Mobile Safariのときtrueにする
        const isSafariBrowser = browser.name.includes('Safari')
        setIsSafari(isSafariBrowser)
    }, [])
    
    // まだ定義が無いの場合は何も返さない
    if (isSafari === undefined) return
    // trueになった場合は、エラーメッセージを表示する
    if (isSafari) return <SafariError />
    // falseになった場合は、そのままchildrenを表示する
    else return children
}
```

# テストの作成

## ブラウザからUAを取得する

UA変更は文字列で全文を指定する必要があるため、ブラウザからUAを取得します。

### Chromeの場合

DevTools(F12)のConsoleタブで`window.navigator.userAgent`を入力してEnter

![UA取得_chromeの場合.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982020/bf1b7a33-fb20-fcc3-e503-2e20e2e453e0.png)

今回、私の環境で取得したUAは以下となります。
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36`

### Safariの場合

手順はChromeの場合と殆同じです。
Webインスペクター(⌥ + ⌘ + c)のConsoleタブで`window.navigator.userAgent`を入力してEnter

![UA取得_safariの場合.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982020/9a67b34b-c504-7b40-d266-0b095eabfbc3.png)

今回、私の環境で取得したUAは以下となります。
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15`

## テストにUA書き換え処理を追加する

[冒頭](#いきなり結論)にも書いた通り、UAの書き換えは`__defineGetter__`で`window.navigator`の`userAgent`を変更することで実現できます。

### beforeにUA書き換え処理を記載する

itの中に書くとUA書き換え前の状態でテストが行われるようなので、
beforeやbeforeEachの中にUA書き換え処理を書いていきます。

```jsx:Component.cy.jsx
before(() => {
    // 実際のブラウザから取得したUA
    const chromeUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
    // UAをchrome用の値に変更する
    window.navigator.__defineGetter__(
        'userAgent',
        () => chromeUA,
    )
})
```

## mountとかitとか書いてく

mountのあとは、React以外のComponentテストで共通だと思います。

```jsx:Component.cy.jsx
describe('Chromeの場合', () => {
    before(() => {
        // 実際のブラウザから取得したUA
        const chromeUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
        // UAをChrome用の値に変更する
        window.navigator.__defineGetter__(
            'userAgent',
            () => chromeUA,
        )
    })
    it('childrenのみ表示されること', () => {
        // エラーメッセージが表示されないこと
        cy.get('[data-cy="エラーメッセージ"]').should('not.be.exist')

        // childrenが表示されること
        cy.get('[data-cy="children"]').should('be.visible')
    })
})

describe('Safariの場合', () => {
    before(() => {
        // 実際のブラウザから取得したUA
        const safariUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15'
        // UAをSafari用の値に変更する
        window.navigator.__defineGetter__(
            'userAgent',
            () => safariUA,
        )
    })
    it('エラーメッセージのみ表示されること', () => {
        // エラーメッセージが表示されること
        cy.get('[data-cy="エラーメッセージ"]').should('be.visible')

        // childrenが表示されないこと
        cy.get('[data-cy="children"]').should('not.be.exist')
    })
})
```

## 実行結果

早すぎて見えませんが、UAごとの表示分岐をテストすることができました。
![move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982020/e3870295-8146-cb1d-42b5-8e0483bfe9f5.gif)

# トレタでは一緒に働く自動化エンジニア募集しています！！

トレタでは、テスト自動化エンジニア/SETの方を募集しています。
カジュアル面談もしていますので、興味のあるかたは是非ご応募ください！

https://corp.toreta.in/recruit/midcareer/midcareer-288/

他の業種も募集してます！

https://corp.toreta.in/recruit/midcareer/