inputのtype別に、puppeteerでの入力方法をまとめた

inputタグに設定できるtype属性は今のところ、[以下の20種類](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input)

```
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だとこんなかんじ

```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()`を利用すれば良い

```javascript
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だと以下

```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

```javascript
await page.click("#button1");
await page.click("#image1");
await page.click("#reset1");
await page.click("#submit1");
```

# radio, checkbox

ラジオボタンやチェックボックスも、素直に`click()`でOK

```html
<input type="radio" id="radio1">
<input type="checkbox" id="checkbox1">
```

```javascript
await page.click("#radio1");
await page.click("#checkbox1");
```

# file

```html
<input type="file" id="file1">
```

type="file"には、専用のメソッドである`waitForFileChooser()`が用意されている。
使い方は、`waitForNavigation()`と同様に、クリック前に実行しておき、resolve後、ファイルを指定する。headlessでなくてもファイル選択のUIは表示されない。

```javascript
const [fileChooser] = await Promise.all([
	page.waitForFileChooser(),
	page.click('#file1'),
]);
await fileChooser.accept(["/path/to/file"]);
```

もしくは、以下でも可。個人的にはネストが深くならないこちらのほうが好み。読みやすさは大差ない。

```javascript
const fileChooserPromise = page.waitForFileChooser();
await page.click('#file1');
await (await fileChooserPromise).accept(["/path/to/file"]);
```

# range

スライドバーが表示されるtype="range"

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44717/8239a192-f1ae-a620-4cdd-0fe0eed87389.png)

```html
<input type="range" id="range1">
```

単純な値指定は難しいため、`focus()`でフォーカスを合わせて[^range-focus]、`右矢印キー`か`左矢印キー`を適切な回数押すと良い。(1回でstep属性の値だけ変化、デフォルト1)

```javascript
await (await page.$("#range1")).focus();
for(let i = 0; i < 20; i++) {
	await page.keyboard.press("ArrowRight");
}
```

[^range-focus]: `focus()`ではなく、`click()`をすると、値が変わることがある(中央付近をクリックしているような挙動)ため、不可。

# color

```html
<input type="color" id="color1">
```
puppeteerの闇を感じることができるtype="color"。
クリックするとカラーピッカーが表示されるが、puppeteerに専用のメソッドは用意されていない[^waitforcolorinput]ため、このカラーピッカーを操作する必要がある。

[^waitforcolorinput]: [issue](https://github.com/puppeteer/puppeteer/issues/5460)となってはいるが、進展なさそう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44717/f4b0b25a-a13c-7657-89b1-17acca3665e2.png)

やってみた結果が以下。

```javascript
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行だ。

```javascript
await (await page.$("#color1")).evaluate((node) => {node.value = "#FF0000"});
```

ただ、この書き方だと、changeイベントが発火しないため、テスト内容によっては利用できない。

# date系

以下のtypeを、まとめてdate系と呼ぶことにする。

```
date, datetime-local, month, time, week
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44717/4ced5c84-3868-61ce-87fe-128a8e8f253c.png)

htmlでは以下。

```javascript
<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要素でないため、開いた後の操作が難しい。ブラウザごとの差異も激しい。
キーボードで日付／時刻を入力するのが最善だ。

その上で、以下の点に気をつける必要がある。[^current-chromium]

1. 項目が一意に特定可能になると、次の項目に自動で遷移する
	* 「月」に5を入力すると自動で「日」にフォーカスが移るが、1を入力してもフォーカスはそのまま(10-12月があるため)
1. 最大275760年9月13日まで入力できる[^why_275760]
	* つまり「年」に4桁入力しても、「月」にフォーカスを移してくれない
1. min,max属性の指定によっては、入力不可な項目が出てくる
	* type="date"で、min="2020-01-01" max="2020-12-31"の場合、「年」は2020に固定され、フォーカスが当たるといきなり「月」の入力になる。
1. ロケールによって年月日の順番が異なる

