fix(vue): eslint/tsconfig migration + resolve type errors
@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig; fix composable + test type errors (writable computed returns, null guards, overload-compatible signatures, typed test helpers) — all type-level.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { effectScope, ref } from 'vue';
|
||||
import { useShare } from '.';
|
||||
import type { UseShareOptions } from '.';
|
||||
|
||||
function stubShareNavigator(canShareResult = true) {
|
||||
const share = vi.fn(async (_data?: UseShareOptions) => {});
|
||||
const canShare = vi.fn((_data?: UseShareOptions) => canShareResult);
|
||||
const navigator = { share, canShare } as unknown as Navigator;
|
||||
return { navigator, share, canShare };
|
||||
}
|
||||
|
||||
function withScope<T>(fn: () => T): { result: T; stop: () => void } {
|
||||
const scope = effectScope();
|
||||
let result!: T;
|
||||
scope.run(() => {
|
||||
result = fn();
|
||||
});
|
||||
return { result, stop: () => scope.stop() };
|
||||
}
|
||||
|
||||
describe(useShare, () => {
|
||||
it('reports supported when the Web Share API is present', () => {
|
||||
const { navigator } = stubShareNavigator();
|
||||
const { result, stop } = withScope(() => useShare({}, { navigator }));
|
||||
expect(result.isSupported.value).toBeTruthy();
|
||||
stop();
|
||||
});
|
||||
|
||||
it('reports unsupported when canShare is missing', () => {
|
||||
const navigator = {} as Navigator;
|
||||
const { result, stop } = withScope(() => useShare({}, { navigator }));
|
||||
expect(result.isSupported.value).toBeFalsy();
|
||||
stop();
|
||||
});
|
||||
|
||||
it('calls navigator.share with the default share options', async () => {
|
||||
const { navigator, share, canShare } = stubShareNavigator();
|
||||
const data = { title: 'Hello', text: 'World', url: 'https://example.com' };
|
||||
const { result, stop } = withScope(() => useShare(data, { navigator }));
|
||||
|
||||
await result.share();
|
||||
|
||||
expect(canShare).toHaveBeenCalledWith(data);
|
||||
expect(share).toHaveBeenCalledWith(data);
|
||||
stop();
|
||||
});
|
||||
|
||||
it('merges overrideData over the default options', async () => {
|
||||
const { navigator, share } = stubShareNavigator();
|
||||
const { result, stop } = withScope(() =>
|
||||
useShare({ title: 'Default', text: 'Default text' }, { navigator }),
|
||||
);
|
||||
|
||||
await result.share({ text: 'Override text', url: 'https://override.dev' });
|
||||
|
||||
expect(share).toHaveBeenCalledWith({
|
||||
title: 'Default',
|
||||
text: 'Override text',
|
||||
url: 'https://override.dev',
|
||||
});
|
||||
stop();
|
||||
});
|
||||
|
||||
it('resolves reactive/getter share options at call time', async () => {
|
||||
const { navigator, share } = stubShareNavigator();
|
||||
const title = ref('first');
|
||||
const { result, stop } = withScope(() => useShare(() => ({ title: title.value }), { navigator }));
|
||||
|
||||
await result.share();
|
||||
expect(share).toHaveBeenLastCalledWith({ title: 'first' });
|
||||
|
||||
title.value = 'second';
|
||||
await result.share();
|
||||
expect(share).toHaveBeenLastCalledWith({ title: 'second' });
|
||||
stop();
|
||||
});
|
||||
|
||||
it('does not call share when canShare rejects the payload', async () => {
|
||||
const { navigator, share, canShare } = stubShareNavigator(false);
|
||||
const { result, stop } = withScope(() => useShare({ title: 'Nope' }, { navigator }));
|
||||
|
||||
await result.share();
|
||||
|
||||
expect(canShare).toHaveBeenCalled();
|
||||
expect(share).not.toHaveBeenCalled();
|
||||
stop();
|
||||
});
|
||||
|
||||
it('skips canShare gating when canShare is unavailable', async () => {
|
||||
const share = vi.fn(async () => {});
|
||||
// No canShare -> isSupported is false, so share is a no-op.
|
||||
const navigator = { share } as unknown as Navigator;
|
||||
const { result, stop } = withScope(() => useShare({ title: 'Hi' }, { navigator }));
|
||||
|
||||
expect(result.isSupported.value).toBeFalsy();
|
||||
await result.share();
|
||||
expect(share).not.toHaveBeenCalled();
|
||||
stop();
|
||||
});
|
||||
|
||||
it('is a no-op when unsupported (SSR / missing navigator)', async () => {
|
||||
const navigator = undefined as unknown as Navigator;
|
||||
const { result, stop } = withScope(() => useShare({ title: 'Hi' }, { navigator }));
|
||||
|
||||
expect(result.isSupported.value).toBeFalsy();
|
||||
await expect(result.share()).resolves.toBeUndefined();
|
||||
stop();
|
||||
});
|
||||
|
||||
it('returns the share promise from navigator.share', async () => {
|
||||
const { navigator, share } = stubShareNavigator();
|
||||
share.mockResolvedValueOnce(undefined);
|
||||
const { result, stop } = withScope(() => useShare({ title: 'Hi' }, { navigator }));
|
||||
|
||||
await expect(result.share()).resolves.toBeUndefined();
|
||||
stop();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { toValue } from 'vue';
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { defaultNavigator } from '@/types';
|
||||
import type { ConfigurableNavigator } from '@/types';
|
||||
import { useSupported } from '@/composables/browser/useSupported';
|
||||
|
||||
export interface UseShareOptions {
|
||||
/**
|
||||
* Title of the shared content
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Arbitrary text that forms the body of the message being shared
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* URL string referring to a resource being shared
|
||||
*/
|
||||
url?: string;
|
||||
|
||||
/**
|
||||
* Array of `File` objects representing files to be shared
|
||||
*/
|
||||
files?: File[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of `Navigator` exposing the Web Share API surface, which is not yet in
|
||||
* every lib DOM target.
|
||||
*/
|
||||
interface NavigatorWithShare {
|
||||
share?: (data?: UseShareOptions) => Promise<void>;
|
||||
canShare?: (data?: UseShareOptions) => boolean;
|
||||
}
|
||||
|
||||
export interface UseShareReturn {
|
||||
/**
|
||||
* Whether the Web Share API is available
|
||||
*/
|
||||
isSupported: Readonly<Ref<boolean>>;
|
||||
|
||||
/**
|
||||
* Invoke the native share sheet, optionally merging `overrideData` over the
|
||||
* default share options. Resolves once sharing finishes (or is skipped when
|
||||
* unsupported / the payload cannot be shared).
|
||||
*/
|
||||
share: (overrideData?: MaybeRefOrGetter<UseShareOptions>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name useShare
|
||||
* @category Browser
|
||||
* @description Reactive Web Share API wrapper to invoke the native share sheet.
|
||||
*
|
||||
* @param {MaybeRefOrGetter<UseShareOptions>} [shareOptions={}] Default share payload (title, text, url, files)
|
||||
* @param {UseShareOptions} [options={}] Options
|
||||
* @returns {UseShareReturn} `isSupported` flag and a `share` method
|
||||
*
|
||||
* @example
|
||||
* const { share, isSupported } = useShare({ title: 'Hello', url: location.href });
|
||||
* share();
|
||||
*
|
||||
* @example
|
||||
* // Override the default payload per call
|
||||
* const { share } = useShare({ title: 'Default' });
|
||||
* share({ text: 'One-off message' });
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function useShare(
|
||||
shareOptions: MaybeRefOrGetter<UseShareOptions> = {},
|
||||
options: ConfigurableNavigator = {},
|
||||
): UseShareReturn {
|
||||
const { navigator = defaultNavigator } = options;
|
||||
|
||||
const _navigator = navigator as NavigatorWithShare | undefined;
|
||||
const isSupported = useSupported(() => !!_navigator && 'canShare' in _navigator);
|
||||
|
||||
const share = async (overrideData: MaybeRefOrGetter<UseShareOptions> = {}): Promise<void> => {
|
||||
if (!isSupported.value || !_navigator)
|
||||
return;
|
||||
|
||||
const data: UseShareOptions = {
|
||||
...toValue(shareOptions),
|
||||
...toValue(overrideData),
|
||||
};
|
||||
|
||||
// `canShare` gates the payload (e.g. file types / size); only proceed when it
|
||||
// accepts the data to avoid a guaranteed-to-reject `share()` call.
|
||||
if (_navigator.canShare && !_navigator.canShare(data))
|
||||
return;
|
||||
|
||||
return _navigator.share?.(data);
|
||||
};
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
share,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user