You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@texera.apache.org by "aglinxinyuan (via GitHub)" <gi...@apache.org> on 2026/07/20 06:06:30 UTC
[PR] test(frontend): extend PortPropertyEditFrameComponent unit test coverage [texera]
aglinxinyuan opened a new pull request, #6588:
URL: https://github.com/apache/texera/pull/6588
### What changes were proposed in this PR?
Extends the stub `port-property-edit-frame.component.spec.ts` with 23 new tests (1 -> 24) for `PortPropertyEditFrameComponent`, targeting previously-uncovered behavior:
- `ngOnInit` registering the port-property / display-name / form-change handlers;
- `ngOnChanges` adopting the new port id and opening the editor (and skipping when undefined);
- `showPortPropertyEditor` guards (missing port throws; customization-off and output-port skip the form build) and the formly form build for a customizable input port;
- the debounced form-change handler propagating a cloned copy to `WorkflowActionService.setPortProperty`; and
- the `checkPort` branches.
Reuses the existing TestBed setup with subject-backed WorkflowActionService doubles. Statement coverage went from ~28% to ~85% (58 -> 8 uncovered lines). No existing tests modified.
### Any related issues, documentation, discussions?
Closes #6585.
### How was this PR tested?
`npx ng test --watch=false --include='**/port-property-edit-frame.component.spec.ts'` -> 24/24 passing. `yarn format:ci` passes.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] test(frontend): extend PortPropertyEditFrameComponent unit test coverage [texera]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #6588:
URL: https://github.com/apache/texera/pull/6588#issuecomment-5019257426
<!-- texera-reviewer-suggestion -->
### Automated Reviewer Suggestions
Based on the `git blame` history of the changed files, we recommend the following reviewers:
- **Contributors with relevant context:** `@Yicong-Huang`
You can notify them by mentioning `@Yicong-Huang` in a comment.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] test(frontend): extend PortPropertyEditFrameComponent unit test coverage [texera]
Posted by "aglinxinyuan (via GitHub)" <gi...@apache.org>.
aglinxinyuan commented on code in PR #6588:
URL: https://github.com/apache/texera/pull/6588#discussion_r3612394713
##########
frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts:
##########
@@ -40,10 +51,333 @@ describe("PortPropertyEditFrameComponent", () => {
fixture = TestBed.createComponent(PortPropertyEditFrameComponent);
component = fixture.componentInstance;
workflowActionService = TestBed.inject(WorkflowActionService);
+ dynamicSchemaService = TestBed.inject(DynamicSchemaService);
+ texeraGraph = (workflowActionService as any).texeraGraph;
fixture.detectChanges();
Review Comment:
Good call — switched to the public getTexeraGraph() instead of reaching into the private texeraGraph field.
##########
frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts:
##########
@@ -40,10 +51,333 @@ describe("PortPropertyEditFrameComponent", () => {
fixture = TestBed.createComponent(PortPropertyEditFrameComponent);
component = fixture.componentInstance;
workflowActionService = TestBed.inject(WorkflowActionService);
+ dynamicSchemaService = TestBed.inject(DynamicSchemaService);
+ texeraGraph = (workflowActionService as any).texeraGraph;
fixture.detectChanges();
});
+ afterEach(() => {
+ vi.restoreAllMocks();
+ fixture.destroy();
+ });
+
it("should create", () => {
expect(component).toBeTruthy();
});
+
+ describe("ngOnInit / handler registration", () => {
+ it("should register all three change handlers", () => {
+ const propertySpy = vi.spyOn(component as any, "registerPortPropertyChangeHandler");
+ const displayNameSpy = vi.spyOn(component as any, "registerPortDisplayNameChangeHandler");
+ const formChangeSpy = vi.spyOn(component as any, "registerOnFormChangeHandler");
+
+ component.ngOnInit();
+
+ expect(propertySpy).toHaveBeenCalledTimes(1);
+ expect(displayNameSpy).toHaveBeenCalledTimes(1);
+ expect(formChangeSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("onFormChanges", () => {
+ it("should forward the raw form event onto the source form-change stream", () => {
+ const received: Record<string, unknown>[] = [];
+ (component as any).sourceFormChangeEventStream.subscribe((e: Record<string, unknown>) => received.push(e));
+
+ const event = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(event);
+
+ expect(received).toEqual([event]);
+ });
+ });
+
+ describe("ngOnChanges", () => {
+ it("should adopt the new port id and open the editor when a port is provided", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: inputPort } } as any);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(showSpy).toHaveBeenCalledWith(inputPort);
+ });
+
+ it("should not open the editor when the new port id is undefined", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: undefined } } as any);
+
+ expect(component.currentPortID).toBeUndefined();
+ expect(showSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("showPortPropertyEditor", () => {
+ it("should throw when the target port does not exist in the graph", () => {
+ expect(() => (component as any).showPortPropertyEditor({ operatorID: "ghost", portID: "input-0" })).toThrowError(
+ /does not exist/
+ );
+ });
+
+ it("should set the title but skip building the form when customization is not allowed", () => {
+ const descriptor: PortDescription = {
+ portID: "input-0",
+ displayName: "Input A",
+ partitionRequirement: { type: "none" },
+ dependencies: [],
+ };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: false },
+ } as any);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formlyFields).toBeUndefined();
+ expect(component.formlyFormGroup).toBeUndefined();
+ });
+
+ it("should skip building the form for an output port even when customization is allowed", () => {
+ const descriptor: PortDescription = { portID: "output-0", displayName: "Output A" };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: true },
+ } as any);
+
+ (component as any).showPortPropertyEditor(outputPort);
+
+ expect(component.formTitle).toBe("Output A");
+ expect(component.formlyFields).toBeUndefined();
+ });
+
+ it("should build the formly form from the port descriptor when customization is allowed on an input port", () => {
+ 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);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formData).toEqual({
+ partitionInfo: { type: "hash", hashAttributeNames: ["a"] },
+ dependencies: [{ id: 1, internal: false }],
+ });
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(Array.isArray(component.formlyFields)).toBe(true);
+ });
+ });
+
+ describe("setFormlyFormBinding (interactive vs. display mode)", () => {
+ it("should keep the form enabled when interactive, and disable it via the onInit hook when not", () => {
+ const fakeField: any = { fieldGroup: [{ key: "partitionInfo" }] };
+ vi.spyOn((component as any).formlyJsonschema, "toFieldConfig").mockReturnValue(fakeField);
+
+ (component as any).setFormlyFormBinding(mockPortSchema.jsonSchema);
+
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(component.formlyFields).toBe(fakeField.fieldGroup);
+
+ const onInit = fakeField.hooks.onInit;
+ const fakeForm = { disable: vi.fn() };
+
+ component.interactive = true;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).not.toHaveBeenCalled();
+
+ component.interactive = false;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).toHaveBeenCalledTimes(1);
+
+ // guard: a missing field config / form must not throw
+ expect(() => onInit(undefined)).not.toThrow();
+ });
+ });
+
+ describe("checkPort", () => {
+ it("should return false when there is no current port", () => {
+ component.currentPortID = undefined;
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should return false when the current port is no longer in the graph", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(false);
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should compare form data against the port's partition requirement", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({
+ portID: "input-0",
+ partitionRequirement: { type: "none" },
+ } as PortDescription);
+
+ // identical to the current partition requirement -> no change
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ // different -> change detected
+ expect((component as any).checkPort({ type: "hash", hashAttributeNames: ["a"] })).toBe(true);
+ });
+ });
+
+ describe("registerOnFormChangeHandler (edit propagation)", () => {
+ it("should push a debounced, changed form value to WorkflowActionService.setPortProperty", fakeAsync(() => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({ portID: "input-0" } as PortDescription);
+ const setSpy = vi.spyOn(workflowActionService, "setPortProperty").mockImplementation(() => {});
+
+ const newValue = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(newValue);
+ tick(FORM_DEBOUNCE_TIME_MS);
+
+ expect(setSpy).toHaveBeenCalledTimes(1);
Review Comment:
Fixed — now ticks FORM_DEBOUNCE_TIME_MS + 10 to clear the debounce boundary without edge-timing flakiness.
##########
frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts:
##########
@@ -40,10 +51,333 @@ describe("PortPropertyEditFrameComponent", () => {
fixture = TestBed.createComponent(PortPropertyEditFrameComponent);
component = fixture.componentInstance;
workflowActionService = TestBed.inject(WorkflowActionService);
+ dynamicSchemaService = TestBed.inject(DynamicSchemaService);
+ texeraGraph = (workflowActionService as any).texeraGraph;
fixture.detectChanges();
});
+ afterEach(() => {
+ vi.restoreAllMocks();
+ fixture.destroy();
+ });
+
it("should create", () => {
expect(component).toBeTruthy();
});
+
+ describe("ngOnInit / handler registration", () => {
+ it("should register all three change handlers", () => {
+ const propertySpy = vi.spyOn(component as any, "registerPortPropertyChangeHandler");
+ const displayNameSpy = vi.spyOn(component as any, "registerPortDisplayNameChangeHandler");
+ const formChangeSpy = vi.spyOn(component as any, "registerOnFormChangeHandler");
+
+ component.ngOnInit();
+
+ expect(propertySpy).toHaveBeenCalledTimes(1);
+ expect(displayNameSpy).toHaveBeenCalledTimes(1);
+ expect(formChangeSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("onFormChanges", () => {
+ it("should forward the raw form event onto the source form-change stream", () => {
+ const received: Record<string, unknown>[] = [];
+ (component as any).sourceFormChangeEventStream.subscribe((e: Record<string, unknown>) => received.push(e));
+
+ const event = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(event);
+
+ expect(received).toEqual([event]);
+ });
+ });
+
+ describe("ngOnChanges", () => {
+ it("should adopt the new port id and open the editor when a port is provided", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: inputPort } } as any);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(showSpy).toHaveBeenCalledWith(inputPort);
+ });
+
+ it("should not open the editor when the new port id is undefined", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: undefined } } as any);
+
+ expect(component.currentPortID).toBeUndefined();
+ expect(showSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("showPortPropertyEditor", () => {
+ it("should throw when the target port does not exist in the graph", () => {
+ expect(() => (component as any).showPortPropertyEditor({ operatorID: "ghost", portID: "input-0" })).toThrowError(
+ /does not exist/
+ );
+ });
+
+ it("should set the title but skip building the form when customization is not allowed", () => {
+ const descriptor: PortDescription = {
+ portID: "input-0",
+ displayName: "Input A",
+ partitionRequirement: { type: "none" },
+ dependencies: [],
+ };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: false },
+ } as any);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formlyFields).toBeUndefined();
+ expect(component.formlyFormGroup).toBeUndefined();
+ });
+
+ it("should skip building the form for an output port even when customization is allowed", () => {
+ const descriptor: PortDescription = { portID: "output-0", displayName: "Output A" };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: true },
+ } as any);
+
+ (component as any).showPortPropertyEditor(outputPort);
+
+ expect(component.formTitle).toBe("Output A");
+ expect(component.formlyFields).toBeUndefined();
+ });
+
+ it("should build the formly form from the port descriptor when customization is allowed on an input port", () => {
+ 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);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formData).toEqual({
+ partitionInfo: { type: "hash", hashAttributeNames: ["a"] },
+ dependencies: [{ id: 1, internal: false }],
+ });
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(Array.isArray(component.formlyFields)).toBe(true);
+ });
+ });
+
+ describe("setFormlyFormBinding (interactive vs. display mode)", () => {
+ it("should keep the form enabled when interactive, and disable it via the onInit hook when not", () => {
+ const fakeField: any = { fieldGroup: [{ key: "partitionInfo" }] };
+ vi.spyOn((component as any).formlyJsonschema, "toFieldConfig").mockReturnValue(fakeField);
+
+ (component as any).setFormlyFormBinding(mockPortSchema.jsonSchema);
+
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(component.formlyFields).toBe(fakeField.fieldGroup);
+
+ const onInit = fakeField.hooks.onInit;
+ const fakeForm = { disable: vi.fn() };
+
+ component.interactive = true;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).not.toHaveBeenCalled();
+
+ component.interactive = false;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).toHaveBeenCalledTimes(1);
+
+ // guard: a missing field config / form must not throw
+ expect(() => onInit(undefined)).not.toThrow();
+ });
+ });
+
+ describe("checkPort", () => {
+ it("should return false when there is no current port", () => {
+ component.currentPortID = undefined;
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should return false when the current port is no longer in the graph", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(false);
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should compare form data against the port's partition requirement", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({
+ portID: "input-0",
+ partitionRequirement: { type: "none" },
+ } as PortDescription);
+
+ // identical to the current partition requirement -> no change
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ // different -> change detected
+ expect((component as any).checkPort({ type: "hash", hashAttributeNames: ["a"] })).toBe(true);
+ });
+ });
+
+ describe("registerOnFormChangeHandler (edit propagation)", () => {
+ it("should push a debounced, changed form value to WorkflowActionService.setPortProperty", fakeAsync(() => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({ portID: "input-0" } as PortDescription);
+ const setSpy = vi.spyOn(workflowActionService, "setPortProperty").mockImplementation(() => {});
+
+ const newValue = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(newValue);
+ tick(FORM_DEBOUNCE_TIME_MS);
+
+ expect(setSpy).toHaveBeenCalledTimes(1);
+ expect(setSpy).toHaveBeenCalledWith(inputPort, newValue);
+ // the setter passes a clone, not the same reference
+ expect(setSpy.mock.calls[0][1]).not.toBe(newValue);
+ expect(component.listeningToChange).toBe(true);
+ }));
+
+ it("should not propagate when the form value matches the current partition requirement", fakeAsync(() => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({
+ portID: "input-0",
+ partitionRequirement: { type: "none" },
+ } as PortDescription);
+ const setSpy = vi.spyOn(workflowActionService, "setPortProperty").mockImplementation(() => {});
+
+ component.onFormChanges({ type: "none" });
+ tick(FORM_DEBOUNCE_TIME_MS);
+
+ expect(setSpy).not.toHaveBeenCalled();
Review Comment:
Fixed — same +10 buffer over FORM_DEBOUNCE_TIME_MS here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] test(frontend): extend PortPropertyEditFrameComponent unit test coverage [texera]
Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #6588:
URL: https://github.com/apache/texera/pull/6588#issuecomment-5019293157
## [Codecov](https://app.codecov.io/gh/apache/texera/pull/6588?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 73.58%. Comparing base ([`f3f3fbe`](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`fafa42b`](https://app.codecov.io/gh/apache/texera/commit/fafa42b253aa5b7dd5d930b4a8a178afa393e9b3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
:warning: Report is 1 commits behind head on main.
<details><summary>Additional details and impacted files</summary>
```diff
@@ Coverage Diff @@
## main #6588 +/- ##
============================================
+ Coverage 73.41% 73.58% +0.16%
Complexity 3439 3439
============================================
Files 1150 1150
Lines 45405 45405
Branches 5013 5013
============================================
+ Hits 33336 33410 +74
+ Misses 10372 10298 -74
Partials 1697 1697
```
| [Flag](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| [access-control-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `70.00% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [agent-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `76.76% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [amber](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `66.82% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [computing-unit-managing-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `17.72% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [config-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `66.66% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [file-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `66.80% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [frontend](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `76.18% <ø> (+0.37%)` | :arrow_up: | |
| [notebook-migration-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `78.94% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [pyamber](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `91.79% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [workflow-compiling-service](https://app.codecov.io/gh/apache/texera/pull/6588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `55.14% <ø> (ø)` | | Carriedforward from [f3f3fbe](https://app.codecov.io/gh/apache/texera/commit/f3f3fbe131891226c9b55261d991df41ed35054d?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to find out more.
</details>
[:umbrella: View full report in Codecov by Harness](https://app.codecov.io/gh/apache/texera/pull/6588?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
<details><summary> :rocket: New features to boost your workflow: </summary>
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] test(frontend): extend PortPropertyEditFrameComponent unit test coverage [texera]
Posted by "aglinxinyuan (via GitHub)" <gi...@apache.org>.
aglinxinyuan merged PR #6588:
URL: https://github.com/apache/texera/pull/6588
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] test(frontend): extend PortPropertyEditFrameComponent unit test coverage [texera]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #6588:
URL: https://github.com/apache/texera/pull/6588#discussion_r3612308056
##########
frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts:
##########
@@ -40,10 +51,333 @@ describe("PortPropertyEditFrameComponent", () => {
fixture = TestBed.createComponent(PortPropertyEditFrameComponent);
component = fixture.componentInstance;
workflowActionService = TestBed.inject(WorkflowActionService);
+ dynamicSchemaService = TestBed.inject(DynamicSchemaService);
+ texeraGraph = (workflowActionService as any).texeraGraph;
fixture.detectChanges();
});
+ afterEach(() => {
+ vi.restoreAllMocks();
+ fixture.destroy();
+ });
+
it("should create", () => {
expect(component).toBeTruthy();
});
+
+ describe("ngOnInit / handler registration", () => {
+ it("should register all three change handlers", () => {
+ const propertySpy = vi.spyOn(component as any, "registerPortPropertyChangeHandler");
+ const displayNameSpy = vi.spyOn(component as any, "registerPortDisplayNameChangeHandler");
+ const formChangeSpy = vi.spyOn(component as any, "registerOnFormChangeHandler");
+
+ component.ngOnInit();
+
+ expect(propertySpy).toHaveBeenCalledTimes(1);
+ expect(displayNameSpy).toHaveBeenCalledTimes(1);
+ expect(formChangeSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("onFormChanges", () => {
+ it("should forward the raw form event onto the source form-change stream", () => {
+ const received: Record<string, unknown>[] = [];
+ (component as any).sourceFormChangeEventStream.subscribe((e: Record<string, unknown>) => received.push(e));
+
+ const event = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(event);
+
+ expect(received).toEqual([event]);
+ });
+ });
+
+ describe("ngOnChanges", () => {
+ it("should adopt the new port id and open the editor when a port is provided", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: inputPort } } as any);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(showSpy).toHaveBeenCalledWith(inputPort);
+ });
+
+ it("should not open the editor when the new port id is undefined", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: undefined } } as any);
+
+ expect(component.currentPortID).toBeUndefined();
+ expect(showSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("showPortPropertyEditor", () => {
+ it("should throw when the target port does not exist in the graph", () => {
+ expect(() => (component as any).showPortPropertyEditor({ operatorID: "ghost", portID: "input-0" })).toThrowError(
+ /does not exist/
+ );
+ });
+
+ it("should set the title but skip building the form when customization is not allowed", () => {
+ const descriptor: PortDescription = {
+ portID: "input-0",
+ displayName: "Input A",
+ partitionRequirement: { type: "none" },
+ dependencies: [],
+ };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: false },
+ } as any);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formlyFields).toBeUndefined();
+ expect(component.formlyFormGroup).toBeUndefined();
+ });
+
+ it("should skip building the form for an output port even when customization is allowed", () => {
+ const descriptor: PortDescription = { portID: "output-0", displayName: "Output A" };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: true },
+ } as any);
+
+ (component as any).showPortPropertyEditor(outputPort);
+
+ expect(component.formTitle).toBe("Output A");
+ expect(component.formlyFields).toBeUndefined();
+ });
+
+ it("should build the formly form from the port descriptor when customization is allowed on an input port", () => {
+ 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);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formData).toEqual({
+ partitionInfo: { type: "hash", hashAttributeNames: ["a"] },
+ dependencies: [{ id: 1, internal: false }],
+ });
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(Array.isArray(component.formlyFields)).toBe(true);
+ });
+ });
+
+ describe("setFormlyFormBinding (interactive vs. display mode)", () => {
+ it("should keep the form enabled when interactive, and disable it via the onInit hook when not", () => {
+ const fakeField: any = { fieldGroup: [{ key: "partitionInfo" }] };
+ vi.spyOn((component as any).formlyJsonschema, "toFieldConfig").mockReturnValue(fakeField);
+
+ (component as any).setFormlyFormBinding(mockPortSchema.jsonSchema);
+
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(component.formlyFields).toBe(fakeField.fieldGroup);
+
+ const onInit = fakeField.hooks.onInit;
+ const fakeForm = { disable: vi.fn() };
+
+ component.interactive = true;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).not.toHaveBeenCalled();
+
+ component.interactive = false;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).toHaveBeenCalledTimes(1);
+
+ // guard: a missing field config / form must not throw
+ expect(() => onInit(undefined)).not.toThrow();
+ });
+ });
+
+ describe("checkPort", () => {
+ it("should return false when there is no current port", () => {
+ component.currentPortID = undefined;
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should return false when the current port is no longer in the graph", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(false);
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should compare form data against the port's partition requirement", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({
+ portID: "input-0",
+ partitionRequirement: { type: "none" },
+ } as PortDescription);
+
+ // identical to the current partition requirement -> no change
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ // different -> change detected
+ expect((component as any).checkPort({ type: "hash", hashAttributeNames: ["a"] })).toBe(true);
+ });
+ });
+
+ describe("registerOnFormChangeHandler (edit propagation)", () => {
+ it("should push a debounced, changed form value to WorkflowActionService.setPortProperty", fakeAsync(() => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({ portID: "input-0" } as PortDescription);
+ const setSpy = vi.spyOn(workflowActionService, "setPortProperty").mockImplementation(() => {});
+
+ const newValue = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(newValue);
+ tick(FORM_DEBOUNCE_TIME_MS);
+
+ expect(setSpy).toHaveBeenCalledTimes(1);
Review Comment:
These debounce-based tests use `tick(FORM_DEBOUNCE_TIME_MS)` exactly. In this codebase other debounce tests add a small buffer (e.g., `+ 1`/`+ 10`) to avoid edge-timing flakiness when the timer is scheduled for the exact boundary.
##########
frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts:
##########
@@ -40,10 +51,333 @@ describe("PortPropertyEditFrameComponent", () => {
fixture = TestBed.createComponent(PortPropertyEditFrameComponent);
component = fixture.componentInstance;
workflowActionService = TestBed.inject(WorkflowActionService);
+ dynamicSchemaService = TestBed.inject(DynamicSchemaService);
+ texeraGraph = (workflowActionService as any).texeraGraph;
fixture.detectChanges();
Review Comment:
The test reaches into WorkflowActionService’s private `texeraGraph` field. This is brittle (a refactor of the service internals would break the test) and isn’t needed since `getTexeraGraph()` already returns the same instance.
##########
frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts:
##########
@@ -40,10 +51,333 @@ describe("PortPropertyEditFrameComponent", () => {
fixture = TestBed.createComponent(PortPropertyEditFrameComponent);
component = fixture.componentInstance;
workflowActionService = TestBed.inject(WorkflowActionService);
+ dynamicSchemaService = TestBed.inject(DynamicSchemaService);
+ texeraGraph = (workflowActionService as any).texeraGraph;
fixture.detectChanges();
});
+ afterEach(() => {
+ vi.restoreAllMocks();
+ fixture.destroy();
+ });
+
it("should create", () => {
expect(component).toBeTruthy();
});
+
+ describe("ngOnInit / handler registration", () => {
+ it("should register all three change handlers", () => {
+ const propertySpy = vi.spyOn(component as any, "registerPortPropertyChangeHandler");
+ const displayNameSpy = vi.spyOn(component as any, "registerPortDisplayNameChangeHandler");
+ const formChangeSpy = vi.spyOn(component as any, "registerOnFormChangeHandler");
+
+ component.ngOnInit();
+
+ expect(propertySpy).toHaveBeenCalledTimes(1);
+ expect(displayNameSpy).toHaveBeenCalledTimes(1);
+ expect(formChangeSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("onFormChanges", () => {
+ it("should forward the raw form event onto the source form-change stream", () => {
+ const received: Record<string, unknown>[] = [];
+ (component as any).sourceFormChangeEventStream.subscribe((e: Record<string, unknown>) => received.push(e));
+
+ const event = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(event);
+
+ expect(received).toEqual([event]);
+ });
+ });
+
+ describe("ngOnChanges", () => {
+ it("should adopt the new port id and open the editor when a port is provided", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: inputPort } } as any);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(showSpy).toHaveBeenCalledWith(inputPort);
+ });
+
+ it("should not open the editor when the new port id is undefined", () => {
+ const showSpy = vi.spyOn(component as any, "showPortPropertyEditor").mockImplementation(() => {});
+
+ component.ngOnChanges({ currentPortID: { currentValue: undefined } } as any);
+
+ expect(component.currentPortID).toBeUndefined();
+ expect(showSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("showPortPropertyEditor", () => {
+ it("should throw when the target port does not exist in the graph", () => {
+ expect(() => (component as any).showPortPropertyEditor({ operatorID: "ghost", portID: "input-0" })).toThrowError(
+ /does not exist/
+ );
+ });
+
+ it("should set the title but skip building the form when customization is not allowed", () => {
+ const descriptor: PortDescription = {
+ portID: "input-0",
+ displayName: "Input A",
+ partitionRequirement: { type: "none" },
+ dependencies: [],
+ };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: false },
+ } as any);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.currentPortID).toEqual(inputPort);
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formlyFields).toBeUndefined();
+ expect(component.formlyFormGroup).toBeUndefined();
+ });
+
+ it("should skip building the form for an output port even when customization is allowed", () => {
+ const descriptor: PortDescription = { portID: "output-0", displayName: "Output A" };
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue(descriptor);
+ vi.spyOn(dynamicSchemaService, "getDynamicSchema").mockReturnValue({
+ additionalMetadata: { allowPortCustomization: true },
+ } as any);
+
+ (component as any).showPortPropertyEditor(outputPort);
+
+ expect(component.formTitle).toBe("Output A");
+ expect(component.formlyFields).toBeUndefined();
+ });
+
+ it("should build the formly form from the port descriptor when customization is allowed on an input port", () => {
+ 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);
+
+ (component as any).showPortPropertyEditor(inputPort);
+
+ expect(component.formTitle).toBe("Input A");
+ expect(component.formData).toEqual({
+ partitionInfo: { type: "hash", hashAttributeNames: ["a"] },
+ dependencies: [{ id: 1, internal: false }],
+ });
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(Array.isArray(component.formlyFields)).toBe(true);
+ });
+ });
+
+ describe("setFormlyFormBinding (interactive vs. display mode)", () => {
+ it("should keep the form enabled when interactive, and disable it via the onInit hook when not", () => {
+ const fakeField: any = { fieldGroup: [{ key: "partitionInfo" }] };
+ vi.spyOn((component as any).formlyJsonschema, "toFieldConfig").mockReturnValue(fakeField);
+
+ (component as any).setFormlyFormBinding(mockPortSchema.jsonSchema);
+
+ expect(component.formlyFormGroup).toBeInstanceOf(FormGroup);
+ expect(component.formlyFields).toBe(fakeField.fieldGroup);
+
+ const onInit = fakeField.hooks.onInit;
+ const fakeForm = { disable: vi.fn() };
+
+ component.interactive = true;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).not.toHaveBeenCalled();
+
+ component.interactive = false;
+ onInit({ form: fakeForm });
+ expect(fakeForm.disable).toHaveBeenCalledTimes(1);
+
+ // guard: a missing field config / form must not throw
+ expect(() => onInit(undefined)).not.toThrow();
+ });
+ });
+
+ describe("checkPort", () => {
+ it("should return false when there is no current port", () => {
+ component.currentPortID = undefined;
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should return false when the current port is no longer in the graph", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(false);
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ });
+
+ it("should compare form data against the port's partition requirement", () => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({
+ portID: "input-0",
+ partitionRequirement: { type: "none" },
+ } as PortDescription);
+
+ // identical to the current partition requirement -> no change
+ expect((component as any).checkPort({ type: "none" })).toBe(false);
+ // different -> change detected
+ expect((component as any).checkPort({ type: "hash", hashAttributeNames: ["a"] })).toBe(true);
+ });
+ });
+
+ describe("registerOnFormChangeHandler (edit propagation)", () => {
+ it("should push a debounced, changed form value to WorkflowActionService.setPortProperty", fakeAsync(() => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({ portID: "input-0" } as PortDescription);
+ const setSpy = vi.spyOn(workflowActionService, "setPortProperty").mockImplementation(() => {});
+
+ const newValue = { type: "hash", hashAttributeNames: ["a"] };
+ component.onFormChanges(newValue);
+ tick(FORM_DEBOUNCE_TIME_MS);
+
+ expect(setSpy).toHaveBeenCalledTimes(1);
+ expect(setSpy).toHaveBeenCalledWith(inputPort, newValue);
+ // the setter passes a clone, not the same reference
+ expect(setSpy.mock.calls[0][1]).not.toBe(newValue);
+ expect(component.listeningToChange).toBe(true);
+ }));
+
+ it("should not propagate when the form value matches the current partition requirement", fakeAsync(() => {
+ component.currentPortID = inputPort;
+ vi.spyOn(texeraGraph, "hasPort").mockReturnValue(true);
+ vi.spyOn(texeraGraph, "getPortDescription").mockReturnValue({
+ portID: "input-0",
+ partitionRequirement: { type: "none" },
+ } as PortDescription);
+ const setSpy = vi.spyOn(workflowActionService, "setPortProperty").mockImplementation(() => {});
+
+ component.onFormChanges({ type: "none" });
+ tick(FORM_DEBOUNCE_TIME_MS);
+
+ expect(setSpy).not.toHaveBeenCalled();
Review Comment:
Same timing concern here: use a small buffer over `FORM_DEBOUNCE_TIME_MS` when ticking past the debounce to reduce boundary-related flakiness.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org