TDD,E2E 개선, 미적용 스타일 개선
This commit is contained in:
@@ -531,6 +531,520 @@ describe("/api/export", () => {
|
||||
expect(formHtml).toContain('name="__config"');
|
||||
});
|
||||
|
||||
it("formInput 플로팅 라벨은 Export 에서 placeholder 텍스트와 라벨이 겹치지 않아야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_floating_1",
|
||||
type: "form",
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
fields: [],
|
||||
fieldIds: ["field_name"],
|
||||
submitButtonId: null,
|
||||
formWidthMode: "full",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "field_name",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이름",
|
||||
formFieldName: "name",
|
||||
labelDisplay: "floating",
|
||||
// placeholder 가 라벨과 같을 때도 Export 에서는 placeholder 텍스트가 인풋 안에 보이지 않아야 한다.
|
||||
placeholder: "이름",
|
||||
required: true,
|
||||
} as any,
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "플로팅 라벨 Export 테스트",
|
||||
slug: "form-floating-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
|
||||
const indexEntry = zip.file("index.html");
|
||||
expect(indexEntry).toBeTruthy();
|
||||
|
||||
const html = await indexEntry!.async("string");
|
||||
|
||||
// 플로팅 라벨의 컨테이너는 pb-form-field pb-form-field--floating 클래스를 포함해야 한다.
|
||||
expect(html).toContain("pb-form-field pb-form-field--floating");
|
||||
|
||||
// name="name" 인 input 태그를 찾아 placeholder 를 검사한다.
|
||||
const inputMatch = html.match(/<input[^>]*name=\"name\"[^>]*>/);
|
||||
expect(inputMatch).not.toBeNull();
|
||||
|
||||
const inputTag = inputMatch![0];
|
||||
// placeholder 안에 "이름" 텍스트가 그대로 노출되면 안 된다.
|
||||
expect(inputTag).not.toContain('placeholder="이름"');
|
||||
// 대신 플로팅 라벨 전용으로 공백 placeholder 를 사용한다.
|
||||
expect(inputTag).toContain('placeholder=" "');
|
||||
|
||||
// 플로팅 라벨 컨테이너 내부에서 input 이 label 보다 먼저 나와야 CSS sibling 기반 플로팅이 동작한다.
|
||||
const fieldMatch = html.match(
|
||||
/<div class=\"pb-form-field pb-form-field--floating\">([\s\S]*?)<\/div>/,
|
||||
);
|
||||
expect(fieldMatch).not.toBeNull();
|
||||
const fieldInner = fieldMatch![1];
|
||||
|
||||
const inputIndex = fieldInner.indexOf("<input");
|
||||
const labelIndex = fieldInner.indexOf("<label");
|
||||
expect(inputIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(labelIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(inputIndex).toBeLessThan(labelIndex);
|
||||
});
|
||||
|
||||
it("formSelect 라벨 레이아웃(inline)이 Export HTML 에도 반영되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_select_export_1",
|
||||
type: "form",
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
fields: [],
|
||||
fieldIds: ["select_plan"],
|
||||
submitButtonId: null,
|
||||
formWidthMode: "full",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "select_plan",
|
||||
type: "formSelect",
|
||||
props: {
|
||||
label: "플랜",
|
||||
formFieldName: "plan",
|
||||
options: [
|
||||
{ label: "Basic", value: "basic" },
|
||||
{ label: "Pro", value: "pro" },
|
||||
],
|
||||
labelDisplay: "visible",
|
||||
labelLayout: "inline",
|
||||
labelGapPx: 16,
|
||||
} as any,
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "셀렉트 라벨/레이아웃 Export 테스트",
|
||||
slug: "form-select-layout-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
|
||||
const indexEntry = zip.file("index.html");
|
||||
expect(indexEntry).toBeTruthy();
|
||||
|
||||
const html = await indexEntry!.async("string");
|
||||
|
||||
// 라벨 표시 방식 visible + inline 레이아웃: pb-form-label 이 정상적으로 노출되어야 한다.
|
||||
expect(html).toMatch(/<label class=\"pb-form-label\"[^>]*>플랜<\/label>/);
|
||||
|
||||
// inline 레이아웃: pb-form-field 컨테이너에 flex-direction:row / align-items:center / column-gap 이 style 로 반영되어야 한다.
|
||||
// labelGapPx: 16px -> 1em
|
||||
const divMatch = html.match(/<div class=\"pb-form-field\"[^>]*style=\"([^\"]*)\"[^>]*>/);
|
||||
expect(divMatch).not.toBeNull();
|
||||
const styleAttr = divMatch![1];
|
||||
expect(styleAttr).toContain("flex-direction:row");
|
||||
expect(styleAttr).toContain("align-items:center");
|
||||
expect(styleAttr).toContain("column-gap:1em");
|
||||
});
|
||||
|
||||
it("formInput 라벨 표시 방식 hidden/floating 이 Export HTML 에도 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_input_hidden_export",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "숨김 라벨",
|
||||
formFieldName: "hidden_label",
|
||||
labelDisplay: "hidden",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "form_input_floating_export",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "플로팅 라벨",
|
||||
formFieldName: "floating_label",
|
||||
labelDisplay: "floating",
|
||||
placeholder: "플로팅 플레이스홀더",
|
||||
paddingY: 16,
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "formInput 라벨 표시 방식 Export 테스트",
|
||||
slug: "form-input-label-display-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const html = buildStaticHtml(blocks, projectConfig);
|
||||
|
||||
// hidden: pb-form-label sr-only 로 렌더되어야 한다.
|
||||
expect(html).toMatch(
|
||||
/<div class="pb-form-field">\s*<input class="pb-input"[^>]*name="hidden_label"[^>]*>\s*<label class="pb-form-label sr-only"[^>]*>숨김 라벨<\/label>\s*<\/div>/,
|
||||
);
|
||||
|
||||
// floating: pb-form-field pb-form-field--floating + placeholder=" " + pb-form-label 구조를 사용해야 한다.
|
||||
expect(html).toMatch(
|
||||
/<div class="pb-form-field pb-form-field--floating"[^>]*>\s*<input class="pb-input"[^>]*name="floating_label"[^>]*placeholder=" "[^>]*>\s*<label class="pb-form-label"[^>]*>플로팅 라벨<\/label>\s*<\/div>/,
|
||||
);
|
||||
|
||||
const floatingDivMatch = html.match(
|
||||
/<div class="pb-form-field pb-form-field--floating"([^>]*)>/,
|
||||
);
|
||||
expect(floatingDivMatch).not.toBeNull();
|
||||
expect(floatingDivMatch![1]).toContain('style="--pb-input-padding-y:1rem"');
|
||||
});
|
||||
|
||||
it("formInput Export HTML 은 label 과 input 을 for/id 로 연결해야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_input_hidden_export_for_id",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "숨김 라벨",
|
||||
formFieldName: "hidden_label_for_id",
|
||||
labelDisplay: "hidden",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "form_input_floating_export_for_id",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "플로팅 라벨",
|
||||
formFieldName: "floating_label_for_id",
|
||||
labelDisplay: "floating",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "formInput for/id Export 테스트",
|
||||
slug: "form-input-for-id-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const html = buildStaticHtml(blocks, projectConfig);
|
||||
|
||||
expect(html).toMatch(
|
||||
/<input[^>]*name="hidden_label_for_id"[^>]*id="hidden_label_for_id"[^>]*>/,
|
||||
);
|
||||
expect(html).toMatch(
|
||||
/<label[^>]*for="hidden_label_for_id"[^>]*>숨김 라벨<\/label>/,
|
||||
);
|
||||
|
||||
expect(html).toMatch(
|
||||
/<input[^>]*name="floating_label_for_id"[^>]*id="floating_label_for_id"[^>]*>/,
|
||||
);
|
||||
expect(html).toMatch(
|
||||
/<label[^>]*for="floating_label_for_id"[^>]*>플로팅 라벨<\/label>/,
|
||||
);
|
||||
});
|
||||
|
||||
it("formSelect Export HTML 은 name 과 option value 를 정확히 반영해야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "select_export_attrs",
|
||||
type: "formSelect",
|
||||
props: {
|
||||
label: "플랜",
|
||||
formFieldName: "plan",
|
||||
options: [
|
||||
{ label: "Basic", value: "basic" },
|
||||
{ label: "Pro", value: "pro" },
|
||||
],
|
||||
required: true,
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "formSelect Export 속성 테스트",
|
||||
slug: "form-select-attrs-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const html = buildStaticHtml(blocks, projectConfig);
|
||||
|
||||
expect(html).toMatch(/<select[^>]*class="pb-select"[^>]*name="plan"[^>]*required[^>]*>/);
|
||||
expect(html).toContain('<option value="basic">Basic<\/option>'.replace("\\/", "/"));
|
||||
expect(html).toContain('<option value="pro">Pro<\/option>'.replace("\\/", "/"));
|
||||
});
|
||||
|
||||
it("formCheckbox/formRadio Export HTML 의 input 은 type/name/value 를 정확히 반영해야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "features_export_attrs",
|
||||
type: "formCheckbox",
|
||||
props: {
|
||||
groupLabel: "기능",
|
||||
formFieldName: "features",
|
||||
options: [
|
||||
{ label: "옵션 1", value: "opt1" },
|
||||
{ label: "옵션 2", value: "opt2" },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
id: "choices_export_attrs",
|
||||
type: "formRadio",
|
||||
props: {
|
||||
groupLabel: "선택",
|
||||
formFieldName: "choice",
|
||||
options: [
|
||||
{ label: "A", value: "a" },
|
||||
{ label: "B", value: "b" },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "formCheckbox/formRadio Export 속성 테스트",
|
||||
slug: "form-option-attrs-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const html = buildStaticHtml(blocks, projectConfig);
|
||||
|
||||
// 체크박스 옵션 input: type="checkbox" name="features" value="opt1"/"opt2"
|
||||
expect(html).toMatch(/<input type="checkbox"[^>]*name="features"[^>]*value="opt1"[^>]*>/);
|
||||
expect(html).toMatch(/<input type="checkbox"[^>]*name="features"[^>]*value="opt2"[^>]*>/);
|
||||
|
||||
// 라디오 옵션 input: type="radio" name="choice" value="a"/"b"
|
||||
expect(html).toMatch(/<input type="radio"[^>]*name="choice"[^>]*value="a"[^>]*>/);
|
||||
expect(html).toMatch(/<input type="radio"[^>]*name="choice"[^>]*value="b"[^>]*>/);
|
||||
});
|
||||
|
||||
it("FormBlock.requiredFieldIds 는 Export HTML input/select 의 required 속성에도 반영되어야 한다", () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_required_export",
|
||||
type: "form",
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
fieldIds: ["field_name", "field_plan"],
|
||||
requiredFieldIds: ["field_name", "field_plan"],
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
id: "field_name",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이름",
|
||||
formFieldName: "name",
|
||||
required: false,
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
id: "field_plan",
|
||||
type: "formRadio",
|
||||
props: {
|
||||
groupLabel: "플랜",
|
||||
formFieldName: "plan",
|
||||
options: [
|
||||
{ label: "Basic", value: "basic" },
|
||||
{ label: "Pro", value: "pro" },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "FormBlock required Export 테스트",
|
||||
slug: "form-required-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const html = buildStaticHtml(blocks, projectConfig);
|
||||
|
||||
// field_name 은 FormBlock.requiredFieldIds 로 인해 required 이어야 한다.
|
||||
expect(html).toMatch(/<input[^>]*name="name"[^>]*required[^>]*>/);
|
||||
|
||||
// 라디오 그룹에서는 첫 번째 옵션만 required 여야 한다.
|
||||
const firstRadioMatch = html.match(/<input type="radio"[^>]*name="plan"[^>]*value="basic"[^>]*>/);
|
||||
const secondRadioMatch = html.match(/<input type="radio"[^>]*name="plan"[^>]*value="pro"[^>]*>/);
|
||||
|
||||
expect(firstRadioMatch).not.toBeNull();
|
||||
expect(firstRadioMatch![0]).toContain("required");
|
||||
expect(secondRadioMatch).not.toBeNull();
|
||||
expect(secondRadioMatch![0]).not.toContain("required");
|
||||
});
|
||||
|
||||
it("formCheckbox/formRadio 의 optionLayout 값이 Export HTML 의 pb-form-options 컨테이너 클래스로 반영되어야 한다", () => {
|
||||
const checkboxBlocks: Block[] = [
|
||||
{
|
||||
id: "features_export_options",
|
||||
type: "formCheckbox",
|
||||
props: {
|
||||
groupLabel: "기능",
|
||||
formFieldName: "features",
|
||||
options: [
|
||||
{ label: "옵션 1", value: "opt1" },
|
||||
{ label: "옵션 2", value: "opt2" },
|
||||
],
|
||||
optionLayout: "stacked",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const radioBlocks: Block[] = [
|
||||
{
|
||||
id: "choices_export_options",
|
||||
type: "formRadio",
|
||||
props: {
|
||||
groupLabel: "선택",
|
||||
formFieldName: "choice",
|
||||
options: [
|
||||
{ label: "A", value: "a" },
|
||||
{ label: "B", value: "b" },
|
||||
],
|
||||
optionLayout: "inline",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "옵션 레이아웃 Export 테스트",
|
||||
slug: "form-option-layout-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const checkboxHtml = buildStaticHtml(checkboxBlocks, projectConfig);
|
||||
expect(checkboxHtml).toContain('class="pb-form-options pb-form-options--stacked"');
|
||||
|
||||
const radioHtml = buildStaticHtml(radioBlocks, projectConfig);
|
||||
expect(radioHtml).toContain('class="pb-form-options pb-form-options--inline"');
|
||||
// 각 옵션 라벨은 pb-form-option 클래스를 사용해야 한다.
|
||||
expect(radioHtml).toContain('class="pb-form-option"');
|
||||
});
|
||||
|
||||
it("formCheckbox/formRadio 그룹 타이틀 표시 방식과 레이아웃이 Export HTML 에도 반영되어야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "form_group_export_1",
|
||||
type: "form",
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
fields: [],
|
||||
fieldIds: ["features", "choices"],
|
||||
submitButtonId: null,
|
||||
formWidthMode: "full",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "features",
|
||||
type: "formCheckbox",
|
||||
props: {
|
||||
groupLabel: "기능",
|
||||
formFieldName: "features",
|
||||
options: [
|
||||
{ label: "옵션 1", value: "opt1" },
|
||||
{ label: "옵션 2", value: "opt2" },
|
||||
],
|
||||
groupLabelDisplay: "hidden",
|
||||
labelLayout: "inline",
|
||||
labelGapPx: 8,
|
||||
} as any,
|
||||
} as any,
|
||||
{
|
||||
id: "choices",
|
||||
type: "formRadio",
|
||||
props: {
|
||||
groupLabel: "선택",
|
||||
formFieldName: "choice",
|
||||
options: [
|
||||
{ label: "A", value: "a" },
|
||||
{ label: "B", value: "b" },
|
||||
],
|
||||
groupLabelDisplay: "visible",
|
||||
labelLayout: "inline",
|
||||
labelGapPx: 24,
|
||||
} as any,
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "체크박스/라디오 그룹 라벨 Export 테스트",
|
||||
slug: "form-group-layout-export-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const payload = { blocks, projectConfig };
|
||||
|
||||
const { POST: handleExport } = await import("@/app/api/export/route");
|
||||
|
||||
const res = await handleExport(
|
||||
new Request(`${BASE_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
|
||||
const indexEntry = zip.file("index.html");
|
||||
expect(indexEntry).toBeTruthy();
|
||||
|
||||
const html = await indexEntry!.async("string");
|
||||
|
||||
// 체크박스 그룹: groupLabelDisplay hidden -> pb-form-label sr-only 여야 한다.
|
||||
expect(html).toContain('<label class="pb-form-label sr-only"');
|
||||
|
||||
// 라디오 그룹: inline 레이아웃 + labelGapPx 24px -> column-gap:1.5em, 세로 중앙 정렬이 pb-form-field style 에 반영되어야 한다.
|
||||
const radioDivMatch = html.match(/<div class=\"pb-form-field\"[^>]*style=\"([^\"]*)\"[^>]*>[^]*선택[^]*<label class=\"pb-form-option/);
|
||||
expect(radioDivMatch).not.toBeNull();
|
||||
const radioStyle = radioDivMatch![1];
|
||||
expect(radioStyle).toContain("flex-direction:row");
|
||||
expect(radioStyle).toContain("align-items:center");
|
||||
expect(radioStyle).toContain("column-gap:1.5em");
|
||||
});
|
||||
|
||||
|
||||
it("FormBlock 에 fieldIds/fields 가 모두 없으면 Export 에서 아무 <form>/필드도 생성하지 않아야 한다", async () => {
|
||||
const blocks: Block[] = [
|
||||
@@ -2231,6 +2745,9 @@ describe("/api/export", () => {
|
||||
fillColorCustom: "#123456",
|
||||
strokeColorCustom: "#ff0000",
|
||||
borderRadius: "lg",
|
||||
fontSizeCustom: "24px",
|
||||
lineHeightCustom: "30px",
|
||||
letterSpacingCustom: "2px",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
@@ -2271,6 +2788,9 @@ describe("/api/export", () => {
|
||||
expect(html).toContain("background-color:#123456");
|
||||
expect(html).toContain("border-color:#ff0000");
|
||||
expect(html).toContain("border-radius:6px");
|
||||
expect(html).toContain("font-size:1.5em");
|
||||
expect(html).toContain("line-height:1.875em");
|
||||
expect(html).toContain("letter-spacing:0.125em");
|
||||
});
|
||||
|
||||
it("formSelect 블록은 스타일 속성으로 셀렉트 배경/보더/패딩/너비/둥글기를 반영해야 한다", async () => {
|
||||
|
||||
@@ -16,14 +16,29 @@ vi.mock("@prisma/client", () => {
|
||||
};
|
||||
|
||||
formSubmission = {
|
||||
findMany: async ({ where: { projectId }, orderBy, take }: any) => {
|
||||
let items = inMemoryFormSubmissions.filter((s) => s.projectId === projectId);
|
||||
findMany: async (args: any = {}) => {
|
||||
const where = args.where ?? {};
|
||||
const orderBy = args.orderBy;
|
||||
const take = args.take;
|
||||
|
||||
let items = [...inMemoryFormSubmissions];
|
||||
|
||||
if (where.projectId) {
|
||||
items = items.filter((s) => s.projectId === where.projectId);
|
||||
}
|
||||
|
||||
if (where.userId) {
|
||||
items = items.filter((s) => s.userId === where.userId);
|
||||
}
|
||||
|
||||
if (orderBy?.createdAt === "desc") {
|
||||
items = items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
if (typeof take === "number") {
|
||||
items = items.slice(0, take);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
};
|
||||
@@ -169,3 +184,113 @@ describe("/api/projects/[slug]/submissions", () => {
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/api/projects/submissions", () => {
|
||||
it("로그인한 사용자가 자신의 모든 프로젝트 제출 내역을 조회할 수 있어야 한다", async () => {
|
||||
const project1 = {
|
||||
id: "proj-1",
|
||||
slug: "proj-1-slug",
|
||||
userId: TEST_USER.id,
|
||||
};
|
||||
const project2 = {
|
||||
id: "proj-2",
|
||||
slug: "proj-2-slug",
|
||||
userId: OTHER_USER.id,
|
||||
};
|
||||
inMemoryProjects.push(project1, project2);
|
||||
|
||||
const createdAt = new Date();
|
||||
inMemoryFormSubmissions.push(
|
||||
{
|
||||
id: "sub-1",
|
||||
projectId: project1.id,
|
||||
projectSlug: project1.slug,
|
||||
userId: TEST_USER.id,
|
||||
createdAt,
|
||||
payloadJson: { name: "홍길동" },
|
||||
sensitiveEnc: undefined,
|
||||
metaJson: null,
|
||||
},
|
||||
{
|
||||
id: "sub-2",
|
||||
projectId: project2.id,
|
||||
projectSlug: project2.slug,
|
||||
userId: OTHER_USER.id,
|
||||
createdAt,
|
||||
payloadJson: { name: "다른 유저" },
|
||||
sensitiveEnc: undefined,
|
||||
metaJson: null,
|
||||
},
|
||||
);
|
||||
|
||||
const { GET: getAllSubmissions } = await import("@/app/api/projects/submissions/route");
|
||||
|
||||
const headers = await buildAuthHeaders();
|
||||
|
||||
const res = await getAllSubmissions(
|
||||
new Request(`${BASE_URL}/api/projects/submissions`, {
|
||||
headers,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as any[];
|
||||
expect(Array.isArray(json)).toBe(true);
|
||||
expect(json.length).toBe(1);
|
||||
expect(json[0].id).toBe("sub-1");
|
||||
expect(json[0].projectSlug).toBe(project1.slug);
|
||||
expect(json[0].payload.name).toBe("홍길동");
|
||||
});
|
||||
|
||||
it("민감정보는 전체 조회에서도 복호화된 값으로 payload 에 합쳐져야 한다", async () => {
|
||||
const project = {
|
||||
id: "proj-sensitive",
|
||||
slug: "proj-sensitive",
|
||||
userId: TEST_USER.id,
|
||||
};
|
||||
inMemoryProjects.push(project);
|
||||
|
||||
const { encryptJson } = await import("@/features/auth/authCrypto");
|
||||
const sensitive = { email: "all@example.com", phone: "010-9999-8888" };
|
||||
const sensitiveEnc = await encryptJson(sensitive);
|
||||
|
||||
const createdAt = new Date();
|
||||
inMemoryFormSubmissions.push({
|
||||
id: "sub-sensitive",
|
||||
projectId: project.id,
|
||||
projectSlug: project.slug,
|
||||
userId: TEST_USER.id,
|
||||
createdAt,
|
||||
payloadJson: { name: "전체조회", message: "테스트" },
|
||||
sensitiveEnc,
|
||||
metaJson: null,
|
||||
});
|
||||
|
||||
const { GET: getAllSubmissions } = await import("@/app/api/projects/submissions/route");
|
||||
const headers = await buildAuthHeaders();
|
||||
|
||||
const res = await getAllSubmissions(
|
||||
new Request(`${BASE_URL}/api/projects/submissions`, {
|
||||
headers,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as any[];
|
||||
expect(json.length).toBe(1);
|
||||
expect(json[0].payload.name).toBe("전체조회");
|
||||
expect(json[0].payload.message).toBe("테스트");
|
||||
expect(json[0].payload.email).toBe("all@example.com");
|
||||
expect(json[0].payload.phone).toBe("010-9999-8888");
|
||||
});
|
||||
|
||||
it("로그인하지 않은 경우 401 을 반환해야 한다", async () => {
|
||||
const { GET: getAllSubmissions } = await import("@/app/api/projects/submissions/route");
|
||||
|
||||
const res = await getAllSubmissions(
|
||||
new Request(`${BASE_URL}/api/projects/submissions`),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user