diff --git a/src/module/alertModal.js b/src/module/alertModal.js index 41e82bd..3692565 100644 --- a/src/module/alertModal.js +++ b/src/module/alertModal.js @@ -29,20 +29,23 @@ const customClass = { "!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ", }; /** - * Shows a modal dialog to save a new filter with a user-provided name. + * Shows a modal dialog prompting for a name and saves through the + * given backend API function. * - * @param {Function} addFilterId - Backend API function to create the filter. - * @param {Function|null} [next=null] - Vue Router next() guard callback. - * @returns {Promise} True if the filter was saved, false otherwise. + * @param {string} title - The dialog title. + * @param {string} inputPlaceholder - The name input placeholder text. + * @param {Function} save - Backend API function receiving the name. + * @param {Function|null} [onCancel=null] - Callback invoked when the + * dialog is cancelled or dismissed. + * @returns {Promise} True if saved, false otherwise. */ -export async function saveFilter(addFilterId, next = null) { +async function promptSaveName(title, inputPlaceholder, save, onCancel = null) { let fileName = ""; - const pageAdminStore = usePageAdminStore(); const { value, isConfirmed } = await Swal.fire({ - title: "SAVE NEW FILTER", + title: title, input: "text", - inputPlaceholder: "Enter Filter Name.", + inputPlaceholder: inputPlaceholder, inputValue: fileName, inputAttributes: { maxlength: 200, @@ -64,7 +67,7 @@ export async function saveFilter(addFilterId, next = null) { // Determine whether to redirect based on the return value if (isConfirmed) { // Save succeeded - await addFilterId(fileName); + await save(fileName); // Show save complete notification if (value) { // Example of value: yes @@ -73,16 +76,34 @@ export async function saveFilter(addFilterId, next = null) { // Clear the input field fileName = ""; return true; - } else { - // Clicked cancel or outside the dialog; save failed. - pageAdminStore.keepPreviousPage(); - - // Not every time we have nontrivial next value - if (next !== null) { - next(false); - } - return false; } + // Clicked cancel or outside the dialog; save failed. + if (onCancel !== null) onCancel(); + return false; +} +/** + * Shows a modal dialog to save a new filter with a user-provided name. + * + * @param {Function} addFilterId - Backend API function to create the filter. + * @param {Function|null} [next=null] - Vue Router next() guard callback. + * @returns {Promise} True if the filter was saved, false otherwise. + */ +export async function saveFilter(addFilterId, next = null) { + const pageAdminStore = usePageAdminStore(); + + return promptSaveName( + "SAVE NEW FILTER", + "Enter Filter Name.", + addFilterId, + () => { + pageAdminStore.keepPreviousPage(); + + // Not every time we have nontrivial next value + if (next !== null) { + next(false); + } + }, + ); } /** * Shows a timed success notification after a file has been saved. @@ -117,9 +138,67 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) { const allMapDataStore = useAllMapDataStore(); const pageAdminStore = usePageAdminStore(); - const result = await Swal.fire({ + await confirmLeave({ title: "SAVE YOUR FILTER?", html: "If you want to continue using this filter in any other page, please select [Yes].", + saveChanges: async () => { + if (allMapDataStore.createFilterId) { + await allMapDataStore.updateFilter(); + if (allMapDataStore.isUpdateFilter) { + await savedSuccessfully(allMapDataStore.filterName); + } + } else { + // Dangerous, here shows a modal + await saveFilter(addFilterId, next); + } + }, + navigateAfterSave: true, + // Handle page admin issue + beforeDismiss: () => pageAdminStore.keepPreviousPage(), + discardTemp: () => { + allMapDataStore.tempFilterId = null; + }, + next: next, + toPath: toPath, + logOut: logOut, + }); +} +/** + * Shows the "save before leaving" confirmation dialog and runs the + * confirm, cancel, and backdrop flows shared by the filter and + * conformance pages. + * + * @param {Object} options - The dialog and flow options. + * @param {string} options.title - The dialog title. + * @param {string} [options.html] - Optional dialog body HTML. + * @param {Function} options.saveChanges - Async callback saving the + * unsaved changes on confirm. + * @param {boolean} options.navigateAfterSave - Whether to navigate (or + * log out) after saving on confirm. + * @param {Function|null} options.beforeDismiss - Callback invoked on + * cancel or backdrop dismiss before anything else, or null. + * @param {Function} options.discardTemp - Callback discarding the + * temporary unsaved data on cancel. + * @param {Function} options.next - Vue Router next() guard callback. + * @param {string} options.toPath - The destination route path. + * @param {Function} [options.logOut] - Optional logout function to call + * instead of navigating. + * @returns {Promise} + */ +async function confirmLeave({ + title, + html, + saveChanges, + navigateAfterSave, + beforeDismiss, + discardTemp, + next, + toPath, + logOut, +}) { + const result = await Swal.fire({ + title: title, + ...(html !== undefined && { html: html }), icon: "warning", iconColor: "#FF3366", reverseButtons: true, @@ -132,31 +211,16 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) { }); if (result.isConfirmed) { - if (allMapDataStore.createFilterId) { - await allMapDataStore.updateFilter(); - if (allMapDataStore.isUpdateFilter) { - await savedSuccessfully(allMapDataStore.filterName); - } - } else { - // Dangerous, here shows a modal - await saveFilter(addFilterId, next); + await saveChanges(); + if (navigateAfterSave) { + logOut ? logOut() : next(toPath); } - - logOut ? logOut() : next(toPath); } else if (result.dismiss === "cancel") { - // console.log('popup cancel case', ); - // Handle page admin issue - // console.log("usePageAdminStore.activePage", usePageAdminStore.activePage); - pageAdminStore.keepPreviousPage(); - - allMapDataStore.tempFilterId = null; + if (beforeDismiss) beforeDismiss(); + discardTemp(); logOut ? logOut() : next(toPath); } else if (result.dismiss === "backdrop") { - // console.log('popup backdrop case', ); - // Handle page admin issue - // console.log("usePageAdminStore.activePage", usePageAdminStore.activePage); - pageAdminStore.keepPreviousPage(); - + if (beforeDismiss) beforeDismiss(); if (!logOut) { next(false); } @@ -171,47 +235,16 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) { * @returns {Promise} True if the rule was saved, false otherwise. */ export async function saveConformance(addConformanceCreateCheckId) { - let fileName = ""; - const { value, isConfirmed } = await Swal.fire({ - title: "SAVE NEW RULE", - input: "text", - inputPlaceholder: "Enter Rule Name.", - inputValue: fileName, - inputAttributes: { - maxlength: 200, - }, - inputValidator: (value) => { - if (!value) return "You need to write something!"; - fileName = value; - }, - icon: "info", - iconHtml: - 'cloud_upload', - iconColor: "#0099FF", - reverseButtons: true, - confirmButtonColor: "#0099FF", - showCancelButton: true, - cancelButtonColor: "#94a3b8", - customClass: customClass, - }); - // Determine whether to redirect based on the return value - if (isConfirmed) { - // Save succeeded - await addConformanceCreateCheckId(fileName); - // Show save complete notification - if (value) savedSuccessfully(value); - // Clear the input field - fileName = ""; - return true; - } else { - // Clicked cancel or outside the dialog; save failed. - return false; - } + return promptSaveName( + "SAVE NEW RULE", + "Enter Rule Name.", + addConformanceCreateCheckId, + ); } /** * Prompts the user to save unsaved conformance rule changes before - * leaving the Conformance page. Delegates to helper functions for - * confirm, cancel, and backdrop scenarios. + * leaving the Conformance page. Handles confirm (save), cancel + * (discard), and backdrop (stay) scenarios. * * @param {Function} next - Vue Router next() guard callback. * @param {Function} addConformanceCreateCheckId - Backend API function @@ -227,91 +260,32 @@ export async function leaveConformance( logOut, ) { const conformanceStore = useConformanceStore(); - const result = await showConfirmationDialog(); - if (result.isConfirmed) { - await handleConfirmed(conformanceStore, addConformanceCreateCheckId); - } else { - await handleDismiss(result.dismiss, conformanceStore, next, toPath, logOut); - } -} -/** - * Displays the "SAVE YOUR RULE?" confirmation dialog. - * @returns {Promise} The SweetAlert2 result object. - */ -async function showConfirmationDialog() { - return Swal.fire({ + await confirmLeave({ title: "SAVE YOUR RULE?", - icon: "warning", - iconColor: "#FF3366", - reverseButtons: true, - confirmButtonText: "Yes", - confirmButtonColor: "#FF3366", - showCancelButton: true, - cancelButtonText: "No", - cancelButtonColor: "#94a3b8", - customClass: customClass, - }); -} - -/** - * Handles the confirmed save action for conformance rules. - * @param {Object} conformanceStore - The conformance Pinia store. - * @param {Function} addConformanceCreateCheckId - API function to create check. - * @returns {Promise} - */ -async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) { - if ( - conformanceStore.conformanceFilterCreateCheckId || - conformanceStore.conformanceLogCreateCheckId - ) { - await conformanceStore.updateConformance(); - if (conformanceStore.isUpdateConformance) { - await savedSuccessfully(conformanceStore.conformanceFileName); - } - } else { - await saveConformance(addConformanceCreateCheckId); - } -} - -/** - * Handles dismiss actions (cancel or backdrop click) for conformance modals. - * @param {string} dismissType - The SweetAlert2 dismiss reason. - * @param {Object} conformanceStore - The conformance Pinia store. - * @param {Function} next - Vue Router next() guard callback. - * @param {string} toPath - The destination route path. - * @param {Function} [logOut] - Optional logout function. - * @returns {Promise} - */ -async function handleDismiss( - dismissType, - conformanceStore, - next, - toPath, - logOut, -) { - switch (dismissType) { - case "cancel": - resetTempCheckId(conformanceStore); - logOut ? logOut() : next(toPath); - break; - case "backdrop": - if (!logOut) { - next(false); + saveChanges: async () => { + if ( + conformanceStore.conformanceFilterCreateCheckId || + conformanceStore.conformanceLogCreateCheckId + ) { + await conformanceStore.updateConformance(); + if (conformanceStore.isUpdateConformance) { + await savedSuccessfully(conformanceStore.conformanceFileName); + } + } else { + await saveConformance(addConformanceCreateCheckId); } - break; - default: - break; - } -} - -/** - * Resets temporary conformance check IDs to null. - * @param {Object} conformanceStore - The conformance Pinia store. - */ -function resetTempCheckId(conformanceStore) { - conformanceStore.conformanceFilterTempCheckId = null; - conformanceStore.conformanceLogTempCheckId = null; + }, + navigateAfterSave: false, + beforeDismiss: null, + discardTemp: () => { + conformanceStore.conformanceFilterTempCheckId = null; + conformanceStore.conformanceLogTempCheckId = null; + }, + next: next, + toPath: toPath, + logOut: logOut, + }); } /** diff --git a/tests/unit/module/alertModal.test.js b/tests/unit/module/alertModal.test.js new file mode 100644 index 0000000..0e4ddab --- /dev/null +++ b/tests/unit/module/alertModal.test.js @@ -0,0 +1,353 @@ +// The Lucia project. +// Copyright 2026-2026 DSP, inc. All rights reserved. +// Authors: +// imacat.yang@dsp.im (imacat), 2026/7/5 +// AI assistance: Claude Code (Anthropic) + +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { http, HttpResponse } from "msw"; +import { server } from "@/mocks/node.js"; +import { findRequest, captureRequest } from "@/mocks/request-log.js"; + +vi.mock("@/module/apiError.js", () => ({ + default: vi.fn(), +})); +vi.mock("sweetalert2", () => ({ + default: { close: vi.fn(), fire: vi.fn() }, +})); + +import Swal from "sweetalert2"; +import { + saveFilter, + saveConformance, + leaveFilter, + leaveConformance, +} from "@/module/alertModal.js"; +import { useAllMapDataStore } from "@/stores/allMapData"; +import { useConformanceStore } from "@/stores/conformance"; +import { usePageAdminStore } from "@/stores/pageAdmin"; + +/** + * Makes Swal.fire simulate a confirmed dialog. If the dialog config + * has an input validator, it is invoked with the value first, the + * same way SweetAlert2 validates the input before resolving. + * + * @param {string} value - The input value the user typed. + */ +function fireConfirmedWithInput(value) { + Swal.fire.mockImplementation(async (config) => { + if (config.inputValidator) config.inputValidator(value); + return { value, isConfirmed: true }; + }); +} + +describe("alertModal", () => { + let pageAdminStore; + + beforeEach(() => { + setActivePinia(createPinia()); + pageAdminStore = usePageAdminStore(); + vi.spyOn(pageAdminStore, "keepPreviousPage"); + Swal.fire.mockReset(); + document.cookie = "luciaToken=fake-test-token"; + }); + + describe("saveFilter", () => { + it("prompts with the filter save dialog config", async () => { + fireConfirmedWithInput("My Filter"); + await saveFilter(vi.fn()); + expect(Swal.fire).toHaveBeenCalledWith( + expect.objectContaining({ + title: "SAVE NEW FILTER", + input: "text", + inputPlaceholder: "Enter Filter Name.", + icon: "info", + confirmButtonColor: "#0099FF", + showCancelButton: true, + cancelButtonColor: "#94a3b8", + }), + ); + }); + + it("rejects an empty input value", async () => { + fireConfirmedWithInput("My Filter"); + await saveFilter(vi.fn()); + const config = Swal.fire.mock.calls[0][0]; + expect(config.inputValidator("")).toBe("You need to write something!"); + expect(config.inputValidator("ok")).toBeUndefined(); + }); + + it("saves via the API function and notifies on confirm", async () => { + fireConfirmedWithInput("My Filter"); + const addFilterId = vi.fn(); + const saved = await saveFilter(addFilterId); + expect(saved).toBe(true); + expect(addFilterId).toHaveBeenCalledWith("My Filter"); + expect(Swal.fire).toHaveBeenCalledTimes(2); + expect(Swal.fire.mock.calls[1][0]).toMatchObject({ + title: "SAVE COMPLETE", + icon: "success", + }); + expect(Swal.fire.mock.calls[1][0].html).toContain("My Filter"); + expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled(); + }); + + it("keeps the previous page and stops navigation on cancel", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" }); + const addFilterId = vi.fn(); + const next = vi.fn(); + const saved = await saveFilter(addFilterId, next); + expect(saved).toBe(false); + expect(addFilterId).not.toHaveBeenCalled(); + expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(false); + }); + + it("cancels without a next() callback", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" }); + const saved = await saveFilter(vi.fn()); + expect(saved).toBe(false); + expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1); + }); + }); + + describe("saveConformance", () => { + it("prompts with the rule save dialog config", async () => { + fireConfirmedWithInput("My Rule"); + await saveConformance(vi.fn()); + expect(Swal.fire).toHaveBeenCalledWith( + expect.objectContaining({ + title: "SAVE NEW RULE", + input: "text", + inputPlaceholder: "Enter Rule Name.", + icon: "info", + confirmButtonColor: "#0099FF", + showCancelButton: true, + cancelButtonColor: "#94a3b8", + }), + ); + }); + + it("saves via the API function and notifies on confirm", async () => { + fireConfirmedWithInput("My Rule"); + const addCheckId = vi.fn(); + const saved = await saveConformance(addCheckId); + expect(saved).toBe(true); + expect(addCheckId).toHaveBeenCalledWith("My Rule"); + expect(Swal.fire).toHaveBeenCalledTimes(2); + expect(Swal.fire.mock.calls[1][0]).toMatchObject({ + title: "SAVE COMPLETE", + icon: "success", + }); + expect(Swal.fire.mock.calls[1][0].html).toContain("My Rule"); + }); + + it("returns false on cancel without touching the page admin", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" }); + const addCheckId = vi.fn(); + const saved = await saveConformance(addCheckId); + expect(saved).toBe(false); + expect(addCheckId).not.toHaveBeenCalled(); + expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled(); + }); + }); + + describe("leaveFilter", () => { + let allMapDataStore; + + beforeEach(() => { + allMapDataStore = useAllMapDataStore(); + }); + + it("shows the leave confirmation dialog config", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "esc" }); + await leaveFilter(vi.fn(), vi.fn(), "/files"); + expect(Swal.fire).toHaveBeenCalledWith( + expect.objectContaining({ + title: "SAVE YOUR FILTER?", + html: "If you want to continue using this filter in any other page, please select [Yes].", + icon: "warning", + confirmButtonText: "Yes", + cancelButtonText: "No", + confirmButtonColor: "#FF3366", + showCancelButton: true, + }), + ); + }); + + it("updates an existing filter and navigates on confirm", async () => { + server.use( + http.put("/api/filters/:id", ({ params }) => { + captureRequest("PUT", `/api/filters/${params.id}`); + return HttpResponse.json({}); + }), + ); + allMapDataStore.createFilterId = 5; + allMapDataStore.filterName = "My Filter"; + Swal.fire.mockResolvedValue({ isConfirmed: true }); + const next = vi.fn(); + await leaveFilter(next, vi.fn(), "/files"); + expect(findRequest("PUT", "/api/filters/5")).toHaveLength(1); + expect(Swal.fire.mock.calls[1][0]).toMatchObject({ + title: "SAVE COMPLETE", + }); + expect(Swal.fire.mock.calls[1][0].html).toContain("My Filter"); + expect(next).toHaveBeenCalledWith("/files"); + }); + + it("prompts to save a new filter and navigates on confirm", async () => { + Swal.fire.mockImplementationOnce(async () => ({ isConfirmed: true })); + Swal.fire.mockImplementationOnce(async (config) => { + config.inputValidator("Fresh Filter"); + return { value: "Fresh Filter", isConfirmed: true }; + }); + const addFilterId = vi.fn(); + const next = vi.fn(); + await leaveFilter(next, addFilterId, "/files"); + expect(Swal.fire.mock.calls[1][0]).toMatchObject({ + title: "SAVE NEW FILTER", + }); + expect(addFilterId).toHaveBeenCalledWith("Fresh Filter"); + expect(next).toHaveBeenCalledWith("/files"); + }); + + it("calls logOut instead of navigating on confirm", async () => { + server.use(http.put("/api/filters/:id", () => HttpResponse.json({}))); + allMapDataStore.createFilterId = 5; + allMapDataStore.filterName = "My Filter"; + Swal.fire.mockResolvedValue({ isConfirmed: true }); + const next = vi.fn(); + const logOut = vi.fn(); + await leaveFilter(next, vi.fn(), "/files", logOut); + expect(logOut).toHaveBeenCalledTimes(1); + expect(next).not.toHaveBeenCalled(); + }); + + it("discards the filter and navigates on cancel", async () => { + allMapDataStore.tempFilterId = 42; + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" }); + const next = vi.fn(); + await leaveFilter(next, vi.fn(), "/files"); + expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1); + expect(allMapDataStore.tempFilterId).toBeNull(); + expect(next).toHaveBeenCalledWith("/files"); + }); + + it("stays on the page on backdrop dismiss", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" }); + const next = vi.fn(); + await leaveFilter(next, vi.fn(), "/files"); + expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(false); + }); + + it("does nothing extra on backdrop dismiss when logging out", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" }); + const next = vi.fn(); + const logOut = vi.fn(); + await leaveFilter(next, vi.fn(), "/files", logOut); + expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1); + expect(next).not.toHaveBeenCalled(); + expect(logOut).not.toHaveBeenCalled(); + }); + }); + + describe("leaveConformance", () => { + let conformanceStore; + + beforeEach(() => { + conformanceStore = useConformanceStore(); + }); + + it("shows the leave confirmation dialog config", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "esc" }); + await leaveConformance(vi.fn(), vi.fn(), "/files"); + expect(Swal.fire).toHaveBeenCalledWith( + expect.objectContaining({ + title: "SAVE YOUR RULE?", + icon: "warning", + confirmButtonText: "Yes", + cancelButtonText: "No", + confirmButtonColor: "#FF3366", + showCancelButton: true, + }), + ); + expect(Swal.fire.mock.calls[0][0].html).toBeUndefined(); + }); + + it("updates an existing rule without navigating on confirm", async () => { + server.use( + http.put("/api/filter-checks/:id", ({ params }) => { + captureRequest("PUT", `/api/filter-checks/${params.id}`); + return HttpResponse.json({}); + }), + ); + conformanceStore.conformanceFilterCreateCheckId = 7; + conformanceStore.conformanceFileName = "My Rule"; + Swal.fire.mockResolvedValue({ isConfirmed: true }); + const next = vi.fn(); + await leaveConformance(next, vi.fn(), "/files"); + expect(findRequest("PUT", "/api/filter-checks/7")).toHaveLength(1); + expect(Swal.fire.mock.calls[1][0]).toMatchObject({ + title: "SAVE COMPLETE", + }); + expect(Swal.fire.mock.calls[1][0].html).toContain("My Rule"); + expect(next).not.toHaveBeenCalled(); + }); + + it("prompts to save a new rule without navigating on confirm", async () => { + Swal.fire.mockImplementationOnce(async () => ({ isConfirmed: true })); + Swal.fire.mockImplementationOnce(async (config) => { + config.inputValidator("Fresh Rule"); + return { value: "Fresh Rule", isConfirmed: true }; + }); + const addCheckId = vi.fn(); + const next = vi.fn(); + await leaveConformance(next, addCheckId, "/files"); + expect(Swal.fire.mock.calls[1][0]).toMatchObject({ + title: "SAVE NEW RULE", + }); + expect(addCheckId).toHaveBeenCalledWith("Fresh Rule"); + expect(next).not.toHaveBeenCalled(); + }); + + it("resets temporary check IDs and navigates on cancel", async () => { + conformanceStore.conformanceFilterTempCheckId = 3; + conformanceStore.conformanceLogTempCheckId = 4; + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" }); + const next = vi.fn(); + await leaveConformance(next, vi.fn(), "/files"); + expect(conformanceStore.conformanceFilterTempCheckId).toBeNull(); + expect(conformanceStore.conformanceLogTempCheckId).toBeNull(); + expect(next).toHaveBeenCalledWith("/files"); + expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled(); + }); + + it("calls logOut instead of navigating on cancel", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" }); + const next = vi.fn(); + const logOut = vi.fn(); + await leaveConformance(next, vi.fn(), "/files", logOut); + expect(logOut).toHaveBeenCalledTimes(1); + expect(next).not.toHaveBeenCalled(); + }); + + it("stays on the page on backdrop dismiss", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" }); + const next = vi.fn(); + await leaveConformance(next, vi.fn(), "/files"); + expect(next).toHaveBeenCalledWith(false); + expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled(); + }); + + it("does nothing on backdrop dismiss when logging out", async () => { + Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" }); + const next = vi.fn(); + const logOut = vi.fn(); + await leaveConformance(next, vi.fn(), "/files", logOut); + expect(next).not.toHaveBeenCalled(); + expect(logOut).not.toHaveBeenCalled(); + }); + }); +});