心理学で、
人間関係を
ちょっとラクにする。
MBTIとアドラー心理学を軸に、
自分と他者を理解するヒントを発信しています。
何が起きたか
KDP自動出版パイプラインを深夜に回したら、価格設定ページで止まり続けた。スクリプトは正常終了を報告しているのに、Kindle管理画面を開くと本が「下書き」のまま。翌朝また回す→また止まる。これを2回繰り返した。原因は「価格フォームの入力は通っているが、保存前に次ページへ遷移しようとしていた」という単純なタイミングバグだった。気づくまでに合計で5時間くらい溶けた。
環境
- macOS Sequoia / MacBook Pro M5 32GB
- Claude Code(Sonnet)でスクリプト修正
- Playwright(非同期)で KDP ダッシュボードを操作
- スクリプト:
01_Scripts/kdp/kdp_ai_book_generator.py - 価格設定ロジック: 同ファイル内
_set_pricing()メソッド
詰まったポイント
最初の失敗ログがこれ。深夜5時過ぎに回したバッチだ。
価格フォームにはちゃんと数字が入っている。KDPの「マーケットプレイスごとの価格設定」テーブルに 9.99 USD が表示されている。見た目は正常。
ところが「保存して続行」ボタンを叩いた直後、ページが遷移せずに止まる。
URLは https://kdp.amazon.co.jp/title-setup/kindle/... の /pricing のまま動かない。エラーメッセージも出ない。ただ固まっている。
スクリプト側のログには "pricing saved: True" と出ているのに、ブラウザ上では保存が完了していなかった。
問題の箇所を抜き出すとこう↓。
# 修正前(バグあり)
await page.fill('input[data-testid="price-input-US"]', "9.99")
await page.click('button[data-testid="save-and-continue"]')
await page.wait_for_url("**/review**", timeout=10000)
fill() は DOM の value を書き換えるが、React の state までは更新しない場合がある。KDPのフォームはReact製で、fill() 直後に save-and-continue を押すと「未入力」扱いでバリデーションが通らないことがある——これが推測だったが、後で page.evaluate() で確認して合っていた。
解決までの手順
ステップ1 — 「保存済み」と「実際に保存された」を分離して確認する
page.fill() 後に入力値を page.input_value() で読み直して、期待値と一致しているか確認するコードを追加。
filled_val = await page.input_value('input[data-testid="price-input-US"]')
assert filled_val == "9.99", f"入力値不一致: {filled_val}"
これは通った。入力値は合っている。
ステップ2 — React の state 更新をトリガーする
fill() だけでは React が変化を検知しないパターンがある。dispatch_event() で input イベントを明示的に送った。
await page.fill('input[data-testid="price-input-US"]', "9.99")
await page.dispatch_event('input[data-testid="price-input-US"]', "input")
await page.dispatch_event('input[data-testid="price-input-US"]', "change")
これで React side effect が走り、ボタンが「押せる状態」に変わった(DOM の disabled 属性が消えるのを wait_for_selector で確認できた)。
ステップ3 — ボタンが enabled になるまで待ってから叩く
await page.wait_for_selector(
'button[data-testid="save-and-continue"]:not([disabled])',
timeout=8000
)
await page.click('button[data-testid="save-and-continue"]')
クリック後の wait_for_url タイムアウトも 10s → 20s に伸ばした。KDP の画面遷移はサーバーサイド処理が入るので遅い。
ステップ4 — 再実行して出版前状態を確認
レビューページへの遷移を確認。/pricing から /review へ移動している。あとは publish ボタンを叩くだけの状態になった。
コード/設定の抜粋
_set_pricing() の修正後の核心部分だけ。
async def _set_pricing(self, page, price_usd: float) -> bool:
price_str = f"{price_usd:.2f}"
# 入力
selector = 'input[data-testid="price-input-US"]'
await page.fill(selector, price_str)
# React state 更新を強制
await page.dispatch_event(selector, "input")
await page.dispatch_event(selector, "change")
# ボタンが有効になるまで待つ
btn = 'button[data-testid="save-and-continue"]:not([disabled])'
try:
await page.wait_for_selector(btn, timeout=8000)
except TimeoutError:
self.logger.error("保存ボタンが有効化されなかった")
return False
await page.click(btn)
# 遷移確認
try:
await page.wait_for_url("**/review**", timeout=20000)
return True
except TimeoutError:
current = page.url
self.logger.error(f"遷移失敗: {current}")
return False
KDP の URL パターンはロケールによって微妙に変わるので **/review** のグロブが安全。
試してわかったこと
React製フォームは fill() で見た目が変わっても内部 state が追いついていないことがある。特に KDP・note・coconala といった「保存ボタンが動的に disabled/enabled を切り替えるタイプ」の画面で詰まりやすい。
手順を整理すると:
– fill() → dispatch_event("input") + dispatch_event("change") → wait_for_selector(:not([disabled])) → click() → wait_for_url()
この5ステップを全部踏まないと「保存した気になっているだけ」になる。
タイムアウト値は「体感より2倍長く」がちょうどいい。KDP のサーバー処理は夜中でも10〜15秒かかることがある。焦って短くすると夜中に失敗を量産する。
あと「スクリプトが成功ログを出した」と「実際に保存された」は別の話だと今回痛感した。成功判定のロジックを「URL遷移の確認」に変えてから、偽陽性がゼロになった。
まとめ
React フォームは fill() だけでは state が更新されない場合がある。dispatch_event("input") と change を両方送ること。ボタンの disabled 解除を待ってからクリックし、URL 遷移で成功を確認するまでがセット。「保存された」と「保存されたログが出た」は別物——これだけで夜中の積み重ねは防げる。
