@storybook/instrumenter 6.4.0-alpha.35
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/README.md +3 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/instrumenter.js +725 -0
- package/dist/cjs/types.js +15 -0
- package/dist/cjs/typings.d.js +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/instrumenter.js +684 -0
- package/dist/esm/types.js +8 -0
- package/dist/esm/typings.d.js +0 -0
- package/dist/modern/index.js +2 -0
- package/dist/modern/instrumenter.js +547 -0
- package/dist/modern/types.js +8 -0
- package/dist/modern/typings.d.js +0 -0
- package/dist/ts3.4/index.d.ts +2 -0
- package/dist/ts3.4/instrumenter.d.ts +53 -0
- package/dist/ts3.4/types.d.ts +37 -0
- package/dist/ts3.9/index.d.ts +2 -0
- package/dist/ts3.9/instrumenter.d.ts +53 -0
- package/dist/ts3.9/types.d.ts +37 -0
- package/package.json +52 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
2
|
+
|
|
3
|
+
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
|
4
|
+
|
|
5
|
+
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
6
|
+
|
|
7
|
+
import "core-js/modules/es.array.reduce-right.js";
|
|
8
|
+
import "core-js/modules/es.array.reduce.js";
|
|
9
|
+
|
|
10
|
+
/* eslint-disable no-underscore-dangle */
|
|
11
|
+
import { addons } from '@storybook/addons';
|
|
12
|
+
import { FORCE_REMOUNT, IGNORED_EXCEPTION, STORY_RENDER_PHASE_CHANGED } from '@storybook/core-events';
|
|
13
|
+
import global from 'global';
|
|
14
|
+
import { CallStates } from './types';
|
|
15
|
+
export const EVENTS = {
|
|
16
|
+
CALL: 'instrumenter/call',
|
|
17
|
+
SYNC: 'instrumenter/sync',
|
|
18
|
+
START: 'instrumenter/start',
|
|
19
|
+
BACK: 'instrumenter/back',
|
|
20
|
+
GOTO: 'instrumenter/goto',
|
|
21
|
+
NEXT: 'instrumenter/next',
|
|
22
|
+
END: 'instrumenter/end'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const isObject = o => Object.prototype.toString.call(o) === '[object Object]';
|
|
26
|
+
|
|
27
|
+
const isModule = o => Object.prototype.toString.call(o) === '[object Module]';
|
|
28
|
+
|
|
29
|
+
const isInstrumentable = o => {
|
|
30
|
+
if (!isObject(o) && !isModule(o)) return false;
|
|
31
|
+
if (o.constructor === undefined) return true;
|
|
32
|
+
const proto = o.constructor.prototype;
|
|
33
|
+
if (!isObject(proto)) return false;
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(proto, 'isPrototypeOf') === false) return false;
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const construct = obj => {
|
|
39
|
+
try {
|
|
40
|
+
return new obj.constructor();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getInitialState = () => ({
|
|
47
|
+
isDebugging: false,
|
|
48
|
+
cursor: 0,
|
|
49
|
+
calls: [],
|
|
50
|
+
shadowCalls: [],
|
|
51
|
+
callRefsByResult: new Map(),
|
|
52
|
+
chainedCallIds: new Set(),
|
|
53
|
+
parentCallId: undefined,
|
|
54
|
+
playUntil: undefined,
|
|
55
|
+
resolvers: {},
|
|
56
|
+
syncTimeout: undefined,
|
|
57
|
+
forwardedException: undefined
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export class Instrumenter {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.channel = void 0;
|
|
63
|
+
this.state = void 0;
|
|
64
|
+
this.channel = addons.getChannel();
|
|
65
|
+
this.state = // Restore state from the parent window in case the iframe was reloaded.
|
|
66
|
+
global.window.parent.__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__ || getInitialState(); // When called from `start`, isDebugging will be true
|
|
67
|
+
|
|
68
|
+
const resetState = ({
|
|
69
|
+
isDebugging = false
|
|
70
|
+
} = {}) => {
|
|
71
|
+
const {
|
|
72
|
+
calls,
|
|
73
|
+
shadowCalls,
|
|
74
|
+
callRefsByResult,
|
|
75
|
+
chainedCallIds,
|
|
76
|
+
playUntil
|
|
77
|
+
} = this.state;
|
|
78
|
+
const retainedCalls = (isDebugging ? shadowCalls : calls).filter(call => call.retain);
|
|
79
|
+
const retainedCallRefs = new Map(Array.from(callRefsByResult.entries()).filter(([, ref]) => ref.retain));
|
|
80
|
+
this.setState(Object.assign({}, getInitialState(), {
|
|
81
|
+
cursor: retainedCalls.length,
|
|
82
|
+
calls: retainedCalls,
|
|
83
|
+
callRefsByResult: retainedCallRefs,
|
|
84
|
+
shadowCalls: isDebugging ? shadowCalls : [],
|
|
85
|
+
chainedCallIds: isDebugging ? chainedCallIds : new Set(),
|
|
86
|
+
playUntil: isDebugging ? playUntil : undefined,
|
|
87
|
+
isDebugging
|
|
88
|
+
})); // Don't sync while debugging, as it'll cause flicker.
|
|
89
|
+
|
|
90
|
+
if (!isDebugging) this.channel.emit(EVENTS.SYNC, this.getLog());
|
|
91
|
+
}; // A forceRemount might be triggered for debugging (on `start`), or elsewhere in Storybook.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
this.channel.on(FORCE_REMOUNT, resetState); // Start with a clean slate before playing, but also clean up when switching to a story that
|
|
95
|
+
// doesn't have a play function (in which case there is no 'playing' phase).
|
|
96
|
+
// Invocation of the play function is guaranteed to always be preceded by the 'rendering' phase.
|
|
97
|
+
|
|
98
|
+
this.channel.on(STORY_RENDER_PHASE_CHANGED, ({
|
|
99
|
+
storyId,
|
|
100
|
+
newPhase
|
|
101
|
+
}) => {
|
|
102
|
+
// TODO keep state per story
|
|
103
|
+
if (newPhase === 'loading') {
|
|
104
|
+
resetState({
|
|
105
|
+
isDebugging: this.state.isDebugging
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (newPhase === 'completed') {
|
|
110
|
+
this.setState({
|
|
111
|
+
isDebugging: false
|
|
112
|
+
}); // Rethrow any unhandled forwarded exception so it doesn't go unnoticed.
|
|
113
|
+
|
|
114
|
+
if (this.state.forwardedException) throw this.state.forwardedException;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const start = ({
|
|
119
|
+
storyId,
|
|
120
|
+
playUntil
|
|
121
|
+
}) => {
|
|
122
|
+
if (!this.state.isDebugging) {
|
|
123
|
+
this.setState(({
|
|
124
|
+
calls
|
|
125
|
+
}) => ({
|
|
126
|
+
calls: [],
|
|
127
|
+
shadowCalls: calls.map(call => Object.assign({}, call, {
|
|
128
|
+
state: CallStates.WAITING
|
|
129
|
+
})),
|
|
130
|
+
isDebugging: true
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const log = this.getLog();
|
|
135
|
+
const firstRowIndex = this.state.shadowCalls.findIndex(call => call.id === log[0].callId);
|
|
136
|
+
this.setState(({
|
|
137
|
+
shadowCalls
|
|
138
|
+
}) => {
|
|
139
|
+
var _shadowCalls$slice$fi;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
playUntil: playUntil || ((_shadowCalls$slice$fi = shadowCalls.slice(0, firstRowIndex).filter(call => call.interceptable).slice(-1)[0]) === null || _shadowCalls$slice$fi === void 0 ? void 0 : _shadowCalls$slice$fi.id)
|
|
143
|
+
};
|
|
144
|
+
}); // Force remount may trigger a page reload if the play function can't be aborted.
|
|
145
|
+
// global.window.location.reload();
|
|
146
|
+
|
|
147
|
+
this.channel.emit(FORCE_REMOUNT, {
|
|
148
|
+
storyId,
|
|
149
|
+
isDebugging: true
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const back = ({
|
|
154
|
+
storyId
|
|
155
|
+
}) => {
|
|
156
|
+
var _log, _log$slice$;
|
|
157
|
+
|
|
158
|
+
const {
|
|
159
|
+
isDebugging
|
|
160
|
+
} = this.state;
|
|
161
|
+
const log = this.getLog();
|
|
162
|
+
const next = log.findIndex(({
|
|
163
|
+
state
|
|
164
|
+
}) => state === CallStates.WAITING);
|
|
165
|
+
const playUntil = ((_log = log[next - 2]) === null || _log === void 0 ? void 0 : _log.callId) || (isDebugging ? null : (_log$slice$ = log.slice(-2)[0]) === null || _log$slice$ === void 0 ? void 0 : _log$slice$.callId);
|
|
166
|
+
start({
|
|
167
|
+
storyId,
|
|
168
|
+
playUntil
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const goto = ({
|
|
173
|
+
storyId,
|
|
174
|
+
callId
|
|
175
|
+
}) => {
|
|
176
|
+
const {
|
|
177
|
+
calls,
|
|
178
|
+
shadowCalls,
|
|
179
|
+
resolvers
|
|
180
|
+
} = this.state;
|
|
181
|
+
const call = calls.find(({
|
|
182
|
+
id
|
|
183
|
+
}) => id === callId);
|
|
184
|
+
const shadowCall = shadowCalls.find(({
|
|
185
|
+
id
|
|
186
|
+
}) => id === callId);
|
|
187
|
+
|
|
188
|
+
if (!call && shadowCall) {
|
|
189
|
+
var _this$getLog$find;
|
|
190
|
+
|
|
191
|
+
const nextCallId = (_this$getLog$find = this.getLog().find(({
|
|
192
|
+
state
|
|
193
|
+
}) => state === CallStates.WAITING)) === null || _this$getLog$find === void 0 ? void 0 : _this$getLog$find.callId;
|
|
194
|
+
if (shadowCall.id !== nextCallId) this.setState({
|
|
195
|
+
playUntil: shadowCall.id
|
|
196
|
+
});
|
|
197
|
+
Object.values(resolvers).forEach(resolve => resolve());
|
|
198
|
+
} else {
|
|
199
|
+
start({
|
|
200
|
+
storyId,
|
|
201
|
+
playUntil: callId
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const next = () => {
|
|
207
|
+
Object.values(this.state.resolvers).forEach(resolve => resolve());
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const end = () => {
|
|
211
|
+
this.setState({
|
|
212
|
+
playUntil: undefined,
|
|
213
|
+
isDebugging: false
|
|
214
|
+
});
|
|
215
|
+
Object.values(this.state.resolvers).forEach(resolve => resolve());
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.channel.on(EVENTS.START, start);
|
|
219
|
+
this.channel.on(EVENTS.BACK, back);
|
|
220
|
+
this.channel.on(EVENTS.GOTO, goto);
|
|
221
|
+
this.channel.on(EVENTS.NEXT, next);
|
|
222
|
+
this.channel.on(EVENTS.END, end);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setState(update) {
|
|
226
|
+
this.state = Object.assign({}, this.state, typeof update === 'function' ? update(this.state) : update); // Track state on the parent window so we can reload the iframe without losing state.
|
|
227
|
+
|
|
228
|
+
global.window.parent.__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__ = this.state;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
getLog() {
|
|
232
|
+
const merged = [...this.state.shadowCalls];
|
|
233
|
+
this.state.calls.forEach((call, index) => {
|
|
234
|
+
merged[index] = call;
|
|
235
|
+
});
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
return merged.reduceRight((acc, call) => {
|
|
238
|
+
call.args.forEach(arg => {
|
|
239
|
+
if (arg !== null && arg !== void 0 && arg.__callId__) {
|
|
240
|
+
seen.add(arg.__callId__);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
call.path.forEach(node => {
|
|
244
|
+
if (node.__callId__) {
|
|
245
|
+
seen.add(node.__callId__);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (call.interceptable && !seen.has(call.id) && !seen.has(call.parentId)) {
|
|
250
|
+
acc.unshift({
|
|
251
|
+
callId: call.id,
|
|
252
|
+
state: call.state
|
|
253
|
+
});
|
|
254
|
+
seen.add(call.id);
|
|
255
|
+
|
|
256
|
+
if (call.parentId) {
|
|
257
|
+
seen.add(call.parentId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return acc;
|
|
262
|
+
}, []);
|
|
263
|
+
} // Traverses the object structure to recursively patch all function properties.
|
|
264
|
+
// Returns the original object, or a new object with the same constructor,
|
|
265
|
+
// depending on whether it should mutate.
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
instrument(obj, options = {}) {
|
|
269
|
+
if (!isInstrumentable(obj)) return obj;
|
|
270
|
+
const {
|
|
271
|
+
mutate = false,
|
|
272
|
+
path = []
|
|
273
|
+
} = options;
|
|
274
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
275
|
+
const value = obj[key]; // Nothing to patch, but might be instrumentable, so we recurse
|
|
276
|
+
|
|
277
|
+
if (typeof value !== 'function') {
|
|
278
|
+
acc[key] = this.instrument(value, Object.assign({}, options, {
|
|
279
|
+
path: path.concat(key)
|
|
280
|
+
}));
|
|
281
|
+
return acc;
|
|
282
|
+
} // Already patched, so we pass through unchanged
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if (typeof value._original === 'function') {
|
|
286
|
+
acc[key] = value;
|
|
287
|
+
return acc;
|
|
288
|
+
} // Patch the function and mark it "patched" by adding a reference to the original function
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
acc[key] = this.patch(key, value, options);
|
|
292
|
+
acc[key]._original = value; // Deal with functions that also act like an object
|
|
293
|
+
|
|
294
|
+
if (Object.keys(value).length > 0) {
|
|
295
|
+
Object.assign(acc[key], this.instrument(Object.assign({}, value), Object.assign({}, options, {
|
|
296
|
+
path: path.concat(key)
|
|
297
|
+
})));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return acc;
|
|
301
|
+
}, mutate ? obj : construct(obj));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
patch(method, fn, options) {
|
|
305
|
+
const patched = (...args) => this.track(method, fn, args, options);
|
|
306
|
+
|
|
307
|
+
Object.defineProperty(patched, 'name', {
|
|
308
|
+
value: method,
|
|
309
|
+
writable: false
|
|
310
|
+
});
|
|
311
|
+
return patched;
|
|
312
|
+
} // Monkey patch an object method to record calls.
|
|
313
|
+
// Returns a function that invokes the original function, records the invocation ("call") and
|
|
314
|
+
// returns the original result.
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
track(method, fn, args, _ref) {
|
|
318
|
+
let {
|
|
319
|
+
path = []
|
|
320
|
+
} = _ref,
|
|
321
|
+
options = _objectWithoutPropertiesLoose(_ref, ["path"]);
|
|
322
|
+
|
|
323
|
+
const index = this.state.cursor;
|
|
324
|
+
this.setState({
|
|
325
|
+
cursor: this.state.cursor + 1
|
|
326
|
+
});
|
|
327
|
+
const id = `${index}-${method}`;
|
|
328
|
+
const {
|
|
329
|
+
intercept = false,
|
|
330
|
+
retain = false
|
|
331
|
+
} = options;
|
|
332
|
+
const interceptable = typeof intercept === 'function' ? intercept(method, path) : intercept;
|
|
333
|
+
const call = {
|
|
334
|
+
id,
|
|
335
|
+
path,
|
|
336
|
+
method,
|
|
337
|
+
args,
|
|
338
|
+
interceptable,
|
|
339
|
+
retain
|
|
340
|
+
};
|
|
341
|
+
const result = (interceptable ? this.intercept : this.invoke).call(this, fn, call);
|
|
342
|
+
return this.instrument(result, Object.assign({}, options, {
|
|
343
|
+
mutate: true,
|
|
344
|
+
path: [{
|
|
345
|
+
__callId__: call.id
|
|
346
|
+
}]
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
intercept(fn, call) {
|
|
351
|
+
// For a "jump to step" action, continue playing until we hit a call by that ID.
|
|
352
|
+
// For chained calls, we can only return a Promise for the last call in the chain.
|
|
353
|
+
const isChainedUpon = this.state.chainedCallIds.has(call.id);
|
|
354
|
+
|
|
355
|
+
if (!this.state.isDebugging || isChainedUpon || this.state.playUntil) {
|
|
356
|
+
if (this.state.playUntil === call.id) {
|
|
357
|
+
this.setState({
|
|
358
|
+
playUntil: undefined
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return this.invoke(fn, call);
|
|
363
|
+
} // Instead of invoking the function, defer the function call until we continue playing.
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
return new Promise(resolve => {
|
|
367
|
+
this.setState(({
|
|
368
|
+
resolvers
|
|
369
|
+
}) => ({
|
|
370
|
+
resolvers: Object.assign({}, resolvers, {
|
|
371
|
+
[call.id]: resolve
|
|
372
|
+
})
|
|
373
|
+
}));
|
|
374
|
+
}).then(() => {
|
|
375
|
+
const _this$state$resolvers = this.state.resolvers,
|
|
376
|
+
_call$id = call.id,
|
|
377
|
+
resolvers = _objectWithoutPropertiesLoose(_this$state$resolvers, [_call$id].map(_toPropertyKey));
|
|
378
|
+
|
|
379
|
+
this.setState({
|
|
380
|
+
resolvers
|
|
381
|
+
});
|
|
382
|
+
return this.invoke(fn, call);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
invoke(fn, call) {
|
|
387
|
+
const {
|
|
388
|
+
parentCallId,
|
|
389
|
+
callRefsByResult,
|
|
390
|
+
forwardedException
|
|
391
|
+
} = this.state;
|
|
392
|
+
const info = Object.assign({}, call, {
|
|
393
|
+
parentId: parentCallId,
|
|
394
|
+
// Map args that originate from a tracked function call to a call reference to enable nesting.
|
|
395
|
+
// These values are often not fully serializable anyway (e.g. HTML elements).
|
|
396
|
+
args: call.args.map(arg => {
|
|
397
|
+
if (callRefsByResult.has(arg)) {
|
|
398
|
+
return callRefsByResult.get(arg);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (arg instanceof global.window.HTMLElement) {
|
|
402
|
+
const {
|
|
403
|
+
prefix,
|
|
404
|
+
localName,
|
|
405
|
+
id,
|
|
406
|
+
classList,
|
|
407
|
+
innerText
|
|
408
|
+
} = arg;
|
|
409
|
+
const classNames = Array.from(classList);
|
|
410
|
+
return {
|
|
411
|
+
__element__: {
|
|
412
|
+
prefix,
|
|
413
|
+
localName,
|
|
414
|
+
id,
|
|
415
|
+
classNames,
|
|
416
|
+
innerText
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return arg;
|
|
422
|
+
})
|
|
423
|
+
}); // Mark any ancestor calls as "chained upon" so we won't attempt to defer it later.
|
|
424
|
+
|
|
425
|
+
call.path.forEach(ref => {
|
|
426
|
+
if (ref !== null && ref !== void 0 && ref.__callId__) {
|
|
427
|
+
this.setState(({
|
|
428
|
+
chainedCallIds
|
|
429
|
+
}) => ({
|
|
430
|
+
chainedCallIds: new Set(Array.from(chainedCallIds).concat(ref.__callId__))
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const handleException = e => {
|
|
436
|
+
if (e instanceof Error) {
|
|
437
|
+
const {
|
|
438
|
+
name,
|
|
439
|
+
message,
|
|
440
|
+
stack
|
|
441
|
+
} = e;
|
|
442
|
+
const exception = {
|
|
443
|
+
name,
|
|
444
|
+
message,
|
|
445
|
+
stack,
|
|
446
|
+
callId: call.id
|
|
447
|
+
};
|
|
448
|
+
this.sync(Object.assign({}, info, {
|
|
449
|
+
state: CallStates.ERROR,
|
|
450
|
+
exception
|
|
451
|
+
})); // Always track errors to their originating call.
|
|
452
|
+
|
|
453
|
+
this.setState(state => ({
|
|
454
|
+
callRefsByResult: new Map([...Array.from(state.callRefsByResult.entries()), [e, {
|
|
455
|
+
__callId__: call.id,
|
|
456
|
+
retain: call.retain
|
|
457
|
+
}]])
|
|
458
|
+
})); // We need to throw to break out of the play function, but we don't want to trigger a redbox
|
|
459
|
+
// so we throw an ignoredException, which is caught and silently ignored by Storybook.
|
|
460
|
+
|
|
461
|
+
if (call.interceptable) {
|
|
462
|
+
throw IGNORED_EXCEPTION;
|
|
463
|
+
} // Non-interceptable calls need their exceptions forwarded to the next interceptable call.
|
|
464
|
+
// In case no interceptable call picks it up, it'll get rethrown in the "completed" phase.
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
this.setState({
|
|
468
|
+
forwardedException: e
|
|
469
|
+
});
|
|
470
|
+
return e;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
throw e;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
// An earlier, non-interceptable call might have forwarded an exception.
|
|
478
|
+
if (forwardedException) {
|
|
479
|
+
this.setState({
|
|
480
|
+
forwardedException: undefined
|
|
481
|
+
});
|
|
482
|
+
throw forwardedException;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const result = fn( // Wrap any callback functions to provide a way to access their "parent" call.
|
|
486
|
+
...call.args.map(arg => {
|
|
487
|
+
if (typeof arg !== 'function' || Object.keys(arg).length) return arg;
|
|
488
|
+
return (...args) => {
|
|
489
|
+
const prev = this.state.parentCallId;
|
|
490
|
+
this.setState({
|
|
491
|
+
parentCallId: call.id
|
|
492
|
+
});
|
|
493
|
+
const res = arg(...args);
|
|
494
|
+
this.setState({
|
|
495
|
+
parentCallId: prev
|
|
496
|
+
});
|
|
497
|
+
return res;
|
|
498
|
+
};
|
|
499
|
+
})); // Track the result so we can trace later uses of it back to the originating call.
|
|
500
|
+
// Primitive results (undefined, null, boolean, string, number, BigInt) are ignored.
|
|
501
|
+
|
|
502
|
+
if (result && ['object', 'function', 'symbol'].includes(typeof result)) {
|
|
503
|
+
this.setState(state => ({
|
|
504
|
+
callRefsByResult: new Map([...Array.from(state.callRefsByResult.entries()), [result, {
|
|
505
|
+
__callId__: call.id,
|
|
506
|
+
retain: call.retain
|
|
507
|
+
}]])
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this.sync(Object.assign({}, info, {
|
|
512
|
+
state: result instanceof Promise ? CallStates.ACTIVE : CallStates.DONE
|
|
513
|
+
}));
|
|
514
|
+
|
|
515
|
+
if (result instanceof Promise) {
|
|
516
|
+
return result.then(value => {
|
|
517
|
+
this.sync(Object.assign({}, info, {
|
|
518
|
+
state: CallStates.DONE
|
|
519
|
+
}));
|
|
520
|
+
return value;
|
|
521
|
+
}, handleException);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return result;
|
|
525
|
+
} catch (e) {
|
|
526
|
+
return handleException(e);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
sync(call) {
|
|
531
|
+
clearTimeout(this.state.syncTimeout);
|
|
532
|
+
this.channel.emit(EVENTS.CALL, call);
|
|
533
|
+
this.setState(({
|
|
534
|
+
calls
|
|
535
|
+
}) => ({
|
|
536
|
+
calls: calls.concat(call),
|
|
537
|
+
syncTimeout: setTimeout(() => this.channel.emit(EVENTS.SYNC, this.getLog()), 0)
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
}
|
|
542
|
+
export const instrument = global.window.parent === global.window ? obj => obj // Don't do anything if not loaded in an iframe.
|
|
543
|
+
: (obj, options = {}) => {
|
|
544
|
+
if (!global.window.__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER__) global.window.__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER__ = new Instrumenter();
|
|
545
|
+
const instrumenter = global.window.__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER__;
|
|
546
|
+
return instrumenter.instrument(obj, options);
|
|
547
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Channel } from '@storybook/addons';
|
|
2
|
+
import { Call, CallRef, LogItem } from './types';
|
|
3
|
+
export declare const EVENTS: {
|
|
4
|
+
CALL: string;
|
|
5
|
+
SYNC: string;
|
|
6
|
+
START: string;
|
|
7
|
+
BACK: string;
|
|
8
|
+
GOTO: string;
|
|
9
|
+
NEXT: string;
|
|
10
|
+
END: string;
|
|
11
|
+
};
|
|
12
|
+
export interface Options {
|
|
13
|
+
intercept?: boolean | ((method: string, path: Array<string | CallRef>) => boolean);
|
|
14
|
+
retain?: boolean;
|
|
15
|
+
mutate?: boolean;
|
|
16
|
+
path?: Array<string | CallRef>;
|
|
17
|
+
}
|
|
18
|
+
export interface State {
|
|
19
|
+
isDebugging: boolean;
|
|
20
|
+
cursor: number;
|
|
21
|
+
calls: Call[];
|
|
22
|
+
shadowCalls: Call[];
|
|
23
|
+
callRefsByResult: Map<any, CallRef & {
|
|
24
|
+
retain: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
chainedCallIds: Set<Call['id']>;
|
|
27
|
+
parentCallId?: Call['id'];
|
|
28
|
+
playUntil?: Call['id'];
|
|
29
|
+
resolvers: Record<Call['id'], Function>;
|
|
30
|
+
syncTimeout: ReturnType<typeof setTimeout>;
|
|
31
|
+
forwardedException?: Error;
|
|
32
|
+
}
|
|
33
|
+
export declare type PatchedObj<TObj> = {
|
|
34
|
+
[Property in keyof TObj]: TObj[Property] & {
|
|
35
|
+
_original: PatchedObj<TObj>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare class Instrumenter {
|
|
39
|
+
channel: Channel;
|
|
40
|
+
state: State;
|
|
41
|
+
constructor();
|
|
42
|
+
setState(update: Partial<State> | ((state: State) => Partial<State>)): void;
|
|
43
|
+
getLog(): LogItem[];
|
|
44
|
+
instrument<TObj extends {
|
|
45
|
+
[x: string]: any;
|
|
46
|
+
}>(obj: TObj, options?: Options): PatchedObj<TObj>;
|
|
47
|
+
patch(method: string, fn: Function, options: Options): (...args: any[]) => PatchedObj<any>;
|
|
48
|
+
track(method: string, fn: Function, args: any[], { path, ...options }: Options): PatchedObj<any>;
|
|
49
|
+
intercept(fn: Function, call: Call): any;
|
|
50
|
+
invoke(fn: Function, call: Call): any;
|
|
51
|
+
sync(call: Call): void;
|
|
52
|
+
}
|
|
53
|
+
export declare const instrument: ((obj: any) => any) | (<TObj extends Record<string, any>>(obj: TObj, options?: Options) => PatchedObj<TObj>);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface Call {
|
|
2
|
+
id: string;
|
|
3
|
+
path: Array<string | CallRef>;
|
|
4
|
+
method: string;
|
|
5
|
+
args: any[];
|
|
6
|
+
interceptable: boolean;
|
|
7
|
+
retain: boolean;
|
|
8
|
+
state?: CallStates.DONE | CallStates.ERROR | CallStates.ACTIVE | CallStates.WAITING;
|
|
9
|
+
exception?: {
|
|
10
|
+
callId: Call['id'];
|
|
11
|
+
message: Error['message'];
|
|
12
|
+
stack: Error['stack'];
|
|
13
|
+
};
|
|
14
|
+
parentId?: Call['id'];
|
|
15
|
+
}
|
|
16
|
+
export declare enum CallStates {
|
|
17
|
+
DONE = "done",
|
|
18
|
+
ERROR = "error",
|
|
19
|
+
ACTIVE = "active",
|
|
20
|
+
WAITING = "waiting"
|
|
21
|
+
}
|
|
22
|
+
export interface CallRef {
|
|
23
|
+
__callId__: Call['id'];
|
|
24
|
+
}
|
|
25
|
+
export interface ElementRef {
|
|
26
|
+
__element__: {
|
|
27
|
+
prefix?: string;
|
|
28
|
+
localName: string;
|
|
29
|
+
id?: string;
|
|
30
|
+
classNames?: string[];
|
|
31
|
+
innerText?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface LogItem {
|
|
35
|
+
callId: Call['id'];
|
|
36
|
+
state: Call['state'];
|
|
37
|
+
}
|