@simplysm/sd-cli 14.0.64 → 14.0.66

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.
Files changed (79) hide show
  1. package/dist/capacitor/capacitor-android.d.ts +2 -0
  2. package/dist/capacitor/capacitor-android.d.ts.map +1 -1
  3. package/dist/capacitor/capacitor-android.js +13 -0
  4. package/dist/capacitor/capacitor-android.js.map +1 -1
  5. package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
  6. package/dist/capacitor/capacitor-npm-config.js +2 -6
  7. package/dist/capacitor/capacitor-npm-config.js.map +1 -1
  8. package/dist/electron/electron.d.ts.map +1 -1
  9. package/dist/electron/electron.js +1 -2
  10. package/dist/electron/electron.js.map +1 -1
  11. package/package.json +13 -13
  12. package/src/capacitor/capacitor-android.ts +14 -0
  13. package/src/capacitor/capacitor-npm-config.ts +2 -6
  14. package/src/electron/electron.ts +1 -2
  15. package/tests/angular/ngtsc-build-core.acc.spec.ts +36 -94
  16. package/tests/capacitor/capacitor-android.spec.ts +65 -28
  17. package/tests/capacitor/capacitor-build.spec.ts +40 -385
  18. package/tests/capacitor/capacitor-config-writer.acc.spec.ts +3 -17
  19. package/tests/capacitor/capacitor-config-writer.spec.ts +3 -17
  20. package/tests/capacitor/capacitor-init.spec.ts +40 -636
  21. package/tests/capacitor/capacitor-npm-config.acc.spec.ts +38 -168
  22. package/tests/capacitor/capacitor-npm-config.spec.ts +33 -71
  23. package/tests/commands/check.spec.ts +25 -36
  24. package/tests/commands/deployment-phase.acc.spec.ts +17 -26
  25. package/tests/commands/git-phase.acc.spec.ts +13 -112
  26. package/tests/commands/lint.spec.ts +7 -24
  27. package/tests/commands/post-publish-phase.acc.spec.ts +5 -10
  28. package/tests/commands/typecheck.spec.ts +43 -65
  29. package/tests/electron/electron.spec.ts +22 -46
  30. package/tests/engines/base-engine.spec.ts +4 -13
  31. package/tests/engines/engine-selection.spec.ts +14 -17
  32. package/tests/engines/engine-typecheck-selection.acc.spec.ts +13 -16
  33. package/tests/engines/esbuild-client-engine.acc.spec.ts +36 -40
  34. package/tests/engines/esbuild-client-engine.spec.ts +4 -23
  35. package/tests/engines/ngtsc-engine.spec.ts +3 -10
  36. package/tests/engines/server-esbuild-engine.spec.ts +3 -10
  37. package/tests/engines/tsc-engine.spec.ts +3 -10
  38. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +3 -8
  39. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +3 -8
  40. package/tests/orchestrators/build-orchestrator.spec.ts +57 -102
  41. package/tests/orchestrators/dev-orchestrator.spec.ts +68 -109
  42. package/tests/orchestrators/typecheck-orchestrator.spec.ts +25 -57
  43. package/tests/orchestrators/watch-orchestrator.spec.ts +73 -99
  44. package/tests/sd-cli-entry.spec.ts +17 -20
  45. package/tests/utils/angular-source-file-cache.spec.ts +4 -8
  46. package/tests/utils/copy-src.spec.ts +9 -20
  47. package/tests/utils/esbuild-client-config.acc.spec.ts +9 -15
  48. package/tests/utils/esbuild-client-config.spec.ts +12 -24
  49. package/tests/utils/esbuild-config.spec.ts +51 -42
  50. package/tests/utils/lint-core.spec.ts +13 -19
  51. package/tests/utils/lint-utils.spec.ts +8 -15
  52. package/tests/utils/lint-with-program.spec.ts +3 -7
  53. package/tests/utils/ngtsc-build-core.spec.ts +2 -99
  54. package/tests/utils/orchestrator-utils.spec.ts +7 -20
  55. package/tests/utils/output-utils.spec.ts +5 -11
  56. package/tests/utils/sd-config.spec.ts +4 -12
  57. package/tests/utils/typecheck-env.spec.ts +49 -77
  58. package/tests/utils/typecheck-non-package.spec.ts +23 -16
  59. package/tests/workers/build-watch-paths.acc.spec.ts +4 -10
  60. package/tests/workers/build-watch-paths.spec.ts +4 -9
  61. package/tests/workers/client-worker.acc.spec.ts +64 -137
  62. package/tests/workers/client-worker.spec.ts +63 -89
  63. package/tests/workers/library-build-lint.spec.ts +19 -30
  64. package/tests/workers/library-build-worker.spec.ts +28 -55
  65. package/tests/workers/server-esbuild-context.acc.spec.ts +6 -15
  66. package/tests/workers/server-esbuild-context.spec.ts +7 -16
  67. package/tests/workers/server-runtime-worker.spec.ts +8 -10
  68. package/tests/workers/shared-worker-lifecycle.acc.spec.ts +3 -5
  69. package/tests/workers/shared-worker-lifecycle.spec.ts +4 -5
  70. package/tests/capacitor/capacitor-icon.spec.ts +0 -285
  71. package/tests/capacitor/capacitor-run.spec.ts +0 -256
  72. package/tests/capacitor/capacitor-workspace.spec.ts +0 -203
  73. package/tests/commands/device.spec.ts +0 -237
  74. package/tests/commands/publish.spec.ts +0 -1183
  75. package/tests/utils/external-modules.spec.ts +0 -217
  76. package/tests/workers/server-build-lint.spec.ts +0 -201
  77. package/tests/workers/server-build-worker.spec.ts +0 -765
  78. package/tests/workers/server-watch-manager.acc.spec.ts +0 -162
  79. package/tests/workers/server-watch-manager.spec.ts +0 -199
