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",
