mirror of
https://github.com/coder/code-server.git
synced 2025-08-02 22:58:50 +08:00
Add language customization flag (#7374)
This allows you to customize any string (that has a translation) or add your own translations.
This commit is contained in:

committed by
GitHub

parent
8b3d9b9e0a
commit
92fca0dcc3
@ -22,6 +22,9 @@
|
||||
- [Proxying to a Svelte app](#proxying-to-a-svelte-app)
|
||||
- [Prefixing `/absproxy/<port>` with a path](#prefixing-absproxyport-with-a-path)
|
||||
- [Preflight requests](#preflight-requests)
|
||||
- [Internationalization and customization](#internationalization-and-customization)
|
||||
- [Available keys and placeholders](#available-keys-and-placeholders)
|
||||
- [Legacy flag](#legacy-flag)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- prettier-ignore-end -->
|
||||
@ -458,3 +461,45 @@ By default, if you have auth enabled, code-server will authenticate all proxied
|
||||
requests including preflight requests. This can cause issues because preflight
|
||||
requests do not typically include credentials. To allow all preflight requests
|
||||
through the proxy without authentication, use `--skip-auth-preflight`.
|
||||
|
||||
## Internationalization and customization
|
||||
|
||||
code-server allows you to provide a JSON file to configure certain strings. This can be used for both internationalization and customization.
|
||||
|
||||
Create a JSON file with your custom strings:
|
||||
|
||||
```json
|
||||
{
|
||||
"WELCOME": "Welcome to {{app}}",
|
||||
"LOGIN_TITLE": "{{app}} Access Portal",
|
||||
"LOGIN_BELOW": "Please log in to continue",
|
||||
"PASSWORD_PLACEHOLDER": "Enter Password"
|
||||
}
|
||||
```
|
||||
|
||||
Then reference the file:
|
||||
|
||||
```shell
|
||||
code-server --i18n /path/to/custom-strings.json
|
||||
```
|
||||
|
||||
Or this can be done in the config file:
|
||||
|
||||
```yaml
|
||||
i18n: /path/to/custom-strings.json
|
||||
```
|
||||
|
||||
You can combine this with the `--locale` flag to configure language support for both code-server and VS Code in cases where code-server has no support but VS Code does. If you are using this for internationalization, please consider sending us a pull request to contribute it to `src/node/i18n/locales`.
|
||||
|
||||
### Available keys and placeholders
|
||||
|
||||
Refer to [../src/node/i18n/locales/en.json](../src/node/i18n/locales/en.json) for a full list of the available keys for translations. Note that the only placeholders supported for each key are the ones used in the default string.
|
||||
|
||||
The `--app-name` flag controls the `{{app}}` placeholder in templates. If you want to change the name, you can either:
|
||||
|
||||
1. Set `--app-name` (potentially alongside `--i18n`)
|
||||
2. Use `--i18n` and hardcode the name in your strings
|
||||
|
||||
### Legacy flag
|
||||
|
||||
The `--welcome-text` flag is now deprecated. Use the `WELCOME` key instead.
|
||||
|
@ -93,6 +93,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
|
||||
"app-name"?: string
|
||||
"welcome-text"?: string
|
||||
"abs-proxy-base-path"?: string
|
||||
i18n?: string
|
||||
/* Positional arguments. */
|
||||
_?: string[]
|
||||
}
|
||||
@ -284,17 +285,24 @@ export const options: Options<Required<UserProvidedArgs>> = {
|
||||
"app-name": {
|
||||
type: "string",
|
||||
short: "an",
|
||||
description: "The name to use in branding. Will be shown in titlebar and welcome message",
|
||||
description:
|
||||
"Will replace the {{app}} placeholder in any strings, which by default includes the title bar and welcome message",
|
||||
},
|
||||
"welcome-text": {
|
||||
type: "string",
|
||||
short: "w",
|
||||
description: "Text to show on login page",
|
||||
deprecated: true,
|
||||
},
|
||||
"abs-proxy-base-path": {
|
||||
type: "string",
|
||||
description: "The base path to prefix to all absproxy requests",
|
||||
},
|
||||
i18n: {
|
||||
type: "string",
|
||||
path: true,
|
||||
description: "Path to JSON file with custom translations. Merges with default strings and supports all i18n keys.",
|
||||
},
|
||||
}
|
||||
|
||||
export const optionDescriptions = (opts: Partial<Options<Required<UserProvidedArgs>>> = options): string[] => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { promises as fs } from "fs"
|
||||
import i18next, { init } from "i18next"
|
||||
import * as en from "./locales/en.json"
|
||||
import * as ja from "./locales/ja.json"
|
||||
@ -5,29 +6,54 @@ import * as th from "./locales/th.json"
|
||||
import * as ur from "./locales/ur.json"
|
||||
import * as zhCn from "./locales/zh-cn.json"
|
||||
|
||||
const defaultResources = {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
"zh-cn": {
|
||||
translation: zhCn,
|
||||
},
|
||||
th: {
|
||||
translation: th,
|
||||
},
|
||||
ja: {
|
||||
translation: ja,
|
||||
},
|
||||
ur: {
|
||||
translation: ur,
|
||||
},
|
||||
}
|
||||
|
||||
export async function loadCustomStrings(filePath: string): Promise<void> {
|
||||
try {
|
||||
// Read custom strings from file path only
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
const customStringsData = JSON.parse(fileContent)
|
||||
|
||||
// User-provided strings override all languages.
|
||||
Object.keys(defaultResources).forEach((locale) => {
|
||||
i18next.addResourceBundle(locale, "translation", customStringsData)
|
||||
})
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
||||
throw new Error(`Custom strings file not found: ${filePath}\nPlease ensure the file exists and is readable.`)
|
||||
} else if (error instanceof SyntaxError) {
|
||||
throw new Error(`Invalid JSON in custom strings file: ${filePath}\n${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to load custom strings from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init({
|
||||
lng: "en",
|
||||
fallbackLng: "en", // language to use if translations in user language are not available.
|
||||
returnNull: false,
|
||||
lowerCaseLng: true,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
resources: {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
"zh-cn": {
|
||||
translation: zhCn,
|
||||
},
|
||||
th: {
|
||||
translation: th,
|
||||
},
|
||||
ja: {
|
||||
translation: ja,
|
||||
},
|
||||
ur: {
|
||||
translation: ur,
|
||||
},
|
||||
},
|
||||
resources: defaultResources,
|
||||
})
|
||||
|
||||
export default i18next
|
||||
|
@ -7,6 +7,7 @@ import { plural } from "../common/util"
|
||||
import { createApp, ensureAddress } from "./app"
|
||||
import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli"
|
||||
import { commit, version, vsRootPath } from "./constants"
|
||||
import { loadCustomStrings } from "./i18n"
|
||||
import { register } from "./routes"
|
||||
import { VSCodeModule } from "./routes/vscode"
|
||||
import { isDirectory, open } from "./util"
|
||||
@ -122,6 +123,12 @@ export const runCodeServer = async (
|
||||
): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => {
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
|
||||
// Load custom strings if provided
|
||||
if (args.i18n) {
|
||||
await loadCustomStrings(args.i18n)
|
||||
logger.info("Loaded custom strings")
|
||||
}
|
||||
|
||||
logger.info(`Using user-data-dir ${args["user-data-dir"]}`)
|
||||
logger.debug(`Using extensions-dir ${args["extensions-dir"]}`)
|
||||
|
||||
|
@ -31,23 +31,32 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
|
||||
const locale = req.args["locale"] || "en"
|
||||
i18n.changeLanguage(locale)
|
||||
const appName = req.args["app-name"] || "code-server"
|
||||
const welcomeText = req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)
|
||||
const welcomeText = escapeHtml(req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string))
|
||||
|
||||
// Determine password message using i18n
|
||||
let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: req.args.config })
|
||||
if (req.args.usingEnvPassword) {
|
||||
passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD")
|
||||
} else if (req.args.usingEnvHashedPassword) {
|
||||
passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD")
|
||||
}
|
||||
passwordMsg = escapeHtml(passwordMsg)
|
||||
|
||||
// Get messages from i18n (with HTML escaping for security)
|
||||
const loginTitle = escapeHtml(i18n.t("LOGIN_TITLE", { app: appName }))
|
||||
const loginBelow = escapeHtml(i18n.t("LOGIN_BELOW"))
|
||||
const passwordPlaceholder = escapeHtml(i18n.t("PASSWORD_PLACEHOLDER"))
|
||||
const submitText = escapeHtml(i18n.t("SUBMIT"))
|
||||
|
||||
return replaceTemplates(
|
||||
req,
|
||||
content
|
||||
.replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE", { app: appName }))
|
||||
.replace(/{{I18N_LOGIN_TITLE}}/g, loginTitle)
|
||||
.replace(/{{WELCOME_TEXT}}/g, welcomeText)
|
||||
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||
.replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW"))
|
||||
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER"))
|
||||
.replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT"))
|
||||
.replace(/{{I18N_LOGIN_BELOW}}/g, loginBelow)
|
||||
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, passwordPlaceholder)
|
||||
.replace(/{{I18N_SUBMIT}}/g, submitText)
|
||||
.replace(/{{ERROR}}/, error ? `<div class="error">${escapeHtml(error.message)}</div>` : ""),
|
||||
)
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ describe("parser", () => {
|
||||
"--verbose",
|
||||
["--app-name", "custom instance name"],
|
||||
["--welcome-text", "welcome to code"],
|
||||
["--i18n", "path/to/custom-strings.json"],
|
||||
"2",
|
||||
|
||||
["--locale", "ja"],
|
||||
@ -145,6 +146,7 @@ describe("parser", () => {
|
||||
verbose: true,
|
||||
"app-name": "custom instance name",
|
||||
"welcome-text": "welcome to code",
|
||||
i18n: path.resolve("path/to/custom-strings.json"),
|
||||
version: true,
|
||||
"bind-addr": "192.169.0.1:8080",
|
||||
"session-socket": "/tmp/override-code-server-ipc-socket",
|
||||
@ -347,6 +349,28 @@ describe("parser", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("should parse i18n flag with file path", async () => {
|
||||
// Test with file path (no validation at CLI parsing level)
|
||||
const args = parse(["--i18n", "/path/to/custom-strings.json"])
|
||||
expect(args).toEqual({
|
||||
i18n: "/path/to/custom-strings.json",
|
||||
})
|
||||
})
|
||||
|
||||
it("should parse i18n flag with relative file path", async () => {
|
||||
// Test with relative file path
|
||||
expect(() => parse(["--i18n", "./custom-strings.json"])).not.toThrow()
|
||||
expect(() => parse(["--i18n", "strings.json"])).not.toThrow()
|
||||
})
|
||||
|
||||
it("should support app-name and deprecated welcome-text flags", async () => {
|
||||
const args = parse(["--app-name", "My App", "--welcome-text", "Welcome!"])
|
||||
expect(args).toEqual({
|
||||
"app-name": "My App",
|
||||
"welcome-text": "Welcome!",
|
||||
})
|
||||
})
|
||||
|
||||
it("should use env var github token", async () => {
|
||||
process.env.GITHUB_TOKEN = "ga-foo"
|
||||
const args = parse([])
|
||||
|
154
test/unit/node/i18n.test.ts
Normal file
154
test/unit/node/i18n.test.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { promises as fs } from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { loadCustomStrings } from "../../../src/node/i18n"
|
||||
|
||||
describe("i18n", () => {
|
||||
let tempDir: string
|
||||
let validJsonFile: string
|
||||
let invalidJsonFile: string
|
||||
let nonExistentFile: string
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temporary directory for test files
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "code-server-i18n-test-"))
|
||||
|
||||
// Create test files
|
||||
validJsonFile = path.join(tempDir, "valid.json")
|
||||
invalidJsonFile = path.join(tempDir, "invalid.json")
|
||||
nonExistentFile = path.join(tempDir, "does-not-exist.json")
|
||||
|
||||
// Write valid JSON file
|
||||
await fs.writeFile(
|
||||
validJsonFile,
|
||||
JSON.stringify({
|
||||
WELCOME: "Custom Welcome",
|
||||
LOGIN_TITLE: "My Custom App",
|
||||
LOGIN_BELOW: "Please log in to continue",
|
||||
}),
|
||||
)
|
||||
|
||||
// Write invalid JSON file
|
||||
await fs.writeFile(invalidJsonFile, '{"WELCOME": "Missing closing quote}')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up temporary directory
|
||||
await fs.rmdir(tempDir, { recursive: true })
|
||||
})
|
||||
|
||||
describe("loadCustomStrings", () => {
|
||||
it("should load valid JSON file successfully", async () => {
|
||||
// Should not throw an error
|
||||
await expect(loadCustomStrings(validJsonFile)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("should throw clear error for non-existent file", async () => {
|
||||
await expect(loadCustomStrings(nonExistentFile)).rejects.toThrow(
|
||||
`Custom strings file not found: ${nonExistentFile}\nPlease ensure the file exists and is readable.`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw clear error for invalid JSON", async () => {
|
||||
await expect(loadCustomStrings(invalidJsonFile)).rejects.toThrow(
|
||||
`Invalid JSON in custom strings file: ${invalidJsonFile}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle empty JSON object", async () => {
|
||||
const emptyJsonFile = path.join(tempDir, "empty.json")
|
||||
await fs.writeFile(emptyJsonFile, "{}")
|
||||
|
||||
await expect(loadCustomStrings(emptyJsonFile)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle nested JSON objects", async () => {
|
||||
const nestedJsonFile = path.join(tempDir, "nested.json")
|
||||
await fs.writeFile(
|
||||
nestedJsonFile,
|
||||
JSON.stringify({
|
||||
WELCOME: "Hello World",
|
||||
NESTED: {
|
||||
KEY: "Value",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(loadCustomStrings(nestedJsonFile)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle special characters and unicode", async () => {
|
||||
const unicodeJsonFile = path.join(tempDir, "unicode.json")
|
||||
await fs.writeFile(
|
||||
unicodeJsonFile,
|
||||
JSON.stringify({
|
||||
WELCOME: "欢迎来到 code-server",
|
||||
LOGIN_TITLE: "Willkommen bei {{app}}",
|
||||
SPECIAL: "Special chars: àáâãäåæçèéêë 🚀 ♠️ ∆",
|
||||
}),
|
||||
"utf8",
|
||||
)
|
||||
|
||||
await expect(loadCustomStrings(unicodeJsonFile)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle generic errors that are not ENOENT or SyntaxError", async () => {
|
||||
const testFile = path.join(tempDir, "test.json")
|
||||
await fs.writeFile(testFile, "{}")
|
||||
|
||||
// Mock fs.readFile to throw a generic error
|
||||
const originalReadFile = fs.readFile
|
||||
const mockError = new Error("Permission denied")
|
||||
fs.readFile = jest.fn().mockRejectedValue(mockError)
|
||||
|
||||
await expect(loadCustomStrings(testFile)).rejects.toThrow(
|
||||
`Failed to load custom strings from ${testFile}: Permission denied`,
|
||||
)
|
||||
|
||||
// Restore original function
|
||||
fs.readFile = originalReadFile
|
||||
})
|
||||
|
||||
it("should handle errors that are not Error instances", async () => {
|
||||
const testFile = path.join(tempDir, "test.json")
|
||||
await fs.writeFile(testFile, "{}")
|
||||
|
||||
// Mock fs.readFile to throw a non-Error object
|
||||
const originalReadFile = fs.readFile
|
||||
fs.readFile = jest.fn().mockRejectedValue("String error")
|
||||
|
||||
await expect(loadCustomStrings(testFile)).rejects.toThrow(
|
||||
`Failed to load custom strings from ${testFile}: String error`,
|
||||
)
|
||||
|
||||
// Restore original function
|
||||
fs.readFile = originalReadFile
|
||||
})
|
||||
|
||||
it("should handle null/undefined errors", async () => {
|
||||
const testFile = path.join(tempDir, "test.json")
|
||||
await fs.writeFile(testFile, "{}")
|
||||
|
||||
// Mock fs.readFile to throw null
|
||||
const originalReadFile = fs.readFile
|
||||
fs.readFile = jest.fn().mockRejectedValue(null)
|
||||
|
||||
await expect(loadCustomStrings(testFile)).rejects.toThrow(`Failed to load custom strings from ${testFile}: null`)
|
||||
|
||||
// Restore original function
|
||||
fs.readFile = originalReadFile
|
||||
})
|
||||
|
||||
it("should complete without errors for valid input", async () => {
|
||||
const testFile = path.join(tempDir, "resource-test.json")
|
||||
const customStrings = {
|
||||
WELCOME: "Custom Welcome Message",
|
||||
LOGIN_TITLE: "Custom Login Title",
|
||||
}
|
||||
await fs.writeFile(testFile, JSON.stringify(customStrings))
|
||||
|
||||
// Should not throw any errors
|
||||
await expect(loadCustomStrings(testFile)).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
175
test/unit/node/main.test.ts
Normal file
175
test/unit/node/main.test.ts
Normal file
@ -0,0 +1,175 @@
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { setDefaults, parse } from "../../../src/node/cli"
|
||||
import { loadCustomStrings } from "../../../src/node/i18n"
|
||||
import { tmpdir } from "../../utils/helpers"
|
||||
|
||||
// Mock the i18n module
|
||||
jest.mock("../../../src/node/i18n", () => ({
|
||||
loadCustomStrings: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock logger to avoid console output during tests
|
||||
jest.mock("@coder/logger", () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
level: 0,
|
||||
},
|
||||
field: jest.fn(),
|
||||
Level: {
|
||||
Trace: 0,
|
||||
Debug: 1,
|
||||
Info: 2,
|
||||
Warn: 3,
|
||||
Error: 4,
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedLoadCustomStrings = loadCustomStrings as jest.MockedFunction<typeof loadCustomStrings>
|
||||
|
||||
describe("main", () => {
|
||||
let tempDir: string
|
||||
let mockServer: any
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await tmpdir("code-server-main-test")
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Mock the server creation to avoid actually starting a server
|
||||
mockServer = {
|
||||
server: {
|
||||
listen: jest.fn(),
|
||||
address: jest.fn(() => ({ address: "127.0.0.1", port: 8080 })),
|
||||
close: jest.fn(),
|
||||
},
|
||||
editorSessionManagerServer: {
|
||||
address: jest.fn(() => null),
|
||||
},
|
||||
dispose: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up temp directory
|
||||
try {
|
||||
await fs.rmdir(tempDir, { recursive: true })
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
describe("runCodeServer", () => {
|
||||
it("should load custom strings when i18n flag is provided", async () => {
|
||||
// Create a test custom strings file
|
||||
const customStringsFile = path.join(tempDir, "custom-strings.json")
|
||||
await fs.writeFile(
|
||||
customStringsFile,
|
||||
JSON.stringify({
|
||||
WELCOME: "Custom Welcome",
|
||||
LOGIN_TITLE: "My App",
|
||||
}),
|
||||
)
|
||||
|
||||
// Create args with i18n flag
|
||||
const cliArgs = parse([
|
||||
`--config=${path.join(tempDir, "config.yaml")}`,
|
||||
`--user-data-dir=${tempDir}`,
|
||||
"--bind-addr=localhost:0",
|
||||
"--log=warn",
|
||||
"--auth=none",
|
||||
`--i18n=${customStringsFile}`,
|
||||
])
|
||||
const args = await setDefaults(cliArgs)
|
||||
|
||||
// Mock the app module
|
||||
jest.doMock("../../../src/node/app", () => ({
|
||||
createApp: jest.fn().mockResolvedValue(mockServer),
|
||||
ensureAddress: jest.fn().mockReturnValue(new URL("http://localhost:8080")),
|
||||
}))
|
||||
|
||||
// Mock routes module
|
||||
jest.doMock("../../../src/node/routes", () => ({
|
||||
register: jest.fn().mockResolvedValue(jest.fn()),
|
||||
}))
|
||||
|
||||
// Mock loadCustomStrings to succeed
|
||||
mockedLoadCustomStrings.mockResolvedValue(undefined)
|
||||
|
||||
// Import runCodeServer after mocking
|
||||
const mainModule = await import("../../../src/node/main")
|
||||
const result = await mainModule.runCodeServer(args)
|
||||
|
||||
// Verify that loadCustomStrings was called with the correct file path
|
||||
expect(mockedLoadCustomStrings).toHaveBeenCalledWith(customStringsFile)
|
||||
expect(mockedLoadCustomStrings).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Clean up
|
||||
await result.dispose()
|
||||
})
|
||||
|
||||
it("should not load custom strings when i18n flag is not provided", async () => {
|
||||
// Create args without i18n flag
|
||||
const cliArgs = parse([
|
||||
`--config=${path.join(tempDir, "config.yaml")}`,
|
||||
`--user-data-dir=${tempDir}`,
|
||||
"--bind-addr=localhost:0",
|
||||
"--log=warn",
|
||||
"--auth=none",
|
||||
])
|
||||
const args = await setDefaults(cliArgs)
|
||||
|
||||
// Mock the app module
|
||||
jest.doMock("../../../src/node/app", () => ({
|
||||
createApp: jest.fn().mockResolvedValue(mockServer),
|
||||
ensureAddress: jest.fn().mockReturnValue(new URL("http://localhost:8080")),
|
||||
}))
|
||||
|
||||
// Mock routes module
|
||||
jest.doMock("../../../src/node/routes", () => ({
|
||||
register: jest.fn().mockResolvedValue(jest.fn()),
|
||||
}))
|
||||
|
||||
// Import runCodeServer after mocking
|
||||
const mainModule = await import("../../../src/node/main")
|
||||
const result = await mainModule.runCodeServer(args)
|
||||
|
||||
// Verify that loadCustomStrings was NOT called
|
||||
expect(mockedLoadCustomStrings).not.toHaveBeenCalled()
|
||||
|
||||
// Clean up
|
||||
await result.dispose()
|
||||
})
|
||||
|
||||
it("should handle errors when loadCustomStrings fails", async () => {
|
||||
// Create args with i18n flag pointing to non-existent file
|
||||
const nonExistentFile = path.join(tempDir, "does-not-exist.json")
|
||||
const cliArgs = parse([
|
||||
`--config=${path.join(tempDir, "config.yaml")}`,
|
||||
`--user-data-dir=${tempDir}`,
|
||||
"--bind-addr=localhost:0",
|
||||
"--log=warn",
|
||||
"--auth=none",
|
||||
`--i18n=${nonExistentFile}`,
|
||||
])
|
||||
const args = await setDefaults(cliArgs)
|
||||
|
||||
// Mock loadCustomStrings to throw an error
|
||||
const mockError = new Error("Custom strings file not found")
|
||||
mockedLoadCustomStrings.mockRejectedValue(mockError)
|
||||
|
||||
// Import runCodeServer after mocking
|
||||
const mainModule = await import("../../../src/node/main")
|
||||
|
||||
// Verify that runCodeServer throws the error from loadCustomStrings
|
||||
await expect(mainModule.runCodeServer(args)).rejects.toThrow("Custom strings file not found")
|
||||
|
||||
// Verify that loadCustomStrings was called
|
||||
expect(mockedLoadCustomStrings).toHaveBeenCalledWith(nonExistentFile)
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user