341 lines
13 KiB
TypeScript
341 lines
13 KiB
TypeScript
![]() |
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
|
||
|
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
|
||
|
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
|
||
|
|
||
|
tap.test('Keyboard: Arrow navigation between blocks', async () => {
|
||
|
const editor: DeesInputWysiwyg = await webhelpers.fixture(
|
||
|
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
|
||
|
);
|
||
|
|
||
|
// Import multiple blocks
|
||
|
editor.importBlocks([
|
||
|
{ id: 'block-1', type: 'paragraph', content: 'First paragraph' },
|
||
|
{ id: 'block-2', type: 'paragraph', content: 'Second paragraph' },
|
||
|
{ id: 'block-3', type: 'paragraph', content: 'Third paragraph' }
|
||
|
]);
|
||
|
|
||
|
await editor.updateComplete;
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Focus first block at end
|
||
|
const firstBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
|
||
|
const firstBlockComponent = firstBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const firstBlockContainer = firstBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const firstParagraph = firstBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
// Focus and set cursor at end of first block
|
||
|
firstParagraph.focus();
|
||
|
const textNode = firstParagraph.firstChild;
|
||
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
||
|
const range = document.createRange();
|
||
|
const selection = window.getSelection();
|
||
|
range.setStart(textNode, textNode.textContent?.length || 0);
|
||
|
range.setEnd(textNode, textNode.textContent?.length || 0);
|
||
|
selection?.removeAllRanges();
|
||
|
selection?.addRange(range);
|
||
|
}
|
||
|
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Press ArrowRight to move to second block
|
||
|
const arrowRightEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'ArrowRight',
|
||
|
code: 'ArrowRight',
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
firstParagraph.dispatchEvent(arrowRightEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Check if second block is focused
|
||
|
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
|
||
|
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
// Check if the second paragraph has focus
|
||
|
const activeElement = secondBlockComponent.shadowRoot?.activeElement;
|
||
|
expect(activeElement).toEqual(secondParagraph);
|
||
|
|
||
|
console.log('Arrow navigation test complete');
|
||
|
});
|
||
|
|
||
|
tap.test('Keyboard: Backspace merges blocks', async () => {
|
||
|
const editor: DeesInputWysiwyg = await webhelpers.fixture(
|
||
|
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
|
||
|
);
|
||
|
|
||
|
// Import two blocks
|
||
|
editor.importBlocks([
|
||
|
{ id: 'merge-1', type: 'paragraph', content: 'First' },
|
||
|
{ id: 'merge-2', type: 'paragraph', content: 'Second' }
|
||
|
]);
|
||
|
|
||
|
await editor.updateComplete;
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Focus second block at beginning
|
||
|
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="merge-2"]');
|
||
|
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
// Focus and set cursor at beginning
|
||
|
secondParagraph.focus();
|
||
|
const textNode = secondParagraph.firstChild;
|
||
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
||
|
const range = document.createRange();
|
||
|
const selection = window.getSelection();
|
||
|
range.setStart(textNode, 0);
|
||
|
range.setEnd(textNode, 0);
|
||
|
selection?.removeAllRanges();
|
||
|
selection?.addRange(range);
|
||
|
}
|
||
|
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Press Backspace to merge with previous block
|
||
|
const backspaceEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'Backspace',
|
||
|
code: 'Backspace',
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
secondParagraph.dispatchEvent(backspaceEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Check if blocks were merged
|
||
|
expect(editor.blocks.length).toEqual(1);
|
||
|
expect(editor.blocks[0].content).toContain('First');
|
||
|
expect(editor.blocks[0].content).toContain('Second');
|
||
|
|
||
|
console.log('Backspace merge test complete');
|
||
|
});
|
||
|
|
||
|
tap.test('Keyboard: Delete key on non-editable blocks', async () => {
|
||
|
const editor: DeesInputWysiwyg = await webhelpers.fixture(
|
||
|
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
|
||
|
);
|
||
|
|
||
|
// Import blocks including a divider
|
||
|
editor.importBlocks([
|
||
|
{ id: 'para-1', type: 'paragraph', content: 'Before divider' },
|
||
|
{ id: 'div-1', type: 'divider', content: '' },
|
||
|
{ id: 'para-2', type: 'paragraph', content: 'After divider' }
|
||
|
]);
|
||
|
|
||
|
await editor.updateComplete;
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Focus the divider block
|
||
|
const dividerBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="div-1"]');
|
||
|
const dividerBlockComponent = dividerBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const dividerBlockContainer = dividerBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const dividerElement = dividerBlockContainer?.querySelector('.block.divider') as HTMLElement;
|
||
|
|
||
|
// Non-editable blocks need to be focused differently
|
||
|
dividerElement?.focus();
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Press Delete to remove the divider
|
||
|
const deleteEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'Delete',
|
||
|
code: 'Delete',
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
dividerElement.dispatchEvent(deleteEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Check if divider was removed
|
||
|
expect(editor.blocks.length).toEqual(2);
|
||
|
expect(editor.blocks.find(b => b.type === 'divider')).toBeUndefined();
|
||
|
|
||
|
console.log('Delete key on non-editable block test complete');
|
||
|
});
|
||
|
|
||
|
tap.test('Keyboard: Tab key in code block', async () => {
|
||
|
const editor: DeesInputWysiwyg = await webhelpers.fixture(
|
||
|
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
|
||
|
);
|
||
|
|
||
|
// Import a code block
|
||
|
editor.importBlocks([
|
||
|
{ id: 'code-1', type: 'code', content: 'function test() {', metadata: { language: 'javascript' } }
|
||
|
]);
|
||
|
|
||
|
await editor.updateComplete;
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Focus code block
|
||
|
const codeBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
|
||
|
const codeBlockComponent = codeBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const codeBlockContainer = codeBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const codeElement = codeBlockContainer?.querySelector('.block.code') as HTMLElement;
|
||
|
|
||
|
// Focus and set cursor at end
|
||
|
codeElement.focus();
|
||
|
const textNode = codeElement.firstChild;
|
||
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
||
|
const range = document.createRange();
|
||
|
const selection = window.getSelection();
|
||
|
range.setStart(textNode, textNode.textContent?.length || 0);
|
||
|
range.setEnd(textNode, textNode.textContent?.length || 0);
|
||
|
selection?.removeAllRanges();
|
||
|
selection?.addRange(range);
|
||
|
}
|
||
|
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Press Tab to insert spaces
|
||
|
const tabEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'Tab',
|
||
|
code: 'Tab',
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
codeElement.dispatchEvent(tabEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Check if spaces were inserted
|
||
|
const updatedContent = codeElement.textContent || '';
|
||
|
expect(updatedContent).toContain(' '); // Tab should insert 2 spaces
|
||
|
|
||
|
console.log('Tab in code block test complete');
|
||
|
});
|
||
|
|
||
|
tap.test('Keyboard: ArrowUp/Down navigation', async () => {
|
||
|
const editor: DeesInputWysiwyg = await webhelpers.fixture(
|
||
|
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
|
||
|
);
|
||
|
|
||
|
// Import multiple blocks
|
||
|
editor.importBlocks([
|
||
|
{ id: 'nav-1', type: 'paragraph', content: 'First line' },
|
||
|
{ id: 'nav-2', type: 'paragraph', content: 'Second line' },
|
||
|
{ id: 'nav-3', type: 'paragraph', content: 'Third line' }
|
||
|
]);
|
||
|
|
||
|
await editor.updateComplete;
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Focus second block
|
||
|
const secondBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-2"]');
|
||
|
const secondBlockComponent = secondBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const secondBlockContainer = secondBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const secondParagraph = secondBlockContainer?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
secondParagraph.focus();
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Press ArrowUp to move to first block
|
||
|
const arrowUpEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'ArrowUp',
|
||
|
code: 'ArrowUp',
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
secondParagraph.dispatchEvent(arrowUpEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Check if first block is focused
|
||
|
const firstBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-1"]');
|
||
|
const firstBlockComponent = firstBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const firstParagraph = firstBlockComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
expect(firstBlockComponent.shadowRoot?.activeElement).toEqual(firstParagraph);
|
||
|
|
||
|
// Now press ArrowDown twice to get to third block
|
||
|
const arrowDownEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'ArrowDown',
|
||
|
code: 'ArrowDown',
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
firstParagraph.dispatchEvent(arrowDownEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Second block should be focused, dispatch again
|
||
|
const secondActiveElement = secondBlockComponent.shadowRoot?.activeElement;
|
||
|
if (secondActiveElement) {
|
||
|
secondActiveElement.dispatchEvent(arrowDownEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
}
|
||
|
|
||
|
// Check if third block is focused
|
||
|
const thirdBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="nav-3"]');
|
||
|
const thirdBlockComponent = thirdBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const thirdParagraph = thirdBlockComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
expect(thirdBlockComponent.shadowRoot?.activeElement).toEqual(thirdParagraph);
|
||
|
|
||
|
console.log('ArrowUp/Down navigation test complete');
|
||
|
});
|
||
|
|
||
|
tap.test('Keyboard: Formatting shortcuts', async () => {
|
||
|
const editor: DeesInputWysiwyg = await webhelpers.fixture(
|
||
|
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
|
||
|
);
|
||
|
|
||
|
// Import a paragraph
|
||
|
editor.importBlocks([
|
||
|
{ id: 'format-1', type: 'paragraph', content: 'Test formatting' }
|
||
|
]);
|
||
|
|
||
|
await editor.updateComplete;
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Focus and select text
|
||
|
const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="format-1"]');
|
||
|
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
|
||
|
const blockContainer = blockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
|
||
|
const paragraph = blockContainer?.querySelector('.block.paragraph') as HTMLElement;
|
||
|
|
||
|
paragraph.focus();
|
||
|
|
||
|
// Select "formatting"
|
||
|
const textNode = paragraph.firstChild;
|
||
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
||
|
const range = document.createRange();
|
||
|
const selection = window.getSelection();
|
||
|
range.setStart(textNode, 5); // After "Test "
|
||
|
range.setEnd(textNode, 15); // After "formatting"
|
||
|
selection?.removeAllRanges();
|
||
|
selection?.addRange(range);
|
||
|
}
|
||
|
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
|
||
|
// Press Cmd/Ctrl+B for bold
|
||
|
const boldEvent = new KeyboardEvent('keydown', {
|
||
|
key: 'b',
|
||
|
code: 'KeyB',
|
||
|
metaKey: true, // Use metaKey for Mac, ctrlKey for Windows/Linux
|
||
|
bubbles: true,
|
||
|
cancelable: true,
|
||
|
composed: true
|
||
|
});
|
||
|
|
||
|
paragraph.dispatchEvent(boldEvent);
|
||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
|
||
|
// Check if bold was applied
|
||
|
const content = paragraph.innerHTML;
|
||
|
expect(content).toContain('<strong>') || expect(content).toContain('<b>');
|
||
|
|
||
|
console.log('Formatting shortcuts test complete');
|
||
|
});
|
||
|
|
||
|
export default tap.start();
|