From 32a90356ebebb72c2796f28e6925e1292559081e Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 22 Apr 2022 11:44:58 +0200 Subject: [PATCH 01/30] Add the useTimeout hook --- src/useTimeout/useTimeout.stories.mdx | 72 +++++++++++++++++ src/useTimeout/useTimeout.stories.ts | 98 ++++++++++++++++++++++++ src/useTimeout/useTimeout.test.ts | 85 ++++++++++++++++++++ src/useTimeout/useTimeout.test.utils.ts | 10 +++ src/useTimeout/useTimeout.ts | 36 +++++++++ src/useTimeout/useTimeoutStories.test.ts | 39 ++++++++++ 6 files changed, 340 insertions(+) create mode 100644 src/useTimeout/useTimeout.stories.mdx create mode 100644 src/useTimeout/useTimeout.stories.ts create mode 100644 src/useTimeout/useTimeout.test.ts create mode 100644 src/useTimeout/useTimeout.test.utils.ts create mode 100644 src/useTimeout/useTimeout.ts create mode 100644 src/useTimeout/useTimeoutStories.test.ts diff --git a/src/useTimeout/useTimeout.stories.mdx b/src/useTimeout/useTimeout.stories.mdx new file mode 100644 index 0000000..40cfa3b --- /dev/null +++ b/src/useTimeout/useTimeout.stories.mdx @@ -0,0 +1,72 @@ +import { Meta } from '@storybook/addon-docs'; + + + +# useTimeout + +The `useTimeout` hook is a wrapper around the native `setTimeout`, it allows you to easily create set +a timeout within your component that will be auto cancelled when the component unmounts. + +## Reference + +```ts +function useTimeout( + callback: () => void, + duration?: number = 100, + startImmediate?: boolean = true, +): { start: () => void, cancel: () => void } +``` + +### Parameters +* `callback` – The function to invoke when the timeout runs out. +* `duration` - The duration of the timeout you want to create. +* `startImmediate` - Whether or not you want to immediately start the timeout. + +### Returns +* `{ start, cancel }` + * `start` – A function that starts the timeout, any running timeouts will automatically be cancelled. + * `cancel` – A function that will cancel the current active timeout. + +## Usage + +```ts +const { start, cancel } = useTimeout(() => { + console.log('The timeout has run out') +}, 1000, false); +```` + +```ts +const Demo = defineComponent({ + name: 'demo', + refs: { + startBtn: 'start-btn' + cancelButton: 'cancel-btn' + }, + setup({ refs }) { + // The timeout runs as soon as the component is mounted. + useTimeout(() => { + console.log('The timeout has run out') + }, 1000); + + // The timeout doesn't start automatically, but requires a user action to start. + const { start, cancel } = useTimeout(() => { + console.log('The timeout has run out') + }, 1000, false); + + return [ + bind(refs.startBtn, { + click() { + start(); // This actually starts the timeout. + } + }), + bind(refs.cancelButton, { + click() { + cancel(); // This cancels the timeout if it's active. + } + }) + ] + } +}) +``` diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts new file mode 100644 index 0000000..218439f --- /dev/null +++ b/src/useTimeout/useTimeout.stories.ts @@ -0,0 +1,98 @@ +/* eslint-disable unicorn/prevent-abbreviations,import/no-extraneous-dependencies */ +import { bind, computed, defineComponent, propType, reactive, ref } from '@muban/muban'; +import type { Story } from '@muban/storybook/types-6-0'; +import { html } from '@muban/template'; +import { useTimeout } from './useTimeout'; + +export default { + title: 'useTimeout', +}; + +type DemoStoryProps = { startImmediate?: boolean; duration?: number }; + +export const Demo: Story = () => ({ + component: defineComponent({ + name: 'story', + props: { + startImmediate: propType.boolean.defaultValue(false), + duration: propType.number, + }, + refs: { + label: 'label', + startButton: 'start-button', + cancelButton: 'cancel-button', + }, + setup({ refs, props }) { + const state = reactive>([]); + const isTimeoutRunning = ref(false); + + function log(message: string) { + state.push(message); + setTimeout(() => { + state.splice(0, 1); + }, 2000); + } + + const { start, cancel } = useTimeout(onTimeoutComplete, props.duration, props.startImmediate); + + function onTimeoutComplete() { + isTimeoutRunning.value = false; + log('timeout complete'); + } + + return [ + bind(refs.label, { + html: computed(() => + state + .map((msg) => html`
${msg}
`) + .join(''), + ), + }), + bind(refs.startButton, { + attr: { + disabled: isTimeoutRunning, + }, + click() { + isTimeoutRunning.value = true; + start(); + }, + }), + bind(refs.cancelButton, { + attr: { + disabled: computed(() => !isTimeoutRunning.value), + }, + click() { + isTimeoutRunning.value = false; + log('canceled timeout'); + cancel(); + }, + }), + ]; + }, + }), + template: ({ startImmediate = false, duration = 2000 }: DemoStoryProps = {}) => html`
+
+

Instructions!

+

+ The demo timeout is set to 2 seconds, you can start it by clicking the start button. You can + cancel the timeout by clicking the cancel button. +

