aglinxinyuan commented on code in PR #6597:
URL: https://github.com/apache/texera/pull/6597#discussion_r3613701985
##########
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:
Fixed — the logged-out test now captures the prior stubUser.user and
restores it in a finally block, so it no longer leaks into later tests.
##########
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:
Fixed — afterEach now clears the overlay via the injected CDK
OverlayContainer (getContainerElement().innerHTML = '') instead of the raw
.cdk-overlay-container querySelectorAll sweep.
##########
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:
Good call on determinism — made it timezone-independent by asserting the
individual calendar fields. One correction to the premise: the component parses
via the LOCAL-time new Date(year, month-1, day), so I used the LOCAL getters
(getFullYear/getMonth/getDate) rather than getUTC* — those recover the intended
calendar day in every runner timezone (getUTC* would actually drift east of
UTC).
##########
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:
Fixed — NotificationService.error returns void, so both spies now use
mockImplementation(() => {}) instead of the undefined-as-never cast.
##########
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:
Fixed — kept the Nov/Dec (two-digit) test with an accurate name and added a
separate 'zero-pads single-digit months and days' test (month index 8 -> '09',
days 5/9 -> '05'/'09') that genuinely exercises the padding branch.
--
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]