@khanacademy/wonder-blocks-testing 8.0.21 → 9.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/es/index.js +417 -24
  3. package/dist/harness/adapters/adapters.d.ts +1 -0
  4. package/dist/harness/adapters/ssr.d.ts +12 -0
  5. package/dist/harness/test-harness.d.ts +1 -0
  6. package/dist/harness/types.d.ts +1 -1
  7. package/dist/index.js +416 -24
  8. package/dist/mock-requester.d.ts +2 -2
  9. package/package.json +2 -2
  10. package/src/fetch/fetch-request-matches-mock.ts +2 -4
  11. package/src/fetch/mock-fetch.ts +1 -1
  12. package/src/fixtures/__tests__/fixtures.test.tsx +9 -14
  13. package/src/fixtures/fixtures.basic.stories.tsx +9 -3
  14. package/src/fixtures/fixtures.tsx +1 -2
  15. package/src/gql/mock-gql-fetch.ts +1 -1
  16. package/src/harness/__tests__/hook-harness.test.ts +4 -2
  17. package/src/harness/__tests__/render-adapters.test.tsx +22 -12
  18. package/src/harness/__tests__/test-harness.test.ts +4 -2
  19. package/src/harness/__tests__/types.typestest.tsx +6 -13
  20. package/src/harness/adapters/__tests__/data.test.tsx +12 -4
  21. package/src/harness/adapters/__tests__/ssr.test.tsx +41 -0
  22. package/src/harness/adapters/adapters.ts +3 -0
  23. package/src/harness/adapters/css.tsx +1 -3
  24. package/src/harness/adapters/router.tsx +5 -17
  25. package/src/harness/adapters/ssr.tsx +33 -0
  26. package/src/harness/{make-hook-harness.ts → make-hook-harness.tsx} +3 -2
  27. package/src/harness/{render-adapters.ts → render-adapters.tsx} +1 -2
  28. package/src/harness/types.ts +1 -1
  29. package/src/mock-requester.ts +4 -11
  30. package/src/respond-with.ts +2 -1
  31. package/src/settle-controller.ts +2 -3
  32. package/tsconfig-build.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @khanacademy/wonder-blocks-testing
2
2
 
3
+ ## 9.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 1920feb8: Added new SSR adapter for test harnesses to support `RenderStateRoot` in tests and stories
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [65c02cff]
12
+ - @khanacademy/wonder-blocks-data@13.0.0
13
+
3
14
  ## 8.0.21
4
15
 
5
16
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import * as React from 'react';
2
+ import { useContext } from 'react';
2
3
  import { action } from '@storybook/addon-actions';
3
4
  import { InterceptRequests } from '@khanacademy/wonder-blocks-data';
4
5
  import { StaticRouter, MemoryRouter, Switch, Route } from 'react-router-dom';
6
+ import { KindError, Errors } from '@khanacademy/wonder-stuff-core';
7
+ import { StyleSheet, css } from 'aphrodite';
5
8
 
6
9
  const fixtures = Component => {
7
10
  const templateMap = new WeakMap();
@@ -247,11 +250,12 @@ class SettleController {
247
250
  return this._signal;
248
251
  }
249
252
  settle() {
250
- this._settleFn();
253
+ var _this$_settleFn;
254
+ (_this$_settleFn = this._settleFn) == null ? void 0 : _this$_settleFn.call(this);
251
255
  }
252
256
  }
253
257
 
