@simplysm/sd-cli 13.0.98 → 13.0.100
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/README.md +215 -120
- package/package.json +4 -5
- package/templates/init/package.json.hbs +4 -5
- package/templates/init/packages/client-admin/package.json.hbs +7 -7
- package/templates/init/packages/db-main/package.json.hbs +2 -2
- package/templates/init/packages/server/package.json.hbs +5 -5
- package/templates/init/pnpm-workspace.yaml +0 -1
- package/templates/init/tsconfig.json.hbs +1 -2
- package/templates/init/tests-e2e/package.json.hbs +0 -16
- package/templates/init/tests-e2e/src/e2e.spec.ts +0 -36
- package/templates/init/tests-e2e/src/employee-crud.ts +0 -204
- package/templates/init/tests-e2e/src/login.ts +0 -61
- package/templates/init/tests-e2e/vitest.setup.ts.hbs +0 -220
- package/templates/init/vitest-e2e.config.ts +0 -23
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @simplysm/sd-cli
|
|
2
2
|
|
|
3
|
-
CLI tool
|
|
3
|
+
Simplysm package - CLI tool. Provides monorepo build, dev server, publish, and code quality tooling for Simplysm projects. Also exports configuration types and a Vite config factory for client packages.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,128 +10,258 @@ npm install @simplysm/sd-cli
|
|
|
10
10
|
|
|
11
11
|
## API Overview
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Config Types
|
|
14
14
|
|
|
15
15
|
| API | Type | Description |
|
|
16
16
|
|-----|------|-------------|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `SdStoragePublishConfig` | interface | FTP/FTPS/SFTP publish configuration |
|
|
22
|
-
| `SdPostPublishScriptConfig` | interface | Post-publish script configuration |
|
|
17
|
+
| `SdConfig` | interface | Top-level `sd.config.ts` configuration |
|
|
18
|
+
| `SdConfigFn` | type | Function signature for `sd.config.ts` default export |
|
|
19
|
+
| `SdConfigParams` | interface | Parameters passed to `SdConfigFn` |
|
|
20
|
+
| `SdPackageConfig` | type | Union of all package config types |
|
|
23
21
|
| `SdBuildPackageConfig` | interface | Package config for node/browser/neutral targets |
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
22
|
+
| `SdClientPackageConfig` | interface | Client package config (Vite dev server) |
|
|
23
|
+
| `SdServerPackageConfig` | interface | Server package config (Fastify server) |
|
|
24
|
+
| `SdScriptsPackageConfig` | interface | Scripts-only package config |
|
|
25
|
+
| `BuildTarget` | type | Build target type (`"node" \| "browser" \| "neutral"`) |
|
|
26
|
+
| `SdPublishConfig` | type | Union of all publish config types |
|
|
27
|
+
| `SdNpmPublishConfig` | interface | npm registry publish config |
|
|
28
|
+
| `SdLocalDirectoryPublishConfig` | interface | Local directory publish config |
|
|
29
|
+
| `SdStoragePublishConfig` | interface | FTP/FTPS/SFTP publish config |
|
|
30
|
+
| `SdPostPublishScriptConfig` | interface | Post-publish script config |
|
|
31
|
+
| `SdCapacitorConfig` | interface | Capacitor configuration |
|
|
32
|
+
| `SdCapacitorAndroidConfig` | interface | Capacitor Android platform config |
|
|
33
|
+
| `SdCapacitorSignConfig` | interface | Capacitor Android signing config |
|
|
34
|
+
| `SdCapacitorPermission` | interface | Capacitor Android permission config |
|
|
35
|
+
| `SdCapacitorIntentFilter` | interface | Capacitor Android Intent Filter config |
|
|
36
|
+
| `SdElectronConfig` | interface | Electron configuration |
|
|
37
|
+
| `SdWatchHookConfig` | interface | Watch hook config for scripts packages |
|
|
38
38
|
|
|
39
39
|
### Vite Utilities
|
|
40
40
|
|
|
41
41
|
| API | Type | Description |
|
|
42
42
|
|-----|------|-------------|
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
43
|
+
| `createViteConfig` | function | Create Vite config for SolidJS + Tailwind client packages |
|
|
44
|
+
| `ViteConfigOptions` | interface | Options for `createViteConfig` |
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
---
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
type BuildTarget = "node" | "browser" | "neutral";
|
|
50
|
-
```
|
|
48
|
+
### `SdConfig`
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| SdLocalDirectoryPublishConfig
|
|
58
|
-
| SdStoragePublishConfig;
|
|
59
|
-
```
|
|
50
|
+
| Field | Type | Description |
|
|
51
|
+
|-------|------|-------------|
|
|
52
|
+
| `packages` | `Record<string, SdPackageConfig \| undefined>` | Per-package configuration (key: subdirectory name under `packages/`) |
|
|
53
|
+
| `replaceDeps` | `Record<string, string>?` | Dependency replacement config (symlink local sources) |
|
|
54
|
+
| `postPublish` | `SdPostPublishScriptConfig[]?` | Scripts to execute after deployment |
|
|
60
55
|
|
|
61
|
-
|
|
56
|
+
### `SdConfigFn`
|
|
62
57
|
|
|
63
58
|
```typescript
|
|
64
|
-
type
|
|
65
|
-
| SdBuildPackageConfig
|
|
66
|
-
| SdClientPackageConfig
|
|
67
|
-
| SdServerPackageConfig
|
|
68
|
-
| SdScriptsPackageConfig;
|
|
59
|
+
type SdConfigFn = (params: SdConfigParams) => SdConfig | Promise<SdConfig>
|
|
69
60
|
```
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
interface SdConfig {
|
|
75
|
-
packages: Record<string, SdPackageConfig | undefined>;
|
|
76
|
-
replaceDeps?: Record<string, string>;
|
|
77
|
-
postPublish?: SdPostPublishScriptConfig[];
|
|
78
|
-
}
|
|
79
|
-
```
|
|
62
|
+
### `SdConfigParams`
|
|
80
63
|
|
|
81
|
-
|
|
64
|
+
| Field | Type | Description |
|
|
65
|
+
|-------|------|-------------|
|
|
66
|
+
| `cwd` | `string` | Current working directory |
|
|
67
|
+
| `dev` | `boolean` | Development mode flag |
|
|
68
|
+
| `options` | `string[]` | Additional options (from CLI `-o` flag) |
|
|
82
69
|
|
|
83
|
-
|
|
70
|
+
### `BuildTarget`
|
|
84
71
|
|
|
85
72
|
```typescript
|
|
86
|
-
type
|
|
73
|
+
type BuildTarget = "node" | "browser" | "neutral"
|
|
87
74
|
```
|
|
88
75
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
## `SdConfigParams`
|
|
76
|
+
### `SdPackageConfig`
|
|
92
77
|
|
|
93
78
|
```typescript
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
79
|
+
type SdPackageConfig =
|
|
80
|
+
| SdBuildPackageConfig
|
|
81
|
+
| SdClientPackageConfig
|
|
82
|
+
| SdServerPackageConfig
|
|
83
|
+
| SdScriptsPackageConfig
|
|
99
84
|
```
|
|
100
85
|
|
|
101
|
-
|
|
86
|
+
### `SdBuildPackageConfig`
|
|
87
|
+
|
|
88
|
+
| Field | Type | Description |
|
|
89
|
+
|-------|------|-------------|
|
|
90
|
+
| `target` | `BuildTarget` | Build target |
|
|
91
|
+
| `publish` | `SdPublishConfig?` | Publish configuration |
|
|
92
|
+
| `copySrc` | `string[]?` | Glob patterns for files to copy from `src/` to `dist/` |
|
|
93
|
+
|
|
94
|
+
### `SdClientPackageConfig`
|
|
95
|
+
|
|
96
|
+
| Field | Type | Description |
|
|
97
|
+
|-------|------|-------------|
|
|
98
|
+
| `target` | `"client"` | Build target |
|
|
99
|
+
| `server` | `string \| number` | Server package name or Vite port number |
|
|
100
|
+
| `env` | `Record<string, string>?` | Environment variables for build |
|
|
101
|
+
| `publish` | `SdPublishConfig?` | Publish configuration |
|
|
102
|
+
| `capacitor` | `SdCapacitorConfig?` | Capacitor configuration |
|
|
103
|
+
| `electron` | `SdElectronConfig?` | Electron configuration |
|
|
104
|
+
| `configs` | `Record<string, unknown>?` | Runtime config (written to `dist/.config.json`) |
|
|
105
|
+
| `exclude` | `string[]?` | Packages to exclude from Vite optimizeDeps |
|
|
106
|
+
|
|
107
|
+
### `SdServerPackageConfig`
|
|
108
|
+
|
|
109
|
+
| Field | Type | Description |
|
|
110
|
+
|-------|------|-------------|
|
|
111
|
+
| `target` | `"server"` | Build target |
|
|
112
|
+
| `env` | `Record<string, string>?` | Environment variables for build |
|
|
113
|
+
| `publish` | `SdPublishConfig?` | Publish configuration |
|
|
114
|
+
| `configs` | `Record<string, unknown>?` | Runtime config (written to `dist/.config.json`) |
|
|
115
|
+
| `externals` | `string[]?` | External modules for esbuild |
|
|
116
|
+
| `pm2` | `{ name?: string; ignoreWatchPaths?: string[] }?` | PM2 configuration |
|
|
117
|
+
| `packageManager` | `"volta" \| "mise"?` | Package manager setting |
|
|
118
|
+
|
|
119
|
+
### `SdScriptsPackageConfig`
|
|
120
|
+
|
|
121
|
+
| Field | Type | Description |
|
|
122
|
+
|-------|------|-------------|
|
|
123
|
+
| `target` | `"scripts"` | Build target |
|
|
124
|
+
| `publish` | `SdPublishConfig?` | Publish configuration |
|
|
125
|
+
| `watch` | `SdWatchHookConfig?` | Watch hook configuration |
|
|
126
|
+
|
|
127
|
+
### `SdWatchHookConfig`
|
|
128
|
+
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
|-------|------|-------------|
|
|
131
|
+
| `target` | `string[]` | Glob patterns to watch (relative to package directory) |
|
|
132
|
+
| `cmd` | `string` | Command to execute on change |
|
|
133
|
+
| `args` | `string[]?` | Command arguments |
|
|
134
|
+
|
|
135
|
+
### `SdPublishConfig`
|
|
102
136
|
|
|
103
137
|
```typescript
|
|
104
|
-
|
|
105
|
-
pkgDir: string;
|
|
106
|
-
name: string;
|
|
107
|
-
tsconfigPath: string;
|
|
108
|
-
compilerOptions: Record<string, unknown>;
|
|
109
|
-
env?: Record<string, string>;
|
|
110
|
-
mode: "build" | "dev";
|
|
111
|
-
serverPort?: number;
|
|
112
|
-
replaceDeps?: string[];
|
|
113
|
-
onScopeRebuild?: () => void;
|
|
114
|
-
outDir?: string;
|
|
115
|
-
base?: string;
|
|
116
|
-
}
|
|
138
|
+
type SdPublishConfig = SdNpmPublishConfig | SdLocalDirectoryPublishConfig | SdStoragePublishConfig
|
|
117
139
|
```
|
|
118
140
|
|
|
119
|
-
|
|
141
|
+
### `SdNpmPublishConfig`
|
|
142
|
+
|
|
143
|
+
| Field | Type | Description |
|
|
144
|
+
|-------|------|-------------|
|
|
145
|
+
| `type` | `"npm"` | Publish type |
|
|
146
|
+
|
|
147
|
+
### `SdLocalDirectoryPublishConfig`
|
|
148
|
+
|
|
149
|
+
| Field | Type | Description |
|
|
150
|
+
|-------|------|-------------|
|
|
151
|
+
| `type` | `"local-directory"` | Publish type |
|
|
152
|
+
| `path` | `string` | Target path (supports `%VER%`, `%PROJECT%` substitution) |
|
|
153
|
+
|
|
154
|
+
### `SdStoragePublishConfig`
|
|
155
|
+
|
|
156
|
+
| Field | Type | Description |
|
|
157
|
+
|-------|------|-------------|
|
|
158
|
+
| `type` | `"ftp" \| "ftps" \| "sftp"` | Protocol type |
|
|
159
|
+
| `host` | `string` | Server hostname |
|
|
160
|
+
| `port` | `number?` | Server port |
|
|
161
|
+
| `path` | `string?` | Remote path |
|
|
162
|
+
| `user` | `string?` | Username |
|
|
163
|
+
| `password` | `string?` | Password |
|
|
164
|
+
|
|
165
|
+
### `SdPostPublishScriptConfig`
|
|
166
|
+
|
|
167
|
+
| Field | Type | Description |
|
|
168
|
+
|-------|------|-------------|
|
|
169
|
+
| `type` | `"script"` | Config type |
|
|
170
|
+
| `cmd` | `string` | Command to execute |
|
|
171
|
+
| `args` | `string[]` | Script arguments (supports `%VER%`, `%PROJECT%` substitution) |
|
|
172
|
+
|
|
173
|
+
### `SdCapacitorConfig`
|
|
174
|
+
|
|
175
|
+
| Field | Type | Description |
|
|
176
|
+
|-------|------|-------------|
|
|
177
|
+
| `appId` | `string` | App ID (e.g., `"com.example.app"`) |
|
|
178
|
+
| `appName` | `string` | App name |
|
|
179
|
+
| `plugins` | `Record<string, Record<string, unknown> \| true>?` | Capacitor plugin configuration |
|
|
180
|
+
| `icon` | `string?` | App icon path (relative to package directory) |
|
|
181
|
+
| `debug` | `boolean?` | Debug build flag |
|
|
182
|
+
| `platform` | `{ android?: SdCapacitorAndroidConfig }?` | Per-platform configuration |
|
|
183
|
+
|
|
184
|
+
### `SdCapacitorAndroidConfig`
|
|
185
|
+
|
|
186
|
+
| Field | Type | Description |
|
|
187
|
+
|-------|------|-------------|
|
|
188
|
+
| `config` | `Record<string, string>?` | AndroidManifest.xml application tag attributes |
|
|
189
|
+
| `bundle` | `boolean?` | AAB bundle build flag (false for APK) |
|
|
190
|
+
| `intentFilters` | `SdCapacitorIntentFilter[]?` | Intent Filter configuration |
|
|
191
|
+
| `sign` | `SdCapacitorSignConfig?` | APK/AAB signing configuration |
|
|
192
|
+
| `sdkVersion` | `number?` | Android SDK version (minSdk, targetSdk) |
|
|
193
|
+
| `permissions` | `SdCapacitorPermission[]?` | Additional permission configuration |
|
|
194
|
+
|
|
195
|
+
### `SdCapacitorSignConfig`
|
|
196
|
+
|
|
197
|
+
| Field | Type | Description |
|
|
198
|
+
|-------|------|-------------|
|
|
199
|
+
| `keystore` | `string` | Keystore file path (relative to package directory) |
|
|
200
|
+
| `storePassword` | `string` | Keystore password |
|
|
201
|
+
| `alias` | `string` | Key alias |
|
|
202
|
+
| `password` | `string` | Key password |
|
|
203
|
+
| `keystoreType` | `string?` | Keystore type (default: `"jks"`) |
|
|
204
|
+
|
|
205
|
+
### `SdCapacitorPermission`
|
|
206
|
+
|
|
207
|
+
| Field | Type | Description |
|
|
208
|
+
|-------|------|-------------|
|
|
209
|
+
| `name` | `string` | Permission name (e.g., `"CAMERA"`) |
|
|
210
|
+
| `maxSdkVersion` | `number?` | Maximum SDK version |
|
|
211
|
+
| `ignore` | `string?` | `tools:ignore` attribute value |
|
|
212
|
+
|
|
213
|
+
### `SdCapacitorIntentFilter`
|
|
214
|
+
|
|
215
|
+
| Field | Type | Description |
|
|
216
|
+
|-------|------|-------------|
|
|
217
|
+
| `action` | `string?` | Intent action (e.g., `"android.intent.action.VIEW"`) |
|
|
218
|
+
| `category` | `string?` | Intent category (e.g., `"android.intent.category.DEFAULT"`) |
|
|
219
|
+
|
|
220
|
+
### `SdElectronConfig`
|
|
221
|
+
|
|
222
|
+
| Field | Type | Description |
|
|
223
|
+
|-------|------|-------------|
|
|
224
|
+
| `appId` | `string` | Electron app ID |
|
|
225
|
+
| `portable` | `boolean?` | Portable `.exe` (true) or NSIS installer (false) |
|
|
226
|
+
| `installerIcon` | `string?` | Installer icon path (`.ico`, relative to package directory) |
|
|
227
|
+
| `reinstallDependencies` | `string[]?` | npm packages to include in Electron |
|
|
228
|
+
| `postInstallScript` | `string?` | npm postinstall script |
|
|
229
|
+
| `nsisOptions` | `Record<string, unknown>?` | NSIS options |
|
|
230
|
+
| `env` | `Record<string, string>?` | Environment variables |
|
|
231
|
+
|
|
232
|
+
### `ViteConfigOptions`
|
|
233
|
+
|
|
234
|
+
| Field | Type | Description |
|
|
235
|
+
|-------|------|-------------|
|
|
236
|
+
| `pkgDir` | `string` | Package directory path |
|
|
237
|
+
| `name` | `string` | Package name |
|
|
238
|
+
| `tsconfigPath` | `string` | tsconfig.json path |
|
|
239
|
+
| `compilerOptions` | `Record<string, unknown>` | TypeScript compiler options |
|
|
240
|
+
| `env` | `Record<string, string>?` | Environment variables |
|
|
241
|
+
| `mode` | `"build" \| "dev"` | Build or dev mode |
|
|
242
|
+
| `serverPort` | `number?` | Server port in dev mode (0 for auto-assign) |
|
|
243
|
+
| `replaceDeps` | `string[]?` | Array of replaceDeps package names |
|
|
244
|
+
| `onScopeRebuild` | `(() => void)?` | Callback when replaceDeps package dist changes |
|
|
245
|
+
| `outDir` | `string?` | Override `build.outDir` |
|
|
246
|
+
| `base` | `string?` | Override base path |
|
|
247
|
+
| `exclude` | `string[]?` | Packages to exclude from optimizeDeps |
|
|
248
|
+
|
|
249
|
+
### `createViteConfig`
|
|
120
250
|
|
|
121
251
|
```typescript
|
|
122
|
-
function createViteConfig(options: ViteConfigOptions): ViteUserConfig
|
|
252
|
+
function createViteConfig(options: ViteConfigOptions): ViteUserConfig
|
|
123
253
|
```
|
|
124
254
|
|
|
125
|
-
|
|
255
|
+
Creates a Vite config for SolidJS + Tailwind CSS client packages. Includes plugins for tsconfig paths, SolidJS, PWA, Tailwind config deps watching, scope package watching, and public-dev directory serving.
|
|
126
256
|
|
|
127
257
|
## Usage Examples
|
|
128
258
|
|
|
129
|
-
###
|
|
259
|
+
### sd.config.ts
|
|
130
260
|
|
|
131
261
|
```typescript
|
|
132
|
-
import type { SdConfigFn } from "@simplysm/sd-cli";
|
|
262
|
+
import type { SdConfigFn, SdConfigParams } from "@simplysm/sd-cli";
|
|
133
263
|
|
|
134
|
-
const config: SdConfigFn = (params) => ({
|
|
264
|
+
const config: SdConfigFn = (params: SdConfigParams) => ({
|
|
135
265
|
packages: {
|
|
136
266
|
"core-common": { target: "neutral" },
|
|
137
267
|
"core-node": { target: "node" },
|
|
@@ -144,57 +274,22 @@ const config: SdConfigFn = (params) => ({
|
|
|
144
274
|
pm2: { name: "my-app" },
|
|
145
275
|
},
|
|
146
276
|
},
|
|
147
|
-
replaceDeps: {
|
|
148
|
-
"@simplysm/*": "../simplysm/packages/*",
|
|
149
|
-
},
|
|
150
277
|
});
|
|
151
278
|
|
|
152
279
|
export default config;
|
|
153
280
|
```
|
|
154
281
|
|
|
155
|
-
###
|
|
282
|
+
### Custom Vite config
|
|
156
283
|
|
|
157
284
|
```typescript
|
|
158
285
|
import { createViteConfig } from "@simplysm/sd-cli";
|
|
159
286
|
|
|
160
287
|
const config = createViteConfig({
|
|
161
|
-
pkgDir: "/path/to/
|
|
288
|
+
pkgDir: "/path/to/package",
|
|
162
289
|
name: "my-client",
|
|
163
|
-
tsconfigPath: "/path/to/
|
|
290
|
+
tsconfigPath: "/path/to/tsconfig.json",
|
|
164
291
|
compilerOptions: { jsx: "preserve" },
|
|
165
292
|
mode: "dev",
|
|
166
293
|
serverPort: 3000,
|
|
167
294
|
});
|
|
168
295
|
```
|
|
169
|
-
|
|
170
|
-
### Configure Capacitor build
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
import type { SdConfigFn } from "@simplysm/sd-cli";
|
|
174
|
-
|
|
175
|
-
const config: SdConfigFn = (params) => ({
|
|
176
|
-
packages: {
|
|
177
|
-
"my-app": {
|
|
178
|
-
target: "client",
|
|
179
|
-
server: "my-server",
|
|
180
|
-
capacitor: {
|
|
181
|
-
appId: "com.example.myapp",
|
|
182
|
-
appName: "My App",
|
|
183
|
-
icon: "resources/icon.png",
|
|
184
|
-
platform: {
|
|
185
|
-
android: {
|
|
186
|
-
sign: {
|
|
187
|
-
keystore: "keystore.jks",
|
|
188
|
-
storePassword: "pass",
|
|
189
|
-
alias: "key0",
|
|
190
|
-
password: "pass",
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
export default config;
|
|
200
|
-
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/sd-cli",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.100",
|
|
4
4
|
"description": "Simplysm package - CLI tool",
|
|
5
5
|
"author": "simplysm",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"docs",
|
|
18
17
|
"src",
|
|
19
18
|
"templates",
|
|
20
19
|
"tests"
|
|
@@ -43,9 +42,9 @@
|
|
|
43
42
|
"vite-plugin-solid": "^2.11.11",
|
|
44
43
|
"vite-tsconfig-paths": "^6.1.1",
|
|
45
44
|
"yargs": "^18.0.0",
|
|
46
|
-
"@simplysm/core-common": "13.0.
|
|
47
|
-
"@simplysm/core-node": "13.0.
|
|
48
|
-
"@simplysm/storage": "13.0.
|
|
45
|
+
"@simplysm/core-common": "13.0.100",
|
|
46
|
+
"@simplysm/core-node": "13.0.100",
|
|
47
|
+
"@simplysm/storage": "13.0.100"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
50
|
"@types/semver": "^7.7.1",
|
|
@@ -15,19 +15,18 @@
|
|
|
15
15
|
"lint:fix": "sd-cli lint --fix",
|
|
16
16
|
"check": "sd-cli check",
|
|
17
17
|
"test": "vitest run",
|
|
18
|
-
"test:e2e": "vitest run -c vitest-e2e.config.ts",
|
|
19
18
|
"postinstall": "playwright-cli install --skills"
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
|
22
|
-
"@simplysm/lint": "~13.0.
|
|
23
|
-
"@simplysm/sd-cli": "~13.0.
|
|
24
|
-
"@simplysm/sd-claude": "~13.0.
|
|
21
|
+
"@simplysm/lint": "~13.0.100",
|
|
22
|
+
"@simplysm/sd-cli": "~13.0.100",
|
|
23
|
+
"@simplysm/sd-claude": "~13.0.100",
|
|
25
24
|
"@playwright/cli": "^0.1.1",
|
|
26
25
|
"@types/node": "^20.19.37",
|
|
27
26
|
"eslint": "^9.39.4",
|
|
28
27
|
"prettier": "^3.8.1",
|
|
29
28
|
"typescript": "^5.9.3",
|
|
30
29
|
"vite-tsconfig-paths": "^6.1.1",
|
|
31
|
-
"vitest": "^4.1.0"
|
|
30
|
+
"vitest": "^4.1.0"
|
|
32
31
|
}
|
|
33
32
|
}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
"private": true,
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@{{projectName}}/db-main": "workspace:*",
|
|
9
|
-
"@simplysm/core-browser": "~13.0.
|
|
10
|
-
"@simplysm/core-common": "~13.0.
|
|
11
|
-
"@simplysm/excel": "~13.0.
|
|
12
|
-
"@simplysm/orm-common": "~13.0.
|
|
13
|
-
"@simplysm/service-client": "~13.0.
|
|
14
|
-
"@simplysm/service-common": "~13.0.
|
|
15
|
-
"@simplysm/solid": "~13.0.
|
|
9
|
+
"@simplysm/core-browser": "~13.0.100",
|
|
10
|
+
"@simplysm/core-common": "~13.0.100",
|
|
11
|
+
"@simplysm/excel": "~13.0.100",
|
|
12
|
+
"@simplysm/orm-common": "~13.0.100",
|
|
13
|
+
"@simplysm/service-client": "~13.0.100",
|
|
14
|
+
"@simplysm/service-common": "~13.0.100",
|
|
15
|
+
"@simplysm/solid": "~13.0.100",
|
|
16
16
|
"@solid-primitives/event-listener": "^2.4.5",
|
|
17
17
|
"@solidjs/router": "^0.15.4",
|
|
18
18
|
"@tabler/icons-solidjs": "^3.40.0",
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@{{projectName}}/db-main": "workspace:*",
|
|
8
|
-
"@simplysm/core-common": "~13.0.
|
|
9
|
-
"@simplysm/excel": "~13.0.
|
|
10
|
-
"@simplysm/orm-common": "~13.0.
|
|
11
|
-
"@simplysm/orm-node": "~13.0.
|
|
12
|
-
"@simplysm/service-server": "~13.0.
|
|
8
|
+
"@simplysm/core-common": "~13.0.100",
|
|
9
|
+
"@simplysm/excel": "~13.0.100",
|
|
10
|
+
"@simplysm/orm-common": "~13.0.100",
|
|
11
|
+
"@simplysm/orm-node": "~13.0.100",
|
|
12
|
+
"@simplysm/service-server": "~13.0.100",
|
|
13
13
|
"bcrypt": "^6.0.0",
|
|
14
14
|
"pg": "^8.20.0",
|
|
15
15
|
"pg-copy-streams": "^7.0.0"
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@{{projectName}}-test/e2e",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "{{projectName}} E2E tests",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"private": true,
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@{{projectName}}/db-main": "workspace:*",
|
|
9
|
-
"@simplysm/orm-node": "~13.0.98",
|
|
10
|
-
"bcrypt": "^6.0.0",
|
|
11
|
-
"playwright": "^1.58.2"
|
|
12
|
-
},
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"@types/bcrypt": "^6.0.0"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, inject, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
|
3
|
-
import { loginTests } from "./login";
|
|
4
|
-
import { employeeCrudTests } from "./employee-crud";
|
|
5
|
-
|
|
6
|
-
const ctx = {} as { page: Page; baseUrl: string };
|
|
7
|
-
|
|
8
|
-
let browser: Browser;
|
|
9
|
-
let context: BrowserContext;
|
|
10
|
-
|
|
11
|
-
describe("E2E", () => {
|
|
12
|
-
beforeAll(async () => {
|
|
13
|
-
ctx.baseUrl = inject("baseUrl");
|
|
14
|
-
browser = await chromium.launch({ headless: true });
|
|
15
|
-
context = await browser.newContext();
|
|
16
|
-
ctx.page = await context.newPage();
|
|
17
|
-
ctx.page.setDefaultTimeout(500);
|
|
18
|
-
|
|
19
|
-
// Output browser console errors to test console
|
|
20
|
-
ctx.page.on("pageerror", (err) => {
|
|
21
|
-
console.error(`[PAGE_ERROR] ${err.message}`);
|
|
22
|
-
});
|
|
23
|
-
ctx.page.on("console", (msg) => {
|
|
24
|
-
if (msg.type() === "error") {
|
|
25
|
-
console.error(`[CONSOLE_ERROR] ${msg.text()}`);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
afterAll(async () => {
|
|
31
|
-
await browser.close();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
loginTests(ctx);
|
|
35
|
-
employeeCrudTests(ctx);
|
|
36
|
-
});
|
|
@@ -1,204 +0,0 @@
|
|
|
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-dialog-panel]").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-dialog-panel]").length === 0 &&
|
|
29
|
-
document.querySelectorAll("[data-dialog-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
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
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,23 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
import tsconfigPaths from "vite-tsconfig-paths";
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
plugins: [
|
|
6
|
-
tsconfigPaths({
|
|
7
|
-
root: ".",
|
|
8
|
-
projects: ["./tsconfig.json"],
|
|
9
|
-
}),
|
|
10
|
-
],
|
|
11
|
-
define: {
|
|
12
|
-
"process.env.DEV": JSON.stringify("true"),
|
|
13
|
-
"process.env.VER": JSON.stringify("1.0.0-test"),
|
|
14
|
-
},
|
|
15
|
-
test: {
|
|
16
|
-
globals: true,
|
|
17
|
-
environment: "node",
|
|
18
|
-
include: ["tests-e2e/src/**/*.spec.ts"],
|
|
19
|
-
globalSetup: "./tests-e2e/vitest.setup.ts",
|
|
20
|
-
fileParallelism: false,
|
|
21
|
-
testTimeout: 30000,
|
|
22
|
-
},
|
|
23
|
-
});
|