Medium のエディタ(contenteditable)に、Playwright で記事の HTML を貼り付けて下書きを作る処理を回しています。
ある日、公開済みの記事を読み返したら、後ろ半分(手順の3〜5とチェックリスト)が丸ごと無い状態で公開されていました。貼り付けた直後の画面のスクリーンショットには、最後の段落まで写っていました。この記事は、その原因と、保存まで確かめる手順です。
起きたこと
- 貼り付けの約6秒後に、確認が失敗したのでブラウザを閉じていた
- 画面上は最後まで入っていたが、サーバーに保存されていたのは途中の段落まで
- その下書きを、人が中身を最後まで見ずに公開してしまった
- 次の週の下書きも、ほぼ同じ位置で切れていた
原因1:閉じるのが早すぎた
エディタの自動保存は、貼り付けた瞬間に全部を送るわけではありません。数秒おきに差分を送るので、送り終わる前にコンテキストを閉じると、そこまでしか残りません。画面(DOM)に入っていることと、保存されたことは別でした。
原因2:確認が「先頭だけ」で、しかも誤って落ちていた
元の確認は、本文の先頭25文字が画面にあるかだけを見ていました。
const head = bodyHtml.replace(/<[^>]+>/g, '').slice(0, 40);
if (!text.includes(head.slice(0, 25))) await fail(4, '本文が入っていない');
これには穴が2つありました。
- エディタが
'を’、"を“ ”に置き換える。HTML 側には'のような実体参照が残る。このため、本文は入っているのに一致せず失敗になり、すぐ閉じていた - 先頭が合っていても、末尾まで入ったか・保存されたかは分からない
手順1:比べる前に表記をそろえる
const norm = s => s
.replace(/'|'/g, "'").replace(/"/g, '"').replace(/&/g, '&')
.replace(/[‘’]/g, "'").replace(/[“”]/g, '"')
.replace(/\s+/g, ' ');
const plain = norm(bodyHtml.replace(/<[^>]+>/g, ' ')).trim();
const head = plain.slice(0, 30);
const tail = plain.slice(-30); // 末尾も見る
手順2:「保存済み」の表示と末尾の両方が出るまで待つ
const editorText = () => page.evaluate(() =>
[...document.querySelectorAll('[contenteditable="true"]')].map(e => e.innerText).join('\n'));
let saved = false;
for (let k = 0; k < 30 && !saved; k++) { // 最大90秒
await page.waitForTimeout(3000);
const header = await page.evaluate(() => document.body.innerText.slice(0, 400));
// 見出しは「DraftSaved」のように単語がつながって出る。\bSaved\b だと一致しない
saved = /Saved/.test(header) && norm(await editorText()).includes(tail);
}
if (!saved) await fail(4, '末尾まで保存されたことを確かめられない');
実際にここで一度はまりました。保存の表示は画面上「Draft」「Saved」と並んで見えます。ところが innerText では DraftSaved とつながっていて、\bSaved\b が一度も一致しませんでした。
手順3:開き直して、末尾が残っているかを見る
保存されたかの最終確認は、ページを読み込み直した後の中身で行います。長い記事は下までスクロールしないと描画されないことがあるので、スクロールしてから読みます。
await page.waitForTimeout(5000);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(9000);
for (let i = 0; i < 15; i++) { await page.mouse.wheel(0, 2500); await page.waitForTimeout(500); }
const after = norm(await editorText());
if (!after.includes(head) || !after.includes(tail)) {
await fail(4, '開き直すと本文の先頭か末尾が無い(途中までしか保存されていない)');
}
確かめ方
- 直した版で同じ記事を流し、開き直した後の本文が全文(8,420字)あることを確認した
- 公開ページの確認は、RSS や「取得した HTML」だけで判断しない。どちらも途中で切れていることがあり、切れた記事と見分けられない。決まった語(各見出し・最後の段落の語)が何回出るかを数える
まとめ
- 「画面に入った」と「保存された」は別。自動保存のエディタは、閉じるのを待たせる
- 確認は先頭だけでなく末尾も見る。比べる前に、引用符や実体参照の表記をそろえる
- 最終確認は開き直した後の中身で行う
- 確認が誤って落ちて早く閉じると、それ自体が「途中保存」の原因になる。誤検知の方向も確かめておく