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-7657-938342afb31a8b51faeec75e5d98116a694b11e2
in repository https://gitbox.apache.org/repos/asf/texera.git

commit d420e2046b6d67e4279a558f83f95b2bfb714aab
Author: Meng Wang <[email protected]>
AuthorDate: Fri Aug 14 06:04:12 2026 +0000

    test(frontend): bring four component templates to full coverage (#7657)
    
    ### What changes were proposed in this PR?
    
    Renders the state-gated blocks that four component specs never reached,
    taking
    each template to full line coverage (8 new tests):
    
    | Template | Before | After |
    | --- | --- | --- |
    | `markdown-description.component.html` | 44/49 | **49/49** |
    | `agent-registration.component.html` | 43/48 | **48/48** |
    | `registration-request-modal.component.html` | 15/19 | **19/19** |
    | `port-property-edit-frame.component.html` | 15/21 | **21/21** |
    
    - **MarkdownDescription** — enters edit mode through the Edit button so
    the
    toolbar and textarea render; asserts a toolbar click wraps the inserted
    markup
    into the draft, the textarea's `ngModelChange` re-renders the preview,
    and the
      Cancel / Save actions are wired.
    - **AgentRegistration** — renders the loading branch (spinner + caption)
    with a
    never-emitting model source, then the loaded picker: one card per model
    type,
      and clicking one selects it and marks it `.selected`.
    - **RegistrationRequestModal** — `modalTitle` is an `<ng-template>`
    handed to
    nz-modal, so nothing renders it during a plain component render; the
    test
    instantiates it explicitly and asserts the label and the logo's
    `src`/`alt`.
    - **PortPropertyEditFrame** — drives the port through the public
    `ngOnChanges`
    hook so the form is built, then asserts the `{{ formTitle }}` heading
    and the
    `[formGroup]` block render and that the form's `(modelChange)` reaches
    the
    component. Its spec now imports
    `FormlyModule.forRoot(TEXERA_FORMLY_CONFIG)` +
    `FormlyNgZorroAntdModule` (mirroring the operator-property-edit-frame
    spec) so
      `formly-form` can instantiate its field types.
    
    Two paths in the issue had drifted and were located by component name:
    `markdown-description` lives under `dashboard/component/user/`, and
    `agent-registration` under `workspace/component/agent/agent-panel/`.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7654.
    
    ### How was this PR tested?
    
    `ng test --watch=false` over the four specs — 74 passed (66 existing + 8
    new); the per-template line coverage above is from the local lcov
    report. `eslint` and `prettier --check` clean. Failure path verified by
    breaking one new assertion in each of the four files: 4 failed / 70
    passed, non-zero exit, then restored to green.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../registration-request-modal.component.spec.ts   | 20 +++++
 .../markdown-description.component.spec.ts         | 90 ++++++++++++++++++++++
 .../agent-registration.component.spec.ts           | 34 +++++++-
 .../port-property-edit-frame.component.spec.ts     | 44 ++++++++++-
 4 files changed, 186 insertions(+), 2 deletions(-)

diff --git 
a/frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.spec.ts
 
b/frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.spec.ts
index c6ae9cdd89..5166382436 100644
--- 
a/frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.spec.ts
+++ 
b/frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.spec.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import { ViewContainerRef } from "@angular/core";
 import { ComponentFixture, TestBed } from "@angular/core/testing";
 import { NZ_MODAL_DATA } from "ng-zorro-antd/modal";
 import { RegistrationRequestModalComponent } from 
"./registration-request-modal.component";
@@ -62,4 +63,23 @@ describe("RegistrationRequestModalComponent", () => {
     const component = (await createFixture({ uid: 1, email: "", name: "" 
})).componentInstance;
     expect(component.getValues()).toEqual({ affiliation: "", reason: "" });
   });
+
+  // `modalTitle` is an <ng-template> handed to nz-modal as its title, so 
nothing
+  // renders it during a plain component render — instantiate it explicitly.
+  it("renders the modal-title template with the label and the logo", async () 
=> {
+    const fixture = await createFixture({ uid: 1, email: "[email protected]", name: 
"Alice" });
+    fixture.detectChanges();
+
+    const view = fixture.debugElement.injector
+      .get(ViewContainerRef)
+      .createEmbeddedView(fixture.componentInstance.modalTitle);
+    view.detectChanges();
+
+    const root = view.rootNodes.find((n: HTMLElement) => 
n.classList?.contains("registration-modal-title"));
+    expect(root).toBeTruthy();
+    expect(root.querySelector("span")?.textContent?.trim()).toBe("Request 
access");
+    const logo = root.querySelector("img.registration-modal-logo") as 
HTMLImageElement;
+    expect(logo.getAttribute("src")).toBe("assets/logos/full_logo_small.png");
+    expect(logo.getAttribute("alt")).toBe("Texera logo");
+  });
 });
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 bec5010beb..ba2d152af5 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
@@ -19,6 +19,7 @@
 
 import { Provider, SimpleChange } from "@angular/core";
 import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
 import { NZ_MODAL_DATA } from "ng-zorro-antd/modal";
 import { MarkdownService } from "ngx-markdown";
 import { MarkdownDescriptionComponent } from 
