@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.
@@ -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
+ };
@@ -0,0 +1,8 @@
1
+ export let CallStates;
2
+
3
+ (function (CallStates) {
4
+ CallStates["DONE"] = "done";
5
+ CallStates["ERROR"] = "error";
6
+ CallStates["ACTIVE"] = "active";
7
+ CallStates["WAITING"] = "waiting";
8
+ })(CallStates || (CallStates = {}));
File without changes
@@ -0,0 +1,2 @@
1
+ export { EVENTS, instrument } from './instrumenter';
2
+ export * from './types';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { EVENTS, instrument } from './instrumenter';
2
+ export * from './types';