inputタグに設定できるtype属性は今のところ、以下の20種類
button, checkbox, color, date, datetime-local, email, file, hidden, image, month, number, password, radio, search, submit, tel, text, time, url, week
puppeteerで簡単に入力できるもの、専用の入力方法が用意されているの、クセが強いもの、など様々だったのでまとめた。
なお、本ページの例は全タグにidがふってある優しい世界なので、要素の特定は別途頑張ってください。
確認環境
- Windows10 2004
- node v14.15.4
- puppeteer 5.5.0
- Chromium 90.0.4400.0
text系
以下のtypeを、まとめてtext系と呼ぶことにする。
email, number, password, search, tel, text, url
htmlだとこんなかんじ
<input type="text" id="text1">
<input type="email" id="email1">
<input type="search" id="search1">
<input type="tel" id="tel1">
<input type="url" id="url1">
<input type="number" id="number1">
<input type="password" id="password1">
submit時にvalidationがかかるもの、入力した文字が見えないものなど、それぞれ違いはあるが、入力する上では全てtype()
を利用すれば良い
await page.type("#text1", "text");
await page.type("#email1", "example@example.com");
await page.type("#search1", "search text");
await page.type("#tel1", "09012345678");
await page.type("#url1", "https://example.com/");
await page.type("#number1", "42");
await page.type("#password1", "yourpassword");
button系
以下のtypeを、まとめてbutton系と呼ぶことにする。
button, image, reset, submit
htmlだと以下
<input type="reset" id="reset1">
<input type="button" id="button1" value="ボタン">
<input type="submit" id="submit1">
<input type="image" id="image1" src="image.png" alt="image-alt-text">
それぞれ役割があるが、入力(クリック)はどれもclick()
でOK
await page.click("#button1");
await page.click("#image1");
await page.click("#reset1");
await page.click("#submit1");
radio, checkbox
ラジオボタンやチェックボックスも、素直にclick()
でOK
<input type="radio" id="radio1">
<input type="checkbox" id="checkbox1">
await page.click("#radio1");
await page.click("#checkbox1");
file
<input type="file" id="file1">
type="file"には、専用のメソッドであるwaitForFileChooser()
が用意されている。
使い方は、waitForNavigation()
と同様に、クリック前に実行しておき、resolve後、ファイルを指定する。headlessでなくてもファイル選択のUIは表示されない。
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#file1'),
]);
await fileChooser.accept(["/path/to/file"]);
もしくは、以下でも可。個人的にはネストが深くならないこちらのほうが好み。読みやすさは大差ない。
const fileChooserPromise = page.waitForFileChooser();
await page.click('#file1');
await (await fileChooserPromise).accept(["/path/to/file"]);
range
スライドバーが表示されるtype="range"
<input type="range" id="range1">
単純な値指定は難しいため、focus()
でフォーカスを合わせて1、右矢印キー
か左矢印キー
を適切な回数押すと良い。(1回でstep属性の値だけ変化、デフォルト1)
await (await page.$("#range1")).focus();
for(let i = 0; i < 20; i++) {
await page.keyboard.press("ArrowRight");
}
color
<input type="color" id="color1">
puppeteerの闇を感じることができるtype="color"。
クリックするとカラーピッカーが表示されるが、puppeteerに専用のメソッドは用意されていない2ため、このカラーピッカーを操作する必要がある。
やってみた結果が以下。
await (await page.$("#color1")).click();
await page.waitForTimeout(300)
await page.keyboard.press("Tab")
await page.keyboard.press("Tab")
await page.keyboard.press("Tab")
await page.keyboard.type("255")
await page.keyboard.press("Tab")
await page.keyboard.type("0")
await page.keyboard.press("Tab")
await page.keyboard.type("0")
await page.keyboard.press("Enter")
まずクリックでカラーピッカーを表示させ、タブを3回押すことでRGB値の入力欄に移動。
タブで移動しながらR,G,Bそれぞれの値を入れることで入力している。
クリック後300ms待っているのは、カラーピッカーが表示されるまでに若干時間がかかるため。ここは環境によって変える必要があるだろう。
DOM要素ならwaitForSelector()
などで待つことができるが、カラーピッカーはDOM要素でない(・・・よね?)ため仕方なくwaitForTimeout()
を利用している。
もうお気づきだろうと思うが、完全にChromiumのカラーピッカーのUIに依存した書き方になっているため、Firefoxではおそらく動かない(未検証)し、今後Chromiumのバージョンアップで動作しなくなる可能性もある。闇だ。
なお、puppeteerでは、ページ上でJavaScriptを動作させることもできるため、以下の書き方もできる。これなら1行だ。
await (await page.$("#color1")).evaluate((node) => {node.value = "#FF0000"});
ただ、この書き方だと、changeイベントが発火しないため、テスト内容によっては利用できない。
date系
以下のtypeを、まとめてdate系と呼ぶことにする。
date, datetime-local, month, time, week
htmlでは以下。
<input type="date" id="date1">
<input type="datetime-local" id="datetime-local1">
<input type="month" id="month1">
<input type="time" id="time1">
<input type="week" id="week1">
まず、入力ボックスの右側のカレンダーや時計をクリックするとピッカーが表示されるが、これを使おうとしてはいけない。マウスでしか開けない(多分)し、ピッカーはDOM要素でないため、開いた後の操作が難しい。ブラウザごとの差異も激しい。
キーボードで日付/時刻を入力するのが最善だ。
その上で、以下の点に気をつける必要がある。3
- 項目が一意に特定可能になると、次の項目に自動で遷移する
- 「月」に5を入力すると自動で「日」にフォーカスが移るが、1を入力してもフォーカスはそのまま(10-12月があるため)
- 最大275760年9月13日まで入力できる4
- つまり「年」に4桁入力しても、「月」にフォーカスを移してくれない
- min,max属性の指定によっては、入力不可な項目が出てくる
- type="date"で、min="2020-01-01" max="2020-12-31"の場合、「年」は2020に固定され、フォーカスが当たるといきなり「月」の入力になる。
- ロケールによって年月日の順番が異なる
1.の解決策は2つ。
1つ目は、タブや右矢印などでフォーカスを移すこと。
「月」に1と入力したあとでタブを押せば、フォーカスが「日」に移ってくれる。
ただし、puppeteer上では月をtype()
したあと、月が1のときのみタブをpress()
してまた日をtype()
する、という少し面倒な書き方になる。5
おすすめは次の2つ目だ。「月」を0埋めし、必ず2桁入力する。puppeteerのコードとしては、タブを押すよりだいぶ簡単になる。[^js_padding]
2.はほとんど1.と同種の問題だ。
「年」を入力したあと、タブでフォーカスを移してもよいが「年」の頭に00を加えて6桁入力するのが楽だ。type()
のみですむ。
また、自動化対象のコードを変更できる場合、maxを指定してしまっても良い。max="9999-12-31"としておく6と、4桁入力した時点で「月」にフォーカスが移ってくれる。あと7900年後くらいまでは困る人もいないだろう。
3.は少し厄介だ。
「年」が入力不可だと分かっているのなら単純に「月」と「日」だけ入力すればよい(dateの場合)のだが、例えば「現在の日付から半年後まで」という仕様の場合、1-6月と7-12月で年の入力可否が変わってしまう。「1年後の前日の日付まで」という仕様なら1月1日のみCIが落ちるかもしれない。7
解決策としては、minとmax属性を読み込んで場合分け、が愚直な方法だろうか。
尤も、自動テストという文脈なら、テストの外部要因(時刻)でテスト内容や結果が変わるテストはイマイチなので、現在時刻をDIできる設計にするのが望ましい。8
4.は解決策を調査中である。ブラウザ上で、年月日がどの順番で表示されているのか、を取得する方法があれば知りたい。
もしくは、Chromiumの起動オプションでロケールを指定して年月日の順番を固定させる方法が利用できるかもしれない。(未調査)
ということを踏まえて、入力するコードは以下となる。(3と4は検討外)
await page.type("#date1", "0020210127");
await page.type("#datetime-local1", "00202101270812");
await page.type("#month1", "00202012");
await page.type("#time1", "0212");
await page.type("#week1", "00201943");
3や4が問題になる場合、changeイベントが発火しない、などの差分が許容できるなら、以下の書き方も利用できる。
await (await page.$("#date1")).evaluate((node) => {node.value = "2021-01-27"});
await (await page.$("#datetime-local1")).evaluate((node) => {node.value = "2021-01-27T08:12"});
await (await page.$("#month1")).evaluate((node) => {node.value = "2020-12"});
await (await page.$("#time1")).evaluate((node) => {node.value = "02:12"});
await (await page.$("#week1")).evaluate((node) => {node.value = "2019-W43"});
hidden
hiddenは普通はpuppeteerから基本的に書き換えないし、書き換えられない。
どうしてもやりたければ、evaluate()
でやるしかない。ブラウザ自動操作の枠を超えているような気もするが。
<input type="hidden" id="hidden1">
await (await page.$("#hidden1")).evaluate((node) => {node.value = "hidden-value"});
おまけ
inputとともによく使われる以下についても簡単に。
textarea tag
<textarea id="textarea_tag1"></textarea>
textareaは、text系と同じで、type()
でOK
await page.type("#textarea_tag1", "textarea");
button tag
<button id="button_tag1">button</button>
ボタンは、名前の通りbutton系、click()
でOK
await page.click("#button_tag1");
select tag
<select id="select1">
<option value="1" id="select1_1">option_1</option>
<option value="2" id="select1_2">option_2</option>
<option value="3" id="select1_3">option_3</option>
</select>
<select id="select_multi1" multiple>
<option value="1" id="select_multi1_1">option_1</option>
<option value="2" id="select_multi1_2">option_2</option>
<option value="3" id="select_multi1_3">option_3</option>
</select>
selectは、専用のメソッドが用意されている。select()
だ。
selectはmultipleか否かでUIが大きく異なるがselect()
は両方に対応している。
第2引数以降に選択するoptionのvalueを指定する。
await page.select("#select1", "2");
await page.select("#select_multi1", "2", "3");