mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-02-06 09:55:47 +00:00
test: add suggestion system component tests for TipTap autocomplete
- Add HashtagSuggestion.cy.tsx (16 tests): popup trigger, filtering, keyboard/click selection, new tag creation, escape to close - Add ItemMentionSuggestion.cy.tsx (16 tests): popup trigger, filtering, keyboard/click selection, markdown serialization, edge cases - Add SuggestionList.cy.tsx (14 tests): rendering, keyboard navigation, click selection, empty state - Update TestEditor.tsx to support enableSuggestions prop for testing These tests validate user-facing autocomplete behaviors for # and @ mentions in the rich text editor.
This commit is contained in:
parent
942559247e
commit
c48aed88cd
293
lib/cypress/component/TipTap/HashtagSuggestion.cy.tsx
Normal file
293
lib/cypress/component/TipTap/HashtagSuggestion.cy.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
/// <reference types="cypress" />
|
||||
import { mount } from 'cypress/react'
|
||||
|
||||
import { TestEditorWithRouter, createTestTag } from './TestEditor'
|
||||
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
describe('Hashtag Suggestion System', () => {
|
||||
const testTags = [
|
||||
createTestTag('nature', '#22C55E'),
|
||||
createTestTag('coding', '#3B82F6'),
|
||||
createTestTag('music', '#EF4444'),
|
||||
createTestTag('travel', '#F59E0B'),
|
||||
createTestTag('food', '#8B5CF6'),
|
||||
]
|
||||
|
||||
describe('Suggestion Popup Trigger', () => {
|
||||
it('shows suggestion popup when typing #', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#')
|
||||
cy.get('.tippy-content').should('be.visible')
|
||||
cy.get('.tippy-content button').should('have.length.at.least', 1)
|
||||
})
|
||||
|
||||
it('shows all tags when # is typed without query', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#')
|
||||
cy.get('.tippy-content button').should('have.length', 5)
|
||||
})
|
||||
|
||||
it('does not show popup when # is inside a word', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('email#test')
|
||||
cy.get('.tippy-content').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filtering', () => {
|
||||
it('filters tags based on typed query', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#na')
|
||||
cy.get('.tippy-content button').should('have.length', 1)
|
||||
cy.get('.tippy-content button').should('contain.text', '#nature')
|
||||
})
|
||||
|
||||
it('filters are case-insensitive', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#NAT')
|
||||
cy.get('.tippy-content button').should('contain.text', '#nature')
|
||||
})
|
||||
|
||||
it('shows multiple matching tags', () => {
|
||||
const tags = [createTestTag('music'), createTestTag('museum'), createTestTag('muse')]
|
||||
|
||||
mount(<TestEditorWithRouter content='' tags={tags} editable={true} enableSuggestions={true} />)
|
||||
|
||||
cy.get('.ProseMirror').type('#mu')
|
||||
cy.get('.tippy-content button').should('have.length', 3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Keyboard Selection', () => {
|
||||
it('selects first item with Enter', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#nat{enter}')
|
||||
cy.get('.hashtag')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('#nature')
|
||||
})
|
||||
})
|
||||
|
||||
it('navigates with ArrowDown and selects with Enter', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#')
|
||||
cy.get('.tippy-content button').first().should('have.class', 'tw:bg-base-200')
|
||||
|
||||
cy.get('.ProseMirror').type('{downarrow}')
|
||||
cy.get('.tippy-content button').eq(1).should('have.class', 'tw:bg-base-200')
|
||||
|
||||
cy.get('.ProseMirror').type('{enter}')
|
||||
cy.get('.hashtag')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('#coding')
|
||||
})
|
||||
})
|
||||
|
||||
it('closes popup with Escape', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#')
|
||||
cy.get('.tippy-content').should('exist')
|
||||
|
||||
cy.get('.ProseMirror').type('{esc}')
|
||||
cy.get('.tippy-box').should('not.exist')
|
||||
})
|
||||
|
||||
it('selects first item with Space', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#nat ')
|
||||
cy.get('.hashtag')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('#nature')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Create New Tag', () => {
|
||||
it('shows "create new" option when no exact match', () => {
|
||||
const onAddTag = cy.stub().as('addTag')
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onAddTag={onAddTag}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#newtag')
|
||||
cy.get('.tippy-content').should('contain.text', 'Neu:')
|
||||
cy.get('.tippy-content').should('contain.text', '#newtag')
|
||||
})
|
||||
|
||||
it('does not show "create new" when exact match exists', () => {
|
||||
const onAddTag = cy.stub().as('addTag')
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onAddTag={onAddTag}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#nature')
|
||||
cy.get('.tippy-content').should('not.contain.text', 'Neu:')
|
||||
})
|
||||
|
||||
it('calls onAddTag when creating new tag', () => {
|
||||
const onAddTag = cy.stub().as('addTag')
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onAddTag={onAddTag}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#brandnew{enter}')
|
||||
cy.get('@addTag').should('have.been.calledOnce')
|
||||
cy.get('@addTag')
|
||||
.its('firstCall.args.0')
|
||||
.should('deep.include', { name: 'brandnew', color: '#888888' })
|
||||
})
|
||||
|
||||
it('inserts new tag into editor', () => {
|
||||
const onAddTag = cy.stub()
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onAddTag={onAddTag}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#brandnew{enter}')
|
||||
cy.get('.hashtag')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('#brandnew')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Click Selection', () => {
|
||||
it('inserts tag when clicking suggestion', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
tags={testTags}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#')
|
||||
cy.get('.tippy-content button').contains('#music').click()
|
||||
cy.get('.hashtag')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('#music')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Suggestion Styling', () => {
|
||||
it('applies tag colors in suggestion list', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' tags={testTags} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('#nat')
|
||||
cy.get('.tippy-content button')
|
||||
.first()
|
||||
.should('have.css', 'color')
|
||||
.and('match', /rgb\(34, 197, 94\)|#22[cC]55[eE]/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
304
lib/cypress/component/TipTap/ItemMentionSuggestion.cy.tsx
Normal file
304
lib/cypress/component/TipTap/ItemMentionSuggestion.cy.tsx
Normal file
@ -0,0 +1,304 @@
|
||||
/// <reference types="cypress" />
|
||||
import { mount } from 'cypress/react'
|
||||
|
||||
import { TestEditorWithRouter, createTestItem } from './TestEditor'
|
||||
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
describe('Item Mention Suggestion System', () => {
|
||||
const testItems = [
|
||||
createTestItem('id-1', 'Alice', '#22C55E'),
|
||||
createTestItem('id-2', 'Bob', '#3B82F6'),
|
||||
createTestItem('id-3', 'Charlie', '#EF4444'),
|
||||
createTestItem('id-4', 'Diana', '#F59E0B'),
|
||||
createTestItem('id-5', 'Eve', '#8B5CF6'),
|
||||
]
|
||||
|
||||
describe('Suggestion Popup Trigger', () => {
|
||||
it('shows suggestion popup when typing @', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content').should('be.visible')
|
||||
cy.get('.tippy-content button').should('have.length.at.least', 1)
|
||||
})
|
||||
|
||||
it('shows all items when @ is typed without query', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content button').should('have.length', 5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filtering', () => {
|
||||
it('filters items based on typed query', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@ali')
|
||||
cy.get('.tippy-content button').should('have.length', 1)
|
||||
cy.get('.tippy-content button').should('contain.text', '@Alice')
|
||||
})
|
||||
|
||||
it('filters are case-insensitive', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@ALICE')
|
||||
cy.get('.tippy-content button').should('contain.text', '@Alice')
|
||||
})
|
||||
|
||||
it('matches items containing query (not just starts with)', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@li')
|
||||
cy.get('.tippy-content button').should('contain.text', '@Alice')
|
||||
cy.get('.tippy-content button').should('contain.text', '@Charlie')
|
||||
})
|
||||
|
||||
it('shows empty state when no items match', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@xyz123')
|
||||
cy.get('.tippy-content').should('contain.text', 'Keine Ergebnisse')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Keyboard Selection', () => {
|
||||
it('selects first item with Enter', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@ali{enter}')
|
||||
cy.get('.item-mention')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('[@Alice]')
|
||||
expect(markdown).to.include('/item/id-1')
|
||||
})
|
||||
})
|
||||
|
||||
it('navigates with ArrowDown and selects with Enter', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content button').first().should('have.class', 'tw:bg-base-200')
|
||||
|
||||
cy.get('.ProseMirror').type('{downarrow}')
|
||||
cy.get('.tippy-content button').eq(1).should('have.class', 'tw:bg-base-200')
|
||||
|
||||
cy.get('.ProseMirror').type('{enter}')
|
||||
cy.get('.item-mention')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('[@Bob]')
|
||||
})
|
||||
})
|
||||
|
||||
it('closes popup with Escape', () => {
|
||||
mount(
|
||||
<TestEditorWithRouter content='' items={testItems} editable={true} enableSuggestions={true} />,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content').should('exist')
|
||||
|
||||
cy.get('.ProseMirror').type('{esc}')
|
||||
cy.get('.tippy-box').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Click Selection', () => {
|
||||
it('inserts mention when clicking suggestion', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content button').contains('@Charlie').click()
|
||||
cy.get('.item-mention')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('[@Charlie]')
|
||||
expect(markdown).to.include('/item/id-3')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Suggestion Styling', () => {
|
||||
it('applies item colors in suggestion list via getItemColor', () => {
|
||||
const getItemColor = (item: any) => item?.color ?? '#000000'
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
getItemColor={getItemColor}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@ali')
|
||||
cy.get('.tippy-content button')
|
||||
.first()
|
||||
.should('have.css', 'color')
|
||||
.and('match', /rgb\(34, 197, 94\)|#22[cC]55[eE]/)
|
||||
})
|
||||
|
||||
it('uses getItemColor function when provided', () => {
|
||||
const getItemColor = () => '#FF00FF'
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
getItemColor={getItemColor}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content button')
|
||||
.first()
|
||||
.should('have.css', 'color')
|
||||
.and('match', /rgb\(255, 0, 255\)|#[fF]{2}00[fF]{2}/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Markdown Serialization', () => {
|
||||
it('serializes item mention to correct markdown format', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@dia{enter}')
|
||||
cy.get('.item-mention')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.match(/\[@Diana\]\(\/item\/id-4\)/)
|
||||
})
|
||||
})
|
||||
|
||||
it('inserts space after mention for continued typing', () => {
|
||||
let editorInstance: Editor
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={testItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
onReady={(editor) => {
|
||||
editorInstance = editor
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@eve{enter}is here')
|
||||
cy.get('.item-mention')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
const markdown = editorInstance.getMarkdown()
|
||||
expect(markdown).to.include('@Eve')
|
||||
expect(markdown).to.include('is here')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles items without names', () => {
|
||||
const itemsWithEmpty = [...testItems, { id: 'no-name', name: '' } as any]
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={itemsWithEmpty}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content button').should('have.length', 5)
|
||||
})
|
||||
|
||||
it('limits suggestions to 8 items', () => {
|
||||
const manyItems = Array.from({ length: 15 }, (_, i) =>
|
||||
createTestItem(`id-${i}`, `Person ${i}`),
|
||||
)
|
||||
|
||||
mount(
|
||||
<TestEditorWithRouter
|
||||
content=''
|
||||
items={manyItems}
|
||||
editable={true}
|
||||
enableSuggestions={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
cy.get('.ProseMirror').type('@')
|
||||
cy.get('.tippy-content button').should('have.length', 8)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
183
lib/cypress/component/TipTap/SuggestionList.cy.tsx
Normal file
183
lib/cypress/component/TipTap/SuggestionList.cy.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
/// <reference types="cypress" />
|
||||
import { mount } from 'cypress/react'
|
||||
import { createRef } from 'react'
|
||||
|
||||
import { SuggestionList, SuggestionListRef } from '#components/TipTap/extensions/suggestions/SuggestionList'
|
||||
|
||||
import { createTestItem, createTestTag } from './TestEditor'
|
||||
|
||||
describe('SuggestionList Component', () => {
|
||||
describe('Rendering', () => {
|
||||
it('renders hashtag items with # prefix', () => {
|
||||
const tags = [createTestTag('nature'), createTestTag('coding')]
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').should('have.length', 2)
|
||||
cy.get('button').first().should('contain.text', '#nature')
|
||||
cy.get('button').last().should('contain.text', '#coding')
|
||||
})
|
||||
|
||||
it('renders item mentions with @ prefix', () => {
|
||||
const items = [createTestItem('1', 'Alice'), createTestItem('2', 'Bob')]
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={items} command={command} type='item' />)
|
||||
|
||||
cy.get('button').should('have.length', 2)
|
||||
cy.get('button').first().should('contain.text', '@Alice')
|
||||
cy.get('button').last().should('contain.text', '@Bob')
|
||||
})
|
||||
|
||||
it('renders "new tag" option with Neu: prefix', () => {
|
||||
const items = [createTestTag('existing'), { isNew: true, name: 'newtag' }]
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={items} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').should('have.length', 2)
|
||||
cy.get('button').last().should('contain.text', 'Neu:')
|
||||
cy.get('button').last().should('contain.text', '#newtag')
|
||||
})
|
||||
|
||||
it('renders empty state message when no items', () => {
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={[]} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').should('not.exist')
|
||||
cy.contains('Keine Ergebnisse').should('exist')
|
||||
})
|
||||
|
||||
it('applies tag color to hashtag items', () => {
|
||||
const tags = [createTestTag('nature', '#22C55E')]
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button')
|
||||
.first()
|
||||
.should('have.css', 'color')
|
||||
.and('match', /rgb\(34, 197, 94\)|#22[cC]55[eE]/)
|
||||
})
|
||||
|
||||
it('applies item color via getItemColor', () => {
|
||||
const items = [createTestItem('1', 'Alice')]
|
||||
const getItemColor = () => '#EF4444'
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={items} command={command} type='item' getItemColor={getItemColor} />)
|
||||
|
||||
cy.get('button')
|
||||
.first()
|
||||
.should('have.css', 'color')
|
||||
.and('match', /rgb\(239, 68, 68\)|#[eE][fF]4444/)
|
||||
})
|
||||
|
||||
it('highlights first item by default', () => {
|
||||
const tags = [createTestTag('first'), createTestTag('second')]
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').first().should('have.class', 'tw:bg-base-200')
|
||||
cy.get('button').last().should('not.have.class', 'tw:bg-base-200')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Click Selection', () => {
|
||||
it('calls command with clicked item', () => {
|
||||
const tags = [createTestTag('nature'), createTestTag('coding')]
|
||||
const command = cy.stub().as('command')
|
||||
|
||||
mount(<SuggestionList items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').last().click()
|
||||
cy.get('@command').should('have.been.calledOnceWith', tags[1])
|
||||
})
|
||||
|
||||
it('calls command with new tag item', () => {
|
||||
const newItem = { isNew: true, name: 'newtag' }
|
||||
const items = [newItem]
|
||||
const command = cy.stub().as('command')
|
||||
|
||||
mount(<SuggestionList items={items} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').click()
|
||||
cy.get('@command').should('have.been.calledOnceWith', newItem)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Keyboard Navigation', () => {
|
||||
it('navigates down with ArrowDown', () => {
|
||||
const tags = [createTestTag('first'), createTestTag('second'), createTestTag('third')]
|
||||
const ref = createRef<SuggestionListRef>()
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList ref={ref} items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.get('button').first().should('have.class', 'tw:bg-base-200')
|
||||
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })))
|
||||
cy.get('button').eq(1).should('have.class', 'tw:bg-base-200')
|
||||
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })))
|
||||
cy.get('button').eq(2).should('have.class', 'tw:bg-base-200')
|
||||
})
|
||||
|
||||
it('navigates up with ArrowUp', () => {
|
||||
const tags = [createTestTag('first'), createTestTag('second')]
|
||||
const ref = createRef<SuggestionListRef>()
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList ref={ref} items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowUp' })))
|
||||
cy.get('button').last().should('have.class', 'tw:bg-base-200')
|
||||
})
|
||||
|
||||
it('wraps around at end of list', () => {
|
||||
const tags = [createTestTag('first'), createTestTag('second')]
|
||||
const ref = createRef<SuggestionListRef>()
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList ref={ref} items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })))
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })))
|
||||
cy.get('button').first().should('have.class', 'tw:bg-base-200')
|
||||
})
|
||||
|
||||
it('selects item with Enter', () => {
|
||||
const tags = [createTestTag('first'), createTestTag('second')]
|
||||
const ref = createRef<SuggestionListRef>()
|
||||
const command = cy.stub().as('command')
|
||||
|
||||
mount(<SuggestionList ref={ref} items={tags} command={command} type='hashtag' />)
|
||||
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })))
|
||||
cy.get('button').eq(1).should('have.class', 'tw:bg-base-200')
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'Enter' })))
|
||||
cy.get('@command').should('have.been.calledOnceWith', tags[1])
|
||||
})
|
||||
|
||||
it('resets selection index when items change', () => {
|
||||
const tags1 = [createTestTag('aaa'), createTestTag('aab')]
|
||||
const tags2 = [createTestTag('bbb'), createTestTag('bbc'), createTestTag('bbd')]
|
||||
const ref = createRef<SuggestionListRef>()
|
||||
const command = cy.stub()
|
||||
|
||||
mount(<SuggestionList ref={ref} items={tags1} command={command} type='hashtag' />).then(
|
||||
({ rerender }) => {
|
||||
cy.then(() => ref.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })))
|
||||
cy.get('button').eq(1).should('have.class', 'tw:bg-base-200')
|
||||
|
||||
rerender(<SuggestionList ref={ref} items={tags2} command={command} type='hashtag' />)
|
||||
cy.get('button').first().should('have.class', 'tw:bg-base-200')
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,10 +3,16 @@ import { Link } from '@tiptap/extension-link'
|
||||
import { Markdown } from '@tiptap/markdown'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
|
||||
import { Hashtag, ItemMention, VideoEmbed } from '#components/TipTap/extensions'
|
||||
import {
|
||||
Hashtag,
|
||||
ItemMention,
|
||||
VideoEmbed,
|
||||
createHashtagSuggestion,
|
||||
createItemMentionSuggestion,
|
||||
} from '#components/TipTap/extensions'
|
||||
import { createConfiguredMarked } from '#components/TipTap/utils/configureMarked'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
@ -20,10 +26,12 @@ export interface TestEditorProps {
|
||||
editable?: boolean
|
||||
tags?: Tag[]
|
||||
onTagClick?: (tag: Tag) => void
|
||||
onAddTag?: (tag: Tag) => void
|
||||
items?: Item[]
|
||||
getItemColor?: (item: Item | undefined, fallback?: string) => string
|
||||
onReady?: (editor: Editor) => void
|
||||
testId?: string
|
||||
enableSuggestions?: boolean
|
||||
}
|
||||
|
||||
export function TestEditor({
|
||||
@ -31,11 +39,23 @@ export function TestEditor({
|
||||
editable = false,
|
||||
tags = [],
|
||||
onTagClick,
|
||||
onAddTag,
|
||||
items = [],
|
||||
getItemColor,
|
||||
onReady,
|
||||
testId = 'test-editor',
|
||||
enableSuggestions = false,
|
||||
}: TestEditorProps) {
|
||||
const hashtagSuggestion = useMemo(
|
||||
() => (enableSuggestions ? createHashtagSuggestion(tags, onAddTag) : undefined),
|
||||
[tags, onAddTag, enableSuggestions],
|
||||
)
|
||||
|
||||
const itemMentionSuggestion = useMemo(
|
||||
() => (enableSuggestions ? createItemMentionSuggestion(items, getItemColor) : undefined),
|
||||
[items, getItemColor, enableSuggestions],
|
||||
)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
@ -51,10 +71,12 @@ export function TestEditor({
|
||||
Hashtag.configure({
|
||||
tags,
|
||||
onTagClick,
|
||||
suggestion: hashtagSuggestion,
|
||||
}),
|
||||
ItemMention.configure({
|
||||
items,
|
||||
getItemColor,
|
||||
suggestion: itemMentionSuggestion,
|
||||
}),
|
||||
],
|
||||
content,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user