Skip to content

Commit cae7b9f

Browse files
committed
cypress: init and editor tests
1 parent 30649e9 commit cae7b9f

File tree

70 files changed

+11992
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+11992
-1
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,8 @@ packages/**/*.tgz
121121
packages/**/dist
122122

123123
uptime-kuma-data
124+
125+
# Cypress
126+
packages/webapp/cypress/videos/
127+
packages/webapp/cypress/screenshots/
128+
packages/webapp/cypress/downloads/

Makefile

+7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ front_dev:
5454
local:
5555
make -j 4 supabase_start back_dev back_ws front_dev
5656

57+
# Run Cypress tests
58+
cypress_open:
59+
cd packages/webapp && npm run cypress:open
60+
61+
cypress_run:
62+
cd packages/webapp && npm run cypress:run
63+
5764
# Start editor development server
5865
dev_editor:
5966
cd packages/webapp && npm run dev

packages/webapp/components/pages/document/components/EditorContent.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const EditorContent = ({ className }: { className?: string }) => {
4242
<TiptapEditor
4343
ref={editorElement}
4444
className={twMerge(
45-
`tiptap__editor relative ${!applyingFilters ? 'block' : 'hidden'}`,
45+
`tiptap__editor docy_editor relative ${!applyingFilters ? 'block' : 'hidden'}`,
4646
className
4747
)}
4848
editor={editor}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* This component provides testing hooks for critical editor operations like copy/paste.
3+
* It renders invisible buttons that can be triggered programmatically or via keyboard shortcuts.
4+
* These buttons are critical for automated testing with Cypress and provide a consistent way
5+
* to trigger clipboard operations across different browsers and environments.
6+
*
7+
* */
8+
9+
import { Editor } from '@tiptap/react'
10+
import { useClipboardShortcuts } from './hooks/useClipboardShortcuts'
11+
import { useHierarchicalSelection } from './hooks/useHierarchicalSelection'
12+
import { useEffect, useState } from 'react'
13+
import { useClipboardListener } from './hooks/useClipboardListener'
14+
/**
15+
* Controllers Component
16+
*
17+
* This component provides testing hooks and advanced selection utilities
18+
* for the editor, including hierarchical selection and clipboard operations.
19+
*/
20+
type SelectionLevel = 'element' | 'parent' | 'section' | 'heading' | 'list' | 'document'
21+
22+
type ControllersProps = {
23+
editor: Editor | null
24+
debug?: boolean
25+
}
26+
27+
const Controllers = ({ editor, debug = false }: ControllersProps) => {
28+
const [isTestEnv, setIsTestEnv] = useState(false)
29+
30+
useEffect(() => {
31+
setIsTestEnv(
32+
process.env.NODE_ENV === 'test' || process.env.NEXT_PUBLIC_DEBUG_CONTROLLERS === 'true'
33+
)
34+
}, [])
35+
36+
// Clipboard shortcuts
37+
useClipboardShortcuts()
38+
useClipboardListener()
39+
// Hierarchical selection utilities
40+
const { selectHierarchical, selectElement } = useHierarchicalSelection(editor)
41+
42+
// Copy selected content
43+
const copySelectedContent = () => {
44+
if (!editor) return
45+
46+
editor.commands.focus()
47+
const copyEvent = new ClipboardEvent('copy', {
48+
bubbles: true,
49+
cancelable: true
50+
})
51+
document.activeElement?.dispatchEvent(copyEvent)
52+
if (!copyEvent.defaultPrevented) {
53+
document.execCommand('copy')
54+
}
55+
56+
console.log('[Controllers] Copy operation triggered')
57+
}
58+
59+
// Paste from clipboard
60+
const pasteFromClipboard = () => {
61+
if (!editor) return
62+
63+
editor.commands.focus()
64+
const pasteEvent = new ClipboardEvent('paste', {
65+
bubbles: true,
66+
cancelable: true
67+
})
68+
document.activeElement?.dispatchEvent(pasteEvent)
69+
if (!pasteEvent.defaultPrevented) {
70+
document.execCommand('paste')
71+
}
72+
73+
console.log('[Controllers] Paste operation triggered')
74+
}
75+
76+
// Select hierarchically and then copy
77+
const selectAndCopy = (level: SelectionLevel) => {
78+
if (!editor) return
79+
selectHierarchical(level)
80+
setTimeout(() => copySelectedContent(), 10)
81+
}
82+
83+
// Expose selection methods globally
84+
useEffect(() => {
85+
if (!editor) return
86+
87+
// @ts-ignore
88+
window._editorSelectAndCopy = selectAndCopy
89+
// @ts-ignore
90+
window._editorSelect = selectHierarchical
91+
// @ts-ignore
92+
window._editorSelectElement = selectElement
93+
94+
return () => {
95+
// @ts-ignore
96+
delete window._editorSelectAndCopy
97+
// @ts-ignore
98+
delete window._editorSelect
99+
// @ts-ignore
100+
delete window._editorSelectElement
101+
}
102+
}, [editor, selectHierarchical, selectElement])
103+
104+
const buttonClasses =
105+
debug || isTestEnv
106+
? 'p-2 bg-blue-500 text-white text-xs rounded m-1'
107+
: 'opacity-0 pointer-events-auto m-1'
108+
109+
return (
110+
<div className="controller-buttons absolute right-0 top-0 z-50 flex flex-col items-end">
111+
{/* Clipboard controls */}
112+
<div className="flex">
113+
<button
114+
onClick={copySelectedContent}
115+
id="btn_copyselectedcontent"
116+
className={buttonClasses}
117+
aria-label="Copy selected text"
118+
title="Copy selected text (like Cmd+C)"
119+
data-testid="copy-button">
120+
{debug || isTestEnv ? 'Copy' : ''}
121+
</button>
122+
123+
<button
124+
onClick={pasteFromClipboard}
125+
id="btn_pastefromclipboard"
126+
className={buttonClasses}
127+
aria-label="Paste from clipboard"
128+
title="Paste from clipboard (like Cmd+V)"
129+
data-testid="paste-button">
130+
{debug || isTestEnv ? 'Paste' : ''}
131+
</button>
132+
</div>
133+
134+
{/* Selection controls */}
135+
{(debug || isTestEnv) && (
136+
<div className="mt-2 flex max-w-xs flex-wrap justify-end">
137+
<button
138+
onClick={() => selectHierarchical('element')}
139+
id="btn_select_element"
140+
className={buttonClasses}
141+
data-testid="select-element">
142+
Select Element
143+
</button>
144+
145+
<button
146+
onClick={() => selectHierarchical('parent')}
147+
id="btn_select_parent"
148+
className={buttonClasses}
149+
data-testid="select-parent">
150+
Select Parent
151+
</button>
152+
153+
<button
154+
onClick={() => selectHierarchical('section')}
155+
id="btn_select_section"
156+
className={buttonClasses}
157+
data-testid="select-section">
158+
Select Section
159+
</button>
160+
161+
<button
162+
onClick={() => selectHierarchical('heading')}
163+
id="btn_select_heading"
164+
className={buttonClasses}
165+
data-testid="select-heading">
166+
Select Heading
167+
</button>
168+
169+
<button
170+
onClick={() => selectHierarchical('list')}
171+
id="btn_select_list"
172+
className={buttonClasses}
173+
data-testid="select-list">
174+
Select List
175+
</button>
176+
177+
<button
178+
onClick={() => selectHierarchical('document')}
179+
id="btn_select_document"
180+
className={buttonClasses}
181+
data-testid="select-document">
182+
Select All
183+
</button>
184+
</div>
185+
)}
186+
</div>
187+
)
188+
}
189+
190+
export default Controllers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Editor } from '@tiptap/core'
2+
3+
export const createDocumentFromStructure = ({ editor }: { editor: Editor }) => {
4+
return (structure: any) => {
5+
if (!editor || !editor.commands) return false
6+
7+
// Clear editor
8+
editor.commands.clearContent()
9+
10+
if (!structure.sections || !Array.isArray(structure.sections)) {
11+
console.error('Invalid document structure: missing sections array')
12+
return false
13+
}
14+
15+
// Build a complete document content array
16+
const content = structure.sections.map((section: any) => {
17+
// Create heading node for section title
18+
const sectionNode = {
19+
type: 'heading',
20+
attrs: { level: 1 },
21+
content: [
22+
{
23+
type: 'contentHeading',
24+
attrs: { level: 1 },
25+
content: [{ type: 'text', text: section.title || 'Untitled Section' }]
26+
},
27+
{
28+
type: 'contentWrapper',
29+
content: []
30+
}
31+
]
32+
}
33+
34+
// Process section contents
35+
if (Array.isArray(section.contents)) {
36+
// Get reference to the contentWrapper where paragraphs will go
37+
const contentWrapper = sectionNode.content[1].content
38+
39+
// Add each content item to the wrapper
40+
section.contents.forEach((item: any) => {
41+
if (item.type === 'paragraph') {
42+
// @ts-ignore
43+
contentWrapper.push({
44+
type: 'paragraph',
45+
content: [{ type: 'text', text: item.content || ' ' }]
46+
})
47+
}
48+
// Add handling for other content types as needed
49+
})
50+
}
51+
52+
return sectionNode
53+
})
54+
55+
// console.log('Inserting content:', JSON.stringify(content, null, 2))
56+
57+
try {
58+
// Insert the complete document structure
59+
if (content.length > 0) {
60+
editor.commands.insertContent(content)
61+
return true
62+
}
63+
return false
64+
} catch (err) {
65+
console.error('Error inserting content:', err)
66+
67+
return false
68+
}
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect } from 'react'
2+
3+
// Custom hook to listen for clipboard events
4+
export const useClipboardListener = () => {
5+
useEffect(() => {
6+
const handleCopy = async () => {
7+
// Small delay to allow the clipboard to update
8+
setTimeout(async () => {
9+
try {
10+
// Try to read HTML content using paste event
11+
const clipboardData = await navigator.clipboard.read()
12+
for (const item of clipboardData) {
13+
if (item.types.includes('text/html')) {
14+
const blob = await item.getType('text/html')
15+
const html = await blob.text()
16+
console.log('Clipboard HTML content =>', html)
17+
}
18+
}
19+
} catch (error) {
20+
console.error('Failed to read clipboard =>', error)
21+
}
22+
}, 100)
23+
}
24+
25+
document.addEventListener('copy', handleCopy)
26+
27+
return () => {
28+
document.removeEventListener('copy', handleCopy)
29+
}
30+
}, [])
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
*
3+
* This hook registers keyboard shortcuts for copy/paste operations and
4+
* exposes functions to programmatically trigger copy/paste actions.
5+
*
6+
* It accomplishes three key tasks:
7+
* 1. Listens for keyboard shortcuts (Cmd/Ctrl+C, Cmd/Ctrl+V)
8+
* 2. Triggers the corresponding buttons in the DOM
9+
* 3. Exposes global triggers via window for testing frameworks
10+
* */
11+
12+
import { useEffect } from 'react'
13+
14+
export const useClipboardShortcuts = () => {
15+
// Function to programmatically trigger the copy button
16+
const triggerCopyButton = () => {
17+
document.getElementById('btn_copyselectedcontent')?.click()
18+
}
19+
20+
// Function to programmatically trigger the paste button
21+
const triggerPasteButton = () => {
22+
document.getElementById('btn_pastefromclipboard')?.click()
23+
}
24+
25+
// Add keyboard shortcut listeners for both Copy and Paste
26+
useEffect(() => {
27+
const handleKeyDown = (e: KeyboardEvent) => {
28+
// Check if Cmd+C (Mac) or Ctrl+C (Windows) is pressed
29+
if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
30+
// Let the default copy happen, but also trigger our button
31+
setTimeout(() => triggerCopyButton(), 0)
32+
}
33+
34+
// Check if Cmd+V (Mac) or Ctrl+V (Windows) is pressed
35+
if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
36+
// Let the default paste happen, but also trigger our button
37+
setTimeout(() => triggerPasteButton(), 0)
38+
}
39+
}
40+
41+
document.addEventListener('keydown', handleKeyDown)
42+
return () => document.removeEventListener('keydown', handleKeyDown)
43+
}, [])
44+
45+
// Expose both trigger functions to window for external access
46+
useEffect(() => {
47+
// @ts-ignore
48+
window._triggerCopy = triggerCopyButton
49+
// @ts-ignore
50+
window._triggerPaste = triggerPasteButton
51+
}, [])
52+
}

0 commit comments

Comments
 (0)