initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,419 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {beforeEach, describe, expect, it} from 'vitest';
import {TextareaStateManager} from './TextareaStateManager';
describe('TextareaStateManager', () => {
let manager: TextareaStateManager;
beforeEach(() => {
manager = new TextareaStateManager();
});
describe('basic state management', () => {
it('should start with empty text', () => {
expect(manager.getText()).toBe('');
});
it('should update text', () => {
manager.setText('Hello world');
expect(manager.getText()).toBe('Hello world');
});
it('should track cursor position', () => {
manager.setCursorPosition(5);
expect(manager.getCursorPosition()).toBe(5);
});
it('should clear all state', () => {
manager.setText('Hello');
manager.setCursorPosition(3);
manager.clear();
expect(manager.getText()).toBe('');
expect(manager.getCursorPosition()).toBe(0);
});
});
describe('text up to cursor', () => {
it('should return text up to cursor position', () => {
manager.setText('Hello world');
manager.setCursorPosition(5);
expect(manager.getTextUpToCursor()).toBe('Hello');
});
it('should return empty string when cursor is at start', () => {
manager.setText('Hello world');
manager.setCursorPosition(0);
expect(manager.getTextUpToCursor()).toBe('');
});
it('should return full text when cursor is at end', () => {
manager.setText('Hello world');
manager.setCursorPosition(11);
expect(manager.getTextUpToCursor()).toBe('Hello world');
});
});
describe('autocomplete detection', () => {
describe('mention detection', () => {
it('should detect mention at start', () => {
manager.setText('@user');
manager.setCursorPosition(5);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'mention',
query: 'user',
matchStart: 0,
matchEnd: 5,
});
});
it('should detect mention after space', () => {
manager.setText('Hello @user');
manager.setCursorPosition(11);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'mention',
query: 'user',
matchStart: 5,
matchEnd: 11,
});
});
it('should detect partial mention', () => {
manager.setText('Hello @u');
manager.setCursorPosition(8);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'mention',
query: 'u',
matchStart: 5,
matchEnd: 8,
});
});
it('should not detect mention in middle of word', () => {
manager.setText('email@user');
manager.setCursorPosition(10);
const match = manager.detectAutocompleteMatch();
expect(match).toBeNull();
});
});
describe('channel detection', () => {
it('should detect channel mention', () => {
manager.setText('#general');
manager.setCursorPosition(8);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'channel',
query: 'general',
matchStart: 0,
matchEnd: 8,
});
});
it('should detect partial channel', () => {
manager.setText('Join #gen');
manager.setCursorPosition(9);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'channel',
query: 'gen',
matchStart: 4,
matchEnd: 9,
});
});
});
describe('emoji detection', () => {
it('should detect emoji', () => {
manager.setText(':smile');
manager.setCursorPosition(6);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'emoji',
query: 'smile',
matchStart: 0,
matchEnd: 6,
});
});
it('should detect emoji with minimum 2 chars', () => {
manager.setText(':sm');
manager.setCursorPosition(3);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'emoji',
query: 'sm',
matchStart: 0,
matchEnd: 3,
});
});
it('should not detect emoji with 1 char', () => {
manager.setText(':s');
manager.setCursorPosition(2);
const match = manager.detectAutocompleteMatch();
expect(match).toBeNull();
});
});
describe('command detection', () => {
it('should detect command at start', () => {
manager.setText('/');
manager.setCursorPosition(1);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'command',
query: '',
matchStart: 0,
matchEnd: 1,
});
});
it('should detect command after space', () => {
manager.setText('Hello /');
manager.setCursorPosition(7);
const match = manager.detectAutocompleteMatch();
expect(match).toEqual({
type: 'command',
query: '',
matchStart: 5,
matchEnd: 7,
});
});
});
describe('priority', () => {
it('should prioritize mention over others', () => {
manager.setText('@user #channel :emoji');
manager.setCursorPosition(5);
const match = manager.detectAutocompleteMatch();
expect(match?.type).toBe('mention');
});
});
});
describe('insertAutocompleteSegment', () => {
it('should insert mention segment', () => {
manager.setText('Hello @u');
manager.setCursorPosition(8);
const match = manager.detectAutocompleteMatch()!;
const result = manager.insertAutocompleteSegment(match, '@User#0001', '<@123>', 'user', '123');
expect(result.newText).toBe('Hello @User#0001 ');
expect(manager.getText()).toBe('Hello @User#0001 ');
expect(manager.getSegmentManager().getSegments()).toHaveLength(1);
});
it('should add space before mention if needed', () => {
manager.setText('Hello @u');
manager.setCursorPosition(8);
const match = manager.detectAutocompleteMatch()!;
const result = manager.insertAutocompleteSegment(match, '@User#0001', '<@123>', 'user', '123');
expect(result.newText).toBe('Hello @User#0001 ');
});
it('should preserve text after match', () => {
manager.setText('Hello @u world');
manager.setCursorPosition(8);
const match = manager.detectAutocompleteMatch()!;
const result = manager.insertAutocompleteSegment(match, '@User#0001', '<@123>', 'user', '123');
expect(result.newText).toBe('Hello @User#0001 world');
});
it('should update cursor position correctly', () => {
manager.setText('Hello @u');
manager.setCursorPosition(8);
const match = manager.detectAutocompleteMatch()!;
const result = manager.insertAutocompleteSegment(match, '@User#0001', '<@123>', 'user', '123');
expect(result.newCursorPosition).toBe(17);
});
});
describe('insertPlainText', () => {
it('should insert command without segment', () => {
manager.setText('/');
manager.setCursorPosition(1);
const match = manager.detectAutocompleteMatch()!;
const result = manager.insertPlainText(match, '¯\\_(ツ)_/¯');
expect(result.newText).toBe('¯\\_(ツ)_/¯ ');
expect(manager.getSegmentManager().getSegments()).toHaveLength(0);
});
it('should add space before text if needed', () => {
manager.setText('Hello /');
manager.setCursorPosition(7);
const match = manager.detectAutocompleteMatch()!;
const result = manager.insertPlainText(match, 'shrug');
expect(result.newText).toBe('Hello shrug ');
});
});
describe('insertSegmentAtCursor', () => {
it('should insert segment at end of text', () => {
manager.setText('Hello ');
const result = manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
expect(result).toBe('Hello @User#0001');
expect(manager.getSegmentManager().getSegments()).toHaveLength(1);
});
it('should add space if text does not end with space', () => {
manager.setText('Hello');
const result = manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
expect(result).toBe('Hello @User#0001');
});
it('should work on empty text', () => {
const result = manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
expect(result).toBe('@User#0001');
});
});
describe('getActualContent', () => {
it('should convert display text to actual content', () => {
manager.setText('Hello ');
manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
const actual = manager.getActualContent();
expect(actual).toBe('Hello <@123>');
});
it('should handle multiple segments', () => {
manager.setText('');
manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
const newText = `${manager.getText()} and `;
manager.handleTextChange(newText);
manager.insertSegmentAtCursor('@Other#0002', '<@456>', 'user', '456');
const actual = manager.getActualContent();
expect(actual).toBe('<@123> and <@456>');
});
});
describe('hasOpenCodeBlock', () => {
it('should detect open code block', () => {
manager.setText('```\ncode');
manager.setCursorPosition(8);
expect(manager.hasOpenCodeBlock()).toBe(true);
});
it('should detect closed code block', () => {
manager.setText('```\ncode\n```');
manager.setCursorPosition(13);
expect(manager.hasOpenCodeBlock()).toBe(false);
});
it('should handle no code blocks', () => {
manager.setText('normal text');
manager.setCursorPosition(11);
expect(manager.hasOpenCodeBlock()).toBe(false);
});
it('should handle multiple open/close blocks', () => {
manager.setText('```\ncode\n``` ```\nmore');
manager.setCursorPosition(19);
expect(manager.hasOpenCodeBlock()).toBe(true);
});
});
describe('handleTextChange', () => {
it('should update text and adjust segments', () => {
manager.setText('Hello ');
manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
manager.handleTextChange('Hello @User#0001 world');
expect(manager.getText()).toBe('Hello @User#0001 world');
const segments = manager.getSegmentManager().getSegments();
expect(segments).toHaveLength(1);
expect(segments[0].start).toBe(6);
expect(segments[0].end).toBe(16);
});
it('should remove segment when edited', () => {
manager.setText('Hello ');
manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
manager.handleTextChange('Hello @User');
const segments = manager.getSegmentManager().getSegments();
expect(segments).toHaveLength(0);
});
});
describe('integration scenarios', () => {
it('should handle complete autocomplete flow', () => {
manager.setText('@u');
manager.setCursorPosition(2);
const match = manager.detectAutocompleteMatch();
expect(match?.type).toBe('mention');
manager.insertAutocompleteSegment(match!, '@User#0001', '<@123>', 'user', '123');
expect(manager.getText()).toBe('@User#0001 ');
expect(manager.getActualContent()).toBe('<@123> ');
});
it('should handle typing after autocomplete', () => {
manager.setText('');
manager.insertSegmentAtCursor('@User#0001', '<@123>', 'user', '123');
manager.handleTextChange('@User#0001 how are you?');
expect(manager.getText()).toBe('@User#0001 how are you?');
expect(manager.getActualContent()).toBe('<@123> how are you?');
});
it('should handle multiple autocompletes', () => {
manager.setText('Hey @u');
manager.setCursorPosition(6);
let match = manager.detectAutocompleteMatch()!;
manager.insertAutocompleteSegment(match, '@User#0001', '<@123>', 'user', '123');
const newText = 'Hey @User#0001 check :smi';
manager.handleTextChange(newText);
manager.setCursorPosition(newText.length);
match = manager.detectAutocompleteMatch()!;
expect(match.type).toBe('emoji');
manager.insertAutocompleteSegment(match, ':smile:', '<:smile:emoji123>', 'emoji', 'emoji123');
expect(manager.getActualContent()).toBe('Hey <@123> check <:smile:emoji123> ');
});
});
});