Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
commit
03634b1ca3
11340 files changed
+3442369
No files matched your search
@@ -0,0 +1,213 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { useState } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { translateBots } from './i18n-test-helper'
|
||||
import type { GroupMember } from './types'
|
||||
|
||||
// Group-composer mentions (#89049): the core composer's @-completion area
|
||||
// doesn't mount inside workspace tiles, so the room's composers wrap the SDK
|
||||
// input with a member-scoped popover of their own. Everything it inserts has
|
||||
// to be a string parseGroupChatMentions resolves.
|
||||
|
||||
const { host } = vi.hoisted(() => ({ host: {} as Record<string, unknown> }))
|
||||
|
||||
vi.mock('@hermes/plugin-sdk', async () => {
|
||||
const { pluginSdkMock } = await import('./group-test-utils')
|
||||
const base = await pluginSdkMock(host)
|
||||
|
||||
return {
|
||||
...base,
|
||||
Button: (props: React.ComponentProps<'button'>) => <button type="button" {...props} />,
|
||||
cn: (...values: unknown[]) => values.filter(Boolean).join(' '),
|
||||
Codicon: () => null,
|
||||
Input: (props: React.ComponentProps<'input'>) => <input {...props} />,
|
||||
RowButton: (props: React.ComponentProps<'button'>) => <button type="button" {...props} />,
|
||||
Textarea: (props: React.ComponentProps<'textarea'>) => <textarea {...props} />,
|
||||
useI18n: () => ({ t: (_key: string, fallback: string) => fallback }),
|
||||
// The plugin bundle normally lands via `ctx.i18n.register` at load, so
|
||||
// without this every localized label renders empty.
|
||||
usePluginI18n: () => translateBots
|
||||
}
|
||||
})
|
||||
|
||||
const MEMBERS: GroupMember[] = [
|
||||
{ handle: 'alpha', name: 'alpha', title: '' },
|
||||
{ handle: 'builder', name: 'builder', title: '' }
|
||||
]
|
||||
|
||||
/** Render the composer the way a room does: value owned by the caller (the
|
||||
* draft atom in production), popover scoped to the seated members. */
|
||||
async function mount(initial = '') {
|
||||
const { GroupMentionInput } = await import('./group-chat-parts')
|
||||
const onChange = vi.fn()
|
||||
const onSubmitDraft = vi.fn()
|
||||
|
||||
function Harness() {
|
||||
const [value, setValue] = useState(initial)
|
||||
|
||||
return (
|
||||
<GroupMentionInput
|
||||
aria-label="Message Core"
|
||||
members={MEMBERS}
|
||||
onChange={next => {
|
||||
onChange(next)
|
||||
setValue(next)
|
||||
}}
|
||||
onSubmitDraft={onSubmitDraft}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Harness />)
|
||||
|
||||
return { input: screen.getByLabelText('Message Core') as HTMLTextAreaElement, onChange, onSubmitDraft }
|
||||
}
|
||||
|
||||
/** Type `text`, then park the caret at `caret` (default: end of the text).
|
||||
* The click is how the component re-reads the caret without a keystroke —
|
||||
* jsdom does not preserve a selection across React's controlled re-write. */
|
||||
function typeInto(input: HTMLTextAreaElement, text: string, caret = text.length) {
|
||||
fireEvent.change(input, { target: { value: text } })
|
||||
input.setSelectionRange(caret, caret)
|
||||
fireEvent.click(input)
|
||||
}
|
||||
|
||||
const options = () => screen.queryAllByRole('button').map(button => button.textContent || '')
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('the @-token at the caret', () => {
|
||||
it('opens the popover on a token that begins a word', async () => {
|
||||
const { input } = await mount()
|
||||
|
||||
typeInto(input, 'hey @al')
|
||||
|
||||
expect(options().some(label => label.startsWith('@alpha'))).toBe(true)
|
||||
})
|
||||
|
||||
it('offers @everyone and @all as the room-wide broadcast quick picks', async () => {
|
||||
const { input } = await mount()
|
||||
|
||||
typeInto(input, '@')
|
||||
|
||||
expect(options().some(label => label.startsWith('@everyone'))).toBe(true)
|
||||
expect(options().some(label => label.startsWith('@all'))).toBe(true)
|
||||
})
|
||||
|
||||
it('narrows to @everyone as the query grows', async () => {
|
||||
const { input } = await mount()
|
||||
|
||||
typeInto(input, 'ping @every')
|
||||
|
||||
expect(options().filter(label => label.startsWith('@everyone'))).toHaveLength(1)
|
||||
expect(options().some(label => label.startsWith('@alpha'))).toBe(false)
|
||||
})
|
||||
|
||||
it('stays closed mid-word, on plain text, and with the caret before the @', async () => {
|
||||
const { input } = await mount()
|
||||
|
||||
typeInto(input, 'email me a@b')
|
||||
|
||||
expect(options()).toHaveLength(0)
|
||||
|
||||
typeInto(input, 'plain text')
|
||||
|
||||
expect(options()).toHaveLength(0)
|
||||
|
||||
typeInto(input, 'hey @al', 3)
|
||||
|
||||
expect(options()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertion', () => {
|
||||
it('writes exactly "@handle " — the shape parseGroupChatMentions resolves', async () => {
|
||||
const { input, onChange } = await mount()
|
||||
|
||||
typeInto(input, 'hey @al')
|
||||
const option = screen.getAllByRole('button').find(button => button.textContent?.startsWith('@alpha'))
|
||||
fireEvent.mouseDown(option!)
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('hey @alpha ')
|
||||
})
|
||||
|
||||
it('preventDefaults the mousedown so the input keeps focus', async () => {
|
||||
const { input } = await mount()
|
||||
|
||||
typeInto(input, 'hey @al')
|
||||
const option = screen.getAllByRole('button').find(button => button.textContent?.startsWith('@alpha'))
|
||||
|
||||
// fireEvent returns false when a handler called preventDefault.
|
||||
expect(fireEvent.mouseDown(option!)).toBe(false)
|
||||
})
|
||||
|
||||
it('replaces the whole token, not just the typed suffix', async () => {
|
||||
const { input, onChange } = await mount()
|
||||
|
||||
typeInto(input, '@bui and then some', 4)
|
||||
const option = screen.getAllByRole('button').find(button => button.textContent?.startsWith('@builder'))
|
||||
fireEvent.mouseDown(option!)
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('@builder and then some')
|
||||
})
|
||||
|
||||
it('lists every seated member under a bare @, keyed by handle', async () => {
|
||||
const { input } = await mount()
|
||||
|
||||
typeInto(input, '@')
|
||||
|
||||
// Handles only — a profile name that never resolved would surface as
|
||||
// "@undefined" and route to nobody.
|
||||
expect(options().some(label => label.startsWith('@alpha'))).toBe(true)
|
||||
expect(options().some(label => label.startsWith('@builder'))).toBe(true)
|
||||
expect(options().some(label => label.startsWith('@undefined'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// #89884: the composer used to be a single-line Input whose form submitted on
|
||||
// every Enter, so multi-line room prompts were impossible.
|
||||
describe('keyboard (#89884)', () => {
|
||||
it('submits on Enter and leaves Shift+Enter to the textarea', async () => {
|
||||
const { input, onSubmitDraft } = await mount('a room prompt')
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect(onSubmitDraft).toHaveBeenCalledTimes(1)
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter', shiftKey: true })
|
||||
|
||||
expect(onSubmitDraft).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('inserts the highlighted mention on Enter while the popover is open', async () => {
|
||||
const { input, onChange, onSubmitDraft } = await mount()
|
||||
|
||||
typeInto(input, 'hey @alp')
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('hey @alpha ')
|
||||
expect(onSubmitDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// #93528: Enter here confirms composed Chinese/Japanese/Korean text. It must
|
||||
// neither insert a mention nor submit the draft. isComposing covers Chromium;
|
||||
// keyCode 229 covers macOS Chinese IMEs that fire Enter after compositionend
|
||||
// with isComposing already false.
|
||||
it('swallows IME composition Enters (#93528)', async () => {
|
||||
const { input, onSubmitDraft } = await mount('中文')
|
||||
|
||||
fireEvent.keyDown(input, { isComposing: true, key: 'Enter' })
|
||||
fireEvent.keyDown(input, { key: 'Enter', keyCode: 229 })
|
||||
|
||||
expect(onSubmitDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user