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-6683-5f9a6d2d8a974556293615f2e96970ee429b6e47
in repository https://gitbox.apache.org/repos/asf/texera.git

commit d3aa22e93fecce420595f7b999be60a2bc2ea924
Author: Matthew B. <[email protected]>
AuthorDate: Tue Jul 21 19:12:28 2026 -0700

    test(frontend): cover formly-utils stream and type helpers (#6683)
    
    ### What changes were proposed in this PR?
    - Append describe blocks to formly-utils.spec.ts covering
    setChildTypeDependency and createOutputFormChangeEventStream.
    - Assert timestamp-attribute collection and the child description
    expression, and the stream debounce, dedupe, and modelCheck gating using
    fake timers.
    ### Any related issues, documentation, discussions?
    Closes: #6682
    ### How was this PR tested?
    - Run: `cd frontend && node --max-old-space-size=8192
    ./node_modules/nx/dist/bin/nx.js test gui --watch=false
    --include=src/app/common/formly/formly-utils.spec.ts`, expect the suite
    passing.
    - Test-only change; no production code is modified.
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.8 in compliance with ASF
---
 .../src/app/common/formly/formly-utils.spec.ts     | 118 ++++++++++++++++++++-
 1 file changed, 117 insertions(+), 1 deletion(-)

diff --git a/frontend/src/app/common/formly/formly-utils.spec.ts 
b/frontend/src/app/common/formly/formly-utils.spec.ts
index 22ee315217..4bec75a669 100644
--- a/frontend/src/app/common/formly/formly-utils.spec.ts
+++ b/frontend/src/app/common/formly/formly-utils.spec.ts
@@ -18,7 +18,16 @@
  */
 
 import { FormlyFieldConfig } from "@ngx-formly/core";
-import { createShouldHideFieldFunc, getFieldByName, setHideExpression } from 
"./formly-utils";
+import {
+  createOutputFormChangeEventStream,
+  createShouldHideFieldFunc,
+  getFieldByName,
+  setChildTypeDependency,
+  setHideExpression,
+} from "./formly-utils";
+import { Subject } from "rxjs";
+import { FORM_DEBOUNCE_TIME_MS } from 
"../../workspace/service/execute-workflow/execute-workflow.service";
+import { PortSchema } from 
"../../workspace/types/workflow-compiling.interface";
 
 describe("getFieldByName", () => {
   it("returns the field whose key matches the name", () => {
@@ -90,3 +99,110 @@ describe("createShouldHideFieldFunc", () => {
     expect(hide(fieldWithModel({ target: 6 }))).toBe(false);
   });
 });
+
+describe("setChildTypeDependency", () => {
+  it("collects timestamp attribute names across all ports and sets a 
description expression on the child", () => {
+    const attributes: Record<string, PortSchema | undefined> = {
+      outPort0: [
+        { attributeName: "ts1", attributeType: "timestamp" },
+        { attributeName: "label", attributeType: "string" },
+      ],
+      outPort1: [{ attributeName: "ts2", attributeType: "timestamp" }],
+    };
+    const child: FormlyFieldConfig = { key: "attrValue" };
+    setChildTypeDependency(attributes, "attrName", [{ key: "other" }, child], 
"attrValue");
+
+    expect(child.expressions).toEqual({
+      "templateOptions.description":
+        "[\"ts1\",\"ts2\"].includes(model.attrName)? 'Input a datetime string' 
: 'Input a positive number'",
+    });
+  });
+
+  it("emits an empty timestamp list when there are no timestamp-typed 
attributes", () => {
+    const attributes: Record<string, PortSchema | undefined> = {
+      outPort0: [{ attributeName: "label", attributeType: "string" }],
+    };
+    const child: FormlyFieldConfig = { key: "attrValue" };
+    setChildTypeDependency(attributes, "attrName", [child], "attrValue");
+
+    expect(child.expressions).toEqual({
+      "templateOptions.description":
+        "[].includes(model.attrName)? 'Input a datetime string' : 'Input a 
positive number'",
+    });
+  });
+
+  it("treats undefined attributes as an empty timestamp list", () => {
+    const child: FormlyFieldConfig = { key: "attrValue" };
+    setChildTypeDependency(undefined, "attrName", [child], "attrValue");
+
+    expect(child.expressions).toEqual({
+      "templateOptions.description":
+        "[].includes(model.attrName)? 'Input a datetime string' : 'Input a 
positive number'",
+    });
+  });
+
+  it("is a no-op when the named child field is not present", () => {
+    const attributes: Record<string, PortSchema | undefined> = {
+      outPort0: [{ attributeName: "ts1", attributeType: "timestamp" }],
+    };
+    const unrelated: FormlyFieldConfig = { key: "other" };
+    setChildTypeDependency(attributes, "attrName", [unrelated], 
"missingChild");
+
+    expect(unrelated.expressions).toBeUndefined();
+  });
+});
+
+describe("createOutputFormChangeEventStream", () => {
+  beforeEach(() => {
+    vi.useFakeTimers();
+  });
+
+  afterEach(() => {
+    vi.useRealTimers();
+  });
+
+  it("debounces bursts, emitting only the latest value once the debounce 
window elapses", () => {
+    const source = new Subject<Record<string, unknown>>();
+    const emissions: Record<string, unknown>[] = [];
+    createOutputFormChangeEventStream(source, () => true).subscribe(v => 
emissions.push(v));
+
+    source.next({ v: 1 });
+    vi.advanceTimersByTime(FORM_DEBOUNCE_TIME_MS - 1);
+    source.next({ v: 2 });
+    // still within the debounce window relative to the second emission
+    vi.advanceTimersByTime(FORM_DEBOUNCE_TIME_MS - 1);
+    expect(emissions).toEqual([]);
+
+    vi.advanceTimersByTime(1);
+    expect(emissions).toEqual([{ v: 2 }]);
+  });
+
+  it("drops a repeated identical value via distinctUntilChanged", () => {
+    const source = new Subject<Record<string, unknown>>();
+    const emissions: Record<string, unknown>[] = [];
+    createOutputFormChangeEventStream(source, () => true).subscribe(v => 
emissions.push(v));
+
+    const sameRef = { v: 1 };
+    source.next(sameRef);
+    vi.advanceTimersByTime(FORM_DEBOUNCE_TIME_MS);
+    source.next(sameRef);
+    vi.advanceTimersByTime(FORM_DEBOUNCE_TIME_MS);
+
+    expect(emissions).toEqual([sameRef]);
+  });
+
+  it("gates emissions through the modelCheck predicate", () => {
+    const source = new Subject<Record<string, unknown>>();
+    const emissions: Record<string, unknown>[] = [];
+    const modelCheck = vi.fn((formData: Record<string, unknown>) => 
formData["keep"] === true);
+    createOutputFormChangeEventStream(source, modelCheck).subscribe(v => 
emissions.push(v));
+
+    source.next({ keep: false });
+    vi.advanceTimersByTime(FORM_DEBOUNCE_TIME_MS);
+    source.next({ keep: true });
+    vi.advanceTimersByTime(FORM_DEBOUNCE_TIME_MS);
+
+    expect(emissions).toEqual([{ keep: true }]);
+    expect(modelCheck).toHaveBeenCalledTimes(2);
+  });
+});

Reply via email to