ytptube/ui/tests/composables/useDirtyCloseGuard.test.ts
2026-05-04 22:23:19 +03:00

178 lines
4.8 KiB
TypeScript

import { beforeAll, beforeEach, afterEach, describe, expect, it, mock } from 'bun:test';
import { ref } from 'vue';
type DialogResult = { status: boolean; value: null };
type RouteGuard = () => Promise<boolean> | boolean;
let beforeRouteLeaveHandler: RouteGuard | null = null;
let beforeRouteUpdateHandler: RouteGuard | null = null;
let unmountHandlers: Array<() => void> = [];
const confirmDialogMock = mock<() => Promise<DialogResult>>(() =>
Promise.resolve({ status: true, value: null }),
);
mock.module('#imports', () => ({
onBeforeRouteLeave: (handler: RouteGuard) => {
beforeRouteLeaveHandler = handler;
},
onBeforeRouteUpdate: (handler: RouteGuard) => {
beforeRouteUpdateHandler = handler;
},
onBeforeUnmount: (handler: () => void) => {
unmountHandlers.push(handler);
},
}));
mock.module('~/composables/useDialog', () => ({
useDialog: () => ({
confirmDialog: confirmDialogMock,
}),
}));
let useDirtyCloseGuard: typeof import('~/composables/useDirtyCloseGuard').useDirtyCloseGuard;
beforeAll(async () => {
({ useDirtyCloseGuard } = await import('~/composables/useDirtyCloseGuard'));
});
beforeEach(() => {
beforeRouteLeaveHandler = null;
beforeRouteUpdateHandler = null;
unmountHandlers = [];
confirmDialogMock.mockReset();
confirmDialogMock.mockImplementation(() => Promise.resolve({ status: true, value: null }));
});
afterEach(() => {
for (const handler of unmountHandlers) {
handler();
}
beforeRouteLeaveHandler = null;
beforeRouteUpdateHandler = null;
unmountHandlers = [];
});
describe('useDirtyCloseGuard', () => {
it('confirm_route_leave', async () => {
const open = ref(true);
const dirty = ref(true);
const onDiscard = mock(() => {});
useDirtyCloseGuard(open, {
dirty,
onDiscard,
});
expect(beforeRouteLeaveHandler).not.toBeNull();
const allowed = await beforeRouteLeaveHandler?.();
expect(allowed).toBe(true);
expect(confirmDialogMock).toHaveBeenCalledTimes(1);
expect(onDiscard).toHaveBeenCalledTimes(1);
expect(open.value).toBe(false);
});
it('block_route_update', async () => {
const open = ref(true);
const dirty = ref(true);
confirmDialogMock.mockImplementationOnce(() => Promise.resolve({ status: false, value: null }));
useDirtyCloseGuard(open, {
dirty,
});
expect(beforeRouteUpdateHandler).not.toBeNull();
const allowed = await beforeRouteUpdateHandler?.();
expect(allowed).toBe(false);
expect(confirmDialogMock).toHaveBeenCalledTimes(1);
expect(open.value).toBe(true);
});
it('guard_beforeunload', () => {
const open = ref(true);
const dirty = ref(true);
useDirtyCloseGuard(open, {
dirty,
});
const guardedEvent = new window.Event('beforeunload', { cancelable: true });
const guardedResult = window.dispatchEvent(guardedEvent);
expect(guardedResult).toBe(false);
expect(guardedEvent.defaultPrevented).toBe(true);
dirty.value = false;
const cleanEvent = new window.Event('beforeunload', { cancelable: true });
const cleanResult = window.dispatchEvent(cleanEvent);
expect(cleanResult).toBe(true);
expect(cleanEvent.defaultPrevented).toBe(false);
for (const handler of unmountHandlers) {
handler();
}
unmountHandlers = [];
dirty.value = true;
const afterUnmountEvent = new window.Event('beforeunload', { cancelable: true });
const afterUnmountResult = window.dispatchEvent(afterUnmountEvent);
expect(afterUnmountResult).toBe(true);
expect(afterUnmountEvent.defaultPrevented).toBe(false);
});
it('dedupe_close_prompt', async () => {
const open = ref(true);
const dirty = ref(true);
const onDiscard = mock(() => {});
let resolveDialog: (result: DialogResult | PromiseLike<DialogResult>) => void = () => {
throw new Error('Expected the discard dialog promise to be pending.');
};
confirmDialogMock.mockImplementationOnce(
() =>
new Promise<DialogResult>((resolve) => {
resolveDialog = resolve;
}),
);
const guard = useDirtyCloseGuard(open, {
dirty,
onDiscard,
});
const first = guard.requestClose();
const second = guard.requestClose();
expect(confirmDialogMock).toHaveBeenCalledTimes(1);
resolveDialog({ status: true, value: null });
expect(await first).toBe(true);
expect(await second).toBe(true);
expect(onDiscard).toHaveBeenCalledTimes(1);
expect(open.value).toBe(false);
});
it('skip_clean_editor', async () => {
const open = ref(true);
const dirty = ref(false);
useDirtyCloseGuard(open, {
dirty,
});
expect(await beforeRouteLeaveHandler?.()).toBe(true);
expect(await beforeRouteUpdateHandler?.()).toBe(true);
expect(confirmDialogMock).not.toHaveBeenCalled();
expect(open.value).toBe(true);
});
});