Files
upstream-rocketchat/apps/meteor/client/views/room/composer/messageBox/wrapSelection.spec.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

192 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { handleSelectionWrapping } from './wrapSelection';
import type { ChatAPI, ComposerAPI } from '../../../../lib/chats/ChatAPI';
const createTextarea = (value: string, selectionStart: number, selectionEnd: number): HTMLTextAreaElement => {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.selectionStart = selectionStart;
textarea.selectionEnd = selectionEnd;
return textarea;
};
const createMockComposer = (text: string): Pick<ComposerAPI, 'text' | 'wrapSelection'> => ({
text,
wrapSelection: jest.fn(() => ({ selectionStart: 0, selectionEnd: 0, value: '' })),
});
const createInputEvent = (textarea: HTMLTextAreaElement, data: string, isComposing = false): InputEvent => {
const event = new InputEvent('input', {
data,
inputType: 'insertText',
isComposing,
bubbles: true,
cancelable: true,
});
Object.defineProperty(event, 'target', { value: textarea, writable: false });
return event;
};
describe('handleSelectionWrapping', () => {
describe('dead key / IME composition scenarios', () => {
it('should NOT wrap when the selected text is the same character just typed (dead key double-press)', () => {
// Simulates: user presses ' twice on a Brazilian/international keyboard
// After composition ends, the browser has:
// - inserted the character ' into the textarea
// - selected it (selectionStart=0, selectionEnd=1)
// - fired an InputEvent with data="'"
const textarea = createTextarea("'", 0, 1);
const composer = createMockComposer("'");
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, "'");
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
it('should NOT wrap when dead key composition produces a character mid-text', () => {
// User is typing "hello'" — the dead key inserts ' and selects it
const textarea = createTextarea("hello'", 5, 6);
const composer = createMockComposer("hello'");
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, "'");
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
it('should NOT wrap when backtick dead key is pressed twice', () => {
const textarea = createTextarea('`', 0, 1);
const composer = createMockComposer('`');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, '`');
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
it('should NOT wrap when tilde dead key is pressed twice', () => {
const textarea = createTextarea('˜', 0, 1);
const composer = createMockComposer('˜');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, '˜');
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
});
describe('legitimate wrapping scenarios', () => {
it('should wrap when user selects text and types a wrapping character', () => {
// User selects "hello" and types '
const textarea = createTextarea('hello', 0, 5);
const composer = createMockComposer('hello');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, "'");
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(true);
expect(composer.wrapSelection).toHaveBeenCalledWith("'{{text}}'");
});
it('should wrap when user selects partial text and types a wrapping character', () => {
// User selects "world" in "hello world" and types *
const textarea = createTextarea('hello world', 6, 11);
const composer = createMockComposer('hello world');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, '*');
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(true);
expect(composer.wrapSelection).toHaveBeenCalledWith('*{{text}}*');
});
it('should wrap with backtick when text is selected', () => {
const textarea = createTextarea('some code here', 5, 9);
const composer = createMockComposer('some code here');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, '`');
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(true);
expect(composer.wrapSelection).toHaveBeenCalledWith('`{{text}}`');
});
});
describe('no-op scenarios', () => {
it('should return false when there is no selection (cursor only)', () => {
const textarea = createTextarea('hello', 3, 3);
const composer = createMockComposer('hello');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, "'");
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
it('should return false when there is no composer', () => {
const textarea = createTextarea('hello', 0, 5);
const chat = {} as unknown as ChatAPI;
const event = createInputEvent(textarea, "'");
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
});
it('should return false for non-wrapping characters', () => {
const textarea = createTextarea('hello', 0, 5);
const composer = createMockComposer('hello');
const chat = { composer } as unknown as ChatAPI;
const event = createInputEvent(textarea, 'a');
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
it('should return false when event.data is null', () => {
const textarea = createTextarea('hello', 0, 5);
const composer = createMockComposer('hello');
const chat = { composer } as unknown as ChatAPI;
const event = new InputEvent('input', {
data: null,
inputType: 'deleteContentBackward',
bubbles: true,
});
Object.defineProperty(event, 'target', { value: textarea, writable: false });
const result = handleSelectionWrapping(event, chat);
expect(result).toBe(false);
expect(composer.wrapSelection).not.toHaveBeenCalled();
});
});
});