Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions apps/client/src/widgets/type_widgets/relation_map/RelationMap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import $ from 'jquery'
import utils from '../../../services/utils'
import dialog from '../../../services/dialog'

vi.mock('../../../services/utils', async (importOriginal) => {
const actual = await importOriginal() as any
return {
...actual,
default: {
...actual.default,
filterAttributeName: vi.fn((val: string) => val.replace(/[^a-z0-9]/gi, ''))
}
}
})
Comment on lines +6 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The mock implementation of filterAttributeName here differs significantly from the actual implementation in apps/client/src/services/utils.ts.

The mock (val.replace(/[^a-z0-9]/gi, '')) strips all non-ASCII alphanumeric characters, including Chinese characters. However, the real implementation (name.replace(/[^\p{L}\p{N}_:]/gu, "")) allows Unicode letters (like '你') because of \p{L}.

This discrepancy means the tests are not accurately reflecting the production behavior. For example, the test 'filters invalid characters from input after IME composition ends' expects '你' to be stripped, but it would be preserved in production. This could lead to unexpected behavior where invalid attribute names are created.

Please update the mock to match the real implementation, or if the intent is to disallow Unicode characters for relation names, the filterAttributeName function itself should be adjusted (though that might be a broader change). A consistent behavior between tests and production code is crucial.


vi.mock('../../../services/dialog', () => ({
default: {
prompt: vi.fn()
}
}))

vi.mock('../../../services/attribute_autocomplete', () => ({
default: {
initAttributeNameAutocomplete: vi.fn()
}
}))

vi.mock('../../../services/i18n', () => ({
t: (key: string) => key
}))

// Call promptForRelationName and extract the $answer input element from the dialog's shown callback
async function getAnswerFromPrompt(): Promise<JQuery<HTMLInputElement>> {
const { promptForRelationName } = await import('./utils')

let $answer!: JQuery<HTMLInputElement>

vi.mocked(dialog.prompt).mockImplementation(({ shown }) => {
document.body.innerHTML = '<input type="text" />'
const input = document.querySelector('input') as HTMLInputElement
$answer = $(input) as JQuery<HTMLInputElement>
shown?.({ $answer })
return Promise.resolve(null)
})

promptForRelationName()
return $answer
}

describe('IME composition handling - Chinese input (promptForRelationName)', () => {
let input: HTMLInputElement
let $answer: JQuery<HTMLInputElement>

beforeEach(async () => {
vi.clearAllMocks()
$answer = await getAnswerFromPrompt()
input = $answer[0] as HTMLInputElement
})

it('does not filter intermediate Chinese characters during composition', () => {
// user starts typing in Chinese IME
input.dispatchEvent(new Event('compositionstart'))

// intermediate IME states — these are pinyin keystrokes shown before final char
input.value = 'n'
input.dispatchEvent(new Event('input'))
input.value = 'ni'
input.dispatchEvent(new Event('input'))
input.value = 'nin'
input.dispatchEvent(new Event('input'))
input.value = 'ning'
input.dispatchEvent(new Event('input'))

expect(input.value).toBe('ning')
})

it('filters invalid characters from input after IME composition ends', () => {
input.dispatchEvent(new Event('compositionstart'))

// intermediate pinyin
input.value = 'n'
input.dispatchEvent(new Event('input'))
input.value = 'ni'
input.dispatchEvent(new Event('input'))

// user selects the Chinese character 你 from the IME picker
input.value = '你'
input.dispatchEvent(new Event('compositionend'))

expect(input.value).toBe('')
})

it('allows normal latin input after Chinese composition ends', () => {
// first do a Chinese composition
input.dispatchEvent(new Event('compositionstart'))
input.value = '你'
input.dispatchEvent(new Event('compositionend'))

// then type normally in latin
input.value = 'hello'
input.dispatchEvent(new Event('input'))

expect(input.value).toBe('hello')
})

it('handles multiple Chinese characters in sequence', () => {
// first character
input.dispatchEvent(new Event('compositionstart'))
input.value = '你'
input.dispatchEvent(new Event('compositionend'))

// second character
input.dispatchEvent(new Event('compositionstart'))
input.value = '好'
input.dispatchEvent(new Event('compositionend'))

expect(input.value).toBe('')
})
});

Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,5 @@ function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObjec

return connectionCallback;
}


13 changes: 12 additions & 1 deletion apps/client/src/widgets/type_widgets/relation_map/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,18 @@ export function promptForRelationName(defaultValue?: string): Promise<string | n
return;
}

$answer.on("keyup", () => {
let isComposing = false;

$answer.on("compositionstart", () => {
isComposing = true;
});
$answer.on("compositionend", () => {
isComposing = false;
const attrName = utils.filterAttributeName($answer.val() as string);
$answer.val(attrName);
});
$answer.on("input", () => {
if (isComposing) return;
const attrName = utils.filterAttributeName($answer.val() as string);
$answer.val(attrName);
});
Expand Down
Loading