Skip to content

Commit

Permalink
Merge branch 'main' into SEARCH-855-write-more-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aiste-tamo authored Oct 26, 2023
2 parents 69f47e3 + 13c8096 commit 348f879
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 62 deletions.
80 changes: 80 additions & 0 deletions spec/custom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'
import { autocomplete } from '../src'

import '@testing-library/jest-dom'
import { AnyPromise, SimplePromise } from '../src/utils/promise'

beforeAll(() => {
document.body.innerHTML = `
Expand Down Expand Up @@ -65,3 +66,82 @@ describe('autocomplete', () => {
expect(screen.getByText('keyword blue')).toBeVisible()
})
})

describe('SimplePromise', () => {
it('has resolve and reject callbacks', async () => {
const promise = new SimplePromise((resolve, reject) => {
// expect resolve and reject to be defined and to be functions which acceps one argument
expect(resolve).toBeDefined()
expect(reject).toBeDefined()
expect(typeof resolve).toBe('function')
expect(typeof reject).toBe('function')

// resolve promise with value of 1
setTimeout(() => {
resolve(1)
}, 0)
})

await promise
})

it('resolve() corectly returns value in .then block', async () => {
const promise = new SimplePromise((resolve) => {
setTimeout(() => {
resolve('promise')
}, 0)
})

let value;
promise.then((v) => {
value = v
})
await promise

expect(value).toBe('promise')
})

it('reject() correctly rejects with a value in .then block', async () => {
const promise = new SimplePromise((_, reject) => {
setTimeout(() => {
reject('error')
}, 0)
})

let error: any;


promise.then(() => {
return
}, (err) => {
error = err
return
})

await waitFor(() => {
expect(error).toBe('error')
}, {
timeout: 1000
})
})

it('resolves promise in .then block', async () => {
const promise = new SimplePromise((resolve) => {
setTimeout(() => {
resolve('promise')
}, 0)
})

let value;
promise.then(() => {
return new SimplePromise((resolve) => {
resolve('promise2')
})
}).then((v) => {
value = v
})

await promise;
expect(value).toBe('promise2')
})
})
1 change: 1 addition & 0 deletions spec/liquid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { screen, waitFor } from '@testing-library/dom'
import userEvent from '@testing-library/user-event'

import {
DefaultState,
autocomplete,
fromLiquidTemplate,
fromRemoteLiquidTemplate,
Expand Down
17 changes: 9 additions & 8 deletions src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { bindClickOutside, findAll } from './utils/dom'
import { bindInput } from './utils/input'
import { History } from './history'
import { Limiter, LimiterError } from './utils/limiter'
import { CancellableError } from './utils/promise'

/**
* @group Autocomplete
Expand Down Expand Up @@ -44,22 +45,19 @@ export function autocomplete<State = DefaultState>(
return
}

let lastRenderTime = Date.now()

const input = bindInput(inputElement, {
onInput: async (value) => {
const requestTime = Date.now()

try {
await limiter.limited(() => {
return actions.updateState(value).then((state) => {
if (requestTime >= lastRenderTime) {
return actions.updateState(value).then(
(state) => {
dropdown.update(state)
}
})
)
})
} catch (err) {
if (!(err instanceof LimiterError)) {
if (!(err instanceof LimiterError || err instanceof CancellableError)) {
throw err
}
}
Expand Down Expand Up @@ -165,7 +163,10 @@ function createInputDropdown<State = DefaultState>({

return new Dropdown<State>(
dropdownElement,
actions.updateState(input.value),
actions.updateState(input.value).then(
(state) => state,
() => ({} as State),
),
config.render,
config.submit,
(value) => (input.value = value),
Expand Down
15 changes: 12 additions & 3 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,23 @@ export const getStateActions = <State>({

if (inputValue && inputValue.length >= minQueryLength) {
cancellable = makeCancellable(fetchState(inputValue, config))
return cancellable.promise
return cancellable.promise.then(
(s) => s as State,
(e) => {
throw e
},
)
} else if (history) {
return getHistoryState(inputValue ?? '')
}

return (
cancellable?.promise ??
AnyPromise.resolve({}).then((s) => s as State)
cancellable?.promise.then(
(s) => s as State,
(e) => {
throw e
},
) ?? AnyPromise.resolve({}).then((s) => s as State)
)
},
addHistoryItem: (item: string) => {
Expand Down
136 changes: 85 additions & 51 deletions src/utils/promise.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,102 @@
class SimplePromise<T> implements PromiseLike<T> {
private value: T | undefined
private error: any
private fulfilled = false
private doneCallback: (() => void) | undefined = undefined
export class SimplePromise<T> implements PromiseLike<T> {
private status: string
private value: T
private onFulfilledCallbacks: Array<(value: T) => void>
private onRejectedCallbacks: Array<(value: any) => void>

constructor(
callback: (
resolve: (value: T) => void,
reject: (value: any) => void,
) => void,
handler: (resolve: (value: T) => void, reject: (reason?: any) => void) => void
) {
callback(
(value) => {
if (!this.fulfilled) {
this.value = value
this.fulfilled = true
this.value = null as T
this.status = 'pending'
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []

if (this.doneCallback) {
this.doneCallback()
}
}
},
(error) => {
if (!this.fulfilled) {
this.error = error
this.fulfilled = true
const resolve = (value: T) => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn(value))
}
}

if (this.doneCallback) {
this.doneCallback()
}
}
},
)
const reject = (value: T) => {
if (this.status === 'pending') {
this.status = 'rejected'
this.value = value
this.onRejectedCallbacks.forEach((fn) => fn(value))
}
}

try {
handler(resolve, reject)
} catch (err: any) {
reject(err)
}
}

then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| null
| undefined,
| undefined
| null,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| null
| undefined,
| undefined
| null,
): PromiseLike<TResult1 | TResult2> {
return new SimplePromise((resolve, reject) => {
const doneCallback = () => {
if (this.error) {
if (onrejected) {
SimplePromise.resolve(onrejected(this.error)).then(
resolve,
reject,
)
if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
try {
const fulfilledFromLastPromise = onfulfilled?.(this.value)
if (fulfilledFromLastPromise && typeof fulfilledFromLastPromise === 'object' && 'then' in fulfilledFromLastPromise) {
fulfilledFromLastPromise.then(resolve, reject)
} else {
resolve(fulfilledFromLastPromise as TResult1)
}
} catch (err) {
reject(err)
}
} else {
if (onfulfilled) {
SimplePromise.resolve(
onfulfilled(this.value as T),
).then(resolve, reject)
})
this.onRejectedCallbacks.push(() => {
try {
const rejectedFromLastPromise = onrejected?.(this.value)
if (rejectedFromLastPromise && typeof rejectedFromLastPromise === 'object' && 'then' in rejectedFromLastPromise) {
rejectedFromLastPromise.then(resolve, reject)
} else {
reject(rejectedFromLastPromise)
}
} catch (err) {
reject(err)
}
})
}

if (this.status === 'fulfilled') {
try {
const fulfilledFromLastPromise = onfulfilled?.(this.value)
if (fulfilledFromLastPromise && typeof fulfilledFromLastPromise === 'object' && 'then' in fulfilledFromLastPromise) {
fulfilledFromLastPromise.then(resolve, reject)
} else {
resolve(fulfilledFromLastPromise as TResult1)
}
} catch (err) {
reject(err)
}
}
if (this.fulfilled) {
doneCallback()
} else {
this.doneCallback = doneCallback

if (this.status === 'rejected') {
try {
const rejectedFromLastPromise = onrejected?.(this.value)
if (rejectedFromLastPromise && typeof rejectedFromLastPromise === 'object' && 'then' in rejectedFromLastPromise) {
rejectedFromLastPromise.then(resolve, reject)
} else {
reject(rejectedFromLastPromise)
}
} catch (err) {
reject(err)
}
}
})
}
Expand All @@ -84,16 +116,18 @@ export let AnyPromise = 'Promise' in window ? window.Promise : SimplePromise

export type Cancellable<T> = { promise: PromiseLike<T>; cancel: () => void }

export class CancellableError extends Error {}

export function makeCancellable<T>(promise: PromiseLike<T>): Cancellable<T> {
let hasCanceled_ = false

const wrappedPromise = new AnyPromise<T>((resolve, reject) => {
promise.then(
(val) => {
hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)
hasCanceled_ ? reject(new CancellableError('cancelled promise')) : resolve(val)
},
(error) => {
hasCanceled_ ? reject({ isCanceled: true }) : reject(error)
hasCanceled_ ? reject(new CancellableError('cancelled promise')) : reject(error)
},
)
})
Expand Down

0 comments on commit 348f879

Please sign in to comment.