리팩터링
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user