ความคืบหน้างาน ไฟล์ต้นฉบับ

tracker/e2e/review-check.mjs

// End-to-end check of the review form in a real browser.
//
// Run it against a tracker that uses a COPY of the data, because it submits a
// review:
//   cp -r data /tmp/review-data && go run . -addr :8098 -data /tmp/review-data
//   node e2e/review-check.mjs <path-to-chrome> http://localhost:8098 /tmp/review-data/milestones.json T-201
//
// Checks: a live update does not wipe a half-written review; Reject + text +
// checklist tick + reviewer name saves; the page shows the saved notice on the
// Rejected tab; the data file changed; the reviewer name is remembered.
// Needs Node 22+ (global fetch and WebSocket).
import { spawn } from "node:child_process";
import { readFileSync, utimesSync } from "node:fs";

const [chrome, base, dataFile, taskId] = process.argv.slice(2);
if (!chrome || !base || !dataFile || !taskId) {
  console.error("usage: node e2e/review-check.mjs <chrome> <base-url> <milestones.json copy> <task-id>");
  process.exit(2);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const proc = spawn(chrome, ["--headless=new", "--no-sandbox", "--disable-gpu", "--remote-debugging-port=9337", "about:blank"], { stdio: "ignore" });
let ws;
let id = 0;
const pending = new Map();
const send = (method, params = {}) =>
  new Promise((res) => {
    const i = ++id;
    pending.set(i, res);
    ws.send(JSON.stringify({ id: i, method, params }));
  });
const evaluate = async (expression) =>
  (await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true })).result.value;

const results = [];
const check = (name, ok, detail = "") => {
  results.push(ok);
  console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " (" + detail + ")" : ""}`);
};

try {
  let target;
  for (let i = 0; i < 50 && !target; i++) {
    try {
      target = (await (await fetch("http://127.0.0.1:9337/json")).json()).find((t) => t.type === "page");
    } catch {}
    await sleep(200);
  }
  ws = new WebSocket(target.webSocketDebuggerUrl);
  await new Promise((r) => ws.addEventListener("open", r));
  ws.addEventListener("message", (e) => {
    const m = JSON.parse(e.data);
    if (pending.has(m.id)) {
      pending.get(m.id)(m.result);
      pending.delete(m.id);
    }
  });

  await send("Page.navigate", { url: `${base}/reviews?state=todo` });
  await sleep(2500);
  const form = `document.querySelector('#${taskId} form.review-form')`;
  check("form is shown for the task", await evaluate(`!!${form}`));

  // Type into the textarea the way a person does, then change the data file.
  await evaluate(`(() => { const t = ${form}.querySelector('textarea'); t.focus(); t.value = 'ร่างผลการรีวิว **ยังไม่บันทึก**'; t.dispatchEvent(new Event('input', { bubbles: true })); })()`);
  const now = new Date();
  utimesSync(dataFile, now, now); // makes the watcher push an update
  await sleep(2500);
  check("live update keeps the half-written text",
    (await evaluate(`${form}.querySelector('textarea').value`)) === "ร่างผลการรีวิว **ยังไม่บันทึก**");

  // Fill the form and save.
  const before = readFileSync(dataFile, "utf8");
  await evaluate(`(() => {
    const f = ${form};
    f.querySelector('textarea').value = 'ต้องแก้ **ข้อ 2** ก่อน';
    f.querySelector('input[value="reject"]').click();
    f.querySelector('input[name="checklist"][value="0"]').click();
    f.querySelector('input[name="reviewer"]').value = 'ทดสอบ e2e';
    f.querySelector('button[type="submit"]').click();
  })()`);
  await sleep(2500);
  const url = await evaluate("location.href");
  check("redirected to the Rejected tab", url.includes("state=rejected") && url.includes(`saved=${taskId}`), url);
  check("saved notice is shown", await evaluate("!!document.querySelector('.notice')"));
  check("decision renders as Markdown", await evaluate(`!!document.querySelector('#${taskId} .decision strong')`));
  const after = readFileSync(dataFile, "utf8");
  check("data file records the review", after !== before && after.includes("ต้องแก้ **ข้อ 2** ก่อน") && after.includes('"decided_by": "ทดสอบ e2e"'));

  await send("Page.navigate", { url: `${base}/reviews?state=todo` });
  await sleep(2000);
  check("reviewer name is remembered",
    (await evaluate(`[...document.querySelectorAll('input[name="reviewer"]')].every((el) => el.value === 'ทดสอบ e2e')`)) === true);
} finally {
  ws?.close();
  proc.kill();
}
const ok = results.length > 0 && results.every(Boolean);
console.log(ok ? "PASS" : "FAIL");
process.exit(ok ? 0 : 1);