Skip to content

208 add tests for a richeditor#308

Open
katsyuta wants to merge 55 commits into
DeepinkApp:masterfrom
katsyuta:208-add-tests-for-a-richeditor
Open

208 add tests for a richeditor#308
katsyuta wants to merge 55 commits into
DeepinkApp:masterfrom
katsyuta:208-add-tests-for-a-richeditor

Conversation

@katsyuta

@katsyuta katsyuta commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #208

I added Vitest projects to separate tests by environment. Projects allow us to use different setup files and test settings for different groups of tests. To distinguish tests that require a DOM environment, I suggest using the .dom.test.ts suffix.

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for the rich text editor, including markdown rendering, formatting, image handling, and list management.
  • Chores

    • Updated testing infrastructure to support DOM-based test execution alongside node-based tests.

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds DOM-level testing infrastructure for the RichEditor component: new devDependencies (@testing-library/react, jsdom), a Vitest config split into node and dom projects, global jsdom mocks in the setup file, a renderRichEditor test helper with Effector event wiring, DOM selection/cursor utilities, and a 244-line test suite covering markdown rendering, formatting, image insertion, and list operations.

Changes

RichEditor Testing Setup and Coverage

Layer / File(s) Summary
Testing infrastructure and environment setup
package.json, packages/app/vitest.config.mts, packages/app/scripts/vitest.setup.ts, packages/app/src/core/.../NotesExporter.dom.test.ts, words.txt
Adds @testing-library/react and jsdom devDependencies, splits Vitest config into node and dom projects (dom project uses jsdom env and vitest.setup.ts), installs global jsdom mocks (MockImage, getClientRects, getBoundingClientRect stubs), removes the per-file @vitest-environment jsdom directive from NotesExporter.dom.test.ts, and adds contenteditable to the word list.
RichEditor test helper utilities
packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx, packages/app/src/features/NoteEditor/tests/utils/utils.ts
Adds renderRichEditor wrapping RichEditor in Redux, Chakra, WorkspaceProvider, and editorPanelContext.Provider with Effector events and act-wrapped rerender/insert/format helpers. Adds DOM utilities selectContent and setCursorPosition that construct ranges and dispatch selectionchange.
Comprehensive RichEditor DOM test suite
packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts
Adds 244-line DOM test suite asserting markdown rendering (H1, paragraph, blockquote, hr, list, code block, link), rerender, read-only mode, image rendering and insertion (between text nodes and after a code block), heading level toggling, bold/italic toggling, combined italic+bold+strikethrough with selective removal, nested checklist states, and unordered-to-ordered list conversion.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hop hop, the tests now play,
In jsdom's garden every day.
Bold, italic, headings tall,
The editor handles them all!
No regressions, pure delight—
The rabbit checks each DOM in sight. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the PR's main objective of adding tests for a RichEditor, clearly summarizing the primary change.
Linked Issues check ✅ Passed The PR successfully establishes test infrastructure for the RichEditor with Vitest project separation, comprehensive DOM-level tests, and utilities supporting safe feature development and regression prevention as required by issue #208.
Out of Scope Changes check ✅ Passed All changes are directly scoped to establishing RichEditor test infrastructure; minor supporting changes like dependency updates and config modifications are all necessary for implementing the testing framework.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread packages/app/src/core/storage/interop/export/NotesExporter.test.ts
@katsyuta katsyuta marked this pull request as ready for review June 7, 2026 10:36
@katsyuta

katsyuta commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx (1)

44-46: 💤 Low value

Consider simplifying the initial render.

React Testing Library's render function already wraps updates in act internally, so the explicit await act(async () => render(...)) wrapper is redundant. The async callback wrapper serves no purpose here since render is synchronous.

♻️ Simplified version
-	// Use `act` to wait for all editor updates to complete before making assertions;
-	// otherwise, some asynchronous updates may not have been applied to the DOM yet.
-	const result = await act(async () => render(renderEditor(props)));
+	const result = render(renderEditor(props));

