Seleniumでasm15を操作する

# はじめに

## Seleniumとは

[Selenium](https://www.selenium.dev/)

Webブラウザを自動で操作できるツール。
多くのプログラミング言語がサポートされているが、今回はPythonを用いる。

## asm15とは

[asm15 assembler for IchigoJam / IchigoLatte](https://ichigojam.github.io/asm15/) (本家)
[asm15 assembler for IchigoJam / IchigoLatte](https://mikecat.github.io/asm15/) (筆者による改造版)

[IchigoJam](https://ichigojam.net/)向けのマシン語のアセンブルを目的に開発されたアセンブラ。
Webブラウザ上で入出力を行うため、ファイルとの連携は一手間かかる。

※IchigoJamはjig.jpの登録商標です。

## 今回の目的

Webブラウザ自動操作ツールSeleniumでアセンブラasm15を操作し、
標準入力から受け取ったアセンブリのソースコードをアセンブルして結果を標準出力に出力する。

# Seleniumのインストール

今回は、PythonおよびGoogle Chromeが既にインストールされているWindows環境でインストールを行った。
インストール方法および一部の操作方法は、この記事を参考にした。

[10分で理解する Selenium - Qiita](https://qiita.com/Chanmoro/items/9a3c86bb465c1cce738a)

コマンドプロンプトでPythonのvirtualenv環境を用意し、
以下のコマンドを実行することで、インストールできた。

```
pip install chromedriver-binary==96.*
pip install selenium
```

`chromedriver-binary==`に続く数字は、記事に従い、
インストールされているGoogle Chromeのバージョンに合わせた。
Google Chromeのバージョンが上がった際のトラブルを避けるため、virtualenvを用いることにした。

# Seleniumによるasm15の操作

SeleniumによるWebブラウザの操作は、Pythonのインタラクティブモードで試すことができる。

## Webブラウザ (Google Chrome) の起動

以下のプログラムにより、Google Chromeを起動してSeleniumで操作できる状態にできる。

```py
import chromedriver_binary
from selenium import webdriver
driver = webdriver.Chrome()
```

`import chromedriver_binary` はGoogle Chrome起動の準備をしているようで、
これを実行しないとGoogle Chromeの起動に失敗した。

また、`--headless` オプションを用いると、Google Chromeのウィンドウを隠した状態で起動できる。

```py
options = webdriver.ChromeOptions()
options.add_argument("--headless")
driver = webdriver.Chrome(options=options)
```

## Webブラウザの終了

`quit`関数により、Seleniumで起動したWebブラウザを終了できる。

```py
driver.quit()
```

また、`with`構文を用いることで、ブロックの実行終了時に自動で終了してくれる。

```py
with webdriver.Chrome(options=options) as driver:
	# do work
```

[Working with windows and tabs | Selenium](https://www.selenium.dev/documentation/webdriver/browser/windows/)

## 指定のURLへの遷移

`get`関数により、指定のURLに遷移できる。

```py
driver.get("https://ichigojam.github.io/asm15/")
```

## HTML要素のハンドラの取得

`find_element`関数により、操作対象のHTML要素を選択し、ハンドラを取得できる。
今回は、要素の`id`属性に基づいて選択する`By.ID`を用いる。

```py
from selenium.webdriver.common.by import By
textarea = driver.find_element(By.ID, "textarea1")
```

[Finding web elements | Selenium](https://www.selenium.dev/documentation/webdriver/elements/finders/)

該当する要素が存在しない場合、
`selenium.common.exceptions.NoSuchElementException`例外が発生するようである。

## 入力欄への文字列の入力

`send_keys`関数により、HTML要素の入力欄に文字列を入力できる。
英語の大文字や小文字、記号、日本語も指定通りに入力できるようである。

```py
textarea.send_keys("' テスト (test)\nRET\n")
```

`clear`関数により、HTML要素の入力欄に入力されている文字を消去できる。

```py
textarea.clear()
```

[Keyboard Actions | Selenium](https://www.selenium.dev/documentation/webdriver/actions_api/keyboard/)

## 入力欄からの文字列の取得

`get_attribute`関数によりHTML要素の属性の値を取得でき、
入力欄に入力されている文字列もこれで取得できるようである。

```py
textarea.get_attribute("value")
```

`text`プロパティもあり、執筆時点におけるasm15のアセンブル結果はこれでも取得できたが、
入力した内容は反映されないようだった。

```py
textarea.text
```

[Information about web elements | Selenium](https://www.selenium.dev/documentation/webdriver/elements/information/)
(執筆時点において、属性の取得については書かれていないようである)

## SELECT要素の操作

HTMLのSELECT要素の操作は、`Select`オブジェクトを用いて行うことができる。
例えば、`select_by_visible_text`関数を用いると、表示される文字列を指定して選択できる。

```py
from selenium.webdriver.support.select import Select
selfmt = driver.find_element(By.ID, "selfmt")
selfmt_selobj = Select(selfmt)
selfmt_selobj.select_by_visible_text("bas (hex)")
```

[Working with select list elements | Selenium](https://www.selenium.dev/documentation/webdriver/elements/select_lists/)

## ボタンのクリック

`click`関数により、HTML要素をクリックできる。

```py
btn = driver.find_element(By.ID, "submit-button")
btn.click()
```

## アラートダイアログへの対処

執筆時点におけるasm15は、アセンブル時のエラーをアラートダイアログで表示する。
アラートダイアログが出ているかをチェックするため、タイムアウトを短く設定した`WebDriverWait`を用い、
`wait`関数でアラートダイアログが出るまで待たせる。
タイムアウトは、`WebDriverWait`の第2引数で、秒単位で設定できるようである。
アラートダイアログが出ていればそのダイアログのハンドラが返り、
出ていなければ`selenium.common.exceptions.TimeoutException`例外が発生するようである。

```py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 1)
alert = wait.until(EC.alert_is_present())
```

アラートダイアログが出ていた場合、`text`プロパティでアラートダイアログの内容を取得し、
`accept`関数でアラートダイアログを閉じることができる。

```py
alert.text
alert.accept()
```

[Waits | Selenium](https://www.selenium.dev/documentation/webdriver/waits/)
[JavaScript alerts, prompts and confirmations | Selenium](https://www.selenium.dev/documentation/webdriver/browser/alerts/)

# まとめ

今回紹介した要素を組み合わせた以下のプログラムにより、asm15を操作し、
標準入力から受け取ったソースコードをアセンブルして結果を標準出力に出力できる。
また、発生したエラーは標準エラー出力に出力する。

```selenium_asm15.py
import sys
import chromedriver_binary
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select
from selenium.common.exceptions import TimeoutException

# open Chrome
options = webdriver.ChromeOptions()
options.add_argument("--headless")

with webdriver.Chrome(options=options) as driver:
	# open asm15
	driver.get("https://ichigojam.github.io/asm15/")

	# set output format
	selfmt = driver.find_element(By.ID, "selfmt")
	selfmt_selobj = Select(selfmt)
	selfmt_selobj.select_by_visible_text("bas (hex)")

	# set assembly source
	asm_source = sys.stdin.read()
	textarea_asm = driver.find_element(By.ID, "textarea1")
	textarea_asm.clear()
	textarea_asm.send_keys(asm_source)

	# assemble
	assemble_button = driver.find_element(By.ID, "submit-button")
	assemble_button.click()

	# get errors from alert dialog(s)
	wait = WebDriverWait(driver, 1)
	while True:
		try:
			alert = wait.until(EC.alert_is_present())
		except TimeoutException:
			break
		sys.stderr.write(alert.text + "\n")
		alert.accept()

	# get assembled result
	textarea_res = driver.find_element(By.ID, "textarea2")
	res = textarea_res.get_attribute("value")
	sys.stdout.write(res)
```

テスト用に以下のソースコードを用意した。

```test.txt
R0 = R0 + 1
RET
```

```test_error.txt
R0 = R0 + 1234
RET
```

実行してみると、システムから余計な出力がされるものの、
アセンブル結果とアセンブル時のエラーを取得できていることがわかる。
1回の実行には7秒くらいかかった。

```
(ve) YUKI.N>selenium_asm15.py < test.txt > test_out.txt

DevTools listening on ws://127.0.0.1:59356/devtools/browser/6cc184b3-4a60-4d3c-b9a4-e36045d28b75
[0103/170014.011:INFO:CONSOLE(0)] "Error with Permissions-Policy header: Unrecognized feature: 'interest-cohort'.", source:  (0)

(ve) YUKI.N>cat test_out.txt
10 poke #700,#40,#1C,#70,#47

(ve) YUKI.N>selenium_asm15.py < test_error.txt > test_error_out.txt

DevTools listening on ws://127.0.0.1:59375/devtools/browser/04e8fb18-1d26-4a91-8764-cfbf4553e7db
[0103/170156.926:INFO:CONSOLE(0)] "Error with Permissions-Policy header: Unrecognized feature: 'interest-cohort'.", source:  (0)
asm error in 1
R0 = R0 + 1234
Error: over!

(ve) YUKI.N>cat test_error_out.txt
10 poke #700,#70,#47

(ve) YUKI.N>
```