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-7322-69c66a17647f3e530a90486d7acde5b0120cda74 in repository https://gitbox.apache.org/repos/asf/texera.git
commit b75e82138278daac68dcec6ab6adfb18179c0f45 Author: Xinyuan Lin <[email protected]> AuthorDate: Wed Aug 5 22:44:55 2026 -0700 test(frontend): cover the files-uploader restart branches (#7322) ### What changes were proposed in this PR? The files-uploader's conflict dialog offers **Resume** and **Restart**, and the existing spec only ever took the Resume half — leaving `Restart`, `Restart For All` and `markForceRestart` untested. The two choices differ by exactly one observable: ``` Resume -> item.restart stays unset -> uploader continues the existing multipart session Restart -> item.restart = true -> uploader calls the backend with type=forceRestart ``` So a Resume/Restart mix-up silently resumes a session the user explicitly asked to discard, and nothing fails. Five tests: | Case | What it pins | |---|---| | Restart sets the flag | the single-file restart branch | | Resume leaves it unset | written as the pair, so a `markForceRestart` wrongly added to Resume is caught too | | `Restart For All` | the flag reaches files that **never prompted** — the latch applies the restart, it does not merely suppress the dialog | | `Resume For All` | must **not** set the flag, distinguishing the two latches | | non-conflicting file | passes straight through, no dialog at all | Both restart assertions were checked by mutation: dropping `markForceRestart` from the corresponding branch turns each red. Reverted afterwards, and the component diff is empty. These are additions to the spec's existing harness — it already captures modal configs and clicks their footer buttons by label — not new infrastructure. No production file is touched. ### Any related issues, documentation, discussions? Closes #7320 ### How was this PR tested? ``` npx ng test --watch=false --include="**/files-uploader.component.spec.ts" ``` ``` ✓ src/app/dashboard/component/user/files-uploader/files-uploader.component.spec.ts (13 tests) Test Files 1 passed (1) ``` `yarn format:ci` passes (prettier-eslint + eslint), which Vitest does not cover on its own. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --------- Signed-off-by: Xinyuan Lin <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> --- .../files-uploader.component.spec.ts | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.spec.ts b/frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.spec.ts index 43398d7efa..1882de68f8 100644 --- a/frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.spec.ts @@ -148,6 +148,97 @@ describe("FilesUploaderComponent", () => { expect((await emitted).map(item => item.name)).toEqual(["same.csv"]); }); + /** + * The Restart choices are the half of the conflict dialog the existing tests never take. They + * differ from Resume by exactly one observable: `item.restart`, which flips from its default + * `false` to `true` and makes the uploader call the backend with type=forceRestart instead of + * continuing the existing multipart session. A Resume/Restart mix-up therefore silently resumes a + * session the user asked to discard. + */ + it("marks a file for force-restart when Restart is chosen", async () => { + datasetService.findExistingUploadFiles.mockReturnValue(of([])); + const emitted = new Promise<FileUploadItem[]>(resolve => component.uploadedFiles.subscribe(resolve)); + + component.fileDropped([droppedFile("failed.csv", new File(["half"], "failed.csv"))]); + + await waitUntil(() => modals.length === 1); + modals[0].nzFooter.find(button => button.label === "Restart")?.onClick(); + + const items = await emitted; + expect(items.map(item => item.name)).toEqual(["failed.csv"]); + expect(items[0].restart).toBe(true); + }); + + it("leaves the restart flag unset when Resume is chosen", async () => { + // The counterpart of the test above: same file, other button. Without this pair, a + // markForceRestart call added to the Resume branch would go unnoticed. + datasetService.findExistingUploadFiles.mockReturnValue(of([])); + const emitted = new Promise<FileUploadItem[]>(resolve => component.uploadedFiles.subscribe(resolve)); + + component.fileDropped([droppedFile("failed.csv", new File(["half"], "failed.csv"))]); + + await waitUntil(() => modals.length === 1); + modals[0].nzFooter.find(button => button.label === "Resume")?.onClick(); + + const items = await emitted; + expect(items.map(item => item.name)).toEqual(["failed.csv"]); + expect(items[0].restart).toBeFalsy(); + }); + + it("restarts every remaining conflicting file after one Restart For All choice", async () => { + datasetService.listMultipartUploads.mockReturnValue(of(["one.csv", "two.csv"])); + datasetService.findExistingUploadFiles.mockReturnValue(of([])); + const emitted = new Promise<FileUploadItem[]>(resolve => component.uploadedFiles.subscribe(resolve)); + + component.fileDropped([ + droppedFile("one.csv", new File(["one"], "one.csv")), + droppedFile("two.csv", new File(["two"], "two.csv")), + ]); + + await waitUntil(() => modals.length === 1); + modals[0].nzFooter.find(button => button.label === "Restart For All")?.onClick(); + + const items = await emitted; + expect(items.map(item => item.name)).toEqual(["one.csv", "two.csv"]); + // Both files carry the flag, and the second one never prompted - the "For All" latch has to + // apply the restart itself rather than just suppressing the dialog. + expect(items.map(item => item.restart)).toEqual([true, true]); + expect(modals).toHaveLength(1); + }); + + it("resumes every remaining conflicting file after one Resume For All choice", async () => { + datasetService.listMultipartUploads.mockReturnValue(of(["one.csv", "two.csv"])); + datasetService.findExistingUploadFiles.mockReturnValue(of([])); + const emitted = new Promise<FileUploadItem[]>(resolve => component.uploadedFiles.subscribe(resolve)); + + component.fileDropped([ + droppedFile("one.csv", new File(["one"], "one.csv")), + droppedFile("two.csv", new File(["two"], "two.csv")), + ]); + + await waitUntil(() => modals.length === 1); + modals[0].nzFooter.find(button => button.label === "Resume For All")?.onClick(); + + const items = await emitted; + expect(items.map(item => item.name)).toEqual(["one.csv", "two.csv"]); + // Distinguishes the two latches: resumeAll must NOT set the flag restartAll sets. + expect(items.every(item => !item.restart)).toBe(true); + expect(modals).toHaveLength(1); + }); + + it("passes a non-conflicting file straight through without prompting", async () => { + datasetService.listMultipartUploads.mockReturnValue(of(["other.csv"])); + datasetService.findExistingUploadFiles.mockReturnValue(of([])); + const emitted = new Promise<FileUploadItem[]>(resolve => component.uploadedFiles.subscribe(resolve)); + + component.fileDropped([droppedFile("clean.csv", new File(["clean"], "clean.csv"))]); + + const items = await emitted; + expect(items.map(item => item.name)).toEqual(["clean.csv"]); + expect(items[0].restart).toBeFalsy(); + expect(modals).toHaveLength(0); + }); + it("skips all matching files after one Skip For All choice", async () => { datasetService.listMultipartUploads.mockReturnValue(of([])); datasetService.findExistingUploadFiles.mockReturnValue(of(["one.csv", "two.csv"]));
