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 <[email protected]>
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;
+        }
+      });
+    });
+  });
 });

Reply via email to