import type { FormulaRule } from '#/api/x/eln/formulas';
|
|
import { flushPromises, mount } from '@vue/test-utils';
|
import { defineComponent } from 'vue';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { useFormulaValidation } from '../useFormulaValidation';
|
|
const apiMocks = vi.hoisted(() => ({ validateFormula: vi.fn() }));
|
|
vi.mock('#/api/x/eln/formulas', () => ({ validateFormula: apiMocks.validateFormula }));
|
|
function deferred<T>() {
|
let resolve!: (value: T) => void;
|
const promise = new Promise<T>((promiseResolve) => {
|
resolve = promiseResolve;
|
});
|
return { promise, resolve };
|
}
|
|
function rule(expression = 'a + b'): FormulaRule {
|
return { biaodashi: expression, jieguoLeixing: 'DECIMAL', parameters: [] };
|
}
|
|
function mountValidation() {
|
let validation!: ReturnType<typeof useFormulaValidation>;
|
const wrapper = mount(
|
defineComponent({
|
setup() {
|
validation = useFormulaValidation();
|
return () => null;
|
},
|
}),
|
);
|
return { validation, wrapper };
|
}
|
|
describe('useFormulaValidation', () => {
|
afterEach(() => {
|
vi.useRealTimers();
|
vi.clearAllMocks();
|
});
|
|
it('waits 400ms before validating a scheduled edit', async () => {
|
vi.useFakeTimers();
|
apiMocks.validateFormula.mockResolvedValue({ data: { diagnostics: [], latex: 'a+b' } });
|
const { validation } = mountValidation();
|
|
validation.schedule(rule());
|
await vi.advanceTimersByTimeAsync(399);
|
expect(apiMocks.validateFormula).not.toHaveBeenCalled();
|
await vi.advanceTimersByTimeAsync(1);
|
await flushPromises();
|
|
expect(apiMocks.validateFormula).toHaveBeenCalledTimes(1);
|
});
|
|
it('aborts an active request when a newer validation starts', async () => {
|
const first = deferred<any>();
|
const second = deferred<any>();
|
apiMocks.validateFormula
|
.mockImplementationOnce((_rule: FormulaRule, signal: AbortSignal) => {
|
expect(signal.aborted).toBe(false);
|
return first.promise;
|
})
|
.mockImplementationOnce(() => second.promise);
|
const { validation } = mountValidation();
|
|
void validation.validateNow(rule('a'));
|
await Promise.resolve();
|
void validation.validateNow(rule('b'));
|
await Promise.resolve();
|
|
expect(apiMocks.validateFormula.mock.calls[0]![1]!.aborted).toBe(true);
|
second.resolve({ data: { diagnostics: [], latex: 'b' } });
|
first.resolve({ data: { diagnostics: [], latex: 'a' } });
|
await flushPromises();
|
});
|
|
it('drops a late response from an older request', async () => {
|
const first = deferred<any>();
|
const second = deferred<any>();
|
apiMocks.validateFormula.mockImplementationOnce(() => first.promise).mockImplementationOnce(() => second.promise);
|
const { validation } = mountValidation();
|
|
void validation.validateNow(rule('a'));
|
void validation.validateNow(rule('b'));
|
second.resolve({ data: { diagnostics: [], latex: 'b' } });
|
await flushPromises();
|
first.resolve({ data: { diagnostics: [], latex: 'a' } });
|
await flushPromises();
|
|
expect(validation.result.value?.latex).toBe('b');
|
});
|
|
it('runs a manual validation immediately and cancels the pending debounce', async () => {
|
vi.useFakeTimers();
|
apiMocks.validateFormula.mockResolvedValue({ data: { diagnostics: [], latex: 'manual' } });
|
const { validation } = mountValidation();
|
|
validation.schedule(rule('scheduled'));
|
await validation.validateNow(rule('manual'));
|
await vi.advanceTimersByTimeAsync(400);
|
|
expect(apiMocks.validateFormula).toHaveBeenCalledTimes(1);
|
expect(apiMocks.validateFormula).toHaveBeenCalledWith(rule('manual'), expect.any(AbortSignal));
|
});
|
|
it('invalidates an active validation as soon as a new edit enters the debounce window', async () => {
|
vi.useFakeTimers();
|
const oldResponse = deferred<any>();
|
apiMocks.validateFormula.mockImplementationOnce((_rule: FormulaRule, signal: AbortSignal) => {
|
expect(signal.aborted).toBe(false);
|
return oldResponse.promise;
|
});
|
const { validation } = mountValidation();
|
|
void validation.validateNow(rule('old_expression'));
|
await Promise.resolve();
|
const oldSignal = apiMocks.validateFormula.mock.calls[0]![1] as AbortSignal;
|
validation.schedule(rule('new_expression'));
|
oldResponse.resolve({ data: { diagnostics: [], latex: 'old_latex' } });
|
await flushPromises();
|
|
expect(oldSignal.aborted).toBe(true);
|
expect(validation.result.value).toBeUndefined();
|
});
|
|
it('keeps a visible retryable failure state instead of rejecting a scheduled validation', async () => {
|
apiMocks.validateFormula
|
.mockRejectedValueOnce(new Error('validation service unavailable'))
|
.mockResolvedValueOnce({ data: { diagnostics: [], latex: 'retry' } });
|
const { validation } = mountValidation();
|
|
await expect(validation.validateNow(rule('first'))).resolves.toBeUndefined();
|
|
expect(validation.error.value).toBe('validation service unavailable');
|
expect(validation.result.value).toEqual({ diagnostics: [], latex: null });
|
await validation.retry();
|
|
expect(apiMocks.validateFormula).toHaveBeenCalledTimes(2);
|
expect(validation.error.value).toBe('');
|
expect(validation.result.value?.latex).toBe('retry');
|
});
|
|
it('clears its timer and aborts an active request on unmount', async () => {
|
vi.useFakeTimers();
|
const pending = deferred<any>();
|
apiMocks.validateFormula.mockReturnValue(pending.promise);
|
const { validation, wrapper } = mountValidation();
|
|
validation.schedule(rule('scheduled'));
|
void validation.validateNow(rule('active'));
|
await Promise.resolve();
|
const signal = apiMocks.validateFormula.mock.calls[0]![1] as AbortSignal;
|
wrapper.unmount();
|
await vi.advanceTimersByTimeAsync(400);
|
|
expect(signal.aborted).toBe(true);
|
expect(apiMocks.validateFormula).toHaveBeenCalledTimes(1);
|
});
|
});
|