mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
## Summary Use TextEncoder/TextDecoder for UTF-8 safe base64 encoding/decoding When copying nodes containing non-Latin1 characters (e.g., Chinese characters in localized_name field), btoa() throws an error: InvalidCharacterError: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range. The copy operation saved data to localStorage successfully, but failed to write to the browser clipboard. On paste, the browser clipboard still contained old data, causing the wrong node to be pasted. <!-- Fixes #ISSUE_NUMBER --> https://github.com/Comfy-Org/ComfyUI_frontend/issues/6993 https://github.com/Comfy-Org/ComfyUI_frontend/issues/5449 https://github.com/comfyanonymous/ComfyUI/issues/8481 ## Screenshots (if applicable) before https://github.com/user-attachments/assets/8abd9049-91bb-4200-8853-e26753376007 after https://github.com/user-attachments/assets/7d969f32-bb0f-4c7a-baa2-65d576a4eba2 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7103-fix-handle-Unicode-characters-in-clipboard-copy-paste-and-add-Paste-menu-option-2bd6d73d365081f39c40e7e7f832b97c) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
/**
|
|
* Encodes a UTF-8 string to base64 (same logic as useCopy.ts)
|
|
*/
|
|
function encodeClipboardData(data: string): string {
|
|
return btoa(
|
|
String.fromCharCode(...Array.from(new TextEncoder().encode(data)))
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Decodes base64 to UTF-8 string (same logic as usePaste.ts)
|
|
*/
|
|
function decodeClipboardData(base64: string): string {
|
|
const binaryString = atob(base64)
|
|
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
|
|
return new TextDecoder().decode(bytes)
|
|
}
|
|
|
|
describe('Clipboard UTF-8 base64 encoding/decoding', () => {
|
|
it('should handle ASCII-only strings', () => {
|
|
const original = '{"nodes":[{"id":1,"type":"LoadImage"}]}'
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle Chinese characters in localized_name', () => {
|
|
const original =
|
|
'{"nodes":[{"id":1,"type":"LoadImage","localized_name":"图像"}]}'
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle Japanese characters', () => {
|
|
const original = '{"localized_name":"画像を読み込む"}'
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle Korean characters', () => {
|
|
const original = '{"localized_name":"이미지 불러오기"}'
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle mixed ASCII and Unicode characters', () => {
|
|
const original =
|
|
'{"nodes":[{"id":1,"type":"LoadImage","localized_name":"加载图像","label":"Load Image 图片"}]}'
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle emoji characters', () => {
|
|
const original = '{"title":"Test Node 🎨🖼️"}'
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle empty string', () => {
|
|
const original = ''
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
})
|
|
|
|
it('should handle complex node data with multiple Unicode fields', () => {
|
|
const original = JSON.stringify({
|
|
nodes: [
|
|
{
|
|
id: 1,
|
|
type: 'LoadImage',
|
|
localized_name: '图像',
|
|
inputs: [{ localized_name: '图片', name: 'image' }],
|
|
outputs: [{ localized_name: '输出', name: 'output' }]
|
|
}
|
|
],
|
|
groups: [{ title: '预处理组 🔧' }],
|
|
links: []
|
|
})
|
|
const encoded = encodeClipboardData(original)
|
|
const decoded = decodeClipboardData(encoded)
|
|
expect(decoded).toBe(original)
|
|
expect(JSON.parse(decoded)).toEqual(JSON.parse(original))
|
|
})
|
|
|
|
it('should produce valid base64 output', () => {
|
|
const original = '{"localized_name":"中文测试"}'
|
|
const encoded = encodeClipboardData(original)
|
|
// Base64 should only contain valid characters
|
|
expect(encoded).toMatch(/^[A-Za-z0-9+/=]+$/)
|
|
})
|
|
|
|
it('should fail with plain btoa for non-Latin1 characters', () => {
|
|
const original = '{"localized_name":"图像"}'
|
|
// This demonstrates why we need TextEncoder - plain btoa fails
|
|
expect(() => btoa(original)).toThrow()
|
|
})
|
|
})
|