@@ -1,55 +1,9 @@
1
1
  /* eslint-disable no-restricted-properties -- 테스트 환경변수 조작 필요 */
2
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
- import { consola } from "consola";
4
-
5
- //#region Mocks
6
-
7
- const mockFsxExists = vi.fn();
8
- const mockFsxRead = vi.fn();
9
- const mockFsxWrite = vi.fn().mockResolvedValue(undefined);
10
- const mockFsxReadJson = vi.fn();
11
- const mockFsxWriteJson = vi.fn().mockResolvedValue(undefined);
12
- const mockFsxMkdir = vi.fn().mockResolvedValue(undefined);
13
- const mockFsxRm = vi.fn().mockResolvedValue(undefined);
14
- const mockFsxGlob = vi.fn();
15
- const mockFsxCopy = vi.fn().mockResolvedValue(undefined);
16
-
17
- vi.mock("@simplysm/core-node", async (importOriginal) => {
18
- const original = await importOriginal<typeof import("@simplysm/core-node")>();
19
- return {
20
- ...original,
21
- fsx: {
22
- exists: mockFsxExists,
23
- read: mockFsxRead,
24
- write: mockFsxWrite,
25
- readJson: mockFsxReadJson,
26
- writeJson: mockFsxWriteJson,
27
- mkdir: mockFsxMkdir,
28
- rm: mockFsxRm,
29
- glob: mockFsxGlob,
30
- copy: mockFsxCopy,
31
- },
32
- cpx: {
33
- spawn: mockCpxSpawn,
34
- spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
35
- },
36
- };
37
- });
38
-
39
- const execaCalls: { command: string; args: string[] }[] = [];
40
- const mockCpxSpawn = vi.fn((...args: unknown[]) => {
41
- execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
42
- return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
43
- });
44
-
45
- const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
46
- vi.mock("node:fs", () => ({
47
- default: { promises: { writeFile: (...args: unknown[]) => mockFsWriteFile(...args) } },
48
- existsSync: (p: string) => {
49
- if (p.includes("pnpm-workspace.yaml")) return true;
50
- return false;
51
- },
52
- }));
2
+ import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { fsx, cpx } from "@simplysm/core-node";
53
7
 
