Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown";
import { FormControl } from "@angular/forms";
import { FormlyFieldConfig } from "@ngx-formly/core";
import { NzMessageService } from "ng-zorro-antd/message";
Expand Down Expand Up @@ -135,6 +137,43 @@ describe("PresetWrapperComponent", () => {
expect(presetServiceStub.getPresets).toHaveBeenCalledWith(presetKey.presetType, presetKey.saveTarget);
expect(component.searchResults).toEqual([testPreset, otherPreset]);
});

it("seeds the search term from the form control's value", () => {
formControl.setValue("seeded");
component.field = buildField();

component.ngOnInit();

expect(component["searchTerm"]).toBe("seeded");
});

it("seeds an empty search term when the form control holds null", () => {
formControl.setValue(null);
component.field = buildField();

component.ngOnInit();

expect(component["searchTerm"]).toBe("");
});
});

describe("setupFieldConfig", () => {
it("merges the preset wrappers and the preset key into the given config", () => {
const config: FormlyFieldConfig = { key: fieldKey, templateOptions: { label: "kept" } };

PresetWrapperComponent.setupFieldConfig(config, "operator", "MySQLSource", "MySQLSource-op-1");

expect(config.wrappers).toEqual(["form-field", "preset-wrapper"]);
expect(config.templateOptions?.presetKey).toEqual({
presetType: "operator",
saveTarget: "MySQLSource",
applyTarget: "MySQLSource-op-1",
});
// the browser's own autocomplete is turned off so it cannot cover the preset menu
expect(config.templateOptions?.attributes).toEqual({ autocomplete: "off" });
// pre-existing options survive the merge
expect(config.templateOptions?.label).toBe("kept");
});
});

