This is an automated email from the ASF dual-hosted git repository.

mengw15 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new c93c8f7975 test: add test cases for PresetWrapperComponent spec (#5009)
c93c8f7975 is described below

commit c93c8f79753abad51fb1f2c567b55dd7e7eb35c4
Author: Matthew B. <[email protected]>
AuthorDate: Sun May 10 09:30:47 2026 -0700

    test: add test cases for PresetWrapperComponent spec (#5009)
    
    ### What changes were proposed in this PR?
    Replaces the placeholder/commented-out body of
    preset-wrapper.component.spec.ts with a real Vitest suite for
    PresetWrapperComponent, and re-enables the file in angular.json and
    tsconfig.spec.json (it was previously excluded from the build).
    
    The new suite uses fake PresetService and NzMessageService providers and
    covers:
    - ngOnInit guards: throws when field.key, templateOptions, or presetKey
    are missing; populates searchResults from getPresets.
    - Functional API: applyPreset / deletePreset forward to PresetService
    with the configured preset key triple; getEntryTitle,
    getEntryDescription, and the show-all / prefix-match / empty-term /
    no-match branches of
      getSearchResults.
      - Dropdown visibility: opening re-fetches presets, closing does not.
    - Stream subscriptions: savePresetsStream updates searchResults only
    when type + target match; valueChanges only refreshes while the menu is
    open; ngOnDestroy tears down subscriptions.
    - savePreset: builds the payload from sibling preset-wrapper fields
    (ignoring non-preset siblings) and routes valid presets to createPreset;
    invalid presets surface a toast error.
    
    ### Any related issues, documentation, or discussions?
    
    Closes: #4963
    
    ### How was this PR tested?
    `npx ng test --watch=false
    
--include='src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts'`
    --> 24 tests pass locally under the Vitest runner.
    
      Was this PR authored or co-authored using generative AI tooling?
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-Authored with Claude Opus 4.7 in compliance with ASF.
    
    Co-authored-by: Meng Wang <[email protected]>
---
 frontend/angular.json                              |   1 -
 .../preset-wrapper.component.spec.ts               | 578 ++++++++++++---------
 frontend/src/tsconfig.spec.json                    |   1 -
 3 files changed, 336 insertions(+), 244 deletions(-)

diff --git a/frontend/angular.json b/frontend/angular.json
index c2f9362329..3c07ded34e 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -94,7 +94,6 @@
             "include": ["**/*.spec.ts"],
             "setupFiles": ["src/jsdom-svg-polyfill.ts"],
             "exclude": [
-              
"**/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts",
               "**/app/common/service/user/config/user-config.service.spec.ts",
               
"**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts",
               "**/app/workspace/component/workspace.component.spec.ts"
diff --git 
a/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
 
b/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
index 6fd1418ff4..09f415f256 100644
--- 
a/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
+++ 
b/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
@@ -17,245 +17,339 @@
  * under the License.
  */
 
-// import { CommonModule } from "@angular/common";
-// import { HttpClientTestingModule, HttpTestingController } from 
"@angular/common/http/testing";
-// import { Component, ViewChild } from "@angular/core";
-// import { ComponentFixture, fakeAsync, TestBed, tick } from 
"@angular/core/testing";
-// import { FormGroup, ReactiveFormsModule } from "@angular/forms";
-// import { BrowserModule, By } from "@angular/platform-browser";
-// import { BrowserAnimationsModule } from 
"@angular/platform-browser/animations";
-// import { FormlyFieldConfig, FormlyModule } from "@ngx-formly/core";
-// import { FormlyNgZorroAntdModule } from "@ngx-formly/ng-zorro-antd";
-// import { NzDropDownModule } from "ng-zorro-antd/dropdown";
-// import { NzMenuModule } from "ng-zorro-antd/menu";
-// import { NzMessageModule } from "ng-zorro-antd/message";
-// import { PresetService } from 
"src/app/workspace/service/preset/preset.service";
-// import { CustomNgMaterialModule } from "../../custom-ng-material.module";
-// import { nonNull } from "../../util/assert";
-// import { TEXERA_FORMLY_CONFIG } from "../formly-config";
-// import { PresetWrapperComponent } from "./preset-wrapper.component";
-
-// const testPreset = { testkey: "testPresetValue", otherkey: 
"otherPresetValue" };
-// const fieldKey = "testkey";
-// const presetKey = {
-//   presetType: "testPresetType",
-//   saveTarget: "testPresetSaveTarget",
-//   applyTarget: "testPresetApplyTarget",
-// };
-
-// /**
-//  * This mock component creates a formly form so that Formly api
-//  * can be used to generate a form with the PresetWrapperComponent
-//  */
-// @Component({
-//   selector: "texera-preset-test-cmp",
-//   template: ` <form [formGroup]="form">
-//     <formly-form [form]="form" [fields]="fields"> </formly-form>
-//   </form>`,
-// })
-// class MockFormComponent {
-//   @ViewChild(PresetWrapperComponent) child!: PresetWrapperComponent;
-//   form = new FormGroup({});
-//   fields: FormlyFieldConfig[] = [
-//     {
-//       wrappers: ["form-field", "preset-wrapper"],
-//       key: fieldKey,
-//       type: "input",
-//       templateOptions: {
-//         presetKey: presetKey,
-//       },
-//       defaultValue: "defaultValue",
-//     },
-//   ];
-// }
-
-// describe("PresetWrapperComponent", () => {
-//   let component: PresetWrapperComponent;
-//   let fixture: ComponentFixture<MockFormComponent>;
-//   let httpMock: HttpTestingController;
-
-//   beforeEach(() => {
-//     TestBed.configureTestingModule({
-//       declarations: [MockFormComponent, PresetWrapperComponent],
-//       imports: [
-//         CommonModule,
-//         BrowserModule,
-//         ReactiveFormsModule,
-//         FormlyModule.forRoot(TEXERA_FORMLY_CONFIG),
-//         FormlyNgZorroAntdModule,
-//         CustomNgMaterialModule,
-//         BrowserAnimationsModule,
-//         HttpClientTestingModule,
-//         NzMessageModule,
-//         NzMenuModule,
-//         NzDropDownModule,
-//       ],
-//     }).compileComponents();
-
-//     fixture = TestBed.createComponent(MockFormComponent);
-//     httpMock = TestBed.inject(HttpTestingController);
-//     fixture.detectChanges();
-//     component = 
fixture.debugElement.query(By.directive(PresetWrapperComponent)).componentInstance;
-//   });
-
-//   it("should create", () => {
-//     expect(component).toBeTruthy();
-//   });
-
-//   describe("functional api", () => {
-//     it("should properly apply a preset", () => {
-//       const presetService = TestBed.inject(PresetService);
-//       vi.spyOn(presetService, "applyPreset");
-
-//       component.applyPreset(testPreset);
-//       expect(presetService.applyPreset).toHaveBeenCalledExactlyOnceWith(
-//         presetKey.presetType,
-//         presetKey.applyTarget,
-//         testPreset
-//       );
-//     });
-
-//     it("should properly delete a preset", () => {
-//       const presetService = TestBed.inject(PresetService);
-//       const otherPreset = { testkey: "otherPresetValue2", otherkey: 
"otherPresetValue2" };
-//       const existingPresets = [testPreset, otherPreset];
-//       vi.spyOn(presetService, 
"getPresets").mockReturnValue(existingPresets);
-//       const deletePreset = vi.spyOn(presetService, "deletePreset");
-
-//       component.deletePreset(testPreset);
-//       expect(deletePreset).toHaveBeenCalledTimes(1);
-//       expect(deletePreset.calls.mostRecent().args.slice(0, 3)).toEqual([
-//         presetKey.presetType,
-//         presetKey.saveTarget,
-//         testPreset,
-//       ]);
-//     });
-
-//     it("should properly generate a preset title", () => {
-//       
expect(component.getEntryTitle(testPreset)).toEqual(expect.any(String));
-//       expect(component.getEntryTitle(testPreset).replace(/\s\s+/g, 
"")).not.toEqual("");
-//     });
-
-//     it("should properly generate a preset description", () => {
-//       
expect(component.getEntryDescription(testPreset)).toEqual(expect.any(String));
-//       expect(component.getEntryDescription(testPreset).replace(/\s\s+/g, 
"")).not.toEqual("");
-//     });
-
-//     it("should properly generate search results", () => {
-//       expect(component.getSearchResults([testPreset], "", 
true)).toEqual([testPreset]);
-//       expect(component.getSearchResults([testPreset], "asdf", 
true)).toEqual([testPreset]);
-//       expect(component.getSearchResults([testPreset], 
component.getEntryTitle(testPreset), true)).toEqual([testPreset]);
-
-//       expect(component.getSearchResults([testPreset], "", 
false)).toEqual([testPreset]);
-//       expect(component.getSearchResults([testPreset], "asdf", 
false)).toEqual([]);
-//       expect(component.getSearchResults([testPreset], 
component.getEntryTitle(testPreset), false)).toEqual([
-//         testPreset,
-//       ]);
-//     });
-//   });
-
-//   describe("template bindings", () => {
-//     it("should update search results when dropdown becomes visible", () => {
-//       const debugElement = 
fixture.debugElement.query(By.directive(PresetWrapperComponent));
-//       component.searchResults = [];
-//       vi.spyOn(component, "getSearchResults").mockReturnValue([testPreset]);
-
-//       expect(component.searchResults).toEqual([]);
-//       // trigger nzVisibleChange, as if the dropdown menu was triggered
-//       
debugElement.query(By.css(".preset-field")).triggerEventHandler("nzVisibleChange",
 true);
-//       fixture.detectChanges();
-//       expect(component.searchResults).toEqual([testPreset]);
-//     });
-
-//     it("should generate an entry in the dropdown for each search result", 
fakeAsync(() => {
-//       // recreate fixture and component in fakeAsync context so that event 
handlers will become synchronous
-//       fixture = TestBed.createComponent(MockFormComponent);
-//       fixture.detectChanges();
-//       component = 
fixture.debugElement.query(By.directive(PresetWrapperComponent)).componentInstance;
-
-//       const otherPreset = { testkey: "otherPresetValue2", otherkey: 
"otherPresetValue2" };
-//       const searchResults = [testPreset, otherPreset];
-//       const debugElement = 
fixture.debugElement.query(By.directive(PresetWrapperComponent));
-
-//       // trigger dropdown menu
-//       vi.spyOn(component, 
"getSearchResults").mockReturnValue(searchResults);
-//       
debugElement.query(By.css(".preset-field")).nativeElement.dispatchEvent(new 
Event("click"));
-//       fixture.detectChanges();
-//       tick(1000);
-//       fixture.detectChanges();
-
-//       const dropdown = nonNull(document.body.querySelector(".preset-menu"));
-//       
expect(dropdown.childElementCount).toEqual(component.searchResults.length);
-
-//       // check that title and description of each dropdown entry match 
their preset
-//       const nodes = dropdown.querySelectorAll("li");
-//       for (let i = 0; i < dropdown.childElementCount; i++) {
-//         let node = nodes[i];
-//         let preset = searchResults[i];
-//         
expect(node.querySelector(".title")?.innerHTML).toEqual(component.getEntryTitle(preset));
-//         
expect(node.querySelector(".description")?.innerHTML).toEqual(component.getEntryDescription(preset));
-//       }
-//     }));
-
-//     it("should apply the preset if a preset entry is clicked", fakeAsync(() 
=> {
-//       // recreate fixture and component in fakeAsync context so that event 
handlers will become synchronous
-//       fixture = TestBed.createComponent(MockFormComponent);
-//       fixture.detectChanges();
-//       component = 
fixture.debugElement.query(By.directive(PresetWrapperComponent)).componentInstance;
-
-//       const searchResults = [testPreset];
-//       const debugElement = 
fixture.debugElement.query(By.directive(PresetWrapperComponent));
-//       vi.spyOn(component, 
"getSearchResults").mockReturnValue(searchResults);
-//       vi.spyOn(component, "applyPreset");
-
-//       // trigger dropdown menu
-//       
debugElement.query(By.css(".preset-field")).nativeElement.dispatchEvent(new 
Event("click"));
-//       fixture.detectChanges();
-//       tick(1000);
-//       fixture.detectChanges();
-
-//       const dropdown = nonNull(document.body.querySelector(".preset-menu"));
-//       const dropdownEntry = 
nonNull(dropdown.querySelector(".dropdown-entry"));
-//       
expect(dropdown.childElementCount).toEqual(component.searchResults.length);
-//       dropdownEntry.dispatchEvent(new Event("click"));
-//       
expect(component.applyPreset).toHaveBeenCalledExactlyOnceWith(testPreset);
-//     }));
-
-//     it("should delete the preset if a preset entry's delete button is 
clicked", fakeAsync(() => {
-//       // recreate fixture and component in fakeAsync context so that event 
handlers will become synchronous
-//       fixture = TestBed.createComponent(MockFormComponent);
-//       fixture.detectChanges();
-//       component = 
fixture.debugElement.query(By.directive(PresetWrapperComponent)).componentInstance;
-
-//       const searchResults = [testPreset];
-//       const debugElement = 
fixture.debugElement.query(By.directive(PresetWrapperComponent));
-//       vi.spyOn(component, 
"getSearchResults").mockReturnValue(searchResults);
-//       vi.spyOn(component, "deletePreset");
-
-//       // trigger dropdown menu
-//       
debugElement.query(By.css(".preset-field")).nativeElement.dispatchEvent(new 
Event("click"));
-//       fixture.detectChanges();
-//       tick(1000);
-//       fixture.detectChanges();
-
-//       // press delete button
-//       const dropdown = nonNull(document.body.querySelector(".preset-menu"));
-//       const dropdownDeleteButton = 
nonNull(dropdown.querySelector(".delete-button"));
-//       
expect(dropdown.childElementCount).toEqual(component.searchResults.length);
-//       dropdownDeleteButton.dispatchEvent(new Event("click"));
-//       
expect(component.deletePreset).toHaveBeenCalledExactlyOnceWith(testPreset);
-//     }));
-
-//     it("should set new search results whenever the value of the field 
changes", fakeAsync(() => {
-//       const inputfield = fixture.debugElement.query(By.css(".preset-field 
input")).nativeElement;
-//       const searchResults = [testPreset];
-//       vi.spyOn(component, 
"getSearchResults").mockReturnValue(searchResults);
-
-//       // trigger input event as if typing
-//       inputfield.value = "asdf";
-//       inputfield.dispatchEvent(new Event("input"));
-//       tick(1000);
-//       expect(component.searchResults).toEqual(searchResults);
-//     }));
-//   });
-// });
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { FormControl } from "@angular/forms";
+import { FormlyFieldConfig } from "@ngx-formly/core";
+import { NzMessageService } from "ng-zorro-antd/message";
+import { Subject, of } from "rxjs";
+import { Preset, PresetService } from 
"src/app/workspace/service/preset/preset.service";
+import { PresetKey, PresetWrapperComponent } from "./preset-wrapper.component";
+
+const fieldKey = "testkey";
+const presetKey: PresetKey = {
+  presetType: "testPresetType",
+  saveTarget: "testPresetSaveTarget",
+  applyTarget: "testPresetApplyTarget",
+};
+const testPreset: Preset = { testkey: "testPresetValue", otherkey: 
"otherPresetValue" };
+const otherPreset: Preset = { testkey: "otherPresetValue2", otherkey: 
"otherPresetValue3" };
+
+describe("PresetWrapperComponent", () => {
+  let component: PresetWrapperComponent;
+  let fixture: ComponentFixture<PresetWrapperComponent>;
+  let formControl: FormControl;
+  let presetServiceStub: {
+    applyPreset: ReturnType<typeof vi.fn>;
+    deletePreset: ReturnType<typeof vi.fn>;
+    createPreset: ReturnType<typeof vi.fn>;
+    getPresets: ReturnType<typeof vi.fn>;
+    isValidPreset: ReturnType<typeof vi.fn>;
+    savePresetsStream: Subject<{ type: string; target: string; presets: 
Preset[] }>;
+    applyPresetStream: Subject<{ type: string; target: string; preset: Preset 
}>;
+  };
+  let messageStub: {
+    error: ReturnType<typeof vi.fn>;
+    success: ReturnType<typeof vi.fn>;
+    info: ReturnType<typeof vi.fn>;
+    warning: ReturnType<typeof vi.fn>;
+  };
+
+  // Builds a minimal FormlyFieldConfig sufficient for ngOnInit to run.
+  // ngOnInit also calls filterPresetFromForm(), which iterates
+  // field.parent.fieldGroup looking for sibling preset-wrapper fields, so
+  // we expose a single sibling pointing at an empty model by default.
+  const buildField = (overrides: Partial<FormlyFieldConfig> = {}): 
FormlyFieldConfig => {
+    const self = {
+      key: fieldKey,
+      wrappers: ["preset-wrapper"],
+      model: { [fieldKey]: "" },
+    } as FormlyFieldConfig;
+    return {
+      key: fieldKey,
+      formControl,
+      templateOptions: { presetKey },
+      parent: { fieldGroup: [self] },
+      ...overrides,
+    } as FormlyFieldConfig;
+  };
+
+  beforeEach(async () => {
+    formControl = new FormControl("");
+
+    presetServiceStub = {
+      applyPreset: vi.fn(),
+      deletePreset: vi.fn(),
+      createPreset: vi.fn(),
+      getPresets: vi.fn().mockReturnValue(of([])),
+      isValidPreset: vi.fn().mockReturnValue(true),
+      savePresetsStream: new Subject(),
+      applyPresetStream: new Subject(),
+    };
+    messageStub = {
+      error: vi.fn(),
+      success: vi.fn(),
+      info: vi.fn(),
+      warning: vi.fn(),
+    };
+
+    // Override the template so the spec doesn't depend on the ng-zorro
+    // dropdown machinery — we exercise the public component API directly.
+    TestBed.overrideComponent(PresetWrapperComponent, { set: { template: "" } 
});
+
+    await TestBed.configureTestingModule({
+      imports: [PresetWrapperComponent],
+      providers: [
+        { provide: PresetService, useValue: presetServiceStub },
+        { provide: NzMessageService, useValue: messageStub },
+      ],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(PresetWrapperComponent);
+    component = fixture.componentInstance;
+  });
+
+  it("should create", () => {
+    component.field = buildField();
+    fixture.detectChanges();
+    expect(component).toBeTruthy();
+  });
+
+  describe("ngOnInit", () => {
+    it("throws when field.key is missing", () => {
+      component.field = buildField({ key: undefined });
+      expect(() => component.ngOnInit()).toThrow();
+    });
+
+    it("throws when templateOptions is missing", () => {
+      component.field = buildField({ templateOptions: undefined });
+      expect(() => component.ngOnInit()).toThrow();
+    });
+
+    it("throws when templateOptions.presetKey is missing", () => {
+      component.field = buildField({ templateOptions: {} });
+      expect(() => component.ngOnInit()).toThrow();
+    });
+
+    it("populates searchResults from presetService.getPresets on init", () => {
+      presetServiceStub.getPresets.mockReturnValue(of([testPreset, 
otherPreset]));
+      component.field = buildField();
+
+      component.ngOnInit();
+
+      
expect(presetServiceStub.getPresets).toHaveBeenCalledWith(presetKey.presetType, 
presetKey.saveTarget);
+      expect(component.searchResults).toEqual([testPreset, otherPreset]);
+    });
+  });
+
+  describe("functional api", () => {
+    beforeEach(() => {
+      component.field = buildField();
+      component.ngOnInit();
+    });
+
+    it("applyPreset forwards to PresetService with the configured presetType + 
applyTarget", () => {
+      component.applyPreset(testPreset);
+      expect(presetServiceStub.applyPreset).toHaveBeenCalledTimes(1);
+      expect(presetServiceStub.applyPreset).toHaveBeenCalledWith(
+        presetKey.presetType,
+        presetKey.applyTarget,
+        testPreset
+      );
+    });
+
+    it("deletePreset forwards to PresetService with the configured presetType 
+ saveTarget", () => {
+      component.deletePreset(testPreset);
+      expect(presetServiceStub.deletePreset).toHaveBeenCalledTimes(1);
+      const args = presetServiceStub.deletePreset.mock.calls[0];
+      expect(args.slice(0, 3)).toEqual([presetKey.presetType, 
presetKey.saveTarget, testPreset]);
+    });
+
+    it("getEntryTitle returns the value at field.key", () => {
+      expect(component.getEntryTitle(testPreset)).toBe("testPresetValue");
+    });
+
+    it("getEntryDescription joins all non-key values with commas", () => {
+      
expect(component.getEntryDescription(testPreset)).toBe("otherPresetValue");
+      expect(
+        component.getEntryDescription({
+          testkey: "title",
+          a: "first",
+          b: "second",
+        })
+      ).toBe("first, second");
+    });
+
+    describe("getSearchResults", () => {
+      it("returns a copy of all presets when showAllResults is true", () => {
+        const presets: Preset[] = [testPreset, otherPreset];
+        const results = component.getSearchResults(presets, "anything", true);
+        expect(results).toEqual(presets);
+        expect(results).not.toBe(presets);
+      });
+
+      it("returns all presets when showAllResults is true even if the search 
term doesn't match", () => {
+        expect(component.getSearchResults([testPreset], "no-match", 
true)).toEqual([testPreset]);
+      });
+
+      it("filters by case-insensitive prefix match on the entry title when 
showAllResults is false", () => {
+        const presets: Preset[] = [testPreset, otherPreset];
+        // testPreset title 'testPresetValue' starts with 'TEST'
+        expect(component.getSearchResults(presets, "TEST", 
false)).toEqual([testPreset]);
+        // otherPreset title 'otherPresetValue2' starts with 'other'
+        expect(component.getSearchResults(presets, "other", 
false)).toEqual([otherPreset]);
+      });
+
+      it("returns the full list when search term is empty and showAllResults 
is false", () => {
+        expect(component.getSearchResults([testPreset], "", 
false)).toEqual([testPreset]);
+      });
+
+      it("returns an empty list when the search term matches nothing", () => {
+        expect(component.getSearchResults([testPreset], "zzzz", 
false)).toEqual([]);
+      });
+    });
+  });
+
+  describe("dropdown visibility", () => {
+    beforeEach(() => {
+      component.field = buildField();
+      component.ngOnInit();
+    });
+
+    it("re-fetches presets and updates searchResults when the dropdown opens", 
() => {
+      presetServiceStub.getPresets.mockReturnValue(of([testPreset]));
+      // ngOnInit has already called getPresets once.
+      const baseline = presetServiceStub.getPresets.mock.calls.length;
+
+      component.onDropdownVisibilityEvent(true);
+
+      expect(presetServiceStub.getPresets.mock.calls.length).toBe(baseline + 
1);
+      expect(component.searchResults).toEqual([testPreset]);
+    });
+
+    it("does not refetch when the dropdown closes", () => {
+      const baseline = presetServiceStub.getPresets.mock.calls.length;
+      component.onDropdownVisibilityEvent(false);
+      expect(presetServiceStub.getPresets.mock.calls.length).toBe(baseline);
+    });
+  });
+
+  describe("PresetService stream subscriptions", () => {
+    beforeEach(() => {
+      component.field = buildField();
+      component.ngOnInit();
+    });
+
+    it("updates searchResults when savePresetsStream emits a matching event", 
() => {
+      component.searchResults = [];
+      const presets: Preset[] = [testPreset, otherPreset];
+
+      presetServiceStub.savePresetsStream.next({
+        type: presetKey.presetType,
+        target: presetKey.saveTarget,
+        presets,
+      });
+
+      expect(component.searchResults).toEqual(presets);
+    });
+
+    it("ignores savePresetsStream events for a different presetType", () => {
+      component.searchResults = [];
+      presetServiceStub.savePresetsStream.next({
+        type: "differentType",
+        target: presetKey.saveTarget,
+        presets: [testPreset],
+      });
+      expect(component.searchResults).toEqual([]);
+    });
+
+    it("ignores savePresetsStream events for a different saveTarget", () => {
+      component.searchResults = [];
+      presetServiceStub.savePresetsStream.next({
+        type: presetKey.presetType,
+        target: "differentTarget",
+        presets: [testPreset],
+      });
+      expect(component.searchResults).toEqual([]);
+    });
+
+    it("does not refresh searchResults from form value changes while the 
dropdown is closed", () => {
+      const baselineCalls = presetServiceStub.getPresets.mock.calls.length;
+      component.presetMenuVisible = false;
+
+      formControl.setValue("typing");
+
+      // No additional getPresets call because the menu is closed.
+      
expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls);
+    });
+
+    it("refreshes searchResults from form value changes while the dropdown is 
open", async () => {
+      component.presetMenuVisible = true;
+      presetServiceStub.getPresets.mockReturnValue(of([testPreset]));
+      const baselineCalls = presetServiceStub.getPresets.mock.calls.length;
+
+      formControl.setValue("typing");
+      // The valueChanges handler is debounced(0) — wait one microtask tick.
+      await new Promise(resolve => setTimeout(resolve, 0));
+
+      
expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls + 1);
+    });
+
+    it("stops responding to stream events after ngOnDestroy", () => {
+      component.searchResults = [];
+      component.ngOnDestroy();
+
+      presetServiceStub.savePresetsStream.next({
+        type: presetKey.presetType,
+        target: presetKey.saveTarget,
+        presets: [testPreset],
+      });
+
+      expect(component.searchResults).toEqual([]);
+    });
+  });
+
+  describe("savePreset", () => {
+    // savePreset() reads sibling preset-wrapper fields off 
field.parent.fieldGroup
+    // to construct the preset payload.
+    const buildFieldWithSiblings = (model: Record<string, unknown>): 
FormlyFieldConfig => {
+      const fieldGroup: FormlyFieldConfig[] = [
+        { key: fieldKey, wrappers: ["preset-wrapper"], model } as 
FormlyFieldConfig,
+        { key: "otherkey", wrappers: ["preset-wrapper"], model } as 
FormlyFieldConfig,
+        // Non-preset sibling — must be ignored.
+        { key: "ignored", wrappers: ["form-field"], model } as 
FormlyFieldConfig,
+      ];
+      return {
+        key: fieldKey,
+        formControl,
+        templateOptions: { presetKey },
+        parent: { fieldGroup },
+      } as FormlyFieldConfig;
+    };
+
+    it("creates a preset built from sibling preset-wrapper fields when the 
preset is valid", () => {
+      component.field = buildFieldWithSiblings({ testkey: "v1", otherkey: 
"v2", ignored: "x" });
+      component.ngOnInit();
+      presetServiceStub.isValidPreset.mockReturnValue(true);
+
+      component.savePreset();
+
+      expect(presetServiceStub.isValidPreset).toHaveBeenCalledWith({ testkey: 
"v1", otherkey: "v2" });
+      
expect(presetServiceStub.createPreset).toHaveBeenCalledWith(presetKey.presetType,
 presetKey.saveTarget, {
+        testkey: "v1",
+        otherkey: "v2",
+      });
+      expect(messageStub.error).not.toHaveBeenCalled();
+    });
+
+    it("shows an error toast and does not create a preset when the preset is 
invalid", () => {
+      component.field = buildFieldWithSiblings({ testkey: "", otherkey: "v2" 
});
+      component.ngOnInit();
+      presetServiceStub.isValidPreset.mockReturnValue(false);
+
+      component.savePreset();
+
+      expect(presetServiceStub.createPreset).not.toHaveBeenCalled();
+      expect(messageStub.error).toHaveBeenCalledTimes(1);
+    });
+  });
+});
diff --git a/frontend/src/tsconfig.spec.json b/frontend/src/tsconfig.spec.json
index 5a73e241a3..5e9a1f049c 100644
--- a/frontend/src/tsconfig.spec.json
+++ b/frontend/src/tsconfig.spec.json
@@ -13,7 +13,6 @@
   "exclude": [
     // Specs whose body is entirely commented out / placeholder — these
     // need real test cases written before they can be re-enabled.
-    "**/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts",
     "**/app/common/service/user/config/user-config.service.spec.ts",
     "**/app/workspace/component/workspace.component.spec.ts",
 

Reply via email to