+
+
+
+
Test Area
+
+ + ${' '} + +
+
+
`, +}); +Demo.storyName = 'demo'; diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts new file mode 100644 index 0000000..8810116 --- /dev/null +++ b/src/useTimeout/useTimeout.test.ts @@ -0,0 +1,85 @@ +import { runComponentSetup } from '@muban/test-utils'; +import { useTimeout } from './useTimeout'; +import { timeout } from './useTimeout.test.utils'; + +jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock()); + +describe('useTimeout', () => { + it('should not crash', async () => { + await runComponentSetup(() => { + useTimeout(() => undefined); + }); + }); + + it('should start immediate and be completed after 1ms', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => { + useTimeout(mockHandler, 1); + }, + () => timeout(2), + ); + + expect(mockHandler).toBeCalledTimes(1); + }); + + it('should start immediate and not be completed', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup(() => { + useTimeout(mockHandler, 1); + }); + + expect(mockHandler).toBeCalledTimes(0); + }); + + it('should trigger start and be completed after 1ms', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => { + const { start } = useTimeout(mockHandler, 1, false); + + start(); + }, + () => timeout(2), + ); + + expect(mockHandler).toBeCalledTimes(1); + }); + + it('should trigger cancel once the timeout is started', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + async () => { + const { start, cancel } = useTimeout(mockHandler, 2, false); + + start(); + await timeout(1); + cancel(); + }, + () => timeout(3), + ); + + expect(mockHandler).toBeCalledTimes(0); + }); + + it('should start a new timeout before the old one running out and only complete once', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + async () => { + const { start } = useTimeout(mockHandler, 2, false); + + start(); + await timeout(1); + start(); + }, + () => timeout(4), + ); + + expect(mockHandler).toBeCalledTimes(1); + }); +}); diff --git a/src/useTimeout/useTimeout.test.utils.ts b/src/useTimeout/useTimeout.test.utils.ts new file mode 100644 index 0000000..9e0c9f8 --- /dev/null +++ b/src/useTimeout/useTimeout.test.utils.ts @@ -0,0 +1,10 @@ +/** + * Util to easily test delayed callbacks + * + * @param duration The duration of the timeout you want to apply + */ +export async function timeout(duration: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, duration); + }); +} diff --git a/src/useTimeout/useTimeout.ts b/src/useTimeout/useTimeout.ts new file mode 100644 index 0000000..5a0d8a1 --- /dev/null +++ b/src/useTimeout/useTimeout.ts @@ -0,0 +1,36 @@ +import { onMounted, onUnmounted } from '@muban/muban'; + +/** + * A hook that can be used to apply a timeout to a certain function but also give you the option + * to cancel it before it's executed. + * + * @param callback The callback you want to trigger once the timeout is completed. + * @param duration The duration of the timeout you want to create. + * @param startImmediate Whether or not you want to immediately start the timeout. + */ +export const useTimeout = ( + callback: () => void, + duration: number = 100, + startImmediate: boolean = true, +): { start: () => void; cancel: () => void } => { + let handle = -1; + + function start() { + cancel(); + handle = setTimeout(callback, duration) as unknown as number; + } + + function cancel() { + clearTimeout(handle); + } + + onUnmounted(() => { + cancel(); + }); + + onMounted(() => { + if (startImmediate) start(); + }); + + return { start, cancel }; +}; diff --git a/src/useTimeout/useTimeoutStories.test.ts b/src/useTimeout/useTimeoutStories.test.ts new file mode 100644 index 0000000..c6fb258 --- /dev/null +++ b/src/useTimeout/useTimeoutStories.test.ts @@ -0,0 +1,39 @@ +import '@testing-library/jest-dom'; +import { waitFor, render } from '@muban/testing-library'; +import { Demo } from './useTimeout.stories'; +import { timeout } from './useTimeout.test.utils'; + +describe('useTimeout stories', () => { + it('should render', () => { + const { getByText } = render(Demo); + + expect(getByText('Test Area')).toBeInTheDocument(); + }); + + it('should start immediate and be completed after 1ms', async () => { + const { getByText } = render(Demo, { startImmediate: true, duration: 1 }); + + await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); + }); + + it('should start after clicking start and be completed after 1ms', async () => { + const { getByText, getByRef } = render(Demo, { duration: 1 }); + const startButton = getByRef('start-button'); + + startButton.click(); + + await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); + }); + + it('should cancel the timeout after starting', async () => { + const { getByText, getByRef } = render(Demo, { duration: 100 }); + const startButton = getByRef('start-button'); + const cancelButton = getByRef('cancel-button'); + + startButton.click(); + await timeout(1); + cancelButton.click(); + + await waitFor(() => expect(getByText('canceled timeout')).toBeInTheDocument()); + }); +}); From 8487c2713273eb8a7de6ff71d84a8eaf9676ddc3 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 22 Apr 2022 12:10:48 +0200 Subject: [PATCH 02/30] Update the useTimeout hook to return more explicit methods --- src/useTimeout/useTimeout.stories.mdx | 20 ++++++++++---------- src/useTimeout/useTimeout.stories.ts | 16 ++++++++++------ src/useTimeout/useTimeout.test.ts | 18 +++++++++--------- src/useTimeout/useTimeout.ts | 4 ++-- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/useTimeout/useTimeout.stories.mdx b/src/useTimeout/useTimeout.stories.mdx index 40cfa3b..a8e53b9 100644 --- a/src/useTimeout/useTimeout.stories.mdx +++ b/src/useTimeout/useTimeout.stories.mdx @@ -16,7 +16,7 @@ function useTimeout( callback: () => void, duration?: number = 100, startImmediate?: boolean = true, -): { start: () => void, cancel: () => void } +): { startTimeout: () => void, cancelTimeout: () => void } ``` ### Parameters @@ -25,14 +25,14 @@ function useTimeout( * `startImmediate` - Whether or not you want to immediately start the timeout. ### Returns -* `{ start, cancel }` - * `start` – A function that starts the timeout, any running timeouts will automatically be cancelled. - * `cancel` – A function that will cancel the current active timeout. +* `{ startTimeout, cancelTimeout }` + * `startTimeout` – A function that starts the timeout, any running timeouts will automatically be cancelled. + * `cancelTimeout` – A function that will cancel the current active timeout. ## Usage ```ts -const { start, cancel } = useTimeout(() => { +const { startTimeout, cancelTimeout } = useTimeout(() => { console.log('The timeout has run out') }, 1000, false); ```` @@ -41,8 +41,8 @@ const { start, cancel } = useTimeout(() => { const Demo = defineComponent({ name: 'demo', refs: { - startBtn: 'start-btn' - cancelButton: 'cancel-btn' + startBtn: 'start-button' + cancelButton: 'cancel-button' }, setup({ refs }) { // The timeout runs as soon as the component is mounted. @@ -51,19 +51,19 @@ const Demo = defineComponent({ }, 1000); // The timeout doesn't start automatically, but requires a user action to start. - const { start, cancel } = useTimeout(() => { + const { startTimeout, cancelTimeout } = useTimeout(() => { console.log('The timeout has run out') }, 1000, false); return [ bind(refs.startBtn, { click() { - start(); // This actually starts the timeout. + startTimeout(); // This actually starts the timeout. } }), bind(refs.cancelButton, { click() { - cancel(); // This cancels the timeout if it's active. + cancelTimeout(); // This cancels the timeout if it's active. } }) ] diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts index 218439f..cc21744 100644 --- a/src/useTimeout/useTimeout.stories.ts +++ b/src/useTimeout/useTimeout.stories.ts @@ -33,7 +33,11 @@ export const Demo: Story = () => ({ }, 2000); } - const { start, cancel } = useTimeout(onTimeoutComplete, props.duration, props.startImmediate); + const { startTimeout, cancelTimeout } = useTimeout( + onTimeoutComplete, + props.duration, + props.startImmediate, + ); function onTimeoutComplete() { isTimeoutRunning.value = false; @@ -44,7 +48,7 @@ export const Demo: Story = () => ({ bind(refs.label, { html: computed(() => state - .map((msg) => html`
${msg}
`) + .map((msg) => html`
${msg}
`) .join(''), ), }), @@ -54,7 +58,7 @@ export const Demo: Story = () => ({ }, click() { isTimeoutRunning.value = true; - start(); + startTimeout(); }, }), bind(refs.cancelButton, { @@ -64,13 +68,13 @@ export const Demo: Story = () => ({ click() { isTimeoutRunning.value = false; log('canceled timeout'); - cancel(); + cancelTimeout(); }, }), ]; }, }), - template: ({ startImmediate = false, duration = 2000 }: DemoStoryProps = {}) => html`
html`
= () => ({

Instructions!

- The demo timeout is set to 2 seconds, you can start it by clicking the start button. You can + The demo timeout is set to 1 second, you can start it by clicking the start button. You can cancel the timeout by clicking the cancel button.

diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts index 8810116..8bc0be4 100644 --- a/src/useTimeout/useTimeout.test.ts +++ b/src/useTimeout/useTimeout.test.ts @@ -39,9 +39,9 @@ describe('useTimeout', () => { await runComponentSetup( () => { - const { start } = useTimeout(mockHandler, 1, false); + const { startTimeout } = useTimeout(mockHandler, 1, false); - start(); + startTimeout(); }, () => timeout(2), ); @@ -54,11 +54,11 @@ describe('useTimeout', () => { await runComponentSetup( async () => { - const { start, cancel } = useTimeout(mockHandler, 2, false); + const { startTimeout, cancelTimeout } = useTimeout(mockHandler, 2, false); - start(); + startTimeout(); await timeout(1); - cancel(); + cancelTimeout(); }, () => timeout(3), ); @@ -71,13 +71,13 @@ describe('useTimeout', () => { await runComponentSetup( async () => { - const { start } = useTimeout(mockHandler, 2, false); + const { startTimeout } = useTimeout(mockHandler, 2, false); - start(); + startTimeout(); await timeout(1); - start(); + startTimeout(); }, - () => timeout(4), + () => timeout(5), ); expect(mockHandler).toBeCalledTimes(1); diff --git a/src/useTimeout/useTimeout.ts b/src/useTimeout/useTimeout.ts index 5a0d8a1..df9f5b2 100644 --- a/src/useTimeout/useTimeout.ts +++ b/src/useTimeout/useTimeout.ts @@ -12,7 +12,7 @@ export const useTimeout = ( callback: () => void, duration: number = 100, startImmediate: boolean = true, -): { start: () => void; cancel: () => void } => { +): { startTimeout: () => void; cancelTimeout: () => void } => { let handle = -1; function start() { @@ -32,5 +32,5 @@ export const useTimeout = ( if (startImmediate) start(); }); - return { start, cancel }; + return { startTimeout: start, cancelTimeout: cancel }; }; From 90ff012d07f46a338c4ce381ab16e4d0696eb964 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Tue, 26 Apr 2022 12:06:24 +0200 Subject: [PATCH 03/30] Add the useInterval hook --- src/useInterval/useInterval.stories.mdx | 72 +++++++++++++++ src/useInterval/useInterval.stories.ts | 101 +++++++++++++++++++++ src/useInterval/useInterval.test.ts | 84 +++++++++++++++++ src/useInterval/useInterval.ts | 36 ++++++++ src/useInterval/useIntervalStories.test.ts | 38 ++++++++ 5 files changed, 331 insertions(+) create mode 100644 src/useInterval/useInterval.stories.mdx create mode 100644 src/useInterval/useInterval.stories.ts create mode 100644 src/useInterval/useInterval.test.ts create mode 100644 src/useInterval/useInterval.ts create mode 100644 src/useInterval/useIntervalStories.test.ts diff --git a/src/useInterval/useInterval.stories.mdx b/src/useInterval/useInterval.stories.mdx new file mode 100644 index 0000000..d096767 --- /dev/null +++ b/src/useInterval/useInterval.stories.mdx @@ -0,0 +1,72 @@ +import { Meta } from '@storybook/addon-docs'; + + + +# useInterval + +The `useInterval` hook is a wrapper around the native `setInterval`, it allows you to easily create set +an interval within your component that will be auto cancelled when the component unmounts. + +## Reference + +```ts +function useInterval( + callback: () => void, + interval?: number = 100, + startImmediate?: boolean = true, +): { startInterval: () => void, stopInterval: () => void } +``` + +### Parameters +* `callback` – The callback you want to trigger once the interval runs. +* `interval` - The duration of the interval you want to create. +* `startImmediate` - Whether or not you want to immediately start the interval. + +### Returns +* `{ startInterval, stopInterval }` + * `startInterval` – A function that starts the interval, any running intervals will automatically be stopped. + * `stopInterval` – A function that will stop the current active interval. + +## Usage + +```ts +const { startInterval, stopInterval } = useInterval(() => { + console.log('The interval has run') +}, 1000, false); +```` + +```ts +const Demo = defineComponent({ + name: 'demo', + refs: { + startBtn: 'start-button' + stopButton: 'stop-button' + }, + setup({ refs }) { + // The interval starts as soon as the component is mounted. + useInterval(() => { + console.log('The immediate interval callback is triggered.') + }, 1000); + + // The interval doesn't start automatically, but requires a user action to start. + const { startInterval, stopInterval } = useInterval(() => { + console.log('The user-action interval callback is triggered.') + }, 1000, false); + + return [ + bind(refs.startBtn, { + click() { + startInterval(); // This actually starts the interval. + } + }), + bind(refs.stopButton, { + click() { + stopInterval(); // This stops the interval if it's active. + } + }) + ] + } +}) +``` diff --git a/src/useInterval/useInterval.stories.ts b/src/useInterval/useInterval.stories.ts new file mode 100644 index 0000000..f93e41b --- /dev/null +++ b/src/useInterval/useInterval.stories.ts @@ -0,0 +1,101 @@ +/* eslint-disable unicorn/prevent-abbreviations,import/no-extraneous-dependencies */ +import { bind, computed, defineComponent, propType, reactive, ref } from '@muban/muban'; +import type { Story } from '@muban/storybook/types-6-0'; +import { html } from '@muban/template'; +import { useInterval } from './useInterval'; + +export default { + title: 'useInterval', +}; + +type DemoStoryProps = { startImmediate?: boolean; interval?: number }; + +export const Demo: Story = () => ({ + component: defineComponent({ + name: 'story', + props: { + startImmediate: propType.boolean.defaultValue(false), + interval: propType.number, + }, + refs: { + label: 'label', + startButton: 'start-button', + cancelButton: 'cancel-button', + }, + setup({ refs, props }) { + const state = reactive>([]); + const isIntervalRunning = ref(false); + + function log(message: string) { + state.push(message); + setTimeout(() => { + state.splice(0, 1); + }, 2000); + } + + const { startInterval, stopInterval } = useInterval( + onInterval, + props.interval, + props.startImmediate, + ); + + function onInterval() { + log('interval called'); + } + + return [ + bind(refs.label, { + html: computed(() => + state + .map((msg) => html`
${msg}
`) + .join(''), + ), + }), + bind(refs.startButton, { + attr: { + disabled: isIntervalRunning, + }, + click() { + isIntervalRunning.value = true; + startInterval(); + }, + }), + bind(refs.cancelButton, { + attr: { + disabled: computed(() => !isIntervalRunning.value), + }, + click() { + isIntervalRunning.value = false; + log('interval stopped'); + stopInterval(); + }, + }), + ]; + }, + }), + template: ({ startImmediate = false, interval = 1000 }: DemoStoryProps = {}) => html`
+
+

Instructions!

+

+ The demo interval is set to 1 second, you can start it by clicking the start button. You can + stop the interval by clicking the cancel button. +

+
+
+
+
Test Area
+
+ + ${' '} + +
+
+
`, +}); +Demo.storyName = 'demo'; diff --git a/src/useInterval/useInterval.test.ts b/src/useInterval/useInterval.test.ts new file mode 100644 index 0000000..e08c865 --- /dev/null +++ b/src/useInterval/useInterval.test.ts @@ -0,0 +1,84 @@ +// import { runComponentSetup } from '@muban/test-utils'; +// import { useInterval } from './useInterval'; + +jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock()); + +describe('useTimeout', () => { + // it('should not crash', async () => { + // await runComponentSetup(() => { + // useInterval(() => undefined); + // }); + // }); + // + // it('should start immediate and be completed after 1ms', async () => { + // const mockHandler = jest.fn(); + // + // await runComponentSetup( + // () => { + // useTimeout(mockHandler, 1); + // }, + // () => timeout(2), + // ); + // + // expect(mockHandler).toBeCalledTimes(1); + // }); + // + // it('should start immediate and not be completed', async () => { + // const mockHandler = jest.fn(); + // + // await runComponentSetup(() => { + // useTimeout(mockHandler, 1); + // }); + // + // expect(mockHandler).toBeCalledTimes(0); + // }); + // + // it('should trigger start and be completed after 1ms', async () => { + // const mockHandler = jest.fn(); + // + // await runComponentSetup( + // () => { + // const { startTimeout } = useTimeout(mockHandler, 1, false); + // + // startTimeout(); + // }, + // () => timeout(2), + // ); + // + // expect(mockHandler).toBeCalledTimes(1); + // }); + // + // it('should trigger cancel once the timeout is started', async () => { + // const mockHandler = jest.fn(); + // + // await runComponentSetup( + // async () => { + // const { startTimeout, cancelTimeout } = useTimeout(mockHandler, 2, false); + // + // startTimeout(); + // await timeout(1); + // cancelTimeout(); + // }, + // () => timeout(3), + // ); + // + // expect(mockHandler).toBeCalledTimes(0); + // }); + // + // it('should start a new timeout before the old one running out and only complete once', async () => { + // const mockHandler = jest.fn(); + // + // await runComponentSetup( + // async () => { + // const { startTimeout } = useTimeout(mockHandler, 2, false); + // + // startTimeout(); + // await timeout(1); + // startTimeout(); + // }, + // () => timeout(5), + // ); + // + // expect(mockHandler).toBeCalledTimes(1); + // }); +}); diff --git a/src/useInterval/useInterval.ts b/src/useInterval/useInterval.ts new file mode 100644 index 0000000..c9c1486 --- /dev/null +++ b/src/useInterval/useInterval.ts @@ -0,0 +1,36 @@ +import { onMounted, onUnmounted } from '@muban/muban'; + +/** + * A hook that can be used to call a function on a provided interval, by default the interval + * will run immediate. You can also start and cancel the interval whenever needed. + * + * @param callback The callback you want to trigger once the interval runs. + * @param interval The duration of the interval you want to create. + * @param startImmediate Whether or not you want to immediately start the interval. + */ +export const useInterval = ( + callback: () => void, + interval: number = 100, + startImmediate: boolean = true, +): { startInterval: () => void; stopInterval: () => void } => { + let handle = -1; + + function start() { + stop(); + handle = setInterval(callback, interval) as unknown as number; + } + + function stop() { + clearInterval(handle); + } + + onUnmounted(() => { + stop(); + }); + + onMounted(() => { + if (startImmediate) start(); + }); + + return { startInterval: start, stopInterval: stop }; +}; diff --git a/src/useInterval/useIntervalStories.test.ts b/src/useInterval/useIntervalStories.test.ts new file mode 100644 index 0000000..37f6802 --- /dev/null +++ b/src/useInterval/useIntervalStories.test.ts @@ -0,0 +1,38 @@ +import '@testing-library/jest-dom'; +import { waitFor, render } from '@muban/testing-library'; +import { Demo } from './useInterval.stories'; + +describe('useTimeout stories', () => { + it('should render', () => { + const { getByText } = render(Demo); + + expect(getByText('Test Area')).toBeInTheDocument(); + }); + + // it('should start immediate and be completed after 1ms', async () => { + // const { getByText } = render(Demo, { startImmediate: true, duration: 1 }); + // + // await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); + // }); + // + // it('should start after clicking start and be completed after 1ms', async () => { + // const { getByText, getByRef } = render(Demo, { duration: 1 }); + // const startButton = getByRef('start-button'); + // + // startButton.click(); + // + // await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); + // }); + // + // it('should cancel the timeout after starting', async () => { + // const { getByText, getByRef } = render(Demo, { duration: 100 }); + // const startButton = getByRef('start-button'); + // const cancelButton = getByRef('cancel-button'); + // + // startButton.click(); + // await timeout(1); + // cancelButton.click(); + // + // await waitFor(() => expect(getByText('canceled timeout')).toBeInTheDocument()); + // }); +}); From 2659a29b5df900fa78ec5d81bec733240fa060cb Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Tue, 26 Apr 2022 14:17:16 +0200 Subject: [PATCH 04/30] Implement the storybook log on the useTimeout hook --- src/useTimeout/useTimeout.stories.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts index cc21744..f3d98e9 100644 --- a/src/useTimeout/useTimeout.stories.ts +++ b/src/useTimeout/useTimeout.stories.ts @@ -1,8 +1,9 @@ /* eslint-disable unicorn/prevent-abbreviations,import/no-extraneous-dependencies */ -import { bind, computed, defineComponent, propType, reactive, ref } from '@muban/muban'; +import { bind, computed, defineComponent, propType, ref } from '@muban/muban'; import type { Story } from '@muban/storybook/types-6-0'; import { html } from '@muban/template'; import { useTimeout } from './useTimeout'; +import { useStorybookLog } from '../hooks/useStorybookLog'; export default { title: 'useTimeout', @@ -23,16 +24,9 @@ export const Demo: Story = () => ({ cancelButton: 'cancel-button', }, setup({ refs, props }) { - const state = reactive>([]); + const [logBinding, log] = useStorybookLog(refs.label); const isTimeoutRunning = ref(false); - function log(message: string) { - state.push(message); - setTimeout(() => { - state.splice(0, 1); - }, 2000); - } - const { startTimeout, cancelTimeout } = useTimeout( onTimeoutComplete, props.duration, @@ -45,13 +39,7 @@ export const Demo: Story = () => ({ } return [ - bind(refs.label, { - html: computed(() => - state - .map((msg) => html`
${msg}
`) - .join(''), - ), - }), + logBinding, bind(refs.startButton, { attr: { disabled: isTimeoutRunning, From a424f756e14cf6878bab9bb6ba0c1ebbc241882b Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Tue, 26 Apr 2022 14:22:01 +0200 Subject: [PATCH 05/30] Remove the excessive space --- src/useTimeout/useTimeout.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts index f3d98e9..3306fcc 100644 --- a/src/useTimeout/useTimeout.stories.ts +++ b/src/useTimeout/useTimeout.stories.ts @@ -62,7 +62,7 @@ export const Demo: Story = () => ({ ]; }, }), - template: ({ startImmediate = false, duration = 1000 }: DemoStoryProps = {}) => html`
html`
Date: Tue, 26 Apr 2022 14:22:18 +0200 Subject: [PATCH 06/30] Update the timeout stories --- src/useTimeout/useTimeout.test.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts index 8bc0be4..6b1d281 100644 --- a/src/useTimeout/useTimeout.test.ts +++ b/src/useTimeout/useTimeout.test.ts @@ -16,9 +16,9 @@ describe('useTimeout', () => { await runComponentSetup( () => { - useTimeout(mockHandler, 1); + useTimeout(mockHandler, 100); }, - () => timeout(2), + () => timeout(200), ); expect(mockHandler).toBeCalledTimes(1); @@ -28,7 +28,7 @@ describe('useTimeout', () => { const mockHandler = jest.fn(); await runComponentSetup(() => { - useTimeout(mockHandler, 1); + useTimeout(mockHandler, 100); }); expect(mockHandler).toBeCalledTimes(0); @@ -38,12 +38,11 @@ describe('useTimeout', () => { const mockHandler = jest.fn(); await runComponentSetup( - () => { - const { startTimeout } = useTimeout(mockHandler, 1, false); - + () => useTimeout(mockHandler, 100, false), + async ({ startTimeout }) => { startTimeout(); + await timeout(200); }, - () => timeout(2), ); expect(mockHandler).toBeCalledTimes(1); @@ -53,14 +52,12 @@ describe('useTimeout', () => { const mockHandler = jest.fn(); await runComponentSetup( - async () => { - const { startTimeout, cancelTimeout } = useTimeout(mockHandler, 2, false); - + () => useTimeout(mockHandler, 500, false), + async ({ startTimeout, cancelTimeout }) => { startTimeout(); - await timeout(1); + await timeout(100); cancelTimeout(); }, - () => timeout(3), ); expect(mockHandler).toBeCalledTimes(0); @@ -70,14 +67,13 @@ describe('useTimeout', () => { const mockHandler = jest.fn(); await runComponentSetup( - async () => { - const { startTimeout } = useTimeout(mockHandler, 2, false); - + () => useTimeout(mockHandler, 200, false), + async ({ startTimeout }) => { startTimeout(); - await timeout(1); + await timeout(100); startTimeout(); + await timeout(300); }, - () => timeout(5), ); expect(mockHandler).toBeCalledTimes(1); From c9adccd0c229665213ecf820c20f74e2a6e447ff Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Tue, 26 Apr 2022 14:22:46 +0200 Subject: [PATCH 07/30] Update the useInterval stories and tests --- src/useInterval/useInterval.stories.ts | 40 ++--- src/useInterval/useInterval.test.ts | 161 +++++++++++---------- src/useInterval/useIntervalStories.test.ts | 55 +++---- 3 files changed, 123 insertions(+), 133 deletions(-) diff --git a/src/useInterval/useInterval.stories.ts b/src/useInterval/useInterval.stories.ts index f93e41b..1d1110a 100644 --- a/src/useInterval/useInterval.stories.ts +++ b/src/useInterval/useInterval.stories.ts @@ -1,8 +1,9 @@ -/* eslint-disable unicorn/prevent-abbreviations,import/no-extraneous-dependencies */ -import { bind, computed, defineComponent, propType, reactive, ref } from '@muban/muban'; +/* eslint-disable import/no-extraneous-dependencies */ +import { bind, computed, defineComponent, propType, ref } from '@muban/muban'; import type { Story } from '@muban/storybook/types-6-0'; import { html } from '@muban/template'; import { useInterval } from './useInterval'; +import { useStorybookLog } from '../hooks/useStorybookLog'; export default { title: 'useInterval', @@ -20,19 +21,12 @@ export const Demo: Story = () => ({ refs: { label: 'label', startButton: 'start-button', - cancelButton: 'cancel-button', + stopButton: 'stop-button', }, setup({ refs, props }) { - const state = reactive>([]); + const [logBinding, log] = useStorybookLog(refs.label); const isIntervalRunning = ref(false); - function log(message: string) { - state.push(message); - setTimeout(() => { - state.splice(0, 1); - }, 2000); - } - const { startInterval, stopInterval } = useInterval( onInterval, props.interval, @@ -44,13 +38,7 @@ export const Demo: Story = () => ({ } return [ - bind(refs.label, { - html: computed(() => - state - .map((msg) => html`
${msg}
`) - .join(''), - ), - }), + logBinding, bind(refs.startButton, { attr: { disabled: isIntervalRunning, @@ -60,7 +48,7 @@ export const Demo: Story = () => ({ startInterval(); }, }), - bind(refs.cancelButton, { + bind(refs.stopButton, { attr: { disabled: computed(() => !isIntervalRunning.value), }, @@ -73,7 +61,7 @@ export const Demo: Story = () => ({ ]; }, }), - template: ({ startImmediate = false, interval = 1000 }: DemoStoryProps = {}) => html`
html`
= () => ({

Instructions!

- The demo interval is set to 1 second, you can start it by clicking the start button. You can - stop the interval by clicking the cancel button. + The demo interval is set to 2.5 seconds, you can start it by clicking the start button. You + can stop the interval by clicking the stop button.

Test Area
- - ${' '} - + ${' '} +
`, diff --git a/src/useInterval/useInterval.test.ts b/src/useInterval/useInterval.test.ts index e08c865..c33b3d7 100644 --- a/src/useInterval/useInterval.test.ts +++ b/src/useInterval/useInterval.test.ts @@ -1,84 +1,85 @@ -// import { runComponentSetup } from '@muban/test-utils'; -// import { useInterval } from './useInterval'; +import { runComponentSetup } from '@muban/test-utils'; +import { timeout } from '../useTimeout/useTimeout.test.utils'; +import { useInterval } from './useInterval'; jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock()); -describe('useTimeout', () => { - // it('should not crash', async () => { - // await runComponentSetup(() => { - // useInterval(() => undefined); - // }); - // }); - // - // it('should start immediate and be completed after 1ms', async () => { - // const mockHandler = jest.fn(); - // - // await runComponentSetup( - // () => { - // useTimeout(mockHandler, 1); - // }, - // () => timeout(2), - // ); - // - // expect(mockHandler).toBeCalledTimes(1); - // }); - // - // it('should start immediate and not be completed', async () => { - // const mockHandler = jest.fn(); - // - // await runComponentSetup(() => { - // useTimeout(mockHandler, 1); - // }); - // - // expect(mockHandler).toBeCalledTimes(0); - // }); - // - // it('should trigger start and be completed after 1ms', async () => { - // const mockHandler = jest.fn(); - // - // await runComponentSetup( - // () => { - // const { startTimeout } = useTimeout(mockHandler, 1, false); - // - // startTimeout(); - // }, - // () => timeout(2), - // ); - // - // expect(mockHandler).toBeCalledTimes(1); - // }); - // - // it('should trigger cancel once the timeout is started', async () => { - // const mockHandler = jest.fn(); - // - // await runComponentSetup( - // async () => { - // const { startTimeout, cancelTimeout } = useTimeout(mockHandler, 2, false); - // - // startTimeout(); - // await timeout(1); - // cancelTimeout(); - // }, - // () => timeout(3), - // ); - // - // expect(mockHandler).toBeCalledTimes(0); - // }); - // - // it('should start a new timeout before the old one running out and only complete once', async () => { - // const mockHandler = jest.fn(); - // - // await runComponentSetup( - // async () => { - // const { startTimeout } = useTimeout(mockHandler, 2, false); - // - // startTimeout(); - // await timeout(1); - // startTimeout(); - // }, - // () => timeout(5), - // ); - // - // expect(mockHandler).toBeCalledTimes(1); - // }); +describe('useInterval', () => { + it('should not crash', async () => { + await runComponentSetup(() => { + useInterval(() => undefined); + }); + }); + + it('should start immediate and not be completed', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup(() => { + useInterval(mockHandler, 100); + }); + + expect(mockHandler).toBeCalledTimes(0); + }); + + it('should start immediate and be called once', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => useInterval(mockHandler, 100), + async ({ stopInterval }) => { + await timeout(100); + stopInterval(); + }, + ); + + expect(mockHandler).toBeCalledTimes(1); + }); + + it('should trigger start and be stopped after three calls', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => useInterval(mockHandler, 100, false), + async ({ startInterval, stopInterval }) => { + startInterval(); + await timeout(400); + stopInterval(); + }, + ); + + expect(mockHandler).toBeCalledTimes(3); + }); + + it('should trigger stop once the interval is started', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => useInterval(mockHandler, 200, false), + async ({ startInterval, stopInterval }) => { + startInterval(); + await timeout(100); + stopInterval(); + }, + ); + + expect(mockHandler).toBeCalledTimes(0); + }); + + it('should start a new interval before the old one was triggered and only complete once', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => { + return useInterval(mockHandler, 100, false); + }, + async ({ startInterval }) => { + startInterval(); + await timeout(50); + startInterval(); + await timeout(100); + }, + ); + + expect(mockHandler).toBeCalledTimes(1); + }); }); diff --git a/src/useInterval/useIntervalStories.test.ts b/src/useInterval/useIntervalStories.test.ts index 37f6802..b9da1ce 100644 --- a/src/useInterval/useIntervalStories.test.ts +++ b/src/useInterval/useIntervalStories.test.ts @@ -1,38 +1,39 @@ import '@testing-library/jest-dom'; import { waitFor, render } from '@muban/testing-library'; import { Demo } from './useInterval.stories'; +import { timeout } from '../useTimeout/useTimeout.test.utils'; -describe('useTimeout stories', () => { +describe('useInterval stories', () => { it('should render', () => { const { getByText } = render(Demo); expect(getByText('Test Area')).toBeInTheDocument(); }); - // it('should start immediate and be completed after 1ms', async () => { - // const { getByText } = render(Demo, { startImmediate: true, duration: 1 }); - // - // await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); - // }); - // - // it('should start after clicking start and be completed after 1ms', async () => { - // const { getByText, getByRef } = render(Demo, { duration: 1 }); - // const startButton = getByRef('start-button'); - // - // startButton.click(); - // - // await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); - // }); - // - // it('should cancel the timeout after starting', async () => { - // const { getByText, getByRef } = render(Demo, { duration: 100 }); - // const startButton = getByRef('start-button'); - // const cancelButton = getByRef('cancel-button'); - // - // startButton.click(); - // await timeout(1); - // cancelButton.click(); - // - // await waitFor(() => expect(getByText('canceled timeout')).toBeInTheDocument()); - // }); + it('should start immediate and be called after 100ms', async () => { + const { getByText } = render(Demo, { startImmediate: true, interval: 100 }); + + await waitFor(() => expect(getByText('interval called')).toBeInTheDocument()); + }); + + it('should start after clicking start and be called after 100ms', async () => { + const { getByText, getByRef } = render(Demo, { interval: 100 }); + const startButton = getByRef('start-button'); + + startButton.click(); + + await waitFor(() => expect(getByText('interval called')).toBeInTheDocument()); + }); + + it('should stop the interval after starting', async () => { + const { getByText, getByRef } = render(Demo, { interval: 100 }); + const startButton = getByRef('start-button'); + const stopButton = getByRef('stop-button'); + + startButton.click(); + await timeout(100); + stopButton.click(); + + await waitFor(() => expect(getByText('interval stopped')).toBeInTheDocument()); + }); }); From baadfb609a048bc3e7844c104bdcef688cea7f3d Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 09:38:29 +0200 Subject: [PATCH 08/30] Choose set in favour of create because it matches the native name --- src/useInterval/useInterval.stories.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useInterval/useInterval.stories.mdx b/src/useInterval/useInterval.stories.mdx index d096767..726e730 100644 --- a/src/useInterval/useInterval.stories.mdx +++ b/src/useInterval/useInterval.stories.mdx @@ -6,7 +6,7 @@ import { Meta } from '@storybook/addon-docs'; # useInterval -The `useInterval` hook is a wrapper around the native `setInterval`, it allows you to easily create set +The `useInterval` hook is a wrapper around the native `setInterval`, it allows you to easily set an interval within your component that will be auto cancelled when the component unmounts. ## Reference From ec95546f1a0391366e4ffdb58c393591442c1307 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 10:13:23 +0200 Subject: [PATCH 09/30] Update the muban peer dependency version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9341e1..209ef09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "npm": ">= 7.0.0" }, "peerDependencies": { - "@muban/muban": "^1.0.0-alpha.28" + "@muban/muban": "^1.0.0-alpha.34" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 54e581c..e004d88 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "plop": "plop" }, "peerDependencies": { - "@muban/muban": "^1.0.0-alpha.28" + "@muban/muban": "^1.0.0-alpha.34" }, "devDependencies": { "@babel/core": "^7.12.10", From a0fc1646612c3e7d4f5f56f68ed00070e8b1efad Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 10:39:02 +0200 Subject: [PATCH 10/30] Expose the state of the interval --- src/useInterval/useInterval.stories.mdx | 7 ++++--- src/useInterval/useInterval.stories.ts | 5 +---- src/useInterval/useInterval.test.ts | 19 ++++++++++++++++--- src/useInterval/useInterval.ts | 19 ++++++++++++++++--- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/useInterval/useInterval.stories.mdx b/src/useInterval/useInterval.stories.mdx index 726e730..e71e0c1 100644 --- a/src/useInterval/useInterval.stories.mdx +++ b/src/useInterval/useInterval.stories.mdx @@ -25,14 +25,15 @@ function useInterval( * `startImmediate` - Whether or not you want to immediately start the interval. ### Returns -* `{ startInterval, stopInterval }` +* `{ startInterval, stopInterval, isIntervalRunning }` * `startInterval` – A function that starts the interval, any running intervals will automatically be stopped. * `stopInterval` – A function that will stop the current active interval. + * `isIntervalRunning` – A computed ref that keeps track whether or not the interval is running. ## Usage ```ts -const { startInterval, stopInterval } = useInterval(() => { +const { startInterval, stopInterval, isIntervalRunning } = useInterval(() => { console.log('The interval has run') }, 1000, false); ```` @@ -51,7 +52,7 @@ const Demo = defineComponent({ }, 1000); // The interval doesn't start automatically, but requires a user action to start. - const { startInterval, stopInterval } = useInterval(() => { + const { startInterval, stopInterval, isIntervalRunning } = useInterval(() => { console.log('The user-action interval callback is triggered.') }, 1000, false); diff --git a/src/useInterval/useInterval.stories.ts b/src/useInterval/useInterval.stories.ts index 1d1110a..f239416 100644 --- a/src/useInterval/useInterval.stories.ts +++ b/src/useInterval/useInterval.stories.ts @@ -25,9 +25,8 @@ export const Demo: Story = () => ({ }, setup({ refs, props }) { const [logBinding, log] = useStorybookLog(refs.label); - const isIntervalRunning = ref(false); - const { startInterval, stopInterval } = useInterval( + const { startInterval, stopInterval, isIntervalRunning } = useInterval( onInterval, props.interval, props.startImmediate, @@ -44,7 +43,6 @@ export const Demo: Story = () => ({ disabled: isIntervalRunning, }, click() { - isIntervalRunning.value = true; startInterval(); }, }), @@ -53,7 +51,6 @@ export const Demo: Story = () => ({ disabled: computed(() => !isIntervalRunning.value), }, click() { - isIntervalRunning.value = false; log('interval stopped'); stopInterval(); }, diff --git a/src/useInterval/useInterval.test.ts b/src/useInterval/useInterval.test.ts index c33b3d7..c180501 100644 --- a/src/useInterval/useInterval.test.ts +++ b/src/useInterval/useInterval.test.ts @@ -65,13 +65,26 @@ describe('useInterval', () => { expect(mockHandler).toBeCalledTimes(0); }); - it('should start a new interval before the old one was triggered and only complete once', async () => { + it('should know that the interval is running', async () => { const mockHandler = jest.fn(); await runComponentSetup( - () => { - return useInterval(mockHandler, 100, false); + () => useInterval(mockHandler, 200, false), + async ({ startInterval, stopInterval, isIntervalRunning }) => { + startInterval(); + await timeout(100); + expect(isIntervalRunning.value).toEqual(true); + stopInterval(); + expect(isIntervalRunning.value).toEqual(false); }, + ); + }); + + it('should start a new interval before the old one was triggered and only complete once', async () => { + const mockHandler = jest.fn(); + + await runComponentSetup( + () => useInterval(mockHandler, 100, false), async ({ startInterval }) => { startInterval(); await timeout(50); diff --git a/src/useInterval/useInterval.ts b/src/useInterval/useInterval.ts index c9c1486..cd75aec 100644 --- a/src/useInterval/useInterval.ts +++ b/src/useInterval/useInterval.ts @@ -1,4 +1,6 @@ -import { onMounted, onUnmounted } from '@muban/muban'; +import type { ComputedRef } from '@muban/muban'; +import { computed, onMounted, onUnmounted } from '@muban/muban'; +import { ref } from '@muban/muban/dist/esm'; /** * A hook that can be used to call a function on a provided interval, by default the interval @@ -12,15 +14,22 @@ export const useInterval = ( callback: () => void, interval: number = 100, startImmediate: boolean = true, -): { startInterval: () => void; stopInterval: () => void } => { +): { + startInterval: () => void; + stopInterval: () => void; + isIntervalRunning: ComputedRef; +} => { + const isIntervalRunning = ref(false); let handle = -1; function start() { stop(); + isIntervalRunning.value = true; handle = setInterval(callback, interval) as unknown as number; } function stop() { + isIntervalRunning.value = false; clearInterval(handle); } @@ -32,5 +41,9 @@ export const useInterval = ( if (startImmediate) start(); }); - return { startInterval: start, stopInterval: stop }; + return { + startInterval: start, + stopInterval: stop, + isIntervalRunning: computed(() => isIntervalRunning.value), + }; }; From 618955794da76ad5a8994112db9a138c9944e958 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 11:11:34 +0200 Subject: [PATCH 11/30] Remove the unused ref import --- src/useInterval/useInterval.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useInterval/useInterval.stories.ts b/src/useInterval/useInterval.stories.ts index f239416..e6fb12e 100644 --- a/src/useInterval/useInterval.stories.ts +++ b/src/useInterval/useInterval.stories.ts @@ -1,5 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { bind, computed, defineComponent, propType, ref } from '@muban/muban'; +import { bind, computed, defineComponent, propType } from '@muban/muban'; import type { Story } from '@muban/storybook/types-6-0'; import { html } from '@muban/template'; import { useInterval } from './useInterval'; From 1a4e173e1821e2bc3f1e234ea09e80747f39b3f8 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 11:11:47 +0200 Subject: [PATCH 12/30] Move the ref import to the main one --- src/useInterval/useInterval.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/useInterval/useInterval.ts b/src/useInterval/useInterval.ts index cd75aec..4485acf 100644 --- a/src/useInterval/useInterval.ts +++ b/src/useInterval/useInterval.ts @@ -1,6 +1,5 @@ import type { ComputedRef } from '@muban/muban'; -import { computed, onMounted, onUnmounted } from '@muban/muban'; -import { ref } from '@muban/muban/dist/esm'; +import { ref, computed, onMounted, onUnmounted } from '@muban/muban'; /** * A hook that can be used to call a function on a provided interval, by default the interval From d9c3bedd2e560c2ff04d39ddbeafb5e0cd351067 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:15:18 +0200 Subject: [PATCH 13/30] Implement the userEvent on the userIntervalStories.test --- src/useInterval/useIntervalStories.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/useInterval/useIntervalStories.test.ts b/src/useInterval/useIntervalStories.test.ts index b9da1ce..d058945 100644 --- a/src/useInterval/useIntervalStories.test.ts +++ b/src/useInterval/useIntervalStories.test.ts @@ -1,9 +1,12 @@ import '@testing-library/jest-dom'; import { waitFor, render } from '@muban/testing-library'; +import userEvent from '@testing-library/user-event'; import { Demo } from './useInterval.stories'; import { timeout } from '../useTimeout/useTimeout.test.utils'; describe('useInterval stories', () => { + const { click } = userEvent.setup(); + it('should render', () => { const { getByText } = render(Demo); @@ -20,7 +23,7 @@ describe('useInterval stories', () => { const { getByText, getByRef } = render(Demo, { interval: 100 }); const startButton = getByRef('start-button'); - startButton.click(); + click(startButton); await waitFor(() => expect(getByText('interval called')).toBeInTheDocument()); }); @@ -30,9 +33,9 @@ describe('useInterval stories', () => { const startButton = getByRef('start-button'); const stopButton = getByRef('stop-button'); - startButton.click(); + click(startButton); await timeout(100); - stopButton.click(); + click(stopButton); await waitFor(() => expect(getByText('interval stopped')).toBeInTheDocument()); }); From 55bd78e5f544b484136b7dc8376e012007dd3f4c Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:16:25 +0200 Subject: [PATCH 14/30] Implement the userEvent on the useTimeoutStories.test --- src/useTimeout/useTimeoutStories.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/useTimeout/useTimeoutStories.test.ts b/src/useTimeout/useTimeoutStories.test.ts index c6fb258..cb93bc4 100644 --- a/src/useTimeout/useTimeoutStories.test.ts +++ b/src/useTimeout/useTimeoutStories.test.ts @@ -1,9 +1,12 @@ import '@testing-library/jest-dom'; import { waitFor, render } from '@muban/testing-library'; +import userEvent from '@testing-library/user-event'; import { Demo } from './useTimeout.stories'; import { timeout } from './useTimeout.test.utils'; describe('useTimeout stories', () => { + const { click } = userEvent.setup(); + it('should render', () => { const { getByText } = render(Demo); @@ -20,7 +23,7 @@ describe('useTimeout stories', () => { const { getByText, getByRef } = render(Demo, { duration: 1 }); const startButton = getByRef('start-button'); - startButton.click(); + click(startButton); await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); }); @@ -30,9 +33,9 @@ describe('useTimeout stories', () => { const startButton = getByRef('start-button'); const cancelButton = getByRef('cancel-button'); - startButton.click(); + click(startButton); await timeout(1); - cancelButton.click(); + click(cancelButton); await waitFor(() => expect(getByText('canceled timeout')).toBeInTheDocument()); }); From 28aa2553c25f3642a8c22d64e9c516373d201366 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:17:08 +0200 Subject: [PATCH 15/30] Implement jest.useFakeTimers on the main tests --- src/useInterval/useInterval.test.ts | 55 +++++++++++++++-------------- src/useTimeout/useTimeout.test.ts | 52 +++++++++++++-------------- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/useInterval/useInterval.test.ts b/src/useInterval/useInterval.test.ts index c180501..d99733f 100644 --- a/src/useInterval/useInterval.test.ts +++ b/src/useInterval/useInterval.test.ts @@ -1,33 +1,36 @@ import { runComponentSetup } from '@muban/test-utils'; -import { timeout } from '../useTimeout/useTimeout.test.utils'; import { useInterval } from './useInterval'; jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock()); describe('useInterval', () => { - it('should not crash', async () => { - await runComponentSetup(() => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + it('should not crash', () => { + runComponentSetup(() => { useInterval(() => undefined); }); }); - it('should start immediate and not be completed', async () => { + it('should start immediate and not be completed', () => { const mockHandler = jest.fn(); - await runComponentSetup(() => { + runComponentSetup(() => { useInterval(mockHandler, 100); }); expect(mockHandler).toBeCalledTimes(0); }); - it('should start immediate and be called once', async () => { + it('should start immediate and be called once', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useInterval(mockHandler, 100), - async ({ stopInterval }) => { - await timeout(100); + ({ stopInterval }) => { + jest.advanceTimersByTime(100); stopInterval(); }, ); @@ -35,14 +38,14 @@ describe('useInterval', () => { expect(mockHandler).toBeCalledTimes(1); }); - it('should trigger start and be stopped after three calls', async () => { + it('should trigger start and be stopped after three calls', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useInterval(mockHandler, 100, false), - async ({ startInterval, stopInterval }) => { + ({ startInterval, stopInterval }) => { startInterval(); - await timeout(400); + jest.advanceTimersByTime(300); stopInterval(); }, ); @@ -50,14 +53,14 @@ describe('useInterval', () => { expect(mockHandler).toBeCalledTimes(3); }); - it('should trigger stop once the interval is started', async () => { + it('should trigger stop once the interval is started', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useInterval(mockHandler, 200, false), - async ({ startInterval, stopInterval }) => { + ({ startInterval, stopInterval }) => { startInterval(); - await timeout(100); + jest.advanceTimersByTime(100); stopInterval(); }, ); @@ -65,14 +68,14 @@ describe('useInterval', () => { expect(mockHandler).toBeCalledTimes(0); }); - it('should know that the interval is running', async () => { + it('should know that the interval is running', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useInterval(mockHandler, 200, false), - async ({ startInterval, stopInterval, isIntervalRunning }) => { + ({ startInterval, stopInterval, isIntervalRunning }) => { startInterval(); - await timeout(100); + jest.advanceTimersByTime(100); expect(isIntervalRunning.value).toEqual(true); stopInterval(); expect(isIntervalRunning.value).toEqual(false); @@ -80,16 +83,16 @@ describe('useInterval', () => { ); }); - it('should start a new interval before the old one was triggered and only complete once', async () => { + it('should start a new interval before the old one was triggered and only complete once', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useInterval(mockHandler, 100, false), - async ({ startInterval }) => { + ({ startInterval }) => { startInterval(); - await timeout(50); + jest.advanceTimersByTime(50); startInterval(); - await timeout(100); + jest.advanceTimersByTime(100); }, ); diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts index 6b1d281..8f635e5 100644 --- a/src/useTimeout/useTimeout.test.ts +++ b/src/useTimeout/useTimeout.test.ts @@ -1,61 +1,61 @@ import { runComponentSetup } from '@muban/test-utils'; import { useTimeout } from './useTimeout'; -import { timeout } from './useTimeout.test.utils'; jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock()); describe('useTimeout', () => { - it('should not crash', async () => { - await runComponentSetup(() => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + it('should not crash', () => { + runComponentSetup(() => { useTimeout(() => undefined); }); }); - it('should start immediate and be completed after 1ms', async () => { + it('should start immediate and be completed after 1ms', () => { const mockHandler = jest.fn(); - await runComponentSetup( - () => { - useTimeout(mockHandler, 100); - }, - () => timeout(200), - ); + runComponentSetup(() => { + useTimeout(mockHandler, 100); + }); + jest.advanceTimersByTime(200); expect(mockHandler).toBeCalledTimes(1); }); - it('should start immediate and not be completed', async () => { + it('should start immediate and not be completed', () => { const mockHandler = jest.fn(); - await runComponentSetup(() => { + runComponentSetup(() => { useTimeout(mockHandler, 100); }); expect(mockHandler).toBeCalledTimes(0); }); - it('should trigger start and be completed after 1ms', async () => { + it('should trigger start and be completed after 1ms', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useTimeout(mockHandler, 100, false), - async ({ startTimeout }) => { + ({ startTimeout }) => { startTimeout(); - await timeout(200); }, ); - + jest.advanceTimersByTime(200); expect(mockHandler).toBeCalledTimes(1); }); - it('should trigger cancel once the timeout is started', async () => { + it('should trigger cancel once the timeout is started', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useTimeout(mockHandler, 500, false), - async ({ startTimeout, cancelTimeout }) => { + ({ startTimeout, cancelTimeout }) => { startTimeout(); - await timeout(100); + jest.advanceTimersByTime(100); cancelTimeout(); }, ); @@ -63,16 +63,16 @@ describe('useTimeout', () => { expect(mockHandler).toBeCalledTimes(0); }); - it('should start a new timeout before the old one running out and only complete once', async () => { + it('should start a new timeout before the old one running out and only complete once', () => { const mockHandler = jest.fn(); - await runComponentSetup( + runComponentSetup( () => useTimeout(mockHandler, 200, false), - async ({ startTimeout }) => { + ({ startTimeout }) => { startTimeout(); - await timeout(100); + jest.advanceTimersByTime(100); startTimeout(); - await timeout(300); + jest.advanceTimersByTime(300); }, ); From a48a9a60b4e93b51f8f5949e2e7018b62598514a Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:17:40 +0200 Subject: [PATCH 16/30] Rename the handle variable to intervalId --- src/useInterval/useInterval.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/useInterval/useInterval.ts b/src/useInterval/useInterval.ts index 4485acf..46b8a89 100644 --- a/src/useInterval/useInterval.ts +++ b/src/useInterval/useInterval.ts @@ -19,17 +19,17 @@ export const useInterval = ( isIntervalRunning: ComputedRef; } => { const isIntervalRunning = ref(false); - let handle = -1; + let intervalId = -1; function start() { stop(); + intervalId = setInterval(callback, interval) as unknown as number; isIntervalRunning.value = true; - handle = setInterval(callback, interval) as unknown as number; } function stop() { + clearInterval(intervalId); isIntervalRunning.value = false; - clearInterval(handle); } onUnmounted(() => { From d4a7889497698d12af65b1d51b53cc22e715beaf Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:17:58 +0200 Subject: [PATCH 17/30] Rename the handle variable to timeoutId --- src/useTimeout/useTimeout.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/useTimeout/useTimeout.ts b/src/useTimeout/useTimeout.ts index df9f5b2..662699b 100644 --- a/src/useTimeout/useTimeout.ts +++ b/src/useTimeout/useTimeout.ts @@ -13,15 +13,15 @@ export const useTimeout = ( duration: number = 100, startImmediate: boolean = true, ): { startTimeout: () => void; cancelTimeout: () => void } => { - let handle = -1; + let timeoutId = -1; function start() { cancel(); - handle = setTimeout(callback, duration) as unknown as number; + timeoutId = setTimeout(callback, duration) as unknown as number; } function cancel() { - clearTimeout(handle); + clearTimeout(timeoutId); } onUnmounted(() => { From c56236b487800a6442b61044757fe61c3bcb3968 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:19:05 +0200 Subject: [PATCH 18/30] Increase the time to ensure stop actually works --- src/useInterval/useInterval.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/useInterval/useInterval.test.ts b/src/useInterval/useInterval.test.ts index d99733f..974c215 100644 --- a/src/useInterval/useInterval.test.ts +++ b/src/useInterval/useInterval.test.ts @@ -62,6 +62,7 @@ describe('useInterval', () => { startInterval(); jest.advanceTimersByTime(100); stopInterval(); + jest.advanceTimersByTime(200); }, ); From 96276ceacf1a89e7c5c87d75abd7898122755fa1 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:46:47 +0200 Subject: [PATCH 19/30] Remove the story that tests the stop button --- src/useInterval/useIntervalStories.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/useInterval/useIntervalStories.test.ts b/src/useInterval/useIntervalStories.test.ts index d058945..ca56d5e 100644 --- a/src/useInterval/useIntervalStories.test.ts +++ b/src/useInterval/useIntervalStories.test.ts @@ -27,16 +27,4 @@ describe('useInterval stories', () => { await waitFor(() => expect(getByText('interval called')).toBeInTheDocument()); }); - - it('should stop the interval after starting', async () => { - const { getByText, getByRef } = render(Demo, { interval: 100 }); - const startButton = getByRef('start-button'); - const stopButton = getByRef('stop-button'); - - click(startButton); - await timeout(100); - click(stopButton); - - await waitFor(() => expect(getByText('interval stopped')).toBeInTheDocument()); - }); }); From 677865306799ba07763e5d41220dfa5e06adf91e Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:47:50 +0200 Subject: [PATCH 20/30] Move up the callback methods --- src/useInterval/useInterval.stories.ts | 54 +++++++++++++------------- src/useTimeout/useTimeout.stories.ts | 10 ++--- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/useInterval/useInterval.stories.ts b/src/useInterval/useInterval.stories.ts index e6fb12e..c922d6e 100644 --- a/src/useInterval/useInterval.stories.ts +++ b/src/useInterval/useInterval.stories.ts @@ -26,16 +26,16 @@ export const Demo: Story = () => ({ setup({ refs, props }) { const [logBinding, log] = useStorybookLog(refs.label); + function onInterval() { + log('interval called'); + } + const { startInterval, stopInterval, isIntervalRunning } = useInterval( onInterval, props.interval, props.startImmediate, ); - function onInterval() { - log('interval called'); - } - return [ logBinding, bind(refs.startButton, { @@ -58,29 +58,31 @@ export const Demo: Story = () => ({ ]; }, }), - template: ({ startImmediate = false, interval = 2500 }: DemoStoryProps = {}) => html`
-
-

