1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 10:54:44 +00:00

feat(configs/oxlint): add linter

This commit is contained in:
2026-02-14 22:49:47 +07:00
parent 2a5412c3b8
commit 49b9f2aa79
98 changed files with 1236 additions and 201 deletions

View File

@@ -1,4 +1,5 @@
import { isArray, isString, noop, type Arrayable, type VoidFunction } from '@robonen/stdlib';
import { isArray, isString, noop } from '@robonen/stdlib';
import type { Arrayable, VoidFunction } from '@robonen/stdlib';
import type { MaybeRefOrGetter } from 'vue';
import { defaultWindow } from '@/types';
@@ -9,9 +10,7 @@ interface InferEventTarget<Events> {
removeEventListener: (event: Events, listener?: any, options?: any) => any;
}
export interface GeneralEventListener<E = Event> {
(evt: E): void;
}
export type GeneralEventListener<E = Event> = (evt: E) => void;
export type WindowEventName = keyof WindowEventMap;
export type DocumentEventName = keyof DocumentEventMap;
@@ -84,7 +83,7 @@ export function useEventListener<Names extends string, EventType = Event>(
event: Arrayable<Names>,
listener: Arrayable<GeneralEventListener<EventType>>,
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
)
): VoidFunction;
/**
* @name useEventListener
@@ -104,13 +103,13 @@ export function useEventListener(...args: any[]) {
let target: MaybeRefOrGetter<EventTarget> | undefined;
let events: Arrayable<string>;
let listeners: Arrayable<Function>;
let options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined;
let _options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined;
if (isString(args[0]) || isArray(args[0])) {
[events, listeners, options] = args;
[events, listeners, _options] = args;
target = defaultWindow;
} else {
[target, events, listeners, options] = args;
[target, events, listeners, _options] = args;
}
if (!target)
@@ -124,13 +123,16 @@ export function useEventListener(...args: any[]) {
const cleanups: Function[] = [];
const cleanup = () => {
const _cleanup = () => {
cleanups.forEach(fn => fn());
cleanups.length = 0;
}
const register = (el: any, event: string, listener: any, options: any) => {
const _register = (el: any, event: string, listener: any, options: any) => {
el.addEventListener(event, listener, options);
return () => el.removeEventListener(event, listener, options);
}
void _cleanup;
void _register;
}

View File

@@ -17,7 +17,7 @@ const setupFocusGuard = (namespace?: string) => {
const getFocusGuards = (namespace: string) =>
document.querySelectorAll(`[data-${namespace}]`);
describe('useFocusGuard', () => {
describe(useFocusGuard, () => {
let component: ReturnType<typeof setupFocusGuard>;
const namespace = 'test-guard';
@@ -33,7 +33,7 @@ describe('useFocusGuard', () => {
component = setupFocusGuard(namespace);
const guards = getFocusGuards(namespace);
expect(guards.length).toBe(2);
expect(guards).toHaveLength(2);
guards.forEach((guard) => {
expect(guard.getAttribute('tabindex')).toBe('0');
@@ -46,7 +46,7 @@ describe('useFocusGuard', () => {
component.unmount();
expect(getFocusGuards(namespace).length).toBe(0);
expect(getFocusGuards(namespace)).toHaveLength(0);
});
it('correctly manage multiple instances with the same namespace', () => {
@@ -54,16 +54,16 @@ describe('useFocusGuard', () => {
const wrapper2 = setupFocusGuard(namespace);
// Guards should not be duplicated
expect(getFocusGuards(namespace).length).toBe(2);
expect(getFocusGuards(namespace)).toHaveLength(2);
wrapper1.unmount();
// Second instance still keeps the guards
expect(getFocusGuards(namespace).length).toBe(2);
expect(getFocusGuards(namespace)).toHaveLength(2);
wrapper2.unmount();
// No guards left after all instances are unmounted
expect(getFocusGuards(namespace).length).toBe(0);
expect(getFocusGuards(namespace)).toHaveLength(0);
});
});

View File

@@ -11,14 +11,14 @@ const ComponentStub = defineComponent({
},
},
setup(props) {
const isSupported = useSupported(() => props.location in window);
const isSupported = useSupported(() => props.location in globalThis);
return { isSupported };
},
template: `<div>{{ isSupported }}</div>`,
});
describe('useSupported', () => {
describe(useSupported, () => {
it('return whether the feature is supported', async () => {
const component = mount(ComponentStub);

View File

@@ -22,6 +22,7 @@ export function useSupported(feature: () => unknown) {
return computed(() => {
// add reactive dependency on isMounted
// eslint-disable-next-line no-unused-expressions
isMounted.value;
return Boolean(feature());

View File

@@ -3,7 +3,7 @@ import { computed, defineComponent, nextTick, ref, shallowRef } from 'vue';
import { mount } from '@vue/test-utils'
import { unrefElement } from '.';
describe('unrefElement', () => {
describe(unrefElement, () => {
it('returns a plain element when passed a raw element', () => {
const htmlEl = document.createElement('div');
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
@@ -47,14 +47,14 @@ describe('unrefElement', () => {
const result = unrefElement(childInstance);
expect(result).toBe(childInstance.$el);
expect((result as HTMLElement).classList.contains('child-el')).toBe(true);
expect((result as HTMLElement).classList.contains('child-el')).toBeTruthy();
});
it('handles null and undefined values', () => {
expect(unrefElement(undefined)).toBe(undefined);
expect(unrefElement(null)).toBe(null);
expect(unrefElement(ref(null))).toBe(null);
expect(unrefElement(ref(undefined))).toBe(undefined);
expect(unrefElement(ref<null>(null))).toBe(null);
expect(unrefElement(ref<undefined>(undefined))).toBe(undefined);
expect(unrefElement(() => null)).toBe(null);
expect(unrefElement(() => undefined)).toBe(undefined);
});

View File

@@ -14,7 +14,7 @@ const ComponentStub = defineComponent({
template: `<div>{{ visibleCount }}</div>`,
});
describe('useRenderCount', () => {
describe(useRenderCount, () => {
it('return the number of times the component has been rendered', async () => {
const component = mount(ComponentStub);

View File

@@ -1,4 +1,5 @@
import { onMounted, onUpdated, readonly, type ComponentInternalInstance } from 'vue';
import { onMounted, onUpdated, readonly } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useCounter } from '@/composables/state/useCounter';
import { getLifeCycleTarger } from '@/utils';

View File

@@ -26,7 +26,7 @@ const UnnamedComponentStub = defineComponent({
template: `<div>{{ visibleCount }}</div>`,
});
describe('useRenderInfo', () => {
describe(useRenderInfo, () => {
it('return uid if component name is not available', async () => {
const wrapper = mount(UnnamedComponentStub);
@@ -42,8 +42,8 @@ describe('useRenderInfo', () => {
expect(wrapper.vm.info.duration.value).toBeGreaterThan(0);
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
let lastRendered = wrapper.vm.info.lastRendered;
let duration = wrapper.vm.info.duration.value;
const lastRendered = wrapper.vm.info.lastRendered;
const duration = wrapper.vm.info.duration.value;
// Will not trigger a render
wrapper.vm.hiddenCount++;
@@ -76,8 +76,8 @@ describe('useRenderInfo', () => {
expect(info.duration.value).toBe(0);
expect(info.lastRendered).toBeGreaterThan(0);
let lastRendered = info.lastRendered;
let duration = info.duration.value;
const lastRendered = info.lastRendered;
const duration = info.duration.value;
// Will not trigger a render
wrapper.vm.hiddenCount++;

View File

@@ -1,5 +1,6 @@
import { timestamp } from '@robonen/stdlib';
import { onBeforeMount, onBeforeUpdate, onMounted, onUpdated, readonly, ref, type ComponentInternalInstance } from 'vue';
import { onBeforeMount, onBeforeUpdate, onMounted, onUpdated, readonly, ref } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useRenderCount } from '../useRenderCount';
import { getLifeCycleTarger } from '@/utils';
@@ -24,7 +25,7 @@ export function useRenderInfo(instance?: ComponentInternalInstance) {
const duration = ref(0);
let renderStartTime = 0;
const startMark = () => renderStartTime = performance.now();
const startMark = () => { renderStartTime = performance.now(); };
const endMark = () => {
duration.value = Math.max(performance.now() - renderStartTime, 0);
renderStartTime = 0;

View File

@@ -1,4 +1,5 @@
import { onBeforeMount, nextTick, type ComponentInternalInstance } from 'vue';
import { onBeforeMount, nextTick } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { getLifeCycleTarger } from '@/utils';
import type { VoidFunction } from '@robonen/stdlib';

View File

@@ -1,5 +1,6 @@
import { describe, it, vi, expect } from 'vitest';
import { defineComponent, nextTick, type PropType } from 'vue';
import { defineComponent, nextTick } from 'vue';
import type { PropType } from 'vue';
import { tryOnMounted } from '.';
import { mount } from '@vue/test-utils';
import type { VoidFunction } from '@robonen/stdlib';
@@ -11,12 +12,12 @@ const ComponentStub = defineComponent({
},
},
setup(props) {
props.callback && tryOnMounted(props.callback);
if (props.callback) { tryOnMounted(props.callback); }
},
template: `<div></div>`,
});
describe('tryOnMounted', () => {
describe(tryOnMounted, () => {
it('run the callback when mounted', () => {
const callback = vi.fn();

View File

@@ -1,4 +1,5 @@
import { onMounted, nextTick, type ComponentInternalInstance } from 'vue';
import { onMounted, nextTick } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { getLifeCycleTarger } from '@/utils';
import type { VoidFunction } from '@robonen/stdlib';

View File

@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { defineComponent, effectScope, type PropType } from 'vue';
import { defineComponent, effectScope } from 'vue';
import type { PropType } from 'vue';
import { tryOnScopeDispose } from '.';
import { mount } from '@vue/test-utils';
import type { VoidFunction } from '@robonen/stdlib';
@@ -17,12 +18,12 @@ const ComponentStub = defineComponent({
template: '<div></div>',
});
describe('tryOnScopeDispose', () => {
describe(tryOnScopeDispose, () => {
it('returns false when the scope is not active', () => {
const callback = vi.fn();
const detectedScope = tryOnScopeDispose(callback);
expect(detectedScope).toBe(false);
expect(detectedScope).toBeFalsy();
expect(callback).not.toHaveBeenCalled();
});
@@ -35,7 +36,7 @@ describe('tryOnScopeDispose', () => {
detectedScope = tryOnScopeDispose(callback);
});
expect(detectedScope).toBe(true);
expect(detectedScope).toBeTruthy();
expect(callback).not.toHaveBeenCalled();
scope.stop();

View File

@@ -12,7 +12,7 @@ const ComponentStub = defineComponent({
template: `<div>{{ isMounted }}</div>`,
});
describe('useMounted', () => {
describe(useMounted, () => {
it('return the mounted state of the component', async () => {
const component = mount(ComponentStub);

View File

@@ -1,4 +1,5 @@
import { onMounted, readonly, ref, type ComponentInternalInstance } from 'vue';
import { onMounted, readonly, ref } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { getLifeCycleTarger } from '@/utils';
/**
@@ -21,7 +22,7 @@ export function useMounted(instance?: ComponentInternalInstance) {
const isMounted = ref(false);
const targetInstance = getLifeCycleTarger(instance);
onMounted(() => isMounted.value = true, targetInstance);
onMounted(() => { isMounted.value = true; }, targetInstance);
return readonly(isMounted);
}

View File

@@ -2,7 +2,7 @@ import { ref, readonly, computed } from 'vue';
import { describe, it, expect } from 'vitest';
import { useClamp } from '.';
describe('useClamp', () => {
describe(useClamp, () => {
it('non-reactive values should be clamped', () => {
const clampedValue = useClamp(10, 0, 5);

View File

@@ -1,5 +1,6 @@
import { clamp, isFunction } from '@robonen/stdlib';
import { computed, isReadonly, ref, toValue, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type WritableComputedRef } from 'vue';
import { computed, isReadonly, ref, toValue } from 'vue';
import type { ComputedRef, MaybeRef, MaybeRefOrGetter, WritableComputedRef } from 'vue';
/**
* @name useClamp

View File

@@ -4,7 +4,7 @@ import { useCached } from '.';
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
describe('useCached', () => {
describe(useCached, () => {
it('default comparator', async () => {
const externalValue = ref(0);
const cachedValue = useCached(externalValue);

View File

@@ -1,4 +1,5 @@
import { ref, watch, toValue, type MaybeRefOrGetter, type Ref, type WatchOptions } from 'vue';
import { ref, watch, toValue } from 'vue';
import type { MaybeRefOrGetter, Ref, WatchOptions } from 'vue';
export type Comparator<Value> = (a: Value, b: Value) => boolean;

View File

@@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest';
import { useLastChanged } from '.';
import { timestamp } from '@robonen/stdlib';
describe('useLastChanged', () => {
describe(useLastChanged, () => {
it('initialize with null if no initialValue is provided', () => {
const source = ref(0);
const lastChanged = useLastChanged(source);

View File

@@ -1,5 +1,6 @@
import { timestamp } from '@robonen/stdlib';
import { ref, watch, type WatchSource, type WatchOptions, type Ref } from 'vue';
import { ref, watch } from 'vue';
import type { WatchSource, WatchOptions, Ref } from 'vue';
export interface UseLastChangedOptions<
Immediate extends boolean,
@@ -32,7 +33,7 @@ export function useLastChanged(source: WatchSource, options: UseLastChangedOptio
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<boolean, any> = {}): Ref<number | null> | Ref<number> {
const lastChanged = ref<number | null>(options.initialValue ?? null);
watch(source, () => lastChanged.value = timestamp(), options);
watch(source, () => { lastChanged.value = timestamp(); }, options);
return lastChanged;
}

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { ref } from 'vue';
import { useSyncRefs } from '.';
describe('useSyncRefs', () => {
describe(useSyncRefs, () => {
it('sync the value of a source ref with multiple target refs', () => {
const source = ref(0);
const target1 = ref(0);

View File

@@ -1,4 +1,5 @@
import { watch, type Ref, type WatchOptions, type WatchSource } from 'vue';
import { watch } from 'vue';
import type { Ref, WatchOptions, WatchSource } from 'vue';
import { isArray } from '@robonen/stdlib';
/**
@@ -40,7 +41,7 @@ export function useSyncRefs<T = unknown>(
return watch(
source,
(value) => targets.forEach((target) => target.value = value),
(value) => targets.forEach((target) => { target.value = value; }),
{ flush, deep, immediate },
);
}

View File

@@ -2,7 +2,7 @@ import { describe, it, vi, expect } from 'vitest';
import { ref, reactive } from 'vue';
import { useAppSharedState } from '.';
describe('useAppSharedState', () => {
describe(useAppSharedState, () => {
it('initialize state only once', () => {
const stateFactory = (initValue?: number) => {
const count = ref(initValue ?? 0);

View File

@@ -2,7 +2,7 @@ import { isShallow, nextTick, ref } from 'vue';
import { it, expect, describe, vi, beforeEach, afterEach } from 'vitest';
import { useAsyncState } from '.';
describe('useAsyncState', () => {
describe(useAsyncState, () => {
beforeEach(() => {
vi.useFakeTimers();
});
@@ -18,15 +18,15 @@ describe('useAsyncState', () => {
);
expect(state.value).toBe('initial');
expect(isReady.value).toBe(false);
expect(isLoading.value).toBe(true);
expect(isReady.value).toBeFalsy();
expect(isLoading.value).toBeTruthy();
expect(error.value).toBe(null);
await nextTick();
expect(state.value).toBe('data');
expect(isReady.value).toBe(true);
expect(isLoading.value).toBe(false);
expect(isReady.value).toBeTruthy();
expect(isLoading.value).toBeFalsy();
expect(error.value).toBe(null);
});
@@ -37,15 +37,15 @@ describe('useAsyncState', () => {
);
expect(state.value).toBe('initial');
expect(isReady.value).toBe(false);
expect(isLoading.value).toBe(true);
expect(isReady.value).toBeFalsy();
expect(isLoading.value).toBeTruthy();
expect(error.value).toBe(null);
await nextTick();
expect(state.value).toBe('data');
expect(isReady.value).toBe(true);
expect(isLoading.value).toBe(false);
expect(isReady.value).toBeTruthy();
expect(isLoading.value).toBeFalsy();
expect(error.value).toBe(null);
});
@@ -56,15 +56,15 @@ describe('useAsyncState', () => {
);
expect(state.value).toBe('initial');
expect(isReady.value).toBe(false);
expect(isLoading.value).toBe(true);
expect(isReady.value).toBeFalsy();
expect(isLoading.value).toBeTruthy();
expect(error.value).toBe(null);
await nextTick();
expect(state.value).toBe('initial');
expect(isReady.value).toBe(false);
expect(isLoading.value).toBe(false);
expect(isReady.value).toBeFalsy();
expect(isLoading.value).toBeFalsy();
expect(error.value).toEqual(new Error('test-error'));
});
@@ -131,14 +131,14 @@ describe('useAsyncState', () => {
);
const promise = execute();
expect(isLoading.value).toBe(true);
expect(isLoading.value).toBeTruthy();
await vi.advanceTimersByTimeAsync(50);
expect(isLoading.value).toBe(true);
expect(isLoading.value).toBeTruthy();
await vi.advanceTimersByTimeAsync(50);
await promise;
expect(isLoading.value).toBe(false);
expect(isLoading.value).toBeFalsy();
});
it('is awaitable', async () => {
@@ -160,15 +160,15 @@ describe('useAsyncState', () => {
executeImmediately();
expect(state.value).toBe('initial');
expect(isLoading.value).toBe(true);
expect(isReady.value).toBe(false);
expect(isLoading.value).toBeTruthy();
expect(isReady.value).toBeFalsy();
expect(error.value).toBe(null);
await nextTick();
expect(state.value).toBe('data');
expect(isReady.value).toBe(true);
expect(isLoading.value).toBe(false);
expect(isReady.value).toBeTruthy();
expect(isLoading.value).toBeFalsy();
expect(error.value).toBe(null);
});
@@ -193,7 +193,7 @@ describe('useAsyncState', () => {
);
expect(state.value.a).toBe(1);
expect(isShallow(state)).toBe(true);
expect(isShallow(state)).toBeTruthy();
});
it('uses ref when shallow is false', async () => {
@@ -204,6 +204,6 @@ describe('useAsyncState', () => {
);
expect(state.value.a).toBe(1);
expect(isShallow(state)).toBe(false);
expect(isShallow(state)).toBeFalsy();
});
});

View File

@@ -1,4 +1,5 @@
import { ref, shallowRef, watch, type Ref, type ShallowRef, type UnwrapRef } from 'vue';
import { ref, shallowRef, watch } from 'vue';
import type { Ref, ShallowRef, UnwrapRef } from 'vue';
import { isFunction, sleep } from '@robonen/stdlib';
export interface UseAsyncStateOptions<Shallow extends boolean, Data = any> {
@@ -103,8 +104,12 @@ export function useAsyncState<Data, Params extends any[] = [], Shallow extends b
watch(
isLoading,
(loading) => {
if (loading === false)
error.value ? reject(error.value) : resolve(shell);
if (loading === false) {
if (error.value)
reject(error.value);
else
resolve(shell);
}
},
{
immediate: true,
@@ -117,6 +122,7 @@ export function useAsyncState<Data, Params extends any[] = [], Shallow extends b
return {
...shell,
// eslint-disable-next-line unicorn/no-thenable
then(onFulfilled, onRejected) {
return waitResolve().then(onFulfilled, onRejected);
},

View File

@@ -35,7 +35,7 @@ function testFactory<Data>(
// TODO: maybe replace template with passing mock functions to setup
describe('useContextFactory', () => {
describe(useContextFactory, () => {
it('provide and inject context correctly', () => {
const { Parent } = testFactory('test', useContextFactory('TestContext'));

View File

@@ -1,4 +1,5 @@
import { inject as vueInject, provide as vueProvide, type InjectionKey, type App } from 'vue';
import { inject as vueInject, provide as vueProvide } from 'vue';
import type { InjectionKey, App } from 'vue';
import { VueToolsError } from '@/utils';
/**

View File

@@ -2,7 +2,7 @@ import { it, expect, describe } from 'vitest';
import { ref } from 'vue';
import { useCounter } from '.';
describe('useCounter', () => {
describe(useCounter, () => {
it('initialize count with the provided initial value', () => {
const { count } = useCounter(5);
expect(count.value).toBe(5);

View File

@@ -1,4 +1,5 @@
import { ref, toValue, type MaybeRefOrGetter, type Ref } from 'vue';
import { ref, toValue } from 'vue';
import type { MaybeRefOrGetter, Ref } from 'vue';
import { clamp } from '@robonen/stdlib';
export interface UseCounterOptions {
@@ -46,14 +47,17 @@ export function useCounter(
max = Number.MAX_SAFE_INTEGER,
} = options;
const increment = (delta = 1) =>
const increment = (delta = 1) => {
count.value = clamp(count.value + delta, min, max);
};
const decrement = (delta = 1) =>
const decrement = (delta = 1) => {
count.value = clamp(count.value - delta, min, max);
};
const set = (value: number) =>
const set = (value: number) => {
count.value = clamp(value, min, max);
};
const get = () => count.value;

View File

@@ -1,5 +1,5 @@
import { useContextFactory } from '../useContextFactory';
import type { App, InjectionKey } from 'vue';
import type { App } from 'vue';
export interface useInjectionStoreOptions<Return> {
injectionName?: string;

View File

@@ -2,56 +2,56 @@ import { it, expect, describe } from 'vitest';
import { ref } from 'vue';
import { useToggle } from '.';
describe('useToggle', () => {
describe(useToggle, () => {
it('initialize with false by default', () => {
const { value } = useToggle();
expect(value.value).toBe(false);
expect(value.value).toBeFalsy();
});
it('initialize with the provided initial value', () => {
const { value } = useToggle(true);
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
});
it('initialize with the provided initial value from a ref', () => {
const { value } = useToggle(ref(true));
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
});
it('toggle from false to true', () => {
const { value, toggle } = useToggle(false);
toggle();
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
});
it('toggle from true to false', () => {
const { value, toggle } = useToggle(true);
toggle();
expect(value.value).toBe(false);
expect(value.value).toBeFalsy();
});
it('toggle multiple times', () => {
const { value, toggle } = useToggle(false);
toggle();
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
toggle();
expect(value.value).toBe(false);
expect(value.value).toBeFalsy();
toggle();
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
});
it('toggle returns the new value', () => {
const { toggle } = useToggle(false);
expect(toggle()).toBe(true);
expect(toggle()).toBe(false);
expect(toggle()).toBeTruthy();
expect(toggle()).toBeFalsy();
});
it('set a specific value via toggle', () => {
const { value, toggle } = useToggle(false);
toggle(true);
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
toggle(true);
expect(value.value).toBe(true);
expect(value.value).toBeTruthy();
});
it('use custom truthy and falsy values', () => {

View File

@@ -1,4 +1,5 @@
import { ref, toValue, type MaybeRefOrGetter, type MaybeRef, type Ref } from 'vue';
import { ref, toValue } from 'vue';
import type { MaybeRefOrGetter, MaybeRef, Ref } from 'vue';
export interface UseToggleOptions<Truthy, Falsy> {
truthyValue?: MaybeRefOrGetter<Truthy>,

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { nextTick } from 'vue';
import { useLocalStorage } from '.';
describe('useLocalStorage', () => {
describe(useLocalStorage, () => {
beforeEach(() => {
localStorage.clear();
});

View File

@@ -1,7 +1,8 @@
import type { MaybeRefOrGetter, Ref } from 'vue';
import { defaultWindow } from '@/types';
import { VueToolsError } from '@/utils/error';
import { useStorage, type UseStorageOptions } from '../useStorage';
import { useStorage } from '../useStorage';
import type { UseStorageOptions } from '../useStorage';
/**
* @name useLocalStorage

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { nextTick } from 'vue';
import { useSessionStorage } from '.';
describe('useSessionStorage', () => {
describe(useSessionStorage, () => {
beforeEach(() => {
sessionStorage.clear();
});

View File

@@ -1,7 +1,8 @@
import type { MaybeRefOrGetter, Ref } from 'vue';
import { defaultWindow } from '@/types';
import { VueToolsError } from '@/utils/error';
import { useStorage, type UseStorageOptions } from '../useStorage';
import { useStorage } from '../useStorage';
import type { UseStorageOptions } from '../useStorage';
/**
* @name useSessionStorage

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { nextTick } from 'vue';
import { useStorage, StorageSerializers, type StorageLike } from '.';
import { useStorage, StorageSerializers } from '.';
import type { StorageLike } from '.';
function createMockStorage(): StorageLike & { store: Map<string, string> } {
const store = new Map<string, string>();
@@ -13,7 +14,7 @@ function createMockStorage(): StorageLike & { store: Map<string, string> } {
};
}
describe('useStorage', () => {
describe(useStorage, () => {
// --- Basic types ---
it('stores and reads a string', async () => {
@@ -45,7 +46,7 @@ describe('useStorage', () => {
const storage = createMockStorage();
const state = useStorage<boolean>('test-bool', true, storage);
expect(state.value).toBe(true);
expect(state.value).toBeTruthy();
state.value = false;
await nextTick();

View File

@@ -1,4 +1,5 @@
import { ref, shallowRef, watch, toValue, type Ref, type MaybeRefOrGetter } from 'vue';
import { ref, shallowRef, watch, toValue } from 'vue';
import type { Ref, MaybeRefOrGetter } from 'vue';
import { isBoolean, isNumber, isString, isObject, isMap, isSet, isDate } from '@robonen/stdlib';
import type { ConfigurableFlush } from '@/types';
@@ -145,7 +146,7 @@ export function useStorage<T>(
flush = 'pre',
writeDefaults = true,
mergeDefaults = false,
onError = console.error,
onError = console.error, // eslint-disable-line no-console
} = options;
const defaults = toValue(initialValue);
@@ -156,8 +157,8 @@ export function useStorage<T>(
function read(): T {
const raw = storage.getItem(key);
if (raw == null) {
if (writeDefaults && defaults != null) {
if (raw === undefined || raw === null) {
if (writeDefaults && defaults !== undefined && defaults !== null) {
try {
storage.setItem(key, serializer.write(defaults));
} catch (e) {
@@ -188,7 +189,7 @@ export function useStorage<T>(
try {
const oldValue = storage.getItem(key);
if (value == null) {
if (value === undefined || value === null) {
storage.removeItem(key);
} else {
const serialized = serializer.write(value);

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { nextTick } from 'vue';
import { useStorageAsync, type StorageLikeAsync } from '.';
import { useStorageAsync } from '.';
import type { StorageLikeAsync } from '.';
function createMockAsyncStorage(): StorageLikeAsync & { store: Map<string, string> } {
const store = new Map<string, string>();
@@ -24,7 +25,7 @@ function createDelayedAsyncStorage(delay: number): StorageLikeAsync & { store: M
};
}
describe('useStorageAsync', () => {
describe(useStorageAsync, () => {
// --- Basic read/write ---
it('returns default value before storage is ready', () => {
@@ -32,7 +33,7 @@ describe('useStorageAsync', () => {
const { state, isReady } = useStorageAsync('key', 'default', storage);
expect(state.value).toBe('default');
expect(isReady.value).toBe(false);
expect(isReady.value).toBeFalsy();
});
it('reads existing value from async storage', async () => {
@@ -42,7 +43,7 @@ describe('useStorageAsync', () => {
const { state, isReady } = await useStorageAsync('key', 'default', storage);
expect(state.value).toBe('stored');
expect(isReady.value).toBe(true);
expect(isReady.value).toBeTruthy();
});
it('writes value to async storage on change', async () => {
@@ -81,7 +82,7 @@ describe('useStorageAsync', () => {
const { state } = await useStorageAsync('flag', false, storage);
expect(state.value).toBe(true);
expect(state.value).toBeTruthy();
state.value = false;
await nextTick();
@@ -108,7 +109,7 @@ describe('useStorageAsync', () => {
const { state, isReady } = await useStorageAsync('delayed', 'default', storage);
expect(state.value).toBe('loaded');
expect(isReady.value).toBe(true);
expect(isReady.value).toBeTruthy();
});
// --- onReady callback ---
@@ -201,7 +202,7 @@ describe('useStorageAsync', () => {
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(storage.store.has('nullable')).toBe(false);
expect(storage.store.has('nullable')).toBeFalsy();
});
// --- Error handling ---
@@ -321,7 +322,7 @@ describe('useStorageAsync', () => {
await useStorageAsync('new-key', 'default-val', storage, { writeDefaults: false });
expect(storage.store.has('new-key')).toBe(false);
expect(storage.store.has('new-key')).toBeFalsy();
});
it('does not overwrite existing value with defaults', async () => {

View File

@@ -1,4 +1,5 @@
import { ref, shallowRef, watch, toValue, type Ref, type ShallowRef, type MaybeRefOrGetter, type UnwrapRef } from 'vue';
import { ref, shallowRef, watch, toValue } from 'vue';
import type { Ref, ShallowRef, MaybeRefOrGetter, UnwrapRef } from 'vue';
import type { ConfigurableFlush } from '@/types';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
import { guessSerializer, shallowMerge } from '../useStorage';
@@ -100,7 +101,7 @@ export function useStorageAsync<T, Shallow extends boolean = true>(
writeDefaults = true,
mergeDefaults = false,
onReady,
onError = console.error,
onError = console.error, // eslint-disable-line no-console
} = options;
const defaults = toValue(initialValue);
@@ -113,8 +114,8 @@ export function useStorageAsync<T, Shallow extends boolean = true>(
try {
const raw = await storage.getItem(key);
if (raw == null) {
if (writeDefaults && defaults != null) {
if (raw === undefined || raw === null) {
if (writeDefaults && defaults !== undefined && defaults !== null) {
try {
await storage.setItem(key, await serializer.write(defaults));
} catch (e) {
@@ -142,7 +143,7 @@ export function useStorageAsync<T, Shallow extends boolean = true>(
async function write(value: T) {
try {
if (value == null) {
if (value === undefined || value === null) {
await storage.removeItem(key);
} else {
const raw = await serializer.write(value);
@@ -179,6 +180,7 @@ export function useStorageAsync<T, Shallow extends boolean = true>(
return {
...shell,
// eslint-disable-next-line unicorn/no-thenable
then(onFulfilled, onRejected) {
return readyPromise.then(onFulfilled, onRejected);
},

View File

@@ -2,14 +2,14 @@ import { describe, it, expect, vi } from 'vitest';
import { nextTick, ref } from 'vue';
import { useOffsetPagination } from '.';
describe('useOffsetPagination', () => {
describe(useOffsetPagination, () => {
it('initialize with default values without options', () => {
const { currentPage, currentPageSize, totalPages, isFirstPage } = useOffsetPagination({});
expect(currentPage.value).toBe(1);
expect(currentPageSize.value).toBe(10);
expect(totalPages.value).toBe(Infinity);
expect(isFirstPage.value).toBe(true);
expect(isFirstPage.value).toBeTruthy();
});
it('calculate total pages correctly', () => {
@@ -51,14 +51,14 @@ describe('useOffsetPagination', () => {
const { currentPage, isFirstPage, isLastPage } = useOffsetPagination({ total: 20, pageSize: 10 });
expect(currentPage.value).toBe(1);
expect(isFirstPage.value).toBe(true);
expect(isLastPage.value).toBe(false);
expect(isFirstPage.value).toBeTruthy();
expect(isLastPage.value).toBeFalsy();
currentPage.value = 2;
expect(currentPage.value).toBe(2);
expect(isFirstPage.value).toBe(false);
expect(isLastPage.value).toBe(true);
expect(isFirstPage.value).toBeFalsy();
expect(isLastPage.value).toBeTruthy();
});
it('call onPageChange callback', async () => {

View File

@@ -1,5 +1,6 @@
import type { VoidFunction } from '@robonen/stdlib';
import { computed, reactive, toValue, watch, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type UnwrapNestedRefs, type WritableComputedRef } from 'vue';
import { computed, reactive, toValue, watch } from 'vue';
import type { ComputedRef, MaybeRef, MaybeRefOrGetter, UnwrapNestedRefs, WritableComputedRef } from 'vue';
import { useClamp } from '@/composables/math/useClamp';
// TODO: sync returned refs with passed refs
@@ -89,7 +90,7 @@ export function useOffsetPagination(options: UseOffsetPaginationOptions): UseOff
const next = () => currentPage.value++;
const previous = () => currentPage.value--;
const select = (page: number) => currentPage.value = page;
const select = (page: number) => { currentPage.value = page; };
const returnValue = {
currentPage,