Jest + PuppeteerのE2Eテストで使うコード集

13 min read読了の目安(約12000字

はじめに

Nuxt on Dockerにて、Jest + Puppeteer でE2Eテスト環境をセットアップする」でJest + PuppeteerでE2Eテストをする環境を整えました。
この記事では、実際にE2Eテストを記述するときに使う操作や検証のコードをまとめておきます。
自分が使ったコードを書いていくので、順次増えていく予定。

基本

テストファイル内の基本構文は以下のとおりです。

describe('テストスイート名', () => {
  test('テストケース名', async () => {
    await page.goto('http://localhost:3000') // 操作とか
    await expect(page.url()).toBe('http://localhost:3000') // 検証とか
  })
})

describeとtest

1つのファイルには1つのdescribeがあります。そして、1つのdescribeの中に複数のtestを記述できます。
testはテストケース、describeはテストスイート(テストケースを目的別などでまとめたもの)といえます。

test内は複数の操作と検証から成り立ちます。これらはasync/awaitを使って直列に処理させます。
例の場合、await page.goto('http://localhost:3000')で「http://localhost:3000にアクセスする」操作を、
await expect(page.url()).toBe('http://localhost:3000')で「表示中のページのURLがhttp://localhost:3000である」検証を行っています。

describe(name, fn)
test(name, fn, timeout)

expect(X).toBe(Y)

検証はexpect(X).toBe(Y)を使うことが多いです。toBeなので「XYである」ことを検証します。
他にも、「XYでない」ことを検証する場合はexpect(X).not.toBe(Y)、「XYが含まれている」ことを検証する場合はexpect(X).toContain(Y)など、toBeの部分を変化させることで色々な検証を行うことができます。

expectのメソッド

操作系

ページにアクセスする

E2Eテストではページにアクセスしないと始まりませんね。ページにアクセスする場合はpage.goto()を使います。
例えばhttp://localhost:3000にアクセスしたい場合は、以下のように書きます。

await page.goto('http://localhost:3000')

page.goto(url)

ボタンやリンクをクリックする

ボタンやリンクなどの要素をクリックするときはpage.click()を使います。
例えば

<button id="hoge">Click me!!</button>

のボタンをクリックする場合、id属性を利用して以下のように書きます。

await page.click('#hoge')

page.click(selector)

入力フォームに文字を入力する

inputtextareaに文字入力をします。例えば

<input type="text" id="hoge" />

のテキストフィールドに「こんにちは」と入力する場合、id属性を利用して以下のように書きます。

await page.type('#hoge', 'こんにちは')

ちなみに、すでに文字が入力されている場合、入力済みの値に文字が追加されるので注意が必要です。

await page.type('#hoge', 'こんにちは') // 「こんにちは」と入力されている状態になる
await page.type('#hoge', 'こんばんは') // 「こんにちはこんばんは」と入力されている状態になる

page.type(selector, text)

入力フォームの文字を消す

page.type()は入力済みの文字列に追加されるので、新しく文字を入力したい場合は一度フォームの入力値をクリアする必要があります。
await page.type('#hoge', '')などでクリアできればいいのですが、そうはいきません。

<input type="text" id="hoge" value="Hello" />

のようにすでに「Hello」が入力されているテキストフィールドの入力値を消す場合、例えば以下のようなコードで実現できます。

const el = await page.$('#hoge')
await el.click({ clickCount: 3 })
await el.press('Backspace')

page.$()selectorに合致する要素を取得し、それを3回クリックし、Backspaceを押す、という操作です。
実際にブラウザでテキストフィールド(内の文字列)を3回クリックすると、入力されている文字列が全選択の状態になり、そこでBackspaceを押すと文字が全部消えますよね?
このように実際にブラウザで行う操作をコーディングすることで入力フォームの文字をクリアできます。

最初に関数定義しておくとより使いやすいです。

const clearInput = async (id) => {
  const el = await page.$(id)
  await el.click({ clickCount })
  await el.press('Backspace')
}

test('hoge' async () => {
  ...
  clearInput('#hoge')
})

これで、clearInput('#hoge')でid属性がhogeinputの入力文字を消すことができます。

page.$(selector)

入力フォームの文字を消して、新しい文字を入力する

上の2つを組み合わせれば、すでに文字が入力されているinputに新しい文字列を1列で入力できる関数もつくれます。

const newInput = async (id, value) => {
  const el = await page.$(id)
  await el.click({ clickCount })
  await el.press('Backspace')
  await page.type(id, value)
}

test('hoge', async () => {
  ...
  await newInput('#hoge', 'hogehoge')
  ...
})

よく使うので、関数化しておくと便利です。

画面遷移やリロードを待つ

await page.waitForNavigation()

で画面遷移やリロードを待つことができます。

page.waitForNavigation()

画面に要素が現れるまで待つ

例えば

<button id="hoge">Click me!!</button>

のボタンがなにかの処理が終わってから表示されるとします。このとき、ボタンが表示される前にpage.click()などをしてしまうと、要素が見つからないためエラーになってしまいます。
そこで、以下のようにして要素が現れるのを待ちます。

await page.waitForSelector('#hoge')

もし、ボタンが現れない場合はテストがタイムアウトで失敗になるので注意です。

page.waitForSelector(selector)

画面から要素が消えるまで待つ

例えば

<button id="hoge">押したら消えるよ</button>

のようなボタンがあり、一度押したら要素が消えるようになっているとします。画面から要素が消えるまで操作を待ちたい場合、以下のように書きます。

await page.waitForSelector('#hoge', { hidden: true })

画面に要素が現れるまで待つpage.waitForSelector()hiddenオプションを有効にすることで、「消えるまで待つ」操作になります。

page.waitForSelector(selector)

指定時間待つ

時間を指定して操作や検証を待ちたい場合、例えば1秒待ちたい場合は

await page.waitForTimeout(1000)

で実現できます。()の単位はミリ秒なので、1秒の場合は1000です。

page.waitForTimeout(milliseconds)

フォーカスを外したい

inputに入力している状態からフォーカスを外す操作はblurを使えばできます。

await page.$eval('#hoge', el => el.blur())

とりあえずキーボード操作したい

要素関係なく矢印を押してみたり、Enterを押してみたり、というケースもあると思います。
その場合はpage.keyboardが便利です。例えば「右矢印(→)」を押したい場合、以下のとおりです。

await page.keyboard.press('ArrowRight')

class: KeyBoard

とりあえず操作したい

とりあえずブラウザを操作したい場合はpage.evaluate(pageFunction)を使います。
例えば、ページをスクロールしたいときはwindow.scrollTo()を使うとできます。スクロールすると出現する要素、とか検証するときに使います。

await page.evaluate(() => {
  window.scrollTo({ top: 1000 })
})

page.evaluate(pageFunction)
window.scrollTo

検証系

表示中のページのURLを検証する

表示中のページのURLはpage.url()で取得できます。これが期待値(例:http://localhost:3000)かどうかは以下のように検証できます。

await expect(page.url()).toBe('http://localhost:3000')

page.url()

要素が存在することを検証する

<p id="hoge">Show me!!</p>

の要素がページに表示されているかどうかは以下のように検証できます。

await expect(page.$('#hoge')).not.toBeNull()

page.$()は要素が見つからない場合nullを返します。なので「Nullであること」を検証する.toBeNull()に否定の.notをつけることで「Nullでないこと」を確認し、要素が存在することを検証できます。

page.$(selector)
.not
.toBeNull()

要素が存在しないことを検証する

<p id="hoge" style="display: none;">見えない</p>

の要素が存在しない(表示されていない)ことを検証します。

await expect(page.$('#hoge')).toBeNull()

page.$()は要素が見つからない場合nullを返すので、toBeNull()で検証します。

page.$(selector)
.toBeNull()

要素の属性を検証する

例えば、

<img id="hoge" src="./images/hoge.png" />

のような画像があるとして、srcが正しく設定されているか検証したいとします。
この場合、page.$eval()を使ってsrc属性を取得して検証できます。

const src = await page.$eval('#hoge', el => el.src)
await expect(src).toBe('./images/hoge.png')

page.$eval(selector, pageFunction)selectorで取得した要素に対してpageFunctionを返します。例の場合はselector#hogepageFunctionel => el.srcとしているので、#hogeimgsrcを取得してます。

page.$eval(selector, pageFunction)

要素のテキストを検証する

例えば、

<p id="greeting">こんにちは!</p>

のような文章があって、ちゃんとテキストが意図通りか検証したいとします。

const text = await page.$eval('#greeting', el => el.innerText)
await expect(text).toBe('こんにちは!')

要素の属性の検証と似ていますが、innerTextを用いてelの中のテキストを抜いてくる方法があります。

page.$eval(selector, pageFunction)
HTMLElement.innerText

inputの入力値を検証する

これも要素の属性検証の応用です。

<input id="hoge" type="text" />

のようなテキストフィールドに「こんにちは」と入力して、「こんにちは」と入力されたかを検証する、といった流れです。
inputの入力値はvalue属性になるので、それを拾ってくればOKです。

await page.type('#hoge', 'こんにちは')
await expect(await page.$eval('#hoge', el => el.value)).toBe('こんにちは')

page.type(selector, text)
page.$eval(selector, pageFunction)
HTMLInputElement

buttonが非活性かを検証する

これも要素の属性検証の応用です。

<button id="hoge" disabled>押せないよ</button>

のような非活性なボタンを、ちゃんと非活性になっているか検証します。

await expect(await page.$eval('#hoge', el => el.disabled)).toBe(true)

HTMLButtonElementはTrue or Falseのdisabledを持っているのでそれで検証します。
逆にボタンが活性状態かは

await expect(await page.$eval('#hoge', el => el.disabled)).toBe(false)

で検証できます。

page.$eval(selector, pageFunction)
HTMLButtonElement

複数の要素の属性を検証する

例えば、

<img class="img" src="./images/hoge.png" />
<img class="img" src="./images/fuga.png" />

のように同じimgclassの要素のsrcがそれぞれ意図通りかを調べたいとします。

const srcs = page.$$eval('.img', els => els.map(el => el.src))
await expect(src[0]).toBe('./images/hoge.png')
await expect(src[1]).toBe('./images/fuga.png')

まず、page.$$rval()imgclassの要素の配列を取得して、後続のfunctionにelsとして流してます。
次に、mapを使って配列の1要素ずつのsrcを取り出し、srcsに返却しています。
srcsimgclassの要素のsrc属性が順番に並んだ配列になっているので、src[index]で順番を指定して検証ができます。

page.$$eval(selector, pageFunction)
Array.prototype.map()

target="_blank"の別タブで開くを検証する

<a id="link_google" href="https://www.google.com/" target="_blank">Google</a>

のように別タブで遷移した場合、新しく開いたタブを検証したいときがあります。
browser.once('targetcreated', function)を使ってやる例が多かったのですが、ちょっとやりたいことができなかったのと直感的じゃないなぁと感じてしまったので、以下のやり方で検証してます。

await page.click('#link_google')
await page.waitForTimeout(2000)
const pages = await browser.pages()
const newPage = pages[pages.length - 1]
await expect(newPage.url()).toBe('https://www.google.com/')

少しダサいですが、リンククリック後、タブの準備が整うまでpage.waitForTimeout()でスリープを入れています。
その後、browser.page()で全てのタブを取得し、pages[pages.length - 1]で一番うしろのタブ、つまり今新しく開かれたタブをnewPageに入れてます。

page.click(selector)
page.waitForTimeout(milliseconds)
browser.pages()
page.url()

meta tagsを検証する

メタタグも要素の1つですので、他の要素と同じように検証できます。
例えば、Nuxt.jsの場合、nuxt.config.js

nuxt.config.js
export default {
  head: {
    meta: [
      { hid: 'og:site_name', property: 'og:site_name', content: 'MyApp' }
    ]
  }
}

のようにメタタグを定義したりすると思いますが、これもブラウザでは

<head>
  <mata data-n-head="ssr" data-hid="og:site_name" property="og:site_name" content="MyApp">
</head>

のように表示されているので扱いは一緒です。

await expect(await page.$eval('meta[property="og:site_name"]', el => el.content)).toBe('MyApp')

で検証が可能です。

page.$$eval(selector, pageFunction)
メタタグと SEO - NuxtJS

おわりに

テストコードを書く機会があったので、よく使いそうだなというコードをユースケースドリブンでまとめてみました。
色々なサイトを巡りましたが、最終的にはソースに落ち着く、ということで以下のページをとても参考にしました。

今後も新しいテストを書いたら追加していきたいなと思います。