Instructions!

-

- The demo interval is set to 2.5 seconds, you can start it by clicking the start button. You - can stop the interval by clicking the stop button. -

-
-
-
-
Test Area
-
- + template: ({ startImmediate = false, interval = 2500 }: DemoStoryProps = {}) => html` +
+
+

Instructions!

+

+ The demo interval is set to 2.5 seconds, you can start it by clicking the start button. + You + can stop the interval by clicking the stop button. +

+
+
+
+
Test Area
+
+ ${' '} - + +
-
-
`, +
`, }); Demo.storyName = 'demo'; diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts index 3306fcc..5dd221d 100644 --- a/src/useTimeout/useTimeout.stories.ts +++ b/src/useTimeout/useTimeout.stories.ts @@ -27,17 +27,17 @@ export const Demo: Story = () => ({ const [logBinding, log] = useStorybookLog(refs.label); const isTimeoutRunning = ref(false); + function onTimeoutComplete() { + isTimeoutRunning.value = false; + log('timeout complete'); + } + const { startTimeout, cancelTimeout } = useTimeout( onTimeoutComplete, props.duration, props.startImmediate, ); - function onTimeoutComplete() { - isTimeoutRunning.value = false; - log('timeout complete'); - } - return [ logBinding, bind(refs.startButton, { From 6a55551daef5c04be34e202e7f10575026f73ec6 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 12:48:59 +0200 Subject: [PATCH 21/30] Update the test description --- src/useTimeout/useTimeout.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts index 8f635e5..2506831 100644 --- a/src/useTimeout/useTimeout.test.ts +++ b/src/useTimeout/useTimeout.test.ts @@ -14,7 +14,7 @@ describe('useTimeout', () => { }); }); - it('should start immediate and be completed after 1ms', () => { + it('should start immediate and be completed after 100ms', () => { const mockHandler = jest.fn(); runComponentSetup(() => { @@ -35,7 +35,7 @@ describe('useTimeout', () => { expect(mockHandler).toBeCalledTimes(0); }); - it('should trigger start and be completed after 1ms', () => { + it('should trigger start and be completed after 100ms', () => { const mockHandler = jest.fn(); runComponentSetup( From 078f18b4a67004647adf0e9e8882c5059d1a7838 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:10:29 +0200 Subject: [PATCH 22/30] Fix prettier formatting --- src/useInterval/useInterval.stories.ts | 46 ++++++++++++-------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/useInterval/useInterval.stories.ts b/src/useInterval/useInterval.stories.ts index c922d6e..6c6ab41 100644 --- a/src/useInterval/useInterval.stories.ts +++ b/src/useInterval/useInterval.stories.ts @@ -58,31 +58,29 @@ export const Demo: Story = () => ({ ]; }, }), - template: ({ startImmediate = false, interval = 2500 }: DemoStoryProps = {}) => html` -
-
-

