Skip to content

Commit 6d040f8

Browse files
authored
Build unsupport block (#183)
* chore: build unsupport block * chore: clippy
1 parent 5e43dc2 commit 6d040f8

File tree

5 files changed

+529
-30
lines changed

5 files changed

+529
-30
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { AuthTestUtils } from '../../../support/auth-utils';
2+
import { waitForReactUpdate } from '../../../support/selectors';
3+
import { generateRandomEmail } from '../../../support/test-config';
4+
5+
describe('Unsupported Block Display', () => {
6+
const authUtils = new AuthTestUtils();
7+
const testEmail = generateRandomEmail();
8+
9+
before(() => {
10+
cy.viewport(1280, 720);
11+
});
12+
13+
beforeEach(() => {
14+
cy.on('uncaught:exception', (err) => {
15+
if (
16+
err.message.includes('Minified React error') ||
17+
err.message.includes('View not found') ||
18+
err.message.includes('No workspace or service found') ||
19+
// Slate editor errors when DOM doesn't match Slate state during testing
20+
err.message.includes('Cannot resolve a DOM point from Slate point') ||
21+
err.message.includes('Cannot resolve a DOM node from Slate node') ||
22+
// React errors during dynamic block injection
23+
err.message.includes('Invalid hook call')
24+
) {
25+
return false;
26+
}
27+
28+
return true;
29+
});
30+
31+
cy.session(
32+
testEmail,
33+
() => {
34+
authUtils.signInWithTestUrl(testEmail);
35+
},
36+
{
37+
validate: () => {
38+
cy.window().then((win) => {
39+
const token = win.localStorage.getItem('af_auth_token');
40+
41+
expect(token).to.be.ok;
42+
});
43+
},
44+
}
45+
);
46+
47+
cy.visit('/app');
48+
cy.url({ timeout: 30000 }).should('include', '/app');
49+
cy.contains('Getting started', { timeout: 10000 }).should('be.visible').click();
50+
cy.wait(2000);
51+
52+
// Ensure any open menus are closed
53+
cy.get('body').type('{esc}');
54+
55+
cy.get('[data-slate-editor="true"]').should('exist').click({ force: true });
56+
cy.focused().type('{selectall}{backspace}');
57+
waitForReactUpdate(500);
58+
});
59+
60+
describe('Unsupported Block Rendering', () => {
61+
it('should display unsupported block message for unknown block types', () => {
62+
// Wait for editor to be ready
63+
waitForReactUpdate(500);
64+
65+
// Insert an unsupported block type via the exposed Yjs document
66+
cy.window().then((win) => {
67+
const testWindow = win as Window & {
68+
__TEST_DOC__?: {
69+
getMap: (key: string) => unknown;
70+
transact: (fn: () => void) => void;
71+
};
72+
Y?: {
73+
Map: new () => Map<string, unknown>;
74+
Text: new () => unknown;
75+
Array: new <T>() => { push: (items: T[]) => void };
76+
};
77+
};
78+
79+
const doc = testWindow.__TEST_DOC__;
80+
const Y = testWindow.Y;
81+
82+
if (!doc || !Y) {
83+
throw new Error('Test utilities not found. Ensure app is running in dev mode.');
84+
}
85+
86+
// Get the document structure
87+
// Structure: doc.getMap('data').get('document') -> { blocks, meta, page_id }
88+
const sharedRoot = doc.getMap('data') as Map<string, unknown>;
89+
const document = sharedRoot.get('document') as Map<string, unknown>;
90+
const blocks = document.get('blocks') as Map<string, unknown>;
91+
const meta = document.get('meta') as Map<string, unknown>;
92+
const pageId = document.get('page_id') as string;
93+
const childrenMap = meta.get('children_map') as Map<string, unknown>;
94+
const textMap = meta.get('text_map') as Map<string, unknown>;
95+
96+
// Generate a unique block ID
97+
const blockId = `test_unsupported_${Date.now()}`;
98+
99+
// Insert an unsupported block type
100+
doc.transact(() => {
101+
const block = new Y.Map();
102+
103+
block.set('id', blockId);
104+
block.set('ty', 'future_block_type_not_yet_implemented'); // Unknown block type
105+
block.set('children', blockId);
106+
block.set('external_id', blockId);
107+
block.set('external_type', 'text');
108+
block.set('parent', pageId);
109+
block.set('data', '{}');
110+
111+
(blocks as Map<string, unknown>).set(blockId, block);
112+
113+
// Add to page children
114+
const pageChildren = childrenMap.get(pageId) as { push: (items: string[]) => void };
115+
116+
if (pageChildren) {
117+
pageChildren.push([blockId]);
118+
}
119+
120+
// Create empty text for the block
121+
const blockText = new Y.Text();
122+
123+
(textMap as Map<string, unknown>).set(blockId, blockText);
124+
125+
// Create empty children array
126+
const blockChildren = new Y.Array<string>();
127+
128+
(childrenMap as Map<string, unknown>).set(blockId, blockChildren);
129+
});
130+
});
131+
132+
waitForReactUpdate(1000);
133+
134+
// Verify the unsupported block component is rendered
135+
cy.get('[data-testid="unsupported-block"]').should('exist');
136+
cy.get('[data-testid="unsupported-block"]').should('be.visible');
137+
138+
// Verify it shows the correct message
139+
cy.get('[data-testid="unsupported-block"]')
140+
.should('contain.text', 'not supported yet')
141+
.and('contain.text', 'future_block_type_not_yet_implemented');
142+
});
143+
144+
it('should display warning icon and block type name', () => {
145+
// Insert an unsupported block with a specific type name
146+
const testBlockType = 'my_custom_unknown_block';
147+
148+
cy.window().then((win) => {
149+
const testWindow = win as Window & {
150+
__TEST_DOC__?: {
151+
getMap: (key: string) => unknown;
152+
transact: (fn: () => void) => void;
153+
};
154+
Y?: {
155+
Map: new () => Map<string, unknown>;
156+
Text: new () => unknown;
157+
Array: new <T>() => { push: (items: T[]) => void };
158+
};
159+
};
160+
161+
const doc = testWindow.__TEST_DOC__;
162+
const Y = testWindow.Y;
163+
164+
if (!doc || !Y) {
165+
throw new Error('Test utilities not found. Ensure app is running in dev mode.');
166+
}
167+
168+
// Structure: doc.getMap('data').get('document') -> { blocks, meta, page_id }
169+
const sharedRoot = doc.getMap('data') as Map<string, unknown>;
170+
const document = sharedRoot.get('document') as Map<string, unknown>;
171+
const blocks = document.get('blocks') as Map<string, unknown>;
172+
const meta = document.get('meta') as Map<string, unknown>;
173+
const pageId = document.get('page_id') as string;
174+
const childrenMap = meta.get('children_map') as Map<string, unknown>;
175+
const textMap = meta.get('text_map') as Map<string, unknown>;
176+
177+
const blockId = `test_${Date.now()}`;
178+
179+
doc.transact(() => {
180+
const block = new Y.Map();
181+
182+
block.set('id', blockId);
183+
block.set('ty', testBlockType);
184+
block.set('children', blockId);
185+
block.set('external_id', blockId);
186+
block.set('external_type', 'text');
187+
block.set('parent', pageId);
188+
block.set('data', '{}');
189+
190+
(blocks as Map<string, unknown>).set(blockId, block);
191+
192+
const pageChildren = childrenMap.get(pageId) as { push: (items: string[]) => void };
193+
194+
if (pageChildren) {
195+
pageChildren.push([blockId]);
196+
}
197+
198+
const blockText = new Y.Text();
199+
200+
(textMap as Map<string, unknown>).set(blockId, blockText);
201+
202+
const blockChildren = new Y.Array<string>();
203+
204+
(childrenMap as Map<string, unknown>).set(blockId, blockChildren);
205+
});
206+
});
207+
208+
waitForReactUpdate(1000);
209+
210+
// Verify the unsupported block shows the type name
211+
cy.get('[data-testid="unsupported-block"]')
212+
.should('be.visible')
213+
.and('contain.text', testBlockType);
214+
215+
// Verify it has the warning styling (contains an SVG icon)
216+
cy.get('[data-testid="unsupported-block"] svg').should('exist');
217+
});
218+
219+
it('should be non-editable', () => {
220+
// Insert an unsupported block
221+
cy.window().then((win) => {
222+
const testWindow = win as Window & {
223+
__TEST_DOC__?: {
224+
getMap: (key: string) => unknown;
225+
transact: (fn: () => void) => void;
226+
};
227+
Y?: {
228+
Map: new () => Map<string, unknown>;
229+
Text: new () => unknown;
230+
Array: new <T>() => { push: (items: T[]) => void };
231+
};
232+
};
233+
234+
const doc = testWindow.__TEST_DOC__;
235+
const Y = testWindow.Y;
236+
237+
if (!doc || !Y) {
238+
throw new Error('Test utilities not found.');
239+
}
240+
241+
// Structure: doc.getMap('data').get('document') -> { blocks, meta, page_id }
242+
const sharedRoot = doc.getMap('data') as Map<string, unknown>;
243+
const document = sharedRoot.get('document') as Map<string, unknown>;
244+
const blocks = document.get('blocks') as Map<string, unknown>;
245+
const meta = document.get('meta') as Map<string, unknown>;
246+
const pageId = document.get('page_id') as string;
247+
const childrenMap = meta.get('children_map') as Map<string, unknown>;
248+
const textMap = meta.get('text_map') as Map<string, unknown>;
249+
250+
const blockId = `test_readonly_${Date.now()}`;
251+
252+
doc.transact(() => {
253+
const block = new Y.Map();
254+
255+
block.set('id', blockId);
256+
block.set('ty', 'readonly_test_block');
257+
block.set('children', blockId);
258+
block.set('external_id', blockId);
259+
block.set('external_type', 'text');
260+
block.set('parent', pageId);
261+
block.set('data', '{}');
262+
263+
(blocks as Map<string, unknown>).set(blockId, block);
264+
265+
const pageChildren = childrenMap.get(pageId) as { push: (items: string[]) => void };
266+
267+
if (pageChildren) {
268+
pageChildren.push([blockId]);
269+
}
270+
271+
const blockText = new Y.Text();
272+
273+
(textMap as Map<string, unknown>).set(blockId, blockText);
274+
275+
const blockChildren = new Y.Array<string>();
276+
277+
(childrenMap as Map<string, unknown>).set(blockId, blockChildren);
278+
});
279+
});
280+
281+
waitForReactUpdate(1000);
282+
283+
// Verify the unsupported block has contentEditable=false
284+
cy.get('[data-testid="unsupported-block"]')
285+
.should('have.attr', 'contenteditable', 'false');
286+
});
287+
});
288+
});

src/components/editor/CollaborativeEditor.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,34 @@ function CollaborativeEditor({
9191
setIsConnected(true);
9292
onEditorConnected?.(editor);
9393

94+
// Expose editor and doc for E2E testing in development/test mode
95+
if (import.meta.env.DEV || import.meta.env.MODE === 'test') {
96+
const testWindow = window as Window & {
97+
__TEST_EDITOR__?: YjsEditor;
98+
__TEST_DOC__?: Y.Doc;
99+
Y?: typeof Y;
100+
};
101+
102+
testWindow.__TEST_EDITOR__ = editor;
103+
testWindow.__TEST_DOC__ = doc;
104+
testWindow.Y = Y; // Expose Yjs module for creating test blocks
105+
}
106+
94107
return () => {
95108
console.debug('disconnect');
96109
editor.disconnect();
110+
// Clean up test references
111+
if (import.meta.env.DEV || import.meta.env.MODE === 'test') {
112+
const testWindow = window as Window & {
113+
__TEST_EDITOR__?: YjsEditor;
114+
__TEST_DOC__?: Y.Doc;
115+
Y?: typeof Y;
116+
};
117+
118+
delete testWindow.__TEST_EDITOR__;
119+
delete testWindow.__TEST_DOC__;
120+
// Keep Y exposed as it might be needed for other editors
121+
}
97122
};
98123
// eslint-disable-next-line react-hooks/exhaustive-deps
99124
}, [editor]);

src/components/editor/components/element/BlockNotFound.tsx

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,38 @@ import { forwardRef } from 'react';
44
import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock';
55
import { EditorElementProps } from '@/components/editor/editor.type';
66

7-
export const BlockNotFound = forwardRef<HTMLDivElement, EditorElementProps>(({ node, children }, ref) => {
7+
export const BlockNotFound = forwardRef<HTMLDivElement, EditorElementProps>(({ node, children, ...attributes }, ref) => {
88
const type = node.type;
99

10-
if (import.meta.env.DEV) {
11-
if (type === 'block_not_found') {
12-
return (
13-
<div
14-
className={'w-full my-1 select-none'}
15-
ref={ref}
16-
contentEditable={false}
10+
// Special case for blocks that reference deleted/moved blocks (dev only)
11+
if (import.meta.env.DEV && type === 'block_not_found') {
12+
return (
13+
<div
14+
className={'my-1 w-full select-none'}
15+
ref={ref}
16+
contentEditable={false}
17+
>
18+
<Alert
19+
className={'h-fit w-full'}
20+
severity={'error'}
1721
>
18-
<Alert
19-
className={'h-fit w-full'}
20-
severity={'error'}
21-
>
22-
<div className={'text-base'}>{`Block not found, id is ${node.blockId}`}</div>
23-
<div>
24-
{'It might be deleted or moved to another place but the children map is still referencing it.'}
25-
</div>
26-
</Alert>
27-
</div>
28-
);
29-
}
22+
<div className={'text-base'}>{`Block not found, id is ${node.blockId}`}</div>
23+
<div>
24+
{'It might be deleted or moved to another place but the children map is still referencing it.'}
25+
</div>
26+
</Alert>
27+
</div>
28+
);
29+
}
3030

31-
return <UnSupportedBlock
31+
// Show unsupported block component for all unknown block types
32+
return (
33+
<UnSupportedBlock
3234
ref={ref}
3335
node={node}
34-
>{children}</UnSupportedBlock>;
35-
}
36-
37-
return <div
38-
className={'w-full h-0 select-none'}
39-
ref={ref}
40-
contentEditable={false}
41-
/>;
36+
{...attributes}
37+
>
38+
{children}
39+
</UnSupportedBlock>
40+
);
4241
});

0 commit comments

Comments
 (0)