This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-7355-2cd542f613df3a07fccff0fbe97a9a9c012ed2a2
in repository https://gitbox.apache.org/repos/asf/texera.git

commit bba2951adb3c089a22c7854cbb8dfc4230500eeb
Author: Xinyuan Lin <[email protected]>
AuthorDate: Thu Aug 6 20:04:44 2026 -0700

    test(frontend): cover the markdown editor's toolbar insert (#7355)
    
    ### What changes were proposed in this PR?
    
    Every toolbar action in the description editor — bold, link, and the
    rest — routes through `insert`, and it was the one method in the
    component with no coverage (the spec had 15 tests, none touching it).
    
    It splices the action's prefix and suffix around the current textarea
    selection, so wrong offsets silently corrupt the text the toolbar is
    meant to format:
    
    ```
    "hello world!"  select [6,11]  +  {prefix:"**", suffix:"**"}   ->  "hello 
**world**!"
    "hello "        caret  [6,6]   +  {default:"bold text"}        ->  "hello 
**bold text**"
    "see docs"      select [4,8]   +  {prefix:"[", suffix:"](url)"} ->  "see 
[docs](url)"
    ```
    
    Four tests, driving the **real textarea from the template** with
    `selectionStart`/`selectionEnd` set the way a user's selection would be,
    rather than stubbing the ViewChild:
    
    - a selection is wrapped and the text either side survives
    - a collapsed caret inserts the action's placeholder instead
    - the action's own **suffix** is used, not a second copy of the prefix —
    a link action is asymmetric, so a symmetric wrap passes a bold test but
    breaks links
    - the preview re-renders from the **spliced** text, not the old text
    
    **Verified by mutation**, all reverted (component diff empty):
    
    | Mutation | Result |
    |---|---|
    | always use `action.default`, ignoring the selection | red |
    | use the prefix on both sides instead of the suffix | red |
    | drop the `renderMarkdown` call | red |
    
    Two lifecycle details are commented in the spec, because each cost a
    debugging round and the next person will hit them:
    
    - The fixture needs **two** change-detection cycles. `ngOnInit` forces
    preview mode, so an edit mode set before the first `detectChanges()` is
    silently overwritten and the `@ViewChild("textarea")` never resolves —
    the symptom is `Cannot read properties of undefined (reading
    'nativeElement')`.
    - The render assertion drains the microtask queue instead of awaiting
    `whenStable()`. `insert` schedules a `requestAnimationFrame` to refocus
    the textarea, which leaves the zone permanently unstable; `whenStable()`
    there hangs until the 5s timeout.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7353
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false 
--include="**/markdown-description.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
    ```
    
    4 new tests on top of the existing 15. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../markdown-description.component.spec.ts         | 75 ++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
index d2f807cd97..7d8b6858c1 100644
--- 
a/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
@@ -219,6 +219,81 @@ describe("MarkdownDescriptionComponent", () => {
     expect(component.currentMode).toBe("preview");
   });
 
+  /**
+   * The editor toolbar actions (bold, link, and so on) all route through 
`insert`, which was untested.
+   * It wraps whatever the user has selected, or a placeholder when nothing 
is, and must splice the
+   * result back without disturbing the text either side - get the offsets 
wrong and the toolbar
+   * silently corrupts the description it is meant to format.
+   *
+   * The tests drive the real textarea from the template, setting 
selectionStart/selectionEnd the
+   * way a user's selection would, rather than stubbing the ref.
+   */
+  describe("insert", () => {
+    const bold = { prefix: "**", suffix: "**", default: "bold text" };
+
+    async function editorWith(
+      content: string,
+      selection: [number, number]
+    ): Promise<ComponentFixture<MarkdownDescriptionComponent>> {
+      const fixture = await createFixture();
+      const component = fixture.componentInstance;
+      component.editable = true;
+      // First cycle runs ngOnInit, which forces preview mode; only then can 
edit mode be set and
+      // the textarea rendered by a second cycle. Setting it beforehand is 
silently overwritten.
+      fixture.detectChanges();
+      component.currentMode = "edit";
+      component.editingContent = content;
+      fixture.detectChanges();
+      const textarea = component.textareaRef.nativeElement;
+      textarea.value = content;
+      textarea.setSelectionRange(selection[0], selection[1]);
+      return fixture;
+    }
+
+    it("wraps the selected text and leaves the surrounding text intact", async 
() => {
+      // Select "world" out of "hello world!" - the leading "hello " and 
trailing "!" must survive.
+      const fixture = await editorWith("hello world!", [6, 11]);
+
+      fixture.componentInstance.insert(bold);
+
+      expect(fixture.componentInstance.editingContent).toBe("hello 
**world**!");
+    });
+
+    it("inserts the placeholder when nothing is selected", async () => {
+      // A collapsed caret means there is nothing to wrap, so the action's 
default stands in and
+      // the user can type over it.
+      const fixture = await editorWith("hello ", [6, 6]);
+
+      fixture.componentInstance.insert(bold);
+
+      expect(fixture.componentInstance.editingContent).toBe("hello **bold 
text**");
+    });
+
+    it("uses the action's own prefix and suffix rather than a fixed pair", 
async () => {
+      // A link action is asymmetric, which a hardcoded "wrap in prefix twice" 
would get wrong.
+      const fixture = await editorWith("see docs", [4, 8]);
+
+      fixture.componentInstance.insert({ prefix: "[", suffix: "](url)", 
default: "text" });
+
+      expect(fixture.componentInstance.editingContent).toBe("see [docs](url)");
+    });
+
+    it("re-renders the preview from the spliced content", async () => {
+      const fixture = await editorWith("hi", [0, 2]);
+      parse.mockClear();
+
+      fixture.componentInstance.insert(bold);
+      // Not whenStable(): insert() schedules a requestAnimationFrame to 
refocus the textarea, which
+      // keeps the zone permanently unstable and hangs the test. 
renderMarkdown settles on the
+      // microtask queue, so yielding a single tick is both sufficient and 
terminating.
+      await Promise.resolve();
+
+      // The preview has to follow the edit; parse must see the NEW text, not 
the old.
+      expect(parse).toHaveBeenCalledWith("**hi**");
+      
expect(fixture.componentInstance.renderedDescription).toBe("<p>**hi**</p>");
+    });
+  });
+
   it("renderMarkdown renders non-empty input and clears on blank input without 
parsing", async () => {
     const fixture = await createFixture();
     const component = fixture.componentInstance;

Reply via email to