Copilot commented on code in PR #6597:
URL: https://github.com/apache/texera/pull/6597#discussion_r3613267027
##########
frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts:
##########
@@ -84,4 +96,289 @@ describe("FiltersComponent", () => {
"another keyword",
]);
});
+
+ describe("backend setup for a logged-in user", () => {
+ it("populates owners from retrieveOwners on init", () => {
+ // StubWorkflowPersistService derives owners from the test workflow
entries (deduplicated).
+ expect(component.owners.map(o => o.userName)).toEqual(["Texera",
"Angular", "UCI"]);
+ expect(component.owners.every(o => !o.checked)).toBe(true);
+ });
+
+ it("populates workflow ids from retrieveWorkflowIDs on init", () => {
+ expect(component.wids.map(w => w.id)).toEqual(["1", "2", "3", "4", "5"]);
+ expect(component.wids.every(w => !w.checked)).toBe(true);
+ });
+
+ it("skips owner and id retrieval when the user is not logged in at init",
() => {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = undefined;
+ const loggedOutFixture = TestBed.createComponent(FiltersComponent);
+ const loggedOutComponent = loggedOutFixture.componentInstance;
+ loggedOutFixture.detectChanges();
+ expect(loggedOutComponent.isLogin).toBe(false);
+ expect(loggedOutComponent.owners).toEqual([]);
+ expect(loggedOutComponent.wids).toEqual([]);
+ // Operator metadata is not login-gated, so it is still loaded.
+ expect(loggedOutComponent.operatorGroups).toEqual(["Source", "Analysis",
"View Results"]);
+ loggedOutFixture.destroy();
+ });
Review Comment:
This test mutates the singleton `StubUserService` instance (`stubUser.user =
undefined`) and never restores it, which can leak state into later tests and
cause order-dependent failures. Consider capturing the previous value and
restoring it in a `finally` block (or resetting the stub in an
`afterEach`/helper), or use a dedicated TestBed configuration for the
logged-out case so other tests remain isolated.
##########
frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts:
##########
@@ -57,6 +64,11 @@ describe("FiltersComponent", () => {
fixture.detectChanges();
});
+ afterEach(() => {
+ fixture.destroy();
+ document.querySelectorAll(".cdk-overlay-container").forEach(c =>
(c.innerHTML = ""));
+ });
Review Comment:
Directly clearing `.cdk-overlay-container` via `document.querySelectorAll`
is a brittle global DOM mutation and can interfere with other tests (and it
bypasses Angular CDK's overlay lifecycle). Prefer using Angular CDK's
`OverlayContainer` in tests: inject it and clear
`overlayContainer.getContainerElement().innerHTML = \"\"`, and restore/destroy
the container via its provided lifecycle methods where possible.
##########
frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts:
##########
@@ -84,4 +96,289 @@ describe("FiltersComponent", () => {
"another keyword",
]);
});
+
+ describe("backend setup for a logged-in user", () => {
+ it("populates owners from retrieveOwners on init", () => {
+ // StubWorkflowPersistService derives owners from the test workflow
entries (deduplicated).
+ expect(component.owners.map(o => o.userName)).toEqual(["Texera",
"Angular", "UCI"]);
+ expect(component.owners.every(o => !o.checked)).toBe(true);
+ });
+
+ it("populates workflow ids from retrieveWorkflowIDs on init", () => {
+ expect(component.wids.map(w => w.id)).toEqual(["1", "2", "3", "4", "5"]);
+ expect(component.wids.every(w => !w.checked)).toBe(true);
+ });
+
+ it("skips owner and id retrieval when the user is not logged in at init",
() => {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = undefined;
+ const loggedOutFixture = TestBed.createComponent(FiltersComponent);
+ const loggedOutComponent = loggedOutFixture.componentInstance;
+ loggedOutFixture.detectChanges();
+ expect(loggedOutComponent.isLogin).toBe(false);
+ expect(loggedOutComponent.owners).toEqual([]);
+ expect(loggedOutComponent.wids).toEqual([]);
+ // Operator metadata is not login-gated, so it is still loaded.
+ expect(loggedOutComponent.operatorGroups).toEqual(["Source", "Analysis",
"View Results"]);
+ loggedOutFixture.destroy();
+ });
+ });
+
+ describe("user project setup", () => {
+ function emitUser(user: typeof MOCK_USER | undefined): void {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = user;
+ stubUser.userChangeSubject.next(user);
+ }
+
+ it("loads the project dropdown and color map when the user is logged in",
() => {
+ emitUser(MOCK_USER);
+ expect(component.userProjectsLoaded).toBe(true);
+ expect(component.userProjectsDropdown).toEqual(
+ testUserProjects.map(p => ({ pid: p.pid, name: p.name, checked: false
}))
+ );
+ expect(component.userProjectsMap.get(1)?.name).toBe("Project1");
+ expect(component.userProjectsMap.size).toBe(testUserProjects.length);
+ });
+
+ it("does not load projects when the user is logged out", () => {
+ emitUser(undefined);
+ expect(component.userProjectsLoaded).toBe(false);
+ expect(component.userProjectsDropdown).toEqual([]);
+ expect(component.userProjectsMap.size).toBe(0);
+ });
+ });
+
+ describe("dropdown checkbox handlers build the master filter list", () => {
+ function loadProjects(): void {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.userChangeSubject.next(MOCK_USER);
+ }
+
+ it("updateSelectedOwners emits an owner tag on masterFilterListChange", ()
=> {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.owners.find(o => o.userName === "Texera")!.checked = true;
+ component.updateSelectedOwners();
+ expect(component.selectedOwners).toEqual(["Texera"]);
+ expect(component.masterFilterList).toEqual(["owner: Texera"]);
+ expect(emissions).toContainEqual(["owner: Texera"]);
+ });
+
+ it("updateSelectedIDs emits an id tag on masterFilterListChange", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.wids.find(w => w.id === "2")!.checked = true;
+ component.updateSelectedIDs();
+ expect(component.selectedIDs).toEqual(["2"]);
+ expect(component.masterFilterList).toEqual(["id: 2"]);
+ expect(emissions).toContainEqual(["id: 2"]);
+ });
+
+ it("updateSelectedOperators emits an operator tag and records full
operator metadata", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.operators.get("Analysis")!.find(o => o.userFriendlyName ===
"Sentiment Analysis")!.checked = true;
+ component.updateSelectedOperators();
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ]);
+ expect(component.masterFilterList).toEqual(["operator: Sentiment
Analysis"]);
+ expect(emissions).toContainEqual(["operator: Sentiment Analysis"]);
+ });
+
+ it("updateSelectedProjects emits a project tag on masterFilterListChange",
() => {
+ loadProjects();
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.userProjectsDropdown.find(p => p.name === "Project1")!.checked
= true;
+ component.updateSelectedProjects();
+ expect(component.selectedProjects).toEqual([{ name: "Project1", pid: 1
}]);
+ expect(component.masterFilterList).toEqual(["project: Project1"]);
+ expect(emissions).toContainEqual(["project: Project1"]);
+ });
+ });
+
+ describe("updateDropdownMenus parses valid search tags", () => {
+ it("checks the matching owner and records it as selected", () => {
+ component.masterFilterList = ["owner: Texera"];
+ expect(component.selectedOwners).toEqual(["Texera"]);
+ expect(component.owners.find(o => o.userName ===
"Texera")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["owner: Texera"]);
+ });
+
+ it("checks the matching workflow id and records it as selected", () => {
+ component.masterFilterList = ["id: 3"];
+ expect(component.selectedIDs).toEqual(["3"]);
+ expect(component.wids.find(w => w.id === "3")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["id: 3"]);
+ });
+
+ it("checks the matching project and records it as selected", () => {
+ (TestBed.inject(UserService) as unknown as
StubUserService).userChangeSubject.next(MOCK_USER);
+ component.masterFilterList = ["project: Project2"];
+ expect(component.selectedProjects).toEqual([{ name: "Project2", pid: 2
}]);
+ expect(component.userProjectsDropdown.find(p => p.name ===
"Project2")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["project: Project2"]);
+ });
+
+ it("reconstructs a previously-selected operator and re-checks it in the
dropdown map", () => {
+ component.selectedOperators = [
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ];
+ component.updateDropdownMenus(["operator: Sentiment Analysis"]);
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ]);
+ expect(component.operators.get("Analysis")!.find(o => o.userFriendlyName
=== "Sentiment Analysis")!.checked).toBe(
+ true
+ );
+ });
+
+ it("preserves a selected operator whose group is absent from the metadata
map", () => {
+ component.selectedOperators = [
+ { userFriendlyName: "Phantom", operatorType: "PhantomOp",
operatorGroup: "NonexistentGroup" },
+ ];
+ component.updateDropdownMenus(["operator: Phantom"]);
+ // The operator is reconstructed even though operators.get(group) is
undefined (no dropdown entries to re-check).
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Phantom", operatorType: "PhantomOp",
operatorGroup: "NonexistentGroup" },
+ ]);
+ expect(component.operators.has("NonexistentGroup")).toBe(false);
+ });
+ });
+
+ describe("updateDropdownMenus rejects invalid search tags", () => {
+ let errorSpy: ReturnType<typeof vi.spyOn>;
+
+ beforeEach(() => {
+ const notificationService = TestBed.inject(NotificationService);
+ errorSpy = vi.spyOn(notificationService, "error").mockImplementation(()
=> undefined as never);
+ });
+
+ it("reports an invalid owner name and removes the tag", () => {
+ component.masterFilterList = ["owner: Nobody"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid owner name");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedOwners).toEqual([]);
+ });
+
+ it("reports an invalid workflow id and removes the tag", () => {
+ component.masterFilterList = ["id: 999"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid workflow id");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedIDs).toEqual([]);
+ });
+
+ it("reports an invalid operator name and removes the tag", () => {
+ component.masterFilterList = ["operator: Ghost Operator"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid operator name");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedOperators).toEqual([]);
+ });
+
+ it("reports an invalid project name and removes the tag", () => {
+ component.masterFilterList = ["project: Missing Project"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid project name");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedProjects).toEqual([]);
+ });
+ });
+
+ describe("updateDropdownMenus guards date tags", () => {
+ let errorSpy: ReturnType<typeof vi.spyOn>;
+
+ beforeEach(() => {
+ const notificationService = TestBed.inject(NotificationService);
+ errorSpy = vi.spyOn(notificationService, "error").mockImplementation(()
=> undefined as never);
+ });
+
+ it("rejects a malformed ctime tag", () => {
+ component.masterFilterList = ["ctime: not-a-date"];
+ expect(errorSpy).toHaveBeenCalledWith("Date format is incorrect");
+ expect(component.selectedCtime).toEqual([]);
+ });
+
+ it("rejects a malformed mtime tag", () => {
+ component.masterFilterList = ["mtime: 22-01-2022"];
+ expect(errorSpy).toHaveBeenCalledWith("Date format is incorrect");
+ expect(component.selectedMtime).toEqual([]);
+ });
+
+ it("keeps only the first ctime tag when multiple are supplied", () => {
+ component.masterFilterList = ["ctime: 2022-01-22 ~ 2022-04-21", "ctime:
2023-01-01 ~ 2023-02-02"];
+ expect(errorSpy).toHaveBeenCalledWith("Multiple search dates is not
allowed");
+ expect(component.selectedCtime).toEqual([new Date(2022, 0, 22), new
Date(2022, 3, 21)]);
+ expect(component.masterFilterList).toEqual(["ctime: 2022-01-22 ~
2022-04-21"]);
+ });
+
+ it("keeps only the first mtime tag when multiple are supplied", () => {
+ component.masterFilterList = ["mtime: 2022-01-22 ~ 2022-04-21", "mtime:
2023-01-01 ~ 2023-02-02"];
+ expect(errorSpy).toHaveBeenCalledWith("Multiple search dates is not
allowed");
+ expect(component.selectedMtime).toEqual([new Date(2022, 0, 22), new
Date(2022, 3, 21)]);
+ expect(component.masterFilterList).toEqual(["mtime: 2022-01-22 ~
2022-04-21"]);
+ });
Review Comment:
These assertions can be timezone-sensitive if the component parses
`YYYY-MM-DD` strings using UTC semantics (common with `Date.parse` / ISO date
parsing), while `new Date(2022, 0, 22)` is local-time midnight. This can make
the test fail depending on the environment timezone. To make the tests
deterministic, assert on year/month/day parts (e.g.,
`getUTCFullYear()`/`getUTCMonth()`/`getUTCDate()`), or build expected dates
using `Date.UTC(...)` to match parsing behavior.
##########
frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts:
##########
@@ -84,4 +96,289 @@ describe("FiltersComponent", () => {
"another keyword",
]);
});
+
+ describe("backend setup for a logged-in user", () => {
+ it("populates owners from retrieveOwners on init", () => {
+ // StubWorkflowPersistService derives owners from the test workflow
entries (deduplicated).
+ expect(component.owners.map(o => o.userName)).toEqual(["Texera",
"Angular", "UCI"]);
+ expect(component.owners.every(o => !o.checked)).toBe(true);
+ });
+
+ it("populates workflow ids from retrieveWorkflowIDs on init", () => {
+ expect(component.wids.map(w => w.id)).toEqual(["1", "2", "3", "4", "5"]);
+ expect(component.wids.every(w => !w.checked)).toBe(true);
+ });
+
+ it("skips owner and id retrieval when the user is not logged in at init",
() => {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = undefined;
+ const loggedOutFixture = TestBed.createComponent(FiltersComponent);
+ const loggedOutComponent = loggedOutFixture.componentInstance;
+ loggedOutFixture.detectChanges();
+ expect(loggedOutComponent.isLogin).toBe(false);
+ expect(loggedOutComponent.owners).toEqual([]);
+ expect(loggedOutComponent.wids).toEqual([]);
+ // Operator metadata is not login-gated, so it is still loaded.
+ expect(loggedOutComponent.operatorGroups).toEqual(["Source", "Analysis",
"View Results"]);
+ loggedOutFixture.destroy();
+ });
+ });
+
+ describe("user project setup", () => {
+ function emitUser(user: typeof MOCK_USER | undefined): void {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = user;
+ stubUser.userChangeSubject.next(user);
+ }
+
+ it("loads the project dropdown and color map when the user is logged in",
() => {
+ emitUser(MOCK_USER);
+ expect(component.userProjectsLoaded).toBe(true);
+ expect(component.userProjectsDropdown).toEqual(
+ testUserProjects.map(p => ({ pid: p.pid, name: p.name, checked: false
}))
+ );
+ expect(component.userProjectsMap.get(1)?.name).toBe("Project1");
+ expect(component.userProjectsMap.size).toBe(testUserProjects.length);
+ });
+
+ it("does not load projects when the user is logged out", () => {
+ emitUser(undefined);
+ expect(component.userProjectsLoaded).toBe(false);
+ expect(component.userProjectsDropdown).toEqual([]);
+ expect(component.userProjectsMap.size).toBe(0);
+ });
+ });
+
+ describe("dropdown checkbox handlers build the master filter list", () => {
+ function loadProjects(): void {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.userChangeSubject.next(MOCK_USER);
+ }
+
+ it("updateSelectedOwners emits an owner tag on masterFilterListChange", ()
=> {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.owners.find(o => o.userName === "Texera")!.checked = true;
+ component.updateSelectedOwners();
+ expect(component.selectedOwners).toEqual(["Texera"]);
+ expect(component.masterFilterList).toEqual(["owner: Texera"]);
+ expect(emissions).toContainEqual(["owner: Texera"]);
+ });
+
+ it("updateSelectedIDs emits an id tag on masterFilterListChange", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.wids.find(w => w.id === "2")!.checked = true;
+ component.updateSelectedIDs();
+ expect(component.selectedIDs).toEqual(["2"]);
+ expect(component.masterFilterList).toEqual(["id: 2"]);
+ expect(emissions).toContainEqual(["id: 2"]);
+ });
+
+ it("updateSelectedOperators emits an operator tag and records full
operator metadata", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.operators.get("Analysis")!.find(o => o.userFriendlyName ===
"Sentiment Analysis")!.checked = true;
+ component.updateSelectedOperators();
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ]);
+ expect(component.masterFilterList).toEqual(["operator: Sentiment
Analysis"]);
+ expect(emissions).toContainEqual(["operator: Sentiment Analysis"]);
+ });
+
+ it("updateSelectedProjects emits a project tag on masterFilterListChange",
() => {
+ loadProjects();
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.userProjectsDropdown.find(p => p.name === "Project1")!.checked
= true;
+ component.updateSelectedProjects();
+ expect(component.selectedProjects).toEqual([{ name: "Project1", pid: 1
}]);
+ expect(component.masterFilterList).toEqual(["project: Project1"]);
+ expect(emissions).toContainEqual(["project: Project1"]);
+ });
+ });
+
+ describe("updateDropdownMenus parses valid search tags", () => {
+ it("checks the matching owner and records it as selected", () => {
+ component.masterFilterList = ["owner: Texera"];
+ expect(component.selectedOwners).toEqual(["Texera"]);
+ expect(component.owners.find(o => o.userName ===
"Texera")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["owner: Texera"]);
+ });
+
+ it("checks the matching workflow id and records it as selected", () => {
+ component.masterFilterList = ["id: 3"];
+ expect(component.selectedIDs).toEqual(["3"]);
+ expect(component.wids.find(w => w.id === "3")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["id: 3"]);
+ });
+
+ it("checks the matching project and records it as selected", () => {
+ (TestBed.inject(UserService) as unknown as
StubUserService).userChangeSubject.next(MOCK_USER);
+ component.masterFilterList = ["project: Project2"];
+ expect(component.selectedProjects).toEqual([{ name: "Project2", pid: 2
}]);
+ expect(component.userProjectsDropdown.find(p => p.name ===
"Project2")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["project: Project2"]);
+ });
+
+ it("reconstructs a previously-selected operator and re-checks it in the
dropdown map", () => {
+ component.selectedOperators = [
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ];
+ component.updateDropdownMenus(["operator: Sentiment Analysis"]);
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ]);
+ expect(component.operators.get("Analysis")!.find(o => o.userFriendlyName
=== "Sentiment Analysis")!.checked).toBe(
+ true
+ );
+ });
+
+ it("preserves a selected operator whose group is absent from the metadata
map", () => {
+ component.selectedOperators = [
+ { userFriendlyName: "Phantom", operatorType: "PhantomOp",
operatorGroup: "NonexistentGroup" },
+ ];
+ component.updateDropdownMenus(["operator: Phantom"]);
+ // The operator is reconstructed even though operators.get(group) is
undefined (no dropdown entries to re-check).
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Phantom", operatorType: "PhantomOp",
operatorGroup: "NonexistentGroup" },
+ ]);
+ expect(component.operators.has("NonexistentGroup")).toBe(false);
+ });
+ });
+
+ describe("updateDropdownMenus rejects invalid search tags", () => {
+ let errorSpy: ReturnType<typeof vi.spyOn>;
+
+ beforeEach(() => {
+ const notificationService = TestBed.inject(NotificationService);
+ errorSpy = vi.spyOn(notificationService, "error").mockImplementation(()
=> undefined as never);
+ });
Review Comment:
`mockImplementation(() => undefined as never)` is an unsafe type cast and
obscures the actual contract. If `NotificationService.error` returns `void`,
prefer `mockImplementation(() => {})`; if it returns something, prefer
`mockReturnValue(...)` with the correct type. This keeps the test typed and
easier to maintain.
##########
frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts:
##########
@@ -84,4 +96,289 @@ describe("FiltersComponent", () => {
"another keyword",
]);
});
+
+ describe("backend setup for a logged-in user", () => {
+ it("populates owners from retrieveOwners on init", () => {
+ // StubWorkflowPersistService derives owners from the test workflow
entries (deduplicated).
+ expect(component.owners.map(o => o.userName)).toEqual(["Texera",
"Angular", "UCI"]);
+ expect(component.owners.every(o => !o.checked)).toBe(true);
+ });
+
+ it("populates workflow ids from retrieveWorkflowIDs on init", () => {
+ expect(component.wids.map(w => w.id)).toEqual(["1", "2", "3", "4", "5"]);
+ expect(component.wids.every(w => !w.checked)).toBe(true);
+ });
+
+ it("skips owner and id retrieval when the user is not logged in at init",
() => {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = undefined;
+ const loggedOutFixture = TestBed.createComponent(FiltersComponent);
+ const loggedOutComponent = loggedOutFixture.componentInstance;
+ loggedOutFixture.detectChanges();
+ expect(loggedOutComponent.isLogin).toBe(false);
+ expect(loggedOutComponent.owners).toEqual([]);
+ expect(loggedOutComponent.wids).toEqual([]);
+ // Operator metadata is not login-gated, so it is still loaded.
+ expect(loggedOutComponent.operatorGroups).toEqual(["Source", "Analysis",
"View Results"]);
+ loggedOutFixture.destroy();
+ });
+ });
+
+ describe("user project setup", () => {
+ function emitUser(user: typeof MOCK_USER | undefined): void {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.user = user;
+ stubUser.userChangeSubject.next(user);
+ }
+
+ it("loads the project dropdown and color map when the user is logged in",
() => {
+ emitUser(MOCK_USER);
+ expect(component.userProjectsLoaded).toBe(true);
+ expect(component.userProjectsDropdown).toEqual(
+ testUserProjects.map(p => ({ pid: p.pid, name: p.name, checked: false
}))
+ );
+ expect(component.userProjectsMap.get(1)?.name).toBe("Project1");
+ expect(component.userProjectsMap.size).toBe(testUserProjects.length);
+ });
+
+ it("does not load projects when the user is logged out", () => {
+ emitUser(undefined);
+ expect(component.userProjectsLoaded).toBe(false);
+ expect(component.userProjectsDropdown).toEqual([]);
+ expect(component.userProjectsMap.size).toBe(0);
+ });
+ });
+
+ describe("dropdown checkbox handlers build the master filter list", () => {
+ function loadProjects(): void {
+ const stubUser = TestBed.inject(UserService) as unknown as
StubUserService;
+ stubUser.userChangeSubject.next(MOCK_USER);
+ }
+
+ it("updateSelectedOwners emits an owner tag on masterFilterListChange", ()
=> {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.owners.find(o => o.userName === "Texera")!.checked = true;
+ component.updateSelectedOwners();
+ expect(component.selectedOwners).toEqual(["Texera"]);
+ expect(component.masterFilterList).toEqual(["owner: Texera"]);
+ expect(emissions).toContainEqual(["owner: Texera"]);
+ });
+
+ it("updateSelectedIDs emits an id tag on masterFilterListChange", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.wids.find(w => w.id === "2")!.checked = true;
+ component.updateSelectedIDs();
+ expect(component.selectedIDs).toEqual(["2"]);
+ expect(component.masterFilterList).toEqual(["id: 2"]);
+ expect(emissions).toContainEqual(["id: 2"]);
+ });
+
+ it("updateSelectedOperators emits an operator tag and records full
operator metadata", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.operators.get("Analysis")!.find(o => o.userFriendlyName ===
"Sentiment Analysis")!.checked = true;
+ component.updateSelectedOperators();
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ]);
+ expect(component.masterFilterList).toEqual(["operator: Sentiment
Analysis"]);
+ expect(emissions).toContainEqual(["operator: Sentiment Analysis"]);
+ });
+
+ it("updateSelectedProjects emits a project tag on masterFilterListChange",
() => {
+ loadProjects();
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.userProjectsDropdown.find(p => p.name === "Project1")!.checked
= true;
+ component.updateSelectedProjects();
+ expect(component.selectedProjects).toEqual([{ name: "Project1", pid: 1
}]);
+ expect(component.masterFilterList).toEqual(["project: Project1"]);
+ expect(emissions).toContainEqual(["project: Project1"]);
+ });
+ });
+
+ describe("updateDropdownMenus parses valid search tags", () => {
+ it("checks the matching owner and records it as selected", () => {
+ component.masterFilterList = ["owner: Texera"];
+ expect(component.selectedOwners).toEqual(["Texera"]);
+ expect(component.owners.find(o => o.userName ===
"Texera")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["owner: Texera"]);
+ });
+
+ it("checks the matching workflow id and records it as selected", () => {
+ component.masterFilterList = ["id: 3"];
+ expect(component.selectedIDs).toEqual(["3"]);
+ expect(component.wids.find(w => w.id === "3")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["id: 3"]);
+ });
+
+ it("checks the matching project and records it as selected", () => {
+ (TestBed.inject(UserService) as unknown as
StubUserService).userChangeSubject.next(MOCK_USER);
+ component.masterFilterList = ["project: Project2"];
+ expect(component.selectedProjects).toEqual([{ name: "Project2", pid: 2
}]);
+ expect(component.userProjectsDropdown.find(p => p.name ===
"Project2")!.checked).toBe(true);
+ expect(component.masterFilterList).toEqual(["project: Project2"]);
+ });
+
+ it("reconstructs a previously-selected operator and re-checks it in the
dropdown map", () => {
+ component.selectedOperators = [
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ];
+ component.updateDropdownMenus(["operator: Sentiment Analysis"]);
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Sentiment Analysis", operatorType:
"NlpSentiment", operatorGroup: "Analysis" },
+ ]);
+ expect(component.operators.get("Analysis")!.find(o => o.userFriendlyName
=== "Sentiment Analysis")!.checked).toBe(
+ true
+ );
+ });
+
+ it("preserves a selected operator whose group is absent from the metadata
map", () => {
+ component.selectedOperators = [
+ { userFriendlyName: "Phantom", operatorType: "PhantomOp",
operatorGroup: "NonexistentGroup" },
+ ];
+ component.updateDropdownMenus(["operator: Phantom"]);
+ // The operator is reconstructed even though operators.get(group) is
undefined (no dropdown entries to re-check).
+ expect(component.selectedOperators).toEqual([
+ { userFriendlyName: "Phantom", operatorType: "PhantomOp",
operatorGroup: "NonexistentGroup" },
+ ]);
+ expect(component.operators.has("NonexistentGroup")).toBe(false);
+ });
+ });
+
+ describe("updateDropdownMenus rejects invalid search tags", () => {
+ let errorSpy: ReturnType<typeof vi.spyOn>;
+
+ beforeEach(() => {
+ const notificationService = TestBed.inject(NotificationService);
+ errorSpy = vi.spyOn(notificationService, "error").mockImplementation(()
=> undefined as never);
+ });
+
+ it("reports an invalid owner name and removes the tag", () => {
+ component.masterFilterList = ["owner: Nobody"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid owner name");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedOwners).toEqual([]);
+ });
+
+ it("reports an invalid workflow id and removes the tag", () => {
+ component.masterFilterList = ["id: 999"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid workflow id");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedIDs).toEqual([]);
+ });
+
+ it("reports an invalid operator name and removes the tag", () => {
+ component.masterFilterList = ["operator: Ghost Operator"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid operator name");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedOperators).toEqual([]);
+ });
+
+ it("reports an invalid project name and removes the tag", () => {
+ component.masterFilterList = ["project: Missing Project"];
+ expect(errorSpy).toHaveBeenCalledWith("Invalid project name");
+ expect(component.masterFilterList).toEqual([]);
+ expect(component.selectedProjects).toEqual([]);
+ });
+ });
+
+ describe("updateDropdownMenus guards date tags", () => {
+ let errorSpy: ReturnType<typeof vi.spyOn>;
+
+ beforeEach(() => {
+ const notificationService = TestBed.inject(NotificationService);
+ errorSpy = vi.spyOn(notificationService, "error").mockImplementation(()
=> undefined as never);
+ });
+
+ it("rejects a malformed ctime tag", () => {
+ component.masterFilterList = ["ctime: not-a-date"];
+ expect(errorSpy).toHaveBeenCalledWith("Date format is incorrect");
+ expect(component.selectedCtime).toEqual([]);
+ });
+
+ it("rejects a malformed mtime tag", () => {
+ component.masterFilterList = ["mtime: 22-01-2022"];
+ expect(errorSpy).toHaveBeenCalledWith("Date format is incorrect");
+ expect(component.selectedMtime).toEqual([]);
+ });
+
+ it("keeps only the first ctime tag when multiple are supplied", () => {
+ component.masterFilterList = ["ctime: 2022-01-22 ~ 2022-04-21", "ctime:
2023-01-01 ~ 2023-02-02"];
+ expect(errorSpy).toHaveBeenCalledWith("Multiple search dates is not
allowed");
+ expect(component.selectedCtime).toEqual([new Date(2022, 0, 22), new
Date(2022, 3, 21)]);
+ expect(component.masterFilterList).toEqual(["ctime: 2022-01-22 ~
2022-04-21"]);
+ });
+
+ it("keeps only the first mtime tag when multiple are supplied", () => {
+ component.masterFilterList = ["mtime: 2022-01-22 ~ 2022-04-21", "mtime:
2023-01-01 ~ 2023-02-02"];
+ expect(errorSpy).toHaveBeenCalledWith("Multiple search dates is not
allowed");
+ expect(component.selectedMtime).toEqual([new Date(2022, 0, 22), new
Date(2022, 3, 21)]);
+ expect(component.masterFilterList).toEqual(["mtime: 2022-01-22 ~
2022-04-21"]);
+ });
+ });
+
+ describe("buildMasterFilterList date handling", () => {
+ it("appends a ctime tag when none is present in the list", () => {
+ const emissions: ReadonlyArray<string>[] = [];
+ component.masterFilterListChange.subscribe(v => emissions.push([...v]));
+ component.selectedCtime = [new Date(2022, 0, 1), new Date(2022, 0, 31)];
+ component.buildMasterFilterList();
+ expect(component.masterFilterList).toEqual(["ctime: 2022-01-01 ~
2022-01-31"]);
+ expect(emissions).toContainEqual(["ctime: 2022-01-01 ~ 2022-01-31"]);
+ });
+
+ it("formats two-digit months without zero padding", () => {
+ component.selectedMtime = [new Date(2022, 10, 15), new Date(2022, 11,
20)];
+ component.buildMasterFilterList();
+ expect(component.masterFilterList).toEqual(["mtime: 2022-11-15 ~
2022-12-20"]);
+ });
Review Comment:
The test description is misleading: months 11 and 12 are already two-digit
and don't demonstrate zero-padding behavior. Consider renaming this test to
reflect the actual assertion (e.g., that month formatting is correct for
Nov/Dec), or change the inputs/assertion to cover a single-digit month case if
the goal is to verify padding behavior.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]