A document with no preview yet answers 404 when the screen asks for its current one, and the screen turns that into "make a preview". The sweep counted it as a failure, so every run ended with the same red line under a healthy deployment. A check that cries wolf on every run stops being read, and a real failure would have sat unnoticed beside it. The session probe's 401 before sign-in is the same kind of expected answer and is excluded on the same terms; every other 4xx and 5xx still fails the sweep.
112 lines
5.4 KiB
TypeScript
112 lines
5.4 KiB
TypeScript
/**
|
|
* 배포본 전수 확인.
|
|
*
|
|
* 이 파일은 절차 실패에서 나왔다 — 고친 화면만 확인하고 배포해서, 나머지가 깨진 것은 매번
|
|
* 사용자가 먼저 발견했다. 운영 환경이므로 배포 전에 모든 화면을 한 번씩 열어 보는 것이 맞다.
|
|
*
|
|
* 각 화면에서 보는 것: main 이 그려졌는지, 콘솔 오류, 4xx/5xx API 응답, 그리고 화면에 뜬
|
|
* 오류 문구. 하나라도 있으면 그 화면을 실패로 적고 끝까지 진행한다.
|
|
*
|
|
* SPW=<비밀번호> node scripts/smoke/production-sweep.mjs [origin]
|
|
*/
|
|
import process from "node:process";
|
|
|
|
import { chromium, type Page } from "@playwright/test";
|
|
|
|
const ORIGIN = process.argv[2] ?? "https://hyeonworks.com";
|
|
const PW = process.env.SPW;
|
|
const ERROR_TEXT =
|
|
/요청을 처리하지 못했습니다|지원 정보 확인|표시할 수 없습니다|불러오지 못했습니다|화면을 찾을 수 없습니다|Not Found/;
|
|
|
|
const results: { label: string; path: string; problems: string[] }[] = [];
|
|
|
|
async function visit(
|
|
page: Page,
|
|
label: string,
|
|
path: string,
|
|
{ expectMain = true }: { expectMain?: boolean } = {},
|
|
) {
|
|
const problems: string[] = [];
|
|
const onConsole = (m: { type(): string; text(): string }) => { if (m.type() === "error" && !/401/.test(m.text())) problems.push(`console: ${m.text().slice(0, 120)}`); };
|
|
const onResponse = (r: {
|
|
url(): string;
|
|
status(): number;
|
|
request(): { method(): string };
|
|
}) => {
|
|
const u = new URL(r.url()).pathname;
|
|
if (r.status() < 400 || !u.startsWith("/api")) return;
|
|
// 두 가지는 화면이 다루는 정상 상태다: 로그인 전 세션 탐침의 401, 그리고 아직 미리보기를
|
|
// 만들지 않은 문서의 404. 이것들을 실패로 세면 매번 같은 줄이 뜨고, 진짜 실패가 그 사이에
|
|
// 묻힌다 — 늑대가 왔다고 매번 외치는 점검은 아무도 읽지 않는다.
|
|
if (u.includes("/studio/session")) return;
|
|
if (r.status() === 404 && r.request().method() === "GET" && u.endsWith("/preview")) return;
|
|
problems.push(`${r.status()} ${r.request().method()} ${u}`);
|
|
};
|
|
page.on("console", onConsole);
|
|
page.on("response", onResponse);
|
|
try {
|
|
await page.goto(ORIGIN + path, { waitUntil: "domcontentloaded", timeout: 45000 });
|
|
await page.waitForTimeout(3000);
|
|
const main = await page.locator("main").count();
|
|
const body = (await page.locator("body").innerText().catch(() => "")).replace(/\s+/g, " ");
|
|
if (expectMain && main === 0) problems.push("main 없음");
|
|
const shown = body.match(ERROR_TEXT);
|
|
if (shown) problems.push(`화면 문구: ${shown[0]}`);
|
|
} catch (error) {
|
|
problems.push(`이동 실패: ${String(error).slice(0, 90)}`);
|
|
} finally {
|
|
page.off("console", onConsole);
|
|
page.off("response", onResponse);
|
|
}
|
|
results.push({ label, path, problems });
|
|
console.log(`${problems.length ? "✗" : "✓"} ${label.padEnd(22)} ${path}`);
|
|
for (const p of problems) console.log(` ${p}`);
|
|
}
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await (await browser.newContext()).newPage();
|
|
|
|
console.log("=== 공개 ===");
|
|
for (const [label, path] of [
|
|
["홈", "/"], ["탐색", "/explore"], ["Case 목록", "/explore/cases"],
|
|
["프로젝트", "/projects"], ["변경 기록", "/releases"], ["릴리즈 상세", "/releases/0.1.0"],
|
|
["검색", "/search"], ["프로필", "/profile"],
|
|
]) await visit(page, label, path);
|
|
|
|
if (!PW) { console.log("\n(SPW 없음 — Studio 생략)"); await browser.close(); process.exit(0); }
|
|
|
|
console.log("\n=== 로그인 ===");
|
|
await page.goto(ORIGIN + "/studio", { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
await page.waitForTimeout(2500);
|
|
const start = page.getByRole("button", { name: /로그인 시작/ }).or(page.getByRole("link", { name: /로그인 시작/ }));
|
|
if (await start.count()) { await start.first().click(); await page.waitForTimeout(5000); }
|
|
await page.fill("#username", "hyeonworks");
|
|
await page.fill("#password", PW);
|
|
await page.click("#kc-login, input[type=submit], button[type=submit]");
|
|
await page.waitForTimeout(6000);
|
|
console.log(" 로그인 후:", page.url().replace(ORIGIN, "") || "/");
|
|
|
|
console.log("\n=== Studio ===");
|
|
for (const [label, path] of [
|
|
["대시보드", "/studio"], ["작업본", "/studio/documents"], ["새 문서", "/studio/documents/new"],
|
|
["게시 기록", "/studio/publications"], ["Asset", "/studio/assets"],
|
|
["주제·프로젝트", "/studio/taxonomy"], ["릴리즈", "/studio/releases"],
|
|
]) await visit(page, label, path);
|
|
|
|
// 작업본 하나를 골라 편집·검증·미리보기까지 연다
|
|
await page.goto(ORIGIN + "/studio/documents", { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(3000);
|
|
const href = await page.locator("a[href*='/studio/documents/'][href$='/edit']").first().getAttribute("href").catch(() => null);
|
|
if (href) {
|
|
const id = href.split("/")[3];
|
|
console.log("\n=== 문서 흐름 ===", id);
|
|
for (const [label, suffix] of [["편집", "/edit"], ["검증", "/validation"], ["미리보기", "/preview"], ["게시", "/publish"]])
|
|
await visit(page, label, `/studio/documents/${id}${suffix}`);
|
|
} else console.log("\n(편집 링크를 찾지 못해 문서 흐름 생략)");
|
|
|
|
await browser.close();
|
|
const failed = results.filter((r) => r.problems.length);
|
|
console.log(`\n=== 결과 === ${results.length - failed.length}/${results.length} 통과`);
|
|
for (const r of failed) console.log(` ✗ ${r.label} (${r.path}): ${r.problems.join(" | ").slice(0, 160)}`);
|
|
process.exit(failed.length ? 1 : 0);
|