describe("functional api", () => {
Expand Down Expand Up @@ -268,13 +307,17 @@ describe("PresetWrapperComponent", () => {
expect(component.searchResults).toEqual([]);
});

it("does not refresh searchResults from form value changes while the dropdown is closed", () => {
it("does not refresh searchResults from form value changes while the dropdown is closed", async () => {
const baselineCalls = presetServiceStub.getPresets.mock.calls.length;
component.presetMenuVisible = false;

formControl.setValue("typing");
// the handler is debounced(0); without this tick it would not have run at all and
// the assertion below would hold no matter what the menu state is
await new Promise(resolve => setTimeout(resolve, 0));

// No additional getPresets call because the menu is closed.
// the term is still tracked, but the menu being closed suppresses the refetch
expect(component["searchTerm"]).toBe("typing");
expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls);
});

Expand All @@ -290,6 +333,44 @@ describe("PresetWrapperComponent", () => {
expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls + 1);
});

it("adopts the preset carried by a matching applyPresetStream event", () => {
presetServiceStub.applyPresetStream.next({
type: presetKey.presetType,
target: presetKey.applyTarget,
preset: testPreset,
});

expect(component["basePreset"]).toBe(testPreset);
});

it("ignores applyPresetStream events for a different presetType or applyTarget", () => {
const before = component["basePreset"];

presetServiceStub.applyPresetStream.next({
type: "someOtherType",
target: presetKey.applyTarget,
preset: testPreset,
});
presetServiceStub.applyPresetStream.next({
type: presetKey.presetType,
target: "someOtherTarget",
preset: otherPreset,
});

expect(component["basePreset"]).toBe(before);
});

it("clears the search term when the form value becomes null while the dropdown is open", async () => {
component.presetMenuVisible = true;
component["searchTerm"] = "typed";

formControl.setValue(null);
// the valueChanges handler is debounced(0) — wait one tick
await new Promise(resolve => setTimeout(resolve, 0));

expect(component["searchTerm"]).toBe("");
});

it("stops responding to stream events after ngOnDestroy", () => {
component.searchResults = [];
component.ngOnDestroy();
Expand Down Expand Up @@ -360,6 +441,20 @@ describe("PresetWrapperComponent", () => {
fixture.detectChanges();
};

it("routes the dropdown's own visibility event into onDropdownVisibilityEvent", () => {
// the tests above call the handler directly; this drives it through the template
// binding, which is the wiring that would break if the output were renamed
initWith([testPreset]);
const handler = vi.spyOn(component, "onDropdownVisibilityEvent");

fixture.debugElement
.query(By.directive(NzDropdownDirective))
.injector.get(NzDropdownDirective)
.nzVisibleChange.emit(true);

expect(handler).toHaveBeenCalledWith(true);
});

it("renders the save button and saves the preset when it is clicked", () => {
initWith([]);

Expand All @@ -372,39 +467,57 @@ describe("PresetWrapperComponent", () => {
expect(savePreset).toHaveBeenCalled();
});

it("feeds the dropdown *ngFor with one entry per preset, titled and described", () => {
// The rows live in an nz-dropdown-menu that only mounts into a CDK overlay on a
// real user open, which jsdom does not drive; assert the list the *ngFor is bound
// to and the interpolations it renders for each row instead.
/**
* The rows live in an nz-dropdown-menu, whose content is an ng-template that only
* mounts into a CDK overlay when the dropdown opens — jsdom never drives that.
* Instantiating the template directly puts the rows in the fixture's DOM so the
* *ngFor, the interpolations and the row click handlers all really run.
*/
const renderDropdownRows = (): HTMLElement[] => {
const menu = fixture.debugElement.query(By.directive(NzDropdownMenuComponent))
.componentInstance as NzDropdownMenuComponent;
menu.viewContainerRef.createEmbeddedView(menu.templateRef);
fixture.detectChanges();
return Array.from(fixture.nativeElement.querySelectorAll(".preset-dropdown-item"));
};

it("renders one dropdown row per preset, titled and described", () => {
initWith([testPreset, otherPreset]);

expect(component.searchResults).toEqual([testPreset, otherPreset]);
// the title cell renders the preset's value under the field's own key, and the
// description cell joins the remaining values
expect(component.getEntryTitle(testPreset)).toBe(testPreset[fieldKey]);
expect(component.getEntryDescription(testPreset)).toBe("otherPresetValue");
const rows = renderDropdownRows();

expect(rows).toHaveLength(2);
expect(rows[0].querySelector(".title")?.textContent).toBe(testPreset[fieldKey]);
expect(rows[0].querySelector(".description")?.textContent).toBe("otherPresetValue");
expect(rows[1].querySelector(".title")?.textContent).toBe(otherPreset[fieldKey]);
});

it("binds an empty dropdown list when there are no presets", () => {
it("renders no dropdown rows when there are no presets", () => {
initWith([]);

expect(component.searchResults).toEqual([]);
expect(renderDropdownRows()).toHaveLength(0);
});

it("applies the preset the row's (click) binding targets", () => {
it("applies the preset when its row is clicked", () => {
initWith([testPreset]);

component.applyPreset(testPreset);
renderDropdownRows()[0].querySelector<HTMLElement>(".dropdown-entry")!.click();

expect(presetServiceStub.applyPreset).toHaveBeenCalledWith(expect.anything(), expect.anything(), testPreset);
expect(presetServiceStub.applyPreset).toHaveBeenCalledWith(
presetKey.presetType,
presetKey.applyTarget,
testPreset
);
});

it("deletes the preset the delete button's (click) binding targets", () => {
it("deletes the preset from its row's delete button without applying it", () => {
initWith([testPreset]);

component.deletePreset(testPreset);
renderDropdownRows()[0].querySelector<HTMLElement>(".delete-button")!.click();

expect(presetServiceStub.deletePreset).toHaveBeenCalled();
// the button stops propagation so the surrounding row does not also apply it
expect(presetServiceStub.applyPreset).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@

import { of, Subject, throwError } from "rxjs";
import { OnDestroy } from "@angular/core";
import { NgxFileDropEntry } from "ngx-file-drop";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { NgxFileDropComponent, NgxFileDropEntry } from "ngx-file-drop";
import { NzAlertComponent } from "ng-zorro-antd/alert";
import { NzModalService } from "ng-zorro-antd/modal";
import { commonTestProviders } from "../../../../common/testing/test-utils";
import { AdminSettingsService } from "../../../service/admin/settings/admin-settings.service";
import { DatasetService } from "../../../service/user/dataset/dataset.service";
import { NotificationService } from "../../../../common/service/notification/notification.service";
Expand Down Expand Up @@ -502,3 +507,101 @@ describe("FilesUploaderComponent", () => {
});
});
});

/**
* The suite above constructs the component directly, so its template has never been
* rendered — the banner's `*ngIf`, the banner bindings and the drop-zone button live
* only in the template. These mount it for real.
*/
describe("FilesUploaderComponent rendered", () => {
let fixture: ComponentFixture<FilesUploaderComponent>;
let component: FilesUploaderComponent;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FilesUploaderComponent, NoopAnimationsModule],
providers: [
{ provide: NotificationService, useValue: { error: vi.fn() } },
{ provide: AdminSettingsService, useValue: { getPublicSetting: vi.fn().mockReturnValue(of("20")) } },
{
provide: DatasetService,
useValue: {
listMultipartUploads: vi.fn().mockReturnValue(of([])),
findExistingUploadFiles: vi.fn().mockReturnValue(of([])),
},
},
{ provide: NzModalService, useValue: { create: vi.fn() } },
...commonTestProviders,
],
}).compileComponents();

fixture = TestBed.createComponent(FilesUploaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

const alert = (): HTMLElement | null => (fixture.nativeElement as HTMLElement).querySelector("nz-alert");

it("hides the banner until the alert is enabled and the upload has finished", () => {
expect(alert()).toBeNull();

component.showUploadAlert = true;
fixture.detectChanges();
expect(alert()).toBeNull();

component.showUploadAlert = false;
component.fileUploadingFinished = true;
fixture.detectChanges();
expect(alert()).toBeNull();
});

it("renders the banner message once both flags are set", () => {
component.showUploadAlert = true;
component.fileUploadingFinished = true;
component.fileUploadBannerType = "error";
component.fileUploadBannerMessage = "Upload failed. Please retry.";
fixture.detectChanges();

const banner = alert();
expect(banner).not.toBeNull();
expect(banner!.textContent).toContain("Upload failed. Please retry.");
});

it("clears the banner when its close control fires", () => {
component.showUploadAlert = true;
component.fileUploadingFinished = true;
component.fileUploadBannerMessage = "done";
fixture.detectChanges();

fixture.debugElement.query(By.directive(NzAlertComponent)).componentInstance.nzOnClose.emit();
fixture.detectChanges();

expect(component.fileUploadingFinished).toBe(false);
expect(alert()).toBeNull();
});

it("opens the file selector from the drop-zone button", () => {
// ngx-file-drop hands its `openFileSelector` to the content template by reference,
// so spying on the component's property after render would not be seen. Assert its
// effect instead: it clicks the hidden file input.
const host = fixture.nativeElement as HTMLElement;
const fileInput: HTMLInputElement = host.querySelector("input.ngx-file-drop__file-input")!;
expect(fileInput).not.toBeNull();
const openDialog = vi.spyOn(fileInput, "click").mockImplementation(() => {});

const button: HTMLButtonElement = host.querySelector(".upload-file-button")!;
expect(button).not.toBeNull();
button.click();

expect(openDialog).toHaveBeenCalled();
});

it("routes a drop on the zone into fileDropped", () => {
const dropped = vi.spyOn(component, "fileDropped").mockImplementation(() => {});
const entries = [droppedFile("a.csv", new File(["a"], "a.csv"))];

fixture.debugElement.query(By.directive(NgxFileDropComponent)).componentInstance.onFileDrop.emit(entries);

expect(dropped).toHaveBeenCalledWith(entries);
});
});
Loading
Loading