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