Compare commits

...

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
d38450d8bd CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes 2026-07-14 16:56:33 +00:00
3 changed files with 209 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
appendWorkflowJsonExt,
ensureWorkflowSuffix,
escapeVueI18nMessageSyntax,
formatLocalizedMediumDate,
formatLocalizedNumber,
getFilePathSeparatorVariants,
@@ -477,4 +478,76 @@ describe('formatUtil', () => {
expect(formatLocalizedMediumDate('not a date', 'en')).toBe('—')
})
})
describe('escapeVueI18nMessageSyntax', () => {
it('escapes a literal @ that would break linked-message compilation', () => {
expect(
escapeVueI18nMessageSyntax('clips (tagged @Audio1-3 in the prompt)')
).toBe("clips (tagged {'@'}Audio1-3 in the prompt)")
})
it('escapes @ in an email address', () => {
expect(escapeVueI18nMessageSyntax('support@comfy.org')).toBe(
"support{'@'}comfy.org"
)
})
it('escapes interpolation braces', () => {
expect(escapeVueI18nMessageSyntax('size {w}x{h}')).toBe(
"size {'{'}w{'}'}x{'{'}h{'}'}"
)
})
it('escapes the plural pipe separator', () => {
expect(escapeVueI18nMessageSyntax('foreground | background')).toBe(
"foreground {'|'} background"
)
})
it('escapes the modulo percent so it cannot re-form %{', () => {
expect(escapeVueI18nMessageSyntax('50%{done}')).toBe(
"50{'%'}{'{'}done{'}'}"
)
})
it('escapes every occurrence in a single pass', () => {
expect(escapeVueI18nMessageSyntax('@a @b @c')).toBe(
"{'@'}a {'@'}b {'@'}c"
)
})
it('leaves strings without syntax characters unchanged', () => {
expect(escapeVueI18nMessageSyntax('no special chars here')).toBe(
'no special chars here'
)
expect(escapeVueI18nMessageSyntax('')).toBe('')
})
it('escapes a mix of different syntax characters in one string', () => {
expect(escapeVueI18nMessageSyntax('@{a|b}%c')).toBe(
"{'@'}{'{'}a{'|'}b{'}'}{'%'}c"
)
})
it('escapes back-to-back occurrences of the same character', () => {
expect(escapeVueI18nMessageSyntax('@@')).toBe("{'@'}{'@'}")
})
it('does not escape characters that are not vue-i18n syntax', () => {
const text = 'price: $10 #tag & "quoted" \'single\''
expect(escapeVueI18nMessageSyntax(text)).toBe(text)
})
it('is not idempotent: re-escaping already-escaped output changes it further', () => {
const once = escapeVueI18nMessageSyntax('@')
const twice = escapeVueI18nMessageSyntax(once)
expect(once).toBe("{'@'}")
expect(twice).not.toBe(once)
})
it('does not leak regex match state across successive calls', () => {
expect(escapeVueI18nMessageSyntax('@first')).toBe("{'@'}first")
expect(escapeVueI18nMessageSyntax('@second')).toBe("{'@'}second")
})
})
})

View File

@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { i18n, st, stRaw } from './i18n'
const TEST_NAMESPACE = 'safeTranslationTest'
beforeEach(() => {
i18n.global.locale.value = 'en'
const messages = i18n.global.getLocaleMessage('en')
delete (messages as Record<string, unknown>)[TEST_NAMESPACE]
i18n.global.setLocaleMessage('en', messages)
})
describe('st', () => {
it('returns the fallback when the key is not found', () => {
expect(st('safeTranslationTest.missing', 'Fallback value')).toBe(
'Fallback value'
)
})
it('uses compiled translations for valid locale messages', () => {
i18n.global.mergeLocaleMessage('en', {
safeTranslationTest: {
valid: 'Translated value'
}
})
expect(st('safeTranslationTest.valid', 'Fallback value')).toBe(
'Translated value'
)
})
it('returns raw locale messages when vue-i18n compilation fails', () => {
const message = 'Provided by @acme/model with JSON such as {"mode":"fast"}'
i18n.global.mergeLocaleMessage('en', {
safeTranslationTest: {
invalidLinkedFormat: message
}
})
expect(
st('safeTranslationTest.invalidLinkedFormat', 'Fallback value')
).toBe(message)
})
})
describe('stRaw', () => {
it('returns raw locale messages for valid keys', () => {
i18n.global.mergeLocaleMessage('en', {
safeTranslationTest: {
rawValue: 'Raw value'
}
})
expect(stRaw('safeTranslationTest.rawValue', 'Fallback value')).toBe(
'Raw value'
)
})
it('returns raw messages containing vue-i18n syntax', () => {
const message = 'Provided by @acme/model with JSON such as {"mode":"fast"}'
i18n.global.mergeLocaleMessage('en', {
safeTranslationTest: {
rawSyntax: message
}
})
expect(stRaw('safeTranslationTest.rawSyntax', 'Fallback value')).toBe(
message
)
})
it('returns the fallback when the key is not found', () => {
expect(stRaw('safeTranslationTest.rawMissing', 'Fallback value')).toBe(
'Fallback value'
)
})
it('returns the fallback when the resolved locale message is not a string', () => {
i18n.global.mergeLocaleMessage('en', {
safeTranslationTest: {
nested: { child: 'value' }
}
})
// The key exists (te() is true), but tm() resolves to an object rather
// than a string, so rawTranslationOrFallback must fall back.
expect(stRaw('safeTranslationTest.nested', 'Fallback value')).toBe(
'Fallback value'
)
})
})

View File

@@ -0,0 +1,42 @@
import { createI18n } from 'vue-i18n'
import { describe, expect, it } from 'vitest'
import { escapeVueI18nMessageSyntax } from '@comfyorg/shared-frontend-utils/formatUtil'
/**
* Node descriptions are compiled by vue-i18n via `t()`/`st()`, which parses
* `@ { } | %` as message syntax — a literal `@` even crashes the compiler with
* `Invalid linked format` (this broke the whole app after the 1.47.7 locale
* sync). `collect-i18n-node-defs.ts` escapes such values with
* `escapeVueI18nMessageSyntax` before writing them; this guards that the escaped
* output actually compiles and renders the original literal text.
*/
describe('escapeVueI18nMessageSyntax output is compiled safely by vue-i18n', () => {
const compile = (message: string) => {
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: { value: message } }
})
return i18n.global.t('value')
}
it.for([
'clips (tagged @Audio1-3 in the prompt)',
'support@comfy.org',
'resolution {width}x{height}',
'foreground | background',
'50%{done}',
'all of @ { } | % together',
'no special chars here',
// Regression fixture for the actual 1.47.7 crash: a node description
// referencing a model repo alongside a JSON example.
'Provided by @acme/model with JSON such as {"mode":"fast"}',
// Node category paths (e.g. `nodeDef.category.split('/')`) are escaped
// per-segment; a segment itself may still contain syntax characters.
'image/@custom-pack',
''
])('renders %s as the original literal text', (raw) => {
expect(compile(escapeVueI18nMessageSyntax(raw))).toBe(raw)
})
})