diff --git a/package.json b/package.json index 895a8ad3293..375c82e8178 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@iconify/json": "^2.1.62", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-replace": "^4.0.0", + "@type-challenges/utils": "^0.1.1", "@types/fs-extra": "^9.0.13", "@types/js-yaml": "^4.0.5", "@types/md5": "^2.3.2", diff --git a/packages/shared/until/index.test.ts b/packages/shared/until/index.test.ts index eb75f84956a..e7af354bf67 100644 --- a/packages/shared/until/index.test.ts +++ b/packages/shared/until/index.test.ts @@ -1,5 +1,7 @@ +import type { Ref } from 'vue-demi' import { ref } from 'vue-demi' import { invoke } from '@vueuse/shared' +import type { Equal, Expect } from '@type-challenges/utils' import { until } from '.' describe('until', () => { @@ -9,8 +11,8 @@ describe('until', () => { invoke(async () => { expect(r.value).toBe(0) - await until(r).toBe(1) - expect(r.value).toBe(1) + const x = await until(r).toBe(1) + expect(x).toBe(1) resolve() }).catch(reject) @@ -26,8 +28,8 @@ describe('until', () => { invoke(async () => { expect(r.value).toBe(0) - await until(r).changedTimes(3) - expect(r.value).toBe(3) + const x = await until(r).changedTimes(3) + expect(x).toBe(3) resolve() }).catch(reject) @@ -45,8 +47,8 @@ describe('until', () => { invoke(async () => { expect(r.value).toBe(0) - await until(r).not.toBe(0) - expect(r.value).toBe(1) + const x = await until(r).not.toBe(0) + expect(x).toBe(1) resolve() }).catch(reject) @@ -62,8 +64,8 @@ describe('until', () => { invoke(async () => { expect(r.value).toBe(null) - await until(r).not.toBeNull() - expect(r.value).toBe(1) + const x = await until(r).not.toBeNull() + expect(x).toBe(1) resolve() }).catch(reject) @@ -79,8 +81,8 @@ describe('until', () => { invoke(async () => { expect(r.value).toEqual([1, 2, 3]) - await until(r).toContains(4, { deep: true }) - expect(r.value).toEqual([1, 2, 3, 4]) + const x = await until(r).toContains(4, { deep: true }) + expect(x).toEqual([1, 2, 3, 4]) resolve() }).catch(reject) @@ -96,8 +98,8 @@ describe('until', () => { invoke(async () => { expect(r.value).toEqual([1, 2, 3]) - await until(r).not.toContains(2, { deep: true }) - expect(r.value).toEqual([1]) + const x = await until(r).not.toContains(2, { deep: true }) + expect(x).toEqual([1]) resolve() }).catch(reject) @@ -107,4 +109,65 @@ describe('until', () => { }, 100) }) }) + + it('should immediately timeout', () => { + return new Promise((resolve, reject) => { + const r = ref(0) + + invoke(async () => { + expect(r.value).toBe(0) + const x = await until(r).toBe(1, { timeout: 0 }) + expect(x).toBe(0) + resolve() + }).catch(reject) + + setTimeout(() => { + r.value = 1 + }, 100) + }) + }) + + it('should type check', () => { + async () => { + const x = ref<'x'>() + // type checks are done this way to prevent unused variable warnings + // and duplicate name warnings + 'test' as any as Expect>> + + const one = await until(x).toBe(1 as const) + 'test' as any as Expect> + + const xTruthy = await until(x).toBeTruthy() + 'test' as any as Expect> + + const xFalsy = await until(x).not.toBeTruthy() + 'test' as any as Expect> + + const xUndef = await until(x).toBeUndefined() + 'test' as any as Expect> + + const xNotUndef = await until(x).not.toBeUndefined() + 'test' as any as Expect> + + const y = ref<'y' | null>(null) + 'test' as any as Expect>> + + const yNull = await until(y).toBeNull() + 'test' as any as Expect> + + const yNotNull = await until(y).not.toBeNull() + 'test' as any as Expect> + + const z = ref<1 | 2 | 3>(1) + 'test' as any as Expect>> + + const is1 = (x: number): x is 1 => x === 1 + + const z1 = await until(z).toMatch(is1) + 'test' as any as Expect> + + const zNot1 = await until(z).not.toMatch(is1) + 'test' as any as Expect> + } + }) }) diff --git a/packages/shared/until/index.ts b/packages/shared/until/index.ts index 59f7098e2a3..499d8f1b610 100644 --- a/packages/shared/until/index.ts +++ b/packages/shared/until/index.ts @@ -1,5 +1,5 @@ import type { WatchOptions, WatchSource } from 'vue-demi' -import { unref, watch } from 'vue-demi' +import { isRef, unref, watch } from 'vue-demi' import type { ElementOf, MaybeRef, ShallowUnwrapRef } from '../utils' import { promiseTimeout } from '../utils' @@ -34,29 +34,35 @@ export interface UntilToMatchOptions { deep?: WatchOptions['deep'] } -export interface UntilBaseInstance { +export interface UntilBaseInstance { + toMatch( + condition: (v: T) => v is U, + options?: UntilToMatchOptions + ): Not extends true ? Promise> : Promise toMatch( condition: (v: T) => boolean, options?: UntilToMatchOptions - ): Promise - changed(options?: UntilToMatchOptions): Promise - changedTimes(n?: number, options?: UntilToMatchOptions): Promise + ): Promise + changed(options?: UntilToMatchOptions): Promise + changedTimes(n?: number, options?: UntilToMatchOptions): Promise } -export interface UntilValueInstance extends UntilBaseInstance { - readonly not: UntilValueInstance +type Falsy = false | void | null | undefined | 0 | 0n | '' + +export interface UntilValueInstance extends UntilBaseInstance { + readonly not: UntilValueInstance - toBe

(value: MaybeRef, options?: UntilToMatchOptions): Promise - toBeTruthy(options?: UntilToMatchOptions): Promise - toBeNull(options?: UntilToMatchOptions): Promise - toBeUndefined(options?: UntilToMatchOptions): Promise - toBeNaN(options?: UntilToMatchOptions): Promise + toBe

(value: MaybeRef

, options?: UntilToMatchOptions): Not extends true ? Promise : Promise

+ toBeTruthy(options?: UntilToMatchOptions): Not extends true ? Promise : Promise> + toBeNull(options?: UntilToMatchOptions): Not extends true ? Promise> : Promise + toBeUndefined(options?: UntilToMatchOptions): Not extends true ? Promise> : Promise + toBeNaN(options?: UntilToMatchOptions): Promise } export interface UntilArrayInstance extends UntilBaseInstance { readonly not: UntilArrayInstance - toContains(value: MaybeRef>>, options?: UntilToMatchOptions): Promise + toContains(value: MaybeRef>>, options?: UntilToMatchOptions): Promise } /** @@ -80,15 +86,15 @@ export function until(r: any): any { function toMatch( condition: (v: any) => boolean, { flush = 'sync', deep = false, timeout, throwOnTimeout }: UntilToMatchOptions = {}, - ): Promise { + ): Promise { let stop: Function | null = null - const watcher = new Promise((resolve) => { + const watcher = new Promise((resolve) => { stop = watch( r, (v) => { - if (condition(v) === !isNot) { + if (condition(v) !== isNot) { stop?.() - resolve() + resolve(v) } }, { @@ -100,11 +106,11 @@ export function until(r: any): any { }) const promises = [watcher] - if (timeout) { + if (timeout != null) { promises.push( - promiseTimeout(timeout, throwOnTimeout).finally(() => { - stop?.() - }), + promiseTimeout(timeout, throwOnTimeout) + .then(() => unref(r)) + .finally(() => stop?.()), ) } @@ -112,7 +118,41 @@ export function until(r: any): any { } function toBe

(value: MaybeRef

, options?: UntilToMatchOptions) { - return toMatch(v => v === unref(value), options) + if (!isRef(value)) + return toMatch(v => v === value, options) + + const { flush = 'sync', deep = false, timeout, throwOnTimeout } = options ?? {} + let stop: Function | null = null + const watcher = new Promise((resolve) => { + stop = watch( + [r, value], + ([v1, v2]) => { + if (isNot !== (v1 === v2)) { + stop?.() + resolve(v1) + } + }, + { + flush, + deep, + immediate: true, + }, + ) + }) + + const promises = [watcher] + if (timeout != null) { + promises.push( + promiseTimeout(timeout, throwOnTimeout) + .then(() => unref(r)) + .finally(() => { + stop?.() + return unref(r) + }), + ) + } + + return Promise.race(promises) } function toBeTruthy(options?: UntilToMatchOptions) { @@ -167,13 +207,13 @@ export function until(r: any): any { return instance } else { - const instance: UntilValueInstance = { + const instance: UntilValueInstance = { toMatch, toBe, - toBeTruthy, - toBeNull, + toBeTruthy: toBeTruthy as any, + toBeNull: toBeNull as any, toBeNaN, - toBeUndefined, + toBeUndefined: toBeUndefined as any, changed, changedTimes, get not() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61d8281350f..a1f30c6d2d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,7 @@ importers: '@iconify/json': ^2.1.62 '@rollup/plugin-json': ^4.1.0 '@rollup/plugin-replace': ^4.0.0 + '@type-challenges/utils': ^0.1.1 '@types/fs-extra': ^9.0.13 '@types/js-yaml': ^4.0.5 '@types/md5': ^2.3.2 @@ -79,6 +80,7 @@ importers: '@iconify/json': 2.1.62 '@rollup/plugin-json': 4.1.0_rollup@2.75.6 '@rollup/plugin-replace': 4.0.0_rollup@2.75.6 + '@type-challenges/utils': 0.1.1 '@types/fs-extra': 9.0.13 '@types/js-yaml': 4.0.5 '@types/md5': 2.3.2 @@ -2503,6 +2505,10 @@ packages: engines: {node: '>= 10'} dev: true + /@type-challenges/utils/0.1.1: + resolution: {integrity: sha512-A7ljYfBM+FLw+NDyuYvGBJiCEV9c0lPWEAdzfOAkb3JFqfLl0Iv/WhWMMARHiRKlmmiD1g8gz/507yVvHdQUYA==} + dev: true + /@types/chai-subset/1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: