@nosto/nosto-cli 1.0.4 → 1.1.0
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/.github/copilot-instructions.md +2 -2
- package/.github/dependabot.yml +12 -1
- package/README.md +2 -2
- package/eslint.config.js +11 -1
- package/package.json +8 -8
- package/src/api/retry.ts +24 -8
- package/src/config/authConfig.ts +2 -2
- package/src/config/config.ts +7 -1
- package/src/errors/InvalidLoginResponseError.ts +1 -1
- package/src/errors/withErrorHandler.ts +10 -14
- package/src/filesystem/homeDirectory.ts +3 -0
- package/src/filesystem/processInBatches.ts +1 -0
- package/src/modules/login.ts +10 -4
- package/src/modules/search-templates/push.ts +1 -21
- package/src/modules/setup.ts +1 -3
- package/src/modules/status.ts +1 -1
- package/src/vite-env.d.ts +1 -0
- package/test/api/retry.test.ts +41 -1
- package/test/commander.test.ts +31 -2
- package/test/config/authConfig.test.ts +62 -0
- package/test/config/config.test.ts +71 -0
- package/test/console/logger.test.ts +16 -0
- package/test/console/userPrompt.test.ts +30 -0
- package/test/errors/withErrorHandler.test.ts +80 -5
- package/test/filesystem/asserts/assertGitRepo.test.ts +35 -0
- package/test/filesystem/asserts/assertNostoTemplate.test.ts +53 -0
- package/test/filesystem/filesystem.test.ts +17 -1
- package/test/filesystem/plugins.test.ts +73 -2
- package/test/filesystem/processInBatches.test.ts +34 -0
- package/test/modules/login.test.ts +38 -0
- package/test/modules/logout.test.ts +24 -0
- package/test/modules/search-templates/build.legacy.test.ts +25 -0
- package/test/modules/search-templates/build.modern.test.ts +5 -1
- package/test/modules/search-templates/dev.legacy.test.ts +23 -0
- package/test/modules/search-templates/pull.test.ts +38 -1
- package/test/modules/search-templates/push.test.ts +145 -1
- package/test/modules/setup.test.ts +16 -0
- package/test/modules/status.test.ts +45 -4
- package/test/setup.ts +11 -0
- package/test/utils/generateEndpointMock.ts +10 -3
- package/test/utils/mockAuthServer.ts +72 -0
- package/test/utils/mockConsole.ts +20 -0
- package/test/utils/mockFileSystem.ts +46 -12
- package/test/utils/mockStarterManifest.ts +3 -2
- package/vitest.config.ts +13 -5
|
@@ -93,4 +93,20 @@ describe("Logger", () => {
|
|
|
93
93
|
Logger.error("Error message", new Error("Test error"))
|
|
94
94
|
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining("Error: Test error"))
|
|
95
95
|
})
|
|
96
|
+
|
|
97
|
+
it("prints merchant id when set in context", () => {
|
|
98
|
+
const consoleMock = vi.spyOn(console, "info").mockImplementation(() => undefined)
|
|
99
|
+
Logger.context.merchantId = "merchant123"
|
|
100
|
+
Logger.context.logLevel = "info"
|
|
101
|
+
Logger.info("Info message")
|
|
102
|
+
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining("[merchant123]"))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("prints the dry run label when isDryRun is true", () => {
|
|
106
|
+
const consoleMock = vi.spyOn(console, "info").mockImplementation(() => undefined)
|
|
107
|
+
Logger.context.isDryRun = true
|
|
108
|
+
Logger.context.logLevel = "info"
|
|
109
|
+
Logger.info("Info message")
|
|
110
|
+
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining("(DRY RUN)"))
|
|
111
|
+
})
|
|
96
112
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { promptForConfirmation } from "#console/userPrompt.ts"
|
|
4
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
5
|
+
|
|
6
|
+
const terminal = setupMockConsole()
|
|
7
|
+
|
|
8
|
+
describe("promptForConfirmation", () => {
|
|
9
|
+
it("should accept positive confirmation", async () => {
|
|
10
|
+
terminal.setUserResponse("y")
|
|
11
|
+
const response = await promptForConfirmation("Test prompt?", "Y")
|
|
12
|
+
expect(response).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should accept negative confirmation", async () => {
|
|
16
|
+
terminal.setUserResponse("n")
|
|
17
|
+
const response = await promptForConfirmation("Test prompt?", "Y")
|
|
18
|
+
expect(response).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("should use default confirmation when input is empty", async () => {
|
|
22
|
+
terminal.setUserResponse("")
|
|
23
|
+
const responseYes = await promptForConfirmation("Test prompt?", "Y")
|
|
24
|
+
expect(responseYes).toBe(true)
|
|
25
|
+
|
|
26
|
+
terminal.setUserResponse("")
|
|
27
|
+
const responseNo = await promptForConfirmation("Test prompt?", "N")
|
|
28
|
+
expect(responseNo).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -1,17 +1,59 @@
|
|
|
1
1
|
import { HTTPError, TimeoutError } from "ky"
|
|
2
|
-
import { describe, expect, it, vi } from "vitest"
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
3
3
|
|
|
4
|
+
import { clearCachedConfig } from "#config/config.ts"
|
|
5
|
+
import { InvalidLoginResponseError } from "#errors/InvalidLoginResponseError.ts"
|
|
4
6
|
import { MissingConfigurationError } from "#errors/MissingConfigurationError.ts"
|
|
5
|
-
import {
|
|
7
|
+
import { NostoError } from "#errors/NostoError.ts"
|
|
8
|
+
import { NotNostoTemplateError } from "#errors/NotNostoTemplateError.ts"
|
|
9
|
+
import { prettyPrintStack, withErrorHandler } from "#errors/withErrorHandler.ts"
|
|
6
10
|
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
11
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
12
|
+
|
|
13
|
+
const terminal = setupMockConsole()
|
|
7
14
|
|
|
8
15
|
describe("Error Handler", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
terminal.resetMocks()
|
|
18
|
+
clearCachedConfig()
|
|
19
|
+
})
|
|
9
20
|
it("should execute function without error handling if no error occurs", async () => {
|
|
10
21
|
const mockFn = vi.fn()
|
|
11
22
|
|
|
12
23
|
await withErrorHandler(mockFn)
|
|
13
24
|
|
|
14
25
|
expect(mockFn).toHaveBeenCalled()
|
|
26
|
+
expect(terminal.getSpy("error")).not.toHaveBeenCalled()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("should handle NostoError", async () => {
|
|
30
|
+
const mockFn = vi.fn(() => {
|
|
31
|
+
throw new NostoError("Test Nosto error")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(() => withErrorHandler(mockFn)).not.toThrow()
|
|
35
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith("- Test Nosto error")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("should handle NotNostoTemplateError", async () => {
|
|
39
|
+
const mockFn = vi.fn(() => {
|
|
40
|
+
throw new NotNostoTemplateError("Test Nosto error")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(() => withErrorHandler(mockFn)).not.toThrow()
|
|
44
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith("- Test Nosto error")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("should handle InvalidLoginResponseError", async () => {
|
|
48
|
+
const mockFn = vi.fn(() => {
|
|
49
|
+
throw new InvalidLoginResponseError("Test Nosto error")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(() => withErrorHandler(mockFn)).not.toThrow()
|
|
53
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith(
|
|
54
|
+
"Received malformed login response from server. This is probably a bug on our side.",
|
|
55
|
+
expect.any(InvalidLoginResponseError)
|
|
56
|
+
)
|
|
15
57
|
})
|
|
16
58
|
|
|
17
59
|
it("should handle MissingConfigurationError", async () => {
|
|
@@ -19,11 +61,11 @@ describe("Error Handler", () => {
|
|
|
19
61
|
throw new MissingConfigurationError("Test config error")
|
|
20
62
|
})
|
|
21
63
|
|
|
22
|
-
|
|
64
|
+
expect(() => withErrorHandler(mockFn)).not.toThrow()
|
|
65
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith("Test config error")
|
|
23
66
|
})
|
|
24
67
|
|
|
25
68
|
it("should handle HTTPError", async () => {
|
|
26
|
-
setupMockConfig({ verbose: true })
|
|
27
69
|
const mockFn = vi.fn(() => {
|
|
28
70
|
throw new HTTPError(
|
|
29
71
|
{
|
|
@@ -38,10 +80,23 @@ describe("Error Handler", () => {
|
|
|
38
80
|
)
|
|
39
81
|
})
|
|
40
82
|
|
|
41
|
-
|
|
83
|
+
expect(() => withErrorHandler(mockFn)).not.toThrow()
|
|
84
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith("- GET https://example.com/api")
|
|
42
85
|
})
|
|
43
86
|
|
|
44
87
|
it("should handle HTTPError", async () => {
|
|
88
|
+
const mockFn = vi.fn(() => {
|
|
89
|
+
throw new TimeoutError({
|
|
90
|
+
method: "GET",
|
|
91
|
+
url: "https://example.com/api"
|
|
92
|
+
} as Request)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(() => withErrorHandler(mockFn)).not.toThrow()
|
|
96
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith("HTTP Request timed out:")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("should print out stack trace", async () => {
|
|
45
100
|
setupMockConfig({ verbose: true })
|
|
46
101
|
const mockFn = vi.fn(() => {
|
|
47
102
|
throw new TimeoutError({
|
|
@@ -51,6 +106,22 @@ describe("Error Handler", () => {
|
|
|
51
106
|
})
|
|
52
107
|
|
|
53
108
|
await withErrorHandler(mockFn)
|
|
109
|
+
expect(terminal.getSpy("raw")).toHaveBeenCalledWith(expect.stringContaining("withErrorHandler.test.ts:"))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("should print out verbosity note", async () => {
|
|
113
|
+
setupMockConfig({ verbose: false, logLevel: "info" })
|
|
114
|
+
terminal.setContext({ logLevel: "info" })
|
|
115
|
+
const mockFn = vi.fn(() => {
|
|
116
|
+
throw new TimeoutError({
|
|
117
|
+
method: "GET",
|
|
118
|
+
url: "https://example.com/api"
|
|
119
|
+
} as Request)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await withErrorHandler(mockFn)
|
|
123
|
+
expect(terminal.getSpy("raw")).not.toHaveBeenCalledWith(expect.stringContaining("withErrorHandler.test.ts:"))
|
|
124
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(expect.stringContaining("Rerun with --verbose to see details"))
|
|
54
125
|
})
|
|
55
126
|
|
|
56
127
|
it("should rethrow unknown errors", async () => {
|
|
@@ -61,4 +132,8 @@ describe("Error Handler", () => {
|
|
|
61
132
|
|
|
62
133
|
await expect(withErrorHandler(mockFn)).rejects.toThrow("Unknown error")
|
|
63
134
|
})
|
|
135
|
+
|
|
136
|
+
it("handles error without stack gracefully", async () => {
|
|
137
|
+
expect(() => prettyPrintStack(undefined)).not.toThrow()
|
|
138
|
+
})
|
|
64
139
|
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { assertGitRepo } from "#filesystem/asserts/assertGitRepo.ts"
|
|
5
|
+
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
6
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
7
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
8
|
+
|
|
9
|
+
const fs = setupMockFileSystem()
|
|
10
|
+
const terminal = setupMockConsole()
|
|
11
|
+
|
|
12
|
+
describe("assertGitRepo", () => {
|
|
13
|
+
it("prints a warning if not in a git repository", () => {
|
|
14
|
+
setupMockConfig({ projectPath: "." })
|
|
15
|
+
assertGitRepo()
|
|
16
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith(
|
|
17
|
+
`We heavily recommend using git for your projects. You can start with just running ${chalk.blueBright("git init")}`
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("prints a warning if not in a git repository and not in project folder", () => {
|
|
22
|
+
setupMockConfig({ projectPath: "/project" })
|
|
23
|
+
assertGitRepo()
|
|
24
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith(
|
|
25
|
+
`We heavily recommend using git for your projects. You can start with just running ${chalk.blueBright("cd /project && git init")}`
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("does not print a warning if in a git repository", () => {
|
|
30
|
+
setupMockConfig({ projectPath: "." })
|
|
31
|
+
fs.writeFolder(".git")
|
|
32
|
+
assertGitRepo()
|
|
33
|
+
expect(terminal.getSpy("warn")).not.toHaveBeenCalled()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { NotNostoTemplateError } from "#errors/NotNostoTemplateError.ts"
|
|
4
|
+
import { assertNostoTemplate } from "#filesystem/asserts/assertNostoTemplate.ts"
|
|
5
|
+
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
6
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
7
|
+
|
|
8
|
+
const fs = setupMockFileSystem()
|
|
9
|
+
|
|
10
|
+
describe("assertNostoTemplate", () => {
|
|
11
|
+
it("throws error when target folder does not exist", () => {
|
|
12
|
+
setupMockConfig({ projectPath: "/nonexistent" })
|
|
13
|
+
|
|
14
|
+
expect(() => assertNostoTemplate()).toThrow(/Target folder does not exist/)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("throws error when target path is not a directory", () => {
|
|
18
|
+
setupMockConfig({ projectPath: "/file.txt" })
|
|
19
|
+
fs.writeFile("/file.txt", "content")
|
|
20
|
+
|
|
21
|
+
expect(() => assertNostoTemplate()).toThrow(/Target path is not a directory/)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("returns early for modern template projects", () => {
|
|
25
|
+
fs.writeFile("nosto.config.ts", "export default {}")
|
|
26
|
+
|
|
27
|
+
expect(() => assertNostoTemplate()).not.toThrow()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("throws NotNostoTemplateError when index.js does not exist in legacy project", () => {
|
|
31
|
+
expect(() => assertNostoTemplate()).toThrow(NotNostoTemplateError)
|
|
32
|
+
expect(() => assertNostoTemplate()).toThrow("Index file does not exist")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("throws NotNostoTemplateError when index.js does not contain @nosto/preact", () => {
|
|
36
|
+
fs.writeFile("index.js", "console.log('hello world')")
|
|
37
|
+
|
|
38
|
+
expect(() => assertNostoTemplate()).toThrow(NotNostoTemplateError)
|
|
39
|
+
expect(() => assertNostoTemplate()).toThrow("Index file does not contain @nosto/preact")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("succeeds for valid legacy template project with @nosto/preact", () => {
|
|
43
|
+
fs.writeFile("index.js", "import { render } from '@nosto/preact'")
|
|
44
|
+
|
|
45
|
+
expect(() => assertNostoTemplate()).not.toThrow()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("succeeds for legacy template project with @nosto/preact in require format", () => {
|
|
49
|
+
fs.writeFile("index.js", "require('@nosto/preact')")
|
|
50
|
+
|
|
51
|
+
expect(() => assertNostoTemplate()).not.toThrow()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
|
|
3
|
-
import { listAllFiles, writeFile } from "#filesystem/filesystem.ts"
|
|
3
|
+
import { listAllFiles, readFileIfExists, writeFile } from "#filesystem/filesystem.ts"
|
|
4
4
|
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
5
5
|
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
6
6
|
|
|
@@ -50,4 +50,20 @@ describe("Filesystem", () => {
|
|
|
50
50
|
unfilteredFileCount: 2
|
|
51
51
|
})
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
describe("readFileIfExists", () => {
|
|
55
|
+
it("should read file content if file exists", () => {
|
|
56
|
+
const filePath = "readable.txt"
|
|
57
|
+
const fileContent = "This is readable content."
|
|
58
|
+
mockFileSystem.writeFile(filePath, fileContent)
|
|
59
|
+
|
|
60
|
+
const content = readFileIfExists(filePath)
|
|
61
|
+
expect(content).toBe(fileContent)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should return null if file does not exist", () => {
|
|
65
|
+
const content = readFileIfExists("nonexistent.txt")
|
|
66
|
+
expect(content).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
53
69
|
})
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { PluginBuild } from "esbuild"
|
|
2
|
-
import { describe, it } from "vitest"
|
|
2
|
+
import { describe, expect, it, vi } from "vitest"
|
|
3
3
|
|
|
4
|
-
import { createLoaderPlugin } from "#filesystem/esbuildPlugins.ts"
|
|
4
|
+
import { createLoaderPlugin, notifyOnRebuildPlugin, pushOnRebuildPlugin } from "#filesystem/esbuildPlugins.ts"
|
|
5
|
+
import * as push from "#modules/search-templates/push.ts"
|
|
6
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
5
7
|
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
6
8
|
|
|
7
9
|
const fs = setupMockFileSystem()
|
|
10
|
+
const terminal = setupMockConsole()
|
|
8
11
|
|
|
9
12
|
describe("createLoaderPlugin", () => {
|
|
10
13
|
it("creates loader after successful build", () => {
|
|
@@ -33,3 +36,71 @@ describe("createLoaderPlugin", () => {
|
|
|
33
36
|
fs.expectFile("build/loader.js").not.toExist()
|
|
34
37
|
})
|
|
35
38
|
})
|
|
39
|
+
|
|
40
|
+
describe("notifyOnRebuildPlugin", () => {
|
|
41
|
+
it("logs build duration after successful build", () => {
|
|
42
|
+
const plugin = notifyOnRebuildPlugin()
|
|
43
|
+
plugin.setup({
|
|
44
|
+
onStart: callback => {
|
|
45
|
+
callback()
|
|
46
|
+
},
|
|
47
|
+
onEnd(callback: (result: { errors: unknown[] }) => void) {
|
|
48
|
+
callback({ errors: [] })
|
|
49
|
+
}
|
|
50
|
+
} as PluginBuild)
|
|
51
|
+
|
|
52
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(expect.stringMatching(/Templates built in \d+ ms\./))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("exits early on failure", () => {
|
|
56
|
+
const plugin = notifyOnRebuildPlugin()
|
|
57
|
+
plugin.setup({
|
|
58
|
+
onStart: (_: unknown) => {},
|
|
59
|
+
onEnd(callback: (result: { errors: unknown[] }) => void) {
|
|
60
|
+
callback({ errors: [new Error("Test error")] })
|
|
61
|
+
}
|
|
62
|
+
} as PluginBuild)
|
|
63
|
+
|
|
64
|
+
expect(terminal.getSpy("info")).not.toHaveBeenCalledWith(expect.stringMatching(/Templates built in \d+ ms\./))
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe("pushOnRebuildPlugin", () => {
|
|
69
|
+
it("pushes search templates after successful build", () => {
|
|
70
|
+
const pushSpy = vi.spyOn(push, "pushSearchTemplate").mockImplementation(() => Promise.resolve())
|
|
71
|
+
|
|
72
|
+
fs.writeFolder("build")
|
|
73
|
+
fs.writeFile("build/index.js", "file content")
|
|
74
|
+
fs.writeFile("build/loader.js", "file content")
|
|
75
|
+
|
|
76
|
+
const plugin = pushOnRebuildPlugin()
|
|
77
|
+
plugin.setup({
|
|
78
|
+
onEnd(callback: (result: { errors: unknown[] }) => void) {
|
|
79
|
+
callback({ errors: [] })
|
|
80
|
+
}
|
|
81
|
+
} as PluginBuild)
|
|
82
|
+
|
|
83
|
+
expect(pushSpy).toHaveBeenCalledWith(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
paths: ["build/index.js", "build/loader.js"]
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("exits early on failure", () => {
|
|
91
|
+
const pushSpy = vi.spyOn(push, "pushSearchTemplate").mockImplementation(() => Promise.resolve())
|
|
92
|
+
|
|
93
|
+
fs.writeFolder("build")
|
|
94
|
+
fs.writeFile("build/index.js", "file content")
|
|
95
|
+
fs.writeFile("build/loader.js", "file content")
|
|
96
|
+
|
|
97
|
+
const plugin = pushOnRebuildPlugin()
|
|
98
|
+
plugin.setup({
|
|
99
|
+
onEnd(callback: (result: { errors: unknown[] }) => void) {
|
|
100
|
+
callback({ errors: [new Error("Test error")] })
|
|
101
|
+
}
|
|
102
|
+
} as PluginBuild)
|
|
103
|
+
|
|
104
|
+
expect(pushSpy).not.toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { processInBatches } from "#filesystem/processInBatches.ts"
|
|
4
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
5
|
+
|
|
6
|
+
const terminal = setupMockConsole()
|
|
7
|
+
|
|
8
|
+
describe("processInBatches", () => {
|
|
9
|
+
it("handles thrown errors gracefully", async () => {
|
|
10
|
+
await processInBatches({
|
|
11
|
+
files: ["first.js"],
|
|
12
|
+
logIcon: "",
|
|
13
|
+
processElement: async () => {
|
|
14
|
+
throw new Error("Test error")
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith(expect.stringContaining("✗ first.js: Test error"))
|
|
19
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith(expect.stringContaining("Batch completed with 1 failures"))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("handles thrown ducks gracefully", async () => {
|
|
23
|
+
await processInBatches({
|
|
24
|
+
files: ["first.js"],
|
|
25
|
+
logIcon: "",
|
|
26
|
+
processElement: async () => {
|
|
27
|
+
throw "a duck"
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(terminal.getSpy("error")).toHaveBeenCalledWith(expect.stringContaining("✗ first.js: a duck"))
|
|
32
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith(expect.stringContaining("Batch completed with 1 failures"))
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { AuthConfigFilePath } from "#config/authConfig.ts"
|
|
4
|
+
import { loginToPlaycart } from "#modules/login.ts"
|
|
5
|
+
import { setupMockAuthServer } from "#test/utils/mockAuthServer.ts"
|
|
6
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
7
|
+
|
|
8
|
+
const fs = setupMockFileSystem()
|
|
9
|
+
const authServer = setupMockAuthServer()
|
|
10
|
+
|
|
11
|
+
describe("Login Module", () => {
|
|
12
|
+
it("authenticates successfully", async () => {
|
|
13
|
+
const authConfig = {
|
|
14
|
+
user: "test-user",
|
|
15
|
+
token: "test-token",
|
|
16
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
authServer.mockValidResponse(authConfig)
|
|
20
|
+
await loginToPlaycart()
|
|
21
|
+
fs.expectFile(AuthConfigFilePath).toContain(JSON.stringify(authConfig, null, 2) + "\n")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("throws on invalid response", async () => {
|
|
25
|
+
authServer.mockGenericResponse({ url: "/?invalidParam=invalid" })
|
|
26
|
+
await expect(loginToPlaycart()).rejects.toThrow(/Failed to parse playcart response/)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("throws on empty url in response", async () => {
|
|
30
|
+
authServer.mockGenericResponse({})
|
|
31
|
+
await expect(loginToPlaycart()).rejects.toThrow(/Failed to parse playcart response/)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("throws if unable to bind to a port", async () => {
|
|
35
|
+
authServer.mockServerAddress(null)
|
|
36
|
+
await expect(loginToPlaycart()).rejects.toThrow("Failed to get server address")
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { AuthConfigFilePath } from "#config/authConfig.ts"
|
|
4
|
+
import { removeLoginCredentials } from "#modules/logout.ts"
|
|
5
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
6
|
+
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
7
|
+
|
|
8
|
+
const fs = setupMockFileSystem()
|
|
9
|
+
const terminal = setupMockConsole()
|
|
10
|
+
|
|
11
|
+
describe("Logout Module", () => {
|
|
12
|
+
it("deletes the stored credentials", () => {
|
|
13
|
+
fs.writeFile(AuthConfigFilePath, "some-credentials")
|
|
14
|
+
|
|
15
|
+
removeLoginCredentials()
|
|
16
|
+
|
|
17
|
+
fs.expectFile(AuthConfigFilePath).not.toExist()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("warns if the credentials file does not exist", () => {
|
|
21
|
+
removeLoginCredentials()
|
|
22
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith("File already deleted.")
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -3,9 +3,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
3
3
|
|
|
4
4
|
import { buildSearchTemplate } from "#modules/search-templates/build.ts"
|
|
5
5
|
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
6
|
+
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
6
7
|
import { mockFetchLibraryFile, setupMockServer } from "#test/utils/mockServer.ts"
|
|
7
8
|
|
|
8
9
|
const server = setupMockServer()
|
|
10
|
+
const terminal = setupMockConsole()
|
|
9
11
|
|
|
10
12
|
vi.mock("esbuild", () => ({
|
|
11
13
|
context: vi.fn()
|
|
@@ -70,5 +72,28 @@ describe("Search Templates build / legacy", () => {
|
|
|
70
72
|
|
|
71
73
|
processOnSpy.mockRestore()
|
|
72
74
|
})
|
|
75
|
+
|
|
76
|
+
it("should handle SIGINT signal correctly", async () => {
|
|
77
|
+
const processOnSpy = vi.spyOn(process, "on").mockImplementation(() => process)
|
|
78
|
+
const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never)
|
|
79
|
+
|
|
80
|
+
await buildSearchTemplate({ watch: true })
|
|
81
|
+
|
|
82
|
+
// Get the SIGINT handler that was registered
|
|
83
|
+
const sigintCall = processOnSpy.mock.calls.find(call => call[0] === "SIGINT")
|
|
84
|
+
const sigintHandler = sigintCall?.[1] as () => void
|
|
85
|
+
expect(sigintHandler).toBeDefined()
|
|
86
|
+
|
|
87
|
+
// Simulate SIGINT signal
|
|
88
|
+
sigintHandler()
|
|
89
|
+
|
|
90
|
+
// Verify the handler behavior
|
|
91
|
+
expect(mockContext.dispose).toHaveBeenCalled()
|
|
92
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(expect.stringContaining("Watch mode stopped."))
|
|
93
|
+
expect(processExitSpy).toHaveBeenCalledWith(0)
|
|
94
|
+
|
|
95
|
+
processOnSpy.mockRestore()
|
|
96
|
+
processExitSpy.mockRestore()
|
|
97
|
+
})
|
|
73
98
|
})
|
|
74
99
|
})
|
|
@@ -22,7 +22,11 @@ describe("Search Templates build / modern", () => {
|
|
|
22
22
|
|
|
23
23
|
it("should build templates with watch mode", async () => {
|
|
24
24
|
const manifest = setupMockStarterManifest({
|
|
25
|
-
mockScript: {
|
|
25
|
+
mockScript: {
|
|
26
|
+
onBuildWatch: vi.fn().mockImplementation(async ({ onAfterBuild }) => {
|
|
27
|
+
await onAfterBuild()
|
|
28
|
+
})
|
|
29
|
+
}
|
|
26
30
|
})
|
|
27
31
|
setupMockConfig({
|
|
28
32
|
searchTemplates: await parseSearchTemplatesConfigFile({ projectPath: "." })
|
|
@@ -65,6 +65,29 @@ describe("Search Templates dev mode / legacy", () => {
|
|
|
65
65
|
processOnSpy.mockRestore()
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
+
it("should handle SIGINT signal correctly", async () => {
|
|
69
|
+
const processOnSpy = vi.spyOn(process, "on").mockImplementation(() => process)
|
|
70
|
+
const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never)
|
|
71
|
+
|
|
72
|
+
await searchTemplateDevMode()
|
|
73
|
+
|
|
74
|
+
// Get the SIGINT handler that was registered
|
|
75
|
+
const sigintCall = processOnSpy.mock.calls.find(call => call[0] === "SIGINT")
|
|
76
|
+
const sigintHandler = sigintCall?.[1] as () => void
|
|
77
|
+
expect(sigintHandler).toBeDefined()
|
|
78
|
+
|
|
79
|
+
// Simulate SIGINT signal
|
|
80
|
+
sigintHandler()
|
|
81
|
+
|
|
82
|
+
// Verify the handler behavior
|
|
83
|
+
expect(mockContext.dispose).toHaveBeenCalled()
|
|
84
|
+
expect(mockConsole.getSpy("info")).toHaveBeenCalledWith(expect.stringContaining("Watch mode stopped."))
|
|
85
|
+
expect(processExitSpy).toHaveBeenCalledWith(0)
|
|
86
|
+
|
|
87
|
+
processOnSpy.mockRestore()
|
|
88
|
+
processExitSpy.mockRestore()
|
|
89
|
+
})
|
|
90
|
+
|
|
68
91
|
it("should have pulled the library", async () => {
|
|
69
92
|
await searchTemplateDevMode()
|
|
70
93
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it } from "vitest"
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
2
|
|
|
3
3
|
import { pullSearchTemplate } from "#modules/search-templates/pull.ts"
|
|
4
4
|
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
@@ -31,6 +31,16 @@ describe("Pull Search Template", () => {
|
|
|
31
31
|
fs.expectFile("wizard.js").toContain('"wizard.js content"')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
+
it("should copy the hash as the new last seen hash", async () => {
|
|
35
|
+
mockListSourceFiles(server, {
|
|
36
|
+
response: []
|
|
37
|
+
})
|
|
38
|
+
fs.writeFile("build/hash", "123")
|
|
39
|
+
|
|
40
|
+
await pullSearchTemplate({ paths: [], force: true })
|
|
41
|
+
fs.expectFile(".nostocache/hash").toContain("123")
|
|
42
|
+
})
|
|
43
|
+
|
|
34
44
|
it("should filter by specified paths", async () => {
|
|
35
45
|
mockListSourceFiles(server, {
|
|
36
46
|
response: [
|
|
@@ -66,6 +76,21 @@ describe("Pull Search Template", () => {
|
|
|
66
76
|
terminal.setUserResponse("N")
|
|
67
77
|
await pullSearchTemplate({ paths: [], force: false })
|
|
68
78
|
terminal.expect.user.toHaveBeenPromptedWith("Are you sure you want to override your local data? (y/N):")
|
|
79
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith(expect.stringContaining("index.js"))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("should prompt for confirmation when many files will be overridden", async () => {
|
|
83
|
+
const files = Array.from({ length: 15 }, (_, i) => {
|
|
84
|
+
fs.writeFile(`file${i}.js`, "old content")
|
|
85
|
+
return { path: `file${i}.js`, size: 10 }
|
|
86
|
+
})
|
|
87
|
+
mockListSourceFiles(server, {
|
|
88
|
+
response: files
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
terminal.setUserResponse("N")
|
|
92
|
+
await pullSearchTemplate({ paths: [], force: false })
|
|
93
|
+
expect(terminal.getSpy("warn")).toHaveBeenCalledWith(expect.stringContaining("and 5 more"))
|
|
69
94
|
})
|
|
70
95
|
|
|
71
96
|
it("should cancel operation when user declines override", async () => {
|
|
@@ -93,4 +118,16 @@ describe("Pull Search Template", () => {
|
|
|
93
118
|
await pullSearchTemplate({ paths: [], force: false })
|
|
94
119
|
fs.expectFile("index.js").toContain('"index.js content"')
|
|
95
120
|
})
|
|
121
|
+
|
|
122
|
+
it("should abort if the remote template is already up to date", async () => {
|
|
123
|
+
fs.writeFile("index.js", "old content")
|
|
124
|
+
mockFetchSourceFile(server, {
|
|
125
|
+
path: "build/hash",
|
|
126
|
+
response: "34a780ad578b997db55b260beb60b501f3e04d30ba1a51fcf43cd8dd1241780d",
|
|
127
|
+
contentType: "raw"
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await pullSearchTemplate({ paths: [], force: false })
|
|
131
|
+
expect(terminal.getSpy("success")).toHaveBeenCalledWith("Local template is already up to date.")
|
|
132
|
+
})
|
|
96
133
|
})
|