Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"permissions": {
"allow": ["Bash(pnpm nx test shared-react -- --reporter=verbose UsjNodesMenuPlugin)"]
}
}
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ nx run-many -t typecheck # Type check all packages
nx format:check # Check formatting
nx format:write # Fix formatting

# API extraction (run after changing a package's public API)
nx extract-api <package-name> # Update API report for specific package
nx run-many -t extract-api # Update API reports for all packages

# Development environments
nx dev perf-react # React-based PERF editor
nx dev perf-vanilla # Vanilla JS PERF editor
Expand Down Expand Up @@ -136,6 +140,7 @@ shared-react (React-specific extensions)
3. **Testing**: Run `nx test <package-name>` for specific package tests
4. **Linting**: Run `nx run-many -t lint` before committing
5. **Building**: Use `nx build <package-name>` to build specific packages
6. **API Changes**: Run `nx extract-api <package-name>` after changing a package's public API to update its API report

### Key Files and Directories

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,22 @@ To run all TS unit tests watching for file changes:

You can also use the [recommended VS Code extensions](/.vscode/extensions.json) to run tests there. This is particularly useful for running individual tests and debugging.

## API Extraction

If you change the public API of a package, run `nx extract-api` to update its API report:

```bash
nx extract-api <package-name> # e.g., nx extract-api platform-editor
```

Or update all packages at once:

```bash
nx run-many -t extract-api
```

The generated API report files should be committed alongside your changes.

## Formatting, Linting and Typechecking

Formatting happens automatically when you commit. If you use VS Code with this repo's recommended extensions, files will be formatted when you save.
Expand Down
176 changes: 12 additions & 164 deletions libs/shared-react/src/plugins/usj/UsjNodesMenuPlugin.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,7 @@ import { $isReactNodeWithMarker } from "../../nodes/usj/node-react.utils";
import * as useUsfmMarkersForMenuModule from "../PerfNodesItems/useUsfmMarkersForMenu";
import { UsjNodesMenuPlugin } from "./UsjNodesMenuPlugin";
import { baseTestEnvironment } from "./react-test.utils";
import { act } from "@testing-library/react";
import {
$getRoot,
$createTextNode,
LexicalEditor,
$getSelection,
$setSelection,
$createRangeSelection,
$createPoint,
TextNode,
} from "lexical";
import { $getRoot, $createTextNode, LexicalEditor, $getSelection, TextNode } from "lexical";
import {
$createImmutableChapterNode,
$createImpliedParaNode,
Expand Down Expand Up @@ -60,127 +50,19 @@ describe("UsjNodesMenuPlugin", () => {
});
});

describe("Verse Renumbering", () => {
it("should insert verse 3 before 3a and renumber to 4a (with verse ranges and segments)", async () => {
const { editor } = await testEnvironment();

await insertVerseNodeAtSelection(editor, "3", firstVerseTextNode);

editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1-2");
expect(secondVerseNode.getNumber()).toBe("4a");
expect(thirdVerseNode.getNumber()).toBe("5-6a");
});
});

it("should insert verse 3b before 4-5 and not renumber (with verse ranges and segments)", async () => {
const { editor } = await testEnvironment();

await insertVerseNodeAtSelection(editor, "3b", secondVerseTextNode);

editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1-2");
expect(secondVerseNode.getNumber()).toBe("3a");
expect(thirdVerseNode.getNumber()).toBe("4-5a");
});
});

it("should insert verse 2 before 2 and renumber to 3 (with normal verse numbers)", async () => {
const { editor } = await testEnvironment(() => {
$defaultInitialEditorState();
firstVerseNode.setNumber("1");
secondVerseNode.setNumber("2");
thirdVerseNode.setNumber("3");
});
editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1");
expect(secondVerseNode.getNumber()).toBe("2");
expect(thirdVerseNode.getNumber()).toBe("3");
});

await insertVerseNodeAtSelection(editor, "2", firstVerseTextNode);

editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1");
expect(secondVerseNode.getNumber()).toBe("3");
expect(thirdVerseNode.getNumber()).toBe("4");
});
});

