You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/18 23:51:04 UTC
(texera) branch gh-readonly-queue/main/pr-6526-8d7664d288ddb5bfd7816a1dc6eb8d2df893a7c3 created (now 5762b8b380)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-6526-8d7664d288ddb5bfd7816a1dc6eb8d2df893a7c3
in repository https://gitbox.apache.org/repos/asf/texera.git
at 5762b8b380 test(frontend): extend AdminUserComponent unit test coverage (#6526)
This branch includes the following new commits:
new 5762b8b380 test(frontend): extend AdminUserComponent unit test coverage (#6526)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(frontend): extend AdminUserComponent unit test coverage (#6526)
Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6526-8d7664d288ddb5bfd7816a1dc6eb8d2df893a7c3
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 5762b8b380b5c95f2b34f116253605c02572cd3a
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sat Jul 18 16:50:44 2026 -0700
test(frontend): extend AdminUserComponent unit test coverage (#6526)
### What changes were proposed in this PR?
Extends the existing Vitest spec for `AdminUserComponent`
(`frontend/src/app/dashboard/component/admin/user/admin-user.component.ts`,
codecov ~42%). The existing spec only covered creation and the
email/comment
search helpers; this adds 13 tests for the previously-untested methods.
The shared `TestBed` setup now mocks `AdminUserService` and
`FeedbackService`
(instead of the real services over `HttpClientTestingModule`), which
keeps
`ngOnInit` deterministic and lets the new tests assert service
interactions
directly. The eight existing tests are unchanged and still pass.
New behavior covered:
- **`addUser`** — calls the service and reloads the list (re-runs
`ngOnInit`).
- **inline-edit lifecycle** — `startEdit` loads a row's values;
`saveEdit`
persists a change (and updates the local lists), no-ops when nothing
changed,
and surfaces a service error via the message service; `stopEdit` clears
the
edit target; `reset` clears search state and restores the display list.
- **`updateRole`** — edits only the role and persists it.
- **display helpers** — `isUserActive` (recent-login window) and
`getAccountCreation` (seconds → ms, default 0).
- **feedback / quota** — `loadFeedbackCounts` + `getFeedbackCount`
caching;
`clickToViewFeedbacks` and `clickToViewQuota` open the correct modal
content.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6518
### How was this PR tested?
Extended unit tests, run locally in `frontend/` (all green; the failure
path was
verified by breaking an assertion to confirm the suite goes red):
```
ng test --watch=false --include src/app/dashboard/component/admin/user/admin-user.component.spec.ts
# Tests 21 passed (21) (8 existing + 13 new)
eslint <spec> # clean
prettier --check # clean
```
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---
.../admin/user/admin-user.component.spec.ts | 231 ++++++++++++++++++++-
1 file changed, 229 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/dashboard/component/admin/user/admin-user.component.spec.ts b/frontend/src/app/dashboard/component/admin/user/admin-user.component.spec.ts
index f3568bf254..e01dc9112f 100644
--- a/frontend/src/app/dashboard/component/admin/user/admin-user.component.spec.ts
+++ b/frontend/src/app/dashboard/component/admin/user/admin-user.component.spec.ts
@@ -18,23 +18,70 @@
*/
import { ComponentFixture, inject, TestBed } from "@angular/core/testing";
+import { of, throwError } from "rxjs";
import { AdminUserComponent } from "./admin-user.component";
import { UserService } from "../../../../common/service/user/user.service";
import { StubUserService } from "../../../../common/service/user/stub-user.service";
import { AdminUserService } from "../../../service/admin/user/admin-user.service";
+import { FeedbackService } from "../../../service/user/feedback/feedback.service";
+import { Role, User } from "../../../../common/type/user";
+import { GuiConfigService } from "../../../../common/service/gui-config.service";
+import { UserQuotaComponent } from "../../user/user-quota/user-quota.component";
+import { FeedbackComponent } from "../../user/feedback/feedback.component";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { FormsModule } from "@angular/forms";
import { NzDropDownModule } from "ng-zorro-antd/dropdown";
-import { NzModalModule } from "ng-zorro-antd/modal";
+import { NzModalModule, NzModalService } from "ng-zorro-antd/modal";
+import { NzMessageService } from "ng-zorro-antd/message";
import { commonTestProviders } from "../../../../common/testing/test-utils";
+const userA: User = {
+ uid: 1,
+ name: "Alice",
+ email: "alice@example.com",
+ role: Role.REGULAR,
+ comment: "c1",
+ joiningReason: "r1",
+ accountCreation: 1000,
+ lastLogin: 2000,
+};
+
+const userB: User = {
+ uid: 2,
+ name: "Bob",
+ email: "bob@example.com",
+ role: Role.ADMIN,
+ comment: "c2",
+ joiningReason: "r2",
+ accountCreation: 3000,
+};
+
describe("AdminUserComponent", () => {
let component: AdminUserComponent;
let fixture: ComponentFixture<AdminUserComponent>;
+ let adminUserServiceSpy: {
+ getUserList: ReturnType<typeof vi.fn>;
+ addUser: ReturnType<typeof vi.fn>;
+ updateUser: ReturnType<typeof vi.fn>;
+ };
+ let feedbackServiceSpy: { getFeedbackCounts: ReturnType<typeof vi.fn> };
+
beforeEach(async () => {
+ adminUserServiceSpy = {
+ getUserList: vi.fn().mockReturnValue(of([])),
+ addUser: vi.fn().mockReturnValue(of(undefined)),
+ updateUser: vi.fn().mockReturnValue(of(undefined)),
+ };
+ feedbackServiceSpy = { getFeedbackCounts: vi.fn().mockReturnValue(of([])) };
+
await TestBed.configureTestingModule({
- providers: [{ provide: UserService, useClass: StubUserService }, AdminUserService, ...commonTestProviders],
+ providers: [
+ { provide: UserService, useClass: StubUserService },
+ { provide: AdminUserService, useValue: adminUserServiceSpy },
+ { provide: FeedbackService, useValue: feedbackServiceSpy },
+ ...commonTestProviders,
+ ],
imports: [AdminUserComponent, FormsModule, HttpClientTestingModule, NzDropDownModule, NzModalModule],
}).compileComponents();
});
@@ -45,6 +92,10 @@ describe("AdminUserComponent", () => {
fixture.detectChanges();
});
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
it("should create", inject([HttpTestingController], () => {
expect(component).toBeTruthy();
}));
@@ -157,4 +208,180 @@ describe("AdminUserComponent", () => {
expect(component.nameSearchValue).toBe("");
expect(component.emailSearchValue).toBe("");
});
+
+ it("addUser creates a user through the service and reloads the list", () => {
+ const listCallsBefore = adminUserServiceSpy.getUserList.mock.calls.length;
+
+ component.addUser();
+
+ expect(adminUserServiceSpy.addUser).toHaveBeenCalledTimes(1);
+ // the subscribe callback re-runs ngOnInit, which re-fetches the user list
+ expect(adminUserServiceSpy.getUserList.mock.calls.length).toBe(listCallsBefore + 1);
+ });
+
+ it("startEdit loads the target row's values into the edit fields", () => {
+ component.startEdit(userB, "email");
+
+ expect(component.editUid).toBe(userB.uid);
+ expect(component.editAttribute).toBe("email");
+ expect(component.editName).toBe(userB.name);
+ expect(component.editEmail).toBe(userB.email);
+ expect(component.editRole).toBe(userB.role);
+ expect(component.editComment).toBe(userB.comment);
+ });
+
+ it("saveEdit persists a changed row and updates the local lists", () => {
+ component.userList = [userA];
+ component.listOfDisplayUser = [userA];
+ component.editUid = userA.uid;
+ component.editName = "Alice Updated";
+ component.editEmail = userA.email;
+ component.editRole = userA.role;
+ component.editComment = userA.comment;
+
+ component.saveEdit();
+
+ expect(adminUserServiceSpy.updateUser).toHaveBeenCalledWith(
+ userA.uid,
+ "Alice Updated",
+ userA.email,
+ userA.role,
+ userA.comment
+ );
+ expect(component.userList.find(u => u.uid === userA.uid)?.name).toBe("Alice Updated");
+ expect(component.listOfDisplayUser.find(u => u.uid === userA.uid)?.name).toBe("Alice Updated");
+ expect(component.editUid).toBe(0); // stopEdit ran
+ });
+
+ it("saveEdit is a no-op when nothing changed", () => {
+ component.userList = [userA];
+ component.editUid = userA.uid;
+ component.editName = userA.name;
+ component.editEmail = userA.email;
+ component.editRole = userA.role;
+ component.editComment = userA.comment;
+
+ component.saveEdit();
+
+ expect(adminUserServiceSpy.updateUser).not.toHaveBeenCalled();
+ expect(component.editUid).toBe(0);
+ });
+
+ it("saveEdit surfaces a service error through the message service", () => {
+ const errorSpy = vi.spyOn(TestBed.inject(NzMessageService), "error").mockReturnValue({} as any);
+ adminUserServiceSpy.updateUser.mockReturnValue(throwError(() => ({ error: { message: "update failed" } })));
+
+ component.userList = [userA];
+ component.editUid = userA.uid;
+ component.editName = "Changed";
+ component.editEmail = userA.email;
+ component.editRole = userA.role;
+ component.editComment = userA.comment;
+
+ component.saveEdit();
+
+ expect(errorSpy).toHaveBeenCalledWith("update failed");
+ });
+
+ it("stopEdit clears the edit target", () => {
+ component.editUid = 5;
+ component.editAttribute = "name";
+
+ component.stopEdit();
+
+ expect(component.editUid).toBe(0);
+ expect(component.editAttribute).toBe("");
+ });
+
+ it("reset clears search state and restores the full display list", () => {
+ component.userList = [userA, userB];
+ component.nameSearchValue = "Alice";
+ component.emailSearchValue = "alice@example.com";
+ component.commentSearchValue = "c";
+ component.nameSearchVisible = true;
+ component.emailSearchVisible = true;
+ component.commentSearchVisible = true;
+ component.listOfDisplayUser = [userA];
+
+ component.reset();
+
+ expect(component.nameSearchValue).toBe("");
+ expect(component.emailSearchValue).toBe("");
+ expect(component.commentSearchValue).toBe("");
+ expect(component.nameSearchVisible).toBe(false);
+ expect(component.emailSearchVisible).toBe(false);
+ expect(component.commentSearchVisible).toBe(false);
+ expect(component.listOfDisplayUser).toEqual([userA, userB]);
+ });
+
+ it("updateRole edits only the role and persists it", () => {
+ component.userList = [userA];
+ component.listOfDisplayUser = [userA];
+
+ component.updateRole(userA, Role.ADMIN);
+
+ expect(adminUserServiceSpy.updateUser).toHaveBeenCalledWith(
+ userA.uid,
+ userA.name,
+ userA.email,
+ Role.ADMIN,
+ userA.comment
+ );
+ expect(component.userList.find(u => u.uid === userA.uid)?.role).toBe(Role.ADMIN);
+ });
+
+ it("isUserActive reflects the recent-login window", () => {
+ // Derive the boundary from the configured window so the test stays correct if the default changes.
+ const windowSeconds = TestBed.inject(GuiConfigService).env.activeTimeInMinutes * 60;
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ // just inside the active window
+ expect(component.isUserActive({ ...userA, lastLogin: nowSeconds - Math.floor(windowSeconds / 2) } as User)).toBe(
+ true
+ );
+ // just outside the active window
+ expect(component.isUserActive({ ...userA, lastLogin: nowSeconds - (windowSeconds + 60) } as User)).toBe(false);
+ expect(component.isUserActive({ ...userA, lastLogin: undefined } as User)).toBe(false);
+ });
+
+ it("getAccountCreation converts seconds to milliseconds and defaults to 0", () => {
+ expect(component.getAccountCreation({ ...userA, accountCreation: 5 } as User)).toBe(5000);
+ expect(component.getAccountCreation({ ...userA, accountCreation: undefined } as User)).toBe(0);
+ });
+
+ it("loadFeedbackCounts caches counts read back by getFeedbackCount", () => {
+ feedbackServiceSpy.getFeedbackCounts.mockReturnValue(
+ of([
+ { uid: 1, count: 3 },
+ { uid: 2, count: 0 },
+ ])
+ );
+
+ component.loadFeedbackCounts();
+
+ expect(component.getFeedbackCount(1)).toBe(3);
+ expect(component.getFeedbackCount(2)).toBe(0);
+ expect(component.getFeedbackCount(999)).toBe(0); // absent uid defaults to 0
+ });
+
+ it("clickToViewFeedbacks opens the feedback modal for the given uid", () => {
+ const createSpy = vi.spyOn(TestBed.inject(NzModalService), "create").mockReturnValue({} as any);
+
+ component.clickToViewFeedbacks(7);
+
+ expect(createSpy).toHaveBeenCalledTimes(1);
+ const config = createSpy.mock.calls[0][0];
+ expect(config.nzContent).toBe(FeedbackComponent);
+ expect(config.nzData).toEqual({ uid: 7 });
+ });
+
+ it("clickToViewQuota opens the quota modal for the given uid", () => {
+ const createSpy = vi.spyOn(TestBed.inject(NzModalService), "create").mockReturnValue({} as any);
+
+ component.clickToViewQuota(9);
+
+ expect(createSpy).toHaveBeenCalledTimes(1);
+ const config = createSpy.mock.calls[0][0];
+ expect(config.nzContent).toBe(UserQuotaComponent);
+ expect(config.nzData).toEqual({ uid: 9 });
+ });
});