機能の end-to-end 動作確認を Playwright で自動化したスクリプト群です。CI には組み込まれていません(手動で node <script>.mjs で実行)。複数タブ間の連動 (Mesh v2 ↔ クラス管理など) のように unit / integration テストでは網羅しづらい挙動を、ローカルでサッと回すための場所です。
- dev server が起動中:
docker compose up -d appで http://localhost:8601 が応答する状態 .envのDEV_BYPASS_TOKEN: 教師の Google ログインをバイパスするためのトークンが設定されている(stg / ローカル前提)- このディレクトリで
npm install済み(最初の 1 回のみ)
cd tools/playwright-verify
npm install # 初回のみ
npx playwright install chromium # 初回のみ(OS の Playwright cache が無い場合)node <script>.mjsヘッドフルブラウザが 2 つ立ち上がります。スクリプトは Redux store / DOM testid の両方を経由して状態を確認し、最後にスクリーンショットを .screenshots/ に保存します。
ブラウザプロファイルは .profiles/teacher .profiles/student に永続化されます (両方 gitignore 済み)。永続化されているのは Chromium の cookie / cache / IndexedDB のみ — 教師 idToken は in-memory なのでリロード/再起動で再ログインが必要です。プロファイルをリセットしたい場合:
rm -rf .profilesdevpod コンテナは画面を持たない(DISPLAY 未設定)ので、コンテナ内で headless:false
にしてもウィンドウは出ない。ブラウザはホスト(Mac)側で開き、dev サーバはコンテナで動かす
構成にする。/app はホストのバインドマウントなので、ここのスクリプトはそのままホストにもある。
前提:
- dev サーバはコンテナ内で起動(
set -a; . ./.env; set +a; SMALRUBY3_HOST=0.0.0.0 PORT=8601 npm start) devcontainer.jsonのforwardPorts: [8601]により ホストからlocalhost:8601に到達可能 (普段ホストのブラウザでエディタを見ているのと同じ経路)
ホスト(Mac)側で実行:
cd <repo>/tools/playwright-verify
# 初回のみ: ホスト用のブラウザを用意(どちらか)
# a) 既存の Google Chrome を使う → 追加インストール不要(CHANNEL=chrome)
# b) Playwright 同梱 Chromium を使う → npx playwright install chromium
# 目視モードで実行(実 Chrome ウィンドウが開く・ゆっくり動く・最後まで開いたまま)
HEADLESS=false CHANNEL=chrome SLOWMO=300 KEEP_OPEN=1 node smoke-teacher-dashboard.mjs同じスクリプトをコンテナ内では node smoke-teacher-dashboard.mjs(headless 既定)で回せる。
env トグル: HEADLESS=false 表示 / CHANNEL=chrome 実 Chrome / SLOWMO=<ms> 低速 /
KEEP_OPEN=1 終了後も開いたまま / BASE_URL=... 接続先上書き。
メモ: ホスト node が Linux 用に入った
node_modulesを読んで不具合が出たら、ホストでnpm installし直す(playwright 本体は JS なので通常はそのまま動く。ブラウザバイナリは OS 別キャッシュ)。
| ファイル | 検証対象 |
|---|---|
verify-notification-center.mjs |
お知らせセンター(EPIC #1111)。classroom API を route interception でスタブするので stg 不要(dev server 8601 のみ前提)。教師モーダル右上 🔔 + 未読バッジ → パネル開で mark-read が飛びバッジが消える → 未読ドット 1 個 → アイテムクリックでパネルが閉じる |
verify-shared-recommendation.mjs |
Admin 推薦 → 全体公開への発展(EPIC #1110)。同じくスタブ式で stg 不要。推薦通知(link.kind='shared-mine')クリック → 自分の投稿へジャンプ → 限定公開/推薦バッジ → 「みんなの課題に公開する」フォーム(初期値・CC BY 同意必須)→ PATCH visibility='public' の payload まで検証 |
verify-share-suggestion.mjs |
共有推奨(EPIC #1106)。同じくスタブ式で stg 不要。share_suggestion 通知クリック → 課題詳細へジャンプ → 「共有しませんか?」バナー → CTA でボードの共有ステップが開く → ボード行の「共有おすすめ」マーク |
verify-assignment-sharing.mjs |
みんなの課題(EPIC #1066)の通し。クラス+課題作成→説明ページ保存→共有フォーム(CC BY 同意)→カタログ絞り込み→詳細(© クレジット・外部リンク確認 D4)→このクラスに取り込み→ボードに新行→自分の投稿で取り下げ/再公開。stg に S1 API(#1068)が必要。LOCALE=ja-JP で日本語スクリーンショット |
verify-admin.mjs |
管理 SPA(EPIC #1073)の通し。stg API への /admin/me プリフライト(403 = allowlist 未登録を明示)→ 8602 の SPA dev server を自前起動(REUSE_SERVER=1 で再利用)→ ログインゲート → ?devlogin= バイパス → みんなの課題キュー → クラス検索 + 期限切れ復元タブ → バグ報告一覧(詳細に書き込み UI が無いこと)。前提: AdminStack-stg デプロイ + SmalrubyAdmins-stg に dev-admin@example.com 登録(docs/admin/operations.md) |
verify-classroom-archive-recovery.mjs |
アーカイブ復旧(EPIC #1049)の通し。クラス+課題作成→保存期限バッジ(あと{days}日)→課題詳細の期限バナー+全作品DL CTA→クラス全体一括DL(zip 名 _全課題.zip)→課題アーカイブ(確認文言が「アーカイブ」であること)→(S1 API デプロイ後)アーカイブ済み課題の復元→クラスの 2 段階アーカイブ確認→アーカイブ済みクラス一覧→復元。LOCALE=ja-JP で日本語スクリーンショット(docs 用) |
mesh-v2-classroom-binding.mjs |
クラス管理と Mesh v2 ドメインの連動。教師タブでクラス作成→サイドバーで選択、生徒タブで ?classcode= 経由参加し、両方の state.scratchGui.meshV2.domain が参加コードに揃うこと、接続モーダル入力欄が disabled になること、解除時に元のドメインに戻ることをチェック |
verify-lesson-support-phase3.mjs |
AI 評価(EPIC #974 Phase 3)の通し。生徒提出→評価画面で読み込み(ブラウザ内 sb3 解析)→AI評価実行(実 Claude via stg)→根拠付き S/A/B/C→コメント下書き→返却→生徒側で表示確認。ポート・CORS の注意は phase1 と同じ |
verify-lesson-support-phase2.mjs |
組(EPIC #974 Phase 2)の UI 通し。組作成→サイドバー階層→クラスの組割当→授業複製(のコピー)→アーカイブでクラス名グルーピングへフォールバック。ポート・CORS の注意は phase1 と同じ |
verify-lesson-support-phase1.mjs |
課題配信(EPIC #974 Phase 1)の通し。教師: クラス作成→課題エディタで2ページ+スターター(今開いているプロジェクト)を保存。生徒: ?classcode= 参加→課題パネル自動表示→ページ送り→スターターボタン→「はじめる!」→ステータスの「課題を見る」で再表示。8601 以外のポートで動かすときは DISABLE_WEB_SECURITY=1(stg の CORS が localhost:8601 のみ許可のため) |
スクリプトを書く / 拡張するときの留意点。失敗を踏み抜いた後の蓄積です。
?devlogin=<DEV_BYPASS_TOKEN> を URL に付けると教師として自動ログインしてダッシュボードまで到達します(.env の DEV_BYPASS_TOKEN を使用)。Google OAuth 待ちが不要になり、CI 化の道筋ができます。
window.smalruby は Ruby タブを訪れた後にしか存在しません。Redux store が必要なら React 18 の Fiber Tree から memoizedProps.store または memoizedProps.value.store を探すヘルパーを使います(各スクリプトの findStore() 参照)。掘り出した store は window.__store に貼って後続 evaluate で使い回します。
classroom-phase-teacher-dashboard: ダッシュボードフェーズの testid はcomponents/classroom-teacher-modal/classroom-teacher-modal.jsxのデフォルト分岐に付けた(旧コードでは login と google-courses 以外に testid が無く、フェーズ検知できなかった)- サイドバー項目:
classroom-sidebar-item-{classroomId}(classroom-item-*ではない)。表示テキストはassignmentName · 人数 · 参加コード(小文字) - クラス作成:
classroom-name-input/classroom-count-input/classroom-assignment-name-inputの 3 つすべて 必須。1 つでも空だとclassroom-create-submitは disabled - 作成後の遷移:
classroom-create-submit押下 →phase==='teacher-dashboard'に戻る(自動で詳細画面に遷移しない)。新クラスはサイドバーに追加されるが selectedClassroom は null のまま — テストでteacherSelectionを埋めたいなら明示的にサイドバー項目をクリック
state.scratchGui.classroom.role: student のみ。teacher ログイン中でも nullstate.scratchGui.classroom.joinCode: 学生セッションの参加コードstate.scratchGui.classroom.teacherSelection.joinCode: 教師がサイドバーで選んだクラスの参加コードstate.scratchGui.meshV2.domain: Mesh v2 ドメイン(バインディングは小文字化した joinCode を入れる)
- 「同じ assignmentName を 2 度使うと前回のクラスが選ばれてしまう」:
Date.now()でユニーク化する - 「全席 taken でテストが進まない」: 同じクラスに既に上限まで参加している。新規クラスを作成するか、空席を
:not([disabled])で動的に選ぶ - 「modal が閉じる」: persistent context は同じプロファイルを使い回すので、前回のテストで残った state が悪さする場合がある。
rm -rf .profilesしてやり直す
先生ログイン後の landing は クラス一覧(classroom-phase-teacher-class-list)。各スクリプトは「クラスを作る」(クラス + 最初の課題の同時作成フォーム classroom-class-create-*)でセットアップし、クラス内の課題ボード(classroom-board)から課題をひらく。旧フロー(サイドバーの classroom-create から作成)は存在しない。