54
8
  vi.mock("sharp", () => ({
55
9
  default: vi.fn().mockReturnValue({
@@ -61,22 +15,41 @@ vi.mock("sharp", () => ({
61
15
  }),
62
16
  }));
63
17
 
64
- const mockLoggerWarn = vi.fn();
65
- vi.spyOn(consola, "withTag").mockReturnValue({ debug: vi.fn(), warn: mockLoggerWarn, error: vi.fn(), info: vi.fn(), success: vi.fn(), start: vi.fn() } as any);
66
-
67
- //#endregion
68
-
69
- //#region Helpers
18
+ const mockFsxExists = vi.spyOn(fsx, "exists");
19
+ const mockFsxRead = vi.spyOn(fsx, "read");
20
+ vi.spyOn(fsx, "write").mockResolvedValue(undefined);
21
+ const mockFsxReadJson = vi.spyOn(fsx, "readJson");
22
+ vi.spyOn(fsx, "writeJson").mockResolvedValue(undefined);
23
+ vi.spyOn(fsx, "mkdir").mockResolvedValue(undefined);
24
+ vi.spyOn(fsx, "rm").mockResolvedValue(undefined);
25
+ const mockFsxGlob = vi.spyOn(fsx, "glob");
26
+ vi.spyOn(fsx, "copy").mockResolvedValue(undefined);
27
+
28
+ vi.spyOn(cpx, "spawn").mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
29
+ vi.spyOn(cpx, "spawnSync").mockReturnValue({ stdout: "", stderr: "", exitCode: 0 });
30
+
31
+ let tmpRoot: string;
32
+ let PKG_PATH: string;
33
+
34
+ beforeAll(() => {
35
+ tmpRoot = mkdtempSync(path.join(tmpdir(), "cap-init-"));
36
+ writeFileSync(path.join(tmpRoot, "pnpm-workspace.yaml"), "");
37
+ PKG_PATH = path.join(tmpRoot, "pkg");
38
+ // capacitor.ts가 .capacitor/.capacitor.lock 파일을 작성하므로 디렉토리 미리 생성
39
+ mkdirSync(path.join(PKG_PATH, ".capacitor"), { recursive: true });
40
+ });
70
41
 
71
- const PKG_PATH = "/fake/pkg";
42
+ afterAll(() => {
43
+ rmSync(tmpRoot, { recursive: true, force: true });
44
+ });
72
45
 
73
46
  function setupDefaultMocks() {
74
- mockFsxExists.mockImplementation((p: string) => {
47
+ mockFsxExists.mockImplementation(((p: string) => {
75
48
  if (p.includes(".capacitor.lock")) return false;
76
49
  return true;
77
- });
50
+ }) as never);
78
51
 
79
- mockFsxReadJson.mockImplementation((p: string) => {
52
+ mockFsxReadJson.mockImplementation(((p: string) => {
80
53
  const normalized = p.replace(/\\/g, "/");
81
54
  if (normalized.includes(".capacitor/package.json")) {
82
55
  return {
@@ -97,9 +70,9 @@ function setupDefaultMocks() {
97
70
  return { name: "test-pkg", version: "1.0.0" };
98
71
  }
99
72
  return {};
100
- });
73
+ }) as never);
101
74
 
102
- mockFsxRead.mockImplementation((p: string) => {
75
+ mockFsxRead.mockImplementation(((p: string) => {
103
76
  if (p.includes("AndroidManifest.xml")) {
104
77
  return '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n<application>\n<activity android:name=".MainActivity">\n</activity>\n</application>\n</manifest>';
105
78
  }
@@ -111,45 +84,19 @@ function setupDefaultMocks() {
111
84
  minSdkVersion rootProject.ext.minSdkVersion
112
85
  targetSdkVersion rootProject.ext.targetSdkVersion
113
86
  }
114
- buildTypes {
115
- release {
116
- }
117
- }
87
+ buildTypes { release { } }
118
88
  }`;
119
89
  }
120
90
  if (p.includes("gradle.properties")) {
121
91
  return "org.gradle.jvmargs=-Xmx2048m";
122
92
  }
123
- if (p.includes("styles.xml")) {
124
- return `<?xml version="1.0" encoding="utf-8"?>
125
- <resources>
126
- <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
127
- <item name="colorPrimary">@color/colorPrimary</item>
128
- </style>
129
- <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
130
- <item name="windowActionBar">false</item>
131
- <item name="windowNoTitle">true</item>
132
- <item name="android:background">@null</item>
133
- </style>
134
- <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
135
- <item name="android:background">@drawable/splash</item>
136
- </style>
137
- </resources>`;
138
- }
139
93
  return "";
140
- });
94
+ }) as never);
141
95
 
142
96
  mockFsxGlob.mockResolvedValue(["C:/Program Files/Amazon Corretto/jdk21.0.1"]);
143
-
144
97
  process.env["ANDROID_HOME"] = "C:/Android/Sdk";
145
-
146
- execaCalls.length = 0;
147
- mockFsWriteFile.mockReset();
148
- mockFsWriteFile.mockResolvedValue(undefined);
149
98
  }
150
99
 
151
- //#endregion
152
-
153
100
  let savedEnv: Record<string, string | undefined>;
154
101
  beforeEach(() => {
155
102
  savedEnv = { ...process.env };
@@ -204,199 +151,15 @@ describe("Capacitor 설정 검증", () => {
204
151
  });
205
152
  });
206
153
 
207
- describe("Capacitor 초기화", () => {
208
- beforeEach(() => {
209
- vi.clearAllMocks();
210
- setupDefaultMocks();
211
- });
212
-
213
- it("최초 초기화: pnpm install, cap init, cap add android를 실행한다", async () => {
214
- let androidAdded = false;
215
- mockFsxExists.mockImplementation((p: string) => {
216
- const n = p.replace(/\\/g, "/");
217
- if (n.includes(".capacitor.lock")) return false;
218
- if (n.includes("node_modules")) return false;
219
- if (n.includes("capacitor.config.ts")) return false;
220
- // _addPlatforms에서 cap add 후 _configureAndroid에서 확인할 때는 true
221
- if (n.endsWith(".capacitor/android")) {
222
- if (!androidAdded) { androidAdded = true; return false; }
223
- return true;
224
- }
225
- return true;
226
- });
227
-
228
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
229
- const cap = await Capacitor.create(PKG_PATH, {
230
- appId: "com.test.app",
231
- appName: "Test App",
232
- platform: { android: {} },
233
- });
234
- await cap.initialize();
235
-
236
- expect(execaCalls.some((c) => c.command === "pnpm" && c.args.includes("install"))).toBe(true);
237
- expect(
238
- execaCalls.some((c) => c.command === "pnpm" && c.args.includes("cap") && c.args.includes("init")),
239
- ).toBe(true);
240
- expect(
241
- execaCalls.some(
242
- (c) => c.command === "pnpm" && c.args.includes("cap") && c.args.includes("add"),
243
- ),
244
- ).toBe(true);
245
- });
246
-
247
- it("재초기화: 설정 미변경 시 pnpm install을 건너뛴다", async () => {
248
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
249
- const cap = await Capacitor.create(PKG_PATH, {
250
- appId: "com.test.app",
251
- appName: "Test App",
252
- platform: { android: {} },
253
- });
254
- await cap.initialize();
255
-
256
- expect(execaCalls.some((c) => c.command === "pnpm" && c.args.includes("install"))).toBe(false);
257
- });
258
-
259
- it("플러그인 추가: package.json에 플러그인을 추가하고 pnpm install을 실행한다", async () => {
260
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
261
-
262
- // 클라이언트 패키지의 deps에 플러그인 포함
263
- mockFsxReadJson.mockImplementation((p: string) => {
264
- const normalized = p.replace(/\\/g, "/");
265
- if (normalized.includes(".capacitor/package.json")) {
266
- return {
267
- name: "com.test.app",
268
- version: "1.0.0",
269
- dependencies: {
270
- "@capacitor/core": "^7.0.0",
271
- "@capacitor/app": "^7.0.0",
272
- "@capacitor/android": "^7.0.0",
273
- },
274
- devDependencies: {
275
- "@capacitor/cli": "^7.0.0",
276
- "@capacitor/assets": "^3.0.0",
277
- },
278
- };
279
- }
280
- return {
281
- name: "test-pkg",
282
- version: "1.0.0",
283
- dependencies: { "@capacitor/camera": "^7.0.0" },
284
- };
285
- });
286
-
287
- const cap = await Capacitor.create(PKG_PATH, {
288
- appId: "com.test.app",
289
- appName: "Test App",
290
- plugins: { "@capacitor/camera": true },
291
- platform: { android: {} },
292
- });
293
- await cap.initialize();
294
-
295
- // writeJson에 plugin이 포함되었는지 확인
296
- const writeJsonCalls = mockFsxWriteJson.mock.calls;
297
- const capPkgWrite = writeJsonCalls.find(
298
- (call) =>
299
- typeof call[0] === "string" && call[0].includes("package.json"),
300
- );
301
- expect(capPkgWrite).toBeDefined();
302
- const writtenConfig = capPkgWrite![1] as Record<string, unknown>;
303
- const deps = writtenConfig["dependencies"] as Record<string, string>;
304
- expect(deps["@capacitor/camera"]).toBe("^7.0.0");
305
- });
306
-
307
- it("플러그인 제거: package.json에서 이전 플러그인을 삭제한다", async () => {
308
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
309
-
310
- mockFsxReadJson.mockImplementation((p: string) => {
311
- const normalized = p.replace(/\\/g, "/");
312
- if (normalized.includes(".capacitor/package.json")) {
313
- return {
314
- name: "com.test.app",
315
- version: "1.0.0",
316
- dependencies: {
317
- "@capacitor/core": "^7.0.0",
318
- "@capacitor/app": "^7.0.0",
319
- "@capacitor/android": "^7.0.0",
320
- "@capacitor/camera": "^7.0.0",
321
- },
322
- devDependencies: {
323
- "@capacitor/cli": "^7.0.0",
324
- "@capacitor/assets": "^3.0.0",
325
- },
326
- };
327
- }
328
- return { name: "test-pkg", version: "1.0.0" };
329
- });
330
-
331
- // plugins가 비어있으면 camera가 제거되어야 함
332
- const cap = await Capacitor.create(PKG_PATH, {
333
- appId: "com.test.app",
334
- appName: "Test App",
335
- platform: { android: {} },
336
- });
337
- await cap.initialize();
338
-
339
- const writeJsonCalls = mockFsxWriteJson.mock.calls;
340
- const capPkgWrite = writeJsonCalls.find(
341
- (call) =>
342
- typeof call[0] === "string" && call[0].includes("package.json"),
343
- );
344
- expect(capPkgWrite).toBeDefined();
345
- const writtenConfig = capPkgWrite![1] as Record<string, unknown>;
346
- const deps = writtenConfig["dependencies"] as Record<string, string>;
347
- expect(deps["@capacitor/camera"]).toBeUndefined();
348
- });
349
- });
350
-
351
154
  describe("Android 개발 도구 감지", () => {
352
155
  beforeEach(() => {
353
156
  vi.clearAllMocks();
354
157
  setupDefaultMocks();
355
158
  });
356
159
 
357
- it("Java 21이 표준 경로에 설치되면 gradle.properties에 설정한다", async () => {
358
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
359
- const cap = await Capacitor.create(PKG_PATH, {
360
- appId: "com.test.app",
361
- appName: "Test App",
362
- platform: { android: {} },
363
- });
364
- await cap.initialize();
365
-
366
- const writeCalls = mockFsxWrite.mock.calls;
367
- const gradleWrite = writeCalls.find(
368
- (call) =>
369
- typeof call[0] === "string" &&
370
- call[0].includes("gradle.properties") &&
371
- typeof call[1] === "string" &&
372
- call[1].includes("org.gradle.java.home"),
373
- );
374
- expect(gradleWrite).toBeDefined();
375
- });
376
-
377
- it("ANDROID_HOME 환경변수로 SDK를 감지한다", async () => {
378
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
379
- const cap = await Capacitor.create(PKG_PATH, {
380
- appId: "com.test.app",
381
- appName: "Test App",
382
- platform: { android: {} },
383
- });
384
- await cap.initialize();
385
-
386
- const writeCalls = mockFsxWrite.mock.calls;
387
- const sdkWrite = writeCalls.find(
388
- (call) =>
389
- typeof call[0] === "string" &&
390
- call[0].includes("local.properties") &&
391
- typeof call[1] === "string" &&
392
- call[1].includes("sdk.dir"),
393
- );
394
- expect(sdkWrite).toBeDefined();
395
- });
396
-
397
160
  it("Android SDK 미설치 시 에러가 발생한다", async () => {
398
161
  delete process.env["ANDROID_HOME"];
399
- mockFsxExists.mockImplementation((p: string) => {
162
+ mockFsxExists.mockImplementation(((p: string) => {
400
163
  const n = p.replace(/\\/g, "/");
401
164
  if (n.includes(".capacitor.lock")) return false;
402
165
  if (n.includes("Android/Sdk")) return false;
@@ -404,7 +167,7 @@ describe("Android 개발 도구 감지", () => {
404
167
  if (n.includes("Program Files/Android")) return false;
405
168
  if (n.includes("C:/Android")) return false;
406
169
  return true;
407
- });
170
+ }) as never);
408
171
 
409
172
  const { Capacitor } = await import("../../src/capacitor/capacitor.js");
410
173
  const cap = await Capacitor.create(PKG_PATH, {
@@ -416,362 +179,3 @@ describe("Android 개발 도구 감지", () => {
416
179
  await expect(cap.initialize()).rejects.toThrow("Android SDK");
417
180
  });
418
181
  });
419
-
420
- describe("Android 네이티브 설정", () => {
421
- beforeEach(() => {
422
- vi.clearAllMocks();
423
- setupDefaultMocks();
424
- });
425
-
426
- describe("AndroidManifest.xml", () => {
427
- it("permissions를 AndroidManifest.xml에 추가한다", async () => {
428
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
429
- const cap = await Capacitor.create(PKG_PATH, {
430
- appId: "com.test.app",
431
- appName: "Test App",
432
- platform: {
433
- android: {
434
- permissions: [{ name: "CAMERA" }],
435
- },
436
- },
437
- });
438
- await cap.initialize();
439
-
440
- const writeCalls = mockFsxWrite.mock.calls;
441
- const manifestWrite = writeCalls.find(
442
- (call) =>
443
- typeof call[0] === "string" &&
444
- call[0].includes("AndroidManifest.xml") &&
445
- typeof call[1] === "string" &&
446
- call[1].includes("android.permission.CAMERA"),
447
- );
448
- expect(manifestWrite).toBeDefined();
449
- });
450
-
451
- it("usesCleartextTraffic을 자동 추가한다", async () => {
452
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
453
- const cap = await Capacitor.create(PKG_PATH, {
454
- appId: "com.test.app",
455
- appName: "Test App",
456
- platform: { android: {} },
457
- });
458
- await cap.initialize();
459
-
460
- const writeCalls = mockFsxWrite.mock.calls;
461
- const manifestWrite = writeCalls.find(
462
- (call) =>
463
- typeof call[0] === "string" &&
464
- call[0].includes("AndroidManifest.xml") &&
465
- typeof call[1] === "string" &&
466
- call[1].includes("usesCleartextTraffic"),
467
- );
468
- expect(manifestWrite).toBeDefined();
469
- });
470
-
471
- it("intentFilters를 MainActivity에 추가한다", async () => {
472
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
473
- const cap = await Capacitor.create(PKG_PATH, {
474
- appId: "com.test.app",
475
- appName: "Test App",
476
- platform: {
477
- android: {
478
- intentFilters: [{ action: "android.intent.action.VIEW" }],
479
- },
480
- },
481
- });
482
- await cap.initialize();
483
-
484
- const writeCalls = mockFsxWrite.mock.calls;
485
- const manifestWrite = writeCalls.find(
486
- (call) =>
487
- typeof call[0] === "string" &&
488
- call[0].includes("AndroidManifest.xml") &&
489
- typeof call[1] === "string" &&
490
- call[1].includes("android.intent.action.VIEW"),
491
- );
492
- expect(manifestWrite).toBeDefined();
493
- });
494
-
495
- it("application 태그에 커스텀 속성을 추가한다", async () => {
496
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
497
- const cap = await Capacitor.create(PKG_PATH, {
498
- appId: "com.test.app",
499
- appName: "Test App",
500
- platform: {
501
- android: { config: { label: "Custom Label" } },
502
- },
503
- });
504
- await cap.initialize();
505
-
506
- const writeCalls = mockFsxWrite.mock.calls;
507
- const manifestWrite = writeCalls.find(
508
- (call) =>
509
- typeof call[0] === "string" &&
510
- call[0].includes("AndroidManifest.xml") &&
511
- typeof call[1] === "string" &&
512
- call[1].includes('android:label="Custom Label"'),
513
- );
514
- expect(manifestWrite).toBeDefined();
515
- });
516
- });
517
-
518
- describe("build.gradle", () => {
519
- it("sdkVersion을 build.gradle에 설정한다", async () => {
520
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
521
- const cap = await Capacitor.create(PKG_PATH, {
522
- appId: "com.test.app",
523
- appName: "Test App",
524
- platform: { android: { sdkVersion: 33 } },
525
- });
526
- await cap.initialize();
527
-
528
- const writeCalls = mockFsxWrite.mock.calls;
529
- const gradleWrite = writeCalls.find(
530
- (call) =>
531
- typeof call[0] === "string" &&
532
- call[0].includes("build.gradle") &&
533
- typeof call[1] === "string" &&
534
- call[1].includes("minSdkVersion 33"),
535
- );
536
- expect(gradleWrite).toBeDefined();
537
- });
538
-
539
- it("versionCode를 package.json version으로 계산한다", async () => {
540
- mockFsxReadJson.mockImplementation((p: string) => {
541
- const normalized = p.replace(/\\/g, "/");
542
- if (normalized.includes(".capacitor/package.json")) {
543
- return {
544
- name: "com.test.app",
545
- version: "1.2.3",
546
- dependencies: { "@capacitor/core": "^7.0.0", "@capacitor/app": "^7.0.0", "@capacitor/android": "^7.0.0" },
547
- devDependencies: { "@capacitor/cli": "^7.0.0", "@capacitor/assets": "^3.0.0" },
548
- };
549
- }
550
- return { name: "test-pkg", version: "1.2.3" };
551
- });
552
-
553
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
554
- const cap = await Capacitor.create(PKG_PATH, {
555
- appId: "com.test.app",
556
- appName: "Test App",
557
- platform: { android: {} },
558
- });
559
- await cap.initialize();
560
-
561
- const writeCalls = mockFsxWrite.mock.calls;
562
- const gradleWrite = writeCalls.find(
563
- (call) =>
564
- typeof call[0] === "string" &&
565
- call[0].includes("build.gradle") &&
566
- typeof call[1] === "string" &&
567
- call[1].includes("versionCode 1002003"),
568
- );
569
- expect(gradleWrite).toBeDefined();
570
- });
571
- });
572
-
573
- describe("styles.xml", () => {
574
- it("styles.xml의 Theme.SplashScreen parent를 변경하고 android:background를 android:windowBackground로 변경한다", async () => {
575
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
576
- const cap = await Capacitor.create(PKG_PATH, {
577
- appId: "com.test.app",
578
- appName: "Test App",
579
- platform: { android: {} },
580
- });
581
- await cap.initialize();
582
-
583
- const writeCalls = mockFsxWrite.mock.calls;
584
- const stylesWrite = writeCalls.find(
585
- (call) =>
586
- typeof call[0] === "string" &&
587
- call[0].includes("styles.xml") &&
588
- typeof call[1] === "string" &&
589
- call[1].includes('parent="Theme.AppCompat.DayNight.NoActionBar"'),
590
- );
591
- expect(stylesWrite).toBeDefined();
592
- const content = stylesWrite![1] as string;
593
- // @drawable/splash는 유지
594
- expect(content).toContain("@drawable/splash");
595
- // Theme.SplashScreen은 제거됨
596
- expect(content).not.toContain('parent="Theme.SplashScreen"');
597
- // android:background → android:windowBackground로 변경됨
598
- expect(content).toContain('"android:windowBackground">@drawable/splash');
599
- expect(content).not.toContain('"android:background">@drawable/splash');
600
- });
601
-
602
- it("이미 변경된 styles.xml은 재변경하지 않는다", async () => {
603
- mockFsxRead.mockImplementation((p: string) => {
604
- if (p.includes("styles.xml")) {
605
- return `<?xml version="1.0" encoding="utf-8"?>
606
- <resources>
607
- <style name="AppTheme.NoActionBarLaunch" parent="Theme.AppCompat.DayNight.NoActionBar">
608
- <item name="android:windowBackground">@drawable/splash</item>
609
- </style>
610
- </resources>`;
611
- }
612
- if (p.includes("AndroidManifest.xml")) {
613
- return '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n<application>\n<activity android:name=".MainActivity">\n</activity>\n</application>\n</manifest>';
614
- }
615
- if (p.includes("build.gradle")) {
616
- return `android {
617
- defaultConfig {
618
- versionCode 1
619
- versionName "1.0"
620
- minSdkVersion rootProject.ext.minSdkVersion
621
- targetSdkVersion rootProject.ext.targetSdkVersion
622
- }
623
- buildTypes { release { } }
624
- }`;
625
- }
626
- if (p.includes("gradle.properties")) {
627
- return "org.gradle.jvmargs=-Xmx2048m";
628
- }
629
- return "";
630
- });
631
-
632
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
633
- const cap = await Capacitor.create(PKG_PATH, {
634
- appId: "com.test.app",
635
- appName: "Test App",
636
- platform: { android: {} },
637
- });
638
- await cap.initialize();
639
-
640
- const writeCalls = mockFsxWrite.mock.calls;
641
- const stylesWrite = writeCalls.find(
642
- (call) =>
643
- typeof call[0] === "string" &&
644
- call[0].includes("styles.xml"),
645
- );
646
- expect(stylesWrite).toBeUndefined();
647
- });
648
-
649
- it("styles.xml이 없으면 warn을 출력한다", async () => {
650
- mockFsxExists.mockImplementation((p: string) => {
651
- const n = p.replace(/\\/g, "/");
652
- if (n.includes(".capacitor.lock")) return false;
653
- if (n.includes("styles.xml")) return false;
654
- return true;
655
- });
656
-
657
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
658
- const cap = await Capacitor.create(PKG_PATH, {
659
- appId: "com.test.app",
660
- appName: "Test App",
661
- platform: { android: {} },
662
- });
663
- await cap.initialize();
664
-
665
- expect(mockLoggerWarn).toHaveBeenCalledWith(
666
- expect.stringContaining("styles.xml"),
667
- );
668
- });
669
- });
670
- });
671
-
672
- describe("플러그인 의존성 해석", () => {
673
- beforeEach(() => {
674
- vi.clearAllMocks();
675
- setupDefaultMocks();
676
- });
677
-
678
- it("일반 npm 플러그인: root package.json에서 버전을 resolve한다", async () => {
679
- mockFsxReadJson.mockImplementation((p: string) => {
680
- const normalized = p.replace(/\\/g, "/");
681
- if (normalized.includes(".capacitor/package.json")) {
682
- return {
683
- name: "com.test.app",
684
- version: "1.0.0",
685
- dependencies: { "@capacitor/core": "^7.0.0", "@capacitor/app": "^7.0.0", "@capacitor/android": "^7.0.0" },
686
- devDependencies: { "@capacitor/cli": "^7.0.0", "@capacitor/assets": "^3.0.0" },
687
- };
688
- }
689
- return {
690
- name: "test-pkg",
691
- version: "1.0.0",
692
- dependencies: { "@capacitor/camera": "^7.1.0" },
693
- };
694
- });
695
-
696
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
697
- const cap = await Capacitor.create(PKG_PATH, {
698
- appId: "com.test.app",
699
- appName: "Test App",
700
- plugins: { "@capacitor/camera": true },
701
- platform: { android: {} },
702
- });
703
- await cap.initialize();
704
-
705
- const writeJsonCalls = mockFsxWriteJson.mock.calls;
706
- const capPkgWrite = writeJsonCalls.find(
707
- (call) => typeof call[0] === "string" && call[0].includes("package.json"),
708
- );
709
- const deps = (capPkgWrite![1] as Record<string, unknown>)["dependencies"] as Record<string, string>;
710
- expect(deps["@capacitor/camera"]).toBe("^7.1.0");
711
- });
712
-
713
- it("exclude 패키지를 .capacitor/package.json에 추가한다", async () => {
714
- mockFsxReadJson.mockImplementation((p: string) => {
715
- const normalized = p.replace(/\\/g, "/");
716
- if (normalized.includes(".capacitor/package.json")) {
717
- return {
718
- name: "com.test.app",
719
- version: "1.0.0",
720
- dependencies: { "@capacitor/core": "^7.0.0", "@capacitor/app": "^7.0.0", "@capacitor/android": "^7.0.0" },
721
- devDependencies: { "@capacitor/cli": "^7.0.0", "@capacitor/assets": "^3.0.0" },
722
- };
723
- }
724
- return {
725
- name: "test-pkg",
726
- version: "1.0.0",
727
- dependencies: { "jeep-sqlite": "^2.0.0" },
728
- };
729
- });
730
-
731
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
732
- const cap = await Capacitor.create(
733
- PKG_PATH,
734
- { appId: "com.test.app", appName: "Test App", platform: { android: {} } },
735
- ["jeep-sqlite"],
736
- );
737
- await cap.initialize();
738
-
739
- const writeJsonCalls = mockFsxWriteJson.mock.calls;
740
- const capPkgWrite = writeJsonCalls.find(
741
- (call) => typeof call[0] === "string" && call[0].includes("package.json"),
742
- );
743
- const deps = (capPkgWrite![1] as Record<string, unknown>)["dependencies"] as Record<string, string>;
744
- expect(deps["jeep-sqlite"]).toBe("^2.0.0");
745
- });
746
-
747
- it("플러그인 버전이 root에 없으면 *를 사용한다", async () => {
748
- mockFsxReadJson.mockImplementation((p: string) => {
749
- const normalized = p.replace(/\\/g, "/");
750
- if (normalized.includes(".capacitor/package.json")) {
751
- return {
752
- name: "com.test.app",
753
- version: "1.0.0",
754
- dependencies: { "@capacitor/core": "^7.0.0", "@capacitor/app": "^7.0.0", "@capacitor/android": "^7.0.0" },
755
- devDependencies: { "@capacitor/cli": "^7.0.0", "@capacitor/assets": "^3.0.0" },
756
- };
757
- }
758
- return { name: "test-pkg", version: "1.0.0" };
759
- });
760
-
761
- const { Capacitor } = await import("../../src/capacitor/capacitor.js");
762
- const cap = await Capacitor.create(PKG_PATH, {
763
- appId: "com.test.app",
764
- appName: "Test App",
765
- plugins: { "unknown-plugin": true },
766
- platform: { android: {} },
767
- });
768
- await cap.initialize();
769
-
770
- const writeJsonCalls = mockFsxWriteJson.mock.calls;
771
- const capPkgWrite = writeJsonCalls.find(
772
- (call) => typeof call[0] === "string" && call[0].includes("package.json"),
773
- );
774
- const deps = (capPkgWrite![1] as Record<string, unknown>)["dependencies"] as Record<string, string>;
775
- expect(deps["unknown-plugin"]).toBe("*");
776
- });
777
- });