254
- const defaultConfig$3 = null;
258
+ const defaultConfig$4 = null;
255
259
  const normalizeConfig = config => {
256
260
  if (typeof config === "string") {
257
261
  return {
@@ -276,7 +280,7 @@ const normalizeConfig = config => {
276
280
  }
277
281
  throw new Error(`Invalid config: ${config}`);
278
282
  };
279
- const adapter$3 = (children, config) => {
283
+ const adapter$4 = (children, config) => {
280
284
  const {
281
285
  classes,
282
286
  style
@@ -288,8 +292,8 @@ const adapter$3 = (children, config) => {
288
292
  }, children);
289
293
  };
290
294
 
291
- const defaultConfig$2 = [];
292
- const adapter$2 = (children, config) => {
295
+ const defaultConfig$3 = [];
296
+ const adapter$3 = (children, config) => {
293
297
  let currentChildren = children;
294
298
  const interceptors = Array.isArray(config) ? config : [config];
295
299
  for (const interceptor of interceptors) {
@@ -300,18 +304,18 @@ const adapter$2 = (children, config) => {
300
304
  return React.createElement(React.Fragment, null, currentChildren);
301
305
  };
302
306
 
303
- const defaultConfig$1 = null;
304
- const adapter$1 = (children, config) => React.createElement(React.Fragment, null, React.createElement("div", {
307
+ const defaultConfig$2 = null;
308
+ const adapter$2 = (children, config) => React.createElement(React.Fragment, null, React.createElement("div", {
305
309
  id: config,
306
310
  "data-test-id": config
307
311
  }), children);
308
312
 
309
- const defaultConfig = {
313
+ const defaultConfig$1 = {
310
314
  location: "/"
311
315
  };
312
316
  const maybeWithRoute = (children, path) => {
313
317
  if (path == null) {
314
- return children;
318
+ return React.createElement(React.Fragment, null, children);
315
319
  }
316
320
  return React.createElement(Switch, null, React.createElement(Route, {
317
321
  exact: true,
@@ -323,28 +327,28 @@ const maybeWithRoute = (children, path) => {
323
327
  }
324
328
  }));
325
329
  };
326
- const adapter = (children, config) => {
330
+ const adapter$1 = (children, config) => {
327
331
  if (typeof config === "string") {
328
332
  config = {
329
333
  location: config
330
334
  };
331
335
  }
332
336
  const wrappedWithRoute = maybeWithRoute(children, config.path);
333
- if (config.forceStatic) {
337
+ if ("forceStatic" in config && config.forceStatic) {
334
338
  return React.createElement(StaticRouter, {
335
339
  location: config.location,
336
340
  context: {}
337
341
  }, wrappedWithRoute);
338
342
  }
339
- if (typeof config.location !== "undefined") {
343
+ if ("location" in config && config.location !== undefined) {
340
344
  return React.createElement(MemoryRouter, {
341
345
  initialEntries: [config.location]
342
346
  }, wrappedWithRoute);
343
347
  }
344
- if (typeof config.initialEntries === "undefined") {
348
+ if (!("initialEntries" in config) || config.initialEntries === undefined) {
345
349
  throw new Error("A location or initial history entries must be provided.");
346
350
  }
347
- const entries = config.initialEntries.length === 0 ? [defaultConfig.location] : config.initialEntries;
351
+ const entries = config.initialEntries.length === 0 ? [defaultConfig$1.location] : config.initialEntries;
348
352
  const routerProps = {
349
353
  initialEntries: entries
350
354
  };
@@ -357,17 +361,406 @@ const adapter = (children, config) => {
357
361
  return React.createElement(MemoryRouter, routerProps, wrappedWithRoute);
358
362
  };
359
363
 
364
+ function _extends$1() {
365
+ _extends$1 = Object.assign ? Object.assign.bind() : function (target) {
366
+ for (var i = 1; i < arguments.length; i++) {
367
+ var source = arguments[i];
368
+ for (var key in source) {
369
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
370
+ target[key] = source[key];
371
+ }
372
+ }
373
+ }
374
+ return target;
375
+ };
376
+ return _extends$1.apply(this, arguments);
377
+ }
378
+ function _objectWithoutPropertiesLoose(source, excluded) {
379
+ if (source == null) return {};
380
+ var target = {};
381
+ var sourceKeys = Object.keys(source);
382
+ var key, i;
383
+ for (i = 0; i < sourceKeys.length; i++) {
384
+ key = sourceKeys[i];
385
+ if (excluded.indexOf(key) >= 0) continue;
386
+ target[key] = source[key];
387
+ }
388
+ return target;
389
+ }
390
+ function flatten(list) {
391
+ const result = [];
392
+ if (!list) {
393
+ return result;
394
+ } else if (Array.isArray(list)) {
395
+ for (const item of list) {
396
+ result.push(...flatten(item));
397
+ }
398
+ } else {
399
+ result.push(list);
400
+ }
401
+ return result;
402
+ }
403
+ function processStyleList(style) {
404
+ const stylesheetStyles = [];
405
+ const inlineStyles = [];
406
+ if (!style) {
407
+ return {
408
+ style: {},
409
+ className: ""
410
+ };
411
+ }
412
+ const shouldInlineStyles = typeof global !== "undefined" && global.SNAPSHOT_INLINE_APHRODITE;
413
+ flatten(style).forEach(child => {
414
+ const _definition = child._definition;
415
+ if (_definition != null) {
416
+ if (shouldInlineStyles) {
417
+ const def = {};
418
+ for (const [key, value] of Object.entries(_definition)) {
419
+ def[key.replace(/-[a-z]/g, match => match[1].toUpperCase())] = value;
420
+ }
421
+ inlineStyles.push(def);
422
+ } else {
423
+ stylesheetStyles.push(child);
424
+ }
425
+ } else {
426
+ inlineStyles.push(child);
427
+ }
428
+ });
429
+ const inlineStylesObject = Object.assign({}, ...inlineStyles);
430
+ if (inlineStyles.length > 0 && !shouldInlineStyles) {
431
+ const inlineStylesStyleSheet = StyleSheet.create({
432
+ inlineStyles: inlineStylesObject
433
+ });
434
+ stylesheetStyles.push(inlineStylesStyleSheet.inlineStyles);
435
+ }
436
+ return {
437
+ style: shouldInlineStyles ? inlineStylesObject : {},
438
+ className: css(...stylesheetStyles)
439
+ };
440
+ }
441
+ const _excluded$2 = ["children", "style", "tag", "testId"];
442
+ const isHeaderRegex = /^h[1-6]$/;
443
+ const styles$1 = StyleSheet.create({
444
+ text: {
445
+ WebkitFontSmoothing: "antialiased",
446
+ MozOsxFontSmoothing: "grayscale"
447
+ },
448
+ header: {
449
+ marginTop: 0,
450
+ marginBottom: 0
451
+ }
452
+ });
453
+ React.forwardRef(function Text(_ref, ref) {
454
+ let {
455
+ children,
456
+ style,
457
+ tag: Tag = "span",
458
+ testId
459
+ } = _ref,
460
+ otherProps = _objectWithoutPropertiesLoose(_ref, _excluded$2);
461
+ const isHeader = isHeaderRegex.test(Tag);
462
+ const styleAttributes = processStyleList([styles$1.text, isHeader && styles$1.header, style]);
463
+ return React.createElement(Tag, _extends$1({}, otherProps, {
464
+ style: styleAttributes.style,
465
+ className: styleAttributes.className,
466
+ "data-test-id": testId,
467
+ ref: ref
468
+ }), children);
469
+ });
470
+ const _excluded$1 = ["className", "style"];
471
+ function addStyle(Component, defaultStyle) {
472
+ return React.forwardRef((props, ref) => {
473
+ const {
474
+ className,
475
+ style
476
+ } = props,
477
+ otherProps = _objectWithoutPropertiesLoose(props, _excluded$1);
478
+ const reset = typeof Component === "string" ? overrides[Component] : null;
479
+ const {
480
+ className: aphroditeClassName,
481
+ style: inlineStyles
482
+ } = processStyleList([reset, defaultStyle, style]);
483
+ return React.createElement(Component, _extends$1({}, otherProps, {
484
+ ref: ref,
485
+ className: [aphroditeClassName, className].filter(Boolean).join(" "),
486
+ style: inlineStyles
487
+ }));
488
+ });
489
+ }
490
+ const overrides = StyleSheet.create({
491
+ button: {
492
+ margin: 0,
493
+ "::-moz-focus-inner": {
494
+ border: 0
495
+ }
496
+ }
497
+ });
498
+ const _excluded = ["testId", "tag"];
499
+ const styles = StyleSheet.create({
500
+ default: {
501
+ alignItems: "stretch",
502
+ borderWidth: 0,
503
+ borderStyle: "solid",
504
+ boxSizing: "border-box",
505
+ display: "flex",
506
+ flexDirection: "column",
507
+ margin: 0,
508
+ padding: 0,
509
+ position: "relative",
510
+ zIndex: 0,
511
+ minHeight: 0,
512
+ minWidth: 0
513
+ }
514
+ });
515
+ const StyledDiv = addStyle("div", styles.default);
516
+ const StyledArticle = addStyle("article", styles.default);
517
+ const StyledAside = addStyle("aside", styles.default);
518
+ const StyledNav = addStyle("nav", styles.default);
519
+ const StyledSection = addStyle("section", styles.default);
520
+ React.forwardRef(function View(props, ref) {
521
+ const {
522
+ testId,
523
+ tag = "div"
524
+ } = props,
525
+ restProps = _objectWithoutPropertiesLoose(props, _excluded);
526
+ const commonProps = _extends$1({}, restProps, {
527
+ "data-test-id": testId
528
+ });
529
+ switch (tag) {
530
+ case "article":
531
+ return React.createElement(StyledArticle, _extends$1({}, commonProps, {
532
+ ref: ref
533
+ }));
534
+ case "aside":
535
+ return React.createElement(StyledAside, _extends$1({}, commonProps, {
536
+ ref: ref
537
+ }));
538
+ case "nav":
539
+ return React.createElement(StyledNav, _extends$1({}, commonProps, {
540
+ ref: ref
541
+ }));
542
+ case "section":
543
+ return React.createElement(StyledSection, _extends$1({}, commonProps, {
544
+ ref: ref
545
+ }));
546
+ case "div":
547
+ return React.createElement(StyledDiv, _extends$1({}, commonProps, {
548
+ ref: ref
549
+ }));
550
+ default:
551
+ throw Error(`${tag} is not an allowed value for the 'tag' prop`);
552
+ }
553
+ });
554
+ let RenderState = function (RenderState) {
555
+ RenderState["Root"] = "root";
556
+ RenderState["Initial"] = "initial";
557
+ RenderState["Standard"] = "standard";
558
+ return RenderState;
559
+ }({});
560
+ const RenderStateContext = React.createContext(RenderState.Root);
561
+ RenderStateContext.displayName = "RenderStateContext";
562
+ class WithSSRPlaceholder extends React.Component {
563
+ constructor(...args) {
564
+ super(...args);
565
+ this.state = {
566
+ mounted: false
567
+ };
568
+ this._isTheRootComponent = false;
569
+ }
570
+ componentDidMount() {
571
+ if (this._isTheRootComponent) {
572
+ this.setState({
573
+ mounted: true
574
+ });
575
+ }
576
+ }
577
+ _renderAsRootComponent() {
578
+ const {
579
+ mounted
580
+ } = this.state;
581
+ const {
582
+ children,
583
+ placeholder
584
+ } = this.props;
585
+ this._isTheRootComponent = true;
586
+ if (mounted) {
587
+ return React.createElement(RenderStateContext.Provider, {
588
+ value: RenderState.Standard
589
+ }, children());
590
+ }
591
+ if (placeholder) {
592
+ return React.createElement(RenderStateContext.Provider, {
593
+ value: RenderState.Initial
594
+ }, placeholder());
595
+ }
596
+ return null;
597
+ }
598
+ _maybeRender(renderState) {
599
+ const {
600
+ children,
601
+ placeholder
602
+ } = this.props;
603
+ switch (renderState) {
604
+ case RenderState.Root:
605
+ return this._renderAsRootComponent();
606
+ case RenderState.Initial:
607
+ if (placeholder) {
608
+ return placeholder();
609
+ }
610
+ return null;
611
+ case RenderState.Standard:
612
+ return children();
613
+ }
614
+ {
615
+ var _JSON$stringify;
616
+ console.log(`We got a render state we don't understand: "${(_JSON$stringify = JSON.stringify(renderState)) != null ? _JSON$stringify : ""}"`);
617
+ return this._maybeRender(RenderState.Root);
618
+ }
619
+ }
620
+ render() {
621
+ return React.createElement(RenderStateContext.Consumer, null, value => this._maybeRender(value));
622
+ }
623
+ }
624
+ class UniqueIDFactory {
625
+ constructor(scope) {
626
+ this._uniqueFactoryName = void 0;
627
+ this.get = key => {
628
+ const normalizedKey = key.toLowerCase();
629
+ if (!this._hasValidIdChars(key)) {
630
+ throw new Error(`Invalid identifier key: ${key}`);
631
+ }
632
+ return `${this._uniqueFactoryName}-${normalizedKey}`;
633
+ };
634
+ scope = typeof scope === "string" ? scope : "";
635
+ const normalizedScope = scope.toLowerCase();
636
+ if (!this._hasValidIdChars(normalizedScope)) {
637
+ throw new Error(`Invalid factory scope: ${scope}`);
638
+ }
639
+ this._uniqueFactoryName = `uid-${normalizedScope}-${UniqueIDFactory._factoryUniquenessCounter++}`;
640
+ }
641
+ _hasValidIdChars(value) {
642
+ if (typeof value !== "string") {
643
+ return false;
644
+ }
645
+ const invalidCharsReplaced = value.replace(/[^\d\w-]/g, "-");
646
+ return value === invalidCharsReplaced;
647
+ }
648
+ }
649
+ UniqueIDFactory._factoryUniquenessCounter = 0;
650
+ class SsrIDFactory {
651
+ get(id) {
652
+ return id;
653
+ }
654
+ }
655
+ SsrIDFactory.Default = new SsrIDFactory();
656
+ var SsrIDFactory$1 = SsrIDFactory.Default;
657
+ class UniqueIDProvider extends React.Component {
658
+ constructor(...args) {
659
+ super(...args);
660
+ this._idFactory = void 0;
661
+ }
662
+ _performRender(firstRender) {
663
+ const {
664
+ children,
665
+ mockOnFirstRender,
666
+ scope
667
+ } = this.props;
668
+ if (firstRender) {
669
+ if (mockOnFirstRender) {
670
+ return children(SsrIDFactory$1);
671
+ }
672
+ return null;
673
+ }
674
+ if (!this._idFactory) {
675
+ this._idFactory = new UniqueIDFactory(scope);
676
+ }
677
+ return children(this._idFactory);
678
+ }
679
+ render() {
680
+ return React.createElement(WithSSRPlaceholder, {
681
+ placeholder: () => this._performRender(true)
682
+ }, () => this._performRender(false));
683
+ }
684
+ }
685
+ class IDProvider extends React.Component {
686
+ renderChildren(ids) {
687
+ const {
688
+ id,
689
+ children
690
+ } = this.props;
691
+ const uniqueId = ids ? ids.get(IDProvider.defaultId) : id;
692
+ if (!uniqueId) {
693
+ throw new Error("Did not get an identifier factory nor a id prop");
694
+ }
695
+ return children(uniqueId);
696
+ }
697
+ render() {
698
+ const {
699
+ id,
700
+ scope
701
+ } = this.props;
702
+ if (id) {
703
+ return this.renderChildren();
704
+ } else {
705
+ return React.createElement(UniqueIDProvider, {
706
+ scope: scope,
707
+ mockOnFirstRender: true
708
+ }, ids => this.renderChildren(ids));
709
+ }
710
+ }
711
+ }
712
+ IDProvider.defaultId = "wb-id";
713
+ const useRenderState = () => useContext(RenderStateContext);
714
+ const {
715
+ useEffect,
716
+ useState
717
+ } = React;
718
+ const RenderStateRoot = ({
719
+ children,
720
+ throwIfNested: _throwIfNested = true
721
+ }) => {
722
+ const [firstRender, setFirstRender] = useState(true);
723
+ const renderState = useRenderState();
724
+ useEffect(() => {
725
+ setFirstRender(false);
726
+ }, []);
727
+ if (renderState !== RenderState.Root) {
728
+ if (_throwIfNested) {
729
+ throw new Error("There's already a <RenderStateRoot> above this instance in " + "the render tree. This instance should be removed.");
730
+ }
731
+ return React.createElement(React.Fragment, null, children);
732
+ }
733
+ const value = firstRender ? RenderState.Initial : RenderState.Standard;
734
+ return React.createElement(RenderStateContext.Provider, {
735
+ value: value
736
+ }, children);
737
+ };
738
+
739
+ const defaultConfig = null;
740
+ const adapter = (children, config) => {
741
+ if (config !== true) {
742
+ throw new KindError("Unexpected configuraiton", Errors.InvalidInput, {
743
+ metadata: {
744
+ config
745
+ }
746
+ });
747
+ }
748
+ return React.createElement(RenderStateRoot, null, children);
749
+ };
750
+
360
751
  const DefaultAdapters = {
361
- css: adapter$3,
362
- data: adapter$2,
363
- portal: adapter$1,
364
- router: adapter
752
+ css: adapter$4,
753
+ data: adapter$3,
754
+ portal: adapter$2,
755
+ router: adapter$1,
756
+ ssr: adapter
365
757
  };
366
758
  const DefaultConfigs = {
367
- css: defaultConfig$3,
368
- data: defaultConfig$2,
369
- portal: defaultConfig$1,
370
- router: defaultConfig
759
+ css: defaultConfig$4,
760
+ data: defaultConfig$3,
761
+ portal: defaultConfig$2,
762
+ router: defaultConfig$1,
763
+ ssr: defaultConfig
371
764
  };
372
765
 
373
766
  var adapters = /*#__PURE__*/Object.freeze({
@@ -400,7 +793,7 @@ const renderAdapters = (adapters, configs, children) => {
400
793
  currentChildren = adapter(currentChildren, config);
401
794
  }
402
795
  }
403
- return currentChildren;
796
+ return React.createElement(React.Fragment, null, currentChildren);
404
797
  };
405
798
 
406
799
  const makeTestHarness = (adapters, defaultConfigs) => {
@@ -416,7 +809,7 @@ const makeTestHarness = (adapters, defaultConfigs) => {
416
809
 
417
810
  const HookHarness = ({
418
811
  children
419
- }) => children;
812
+ }) => React.createElement(React.Fragment, null, children);
420
813
  const makeHookHarness = (adapters, defaultConfigs) => {
421
814
  const testHarness = makeTestHarness(adapters, defaultConfigs);
422
815
  return configs => testHarness(HookHarness, configs);
@@ -28,6 +28,7 @@ export declare const DefaultAdapters: {
28
28
  location: import("history").LocationDescriptor<unknown>;
29
29
  path?: string | undefined;
30
30
  }>>;
31
+ readonly ssr: import("../types").TestHarnessAdapter<true | null>;
31
32
  };
32
33
  /**
33
34
  * The default configurations to use with the `DefaultAdapters`.
@@ -0,0 +1,12 @@
1
+ import type { TestHarnessAdapter } from "../types";
2
+ type Config = true | null;
3
+ export declare const defaultConfig: Config;
4
+ /**
5
+ * Test harness adapter for supporting portals.
6
+ *
7
+ * Some components rely on rendering with a React Portal. This adapter ensures
8
+ * that the DOM contains a mounting point for the portal with the expected
9
+ * identifier.
10
+ */
11
+ export declare const adapter: TestHarnessAdapter<Config>;
12
+ export {};
@@ -29,4 +29,5 @@ export declare const testHarness: <TProps extends object>(Component: import("rea
29
29
  location: import("history").LocationDescriptor<unknown>;
30
30
  path?: string | undefined;
31
31
  }>>;
32
+ readonly ssr: import("./types").TestHarnessAdapter<true | null>;
32
33
  }>> | undefined) => import("react").ForwardRefExoticComponent<import("react").PropsWithoutRef<TProps> & import("react").RefAttributes<unknown>>;
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  /**
3
3
  * A adapter to be composed with our test harness infrastructure.
4
4
  */
5
- export type TestHarnessAdapter<TConfig> = (children: React.ReactNode, config: TConfig) => React.ReactElement<any>;
5
+ export type TestHarnessAdapter<TConfig> = (children: React.ReactNode, config: TConfig) => React.ReactElement;
6
6
  /**
7
7
  * A general map of adapters by their identifiers.
8
8
  *