kikuta@dev:~$cat ./notes/spex-real-sheets-e2e-verification.md
·13 min·kikuta

外部Webアプリを操作するE2Eは、イベントではなく結果を検証する

Google Sheets向け拡張で、synthetic eventの送信成功とユーザーから見た操作成功を分離したE2E設計。

Google Sheets上にキーボード操作を追加するChrome拡張Spexを作っています。

開発初期に一番危険だったのが、拡張機能側では成功しているように見えるのに、Google Sheets側では何も起きていない状態です。

例えば、

button.click();
return { status: "executed" };

はコードとしては正常終了します。

しかし既存Webアプリを操作するextensionでは、

eventを送れた

ことと、

ユーザーが期待した結果になった

ことは別です。

Spexでは最終的に、Commandを「どう呼ぶか」だけでなく「何を観測できれば成功といえるか」までセットで設計するようにしました。

Synthetic eventが届いても操作は成立しないことがある

Google Sheetsのような大きなWebアプリでは、操作ごとにUI実装が異なります。

  • 普通のDOM clickで動く
  • native shortcutで動く
  • popupは開くがselectionは変わらない
  • synthetic eventを無視する
  • control自体は存在するが、その時点ではdisabled

などが混在します。

そのためadapterが例外を投げなかっただけでは、成功判定として弱すぎました。

Commandごとにobservable resultを決める

SpexではCommand単位で、「ユーザーから見た成功」を観測可能な状態へ落とします。

Bold

Alt → O → B を実行したなら、toolbarのBold controlの aria-pressed が実際に反転することを見る。

before: aria-pressed=false
execute
after:  aria-pressed=true

Freeze First Row

単にView menuを開けたかではなく、1行目固定後のgrid構造が変わったことを確認する。

Insert Row Above

menu itemを押せたかではなく、行追加に伴うSpreadsheet構造の変化を確認する。

Dialog / Sidebar

「open actionを送れた」ではなく、期待したresult surfaceが実際に表示されたかを見る。

ここで大切なのは、すべてのCommandに共通のsuccess detectorを作らないことでした。

Formattingとrow mutationとdialog openでは、成功の意味が違います。

executedinvoked を分ける

すべての操作で結果を100%検証できるとは限りません。

例えばWeb permissionの都合でclipboard内容自体は確認できない、といった種類の操作があります。

その場合、

invoked

と、

result verified

を区別します。

ユーザーへ「成功しました」と断定できるのは、原則として結果まで観測できた場合です。

「処理は送ったが結果を検証できない」という状態も一級のstatusとして扱います。

この区別を持っておくと、adapter実装で不明なケースを無理に成功へ丸めなくて済みます。

Fail Closed

外部WebアプリのDOMは自分たちの管理下にありません。

selectorが見つからない、候補が複数ある、期待したattributeが消えた、といったことは将来必ず起こり得ます。

Spexでは、分からないときに「たぶんこれだろう」で実行しない方針を取っています。

Locator not found
Locator ambiguous
Action dispatch failure
Verification failure

を区別し、確信できなければfail closedします。

ショートカットツールで最悪なのは、無反応よりも「別の操作を成功として実行すること」です。

Mock E2Eは必要だが、それだけでは不足する

もちろん毎回実Google Sheetsだけでテストすると、遅く、不安定で、原因切り分けもしづらくなります。

そのためSpexではテストを階層化しています。

Core Unit Tests
  -> shortcut state machine
  -> context rules

Browser fixture / Adapter Tests
  -> controlled DOMでlocator/action/verification

Real Google Sheets E2E
  -> production bundle
  -> actual observable result

Mockやfixtureは「自分のコードが想定通り動くか」を高速に確認するために必要です。

実Sheets E2Eは、

自分の想定が現在のGoogle Sheetsでも成立しているか

を確認する役割です。

この二つを混ぜないようにしています。

Real E2EでToastだけを見ない

ExtensionのE2Eでは、自分たちが作ったUIだけをassertすると簡単です。

例えば、

Alt → O → B
-> Spex Toastに「Bold executed」
-> PASS

とすればテストは安定します。

しかしToastを出しているのもSpexです。

Spexの誤判定をSpex自身の表示で検証しても循環しています。

そのため対応済みCommandのacceptanceでは、Google Sheets側のobservable stateを必須にしました。

KeyTipsやToastのテストは別にUI contractとして持ちます。

Boldは小さいが優秀なCanaryだった

BoldはE2Eのcanaryとしてかなり役立ちました。

理由は、

  • controlが見つけやすい
  • aria-pressedでbefore/afterが明確
  • toggleなので往復できる
  • セル内容を壊さず何度も試せる
  • focus / IME / sequence resetまで同時に確認できる

からです。

実際のacceptanceでは、

  1. AltでKeyTipsが出る
  2. Alt → O → B
  3. aria-pressedが反転
  4. 直後にもう一度同じsequenceを実行
  5. 元の状態へ戻る
  6. IMEイベントが混ざっても文字入力されない
  7. 次のAlt sequenceを再開できる

まで確認します。

単なるBold testというより、keyboard interaction全体の健康診断になっています。

破壊的Commandは前提状態を自分で作る

行挿入やFreezeなどは、テスト同士が状態を共有するとすぐ壊れます。

そのため各テストが、

  • 自分の前提を作る
  • operationを実行する
  • observable resultを確認する
  • 必要なら元状態へ戻す

ようにします。

「前のテストで1行追加されているはず」のような順序依存を作りません。

外部WebアプリのE2Eはそれだけで不確実性が高いので、自分たちで増やす不確実性はできるだけ減らします。

失敗を分類できるようにする

Real E2Eが落ちたとき、単に

Expected true but got false

だけでは、Google側の変更なのか自分のCore bugなのか分かりません。

Spexでは少なくとも、

Core resolution failure
Context Gate rejection
Locator not found
Locator ambiguous
Action dispatch failure
Verification failure

を別のfailure classとして考えています。

例えば Locator not found が急増したならhost DOM changeを疑えます。

Verification failureならaction pathは通っているがSheets側の挙動が変わった可能性があります。

テストを「赤くする」だけでなく、保守の入口になる情報を返すことが重要でした。

Definition of DoneにReal E2Eを入れる

Spexでは新しいCommandを実装しただけでは「対応済み」としません。

概念的なDefinition of Doneは次です。

  1. Shortcut catalogへ登録
  2. Core unit test
  3. Adapter実装
  4. Result verification
  5. Real Google Sheets E2E
  6. 非干渉テスト
  7. 失敗しても通常入力へ安全に戻る

これによって「KeyTipsには見えるが実際には動かないCommand」を公開状態へ持ち込むのを防ぎます。

外部UI integrationでは「成功」を自分で定義しない

今回の一番の学びはここでした。

自分のコードが正常終了したことをsuccessとするのは簡単です。

でもintegration layerで大切なのは、相手側に起きたobservable resultをsuccessの根拠にすることです。

API integrationならHTTP responseやresource stateを見るのと同じで、DOM integrationでも「eventを送った」はrequestを送ったにすぎません。

結果の検証を別責務として設計すると、外部Webアプリの変更にもかなり気づきやすくなりました。

Spexで実際に検証しています

Spexのproduction bundleは、実Google Sheetsを使ったPlaywright E2Eでも継続的に検証しています。

外部Webアプリを自動操作するextensionやuserscriptを作る場合、「操作方法」の次に「何が見えれば成功なのか」をCommandごとに書いておくことをおすすめします。