@thinkdx/storybook-addon-chronokit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/index.cjs +136 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/dist/mockDateDecorator.d.cts +15 -0
- package/dist/mockDateDecorator.d.ts +15 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick Ferraro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# storybook-addon-chronokit
|
|
2
|
+
|
|
3
|
+
Mock time in your Storybook stories. Chronokit replaces the global clock so a
|
|
4
|
+
story can render at any moment you choose — **frozen** for a deterministic
|
|
5
|
+
screenshot, or **fast-forwarded** so a minutes-long flow plays out in seconds.
|
|
6
|
+
|
|
7
|
+
**▶︎ Live demo & docs: https://thinkdx.github.io/storybook-addon-chronokit/**
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
It's a single preview decorator driven by one story parameter:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
parameters: {
|
|
15
|
+
date: {
|
|
16
|
+
now: new Date('2025-06-01T12:00:00').getTime(),
|
|
17
|
+
canProgress: true, // false = frozen
|
|
18
|
+
clockSpeed: 30, // optional real-time multiplier
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Why?
|
|
24
|
+
|
|
25
|
+
Anything that renders differently depending on "what time is it now?" is painful
|
|
26
|
+
to build and review against the real clock:
|
|
27
|
+
|
|
28
|
+
- **Countdowns and timers** — you'd have to wait for them to reach interesting states.
|
|
29
|
+
- **Relative timestamps** ("2 minutes ago") — never look the same twice.
|
|
30
|
+
- **Expiry / scheduling UI** — sales, sessions, holds, tokens.
|
|
31
|
+
|
|
32
|
+
Chronokit makes those states **deterministic** (freeze the clock) or **fast**
|
|
33
|
+
(speed it up) without touching your component code — it mocks `Date` *and* the
|
|
34
|
+
scheduling APIs (`setTimeout`, `setInterval`, `requestAnimationFrame`).
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Storybook **9 or later**
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
> **Status:** chronokit isn't published to npm yet — see [Status](#status). The
|
|
43
|
+
> setup below shows the intended API.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install --save-dev @thinkdx/storybook-addon-chronokit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
Register the decorator globally in `.storybook/preview`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// .storybook/preview.ts
|
|
55
|
+
import type { Preview } from '@storybook/react-vite'
|
|
56
|
+
import { mockDateDecorator } from '@thinkdx/storybook-addon-chronokit'
|
|
57
|
+
|
|
58
|
+
const preview: Preview = {
|
|
59
|
+
decorators: [mockDateDecorator],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default preview
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Control the clock with the `date` parameter on any story, a component's `meta`,
|
|
68
|
+
or globally in `preview`.
|
|
69
|
+
|
|
70
|
+
### Freeze the clock (static time)
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export const Frozen = {
|
|
74
|
+
// Shorthand — a string, number (ms), or Date freezes the clock at that instant.
|
|
75
|
+
parameters: { date: '2025-06-01T12:00:00' },
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Let it run, optionally fast (dynamic time)
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
export const FastForward = {
|
|
83
|
+
parameters: {
|
|
84
|
+
date: {
|
|
85
|
+
now: '2025-06-01T12:00:00',
|
|
86
|
+
canProgress: true,
|
|
87
|
+
clockSpeed: 20, // 20× faster than wall time
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### The `date` parameter
|
|
94
|
+
|
|
95
|
+
| Field | Type | Default | Description |
|
|
96
|
+
| ------------- | -------------------------- | ------- | -------------------------------------------------------------------- |
|
|
97
|
+
| `now` | `string \| number \| Date` | — | The mocked "current" time. Required (in object form). |
|
|
98
|
+
| `canProgress` | `boolean` | `false` | `false` freezes the clock at `now`; `true` lets it advance. |
|
|
99
|
+
| `clockSpeed` | `number` | `1` | Real-time multiplier while progressing — `30` runs time 30× faster. |
|
|
100
|
+
|
|
101
|
+
## Using it in docs (MDX) pages
|
|
102
|
+
|
|
103
|
+
The decorator replaces **global** browser APIs, so two stories with different
|
|
104
|
+
mocked clocks can't share one document. When you embed multiple examples on a
|
|
105
|
+
single docs page, render each in its own iframe so each gets an isolated clock:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// .storybook/preview.ts
|
|
109
|
+
const preview: Preview = {
|
|
110
|
+
decorators: [mockDateDecorator],
|
|
111
|
+
parameters: {
|
|
112
|
+
docs: { story: { inline: false } },
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## How it works
|
|
118
|
+
|
|
119
|
+
On each story, the decorator swaps the global `Date`, `setTimeout`,
|
|
120
|
+
`setInterval`, `requestAnimationFrame`, and `cancelAnimationFrame` for mocked
|
|
121
|
+
versions tied to the `date` parameter, then restores the originals when the story
|
|
122
|
+
unmounts — so a mocked clock in one story never leaks into the next.
|
|
123
|
+
|
|
124
|
+
Because the swap is global, your components don't need to know chronokit exists:
|
|
125
|
+
they call `Date.now()` / `requestAnimationFrame` as usual and transparently see
|
|
126
|
+
the mocked clock.
|
|
127
|
+
|
|
128
|
+
## Status
|
|
129
|
+
|
|
130
|
+
This is the first edition. The addon and a full set of examples and documentation
|
|
131
|
+
live in this Storybook (see the demo link above). Packaging for npm
|
|
132
|
+
(`exports`, build, published artifact) is in progress.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/addon/mockDateDecorator.ts
|
|
6
|
+
var RealDate = globalThis.Date;
|
|
7
|
+
var realSetTimeout = globalThis.setTimeout;
|
|
8
|
+
var realSetInterval = globalThis.setInterval;
|
|
9
|
+
var realRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
10
|
+
var realCancelAnimationFrame = globalThis.cancelAnimationFrame;
|
|
11
|
+
var pendingFrames = /* @__PURE__ */ new Map();
|
|
12
|
+
var nextFrameId = 1;
|
|
13
|
+
var MockDate = class _MockDate extends RealDate {
|
|
14
|
+
static mockNow = RealDate.now();
|
|
15
|
+
static realtimeStart = null;
|
|
16
|
+
static clockSpeed = 1;
|
|
17
|
+
constructor(value) {
|
|
18
|
+
if (value === void 0) {
|
|
19
|
+
super(_MockDate.now());
|
|
20
|
+
} else {
|
|
21
|
+
super(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
static now() {
|
|
25
|
+
if (_MockDate.realtimeStart === null) {
|
|
26
|
+
return _MockDate.mockNow;
|
|
27
|
+
}
|
|
28
|
+
const elapsed = RealDate.now() - _MockDate.realtimeStart;
|
|
29
|
+
return _MockDate.mockNow + elapsed * _MockDate.clockSpeed;
|
|
30
|
+
}
|
|
31
|
+
static parse(dateString) {
|
|
32
|
+
return RealDate.parse(dateString);
|
|
33
|
+
}
|
|
34
|
+
static UTC(...args) {
|
|
35
|
+
return RealDate.UTC(...args);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function mockSetTimeout(speed) {
|
|
39
|
+
return ((callback, ms, ...args) => {
|
|
40
|
+
return realSetTimeout(callback, (ms ?? 0) / speed, ...args);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function mockSetInterval(speed) {
|
|
44
|
+
return ((callback, ms, ...args) => {
|
|
45
|
+
return realSetInterval(callback, (ms ?? 0) / speed, ...args);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function mockRequestAnimationFrame(speed) {
|
|
49
|
+
return (callback) => {
|
|
50
|
+
const frameId = nextFrameId++;
|
|
51
|
+
const frameDelay = Math.max(1, 16.67 / speed);
|
|
52
|
+
const timeoutId = realSetTimeout(() => {
|
|
53
|
+
pendingFrames.delete(frameId);
|
|
54
|
+
callback(MockDate.now());
|
|
55
|
+
}, frameDelay);
|
|
56
|
+
pendingFrames.set(frameId, timeoutId);
|
|
57
|
+
return frameId;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function mockCancelAnimationFrame() {
|
|
61
|
+
return (frameId) => {
|
|
62
|
+
const timeoutId = pendingFrames.get(frameId);
|
|
63
|
+
if (timeoutId !== void 0) {
|
|
64
|
+
clearTimeout(timeoutId);
|
|
65
|
+
pendingFrames.delete(frameId);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parseDate(date) {
|
|
70
|
+
if (date instanceof Date) {
|
|
71
|
+
return date.getTime();
|
|
72
|
+
}
|
|
73
|
+
if (typeof date === "number") {
|
|
74
|
+
return date;
|
|
75
|
+
}
|
|
76
|
+
return new RealDate(date).getTime();
|
|
77
|
+
}
|
|
78
|
+
function applyMock(dateParam) {
|
|
79
|
+
const {
|
|
80
|
+
now,
|
|
81
|
+
canProgress = false,
|
|
82
|
+
clockSpeed = 1
|
|
83
|
+
} = typeof dateParam === "object" && "now" in dateParam ? dateParam : { now: dateParam };
|
|
84
|
+
MockDate.mockNow = parseDate(now);
|
|
85
|
+
MockDate.realtimeStart = canProgress ? RealDate.now() : null;
|
|
86
|
+
MockDate.clockSpeed = clockSpeed;
|
|
87
|
+
globalThis.Date = MockDate;
|
|
88
|
+
if (canProgress && clockSpeed !== 1) {
|
|
89
|
+
globalThis.setTimeout = mockSetTimeout(clockSpeed);
|
|
90
|
+
globalThis.setInterval = mockSetInterval(clockSpeed);
|
|
91
|
+
globalThis.requestAnimationFrame = mockRequestAnimationFrame(clockSpeed);
|
|
92
|
+
globalThis.cancelAnimationFrame = mockCancelAnimationFrame();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function restoreMock() {
|
|
96
|
+
globalThis.Date = RealDate;
|
|
97
|
+
globalThis.setTimeout = realSetTimeout;
|
|
98
|
+
globalThis.setInterval = realSetInterval;
|
|
99
|
+
globalThis.requestAnimationFrame = realRequestAnimationFrame;
|
|
100
|
+
globalThis.cancelAnimationFrame = realCancelAnimationFrame;
|
|
101
|
+
for (const timeoutId of pendingFrames.values()) {
|
|
102
|
+
clearTimeout(timeoutId);
|
|
103
|
+
}
|
|
104
|
+
pendingFrames.clear();
|
|
105
|
+
}
|
|
106
|
+
var mockDateDecorator = (Story, context) => {
|
|
107
|
+
var _a;
|
|
108
|
+
const dateParam = (_a = context.parameters) == null ? void 0 : _a.date;
|
|
109
|
+
const appliedRef = react.useRef(false);
|
|
110
|
+
if (dateParam && !appliedRef.current) {
|
|
111
|
+
applyMock(dateParam);
|
|
112
|
+
appliedRef.current = true;
|
|
113
|
+
}
|
|
114
|
+
react.useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
if (appliedRef.current) {
|
|
117
|
+
restoreMock();
|
|
118
|
+
appliedRef.current = false;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
react.useEffect(() => {
|
|
123
|
+
if (dateParam) {
|
|
124
|
+
applyMock(dateParam);
|
|
125
|
+
appliedRef.current = true;
|
|
126
|
+
} else if (appliedRef.current) {
|
|
127
|
+
restoreMock();
|
|
128
|
+
appliedRef.current = false;
|
|
129
|
+
}
|
|
130
|
+
}, [dateParam]);
|
|
131
|
+
return Story();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
exports.mockDateDecorator = mockDateDecorator;
|
|
135
|
+
//# sourceMappingURL=index.cjs.map
|
|
136
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/addon/mockDateDecorator.ts"],"names":["useRef","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Decorator } from '@storybook/react-vite'\nimport { useEffect, useRef } from 'react'\n\nexport type MockDateParameters =\n | string\n | number\n | Date\n | {\n /** The mocked \"current\" time. */\n now: string | number | Date\n /** When true the clock advances from `now`; when false it stays frozen. @default false */\n canProgress?: boolean\n /** Real-time multiplier for the advancing clock (e.g. 20 = 20x faster). @default 1 */\n clockSpeed?: number\n }\n\ndeclare module '@storybook/react-vite' {\n interface Parameters {\n date?: MockDateParameters\n }\n}\n\nconst RealDate = globalThis.Date\nconst realSetTimeout = globalThis.setTimeout\nconst realSetInterval = globalThis.setInterval\nconst realRequestAnimationFrame = globalThis.requestAnimationFrame\nconst realCancelAnimationFrame = globalThis.cancelAnimationFrame\n\n// Track pending animation frames for cancellation\nconst pendingFrames = new Map<number, ReturnType<typeof setTimeout>>()\nlet nextFrameId = 1\n\nclass MockDate extends RealDate {\n static mockNow: number = RealDate.now()\n static realtimeStart: number | null = null\n static clockSpeed: number = 1.0\n\n constructor(value?: string | number | Date) {\n if (value === undefined) {\n super(MockDate.now())\n } else {\n super(value as string | number)\n }\n }\n\n static override now(): number {\n if (MockDate.realtimeStart === null) {\n return MockDate.mockNow\n }\n const elapsed = RealDate.now() - MockDate.realtimeStart\n return MockDate.mockNow + elapsed * MockDate.clockSpeed\n }\n\n static override parse(dateString: string): number {\n return RealDate.parse(dateString)\n }\n\n static override UTC(...args: Parameters<typeof Date.UTC>): number {\n return RealDate.UTC(...args)\n }\n}\n\nfunction mockSetTimeout(speed: number) {\n return ((callback: TimerHandler, ms?: number, ...args: unknown[]) => {\n return realSetTimeout(callback, (ms ?? 0) / speed, ...args)\n }) as typeof setTimeout\n}\n\nfunction mockSetInterval(speed: number) {\n return ((callback: TimerHandler, ms?: number, ...args: unknown[]) => {\n return realSetInterval(callback, (ms ?? 0) / speed, ...args)\n }) as typeof setInterval\n}\n\nfunction mockRequestAnimationFrame(speed: number) {\n return (callback: FrameRequestCallback): number => {\n const frameId = nextFrameId++\n // Normal RAF is ~60fps = 16.67ms between frames\n // With speed multiplier, frames should come faster in real time\n const frameDelay = Math.max(1, 16.67 / speed)\n\n const timeoutId = realSetTimeout(() => {\n pendingFrames.delete(frameId)\n // Pass the mocked timestamp to the callback\n callback(MockDate.now())\n }, frameDelay)\n\n pendingFrames.set(frameId, timeoutId)\n return frameId\n }\n}\n\nfunction mockCancelAnimationFrame() {\n return (frameId: number): void => {\n const timeoutId = pendingFrames.get(frameId)\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId)\n pendingFrames.delete(frameId)\n }\n }\n}\n\nfunction parseDate(date: string | number | Date): number {\n if (date instanceof Date) {\n return date.getTime()\n }\n if (typeof date === 'number') {\n return date\n }\n return new RealDate(date).getTime()\n}\n\nfunction applyMock(dateParam: MockDateParameters) {\n const {\n now,\n canProgress = false,\n clockSpeed = 1.0,\n } = typeof dateParam === 'object' && 'now' in dateParam\n ? dateParam\n : { now: dateParam }\n\n MockDate.mockNow = parseDate(now)\n MockDate.realtimeStart = canProgress ? RealDate.now() : null\n MockDate.clockSpeed = clockSpeed\n\n // @ts-expect-error - replacing global Date with MockDate\n globalThis.Date = MockDate\n\n if (canProgress && clockSpeed !== 1.0) {\n globalThis.setTimeout = mockSetTimeout(clockSpeed)\n globalThis.setInterval = mockSetInterval(clockSpeed)\n globalThis.requestAnimationFrame = mockRequestAnimationFrame(clockSpeed)\n globalThis.cancelAnimationFrame = mockCancelAnimationFrame()\n }\n}\n\nfunction restoreMock() {\n globalThis.Date = RealDate\n globalThis.setTimeout = realSetTimeout\n globalThis.setInterval = realSetInterval\n globalThis.requestAnimationFrame = realRequestAnimationFrame\n globalThis.cancelAnimationFrame = realCancelAnimationFrame\n\n // Cancel any pending animation frames\n for (const timeoutId of pendingFrames.values()) {\n clearTimeout(timeoutId)\n }\n pendingFrames.clear()\n}\n\nexport const mockDateDecorator: Decorator = (Story, context) => {\n const dateParam = context.parameters?.date as MockDateParameters | undefined\n const appliedRef = useRef(false)\n\n // Apply mock synchronously on first render\n if (dateParam && !appliedRef.current) {\n applyMock(dateParam)\n appliedRef.current = true\n }\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (appliedRef.current) {\n restoreMock()\n appliedRef.current = false\n }\n }\n }, [])\n\n // Re-apply if dateParam changes\n useEffect(() => {\n if (dateParam) {\n applyMock(dateParam)\n appliedRef.current = true\n } else if (appliedRef.current) {\n restoreMock()\n appliedRef.current = false\n }\n }, [dateParam])\n\n return Story()\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type MockDateParameters, mockDateDecorator } from './mockDateDecorator';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type MockDateParameters, mockDateDecorator } from './mockDateDecorator';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/addon/mockDateDecorator.ts
|
|
4
|
+
var RealDate = globalThis.Date;
|
|
5
|
+
var realSetTimeout = globalThis.setTimeout;
|
|
6
|
+
var realSetInterval = globalThis.setInterval;
|
|
7
|
+
var realRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
8
|
+
var realCancelAnimationFrame = globalThis.cancelAnimationFrame;
|
|
9
|
+
var pendingFrames = /* @__PURE__ */ new Map();
|
|
10
|
+
var nextFrameId = 1;
|
|
11
|
+
var MockDate = class _MockDate extends RealDate {
|
|
12
|
+
static mockNow = RealDate.now();
|
|
13
|
+
static realtimeStart = null;
|
|
14
|
+
static clockSpeed = 1;
|
|
15
|
+
constructor(value) {
|
|
16
|
+
if (value === void 0) {
|
|
17
|
+
super(_MockDate.now());
|
|
18
|
+
} else {
|
|
19
|
+
super(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
static now() {
|
|
23
|
+
if (_MockDate.realtimeStart === null) {
|
|
24
|
+
return _MockDate.mockNow;
|
|
25
|
+
}
|
|
26
|
+
const elapsed = RealDate.now() - _MockDate.realtimeStart;
|
|
27
|
+
return _MockDate.mockNow + elapsed * _MockDate.clockSpeed;
|
|
28
|
+
}
|
|
29
|
+
static parse(dateString) {
|
|
30
|
+
return RealDate.parse(dateString);
|
|
31
|
+
}
|
|
32
|
+
static UTC(...args) {
|
|
33
|
+
return RealDate.UTC(...args);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function mockSetTimeout(speed) {
|
|
37
|
+
return ((callback, ms, ...args) => {
|
|
38
|
+
return realSetTimeout(callback, (ms ?? 0) / speed, ...args);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function mockSetInterval(speed) {
|
|
42
|
+
return ((callback, ms, ...args) => {
|
|
43
|
+
return realSetInterval(callback, (ms ?? 0) / speed, ...args);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function mockRequestAnimationFrame(speed) {
|
|
47
|
+
return (callback) => {
|
|
48
|
+
const frameId = nextFrameId++;
|
|
49
|
+
const frameDelay = Math.max(1, 16.67 / speed);
|
|
50
|
+
const timeoutId = realSetTimeout(() => {
|
|
51
|
+
pendingFrames.delete(frameId);
|
|
52
|
+
callback(MockDate.now());
|
|
53
|
+
}, frameDelay);
|
|
54
|
+
pendingFrames.set(frameId, timeoutId);
|
|
55
|
+
return frameId;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function mockCancelAnimationFrame() {
|
|
59
|
+
return (frameId) => {
|
|
60
|
+
const timeoutId = pendingFrames.get(frameId);
|
|
61
|
+
if (timeoutId !== void 0) {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
pendingFrames.delete(frameId);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseDate(date) {
|
|
68
|
+
if (date instanceof Date) {
|
|
69
|
+
return date.getTime();
|
|
70
|
+
}
|
|
71
|
+
if (typeof date === "number") {
|
|
72
|
+
return date;
|
|
73
|
+
}
|
|
74
|
+
return new RealDate(date).getTime();
|
|
75
|
+
}
|
|
76
|
+
function applyMock(dateParam) {
|
|
77
|
+
const {
|
|
78
|
+
now,
|
|
79
|
+
canProgress = false,
|
|
80
|
+
clockSpeed = 1
|
|
81
|
+
} = typeof dateParam === "object" && "now" in dateParam ? dateParam : { now: dateParam };
|
|
82
|
+
MockDate.mockNow = parseDate(now);
|
|
83
|
+
MockDate.realtimeStart = canProgress ? RealDate.now() : null;
|
|
84
|
+
MockDate.clockSpeed = clockSpeed;
|
|
85
|
+
globalThis.Date = MockDate;
|
|
86
|
+
if (canProgress && clockSpeed !== 1) {
|
|
87
|
+
globalThis.setTimeout = mockSetTimeout(clockSpeed);
|
|
88
|
+
globalThis.setInterval = mockSetInterval(clockSpeed);
|
|
89
|
+
globalThis.requestAnimationFrame = mockRequestAnimationFrame(clockSpeed);
|
|
90
|
+
globalThis.cancelAnimationFrame = mockCancelAnimationFrame();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function restoreMock() {
|
|
94
|
+
globalThis.Date = RealDate;
|
|
95
|
+
globalThis.setTimeout = realSetTimeout;
|
|
96
|
+
globalThis.setInterval = realSetInterval;
|
|
97
|
+
globalThis.requestAnimationFrame = realRequestAnimationFrame;
|
|
98
|
+
globalThis.cancelAnimationFrame = realCancelAnimationFrame;
|
|
99
|
+
for (const timeoutId of pendingFrames.values()) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
}
|
|
102
|
+
pendingFrames.clear();
|
|
103
|
+
}
|
|
104
|
+
var mockDateDecorator = (Story, context) => {
|
|
105
|
+
var _a;
|
|
106
|
+
const dateParam = (_a = context.parameters) == null ? void 0 : _a.date;
|
|
107
|
+
const appliedRef = useRef(false);
|
|
108
|
+
if (dateParam && !appliedRef.current) {
|
|
109
|
+
applyMock(dateParam);
|
|
110
|
+
appliedRef.current = true;
|
|
111
|
+
}
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
return () => {
|
|
114
|
+
if (appliedRef.current) {
|
|
115
|
+
restoreMock();
|
|
116
|
+
appliedRef.current = false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, []);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (dateParam) {
|
|
122
|
+
applyMock(dateParam);
|
|
123
|
+
appliedRef.current = true;
|
|
124
|
+
} else if (appliedRef.current) {
|
|
125
|
+
restoreMock();
|
|
126
|
+
appliedRef.current = false;
|
|
127
|
+
}
|
|
128
|
+
}, [dateParam]);
|
|
129
|
+
return Story();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export { mockDateDecorator };
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
134
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/addon/mockDateDecorator.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Decorator } from '@storybook/react-vite'\nimport { useEffect, useRef } from 'react'\n\nexport type MockDateParameters =\n | string\n | number\n | Date\n | {\n /** The mocked \"current\" time. */\n now: string | number | Date\n /** When true the clock advances from `now`; when false it stays frozen. @default false */\n canProgress?: boolean\n /** Real-time multiplier for the advancing clock (e.g. 20 = 20x faster). @default 1 */\n clockSpeed?: number\n }\n\ndeclare module '@storybook/react-vite' {\n interface Parameters {\n date?: MockDateParameters\n }\n}\n\nconst RealDate = globalThis.Date\nconst realSetTimeout = globalThis.setTimeout\nconst realSetInterval = globalThis.setInterval\nconst realRequestAnimationFrame = globalThis.requestAnimationFrame\nconst realCancelAnimationFrame = globalThis.cancelAnimationFrame\n\n// Track pending animation frames for cancellation\nconst pendingFrames = new Map<number, ReturnType<typeof setTimeout>>()\nlet nextFrameId = 1\n\nclass MockDate extends RealDate {\n static mockNow: number = RealDate.now()\n static realtimeStart: number | null = null\n static clockSpeed: number = 1.0\n\n constructor(value?: string | number | Date) {\n if (value === undefined) {\n super(MockDate.now())\n } else {\n super(value as string | number)\n }\n }\n\n static override now(): number {\n if (MockDate.realtimeStart === null) {\n return MockDate.mockNow\n }\n const elapsed = RealDate.now() - MockDate.realtimeStart\n return MockDate.mockNow + elapsed * MockDate.clockSpeed\n }\n\n static override parse(dateString: string): number {\n return RealDate.parse(dateString)\n }\n\n static override UTC(...args: Parameters<typeof Date.UTC>): number {\n return RealDate.UTC(...args)\n }\n}\n\nfunction mockSetTimeout(speed: number) {\n return ((callback: TimerHandler, ms?: number, ...args: unknown[]) => {\n return realSetTimeout(callback, (ms ?? 0) / speed, ...args)\n }) as typeof setTimeout\n}\n\nfunction mockSetInterval(speed: number) {\n return ((callback: TimerHandler, ms?: number, ...args: unknown[]) => {\n return realSetInterval(callback, (ms ?? 0) / speed, ...args)\n }) as typeof setInterval\n}\n\nfunction mockRequestAnimationFrame(speed: number) {\n return (callback: FrameRequestCallback): number => {\n const frameId = nextFrameId++\n // Normal RAF is ~60fps = 16.67ms between frames\n // With speed multiplier, frames should come faster in real time\n const frameDelay = Math.max(1, 16.67 / speed)\n\n const timeoutId = realSetTimeout(() => {\n pendingFrames.delete(frameId)\n // Pass the mocked timestamp to the callback\n callback(MockDate.now())\n }, frameDelay)\n\n pendingFrames.set(frameId, timeoutId)\n return frameId\n }\n}\n\nfunction mockCancelAnimationFrame() {\n return (frameId: number): void => {\n const timeoutId = pendingFrames.get(frameId)\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId)\n pendingFrames.delete(frameId)\n }\n }\n}\n\nfunction parseDate(date: string | number | Date): number {\n if (date instanceof Date) {\n return date.getTime()\n }\n if (typeof date === 'number') {\n return date\n }\n return new RealDate(date).getTime()\n}\n\nfunction applyMock(dateParam: MockDateParameters) {\n const {\n now,\n canProgress = false,\n clockSpeed = 1.0,\n } = typeof dateParam === 'object' && 'now' in dateParam\n ? dateParam\n : { now: dateParam }\n\n MockDate.mockNow = parseDate(now)\n MockDate.realtimeStart = canProgress ? RealDate.now() : null\n MockDate.clockSpeed = clockSpeed\n\n // @ts-expect-error - replacing global Date with MockDate\n globalThis.Date = MockDate\n\n if (canProgress && clockSpeed !== 1.0) {\n globalThis.setTimeout = mockSetTimeout(clockSpeed)\n globalThis.setInterval = mockSetInterval(clockSpeed)\n globalThis.requestAnimationFrame = mockRequestAnimationFrame(clockSpeed)\n globalThis.cancelAnimationFrame = mockCancelAnimationFrame()\n }\n}\n\nfunction restoreMock() {\n globalThis.Date = RealDate\n globalThis.setTimeout = realSetTimeout\n globalThis.setInterval = realSetInterval\n globalThis.requestAnimationFrame = realRequestAnimationFrame\n globalThis.cancelAnimationFrame = realCancelAnimationFrame\n\n // Cancel any pending animation frames\n for (const timeoutId of pendingFrames.values()) {\n clearTimeout(timeoutId)\n }\n pendingFrames.clear()\n}\n\nexport const mockDateDecorator: Decorator = (Story, context) => {\n const dateParam = context.parameters?.date as MockDateParameters | undefined\n const appliedRef = useRef(false)\n\n // Apply mock synchronously on first render\n if (dateParam && !appliedRef.current) {\n applyMock(dateParam)\n appliedRef.current = true\n }\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (appliedRef.current) {\n restoreMock()\n appliedRef.current = false\n }\n }\n }, [])\n\n // Re-apply if dateParam changes\n useEffect(() => {\n if (dateParam) {\n applyMock(dateParam)\n appliedRef.current = true\n } else if (appliedRef.current) {\n restoreMock()\n appliedRef.current = false\n }\n }, [dateParam])\n\n return Story()\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Decorator } from '@storybook/react-vite';
|
|
2
|
+
export type MockDateParameters = string | number | Date | {
|
|
3
|
+
/** The mocked "current" time. */
|
|
4
|
+
now: string | number | Date;
|
|
5
|
+
/** When true the clock advances from `now`; when false it stays frozen. @default false */
|
|
6
|
+
canProgress?: boolean;
|
|
7
|
+
/** Real-time multiplier for the advancing clock (e.g. 20 = 20x faster). @default 1 */
|
|
8
|
+
clockSpeed?: number;
|
|
9
|
+
};
|
|
10
|
+
declare module '@storybook/react-vite' {
|
|
11
|
+
interface Parameters {
|
|
12
|
+
date?: MockDateParameters;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export declare const mockDateDecorator: Decorator;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Decorator } from '@storybook/react-vite';
|
|
2
|
+
export type MockDateParameters = string | number | Date | {
|
|
3
|
+
/** The mocked "current" time. */
|
|
4
|
+
now: string | number | Date;
|
|
5
|
+
/** When true the clock advances from `now`; when false it stays frozen. @default false */
|
|
6
|
+
canProgress?: boolean;
|
|
7
|
+
/** Real-time multiplier for the advancing clock (e.g. 20 = 20x faster). @default 1 */
|
|
8
|
+
clockSpeed?: number;
|
|
9
|
+
};
|
|
10
|
+
declare module '@storybook/react-vite' {
|
|
11
|
+
interface Parameters {
|
|
12
|
+
date?: MockDateParameters;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export declare const mockDateDecorator: Decorator;
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thinkdx/storybook-addon-chronokit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Mock time in your Storybook stories — freeze the clock or fast-forward it with a single decorator.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"storybook",
|
|
10
|
+
"storybook-addons",
|
|
11
|
+
"addon",
|
|
12
|
+
"date",
|
|
13
|
+
"time",
|
|
14
|
+
"clock",
|
|
15
|
+
"mock",
|
|
16
|
+
"testing"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://thinkdx.github.io/storybook-addon-chronokit/",
|
|
19
|
+
"bugs": "https://github.com/ThinkDX/storybook-addon-chronokit/issues",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ThinkDX/storybook-addon-chronokit.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Nick Ferraro",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/index.d.cts",
|
|
35
|
+
"default": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.cjs",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup && tsc -p tsconfig.build.json && node scripts/make-cts-types.mjs",
|
|
51
|
+
"prepublishOnly": "npm run build",
|
|
52
|
+
"test": "vitest",
|
|
53
|
+
"test:run": "vitest run",
|
|
54
|
+
"storybook": "storybook dev -p 6006",
|
|
55
|
+
"build-storybook": "storybook build",
|
|
56
|
+
"lint": "biome lint .",
|
|
57
|
+
"format": "biome format --write .",
|
|
58
|
+
"check": "biome check --write .",
|
|
59
|
+
"typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@storybook/react-vite": ">=9.0.0",
|
|
63
|
+
"react": ">=18",
|
|
64
|
+
"storybook": ">=9.0.0"
|
|
65
|
+
},
|
|
66
|
+
"overrides": {
|
|
67
|
+
"tsup": {
|
|
68
|
+
"esbuild": "^0.28.1"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@biomejs/biome": "^2.3.11",
|
|
73
|
+
"@storybook/addon-a11y": "^10.4.6",
|
|
74
|
+
"@storybook/addon-docs": "^10.4.6",
|
|
75
|
+
"@storybook/addon-vitest": "^10.4.6",
|
|
76
|
+
"@storybook/react-vite": "^10.4.6",
|
|
77
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
78
|
+
"@testing-library/react": "^16.3.1",
|
|
79
|
+
"@types/node": "^25.0.9",
|
|
80
|
+
"@types/react": "^19.0.0",
|
|
81
|
+
"@types/react-dom": "^19.0.0",
|
|
82
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
83
|
+
"@vitest/browser-playwright": "^4.1.9",
|
|
84
|
+
"jsdom": "^27.4.0",
|
|
85
|
+
"playwright": "^1.57.0",
|
|
86
|
+
"react": "^19.0.0",
|
|
87
|
+
"react-dom": "^19.0.0",
|
|
88
|
+
"storybook": "^10.4.6",
|
|
89
|
+
"tsup": "^8.3.5",
|
|
90
|
+
"typescript": "~5.7.2",
|
|
91
|
+
"vite": "^6.0.0",
|
|
92
|
+
"vitest": "^4.1.9"
|
|
93
|
+
}
|
|
94
|
+
}
|