Instructions!

-

- The demo interval is set to 2.5 seconds, you can start it by clicking the start button. - You - can stop the interval by clicking the stop button. -

-
-
-
-
Test Area
-
- + template: ({ startImmediate = false, interval = 2500 }: DemoStoryProps = {}) => html`
+
+

Instructions!

+

+ The demo interval is set to 2.5 seconds, you can start it by clicking the start button. You + can stop the interval by clicking the stop button. +

+
+
+
+
Test Area
+
+ ${' '} - -
+
-
`, +
+
`, }); Demo.storyName = 'demo'; From 0174fe2abf96f9853b635d61d78bef1865bf8c64 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:10:34 +0200 Subject: [PATCH 23/30] Switch to a Readonly Ref instead of a ComputedRef --- src/useInterval/useInterval.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/useInterval/useInterval.ts b/src/useInterval/useInterval.ts index 46b8a89..402e82b 100644 --- a/src/useInterval/useInterval.ts +++ b/src/useInterval/useInterval.ts @@ -1,5 +1,5 @@ -import type { ComputedRef } from '@muban/muban'; -import { ref, computed, onMounted, onUnmounted } from '@muban/muban'; +import type { Ref } from '@muban/muban'; +import { ref, onMounted, onUnmounted, readonly } from '@muban/muban'; /** * A hook that can be used to call a function on a provided interval, by default the interval @@ -16,7 +16,7 @@ export const useInterval = ( ): { startInterval: () => void; stopInterval: () => void; - isIntervalRunning: ComputedRef; + isIntervalRunning: Readonly>; } => { const isIntervalRunning = ref(false); let intervalId = -1; @@ -43,6 +43,6 @@ export const useInterval = ( return { startInterval: start, stopInterval: stop, - isIntervalRunning: computed(() => isIntervalRunning.value), + isIntervalRunning: readonly(isIntervalRunning), }; }; From 8d8de00dfe3ffd07ff23f2b6a5ccae7ff6024b83 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:20:11 +0200 Subject: [PATCH 24/30] Use the intervalId to keep track if the interval is running. --- src/useInterval/useInterval.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/useInterval/useInterval.ts b/src/useInterval/useInterval.ts index 402e82b..c44264d 100644 --- a/src/useInterval/useInterval.ts +++ b/src/useInterval/useInterval.ts @@ -1,5 +1,8 @@ -import type { Ref } from '@muban/muban'; -import { ref, onMounted, onUnmounted, readonly } from '@muban/muban'; +import type { ComputedRef } from '@muban/muban'; +import { ref, onMounted, onUnmounted, computed } from '@muban/muban'; + +// We use `-1` as the value to indicate that an interval is not running. +const NOT_RUNNING = -1; /** * A hook that can be used to call a function on a provided interval, by default the interval @@ -16,20 +19,18 @@ export const useInterval = ( ): { startInterval: () => void; stopInterval: () => void; - isIntervalRunning: Readonly>; + isIntervalRunning: ComputedRef; } => { - const isIntervalRunning = ref(false); - let intervalId = -1; + const intervalId = ref(NOT_RUNNING); function start() { stop(); - intervalId = setInterval(callback, interval) as unknown as number; - isIntervalRunning.value = true; + intervalId.value = setInterval(callback, interval) as unknown as number; } function stop() { - clearInterval(intervalId); - isIntervalRunning.value = false; + clearInterval(intervalId.value); + intervalId.value = NOT_RUNNING; } onUnmounted(() => { @@ -43,6 +44,6 @@ export const useInterval = ( return { startInterval: start, stopInterval: stop, - isIntervalRunning: readonly(isIntervalRunning), + isIntervalRunning: computed(() => intervalId.value !== NOT_RUNNING), }; }; From 4f85184e270ebfe85822625057923bc66f4f4d42 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:26:26 +0200 Subject: [PATCH 25/30] Implement the isTimeoutRunning state in the useTimeout hook to stay consistent with the useInterval hook --- src/useTimeout/useTimeout.stories.ts | 6 +----- src/useTimeout/useTimeout.ts | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts index 5dd221d..13c6f48 100644 --- a/src/useTimeout/useTimeout.stories.ts +++ b/src/useTimeout/useTimeout.stories.ts @@ -25,14 +25,12 @@ export const Demo: Story = () => ({ }, setup({ refs, props }) { const [logBinding, log] = useStorybookLog(refs.label); - const isTimeoutRunning = ref(false); function onTimeoutComplete() { - isTimeoutRunning.value = false; log('timeout complete'); } - const { startTimeout, cancelTimeout } = useTimeout( + const { startTimeout, cancelTimeout, isTimeoutRunning } = useTimeout( onTimeoutComplete, props.duration, props.startImmediate, @@ -45,7 +43,6 @@ export const Demo: Story = () => ({ disabled: isTimeoutRunning, }, click() { - isTimeoutRunning.value = true; startTimeout(); }, }), @@ -54,7 +51,6 @@ export const Demo: Story = () => ({ disabled: computed(() => !isTimeoutRunning.value), }, click() { - isTimeoutRunning.value = false; log('canceled timeout'); cancelTimeout(); }, diff --git a/src/useTimeout/useTimeout.ts b/src/useTimeout/useTimeout.ts index 662699b..43820e6 100644 --- a/src/useTimeout/useTimeout.ts +++ b/src/useTimeout/useTimeout.ts @@ -1,4 +1,8 @@ -import { onMounted, onUnmounted } from '@muban/muban'; +import type { ComputedRef } from '@muban/muban'; +import { computed, onMounted, onUnmounted, ref } from '@muban/muban'; + +// We use `-1` as the value to indicate that an interval is not running. +const NOT_RUNNING = -1; /** * A hook that can be used to apply a timeout to a certain function but also give you the option @@ -12,16 +16,24 @@ export const useTimeout = ( callback: () => void, duration: number = 100, startImmediate: boolean = true, -): { startTimeout: () => void; cancelTimeout: () => void } => { - let timeoutId = -1; +): { + startTimeout: () => void; + cancelTimeout: () => void; + isTimeoutRunning: ComputedRef; +} => { + const timeoutId = ref(NOT_RUNNING); function start() { cancel(); - timeoutId = setTimeout(callback, duration) as unknown as number; + timeoutId.value = setTimeout(() => { + timeoutId.value = NOT_RUNNING; + callback(); + }, duration) as unknown as number; } function cancel() { - clearTimeout(timeoutId); + clearTimeout(timeoutId.value); + timeoutId.value = NOT_RUNNING; } onUnmounted(() => { @@ -32,5 +44,9 @@ export const useTimeout = ( if (startImmediate) start(); }); - return { startTimeout: start, cancelTimeout: cancel }; + return { + startTimeout: start, + cancelTimeout: cancel, + isTimeoutRunning: computed(() => timeoutId.value !== NOT_RUNNING), + }; }; From 483fc839f445720c627214b06b74878c231eeb17 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:29:25 +0200 Subject: [PATCH 26/30] Update the docs to include the new isTimeoutRunning --- src/useTimeout/useTimeout.stories.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/useTimeout/useTimeout.stories.mdx b/src/useTimeout/useTimeout.stories.mdx index a8e53b9..92ec1c7 100644 --- a/src/useTimeout/useTimeout.stories.mdx +++ b/src/useTimeout/useTimeout.stories.mdx @@ -25,14 +25,15 @@ function useTimeout( * `startImmediate` - Whether or not you want to immediately start the timeout. ### Returns -* `{ startTimeout, cancelTimeout }` +* `{ startTimeout, cancelTimeout, isTimeoutRunning }` * `startTimeout` – A function that starts the timeout, any running timeouts will automatically be cancelled. * `cancelTimeout` – A function that will cancel the current active timeout. + * `isTimeoutRunning` – A computed ref that keeps track whether or not the timeout is running. ## Usage ```ts -const { startTimeout, cancelTimeout } = useTimeout(() => { +const { startTimeout, cancelTimeout, isTimeoutRunning } = useTimeout(() => { console.log('The timeout has run out') }, 1000, false); ```` From 70a6a7cee9c2e5bf5c5b9060bae83469c8c41434 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:29:44 +0200 Subject: [PATCH 27/30] Add a test to check if the isTimeoutRunning is correctly set --- src/useTimeout/useTimeout.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts index 2506831..6ac1f9f 100644 --- a/src/useTimeout/useTimeout.test.ts +++ b/src/useTimeout/useTimeout.test.ts @@ -48,6 +48,21 @@ describe('useTimeout', () => { expect(mockHandler).toBeCalledTimes(1); }); + it('should know that the timeout is running', () => { + const mockHandler = jest.fn(); + + runComponentSetup( + () => useTimeout(mockHandler, 200, false), + ({ startTimeout, cancelTimeout, isTimeoutRunning }) => { + startTimeout(); + jest.advanceTimersByTime(100); + expect(isTimeoutRunning.value).toEqual(true); + cancelTimeout(); + expect(isTimeoutRunning.value).toEqual(false); + }, + ); + }); + it('should trigger cancel once the timeout is started', () => { const mockHandler = jest.fn(); From 13e6fb5fa5b63288bd3e65fe7e55387fa7455c8c Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:32:15 +0200 Subject: [PATCH 28/30] Include the isIntervalRunning type to the useInterval docs --- src/useInterval/useInterval.stories.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useInterval/useInterval.stories.mdx b/src/useInterval/useInterval.stories.mdx index e71e0c1..8d9e1d4 100644 --- a/src/useInterval/useInterval.stories.mdx +++ b/src/useInterval/useInterval.stories.mdx @@ -16,7 +16,7 @@ function useInterval( callback: () => void, interval?: number = 100, startImmediate?: boolean = true, -): { startInterval: () => void, stopInterval: () => void } +): { startInterval: () => void, stopInterval: () => void; isIntervalRunning: ComputedRef } ``` ### Parameters From e8df249479613c0565926e9d6c8e7c39a6ac2914 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:35:51 +0200 Subject: [PATCH 29/30] Remove the usage of the actual timeout in the tests --- src/useInterval/useIntervalStories.test.ts | 1 - src/useTimeout/useTimeout.test.utils.ts | 10 ---------- src/useTimeout/useTimeoutStories.test.ts | 13 ------------- 3 files changed, 24 deletions(-) delete mode 100644 src/useTimeout/useTimeout.test.utils.ts diff --git a/src/useInterval/useIntervalStories.test.ts b/src/useInterval/useIntervalStories.test.ts index ca56d5e..9b2180d 100644 --- a/src/useInterval/useIntervalStories.test.ts +++ b/src/useInterval/useIntervalStories.test.ts @@ -2,7 +2,6 @@ import '@testing-library/jest-dom'; import { waitFor, render } from '@muban/testing-library'; import userEvent from '@testing-library/user-event'; import { Demo } from './useInterval.stories'; -import { timeout } from '../useTimeout/useTimeout.test.utils'; describe('useInterval stories', () => { const { click } = userEvent.setup(); diff --git a/src/useTimeout/useTimeout.test.utils.ts b/src/useTimeout/useTimeout.test.utils.ts deleted file mode 100644 index 9e0c9f8..0000000 --- a/src/useTimeout/useTimeout.test.utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Util to easily test delayed callbacks - * - * @param duration The duration of the timeout you want to apply - */ -export async function timeout(duration: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, duration); - }); -} diff --git a/src/useTimeout/useTimeoutStories.test.ts b/src/useTimeout/useTimeoutStories.test.ts index cb93bc4..a111241 100644 --- a/src/useTimeout/useTimeoutStories.test.ts +++ b/src/useTimeout/useTimeoutStories.test.ts @@ -2,7 +2,6 @@ import '@testing-library/jest-dom'; import { waitFor, render } from '@muban/testing-library'; import userEvent from '@testing-library/user-event'; import { Demo } from './useTimeout.stories'; -import { timeout } from './useTimeout.test.utils'; describe('useTimeout stories', () => { const { click } = userEvent.setup(); @@ -27,16 +26,4 @@ describe('useTimeout stories', () => { await waitFor(() => expect(getByText('timeout complete')).toBeInTheDocument()); }); - - it('should cancel the timeout after starting', async () => { - const { getByText, getByRef } = render(Demo, { duration: 100 }); - const startButton = getByRef('start-button'); - const cancelButton = getByRef('cancel-button'); - - click(startButton); - await timeout(1); - click(cancelButton); - - await waitFor(() => expect(getByText('canceled timeout')).toBeInTheDocument()); - }); }); From 979e56e35a13254af7db401c6d4c9b3b5ea5daf6 Mon Sep 17 00:00:00 2001 From: Lars van Braam Date: Fri, 29 Apr 2022 13:36:38 +0200 Subject: [PATCH 30/30] rename cancelTimeout to clearTimeout to be more consistent with the native naming --- src/useTimeout/useTimeout.stories.mdx | 14 +++++++------- src/useTimeout/useTimeout.stories.ts | 14 ++++++-------- src/useTimeout/useTimeout.test.ts | 8 ++++---- src/useTimeout/useTimeout.ts | 10 +++++----- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/useTimeout/useTimeout.stories.mdx b/src/useTimeout/useTimeout.stories.mdx index 92ec1c7..28c6dc5 100644 --- a/src/useTimeout/useTimeout.stories.mdx +++ b/src/useTimeout/useTimeout.stories.mdx @@ -16,7 +16,7 @@ function useTimeout( callback: () => void, duration?: number = 100, startImmediate?: boolean = true, -): { startTimeout: () => void, cancelTimeout: () => void } +): { startTimeout: () => void, clearButton: () => void, isTimeoutRunning: ComputedRef } ``` ### Parameters @@ -25,15 +25,15 @@ function useTimeout( * `startImmediate` - Whether or not you want to immediately start the timeout. ### Returns -* `{ startTimeout, cancelTimeout, isTimeoutRunning }` +* `{ startTimeout, clearTimeout, isTimeoutRunning }` * `startTimeout` – A function that starts the timeout, any running timeouts will automatically be cancelled. - * `cancelTimeout` – A function that will cancel the current active timeout. + * `clearTimeout` – A function that will cancel the current active timeout. * `isTimeoutRunning` – A computed ref that keeps track whether or not the timeout is running. ## Usage ```ts -const { startTimeout, cancelTimeout, isTimeoutRunning } = useTimeout(() => { +const { startTimeout, clearTimeout, isTimeoutRunning } = useTimeout(() => { console.log('The timeout has run out') }, 1000, false); ```` @@ -43,7 +43,7 @@ const Demo = defineComponent({ name: 'demo', refs: { startBtn: 'start-button' - cancelButton: 'cancel-button' + clearButton: 'clear-button' }, setup({ refs }) { // The timeout runs as soon as the component is mounted. @@ -52,7 +52,7 @@ const Demo = defineComponent({ }, 1000); // The timeout doesn't start automatically, but requires a user action to start. - const { startTimeout, cancelTimeout } = useTimeout(() => { + const { startTimeout, clearTimeout } = useTimeout(() => { console.log('The timeout has run out') }, 1000, false); @@ -64,7 +64,7 @@ const Demo = defineComponent({ }), bind(refs.cancelButton, { click() { - cancelTimeout(); // This cancels the timeout if it's active. + clearTimeout(); // This clears the timeout if it's active. } }) ] diff --git a/src/useTimeout/useTimeout.stories.ts b/src/useTimeout/useTimeout.stories.ts index 13c6f48..603e658 100644 --- a/src/useTimeout/useTimeout.stories.ts +++ b/src/useTimeout/useTimeout.stories.ts @@ -21,7 +21,7 @@ export const Demo: Story = () => ({ refs: { label: 'label', startButton: 'start-button', - cancelButton: 'cancel-button', + clearButton: 'clear-button', }, setup({ refs, props }) { const [logBinding, log] = useStorybookLog(refs.label); @@ -30,7 +30,7 @@ export const Demo: Story = () => ({ log('timeout complete'); } - const { startTimeout, cancelTimeout, isTimeoutRunning } = useTimeout( + const { startTimeout, clearTimeout, isTimeoutRunning } = useTimeout( onTimeoutComplete, props.duration, props.startImmediate, @@ -46,13 +46,13 @@ export const Demo: Story = () => ({ startTimeout(); }, }), - bind(refs.cancelButton, { + bind(refs.clearButton, { attr: { disabled: computed(() => !isTimeoutRunning.value), }, click() { - log('canceled timeout'); - cancelTimeout(); + log('cleared timeout'); + clearTimeout(); }, }), ]; @@ -76,9 +76,7 @@ export const Demo: Story = () => ({
${' '} - +
`, diff --git a/src/useTimeout/useTimeout.test.ts b/src/useTimeout/useTimeout.test.ts index 6ac1f9f..0b0563a 100644 --- a/src/useTimeout/useTimeout.test.ts +++ b/src/useTimeout/useTimeout.test.ts @@ -53,11 +53,11 @@ describe('useTimeout', () => { runComponentSetup( () => useTimeout(mockHandler, 200, false), - ({ startTimeout, cancelTimeout, isTimeoutRunning }) => { + ({ startTimeout, clearTimeout, isTimeoutRunning }) => { startTimeout(); jest.advanceTimersByTime(100); expect(isTimeoutRunning.value).toEqual(true); - cancelTimeout(); + clearTimeout(); expect(isTimeoutRunning.value).toEqual(false); }, ); @@ -68,10 +68,10 @@ describe('useTimeout', () => { runComponentSetup( () => useTimeout(mockHandler, 500, false), - ({ startTimeout, cancelTimeout }) => { + ({ startTimeout, clearTimeout }) => { startTimeout(); jest.advanceTimersByTime(100); - cancelTimeout(); + clearTimeout(); }, ); diff --git a/src/useTimeout/useTimeout.ts b/src/useTimeout/useTimeout.ts index 43820e6..88567ea 100644 --- a/src/useTimeout/useTimeout.ts +++ b/src/useTimeout/useTimeout.ts @@ -18,26 +18,26 @@ export const useTimeout = ( startImmediate: boolean = true, ): { startTimeout: () => void; - cancelTimeout: () => void; + clearTimeout: () => void; isTimeoutRunning: ComputedRef; } => { const timeoutId = ref(NOT_RUNNING); function start() { - cancel(); + clear(); timeoutId.value = setTimeout(() => { timeoutId.value = NOT_RUNNING; callback(); }, duration) as unknown as number; } - function cancel() { + function clear() { clearTimeout(timeoutId.value); timeoutId.value = NOT_RUNNING; } onUnmounted(() => { - cancel(); + clear(); }); onMounted(() => { @@ -46,7 +46,7 @@ export const useTimeout = ( return { startTimeout: start, - cancelTimeout: cancel, + clearTimeout: clear, isTimeoutRunning: computed(() => timeoutId.value !== NOT_RUNNING), }; };