"./markdown-description.component";
@@ -398,4 +399,93 @@ describe("MarkdownDescriptionComponent", () => {
       expect(fixture.nativeElement.querySelector(".view-more-btn")).toBeNull();
     });
   });
+
+  // The edit-mode markup (toolbar + textarea) only renders once currentMode 
is "edit".
+  describe("edit-mode template", () => {
+    async function enterEditMode(): 
Promise<ComponentFixture<MarkdownDescriptionComponent>> {
+      const fixture = await createFixture();
+      fixture.componentInstance.description = "hello";
+      fixture.componentInstance.editable = true;
+      fixture.detectChanges();
+
+      // Go through the Edit button so its (click) binding executes.
+      const editButton = fixture.debugElement.query(By.css(".md-actions 
button"));
+      expect(editButton).toBeTruthy();
+      editButton.triggerEventHandler("click", new MouseEvent("click"));
+      fixture.detectChanges();
+      return fixture;
+    }
+
+    it("renders one toolbar button per action and a textarea bound to the 
draft", async () => {
+      const fixture = await enterEditMode();
+
+      const buttons = fixture.debugElement.queryAll(By.css(".md-toolbar 
button"));
+      expect(buttons.length).toBe(fixture.componentInstance.toolbar.length);
+      const textarea = fixture.debugElement.query(By.css(".md-textarea"));
+      expect(textarea).toBeTruthy();
+      expect((textarea.nativeElement as 
HTMLTextAreaElement).value).toBe("hello");
+    });
+
+    it("wraps the selected text when a toolbar button is clicked", async () => 
{
+      const fixture = await enterEditMode();
+
+      // insert() reads the textarea's selection offsets, so set them 
explicitly
+      // rather than relying on the environment's default caret position.
+      const textarea = 
fixture.debugElement.query(By.css(".md-textarea")).nativeElement as 
HTMLTextAreaElement;
+      textarea.setSelectionRange(0, "hello".length);
+
+      // the first action is Bold
+      fixture.debugElement
+        .queryAll(By.css(".md-toolbar button"))[0]
+        .triggerEventHandler("click", new MouseEvent("click"));
+      fixture.detectChanges();
+
+      expect(fixture.componentInstance.editingContent).toBe("**hello**");
+    });
+
+    it("inserts the action's default text when nothing is selected", async () 
=> {
+      const fixture = await enterEditMode();
+
+      // empty selection at the end of the draft -> insert() falls back to 
action.default
+      const textarea = 
fixture.debugElement.query(By.css(".md-textarea")).nativeElement as 
HTMLTextAreaElement;
+      textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+
+      fixture.debugElement
+        .queryAll(By.css(".md-toolbar button"))[0]
+        .triggerEventHandler("click", new MouseEvent("click"));
+      fixture.detectChanges();
+
+      expect(fixture.componentInstance.editingContent).toBe("hello**bold**");
+    });
+
+    it("wires the edit-mode Cancel and Save actions", async () => {
+      const fixture = await enterEditMode();
+      const component = fixture.componentInstance;
+      // In edit mode .md-actions holds [Cancel, Save].
+      const actions = fixture.debugElement.queryAll(By.css(".md-actions 
button"));
+      expect(actions.length).toBe(2);
+
+      component.editingContent = "draft";
+      actions[0].triggerEventHandler("click", new MouseEvent("click")); // 
Cancel
+      expect(component.editingContent).toBe("hello"); // reverted to the 
description
+
+      const saved: string[] = [];
+      component.descriptionChange.subscribe(v => saved.push(v));
+      component.editingContent = "saved text";
+      actions[1].triggerEventHandler("click", new MouseEvent("click")); // Save
+      expect(saved).toEqual(["saved text"]);
+    });
+
+    it("re-renders the preview from the textarea's ngModelChange", async () => 
{
+      const fixture = await enterEditMode();
+
+      const textarea = fixture.debugElement.query(By.css(".md-textarea"));
+      (textarea.nativeElement as HTMLTextAreaElement).value = "typed";
+      textarea.nativeElement.dispatchEvent(new Event("input"));
+      fixture.detectChanges();
+
+      expect(fixture.componentInstance.editingContent).toBe("typed");
+      expect(fixture.nativeElement.querySelector(".md-right 
.md-rendered")).toBeTruthy();
+    });
+  });
 });
diff --git 
a/frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.spec.ts
 
b/frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.spec.ts
index 052d3e2133..16c926d31e 100644
--- 
a/frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.spec.ts
@@ -18,9 +18,10 @@
  */
 
 import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
 import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
 import { HttpClientTestingModule } from "@angular/common/http/testing";
-import { of, throwError } from "rxjs";
+import { NEVER, of, throwError } from "rxjs";
 import { AgentRegistrationComponent } from "./agent-registration.component";
 import { AgentService, ModelType } from 
"../../../../service/agent/agent.service";
 import { NotificationService } from 
"../../../../../common/service/notification/notification.service";
@@ -183,4 +184,35 @@ describe("AgentRegistrationComponent", () => {
       expect(component.canCreate()).toBe(false);
     });
   });
+
+  describe("template rendering", () => {
+    it("renders the spinner and its caption while the models are loading", () 
=> {
+      // never-emitting source: ngOnInit leaves isLoadingModels true
+      fetchModelTypes.mockReturnValue(NEVER);
+      fixture.detectChanges();
+
+      expect(component.isLoadingModels).toBe(true);
+      expect(fixture.debugElement.query(By.css("nz-spin"))).toBeTruthy();
+      expect(fixture.nativeElement.textContent).toContain("Loading available 
models...");
+      // the picker only appears once loading finishes
+      expect(fixture.debugElement.query(By.css(".model-card"))).toBeNull();
+    });
+
+    it("renders a card per model type and selects the clicked one", () => {
+      const second: ModelType = { ...MODEL, id: "other-model", name: "Other 
Model" };
+      fetchModelTypes.mockReturnValue(of([MODEL, second]));
+      fixture.detectChanges();
+
+      const cards = fixture.debugElement.queryAll(By.css(".model-card"));
+      expect(cards.length).toBe(2);
+      expect(fixture.debugElement.query(By.css("nz-spin"))).toBeNull();
+
+      cards[1].triggerEventHandler("click", new MouseEvent("click"));
+      fixture.detectChanges();
+
+      expect(component.selectedModelType).toBe("other-model");
+      expect((cards[1].nativeElement as 
HTMLElement).classList).toContain("selected");
+      expect((cards[0].nativeElement as 
HTMLElement).classList).not.toContain("selected");
+    });
+  });
 });
diff --git 
a/frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts
 
b/frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts
index fc6b03f9b6..8c6528397f 100644
--- 
a/frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts
@@ -17,7 +17,9 @@
  * under the License.
  */
 
+import { SimpleChange } from "@angular/core";
 import { ComponentFixture, fakeAsync, TestBed, tick } from 
"@angular/core/testing";
+import { By } from "@angular/platform-browser";
 
 import { PortPropertyEditFrameComponent } from 
"./port-property-edit-frame.component";
 import { WorkflowActionService } from 
"../../../service/workflow-graph/model/workflow-action.service";
@@ -25,6 +27,9 @@ import { HttpClientTestingModule } from 
"@angular/common/http/testing";
 import { commonTestProviders } from "../../../../common/testing/test-utils";
 import { DynamicSchemaService } from 
"../../../service/dynamic-schema/dynamic-schema.service";
 import { FormGroup } from "@angular/forms";
+import { FormlyModule } from "@ngx-formly/core";
+import { TEXERA_FORMLY_CONFIG } from "../../../../common/formly/formly-config";
+import { FormlyNgZorroAntdModule } from "@ngx-formly/ng-zorro-antd";
 import { LogicalPort, PortDescription } from 
"../../../types/workflow-common.interface";
 import { mockPortSchema } from 
"../../../service/operator-metadata/mock-operator-metadata.data";
 import { FORM_DEBOUNCE_TIME_MS } from 
"../../../service/execute-workflow/execute-workflow.service";
@@ -43,7 +48,12 @@ describe("PortPropertyEditFrameComponent", () => {
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       providers: [WorkflowActionService, ...commonTestProviders],
-      imports: [PortPropertyEditFrameComponent, HttpClientTestingModule],
+      imports: [
+        PortPropertyEditFrameComponent,
+        HttpClientTestingModule,
+        FormlyModule.forRoot(TEXERA_FORMLY_CONFIG),
+        FormlyNgZorroAntdModule,
+      ],
     }).compileComponents();
   });
 
@@ -153,6 +163,38 @@ describe("PortPropertyEditFrameComponent", () => {
       expect(component.formlyFields).toBeUndefined();
     });
 
+    // The heading and the [formGroup] block only render once the form has 
been built.
+    it("should render the title heading and the formly form once the form is 
built", () => {
+      const descriptor: PortDescription = {
+        portID: "input-0",
+        displayName: "Input A",
+        partitionRequirement: { type: "hash", hashAttributeNames: ["a"] },
+        dependencies: [{ id: 1, internal: false }],
+      };
+      vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+      vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+      vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+        additionalMetadata: { allowPortCustomization: true },
+      } as any);
+
+      // Drive it through the public input hook rather than the private opener.
+      component.ngOnChanges({ currentPortID: new SimpleChange(undefined, 
inputPort, true) });
+      fixture.detectChanges();
+
+      const heading = 
fixture.debugElement.query(By.css("h3.texera-workspace-property-editor-title"));
+      expect(heading.nativeElement.textContent.trim()).toBe("Input A");
+      const form = 
fixture.debugElement.query(By.css("form.texera-workspace-property-editor-form"));
+      expect(form).toBeTruthy();
+      const formly = form.query(By.css("formly-form"));
+      expect(formly).toBeTruthy();
+
+      // the rendered form's (modelChange) forwards onto the source stream
+      const received: Record<string, unknown>[] = [];
+      (component as any).sourceFormChangeEventStream.subscribe((e: 
Record<string, unknown>) => received.push(e));
+      formly.triggerEventHandler("modelChange", { type: "none" });
+      expect(received).toEqual([{ type: "none" }]);
+    });
+
     it("should build the formly form from the port descriptor when 
customization is allowed on an input port", () => {
       const descriptor: PortDescription = {
         portID: "input-0",

Reply via email to