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:55 UTC
(texera) branch gh-readonly-queue/main/pr-7657-938342afb31a8b51faeec75e5d98116a694b11e2 created (now d420e2046b)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-7657-938342afb31a8b51faeec75e5d98116a694b11e2
in repository https://gitbox.apache.org/repos/asf/texera.git
at d420e2046b test(frontend): bring four component templates to full coverage (#7657)
This branch includes the following new commits:
new d420e2046b test(frontend): bring four component templates to full coverage (#7657)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(frontend): bring four component templates to full coverage (#7657)
Posted by gi...@apache.org.
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",