Files
page-builder/tests/e2e/public-seo.spec.ts
jaybe 7491dacba2
CI / test (push) Successful in 5m38s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Successful in 1m12s
리팩터링
2025-12-16 17:49:25 +09:00

232 lines
7.8 KiB
TypeScript

import { test, expect } from "@playwright/test";
async function signupAndGetAccessToken(
request: import("@playwright/test").APIRequestContext,
email: string,
password: string,
): Promise<string> {
const signupRes = await request.post("/api/auth/signup", {
headers: { "Content-Type": "application/json" },
data: { email, password },
});
const signupStatus = signupRes.status();
if (signupStatus !== 201) {
// 디버깅용: CI 등에서 500 이 떨어질 때 응답 바디를 함께 로그로 출력해 원인을 파악할 수 있게 한다.
console.log("[public-seo] signup error status=", signupStatus);
try {
const bodyText = await signupRes.text();
console.log("[public-seo] signup error body=", bodyText);
} catch (e) {
console.log("[public-seo] signup error: body read failed", e);
}
}
expect(signupStatus).toBe(201);
const setCookieHeader = signupRes.headers()["set-cookie"];
expect(setCookieHeader).toBeTruthy();
const cookieHeaderString = Array.isArray(setCookieHeader)
? setCookieHeader.join("; ")
: (setCookieHeader as string);
const match = cookieHeaderString.match(/pb_access=([^;]+)/);
expect(match).not.toBeNull();
const accessToken = match![1];
return accessToken;
}
async function createProjectWithSnapshot(
request: import("@playwright/test").APIRequestContext,
accessToken: string,
params: { slug: string; title: string; projectConfig: any },
) {
const { slug, title, projectConfig } = params;
const res = await request.post("/api/projects", {
headers: {
"Content-Type": "application/json",
Cookie: `pb_access=${accessToken}`,
},
data: {
title,
slug,
// 새 스펙: contentJson 은 blocks + projectConfig 스냅샷으로 보낸다.
contentJson: {
blocks: [],
projectConfig,
},
},
});
const status = res.status();
if (status !== 201) {
console.log("[public-seo] createProject error status=", status);
try {
const bodyText = await res.text();
console.log("[public-seo] createProject error body=", bodyText);
} catch (e) {
console.log("[public-seo] createProject error: body read failed", e);
}
}
expect(status).toBe(201);
}
async function patchProjectStatus(
request: import("@playwright/test").APIRequestContext,
accessToken: string,
slug: string,
status: "DRAFT" | "PUBLISHED" | "ARCHIVED",
) {
const res = await request.patch(`/api/projects/${slug}`, {
headers: {
"Content-Type": "application/json",
Cookie: `pb_access=${accessToken}`,
},
data: { status },
});
const code = res.status();
if (code !== 200) {
console.log("[public-seo] patchProjectStatus error status=", code);
try {
const bodyText = await res.text();
console.log("[public-seo] patchProjectStatus error body=", bodyText);
} catch (e) {
console.log("[public-seo] patchProjectStatus error: body read failed", e);
}
}
expect(code).toBe(200);
}
// 퍼블릭 페이지(/p/[slug])에서 프로젝트 SEO 설정이 실제 <title>/메타 태그/스크립트에 반영되는지 검증하는 E2E.
// - seoTitle/seoDescription/seoCanonicalUrl/seoOgImageUrl → title 및 OG/Twitter 메타
// - seoNoIndex=true → <meta name="robots" content="noindex, nofollow"> 존재
// - seoCanonicalUrl 은 canonical <link> 가 아니라 og:url 로만 노출되어야 한다.
// - headHtml 은 <head> 에 그대로 삽입되고, trackingScript 는 <body> 에 삽입/실행되어야 한다.
test.describe("Public SEO metadata", () => {
test("PUBLISHED 프로젝트의 SEO 설정이 <title> 및 메타 태그에 반영되어야 한다", async ({ page, request }) => {
const now = Date.now();
const email = `public-seo-${now}@example.com`;
const password = "public-seo-password";
const slug = `public-seo-${now}`;
const accessToken = await signupAndGetAccessToken(request, email, password);
const seoTitle = "SEO 타이틀 - My landing";
const seoDescription = "SEO 설명입니다.";
const seoUrl = `https://example.com/${slug}`;
const seoImage = "https://example.com/og-image.png";
const projectConfig = {
title: "에디터 타이틀",
slug,
canvasPreset: "full",
canvasBgColorHex: "#020617",
bodyBgColorHex: "#020617",
seoTitle,
seoDescription,
seoCanonicalUrl: seoUrl,
seoOgImageUrl: seoImage,
seoNoIndex: false,
headHtml: '<meta name="x-public-head" content="public-seo-test" />',
trackingScript: "<script>window.__pbPublicSeoTracking = 'ok';</script>",
};
await createProjectWithSnapshot(request, accessToken, {
slug,
title: projectConfig.title,
projectConfig,
});
await patchProjectStatus(request, accessToken, slug, "PUBLISHED");
const response = await page.goto(`/p/${slug}`);
expect(response?.status()).toBe(200);
// 1) <title>
await expect(page).toHaveTitle(seoTitle);
// 2) 기본/OG/Twitter 메타 태그
const metaDesc = page.locator('meta[name="description"]');
await expect(metaDesc).toHaveAttribute("content", seoDescription);
const ogTitle = page.locator('meta[property="og:title"]');
await expect(ogTitle).toHaveAttribute("content", seoTitle);
const ogDesc = page.locator('meta[property="og:description"]');
await expect(ogDesc).toHaveAttribute("content", seoDescription);
const ogUrl = page.locator('meta[property="og:url"]');
await expect(ogUrl).toHaveAttribute("content", seoUrl);
const ogImage = page.locator('meta[property="og:image"]');
await expect(ogImage).toHaveAttribute("content", seoImage);
const twitterTitle = page.locator('meta[name="twitter:title"]');
await expect(twitterTitle).toHaveAttribute("content", seoTitle);
// 3) seoNoIndex=false 인 경우 robots 메타는 없어야 한다.
const robots = page.locator('meta[name="robots"]');
await expect(robots).toHaveCount(0);
// 4) canonical <link> 는 생성되지 않아야 한다.
const canonicalLink = page.locator('link[rel="canonical"]');
await expect(canonicalLink).toHaveCount(0);
// 5) headHtml 스니펫이 <head> 에 삽입되어야 한다.
const customHeadMeta = page.locator('meta[name="x-public-head"]');
await expect(customHeadMeta).toHaveAttribute("content", "public-seo-test");
// 6) trackingScript 가 <body> 에 삽입 및 실행되어 window 플래그를 남겨야 한다.
const trackingValue = await page.evaluate(() => (window as any).__pbPublicSeoTracking ?? null);
expect(trackingValue).toBe("ok");
});
test("seoNoIndex=true 인 PUBLISHED 프로젝트는 robots noindex 메타 태그를 포함해야 한다", async ({ page, request }) => {
const now = Date.now();
const email = `public-seo-noindex-${now}@example.com`;
const password = "public-seo-noindex-password";
const slug = `public-seo-noindex-${now}`;
const accessToken = await signupAndGetAccessToken(request, email, password);
const seoTitle = "Noindex SEO 타이틀";
const seoDescription = "Noindex SEO 설명입니다.";
const projectConfig = {
title: "Noindex 프로젝트",
slug,
canvasPreset: "full",
canvasBgColorHex: "#020617",
bodyBgColorHex: "#020617",
seoTitle,
seoDescription,
seoCanonicalUrl: "",
seoOgImageUrl: "",
seoNoIndex: true,
};
await createProjectWithSnapshot(request, accessToken, {
slug,
title: projectConfig.title,
projectConfig,
});
await patchProjectStatus(request, accessToken, slug, "PUBLISHED");
const response = await page.goto(`/p/${slug}`);
expect(response?.status()).toBe(200);
await expect(page).toHaveTitle(seoTitle);
const robots = page.locator('meta[name="robots"]');
await expect(robots).toHaveAttribute("content", /noindex/);
});
});