@simplysm/sd-cli 13.0.70 → 13.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.d.ts +4 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/sd-cli-entry.d.ts.map +1 -1
- package/dist/sd-cli-entry.js +0 -20
- package/dist/sd-cli-entry.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/init.ts +40 -21
- package/src/sd-cli-entry.ts +0 -24
- package/templates/init/{.prettierrc.yaml.hbs → .prettierrc.yaml} +1 -1
- package/templates/init/eslint.config.ts +15 -0
- package/templates/init/mise.toml +3 -0
- package/templates/init/package.json.hbs +8 -7
- package/templates/init/packages/client-admin/index.html.hbs +144 -0
- package/templates/init/packages/client-admin/package.json.hbs +26 -0
- package/templates/init/packages/client-admin/public/assets/logo-landscape.png +0 -0
- package/templates/init/packages/client-admin/public/assets/logo.png +0 -0
- package/templates/init/packages/client-admin/src/App.tsx +42 -0
- package/templates/init/packages/client-admin/src/dev/DevDialog.tsx +34 -0
- package/templates/{add-client/__CLIENT__/src/main.css.hbs → init/packages/client-admin/src/main.css} +1 -1
- package/templates/init/packages/client-admin/src/main.tsx.hbs +146 -0
- package/templates/init/packages/client-admin/src/providers/AppServiceProvider.tsx.hbs +103 -0
- package/templates/init/packages/client-admin/src/providers/AppStructureProvider.tsx +84 -0
- package/templates/init/packages/client-admin/src/providers/AuthProvider.tsx.hbs +71 -0
- package/templates/init/packages/client-admin/src/providers/configureSharedData.ts.hbs +67 -0
- package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +132 -0
- package/templates/init/packages/client-admin/src/views/home/HomeView.tsx +108 -0
- package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +262 -0
- package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +271 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +154 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +123 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +52 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +125 -0
- package/templates/init/packages/client-admin/src/views/home/main/MainView.tsx.hbs +13 -0
- package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +248 -0
- package/templates/init/packages/client-admin/src/views/home/system/system-log/SystemLogSheet.tsx.hbs +169 -0
- package/templates/init/packages/client-admin/src/views/not-found/NotFoundView.tsx +15 -0
- package/templates/init/packages/client-admin/tailwind.config.ts +10 -0
- package/templates/init/packages/db-main/package.json.hbs +13 -0
- package/templates/init/packages/db-main/src/MainDbContext.ts +20 -0
- package/templates/init/packages/db-main/src/dataLogExt.ts +127 -0
- package/templates/init/packages/db-main/src/index.ts +10 -0
- package/templates/init/packages/db-main/src/tables/Employee.ts +24 -0
- package/templates/init/packages/db-main/src/tables/EmployeeConfig.ts +13 -0
- package/templates/init/packages/db-main/src/tables/Role.ts +9 -0
- package/templates/init/packages/db-main/src/tables/RolePermission.ts +13 -0
- package/templates/init/packages/db-main/src/tables/_DataLog.ts +19 -0
- package/templates/init/packages/db-main/src/tables/_Log.ts +16 -0
- package/templates/init/packages/server/package.json.hbs +20 -0
- package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
- package/templates/init/packages/server/src/index.ts +4 -0
- package/templates/init/packages/server/src/main.ts.hbs +34 -0
- package/templates/init/packages/server/src/services/AuthService.ts.hbs +171 -0
- package/templates/init/packages/server/src/services/DevService.ts.hbs +94 -0
- package/templates/init/packages/server/src/services/EmployeeService.ts.hbs +122 -0
- package/templates/init/packages/server/src/services/RoleService.ts.hbs +59 -0
- package/templates/init/{pnpm-workspace.yaml.hbs → pnpm-workspace.yaml} +3 -1
- package/templates/init/sd.config.ts.hbs +30 -1
- package/templates/init/tests/e2e/package.json.hbs +16 -0
- package/templates/init/tests/e2e/src/e2e.spec.ts +36 -0
- package/templates/init/tests/e2e/src/employee-crud.ts +204 -0
- package/templates/init/tests/e2e/src/login.ts +61 -0
- package/templates/init/tests/e2e/vitest.setup.ts.hbs +220 -0
- package/templates/init/tsconfig.json.hbs +0 -11
- package/templates/init/{vitest.config.ts.hbs → vitest.config.ts} +16 -12
- package/tests/infra/WorkerManager.spec.ts +1 -1
- package/tests/replace-deps.spec.ts +2 -2
- package/tests/run-lint.spec.ts +6 -6
- package/tests/run-typecheck.spec.ts +3 -3
- package/tests/sd-cli.spec.ts +1 -1
- package/dist/commands/add-client.d.ts +0 -18
- package/dist/commands/add-client.d.ts.map +0 -1
- package/dist/commands/add-client.js +0 -79
- package/dist/commands/add-client.js.map +0 -6
- package/dist/commands/add-server.d.ts +0 -18
- package/dist/commands/add-server.d.ts.map +0 -1
- package/dist/commands/add-server.js +0 -83
- package/dist/commands/add-server.js.map +0 -6
- package/dist/utils/config-editor.d.ts +0 -17
- package/dist/utils/config-editor.d.ts.map +0 -1
- package/dist/utils/config-editor.js +0 -79
- package/dist/utils/config-editor.js.map +0 -6
- package/src/commands/add-client.ts +0 -126
- package/src/commands/add-server.ts +0 -138
- package/src/utils/config-editor.ts +0 -141
- package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
- package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
- package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
- package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
- package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
- package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
- package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
- package/templates/add-server/__SERVER__/package.json.hbs +0 -10
- package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
- package/templates/init/.gitignore.hbs +0 -26
- package/templates/init/.npmrc.hbs +0 -1
- package/templates/init/eslint.config.ts.hbs +0 -5
- package/templates/init/mise.toml.hbs +0 -3
- package/tests/config-editor.spec.ts +0 -160
- /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
- /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
- /package/templates/init/{stylelint.config.ts.hbs → stylelint.config.ts} +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import type { Page } from "playwright";
|
|
3
|
+
|
|
4
|
+
export function employeeCrudTests(ctx: { page: Page; baseUrl: string }) {
|
|
5
|
+
describe("EmployeeSheet CRUD", () => {
|
|
6
|
+
const sheetSelector = '[data-sheet="employee-page-sheet"]';
|
|
7
|
+
|
|
8
|
+
function sheetRows() {
|
|
9
|
+
return ctx.page.locator(`${sheetSelector} tbody tr`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function dialogLocator() {
|
|
13
|
+
return ctx.page.locator("[data-modal-dialog]").last();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function waitForSheetLoaded() {
|
|
17
|
+
await sheetRows().first().waitFor({ timeout: 1000 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function waitForBusyDone() {
|
|
21
|
+
await ctx.page.waitForTimeout(50);
|
|
22
|
+
await ctx.page.waitForFunction(() => !document.querySelector(".animate-spin"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function waitForDialogClosed() {
|
|
26
|
+
await ctx.page.waitForFunction(
|
|
27
|
+
() =>
|
|
28
|
+
document.querySelectorAll("[data-modal-dialog]").length === 0 &&
|
|
29
|
+
document.querySelectorAll("[data-modal-backdrop]").length === 0,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function assertNotification(text: string) {
|
|
34
|
+
const alert = ctx.page
|
|
35
|
+
.locator("[data-notification-banner]")
|
|
36
|
+
.filter({ hasText: text })
|
|
37
|
+
.first();
|
|
38
|
+
await alert.waitFor();
|
|
39
|
+
const closeButtons = ctx.page.locator(
|
|
40
|
+
'[data-notification-banner] button[aria-label="알림 닫기"]',
|
|
41
|
+
);
|
|
42
|
+
const count = await closeButtons.count();
|
|
43
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
44
|
+
await closeButtons
|
|
45
|
+
.nth(i)
|
|
46
|
+
.click()
|
|
47
|
+
.catch(() => {});
|
|
48
|
+
}
|
|
49
|
+
await ctx.page.waitForFunction(
|
|
50
|
+
() => document.querySelectorAll("[data-notification-banner]").length === 0,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function clickSearch() {
|
|
55
|
+
await ctx.page.getByRole("button", { name: "조회" }).click();
|
|
56
|
+
await waitForBusyDone();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function clickAdd() {
|
|
60
|
+
await ctx.page.getByRole("button", { name: /등록/ }).click();
|
|
61
|
+
await dialogLocator().waitFor();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function clickEditRow(rowIndex: number) {
|
|
65
|
+
await sheetRows().nth(rowIndex).locator("a").first().click();
|
|
66
|
+
await dialogLocator().waitFor();
|
|
67
|
+
await waitForBusyDone();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fillDialogField(label: string, value: string) {
|
|
71
|
+
const dlg = dialogLocator();
|
|
72
|
+
const th = dlg.locator("th").filter({ hasText: label });
|
|
73
|
+
const td = th.locator("xpath=following-sibling::td[1]");
|
|
74
|
+
const field = td.locator("input:not([aria-hidden])").first();
|
|
75
|
+
await field.fill(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function clickDialogSubmit() {
|
|
79
|
+
await dialogLocator().getByRole("button", { name: "확인" }).click();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function closeDialog() {
|
|
83
|
+
await ctx.page.keyboard.press("Escape");
|
|
84
|
+
await waitForDialogClosed();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
beforeAll(async () => {
|
|
88
|
+
await ctx.page.goto(`${ctx.baseUrl}/#/home/base/employee`);
|
|
89
|
+
await waitForSheetLoaded();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Create", () => {
|
|
93
|
+
it("새 직원 등록", async () => {
|
|
94
|
+
await clickAdd();
|
|
95
|
+
|
|
96
|
+
await fillDialogField("이름", "E2E테스트유저");
|
|
97
|
+
await fillDialogField("이메일", "e2e@test.com");
|
|
98
|
+
|
|
99
|
+
await clickDialogSubmit();
|
|
100
|
+
await assertNotification("저장되었습니다");
|
|
101
|
+
await waitForDialogClosed();
|
|
102
|
+
await clickSearch();
|
|
103
|
+
|
|
104
|
+
const firstCell = sheetRows().first().locator("td").nth(1);
|
|
105
|
+
await expect(firstCell.textContent()).resolves.toContain("E2E테스트유저");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("Read / Filter", () => {
|
|
110
|
+
it("검색어 필터링", async () => {
|
|
111
|
+
const searchInput = ctx.page.locator("form").first().locator("input").first();
|
|
112
|
+
await searchInput.fill("E2E테스트");
|
|
113
|
+
await clickSearch();
|
|
114
|
+
|
|
115
|
+
const rowCount = await sheetRows().count();
|
|
116
|
+
expect(rowCount).toBe(1);
|
|
117
|
+
await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
|
|
118
|
+
"E2E테스트유저",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await searchInput.fill("");
|
|
122
|
+
await clickSearch();
|
|
123
|
+
const allRowCount = await sheetRows().count();
|
|
124
|
+
expect(allRowCount).toBe(2);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Update", () => {
|
|
129
|
+
it("기존 유저 이름 수정", async () => {
|
|
130
|
+
await clickEditRow(0);
|
|
131
|
+
|
|
132
|
+
await fillDialogField("이름", "E2E수정유저");
|
|
133
|
+
|
|
134
|
+
await clickDialogSubmit();
|
|
135
|
+
await assertNotification("저장되었습니다");
|
|
136
|
+
await waitForDialogClosed();
|
|
137
|
+
await clickSearch();
|
|
138
|
+
|
|
139
|
+
await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
|
|
140
|
+
"E2E수정유저",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("Delete", () => {
|
|
146
|
+
it("유저 soft delete", async () => {
|
|
147
|
+
await clickEditRow(0);
|
|
148
|
+
|
|
149
|
+
await dialogLocator().getByRole("button", { name: "삭제" }).click();
|
|
150
|
+
await assertNotification("삭제되었습니다");
|
|
151
|
+
await waitForDialogClosed();
|
|
152
|
+
await clickSearch();
|
|
153
|
+
|
|
154
|
+
const rowCount = await sheetRows().count();
|
|
155
|
+
expect(rowCount).toBe(1);
|
|
156
|
+
await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
|
|
157
|
+
"테스트",
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("삭제항목 포함 필터로 삭제된 유저 확인", async () => {
|
|
162
|
+
await ctx.page.getByText("삭제항목 포함").click();
|
|
163
|
+
await clickSearch();
|
|
164
|
+
|
|
165
|
+
const rowCount = await sheetRows().count();
|
|
166
|
+
expect(rowCount).toBe(2);
|
|
167
|
+
|
|
168
|
+
await ctx.page.getByText("삭제항목 포함").click();
|
|
169
|
+
await clickSearch();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("Error cases", () => {
|
|
174
|
+
it("이름 중복 에러", async () => {
|
|
175
|
+
await clickAdd();
|
|
176
|
+
await fillDialogField("이름", "테스트");
|
|
177
|
+
await clickDialogSubmit();
|
|
178
|
+
await assertNotification("동일한 이름이 이미 등록되어 있습니다");
|
|
179
|
+
|
|
180
|
+
await closeDialog();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("이메일 중복 에러", async () => {
|
|
184
|
+
await clickAdd();
|
|
185
|
+
await fillDialogField("이름", "고유이름");
|
|
186
|
+
await fillDialogField("이메일", "admin@test.com");
|
|
187
|
+
await clickDialogSubmit();
|
|
188
|
+
await assertNotification("동일한 이메일이 이미 등록되어 있습니다");
|
|
189
|
+
|
|
190
|
+
await closeDialog();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("자기 자신 삭제 불가", async () => {
|
|
194
|
+
await clickEditRow(0);
|
|
195
|
+
await waitForBusyDone();
|
|
196
|
+
|
|
197
|
+
const deleteBtn = dialogLocator().getByRole("button", { name: "삭제" });
|
|
198
|
+
expect(await deleteBtn.count()).toBe(0);
|
|
199
|
+
|
|
200
|
+
await closeDialog();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Page } from "playwright";
|
|
3
|
+
|
|
4
|
+
export function loginTests(ctx: { page: Page; baseUrl: string }) {
|
|
5
|
+
describe("Login", () => {
|
|
6
|
+
it("/ 접속 시 /login으로 리다이렉트", async () => {
|
|
7
|
+
await ctx.page.goto(`${ctx.baseUrl}/`, { timeout: 5000 });
|
|
8
|
+
await ctx.page.waitForURL(`${ctx.baseUrl}/#/login`);
|
|
9
|
+
await expect(
|
|
10
|
+
ctx.page.getByRole("button", { name: "로그인" }).textContent(),
|
|
11
|
+
).resolves.toContain("로그인");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("토큰 없이 /home/main 접근 시 /login으로 리다이렉트", async () => {
|
|
15
|
+
await ctx.page.goto(`${ctx.baseUrl}/#/home/main`);
|
|
16
|
+
await ctx.page.waitForURL(`${ctx.baseUrl}/#/login`);
|
|
17
|
+
await expect(
|
|
18
|
+
ctx.page.getByRole("button", { name: "로그인" }).textContent(),
|
|
19
|
+
).resolves.toContain("로그인");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("틀린 비밀번호 시 에러 메시지", async () => {
|
|
23
|
+
await ctx.page.getByPlaceholder("이메일을 입력하세요").fill("admin@test.com");
|
|
24
|
+
await ctx.page.getByPlaceholder("비밀번호를 입력하세요").fill("wrongpassword");
|
|
25
|
+
await ctx.page.getByRole("button", { name: "로그인" }).click();
|
|
26
|
+
|
|
27
|
+
const notification = ctx.page.getByRole("alert");
|
|
28
|
+
await notification.waitFor({ timeout: 1000 });
|
|
29
|
+
await expect(notification.textContent()).resolves.toContain(
|
|
30
|
+
"이메일 또는 비밀번호가 올바르지 않습니다",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("올바른 자격 증명으로 로그인 성공", async () => {
|
|
35
|
+
await ctx.page.goto(`${ctx.baseUrl}/#/login`);
|
|
36
|
+
await ctx.page.getByRole("button", { name: "로그인" }).waitFor({ timeout: 2000 });
|
|
37
|
+
|
|
38
|
+
await ctx.page.getByPlaceholder("이메일을 입력하세요").fill("admin@test.com");
|
|
39
|
+
await ctx.page.getByPlaceholder("비밀번호를 입력하세요").fill("test1234");
|
|
40
|
+
await ctx.page.getByRole("button", { name: "로그인" }).click({ timeout: 2000 });
|
|
41
|
+
|
|
42
|
+
await ctx.page.waitForURL(`${ctx.baseUrl}/#/home/main`, { timeout: 2000 });
|
|
43
|
+
const mainContent = ctx.page.locator("main").last();
|
|
44
|
+
await expect(mainContent.locator("h1").textContent()).resolves.toContain("테스트");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("마지막 로그인 이메일 기억", async () => {
|
|
48
|
+
const lastLoginEmail = await ctx.page.evaluate(() =>
|
|
49
|
+
JSON.parse(localStorage.getItem("client-admin.last-login-email") ?? "null"),
|
|
50
|
+
);
|
|
51
|
+
expect(lastLoginEmail).toBe("admin@test.com");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("페이지 새로고침 시 자동 로그인", async () => {
|
|
55
|
+
await ctx.page.reload();
|
|
56
|
+
await ctx.page.waitForURL(`${ctx.baseUrl}/#/home/main`, { timeout: 2000 });
|
|
57
|
+
const mainContent = ctx.page.locator("main").last();
|
|
58
|
+
await expect(mainContent.locator("h1").textContent()).resolves.toContain("테스트");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { type ChildProcess, spawn, execSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { createDbConn, createOrm } from "@simplysm/orm-node";
|
|
5
|
+
import { MainDbContext } from "@{{projectName}}/db-main";
|
|
6
|
+
import bcrypt from "bcrypt";
|
|
7
|
+
import { chromium } from "playwright";
|
|
8
|
+
import type { TestProject } from "vitest/node";
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const rootDir = path.resolve(__dirname, "../..");
|
|
12
|
+
|
|
13
|
+
const DB_CONFIG = {
|
|
14
|
+
dialect: "postgresql" as const,
|
|
15
|
+
host: "localhost",
|
|
16
|
+
port: 5432,
|
|
17
|
+
username: "postgres",
|
|
18
|
+
password: "1234",
|
|
19
|
+
database: "{{projectName}}-test",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const TEST_USER = {
|
|
23
|
+
name: "테스트",
|
|
24
|
+
email: "admin@test.com",
|
|
25
|
+
password: "test1234",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let devServerProcess: ChildProcess | undefined;
|
|
29
|
+
|
|
30
|
+
function killProcessTree(proc: ChildProcess): void {
|
|
31
|
+
if (proc.pid == null) return;
|
|
32
|
+
try {
|
|
33
|
+
if (process.platform === "win32") {
|
|
34
|
+
execSync(`taskkill /T /F /PID ${proc.pid}`, { stdio: "ignore" });
|
|
35
|
+
} else {
|
|
36
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
/* 무시 */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stripAnsi(str: string): string {
|
|
44
|
+
return str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function waitForServerUrl(process: ChildProcess, timeout: number): Promise<string> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let settled = false;
|
|
50
|
+
let output = "";
|
|
51
|
+
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
if (!settled) {
|
|
54
|
+
settled = true;
|
|
55
|
+
reject(
|
|
56
|
+
new Error(`dev 서버 URL 대기 시간 초과 (${timeout}ms)\n출력:\n${output.slice(-2000)}`),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}, timeout);
|
|
60
|
+
|
|
61
|
+
function onData(data: Buffer) {
|
|
62
|
+
const text = stripAnsi(data.toString());
|
|
63
|
+
console.log(text);
|
|
64
|
+
output += text;
|
|
65
|
+
|
|
66
|
+
const urlMatch = text.match(/http:\/\/localhost:\d+\/client-admin\/?/);
|
|
67
|
+
if (urlMatch && !settled) {
|
|
68
|
+
settled = true;
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
resolve(urlMatch[0].endsWith("/") ? urlMatch[0].slice(0, -1) : urlMatch[0]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.stdout?.on("data", onData);
|
|
75
|
+
process.stderr?.on("data", onData);
|
|
76
|
+
|
|
77
|
+
process.on("exit", (code) => {
|
|
78
|
+
if (!settled) {
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
reject(
|
|
82
|
+
new Error(`dev 서버가 비정상 종료됨 (code: ${code})\n출력:\n${output.slice(-2000)}`),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function warmup(baseUrl: string, timeout: number): Promise<void> {
|
|
90
|
+
const browser = await chromium.launch({ headless: true });
|
|
91
|
+
try {
|
|
92
|
+
const page = await browser.newPage();
|
|
93
|
+
await page.goto(baseUrl, { timeout });
|
|
94
|
+
|
|
95
|
+
const result = await Promise.race([
|
|
96
|
+
page
|
|
97
|
+
.locator(".app-loading")
|
|
98
|
+
.waitFor({ state: "detached", timeout })
|
|
99
|
+
.then(() => "ready" as const),
|
|
100
|
+
page
|
|
101
|
+
.locator("vite-error-overlay")
|
|
102
|
+
.waitFor({ state: "attached", timeout })
|
|
103
|
+
.then(async () => {
|
|
104
|
+
const errorText = await page
|
|
105
|
+
.locator("vite-error-overlay")
|
|
106
|
+
.evaluate((el) => el.shadowRoot?.textContent ?? "");
|
|
107
|
+
return `error:${errorText}` as const;
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
if (result.startsWith("error:")) {
|
|
112
|
+
throw new Error(`Vite 빌드 에러:\n${result.slice(6).slice(0, 2000)}`);
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await browser.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function setup(project: TestProject) {
|
|
120
|
+
// 1. 테스트 DB 생성 (없으면)
|
|
121
|
+
console.log("[e2e] 테스트 DB 확인...");
|
|
122
|
+
const adminConn = await createDbConn({ ...DB_CONFIG, database: "postgres" });
|
|
123
|
+
await adminConn.connect();
|
|
124
|
+
try {
|
|
125
|
+
const result = await adminConn.executeParametrized(
|
|
126
|
+
`SELECT 1 FROM pg_database WHERE datname = $1`,
|
|
127
|
+
[DB_CONFIG.database],
|
|
128
|
+
);
|
|
129
|
+
if (result[0].length === 0) {
|
|
130
|
+
await adminConn.execute([`CREATE DATABASE "${DB_CONFIG.database}"`]);
|
|
131
|
+
console.log(`[e2e] DB "${DB_CONFIG.database}" 생성됨.`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`[e2e] DB "${DB_CONFIG.database}" 이미 존재.`);
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
await adminConn.close();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. DB 초기화 및 시딩
|
|
140
|
+
const orm = createOrm(MainDbContext, DB_CONFIG);
|
|
141
|
+
await orm.connectWithoutTransaction(async (db) => {
|
|
142
|
+
await db.initialize({ force: true });
|
|
143
|
+
|
|
144
|
+
// 권한그룹 생성
|
|
145
|
+
await db.role().insert([{ name: "관리자" }]);
|
|
146
|
+
const role = (await db.role().first())!;
|
|
147
|
+
|
|
148
|
+
// 전체 권한 부여
|
|
149
|
+
await db.rolePermission().insert([
|
|
150
|
+
{ roleId: role.id, code: "/home/base/employee/use", valueJson: "true" },
|
|
151
|
+
{ roleId: role.id, code: "/home/base/employee/edit", valueJson: "true" },
|
|
152
|
+
{ roleId: role.id, code: "/home/base/employee/auth/use", valueJson: "true" },
|
|
153
|
+
{ roleId: role.id, code: "/home/base/employee/auth/edit", valueJson: "true" },
|
|
154
|
+
{ roleId: role.id, code: "/home/base/employee/personal/use", valueJson: "true" },
|
|
155
|
+
{ roleId: role.id, code: "/home/base/employee/personal/edit", valueJson: "true" },
|
|
156
|
+
{ roleId: role.id, code: "/home/base/employee/payroll/use", valueJson: "true" },
|
|
157
|
+
{ roleId: role.id, code: "/home/base/employee/payroll/edit", valueJson: "true" },
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
// 테스트 유저 생성 (roleId 포함)
|
|
161
|
+
const encryptedPassword = await bcrypt.hash(TEST_USER.password, 10);
|
|
162
|
+
await db.employee().insert([
|
|
163
|
+
{
|
|
164
|
+
name: TEST_USER.name,
|
|
165
|
+
email: TEST_USER.email,
|
|
166
|
+
encryptedPassword,
|
|
167
|
+
roleId: role.id,
|
|
168
|
+
isDeleted: false,
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
console.log("[e2e] DB 초기화 및 시딩 완료.");
|
|
173
|
+
|
|
174
|
+
// 3. dev 서버 시작
|
|
175
|
+
console.log("[e2e] dev 서버 시작...");
|
|
176
|
+
devServerProcess = spawn("node", ["node_modules/@simplysm/sd-cli/dist/sd-cli.js", "dev"], {
|
|
177
|
+
cwd: rootDir,
|
|
178
|
+
stdio: "pipe",
|
|
179
|
+
...(process.platform !== "win32" ? { detached: true } : {}),
|
|
180
|
+
env: {
|
|
181
|
+
...process.env,
|
|
182
|
+
DB_DATABASE: DB_CONFIG.database,
|
|
183
|
+
DB_PORT: String(DB_CONFIG.port),
|
|
184
|
+
NODE_ENV: undefined, // consola가 콘솔을 강제로 warn이상으로 설정하지 못하도록 하기 위함
|
|
185
|
+
TEST: undefined, // consola가 콘솔을 강제로 warn이상으로 설정하지 못하도록 하기 위함
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
let baseUrl: string;
|
|
190
|
+
try {
|
|
191
|
+
// 4. stdout에서 URL 파싱 (90초 타임아웃)
|
|
192
|
+
baseUrl = await waitForServerUrl(devServerProcess, 90_000);
|
|
193
|
+
console.log(`[e2e] dev 서버 URL 감지: ${baseUrl}`);
|
|
194
|
+
|
|
195
|
+
// 5. Playwright warmup — Vite 빌드 트리거 및 에러 감지 (60초 타임아웃)
|
|
196
|
+
console.log("[e2e] Vite 빌드 warmup...");
|
|
197
|
+
await warmup(baseUrl, 60_000);
|
|
198
|
+
console.log("[e2e] warmup 완료. 테스트 시작 준비됨.");
|
|
199
|
+
|
|
200
|
+
project.provide("baseUrl", baseUrl);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
killProcessTree(devServerProcess);
|
|
203
|
+
devServerProcess = undefined;
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function teardown() {
|
|
209
|
+
if (devServerProcess != null) {
|
|
210
|
+
console.log("[e2e] dev 서버 종료...");
|
|
211
|
+
killProcessTree(devServerProcess);
|
|
212
|
+
devServerProcess = undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
declare module "vitest" {
|
|
217
|
+
export interface ProvidedContext {
|
|
218
|
+
baseUrl: string;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
// 출력
|
|
4
3
|
"target": "ESNext",
|
|
5
4
|
"module": "ESNext",
|
|
6
5
|
"lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
|
|
7
6
|
"moduleResolution": "bundler",
|
|
8
|
-
|
|
9
|
-
// JSX (SolidJS)
|
|
10
7
|
"jsx": "preserve",
|
|
11
8
|
"jsxImportSource": "solid-js",
|
|
12
|
-
|
|
13
|
-
// Strict
|
|
14
9
|
"strict": true,
|
|
15
10
|
"noImplicitOverride": true,
|
|
16
11
|
"noImplicitReturns": true,
|
|
@@ -20,19 +15,13 @@
|
|
|
20
15
|
"noPropertyAccessFromIndexSignature": true,
|
|
21
16
|
"forceConsistentCasingInFileNames": true,
|
|
22
17
|
"useDefineForClassFields": true,
|
|
23
|
-
|
|
24
|
-
// 모듈
|
|
25
18
|
"esModuleInterop": true,
|
|
26
19
|
"verbatimModuleSyntax": true,
|
|
27
|
-
|
|
28
|
-
// 빌드
|
|
29
20
|
"declaration": true,
|
|
30
21
|
"declarationMap": true,
|
|
31
22
|
"sourceMap": true,
|
|
32
23
|
"skipLibCheck": true,
|
|
33
24
|
"importHelpers": true,
|
|
34
|
-
|
|
35
|
-
// 경로
|
|
36
25
|
"baseUrl": ".",
|
|
37
26
|
"paths": {
|
|
38
27
|
"@{{projectName}}/*": ["packages/*/src/index.ts"]
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
1
3
|
import { defineConfig } from "vitest/config";
|
|
2
4
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
3
5
|
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
4
8
|
export default defineConfig({
|
|
5
9
|
plugins: [
|
|
6
10
|
tsconfigPaths({
|
|
@@ -24,22 +28,22 @@ export default defineConfig({
|
|
|
24
28
|
{
|
|
25
29
|
extends: true,
|
|
26
30
|
test: {
|
|
27
|
-
name: "
|
|
28
|
-
include: ["packages/*/tests/**/*.spec.{ts,tsx}"],
|
|
31
|
+
name: "node",
|
|
32
|
+
include: ["packages/*/tests/**/*.spec.{ts,tsx,js}"],
|
|
29
33
|
testTimeout: 30000,
|
|
30
34
|
},
|
|
31
35
|
},
|
|
32
36
|
// E2E 테스트 (Docker + dev 서버 필요)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
{
|
|
38
|
+
extends: true,
|
|
39
|
+
test: {
|
|
40
|
+
name: "e2e",
|
|
41
|
+
include: ["tests/e2e/src/**/*.spec.ts"],
|
|
42
|
+
globalSetup: "./tests/e2e/vitest.setup.ts",
|
|
43
|
+
fileParallelism: false,
|
|
44
|
+
testTimeout: 30000,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
43
47
|
],
|
|
44
48
|
},
|
|
45
49
|
});
|
|
@@ -258,12 +258,12 @@ describe("watchReplaceDeps", () => {
|
|
|
258
258
|
beforeEach(async () => {
|
|
259
259
|
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "sd-watch-replace-"));
|
|
260
260
|
|
|
261
|
-
//
|
|
261
|
+
// Source package (simplysm/packages/solid)
|
|
262
262
|
const sourceDir = path.join(tmpDir, "simplysm", "packages", "solid");
|
|
263
263
|
await fs.promises.mkdir(sourceDir, { recursive: true });
|
|
264
264
|
await fs.promises.writeFile(path.join(sourceDir, "index.js"), "export default 1;");
|
|
265
265
|
|
|
266
|
-
//
|
|
266
|
+
// Target project (.pnpm store style)
|
|
267
267
|
const appRoot = path.join(tmpDir, "app");
|
|
268
268
|
const pnpmStoreTarget = path.join(
|
|
269
269
|
appRoot,
|
package/tests/run-lint.spec.ts
CHANGED
|
@@ -11,7 +11,7 @@ const { mockState, mockJitiImportFn } = vi.hoisted(() => ({
|
|
|
11
11
|
mockJitiImportFn: vi.fn(),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Mock external dependencies
|
|
15
15
|
|
|
16
16
|
vi.mock("eslint", () => {
|
|
17
17
|
class MockESLint {
|
|
@@ -94,7 +94,7 @@ describe("runLint", () => {
|
|
|
94
94
|
originalCwd = process.cwd;
|
|
95
95
|
process.cwd = vi.fn().mockReturnValue("/project");
|
|
96
96
|
|
|
97
|
-
//
|
|
97
|
+
// Reset state
|
|
98
98
|
mockState.lintResults = [];
|
|
99
99
|
mockState.lintedFiles = [];
|
|
100
100
|
mockState.outputFixesCalled = false;
|
|
@@ -237,7 +237,7 @@ describe("runLint", () => {
|
|
|
237
237
|
|
|
238
238
|
await runLint({ targets: [], fix: false, timing: false });
|
|
239
239
|
|
|
240
|
-
// ESLint
|
|
240
|
+
// ESLint is not called, so lintedFiles stays empty
|
|
241
241
|
expect(mockState.lintedFiles).toHaveLength(0);
|
|
242
242
|
expect(process.exitCode).toBeUndefined();
|
|
243
243
|
});
|
|
@@ -293,7 +293,7 @@ describe("runLint", () => {
|
|
|
293
293
|
|
|
294
294
|
await runLint({ targets: [], fix: false, timing: true });
|
|
295
295
|
|
|
296
|
-
// TIMING
|
|
296
|
+
// Verify TIMING is set (set inside the function)
|
|
297
297
|
expect(process.env["TIMING"]).toBe("1");
|
|
298
298
|
|
|
299
299
|
// cleanup
|
|
@@ -350,12 +350,12 @@ describe("executeLint", () => {
|
|
|
350
350
|
originalCwd = process.cwd;
|
|
351
351
|
process.cwd = vi.fn().mockReturnValue("/project");
|
|
352
352
|
|
|
353
|
-
//
|
|
353
|
+
// Reset state
|
|
354
354
|
mockState.lintResults = [];
|
|
355
355
|
mockState.lintedFiles = [];
|
|
356
356
|
mockState.outputFixesCalled = false;
|
|
357
357
|
|
|
358
|
-
//
|
|
358
|
+
// Default ESLint config mock
|
|
359
359
|
const cwd = "/project";
|
|
360
360
|
vi.mocked(fsExists).mockImplementation((filePath: string) => {
|
|
361
361
|
return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import "@simplysm/core-common"; // Import to use Map.getOrCreate extension method
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Mock external dependencies
|
|
5
5
|
vi.mock("typescript", () => {
|
|
6
6
|
const DiagnosticCategory = {
|
|
7
7
|
Error: 1,
|
|
@@ -194,7 +194,7 @@ describe("runTypecheck", () => {
|
|
|
194
194
|
options: [],
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
-
// Worker
|
|
197
|
+
// Runs via Worker, so exitCode should not be set
|
|
198
198
|
expect(process.exitCode).toBeUndefined();
|
|
199
199
|
});
|
|
200
200
|
|
|
@@ -393,7 +393,7 @@ describe("runTypecheck", () => {
|
|
|
393
393
|
vi.mocked(fsExists).mockResolvedValue(false);
|
|
394
394
|
vi.mocked(fsReadJson).mockResolvedValue({ devDependencies: {} });
|
|
395
395
|
|
|
396
|
-
// Worker
|
|
396
|
+
// Mock Worker to return error results
|
|
397
397
|
const { Worker } = await import("@simplysm/core-node");
|
|
398
398
|
vi.mocked(Worker.create).mockReturnValue({
|
|
399
399
|
build: vi.fn(() =>
|
package/tests/sd-cli.spec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { consola, LogLevels } from "consola";
|
|
3
3
|
|
|
4
|
-
// runLint, runTypecheck, runWatch, runCheck
|
|
4
|
+
// Mock runLint, runTypecheck, runWatch, runCheck
|
|
5
5
|
vi.mock("../src/commands/lint", () => ({
|
|
6
6
|
runLint: vi.fn(),
|
|
7
7
|
}));
|