test: fix flaky test depending on mutex timing
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled

This commit is contained in:
Nicolas Meienberger 2026-04-01 09:27:13 +02:00
parent add0f2788f
commit e265f7d478
4 changed files with 64 additions and 30 deletions

View file

@ -1,7 +1,7 @@
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { afterEach, describe, expect, test } from "bun:test"; import { afterEach, describe, expect, test } from "bun:test";
import { HttpResponse, http, server } from "~/test/msw/server"; import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, render, screen, userEvent, waitFor, within } from "~/test/test-utils"; import { cleanup, fireEvent, render, screen, userEvent, waitFor, within } from "~/test/test-utils";
type SnapshotFilesRequest = { type SnapshotFilesRequest = {
shortId: string; shortId: string;
@ -82,7 +82,7 @@ describe("SnapshotTreeBrowser", () => {
expect(await screen.findByRole("button", { name: "project" })).toBeTruthy(); expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
}); });
test("renders a single file when no display base path is available", async () => { test("renders synthesized ancestor folders for a single file when no display base path is available", async () => {
const requests = mockListSnapshotFiles({ const requests = mockListSnapshotFiles({
files: [{ name: "report.txt", path: "/mnt/project/report.txt", type: "file" }], files: [{ name: "report.txt", path: "/mnt/project/report.txt", type: "file" }],
}); });
@ -92,7 +92,14 @@ describe("SnapshotTreeBrowser", () => {
displayBasePath: undefined, displayBasePath: undefined,
}); });
expect(await screen.findByRole("button", { name: "report.txt" })).toBeTruthy(); const mntRow = await screen.findByRole("button", { name: "mnt" });
const mntExpandIcon = mntRow.querySelector("svg");
if (!mntExpandIcon) {
throw new Error("Expected expand icon for mnt row");
}
fireEvent.click(mntExpandIcon);
expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
expect(requests[0]).toEqual({ expect(requests[0]).toEqual({
shortId: "repo-1", shortId: "repo-1",
snapshotId: "snap-1", snapshotId: "snap-1",
@ -252,7 +259,7 @@ describe("SnapshotTreeBrowser", () => {
} }
const initialRequestCount = requests.length; const initialRequestCount = requests.length;
await userEvent.click(expandIcon); fireEvent.click(expandIcon);
await waitFor(() => { await waitFor(() => {
expect(requests.length).toBeGreaterThan(initialRequestCount); expect(requests.length).toBeGreaterThan(initialRequestCount);

View file

@ -114,20 +114,25 @@ export const FileTree = memo((props: Props) => {
const toggleCollapseState = useCallback( const toggleCollapseState = useCallback(
(fullPath: string) => { (fullPath: string) => {
const shouldExpand = collapsedFolders.has(fullPath);
setCollapsedFolders((prevSet) => { setCollapsedFolders((prevSet) => {
const newSet = new Set(prevSet); const newSet = new Set(prevSet);
if (newSet.has(fullPath)) { if (newSet.has(fullPath)) {
newSet.delete(fullPath); newSet.delete(fullPath);
onFolderExpand?.(fullPath);
} else { } else {
newSet.add(fullPath); newSet.add(fullPath);
} }
return newSet; return newSet;
}); });
if (shouldExpand) {
onFolderExpand?.(fullPath);
}
}, },
[onFolderExpand], [collapsedFolders, onFolderExpand],
); );
// Add new folders to collapsed set when file list changes // Add new folders to collapsed set when file list changes

View file

@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { logger } from "~/client/lib/logger"; import { logger } from "~/client/lib/logger";
import type { FileEntry } from "../components/file-tree"; import type { FileEntry } from "../components/file-tree";
@ -45,7 +45,7 @@ export const useFileBrowser = (props: UseFileBrowserOptions) => {
const stripPath = pathTransform?.strip; const stripPath = pathTransform?.strip;
const addPath = pathTransform?.add; const addPath = pathTransform?.add;
useMemo(() => { useEffect(() => {
if (initialData?.files) { if (initialData?.files) {
const files = initialData.files; const files = initialData.files;
setAllFiles((prev) => { setAllFiles((prev) => {

View file

@ -218,12 +218,24 @@ describe("stop backup", () => {
repositoryId: repository.id, repositoryId: repository.id,
}); });
resticBackupMock.mockImplementation(async () => { resticBackupMock.mockImplementation(({ signal }: SafeSpawnParams) => {
await new Promise((resolve) => setTimeout(resolve, 500)); return new Promise((resolve) => {
return { exitCode: 0, summary: generateBackupOutput(), error: "" }; if (signal?.aborted) {
resolve({ exitCode: 1, summary: "", error: "" });
return;
}
signal?.addEventListener(
"abort",
() => {
resolve({ exitCode: 1, summary: "", error: "" });
},
{ once: true },
);
});
}); });
void backupsExecutionService.executeBackup(schedule.id); const executePromise = backupsExecutionService.executeBackup(schedule.id);
await waitForExpect(async () => { await waitForExpect(async () => {
const runningSchedule = await backupsService.getScheduleById(schedule.id); const runningSchedule = await backupsService.getScheduleById(schedule.id);
@ -232,6 +244,7 @@ describe("stop backup", () => {
// act // act
await backupsExecutionService.stopBackup(schedule.id); await backupsExecutionService.stopBackup(schedule.id);
await executePromise;
// assert // assert
const updatedSchedule = await backupsService.getScheduleById(schedule.id); const updatedSchedule = await backupsService.getScheduleById(schedule.id);
@ -248,30 +261,39 @@ describe("stop backup", () => {
repositoryId: repository.id, repositoryId: repository.id,
}); });
const releaseLock = await repoMutex.acquireExclusive(repository.id, "test"); spyOn(repoMutex, "acquireShared").mockImplementation((_repositoryId, _operation, signal) => {
return new Promise((_, reject) => {
if (signal?.aborted) {
reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
return;
}
try { signal?.addEventListener(
const executePromise = backupsExecutionService.executeBackup(schedule.id); "abort",
() => {
await waitForExpect(async () => { reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
const queuedSchedule = await backupsService.getScheduleById(schedule.id); },
expect(queuedSchedule.lastBackupStatus).toBe("in_progress"); { once: true },
);
}); });
});
expect(resticBackupMock).not.toHaveBeenCalled(); const executePromise = backupsExecutionService.executeBackup(schedule.id);
await backupsExecutionService.stopBackup(schedule.id); await waitForExpect(async () => {
releaseLock(); const queuedSchedule = await backupsService.getScheduleById(schedule.id);
expect(queuedSchedule.lastBackupStatus).toBe("in_progress");
});
await executePromise; expect(resticBackupMock).not.toHaveBeenCalled();
const updatedSchedule = await backupsService.getScheduleById(schedule.id); await backupsExecutionService.stopBackup(schedule.id);
expect(updatedSchedule.lastBackupStatus).toBe("warning"); await executePromise;
expect(updatedSchedule.lastBackupError).toBe("Backup was stopped by the user");
expect(resticBackupMock).not.toHaveBeenCalled(); const updatedSchedule = await backupsService.getScheduleById(schedule.id);
} finally { expect(updatedSchedule.lastBackupStatus).toBe("warning");
releaseLock(); expect(updatedSchedule.lastBackupError).toBe("Backup was stopped by the user");
} expect(resticBackupMock).not.toHaveBeenCalled();
}); });
test("should throw ConflictError when trying to stop non-running backup", async () => { test("should throw ConflictError when trying to stop non-running backup", async () => {