it("should insert verse 2 before 2 and renumber to 3 but only in chapter 1 (with normal verse numbers)", async () => {
let ch2FirstVerseNode: ImmutableVerseNode;
let ch2SecondVerseNode: ImmutableVerseNode;
const { editor } = await testEnvironment(() => {
$defaultInitialEditorState();
firstVerseNode.setNumber("1");
secondVerseNode.setNumber("2");
thirdVerseNode.setNumber("3");
ch2FirstVerseNode = $createImmutableVerseNode("1");
ch2SecondVerseNode = $createImmutableVerseNode("2");
$getRoot().append(
$createImmutableChapterNode("2"),
$createParaNode().append(ch2FirstVerseNode, $createTextNode("first verse text ")),
$createParaNode().append(ch2SecondVerseNode, $createTextNode("second verse text ")),
);
});
editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1");
expect(secondVerseNode.getNumber()).toBe("2");
expect(thirdVerseNode.getNumber()).toBe("3");
expect(ch2FirstVerseNode.getNumber()).toBe("1");
expect(ch2SecondVerseNode.getNumber()).toBe("2");
});

await insertVerseNodeAtSelection(editor, "2", firstVerseTextNode);

editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1");
expect(secondVerseNode.getNumber()).toBe("3");
expect(thirdVerseNode.getNumber()).toBe("4");
expect(ch2FirstVerseNode.getNumber()).toBe("1");
expect(ch2SecondVerseNode.getNumber()).toBe("2");
});
it("should get 'p' marker from implied para to enable menu there", async () => {
let impliedPara: ImpliedParaNode;
const { editor } = await testEnvironment(() => {
impliedPara = $createImpliedParaNode();
$getRoot().append($createImmutableChapterNode("1"), impliedPara);
});

it("should get 'p' marker from implied para to enable menu there", async () => {
let impliedPara: ImpliedParaNode;
const { editor } = await testEnvironment(() => {
impliedPara = $createImpliedParaNode();
$getRoot().append($createImmutableChapterNode("1"), impliedPara);
});

editor.getEditorState().read(() => {
if (!$isImpliedParaNode(impliedPara))
throw new Error("impliedPara is not an implied para node");
if (!$isReactNodeWithMarker(impliedPara))
throw new Error("impliedPara is not a React node with marker");
expect(impliedPara.getMarker()).toBe("p");
});
});

it("should insert a verse when the paragraph is implied", async () => {
const { editor } = await testEnvironment(() => {
firstVerseNode = $createImmutableVerseNode("1");
firstVerseTextNode = $createTextNode("first verse text ");
secondVerseNode = $createImmutableVerseNode("2");
secondVerseTextNode = $createTextNode("second verse text ");
$getRoot().append(
$createImmutableChapterNode("1"),
$createImpliedParaNode().append(
firstVerseNode,
firstVerseTextNode,
secondVerseNode,
secondVerseTextNode,
),
);
});

await insertVerseNodeAtSelection(editor, "2", firstVerseTextNode);

editor.getEditorState().read(() => {
expect(firstVerseNode.getNumber()).toBe("1");
expect(secondVerseNode.getNumber()).toBe("3");
});
editor.getEditorState().read(() => {
if (!$isImpliedParaNode(impliedPara))
throw new Error("impliedPara is not an implied para node");
if (!$isReactNodeWithMarker(impliedPara))
throw new Error("impliedPara is not a React node with marker");
expect(impliedPara.getMarker()).toBe("p");
});
});

Expand Down Expand Up @@ -238,37 +120,3 @@ async function testEnvironment(
/>,
);
}

/**
* Insert a VerseNode at the selection range in the LexicalEditor.
*
* @param editor - The LexicalEditor instance where the selection will be set.
* @param verseToInsert - The verse to insert at the selection.
* @param startNode - The starting TextNode of the selection.
* @param startOffset - The offset within the startNode where the selection begins. Defaults to the
* end of the startNode's text content.
* @param endNode - The ending TextNode of the selection. Defaults to the startNode.
* @param endOffset - The offset within the endNode where the selection ends. Defaults to the
* end of the endNode's text content.
*/
async function insertVerseNodeAtSelection(
editor: LexicalEditor,
verseToInsert: string,
startNode: TextNode,
startOffset?: number,
endNode?: TextNode,
endOffset?: number,
) {
await act(async () => {
editor.update(() => {
startOffset ??= startNode.getTextContentSize();
endOffset ??= endNode ? endNode.getTextContentSize() : startOffset;
endNode ??= startNode;
const rangeSelection = $createRangeSelection();
rangeSelection.anchor = $createPoint(startNode.getKey(), startOffset, "text");
rangeSelection.focus = $createPoint(endNode.getKey(), endOffset, "text");
$setSelection(rangeSelection);
rangeSelection.insertNodes([$createImmutableVerseNode(verseToInsert)]);
});
});
}
Loading
Loading