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/19 01:35:10 UTC
(texera) 01/01: test(frontend): extend AdminSettingsComponent coverage for save/reset handlers (#6527)
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-6527-90eabda0d24a772b241ad97773c84aac9adaa680
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 11df960395d14765e23ffcec504471a9737d2fb9
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sat Jul 18 18:34:49 2026 -0700
test(frontend): extend AdminSettingsComponent coverage for save/reset handlers (#6527)
### What changes were proposed in this PR?
Extends the existing `AdminSettingsComponent` spec (previously only
`should
create` + a template check, ~35% coverage) to cover the untested
save/reset
handlers. `AdminSettingsService`, `NzMessageService`, and
`NotificationService`
are stubbed; fake timers keep the handlers' `window.location.reload()`
from
firing under jsdom.
15 added tests cover:
- `saveLogos` — persists only the branding assets that are set + success
message; no-op when none set; error message on failure.
- `resetBranding` — resets `logo` / `mini_logo` / `favicon`.
- `saveTabs` / `resetTabs` — persist / reset every sidebar tab.
- `saveDatasetSettings` — persists the four upload settings + success;
rejects non-positive values; rejects a config that would exceed the
10,000-part limit (no save in either).
- `resetDatasetSettings` — resets all four upload settings.
- `saveCsvSettings` — persists the max-columns value + success; error on
failure.
- `resetCsvSettings` — resets the max-columns setting.
- `onFileChange` — rejects a non-image file with an error; reads a valid
image into the matching branding field.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6517
### How was this PR tested?
Extended unit tests, run locally in `frontend/` (all green; the failure
path was
verified by deliberately breaking an assertion to confirm the suite goes
red):
```
ng test --watch=false --include src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts
# Test Files 1 passed (1) | Tests 17 passed (17)
prettier --write <spec> # unchanged
eslint <spec> # clean
```
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---
.../settings/admin-settings.component.spec.ts | 295 +++++++++++++++++++++
1 file changed, 295 insertions(+)
diff --git a/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts b/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts
index 69daa21716..daf3f7a112 100644
--- a/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts
+++ b/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts
@@ -22,6 +22,7 @@ import { AdminSettingsComponent } from "./admin-settings.component";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { NzCardModule } from "ng-zorro-antd/card";
import { NzMessageService } from "ng-zorro-antd/message";
+import { NotificationService } from "../../../../common/service/notification/notification.service";
describe("AdminSettingsComponent", () => {
let component: AdminSettingsComponent;
@@ -123,4 +124,298 @@ describe("AdminSettingsComponent", () => {
httpTestingController.expectNone((req: { method: string }) => req.method === "PUT");
expect(errorSpy).toHaveBeenCalledWith("Settings have not loaded; refresh before saving.");
});
+
+ describe("save / reset handlers", () => {
+ let msgSuccess: ReturnType<typeof vi.fn>;
+ let msgError: ReturnType<typeof vi.fn>;
+ let msgInfo: ReturnType<typeof vi.fn>;
+ let notifySuccess: ReturnType<typeof vi.fn>;
+ let notifyError: ReturnType<typeof vi.fn>;
+ let notifyInfo: ReturnType<typeof vi.fn>;
+
+ const updateUrl = (key: string) => `${SETTINGS_URL}/${key}`;
+ const resetUrl = (key: string) => `${SETTINGS_URL}/reset/${key}`;
+ const HTTP_ERROR = { status: 500, statusText: "Server Error" };
+
+ // Flush the ngOnInit bulk GET so the component is `settingsLoaded`.
+ function completeLoad(settings: Record<string, string> = {}): void {
+ httpTestingController.expectOne(SETTINGS_URL).flush(settings);
+ }
+
+ beforeEach(() => {
+ // The reset/save handlers schedule window.location.reload() via setTimeout;
+ // fake timers keep it from firing (jsdom can't navigate).
+ vi.useFakeTimers();
+
+ const message = TestBed.inject(NzMessageService);
+ const notification = TestBed.inject(NotificationService);
+ msgSuccess = vi.spyOn(message, "success").mockReturnValue({} as ReturnType<NzMessageService["success"]>);
+ msgError = vi.spyOn(message, "error").mockReturnValue({} as ReturnType<NzMessageService["error"]>);
+ msgInfo = vi.spyOn(message, "info").mockReturnValue({} as ReturnType<NzMessageService["info"]>);
+ notifySuccess = vi.spyOn(notification, "success").mockImplementation(() => {});
+ notifyError = vi.spyOn(notification, "error").mockImplementation(() => {});
+ notifyInfo = vi.spyOn(notification, "info").mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ // nz-icon lazily fetches its SVG assets over HTTP; drain those so verify()
+ // only asserts on the requests the handlers under test actually issue.
+ httpTestingController.match(req => req.url.startsWith("assets/")).forEach(req => req.flush(""));
+ httpTestingController.verify();
+ vi.useRealTimers();
+ vi.restoreAllMocks();
+ });
+
+ describe("branding", () => {
+ it("saveLogos PUTs only the branding assets that are set and notifies success", () => {
+ completeLoad();
+ component.logoData = "logo.png";
+ component.miniLogoData = "mini.png";
+ component.faviconData = null;
+
+ component.saveLogos();
+
+ const logoReq = httpTestingController.expectOne(updateUrl("logo"));
+ expect(logoReq.request.method).toBe("PUT");
+ expect(logoReq.request.body).toEqual({ value: "logo.png" });
+ const miniReq = httpTestingController.expectOne(updateUrl("mini_logo"));
+ expect(miniReq.request.body).toEqual({ value: "mini.png" });
+ httpTestingController.expectNone(updateUrl("favicon"));
+ logoReq.flush(null);
+ miniReq.flush(null);
+
+ expect(msgSuccess).toHaveBeenCalledWith("Branding saved successfully.");
+ });
+
+ it("saveLogos does nothing when no branding asset is set", () => {
+ completeLoad();
+ component.logoData = null;
+ component.miniLogoData = null;
+ component.faviconData = null;
+
+ component.saveLogos();
+
+ httpTestingController.expectNone((req: { method: string }) => req.method === "PUT");
+ expect(msgSuccess).not.toHaveBeenCalled();
+ });
+
+ it("saveLogos notifies an error when the request fails", () => {
+ completeLoad();
+ component.logoData = "logo.png";
+ component.miniLogoData = null;
+ component.faviconData = null;
+
+ component.saveLogos();
+
+ httpTestingController.expectOne(updateUrl("logo")).flush("boom", HTTP_ERROR);
+ expect(msgError).toHaveBeenCalledWith("Failed to save branding.");
+ });
+
+ it("resetBranding POSTs a reset for all three branding settings", () => {
+ completeLoad();
+
+ component.resetBranding();
+
+ ["logo", "mini_logo", "favicon"].forEach(key => {
+ const req = httpTestingController.expectOne(resetUrl(key));
+ expect(req.request.method).toBe("POST");
+ req.flush(null);
+ });
+ expect(msgInfo).toHaveBeenCalledWith("Resetting branding...");
+ });
+ });
+
+ describe("tabs", () => {
+ it("saveTabs PUTs every sidebar tab and notifies success", () => {
+ completeLoad();
+
+ component.saveTabs();
+
+ Object.keys(component.sidebarTabs).forEach(tab => {
+ const req = httpTestingController.expectOne(updateUrl(tab));
+ expect(req.request.method).toBe("PUT");
+ req.flush(null);
+ });
+ expect(msgSuccess).toHaveBeenCalledWith("Tabs saved successfully.");
+ });
+
+ it("saveTabs notifies an error when a request fails", () => {
+ completeLoad();
+
+ component.saveTabs();
+
+ // Fail the last request; forkJoin errors only after the earlier ones
+ // have resolved, so every issued PUT is flushed (none left pending).
+ const tabs = Object.keys(component.sidebarTabs);
+ tabs.forEach((tab, i) => {
+ const req = httpTestingController.expectOne(updateUrl(tab));
+ if (i === tabs.length - 1) req.flush("boom", HTTP_ERROR);
+ else req.flush(null);
+ });
+ expect(msgError).toHaveBeenCalledWith("Failed to save tabs.");
+ });
+
+ it("resetTabs POSTs a reset for every sidebar tab", () => {
+ completeLoad();
+
+ component.resetTabs();
+
+ Object.keys(component.sidebarTabs).forEach(tab => {
+ httpTestingController.expectOne(resetUrl(tab)).flush(null);
+ });
+ expect(msgInfo).toHaveBeenCalledWith("Resetting tabs...");
+ });
+ });
+
+ describe("dataset settings", () => {
+ it("saveDatasetSettings PUTs the four upload settings and notifies success", () => {
+ completeLoad(); // defaults (20 / 3 / 10 / 50) are valid
+
+ component.saveDatasetSettings();
+
+ const expectPut = (key: string, value: string) => {
+ const req = httpTestingController.expectOne(updateUrl(key));
+ expect(req.request.method).toBe("PUT");
+ expect(req.request.body).toEqual({ value });
+ req.flush(null);
+ };
+ expectPut("max_number_of_concurrent_uploading_file", "3");
+ expectPut("single_file_upload_max_size_mib", "20");
+ expectPut("max_number_of_concurrent_uploading_file_chunks", "10");
+ expectPut("multipart_upload_chunk_size_mib", "50");
+
+ expect(msgSuccess).toHaveBeenCalledWith("Dataset upload settings saved successfully.");
+ });
+
+ it("saveDatasetSettings rejects non-positive values without saving", () => {
+ completeLoad();
+ component.maxFileSizeMiB = 0;
+
+ component.saveDatasetSettings();
+
+ httpTestingController.expectNone((req: { method: string }) => req.method === "PUT");
+ expect(msgError).toHaveBeenCalledWith("Please enter valid integer values.");
+ });
+
+ it("saveDatasetSettings rejects a configuration that would exceed the 10,000-part limit", () => {
+ completeLoad();
+ component.maxFileSizeMiB = 100000;
+ component.chunkSizeMiB = 1;
+
+ component.saveDatasetSettings();
+
+ httpTestingController.expectNone((req: { method: string }) => req.method === "PUT");
+ expect(msgError).toHaveBeenCalled();
+ });
+
+ it("saveDatasetSettings notifies an error when a request fails", () => {
+ completeLoad();
+
+ component.saveDatasetSettings();
+
+ // Fail the last of the four PUTs so forkJoin errors with every request flushed.
+ const keys = [
+ "max_number_of_concurrent_uploading_file",
+ "single_file_upload_max_size_mib",
+ "max_number_of_concurrent_uploading_file_chunks",
+ "multipart_upload_chunk_size_mib",
+ ];
+ keys.forEach((key, i) => {
+ const req = httpTestingController.expectOne(updateUrl(key));
+ if (i === keys.length - 1) req.flush("boom", HTTP_ERROR);
+ else req.flush(null);
+ });
+ expect(msgError).toHaveBeenCalledWith("Failed to save dataset settings.");
+ });
+
+ it("resetDatasetSettings POSTs a reset for all four upload settings", () => {
+ completeLoad();
+
+ component.resetDatasetSettings();
+
+ [
+ "max_number_of_concurrent_uploading_file",
+ "single_file_upload_max_size_mib",
+ "max_number_of_concurrent_uploading_file_chunks",
+ "multipart_upload_chunk_size_mib",
+ ].forEach(key => httpTestingController.expectOne(resetUrl(key)).flush(null));
+ expect(msgInfo).toHaveBeenCalledWith("Resetting dataset settings...");
+ });
+ });
+
+ describe("csv (result panel) settings", () => {
+ it("saveCsvSettings PUTs the max-columns value and notifies success", () => {
+ completeLoad();
+ component.csvMaxColumns = 256;
+
+ component.saveCsvSettings();
+
+ const req = httpTestingController.expectOne(updateUrl("csv_parser_max_columns"));
+ expect(req.request.method).toBe("PUT");
+ expect(req.request.body).toEqual({ value: "256" });
+ req.flush(null);
+
+ expect(notifySuccess).toHaveBeenCalledWith("Result panel settings saved.");
+ });
+
+ it("saveCsvSettings notifies an error when the request fails", () => {
+ completeLoad();
+
+ component.saveCsvSettings();
+
+ httpTestingController.expectOne(updateUrl("csv_parser_max_columns")).flush("boom", HTTP_ERROR);
+ expect(notifyError).toHaveBeenCalledWith("Could not save result panel settings.");
+ });
+
+ it("resetCsvSettings POSTs a reset and notifies info", () => {
+ completeLoad();
+
+ component.resetCsvSettings();
+
+ httpTestingController.expectOne(resetUrl("csv_parser_max_columns")).flush(null);
+ expect(notifyInfo).toHaveBeenCalledWith("Resetting result panel settings...");
+ });
+ });
+
+ describe("onFileChange", () => {
+ it("rejects a non-image file with an error and leaves the existing logo untouched", () => {
+ completeLoad();
+ component.logoData = "data:image/png;base64,EXISTING";
+ const event = {
+ target: { files: [new File(["x"], "notes.txt", { type: "text/plain" })] },
+ } as unknown as Event;
+
+ component.onFileChange("logo", event);
+
+ expect(msgError).toHaveBeenCalledWith("Please upload a valid image file.");
+ expect(component.logoData).toBe("data:image/png;base64,EXISTING");
+ });
+
+ it("reads a valid image file into the matching branding field", async () => {
+ completeLoad();
+ const dataUrl = "data:image/png;base64,AAA";
+ class FakeFileReader {
+ onload: ((e: { target: { result: string } }) => void) | null = null;
+ readAsDataURL(): void {
+ queueMicrotask(() => this.onload?.({ target: { result: dataUrl } }));
+ }
+ }
+ const realFileReader = globalThis.FileReader;
+ (globalThis as any).FileReader = FakeFileReader;
+
+ try {
+ const event = {
+ target: { files: [new File(["x"], "logo.png", { type: "image/png" })] },
+ } as unknown as Event;
+
+ component.onFileChange("mini_logo", event);
+ await Promise.resolve();
+
+ expect(component.miniLogoData).toBe(dataUrl);
+ } finally {
+ (globalThis as any).FileReader = realFileReader;
+ }
+ });
+ });
+ });
});