Note: The custom rerender, insert, and format methods correctly use act since they trigger state updates outside of RTL's automatic wrapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx` around
lines 44 - 46, The initial render in renderRichEditor.tsx wraps
render(renderEditor(props)) in an unnecessary await act(...) call; replace the
explicit await act(async () => render(renderEditor(props))) with a direct call
to render(renderEditor(props)) and assign its return to result (since RTL's
render already handles act), while leaving the custom rerender, insert, and
format helpers that explicitly use act unchanged; locate the call around
renderEditor and act imports and update accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app/scripts/vitest.setup.ts`:
- Around line 17-22: Element.prototype.getClientRects currently sets
Symbol.iterator to an empty function which returns undefined and breaks
iteration; update the mock returned by Element.prototype.getClientRects (the
DOMRectList stub) so its [Symbol.iterator] property is a function that returns
an iterator object with a next() method (e.g., returning { done: true }
immediately or iterating over an internal array), ensuring the returned object
conforms to the iterator protocol and matches DOMRectList behavior for for...of
and spread usage.

---

Nitpick comments:
In `@packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx`:
- Around line 44-46: The initial render in renderRichEditor.tsx wraps
render(renderEditor(props)) in an unnecessary await act(...) call; replace the
explicit await act(async () => render(renderEditor(props))) with a direct call
to render(renderEditor(props)) and assign its return to result (since RTL's
render already handles act), while leaving the custom rerender, insert, and
format helpers that explicitly use act unchanged; locate the call around
renderEditor and act imports and update accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7501d73-29c8-475e-96a6-79c0b93d9557

📥 Commits

Reviewing files that changed from the base of the PR and between c0a5387 and 78c1602.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/app/src/core/storage/interop/export/__snapshots__/NotesExporter.dom.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (8)
  • package.json
  • packages/app/scripts/vitest.setup.ts
  • packages/app/src/core/storage/interop/export/NotesExporter.dom.test.ts
  • packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts
  • packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx
  • packages/app/src/features/NoteEditor/tests/utils/utils.tsx
  • packages/app/vitest.config.mts
  • words.txt
💤 Files with no reviewable changes (1)
  • packages/app/src/core/storage/interop/export/NotesExporter.dom.test.ts

Comment thread packages/app/scripts/vitest.setup.ts Outdated
Comment on lines +17 to +18
const onFormatting = createEvent<TextFormat>();
const onInserting = createEvent<InsertingPayload>();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of simulating clicks on editor panel buttons, i suggests directly call the APIs that those buttons use under the hood. So the tests is not depends on DOM structure in editor panel ans we can change the behavior of the panel without changing the tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx`:
- Around line 51-60: The test helper functions insert and format are declared
async but do not await anything, misleading callers who use await
editor.insert(...) / await editor.format(...); update the helper implementations
in renderRichEditor.tsx by removing the async keyword from the insert and format
functions (the ones calling onInserting(payload) and onFormatting(format)) so
they are plain synchronous helpers, ensuring tests do not falsely rely on
awaiting them for Lexical/React DOM flushes.

In `@packages/app/src/features/NoteEditor/tests/utils/utils.tsx`:
- Line 34: The error message in utils.tsx is using the wrong variable: when
endNode is null the thrown Error interpolates endNode (null) instead of the
search string endText; update the throw in the code that checks if (!endNode) to
reference endText (or the correct search variable) so the Error reads something
like "Text node not found for \"<searched text>\"" to show what was being looked
for (locate the check that throws when endNode is falsy and replace the
interpolated variable).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b06610c8-eaf5-45e1-a4c2-29930f203051

📥 Commits

Reviewing files that changed from the base of the PR and between 78c1602 and 7deb02e.

📒 Files selected for processing (4)
  • packages/app/scripts/vitest.setup.ts
  • packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts
  • packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx
  • packages/app/src/features/NoteEditor/tests/utils/utils.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/app/scripts/vitest.setup.ts
  • packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts

Comment thread packages/app/src/features/NoteEditor/tests/utils/utils.tsx Outdated
@katsyuta katsyuta force-pushed the 208-add-tests-for-a-richeditor branch from e145c11 to b31cb56 Compare June 8, 2026 12:30
@katsyuta katsyuta requested a review from vitonsky June 8, 2026 12:55
Comment thread packages/app/scripts/vitest.setup.ts Outdated
"/_resources/[REDACTED-UUID]-test.txt",
"/[REDACTED-UUID].md",
"/[REDACTED-UUID].md",
"/_resources/[REDACTED-UUID]-test.txt",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why snapshot is changed?
It must not be changed. Because it means something have been changed, but it would be out of scope of that PR.

We need in reproducible tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can’t understand why the snapshot changed. This happened when I added projects to the Vitest config. Only the order of the results changed.
Maybe Vitest changes the order of test runs, and this affected the snapshot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at the snapshot. There are difference in array items order, not in test runs order.
Explore that problem reasons and fix it.

In case it cannot be fixed, we have to at least manually order results before snapshot it, to make sure we will not have such unexpected changes for snapshots in future

Comment thread packages/app/src/features/NoteEditor/__tests__/utils/renderRichEditor.tsx Outdated
Comment thread packages/app/src/features/NoteEditor/tests/utils/utils.ts Outdated
Comment thread packages/app/src/features/NoteEditor/__tests__/richEditor.dom.test.ts Outdated
Comment thread packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts Outdated
Comment thread packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts Outdated
expect(screen.getByText(content)).toBeInTheDocument();
});

