Skip to content

Commit b691cf0

Browse files
karam-qaoudKaram QaoudzurfyxLuciNyanhanford
authored
Add E2E test for TableOfContentsPlugin (#2675)
* Fixed getStyleObjectFromRawCSS function to work for unformatted css strings * Testing that handles unformatted css text * Testing that $getStyleObjectFromRawCss handles unformatted css * Added TableOfContents * Renamed TableOfContetnsPlugin file name and added flow file * Added TableOfContentsPlugin to config files and added styling * Fixed types * Added TableOfContentsList as a seperate module * Fixed type of tag from string to HeadingTagType * Table of contents updates as user scrolls * Wrapped plugin in a feature * Deleted package-lock.json * Fixed conditioanl rendering syntax * Removed extra parameter * package-lock * fix imports * Update packages/lexical-playground/src/plugins/TableOfContentsPlugin.tsx Co-authored-by: Gerard Rovira <zurfyx@users.noreply.github.com> * Added sticky styling and handled text overflow * Table of contents updates automtaically on scroll without observing all heading nodes * Update table correctly when headings are not visible but exist either up or down * Fix failing E2E * Changed isTableOfContentes to showTableOfContents in settings * Added useEffect to fix memory leak * Hoisted functions that don't use props * Renamed isTableOfContets to showTableOfContents * Changing selectedHeading by observing page top * resolved lint error * Refactored scroll up logic * Added comments * Added better css * Changed place of toc div to fix failing test * Fixed adjacent headings scrolling * Fixed adjacent headings bug * Renamed helper methods * Fixed test * Added dependency array to useEffect * Added TableOfContents to dependency array * Updated dependeny array in useEffect * Created e2e test file for table of contents plugin * Added scroll test * E2E test: Adding heading to editor adds them to table-of-contents * clean up * Refactored getEditorElement * Scrolling callback has better conditions * Table of contents is now covering all edge cases and doesn't freeze webpage * Solved page freezing * Added one more assert statment to second test * chore(lexical-playground): make directory clear (#2674) * Conditionally utilize `startTransition` if it's present (#2676) * Only utilize startTransition if it's available * Add type annotation * Run prettier * fix(lexical-list): remove list breaks if selection in empty (#2672) * fix(lexical-list): remove list breaks if selection in empty * chore: add a comment * chore: add test * Separate `@lexical/code` into more atomic modules (#2673) * separate code package into more atomic modules * remove utils * named exports * Fixed typo (#2678) * fix: path to icons (#2683) * Fix VALID_TWITTER_URL to allow underscores. (#2690) * fix(lexical-playground): LexicalTypeaheadMenuPlugin import (#2689) Use the correct import path that available in NPM package * Collapse and Expand DevTools Tree Nodes (#2679) * fix(playground): fix rendering Exclidraw (#2694) * Make includeHeaders a boolean (#2697) Changed type for includeHeaders parameter from string to boolean to match the type of the parameter from the $createTableNodeWithDimensions function. * Remove coverage reports (#2699) * fix: check if options are empty (#2701) * feat: Link node with target and rel (#2687) * OnChangePlugin ignoreInitialChange -> ignoreHistoryMergeTagChange (#2706) * OnChangePlugin ignoreInitialChange -> ignoreHistoryMergeTag * . * default to false because 0.4 Co-authored-by: Karam Qaoud <kqaoud@fb.com> Co-authored-by: Gerard Rovira <zurfyx@users.noreply.github.com> Co-authored-by: 子瞻 Luci <haru.lucinyan@gmail.com> Co-authored-by: Jack Hanford <jackhanford@gmail.com> Co-authored-by: John Flockton <thegreatercurve@users.noreply.github.com> Co-authored-by: SalvadorLekan <66782276+SalvadorLekan@users.noreply.github.com> Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com> Co-authored-by: hiraoka <62982380+y-hiraoka@users.noreply.github.com> Co-authored-by: Elvin Dzhavadov <elvin.d@outlook.com> Co-authored-by: Will <will.gutierrez@gmail.com> Co-authored-by: Bryan <ImSingee@users.noreply.github.com> Co-authored-by: alinamusuroi <44519061+alinamusuroi@users.noreply.github.com> Co-authored-by: Andriy Chemerynskiy <andrzej.chem@gmail.com>
1 parent 9f65cbd commit b691cf0

File tree

4 files changed

+155
-37
lines changed

4 files changed

+155
-37
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import {
10+
assertHTML,
11+
focusEditor,
12+
getElement,
13+
html,
14+
initialize,
15+
repeat,
16+
selectFromFormatDropdown,
17+
sleep,
18+
test,
19+
} from '../utils/index.mjs';
20+
21+
test.describe('Table of Contents', () => {
22+
test.beforeEach(({isCollab, page}) => initialize({isCollab, page}));
23+
test(`Adding headigns to editor adds them to table of contents`, async ({
24+
page,
25+
}) => {
26+
await focusEditor(page);
27+
await selectFromFormatDropdown(page, '.h1');
28+
await page.keyboard.type('Hello');
29+
await page.keyboard.type('\n');
30+
await selectFromFormatDropdown(page, '.h1');
31+
await page.keyboard.type('World!');
32+
const tableOfContents = await getElement(page, 'ul.table-of-contents');
33+
await assertHTML(
34+
tableOfContents,
35+
html`
36+
<div class="heading" role="button" tabindex="0">
37+
<div class="bar"></div>
38+
<li>Hello</li>
39+
</div>
40+
<div class="heading" role="button" tabindex="0">
41+
<div class="bar"></div>
42+
<li>World!</li>
43+
</div>
44+
`,
45+
);
46+
});
47+
test(`Scrolling through headigns in the editor makes them scroll inside the table of contents`, async ({
48+
page,
49+
}) => {
50+
await focusEditor(page);
51+
await selectFromFormatDropdown(page, '.h1');
52+
await page.keyboard.type('Hello');
53+
54+
await repeat(20, () => {
55+
page.keyboard.type('\n');
56+
});
57+
await selectFromFormatDropdown(page, '.h2');
58+
await page.keyboard.type(' World');
59+
await repeat(400, () => {
60+
page.keyboard.type('\n');
61+
});
62+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
63+
await sleep(50);
64+
const tableOfContents = await getElement(page, 'ul.table-of-contents');
65+
await assertHTML(
66+
tableOfContents,
67+
html`
68+
<div class="heading" role="button" tabindex="0">
69+
<div class="bar"></div>
70+
<li>Hello</li>
71+
</div>
72+
<div class="selectedHeading" role="button" tabindex="0">
73+
<div class="circle"></div>
74+
<li class="heading2">World</li>
75+
</div>
76+
`,
77+
);
78+
await page.evaluate(() => window.scrollTo(0, 0));
79+
await sleep(50);
80+
await assertHTML(
81+
tableOfContents,
82+
html`
83+
<div class="selectedHeading" role="button" tabindex="0">
84+
<div class="circle"></div>
85+
<li>Hello</li>
86+
</div>
87+
<div class="heading" role="button" tabindex="0">
88+
<div class="bar"></div>
89+
<li class="heading2">World</li>
90+
</div>
91+
`,
92+
);
93+
94+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
95+
await sleep(50);
96+
await assertHTML(
97+
tableOfContents,
98+
html`
99+
<div class="heading" role="button" tabindex="0">
100+
<div class="bar"></div>
101+
<li>Hello</li>
102+
</div>
103+
<div class="selectedHeading" role="button" tabindex="0">
104+
<div class="circle"></div>
105+
<li class="heading2">World</li>
106+
</div>
107+
`,
108+
);
109+
});
110+
});

packages/lexical-playground/__tests__/utils/index.mjs

+13-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function initialize({
3636
isCharLimitUtf8,
3737
isMaxLength,
3838
showNestedEditorTreeView,
39+
showTableOfContents,
3940
}) {
4041
const appSettings = {};
4142
appSettings.isRichText = IS_RICH_TEXT;
@@ -48,6 +49,9 @@ export async function initialize({
4849
if (showNestedEditorTreeView === undefined) {
4950
appSettings.showNestedEditorTreeView = true;
5051
}
52+
if (showTableOfContents === undefined) {
53+
appSettings.showTableOfContents = true;
54+
}
5155
appSettings.isAutocomplete = !!isAutocomplete;
5256
appSettings.isCharLimit = !!isCharLimit;
5357
appSettings.isCharLimitUtf8 = !!isCharLimitUtf8;
@@ -385,9 +389,10 @@ export async function getHTML(page, selector = 'div[contenteditable="true"]') {
385389
return element.innerHTML();
386390
}
387391

388-
export async function getEditorElement(page, parentSelector = '.editor-shell') {
389-
const selector = `${parentSelector} div[contenteditable="true"]`;
390-
392+
export async function getElement(
393+
page,
394+
selector = 'div[contenteditable="true"]',
395+
) {
391396
if (IS_COLLAB) {
392397
const leftFrame = await page.frame('left');
393398
await leftFrame.waitForSelector(selector);
@@ -398,6 +403,11 @@ export async function getEditorElement(page, parentSelector = '.editor-shell') {
398403
}
399404
}
400405

406+
export async function getEditorElement(page, parentSelector = '.editor-shell') {
407+
const selector = `${parentSelector} div[contenteditable="true"]`;
408+
return getElement(selector);
409+
}
410+
401411
export async function waitForSelector(page, selector, options) {
402412
if (IS_COLLAB) {
403413
const leftFrame = await page.frame('left');

packages/lexical-playground/src/plugins/TableOfContentsPlugin/index.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
font-size: 15px;
4242
}
4343

44-
.remove-ul-style {
44+
.table-of-contents {
4545
list-style: none;
4646
padding: 0%;
4747
margin-top: 10px;

packages/lexical-playground/src/plugins/TableOfContentsPlugin/index.tsx

+31-33
Original file line numberDiff line numberDiff line change
@@ -57,60 +57,58 @@ function TableOfContentsList({
5757

5858
useEffect(() => {
5959
function scrollCallback() {
60-
if (
61-
tableOfContents.length !== 0 &&
62-
selectedIndex.current < tableOfContents.length - 1
63-
) {
60+
if (tableOfContents.length !== 0) {
6461
let currentHeading = editor.getElementByKey(
6562
tableOfContents[selectedIndex.current][0],
6663
);
6764
if (currentHeading !== null) {
68-
if (isHeadingBelowTheTopOfThePage(currentHeading)) {
69-
//On natural scroll, user is scrolling up
65+
if (isHeadingAboveViewport(currentHeading)) {
66+
//On natural scroll, user is scrolling down
7067
while (
7168
currentHeading !== null &&
72-
isHeadingBelowTheTopOfThePage(currentHeading) &&
73-
selectedIndex.current > 0
69+
isHeadingAboveViewport(currentHeading) &&
70+
selectedIndex.current < tableOfContents.length - 1
7471
) {
75-
const prevHeading = editor.getElementByKey(
76-
tableOfContents[selectedIndex.current - 1][0],
72+
const nextHeading = editor.getElementByKey(
73+
tableOfContents[++selectedIndex.current][0],
7774
);
7875
if (
79-
prevHeading !== null &&
80-
(isHeadingAboveViewport(prevHeading) ||
81-
isHeadingBelowTheTopOfThePage(prevHeading))
76+
nextHeading !== null &&
77+
isHeadingBelowTheTopOfThePage(nextHeading)
8278
) {
83-
selectedIndex.current--;
79+
break;
80+
} else {
81+
const nextHeadingKey =
82+
tableOfContents[selectedIndex.current][0];
83+
setSelectedKey(nextHeadingKey);
84+
currentHeading = nextHeading;
8485
}
85-
currentHeading = prevHeading;
8686
}
87-
const prevHeadingKey = tableOfContents[selectedIndex.current][0];
88-
setSelectedKey(prevHeadingKey);
89-
} else if (isHeadingAboveViewport(currentHeading)) {
90-
//On natural scroll, user is scrolling down
87+
} else if (isHeadingBelowTheTopOfThePage(currentHeading)) {
88+
//On natural scroll, user is scrolling up
9189
while (
9290
currentHeading !== null &&
93-
isHeadingAboveViewport(currentHeading) &&
94-
selectedIndex.current < tableOfContents.length - 1
91+
isHeadingBelowTheTopOfThePage(currentHeading) &&
92+
selectedIndex.current > 0
9593
) {
96-
const nextHeading = editor.getElementByKey(
97-
tableOfContents[selectedIndex.current + 1][0],
94+
const prevHeading = editor.getElementByKey(
95+
tableOfContents[--selectedIndex.current][0],
9896
);
97+
const prevHeadingKey = tableOfContents[selectedIndex.current][0];
98+
setSelectedKey(prevHeadingKey);
9999
if (
100-
nextHeading !== null &&
101-
(isHeadingAtTheTopOfThePage(nextHeading) ||
102-
isHeadingAboveViewport(nextHeading))
100+
prevHeading !== null &&
101+
isHeadingBelowTheTopOfThePage(currentHeading) &&
102+
(isHeadingAboveViewport(prevHeading) ||
103+
isHeadingAtTheTopOfThePage(prevHeading))
103104
) {
104-
selectedIndex.current++;
105+
break;
106+
} else {
107+
currentHeading = prevHeading;
105108
}
106-
currentHeading = nextHeading;
107109
}
108-
const nextHeadingKey = tableOfContents[selectedIndex.current][0];
109-
setSelectedKey(nextHeadingKey);
110110
}
111111
}
112-
} else {
113-
selectedIndex.current = 0;
114112
}
115113
}
116114
let timerId: ReturnType<typeof setTimeout>;
@@ -129,7 +127,7 @@ function TableOfContentsList({
129127
}, [tableOfContents, editor]);
130128

131129
return (
132-
<ul className="remove-ul-style">
130+
<ul className="table-of-contents">
133131
{tableOfContents.map(([key, text, tag], index) => (
134132
<div
135133
className={selectedKey === key ? 'selectedHeading' : 'heading'}

0 commit comments

Comments
 (0)