You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/14 06:40:56 UTC
(texera) 01/01: test(frontend): bring four component templates to full coverage (#7657)
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 <me...@uci.edu>
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: "a@b.com", 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",