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-6832-362e6baca217b29d6df411d669e029ab5bc1af7d in repository https://gitbox.apache.org/repos/asf/texera.git
commit d2ab9668a759d732e41144966a6f85c12ec91630 Author: Meng Wang <[email protected]> AuthorDate: Thu Jul 23 16:21:15 2026 -0700 test(frontend): cover OperatorPropertyEditFrameComponent modify-logic and subscription handlers (#6832) ### What changes were proposed in this PR? Extends the existing `OperatorPropertyEditFrameComponent` spec (codecov ~72%) with 8 deterministic tests for the non-Quill methods it did not reach: - **Modify-logic gating** — `allowModifyOperatorLogic`, and `confirmModifyOperatorLogic` across its three paths: applies the change and re-locks the form; surfaces a thrown error through `NotificationService` while leaving the form editable; no-ops when no operator is selected. - **Subscription handlers** — the display-name handler retitles only the current operator; the workflow-modification handler mirrors the flag onto the form's interactivity and stays out of the way when no operator is selected; the dynamic-schema handler re-renders only for the current operator. - **`rerenderEditorForm`** — early return when no operator is selected. Already-covered behaviour was deliberately not duplicated: the form-change → `setOperatorProperty` path is covered by the existing "should change Texera graph property when the form is edited by the user" test, and `formatTaskTitle` / `isHuggingFaceOperator` are exercised by the HuggingFace preview tests. The Quill binding stays out of scope per the issue. No production code was changed. ### Any related issues, documentation, discussions? Closes #6826. ### How was this PR tested? `ng test --watch=false --include src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts` — 190 passed, 1 skipped, exit 0. `eslint` exit 0 and `prettier --check` clean on the spec. Failure path verified by breaking two of the new assertions and confirming a non-zero exit, then restoring. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../operator-property-edit-frame.component.spec.ts | 126 +++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts index e23aa7ae70..9bb72cea23 100644 --- a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts +++ b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts @@ -57,6 +57,9 @@ import { FormlyNgZorroAntdModule } from "@ngx-formly/ng-zorro-antd"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; +import { DynamicSchemaService } from "../../../service/dynamic-schema/dynamic-schema.service"; +import { NotificationService } from "../../../../common/service/notification/notification.service"; +import { WorkflowGraph } from "../../../service/workflow-graph/model/workflow-graph"; const { marbles } = configure({ run: false }); @@ -2084,4 +2087,127 @@ describe("OperatorPropertyEditFrameComponent", () => { expect(realFixture.debugElement.query(By.css(".hf-task-preview-pills"))).toBeNull(); }); }); + + describe("modify-operator-logic gating", () => { + it("allowModifyOperatorLogic re-enables editing", () => { + fixture.detectChanges(); + component.setInteractivity(false); + + component.allowModifyOperatorLogic(); + + expect(component.interactive).toBe(true); + }); + + it("confirmModifyOperatorLogic pushes the change and locks the form again", () => { + fixture.detectChanges(); + component.currentOperatorId = mockScanPredicate.operatorID; + const modifySpy = vi.spyOn(component.executeWorkflowService, "modifyOperatorLogic").mockImplementation(() => {}); + component.setInteractivity(true); + + component.confirmModifyOperatorLogic(); + + expect(modifySpy).toHaveBeenCalledWith(mockScanPredicate.operatorID); + expect(component.interactive).toBe(false); + }); + + it("confirmModifyOperatorLogic reports a failure and leaves the form editable", () => { + fixture.detectChanges(); + component.currentOperatorId = mockScanPredicate.operatorID; + vi.spyOn(component.executeWorkflowService, "modifyOperatorLogic").mockImplementation(() => { + throw new Error("cannot modify while running"); + }); + const errorSpy = vi.spyOn(TestBed.inject(NotificationService), "error").mockImplementation(() => {}); + component.setInteractivity(true); + + component.confirmModifyOperatorLogic(); + + expect(errorSpy).toHaveBeenCalledWith("cannot modify while running"); + expect(component.interactive).toBe(true); + }); + + it("confirmModifyOperatorLogic does nothing when no operator is selected", () => { + fixture.detectChanges(); + component.currentOperatorId = undefined; + const modifySpy = vi.spyOn(component.executeWorkflowService, "modifyOperatorLogic"); + + component.confirmModifyOperatorLogic(); + + expect(modifySpy).not.toHaveBeenCalled(); + }); + }); + + describe("subscription handlers", () => { + it("retitles the form only when the renamed operator is the current one", () => { + fixture.detectChanges(); + component.currentOperatorId = mockScanPredicate.operatorID; + component.formTitle = "original"; + // getTexeraGraph() narrows to WorkflowGraphReadonly; the instance really is a + // WorkflowGraph, whose display-name subject is only pushed by the shared-model + // handler, so drive it directly to exercise the subscriber. + const graph = workflowActionService.getTexeraGraph() as unknown as WorkflowGraph; + + graph.operatorDisplayNameChangedSubject.next({ + operatorID: "some-other-operator", + newDisplayName: "ignored", + }); + expect(component.formTitle).toBe("original"); + + graph.operatorDisplayNameChangedSubject.next({ + operatorID: mockScanPredicate.operatorID, + newDisplayName: "renamed", + }); + expect(component.formTitle).toBe("renamed"); + }); + + it("mirrors the workflow-modification flag onto the form's interactivity", () => { + fixture.detectChanges(); + component.currentOperatorId = mockScanPredicate.operatorID; + + workflowActionService.disableWorkflowModification(); + expect(component.interactive).toBe(false); + + workflowActionService.enableWorkflowModification(); + expect(component.interactive).toBe(true); + }); + + it("ignores the workflow-modification flag while no operator is selected", () => { + fixture.detectChanges(); + component.currentOperatorId = undefined; + component.setInteractivity(true); + + workflowActionService.disableWorkflowModification(); + + expect(component.interactive).toBe(true); + workflowActionService.enableWorkflowModification(); + }); + + it("re-renders only when the current operator's dynamic schema changes", () => { + workflowActionService.addOperator(mockScanPredicate, mockPoint); + workflowActionService.addOperator(mockResultPredicate, mockPoint); + fixture.detectChanges(); + component.currentOperatorId = mockScanPredicate.operatorID; + const dynamicSchemaService = TestBed.inject(DynamicSchemaService); + const rerenderSpy = vi.spyOn(component, "rerenderEditorForm").mockImplementation(() => {}); + // setDynamicSchema is a no-op when the schema is unchanged, so vary it to force an emit. + const bumped = (schema: typeof mockScanSourceSchema) => ({ ...schema, operatorVersion: "bumped" }); + + dynamicSchemaService.setDynamicSchema(mockResultPredicate.operatorID, bumped(mockViewResultsSchema)); + expect(rerenderSpy).not.toHaveBeenCalled(); + + dynamicSchemaService.setDynamicSchema(mockScanPredicate.operatorID, bumped(mockScanSourceSchema)); + expect(rerenderSpy).toHaveBeenCalled(); + }); + }); + + describe("rerenderEditorForm", () => { + it("is a no-op when no operator is selected", () => { + fixture.detectChanges(); + component.currentOperatorId = undefined; + component.formTitle = "untouched"; + + component.rerenderEditorForm(); + + expect(component.formTitle).toBe("untouched"); + }); + }); });