test('Toggles text formatting', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need in test that would work with a multi line text and to format its parts, not whole line.

Like that

That **text** is *partially* formatted.

Only _that_ word is selected. **Not a whole line**

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the editor works incorrectly, we can’t format a part of a string or multiple strings.

So I added a test, but marked it as fails, and when we fix the implementation, it should be updated (just remove the .fails).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The tests is incomplete. We need to test a user interactions that includes clicks and keyboard inputs. Add such tests. Use only DOM events for that interactions like real user does.
  2. We must split tests into few files. We will have a hundreds tests for rich editor eventually. So we must invent the rules how to organize them. Each file must have not very much tests, but we have to support very large amount of test files. We could break the tests at least for "interactive" and non interactive. The interactive ones is where user actively interact with editor (via DOM events) and we check that editor behaves as expected. Also we could breakdown them by scope like formatting, editing, etc. Think about that, maybe consult with ChatGPT
  3. We have to support the tests with many editors. I don't see such tests that proves that 2 instances will work fine together in one document
  4. I don't see any cleanups before each test. How it does work? We must be able to control the DOM.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup with unmount calls automatically. If a test needs to call unmount manually, we can do so like this:

const editor = await renderRichEditor({ value: `` });
editor.unmount();

@katsyuta katsyuta force-pushed the 208-add-tests-for-a-richeditor branch from fa800ff to b3d1a58 Compare June 18, 2026 09:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app/src/features/NoteEditor/tests/utils/utils.ts`:
- Around line 34-37: The range.setEnd calls at lines 34 and 36 are using
endText.length and startText.length (the query string lengths) as offsets, which
can exceed the actual text node's length and cause IndexSizeError when content
spans multiple nodes. Replace endText.length with endNode.length in the
range.setEnd call within the if block, and replace startText.length with
startNode.length in the range.setEnd call within the else block to use the
actual text node bounds instead of the search query length.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 280cf089-575c-4ae9-88d4-c4a1b9664429

📥 Commits

Reviewing files that changed from the base of the PR and between b31cb56 and b3d1a58.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/app/src/core/storage/interop/export/__snapshots__/NotesExporter.dom.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (8)
  • package.json
  • packages/app/scripts/vitest.setup.ts
  • packages/app/src/core/storage/interop/export/NotesExporter.dom.test.ts
  • packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts
  • packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx
  • packages/app/src/features/NoteEditor/tests/utils/utils.ts
  • packages/app/vitest.config.mts
  • words.txt
💤 Files with no reviewable changes (1)
  • packages/app/src/core/storage/interop/export/NotesExporter.dom.test.ts
✅ Files skipped from review due to trivial changes (1)
  • words.txt
🚧 Files skipped from review as they are similar to previous changes (5)
  • package.json
  • packages/app/src/features/NoteEditor/tests/utils/renderRichEditor.tsx
  • packages/app/vitest.config.mts
  • packages/app/scripts/vitest.setup.ts
  • packages/app/src/features/NoteEditor/tests/richEditor.dom.test.ts

Comment thread packages/app/src/features/NoteEditor/__tests__/utils/utils.ts Outdated
@katsyuta katsyuta requested a review from vitonsky June 24, 2026 11:25
Comment thread package.json
Comment on lines +35 to +36
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that deps must be installed in workspace app, not globally

"/_resources/[REDACTED-UUID]-test.txt",
"/[REDACTED-UUID].md",
"/[REDACTED-UUID].md",
"/_resources/[REDACTED-UUID]-test.txt",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at the snapshot. There are difference in array items order, not in test runs order.
Explore that problem reasons and fix it.

In case it cannot be fixed, we have to at least manually order results before snapshot it, to make sure we will not have such unexpected changes for snapshots in future

await user.click(screen.getByRole('heading'));
await user.keyboard('Some text');

expect(screen.getByRole('textbox')).not.toHaveTextContent('Some text');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What text content it must contain instead? Add more specific expectation

Comment on lines +73 to +74
const [editorBoxA, editorBoxB] = screen.getAllByRole('textbox');
expect(screen.getAllByRole('textbox')).toHaveLength(2);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it is possible that after call the getAllByRole on first line the second call will return different value.

So it is more correct to call that code once, assign array into some variable, check its size, and then assign constants from there

Suggested change
const [editorBoxA, editorBoxB] = screen.getAllByRole('textbox');
expect(screen.getAllByRole('textbox')).toHaveLength(2);
const textBoxes = screen.getAllByRole('textbox');
expect(textBoxes).toHaveLength(2);
const [editorBoxA, editorBoxB] = textBoxes;

But maybe even better to use values by its indexes like that textBoxes[1].

Comment on lines +84 to +85
// editorB must stay untouched
expect(within(editorBoxB).queryByRole('emphasis')).not.toBeInTheDocument();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course it will be untouched. Because you never tried to change its content. You call format only for editorA here. What this test prove at all then?

Comment on lines +13 to +15
expect(within(editor).getAllByRole('paragraph')).toHaveLength(1);
const [paragraph] = within(editor).getAllByRole('paragraph');
expect(paragraph).toHaveTextContent('My favorite dish is cake');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as https://github.com/DeepinkApp/deepink/pull/308/changes#r3481341356
Fix that problem everywhere. It is difficult to understand the code for now because of that.

Btw, is there any methods like get or first to select first matched element?

Comment on lines +23 to +26
const [firstParagraph, secondParagraph] = within(editor).getAllByRole('paragraph');
expect(within(editor).getAllByRole('paragraph')).toHaveLength(2);
expect(firstParagraph).toHaveTextContent('My favorite dis');
expect(secondParagraph).toHaveTextContent('h is cake');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there will be 2 paragraph and 100 dividers <hr/>?
This test will not catch that problem.

Can we build our tests in a way to catch such unexpected changes?
My expectation is test would declare what is expected DOM structure and assertions would check that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tests for a RichEditor

2 participants