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 { afterEach, describe, expect, test } from "bun:test";
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 = {
shortId: string;
@ -82,7 +82,7 @@ describe("SnapshotTreeBrowser", () => {
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({
files: [{ name: "report.txt", path: "/mnt/project/report.txt", type: "file" }],
});
@ -92,7 +92,14 @@ describe("SnapshotTreeBrowser", () => {
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({
shortId: "repo-1",
snapshotId: "snap-1",
@ -252,7 +259,7 @@ describe("SnapshotTreeBrowser", () => {
}
const initialRequestCount = requests.length;
await userEvent.click(expandIcon);
fireEvent.click(expandIcon);
await waitFor(() => {
expect(requests.length).toBeGreaterThan(initialRequestCount);

View file

@ -114,20 +114,25 @@ export const FileTree = memo((props: Props) => {
const toggleCollapseState = useCallback(
(fullPath: string) => {
const shouldExpand = collapsedFolders.has(fullPath);
setCollapsedFolders((prevSet) => {
const newSet = new Set(prevSet);
if (newSet.has(fullPath)) {
newSet.delete(fullPath);
onFolderExpand?.(fullPath);
} else {
newSet.add(fullPath);
}
return newSet;
});
if (shouldExpand) {
onFolderExpand?.(fullPath);
}
},
[onFolderExpand],
[collapsedFolders, onFolderExpand],
);
// 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 type { FileEntry } from "../components/file-tree";
@ -45,7 +45,7 @@ export const useFileBrowser = (props: UseFileBrowserOptions) => {
const stripPath = pathTransform?.strip;
const addPath = pathTransform?.add;
useMemo(() => {
useEffect(() => {
if (initialData?.files) {
const files = initialData.files;
setAllFiles((prev) => {

View file

@ -218,12 +218,24 @@ describe("stop backup", () => {
repositoryId: repository.id,
});
resticBackupMock.mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
return { exitCode: 0, summary: generateBackupOutput(), error: "" };
resticBackupMock.mockImplementation(({ signal }: SafeSpawnParams) => {
return new Promise((resolve) => {
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 () => {
const runningSchedule = await backupsService.getScheduleById(schedule.id);
@ -232,6 +244,7 @@ describe("stop backup", () => {
// act
await backupsExecutionService.stopBackup(schedule.id);
await executePromise;
// assert
const updatedSchedule = await backupsService.getScheduleById(schedule.id);
@ -248,30 +261,39 @@ describe("stop backup", () => {
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 {
const executePromise = backupsExecutionService.executeBackup(schedule.id);
await waitForExpect(async () => {
const queuedSchedule = await backupsService.getScheduleById(schedule.id);
expect(queuedSchedule.lastBackupStatus).toBe("in_progress");
signal?.addEventListener(
"abort",
() => {
reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
},
{ once: true },
);
});
});
expect(resticBackupMock).not.toHaveBeenCalled();
const executePromise = backupsExecutionService.executeBackup(schedule.id);
await backupsExecutionService.stopBackup(schedule.id);
releaseLock();
await waitForExpect(async () => {
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);
expect(updatedSchedule.lastBackupStatus).toBe("warning");
expect(updatedSchedule.lastBackupError).toBe("Backup was stopped by the user");
expect(resticBackupMock).not.toHaveBeenCalled();
} finally {
releaseLock();
}
await backupsExecutionService.stopBackup(schedule.id);
await executePromise;
const updatedSchedule = await backupsService.getScheduleById(schedule.id);
expect(updatedSchedule.lastBackupStatus).toBe("warning");
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 () => {