This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new b4d1865658 test(frontend): extend UserDatasetFileRendererComponent
unit test coverage (#6816)
b4d1865658 is described below
commit b4d18656589523e3132a41607557da0d8bc44e18
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Jul 26 05:43:09 2026 -0700
test(frontend): extend UserDatasetFileRendererComponent unit test coverage
(#6816)
### What changes were proposed in this PR?
Extends `user-dataset-file-renderer.component.spec.ts` with 15 tests (18
-> 33), covering `ngOnChanges` reload triggers, object-URL revocation on
destroy / turn-off, the file-type viewer selection switch (image / mp4 /
mp3 / markdown / json / csv, with CSV parsed into a table), and the
in-callback oversize and unsupported-preview guards. Coverage 69% -> 92%
lines. No existing tests modified.
### Any related issues, documentation, discussions?
Closes #6812.
### How was this PR tested?
`ng test --include='**/user-dataset-file-renderer.component.spec.ts'` ->
33/33 passing. `yarn format:ci` passes.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---------
Co-authored-by: Meng Wang <[email protected]>
---
.../user-dataset-file-renderer.component.spec.ts | 177 +++++++++++++++++++++
1 file changed, 177 insertions(+)
diff --git
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts
index ab19ff9539..73ac5f04e2 100644
---
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts
+++
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts
@@ -25,6 +25,8 @@ import { NotificationService } from
"../../../../../../common/service/notificati
import { DomSanitizer } from "@angular/platform-browser";
import { commonTestProviders } from
"../../../../../../common/testing/test-utils";
import { of } from "rxjs";
+import * as Papa from "papaparse";
+import { SimpleChange, SimpleChanges } from "@angular/core";
describe("UserDatasetFileRendererComponent", () => {
let component: UserDatasetFileRendererComponent;
@@ -237,4 +239,179 @@ describe("UserDatasetFileRendererComponent", () => {
}
});
});
+
+ describe("ngOnChanges", () => {
+ // Realistic SimpleChange(previousValue, currentValue, firstChange) inputs
so the tests
+ // don't rely on empty {} placeholders and stay valid if ngOnChanges
starts reading the values.
+ const chg = (previous: unknown, current: unknown, firstChange = false):
SimpleChange =>
+ new SimpleChange(previous, current, firstChange);
+
+ it("reloads when filePath changes", () => {
+ const reloadSpy = vi.spyOn(component,
"reloadFileContent").mockImplementation(() => {});
+ component.ngOnChanges({ filePath: chg("/old.txt", "/new.txt") } as
SimpleChanges);
+ expect(reloadSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("reloads when both did and dvid change together", () => {
+ const reloadSpy = vi.spyOn(component,
"reloadFileContent").mockImplementation(() => {});
+ component.ngOnChanges({ did: chg(1, 2), dvid: chg(3, 4) } as
SimpleChanges);
+ expect(reloadSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not reload when only did changes without dvid", () => {
+ const reloadSpy = vi.spyOn(component,
"reloadFileContent").mockImplementation(() => {});
+ component.ngOnChanges({ did: chg(1, 2) } as SimpleChanges);
+ expect(reloadSpy).not.toHaveBeenCalled();
+ });
+
+ it("does not reload for an unrelated input change", () => {
+ const reloadSpy = vi.spyOn(component,
"reloadFileContent").mockImplementation(() => {});
+ component.ngOnChanges({ isMaximized: chg(false, true) } as
SimpleChanges);
+ expect(reloadSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("ngOnDestroy", () => {
+ it("revokes the object URL when one was created", () => {
+ const originalRevoke = URL.revokeObjectURL;
+ const revokeSpy = vi.fn();
+ (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL =
revokeSpy;
+ try {
+ component.fileURL = "blob:to-revoke";
+ component.ngOnDestroy();
+ expect(revokeSpy).toHaveBeenCalledWith("blob:to-revoke");
+ } finally {
+ (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL =
originalRevoke;
+ }
+ });
+
+ it("does nothing when no object URL exists", () => {
+ const originalRevoke = URL.revokeObjectURL;
+ const revokeSpy = vi.fn();
+ (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL =
revokeSpy;
+ try {
+ component.fileURL = undefined;
+ component.ngOnDestroy();
+ expect(revokeSpy).not.toHaveBeenCalled();
+ } finally {
+ (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL =
originalRevoke;
+ }
+ });
+ });
+
+ describe("turnOffAllDisplay URL cleanup", () => {
+ it("revokes both the raw and the safe object URLs when present", () => {
+ const originalRevoke = URL.revokeObjectURL;
+ const revokeSpy = vi.fn();
+ (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL =
revokeSpy;
+ try {
+ component.fileURL = "blob:raw";
+ // safeFileURL is a SafeUrl (an opaque object); the code calls
.toString() on it before
+ // revoking, so use an object with a toString() rather than a raw
string cast.
+ component.safeFileURL = { toString: () => "blob:safe" } as unknown as
typeof component.safeFileURL;
+ component.turnOffAllDisplay();
+ expect(revokeSpy).toHaveBeenCalledWith("blob:raw");
+ expect(revokeSpy).toHaveBeenCalledWith("blob:safe");
+ } finally {
+ (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL =
originalRevoke;
+ }
+ });
+ });
+
+ describe("reloadFileContent viewer selection", () => {
+ // Helper: drive reloadFileContent through the async subscribe with a
canned blob.
+ // fileSize is left undefined so the pre-check size guard is skipped and
the
+ // blob-size branch inside next() is exercised instead.
+ function loadWith(filePath: string, blob: Blob) {
+ const datasetService = TestBed.inject(DatasetService);
+ vi.spyOn(datasetService,
"retrieveDatasetVersionSingleFile").mockReturnValue(of(blob));
+ component.did = 1;
+ component.dvid = 2;
+ component.filePath = filePath;
+ component.isLogin = false;
+ component.fileSize = undefined;
+ component.reloadFileContent();
+ }
+
+ let originalCreate: typeof URL.createObjectURL;
+ let createSpy: ReturnType<typeof vi.fn>;
+
+ beforeEach(() => {
+ originalCreate = URL.createObjectURL;
+ createSpy = vi.fn(() => "blob:created");
+ (URL as unknown as { createObjectURL: unknown }).createObjectURL =
createSpy;
+ });
+
+ afterEach(() => {
+ (URL as unknown as { createObjectURL: unknown }).createObjectURL =
originalCreate;
+ });
+
+ it("selects the image viewer and builds a safe URL for a PNG", () => {
+ const blob = new Blob(["img"], { type: "image/png" });
+ loadWith("photo.png", blob);
+ expect(component.displayImage).toBe(true);
+ expect(createSpy).toHaveBeenCalledWith(blob);
+ expect(component.fileURL).toBe("blob:created");
+ });
+
+ it("selects the video viewer for an MP4", () => {
+ const blob = new Blob(["vid"], { type: "video/mp4" });
+ loadWith("clip.mp4", blob);
+ expect(component.displayMP4).toBe(true);
+ expect(createSpy).toHaveBeenCalledWith(blob);
+ });
+
+ it("selects the audio viewer for an MP3", () => {
+ const blob = new Blob(["aud"], { type: "audio/mpeg" });
+ loadWith("song.mp3", blob);
+ expect(component.displayMP3).toBe(true);
+ expect(createSpy).toHaveBeenCalledWith(blob);
+ });
+
+ it("selects the markdown viewer for a MD file", () => {
+ const blob = new Blob(["# hi"], { type: "text/markdown" });
+ loadWith("readme.md", blob);
+ expect(component.displayMarkdown).toBe(true);
+ });
+
+ it("selects the JSON viewer for a JSON file", () => {
+ const blob = new Blob(['{"a":1}'], { type: "application/json" });
+ loadWith("data.json", blob);
+ expect(component.displayJson).toBe(true);
+ });
+
+ // Papa.parse's property on the vite namespace is a non-configurable
getter (cannot be spied or
+ // reassigned), and module-mocking papaparse is disallowed, so the real
parser runs against a
+ // real CSV File and the async result is awaited via vi.waitFor.
+ it("selects the CSV viewer and parses the file into the tabular header and
content", async () => {
+ // Reference the imported namespace so the papaparse import is retained
for the real parse path.
+ expect(typeof Papa.parse).toBe("function");
+ const blob = new Blob(["h1,h2\na,b\n"], { type: "text/csv" });
+ loadWith("data.csv", blob);
+ expect(component.displayCSV).toBe(true);
+ await vi.waitFor(() =>
expect(component.tableDataHeader.length).toBeGreaterThan(0));
+ expect(component.tableDataHeader).toEqual(["h1", "h2"]);
+ expect(component.tableContent[0]).toEqual(["a", "b"]);
+ });
+
+ it("flags an oversized blob returned by the backend and warns the user",
() => {
+ const notificationService = TestBed.inject(NotificationService);
+ const warnSpy = vi.spyOn(notificationService,
"warning").mockImplementation(() => {});
+ // TXT limit is 1 MB; build a blob just over it. Pre-check is skipped
(fileSize undefined),
+ // so the inner blob.size guard is what rejects it.
+ const oversized = new Blob(["x".repeat(1024 * 1024 + 10)], { type:
"text/plain" });
+ loadWith("big.txt", oversized);
+ expect(component.isFileSizeUnloadable).toBe(true);
+ expect(warnSpy).toHaveBeenCalled();
+ });
+
+ it("re-checks preview support on the loaded blob and rejects an
unsupported one", () => {
+ // The pre-check passes (first call true) but the in-callback re-check
fails (subsequent false),
+ // exercising the defensive guard inside next().
+ vi.spyOn(component,
"isPreviewSupported").mockReturnValueOnce(true).mockReturnValue(false);
+ const blob = new Blob(["hi"], { type: "text/plain" });
+ loadWith("notes.txt", blob);
+ expect(component.isFileTypePreviewUnsupported).toBe(true);
+ });
+ });
});