Skip to content
Merged
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 @@ -30,6 +30,21 @@ import { SearchService } from "../../../service/user/search.service";
import { UserService } from "../../../../common/service/user/user.service";
import { SortMethod } from "../../../type/sort-method";
import { commonTestProviders } from "../../../../common/testing/test-utils";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { FormsModule } from "@angular/forms";
import { JWT_OPTIONS, JwtHelperService } from "@auth0/angular-jwt";
import { NzDropDownModule } from "ng-zorro-antd/dropdown";
import { NzModalModule } from "ng-zorro-antd/modal";
import { en_US, provideNzI18n } from "ng-zorro-antd/i18n";
import { By } from "@angular/platform-browser";
import { MOCK_USER_ID, StubUserService } from "../../../../common/service/user/stub-user.service";
import { OperatorMetadataService } from "src/app/workspace/service/operator-metadata/operator-metadata.service";
import { StubOperatorMetadataService } from "src/app/workspace/service/operator-metadata/stub-operator-metadata.service";
import { UserProjectService } from "../../../service/user/project/user-project.service";
import { StubUserProjectService } from "../../../service/user/project/stub-user-project.service";
import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service";
import { StubWorkflowPersistService } from "src/app/common/service/workflow-persist/stub-workflow-persist.service";
import { SortButtonComponent } from "../sort-button/sort-button.component";

// Lightweight stand-in for FiltersComponent. It registers itself under the real
// FiltersComponent token so SearchComponent's `@ViewChild(FiltersComponent)`
Expand Down Expand Up @@ -240,3 +255,119 @@ describe("SearchComponent", () => {
expect(component.sortMethod).toBe(SortMethod.EditTimeDesc);
});
});

// The suite above stubs the children out, and *any* `overrideComponent` makes
// Angular re-JIT SearchComponent from its retained decorator metadata; the
// recompiled template loses its source map back to search.component.html, so
// every binding still runs but none of it is attributed (issue #7458). This
// suite therefore stands up its own TestBed with the REAL children and asserts
// on the rendered DOM, leaving the stubbed tests above untouched.
describe("SearchComponent rendered template", () => {
let fixture: ComponentFixture<SearchComponent>;
let locationStub: { back: ReturnType<typeof vi.fn> };
let executeSearch: ReturnType<typeof vi.fn>;

const host = (): HTMLElement => fixture.nativeElement as HTMLElement;

/** The four resource-type buttons — direct children of .left-controls, which excludes the sort button. */
const typeButtons = (): HTMLButtonElement[] =>
Array.from(host().querySelectorAll<HTMLButtonElement>(".left-controls > button"));

const labels = (): string[] => typeButtons().map(button => button.textContent!.trim());

const selectedFlags = (): boolean[] => typeButtons().map(button => button.classList.contains("selected"));

/** The `type` argument of the most recent SearchService.executeSearch call. */
const lastSearchedType = (): unknown => executeSearch.mock.calls[executeSearch.mock.calls.length - 1][4];

beforeEach(async () => {
TestBed.resetTestingModule();
locationStub = { back: vi.fn() };
executeSearch = vi.fn(() => of({ entries: [], more: false }));

await TestBed.configureTestingModule({
imports: [SearchComponent, NzModalModule, NzDropDownModule, FormsModule, HttpClientTestingModule],
providers: [
JwtHelperService,
{ provide: JWT_OPTIONS, useValue: {} },
{ provide: SearchService, useValue: { executeSearch } },
{ provide: UserService, useClass: StubUserService },
{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService },
{ provide: UserProjectService, useClass: StubUserProjectService },
{ provide: WorkflowPersistService, useValue: new StubWorkflowPersistService([]) },
{ provide: ActivatedRoute, useValue: { queryParams: new Subject<Params>() } },
{ provide: Location, useValue: locationStub },
provideNzI18n(en_US),
...commonTestProviders,
],
}).compileComponents();

fixture = TestBed.createComponent(SearchComponent);
fixture.detectChanges();
});

it("renders the four resource-type buttons, each with its own label and icon", () => {
expect(labels()).toEqual(["All", "Project", "Workflow", "Dataset"]);
// nz-icon turns nzType into an `anticon-<type>` class, so this pins the icon
// each button asks for — and that "All" asks for none.
const icons = typeButtons().map(button => {
const icon = button.querySelector("span[nz-icon]");
return icon && Array.from(icon.classList).find(name => name.startsWith("anticon-"));
});
expect(icons).toEqual([null, "anticon-container", "anticon-project", "anticon-database"]);
});

it("highlights only the All button before a resource type is chosen", () => {
expect(selectedFlags()).toEqual([true, false, false, false]);
});

it("moves the highlight onto whichever resource-type button is clicked", () => {
// Click each typed button in turn; the highlight must be one-hot on that
// same button, which pins both its (click) argument and its [ngClass] test.
typeButtons().forEach((button, index) => {
button.click();
fixture.detectChanges();
expect(selectedFlags()).toEqual(labels().map((_, i) => i === index));
});
});

it("scopes the search to the clicked resource type", () => {
typeButtons()[3].click();
expect(lastSearchedType()).toBe("dataset");

// Back to All: the type filter is cleared rather than left on "dataset".
typeButtons()[0].click();
expect(lastSearchedType()).toBeNull();
});

it("re-runs the search with the sort method the sort button emits", () => {
const sortButton = fixture.debugElement.query(By.directive(SortButtonComponent))
.componentInstance as SortButtonComponent;

sortButton.dateSort();

// Kills both halves of `sortMethod = $event; search()`: drop the assignment
// and the search runs with the EditTimeDesc default; drop the call and
// executeSearch is never reached at all.
expect(executeSearch).toHaveBeenCalledTimes(1);
expect(executeSearch.mock.calls[0][5]).toBe(SortMethod.CreateTimeDesc);
});

it("renders a back arrow that returns to the previous page", () => {
const goBack = host().querySelector<HTMLButtonElement>(".go-back-button")!;
expect(goBack.querySelector("span.anticon-arrow-left")).not.toBeNull();

goBack.click();

expect(locationStub.back).toHaveBeenCalledTimes(1);
});

it("hands the signed-in user's uid to the results list", () => {
const results = fixture.debugElement.query(By.directive(SearchResultsComponent))
.componentInstance as SearchResultsComponent;
// Asserted on the child input rather than the DOM because currentUid only
// reaches the markup through texera-list-item, which is not rendered while
// the result list is empty.
expect(results.currentUid).toBe(MOCK_USER_ID);
});
});
Loading