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-7406-1362167975e93322282908bbfbe7ea8ac2c4d2fe in repository https://gitbox.apache.org/repos/asf/texera.git
commit dce17e8446358cabf6b8910705cd7a9c7737f159 Author: Xinyuan Lin <[email protected]> AuthorDate: Fri Aug 7 18:31:40 2026 -0700 test(frontend): render the dataset file preview's viewer branches (#7406) ### What changes were proposed in this PR? The file renderer's class was already at 91% locally, but its template was at **25%** of statements. The template is a viewer switch — each `displayX` flag selects exactly one preview — and only the CSV path had ever been rendered. Adds 16 tests that drive each viewer and assert what actually reaches the screen: the four status alerts (each excluding the others), the shared table for CSV and spreadsheets, image plus the click that opens the full-size modal, video, audio, markdown, JSON, plain text, the empty initial state, and the maximized height. It also pins the `&& safeFileURL` guards on the media branches. Those are load-bearing: the flag is set as soon as the MIME type is known while the object URL is built asynchronously, so rendering on the flag alone emits a source-less `<img>`/`<video>`/`<audio>`. **Verified by mutation**, all reverted (template diff empty): | Mutation | Result | |---|---| | drop the `&& safeFileURL` guard on the video branch | red | | drop the same guard on the image branch | red | | key the markdown branch off `displayJson` | red | | remove the image's click handler | red | | drop `displayXlsx` from the table guard | red | | reword the too-large message | red | | use 100% height when not maximized | red | Local coverage for the component directory: **68.34% → 93.57%** of statements. Three details are commented in the spec, each of which cost a debugging round: - The first `detectChanges()` runs `ngOnInit`, which inspects the empty `filePath` and settles on "preview unsupported"; flags set beforehand are silently overwritten. The helper clears state via the component's own `turnOffAllDisplay()` afterwards. - Binding `[src]` makes Angular call `DomSanitizer.sanitize`, which the existing stub does not provide, so the new block supplies its own. - `<markdown>` needs `MarkdownModule.forRoot()`, following `agent-chat.component.spec.ts`. No production file is touched. ### Any related issues, documentation, discussions? Closes #7403 ### How was this PR tested? ``` npx ng test --watch=false --include="**/user-dataset-file-renderer.component.spec.ts" ``` ``` Test Files 1 passed (1) Tests 48 passed (48) ``` 16 new on top of the existing 32. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --- .../user-dataset-file-renderer.component.spec.ts | 209 ++++++++++++++++++++- 1 file changed, 208 insertions(+), 1 deletion(-) 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 73ac5f04e2..245f3be93c 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 @@ -17,7 +17,7 @@ * under the License. */ -import { TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { getMimeType, MIME_TYPES, UserDatasetFileRendererComponent } from "./user-dataset-file-renderer.component"; import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; @@ -27,6 +27,7 @@ import { commonTestProviders } from "../../../../../../common/testing/test-utils import { of } from "rxjs"; import * as Papa from "papaparse"; import { SimpleChange, SimpleChanges } from "@angular/core"; +import { MarkdownModule } from "ngx-markdown"; describe("UserDatasetFileRendererComponent", () => { let component: UserDatasetFileRendererComponent; @@ -415,3 +416,209 @@ describe("UserDatasetFileRendererComponent", () => { }); }); }); + +/** + * The template is a viewer switch: each `displayX` flag selects exactly one preview, and the media + * branches additionally require `safeFileURL` so a flag on its own cannot render a source-less + * <img>/<video>/<audio>. The suite above drives the flags; this one checks what they put on screen. + * + * Its own TestBed configuration supplies a DomSanitizer with `sanitize`, which Angular calls when + * binding [src] — the stub used above only has `bypassSecurityTrustUrl` and cannot render media. + */ +describe("UserDatasetFileRendererComponent rendering", () => { + let fixture: ComponentFixture<UserDatasetFileRendererComponent>; + let component: UserDatasetFileRendererComponent; + + beforeEach(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + // MarkdownModule.forRoot() backs the <markdown> element in the markdown preview. + imports: [UserDatasetFileRendererComponent, HttpClientTestingModule, MarkdownModule.forRoot()], + providers: [ + DatasetService, + NotificationService, + { + provide: DomSanitizer, + useValue: { + bypassSecurityTrustUrl: (url: string) => url, + sanitize: (_context: number, value: string) => value, + }, + }, + ...commonTestProviders, + ], + }); + fixture = TestBed.createComponent(UserDatasetFileRendererComponent); + component = fixture.componentInstance; + }); + + /** + * Applies a viewer state and renders it. + * + * The first change-detection cycle runs ngOnInit, which inspects the (empty) filePath and settles + * on "preview unsupported". Flags set before that cycle are silently overwritten, so the state is + * cleared through the component's own reset and applied afterwards. + */ + function render(setup: (c: UserDatasetFileRendererComponent) => void): HTMLElement { + fixture.detectChanges(); + component.turnOffAllDisplay(); + setup(component); + fixture.detectChanges(); + return fixture.nativeElement as HTMLElement; + } + + describe("status messages", () => { + it("shows a spinner while the file is loading", () => { + const el = render(c => (c.isLoading = true)); + + expect(el.querySelector("nz-spin")).not.toBeNull(); + expect(el.textContent).toContain("File content is loading"); + }); + + it("shows an error alert when loading failed", () => { + const el = render(c => (c.isFileLoadingError = true)); + + expect(el.textContent).toContain("File loading encounter error."); + }); + + it("shows a distinct alert for a file that is too large", () => { + const el = render(c => (c.isFileSizeUnloadable = true)); + + expect(el.textContent).toContain("File is too large to preview"); + expect(el.textContent).not.toContain("Preview of the file type is currently not supported"); + }); + + it("shows a distinct alert for an unsupported file type", () => { + const el = render(c => (c.isFileTypePreviewUnsupported = true)); + + expect(el.textContent).toContain("Preview of the file type is currently not supported"); + expect(el.textContent).not.toContain("File is too large to preview"); + }); + }); + + describe("tabular preview", () => { + it("renders the parsed header and cells for a CSV", () => { + const el = render(c => { + c.displayCSV = true; + c.tableDataHeader = ["name", "score"]; + c.tableContent = [ + ["ada", "10"], + ["grace", "20"], + ]; + }); + + expect(el.querySelectorAll("th")).toHaveLength(2); + expect(Array.from(el.querySelectorAll("th")).map(th => th.textContent?.trim())).toEqual(["name", "score"]); + expect(el.textContent).toContain("grace"); + }); + + it("uses the same table for a spreadsheet", () => { + const el = render(c => { + c.displayXlsx = true; + c.tableDataHeader = ["col"]; + c.tableContent = [["cell"]]; + }); + + expect(el.querySelector("nz-table")).not.toBeNull(); + expect(el.textContent).toContain("cell"); + }); + }); + + describe("media previews", () => { + it("renders an image and opens the full-size modal when it is clicked", () => { + const el = render(c => { + c.displayImage = true; + c.safeFileURL = "blob:image"; + }); + const img = el.querySelector<HTMLImageElement>(".file-display-area img"); + expect(img).not.toBeNull(); + + img!.click(); + fixture.detectChanges(); + + expect(component.showImageModal).toBe(true); + expect(el.querySelector(".image-modal")).not.toBeNull(); + }); + + it("renders a video for an MP4 and not an audio player", () => { + const el = render(c => { + c.displayMP4 = true; + c.safeFileURL = "blob:video"; + }); + + expect(el.querySelector("video")).not.toBeNull(); + expect(el.querySelector("audio")).toBeNull(); + }); + + it("renders an audio player for an MP3 and not a video", () => { + const el = render(c => { + c.displayMP3 = true; + c.safeFileURL = "blob:audio"; + }); + + expect(el.querySelector("audio")).not.toBeNull(); + expect(el.querySelector("video")).toBeNull(); + }); + + it("renders no media element while the safe URL is still missing", () => { + // The flag is set as soon as the MIME type is known, but the object URL is built + // asynchronously; rendering on the flag alone would emit a source-less element. + const el = render(c => { + c.displayImage = true; + c.displayMP4 = true; + c.displayMP3 = true; + c.safeFileURL = undefined; + }); + + expect(el.querySelector(".file-display-area img")).toBeNull(); + expect(el.querySelector("video")).toBeNull(); + expect(el.querySelector("audio")).toBeNull(); + }); + }); + + describe("text previews", () => { + it("renders markdown through the markdown viewer", () => { + const el = render(c => { + c.displayMarkdown = true; + c.textContent = "# heading"; + }); + + expect(el.querySelector("markdown")).not.toBeNull(); + expect(el.querySelector("ngx-json-viewer")).toBeNull(); + }); + + it("renders JSON through the JSON viewer", () => { + const el = render(c => { + c.displayJson = true; + c.textContent = '{"a":1}'; + }); + + expect(el.querySelector("ngx-json-viewer")).not.toBeNull(); + expect(el.querySelector("markdown")).toBeNull(); + }); + + it("renders plain text inline", () => { + const el = render(c => { + c.displayPlainText = true; + c.textContent = "hello world"; + }); + + expect(el.textContent).toContain("hello world"); + expect(el.querySelector("markdown")).toBeNull(); + }); + }); + + it("renders nothing in the display area until a viewer is selected", () => { + const el = render(() => {}); + + const area = el.querySelector(".file-display-area")!; + expect(area.textContent?.trim()).toBe(""); + }); + + it("fills the container height only when maximized", () => { + const outer = render(c => (c.isMaximized = false)).querySelector<HTMLElement>("div")!; + expect(outer.style.height).toBe("80%"); + + const maximized = render(c => (c.isMaximized = true)).querySelector<HTMLElement>("div")!; + expect(maximized.style.height).toBe("100%"); + }); +});