[^why_275760]: [1970/1/1から1億日らしい](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps)

[^current-chromium]: この注意点も現在のChromiumの実装に依存している。他のブラウザでは状況は異なるだろうし、Chromiumのバージョンアップでも変わる可能がある

1.の解決策は2つ。
1つ目は、タブや右矢印などでフォーカスを移すこと。
「月」に1と入力したあとでタブを押せば、フォーカスが「日」に移ってくれる。
ただし、puppeteer上では月を`type()`したあと、月が1のときのみタブを`press()`してまた日を`type()`する、という少し面倒な書き方になる。[^ji-too]
おすすめは次の2つ目だ。「月」を0埋めし、必ず2桁入力する。puppeteerのコードとしては、タブを押すよりだいぶ簡単になる。[^js_padding]

[^js-padding]: JavaScriptで0パディングするのは意外

[^ji-too]: ここでは「月」のみの説明をしているが、「年」「時」でも同様の問題は起こり得る

2.はほとんど1.と同種の問題だ。
「年」を入力したあと、タブでフォーカスを移してもよいが「年」の頭に00を加えて6桁入力するのが楽だ。`type()`のみですむ。
また、自動化対象のコードを変更できる場合、maxを指定してしまっても良い。max="9999-12-31"としておく[^better-max]と、4桁入力した時点で「月」にフォーカスが移ってくれる。あと7900年後くらいまでは困る人もいないだろう。

[^better-max]: もちろん、もっと妥当なmaxがあるのなら、それを記載するのが望ましい

3.は少し厄介だ。
「年」が入力不可だと分かっているのなら単純に「月」と「日」だけ入力すればよい(dateの場合)のだが、例えば「現在の日付から半年後まで」という仕様の場合、1-6月と7-12月で年の入力可否が変わってしまう。「1年後の前日の日付まで」という仕様なら1月1日のみCIが落ちるかもしれない。[^getsu-too]
解決策としては、minとmax属性を読み込んで場合分け、が愚直な方法だろうか。
尤も、自動テストという文脈なら、テストの外部要因(時刻)でテスト内容や結果が変わるテストはイマイチなので、現在時刻をDIできる設計にするのが望ましい。[^fake-clock]

[^fake-clock]: 一例: https://qiita.com/suin/items/bcd7488df4403a53d7d9
[^getsu-too]: ここでは「年」のみの説明をしているが、「月」「日」「時」でも同様の問題は起こり得る

4.は解決策を調査中である。ブラウザ上で、年月日がどの順番で表示されているのか、を取得する方法があれば知りたい。
もしくは、Chromiumの起動オプションでロケールを指定して年月日の順番を固定させる方法が利用できるかもしれない。(未調査)

ということを踏まえて、入力するコードは以下となる。(3と4は検討外)

```javascript
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イベントが発火しない、などの差分が許容できるなら、以下の書き方も利用できる。

```javascript
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()`でやるしかない。ブラウザ自動操作の枠を超えているような気もするが。

```html
<input type="hidden" id="hidden1">
```

```javascript
await (await page.$("#hidden1")).evaluate((node) => {node.value = "hidden-value"});
```

# おまけ

inputとともによく使われる以下についても簡単に。

## textarea tag

```html
<textarea id="textarea_tag1"></textarea>
```

textareaは、text系と同じで、`type()`でOK

```javascript
await page.type("#textarea_tag1", "textarea");
```
## button tag

```html
<button id="button_tag1">button</button>
```

ボタンは、名前の通りbutton系、`click()`でOK

```javascript
await page.click("#button_tag1");
```
## select tag

```html
<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>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44717/44cf8271-976d-6dd4-bc1a-3effe988420d.png)


selectは、専用のメソッドが用意されている。`select()`だ。
selectはmultipleか否かでUIが大きく異なるが`select()`は両方に対応している。
第2引数以降に選択するoptionのvalueを指定する。

```javascript
await page.select("#select1", "2");
await page.select("#select_multi1", "2", "3");
```