@khanacademy/wonder-blocks-testing 9.0.0 → 9.2.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/dist/index.js CHANGED
@@ -7,7 +7,7 @@ var addonActions = require('@storybook/addon-actions');
7
7
  var wonderBlocksData = require('@khanacademy/wonder-blocks-data');
8
8
  var reactRouterDom = require('react-router-dom');
9
9
  var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
10
- var aphrodite = require('aphrodite');
10
+ var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
11
11
 
12
12
  function _interopNamespace(e) {
13
13
  if (e && e.__esModule) return e;
@@ -384,391 +384,16 @@ const adapter$1 = (children, config) => {
384
384
  return React__namespace.createElement(reactRouterDom.MemoryRouter, routerProps, wrappedWithRoute);
385
385
  };
386
386
 
387
- function _extends$1() {
388
- _extends$1 = Object.assign ? Object.assign.bind() : function (target) {
389
- for (var i = 1; i < arguments.length; i++) {
390
- var source = arguments[i];
391
- for (var key in source) {
392
- if (Object.prototype.hasOwnProperty.call(source, key)) {
393
- target[key] = source[key];
394
- }
395
- }
396
- }
397
- return target;
398
- };
399
- return _extends$1.apply(this, arguments);
400
- }
401
- function _objectWithoutPropertiesLoose(source, excluded) {
402
- if (source == null) return {};
403
- var target = {};
404
- var sourceKeys = Object.keys(source);
405
- var key, i;
406
- for (i = 0; i < sourceKeys.length; i++) {
407
- key = sourceKeys[i];
408
- if (excluded.indexOf(key) >= 0) continue;
409
- target[key] = source[key];
410
- }
411
- return target;
412
- }
413
- function flatten(list) {
414
- const result = [];
415
- if (!list) {
416
- return result;
417
- } else if (Array.isArray(list)) {
418
- for (const item of list) {
419
- result.push(...flatten(item));
420
- }
421
- } else {
422
- result.push(list);
423
- }
424
- return result;
425
- }
426
- function processStyleList(style) {
427
- const stylesheetStyles = [];
428
- const inlineStyles = [];
429
- if (!style) {
430
- return {
431
- style: {},
432
- className: ""
433
- };
434
- }
435
- const shouldInlineStyles = typeof global !== "undefined" && global.SNAPSHOT_INLINE_APHRODITE;
436
- flatten(style).forEach(child => {
437
- const _definition = child._definition;
438
- if (_definition != null) {
439
- if (shouldInlineStyles) {
440
- const def = {};
441
- for (const [key, value] of Object.entries(_definition)) {
442
- def[key.replace(/-[a-z]/g, match => match[1].toUpperCase())] = value;
443
- }
444
- inlineStyles.push(def);
445
- } else {
446
- stylesheetStyles.push(child);
447
- }
448
- } else {
449
- inlineStyles.push(child);
450
- }
451
- });
452
- const inlineStylesObject = Object.assign({}, ...inlineStyles);
453
- if (inlineStyles.length > 0 && !shouldInlineStyles) {
454
- const inlineStylesStyleSheet = aphrodite.StyleSheet.create({
455
- inlineStyles: inlineStylesObject
456
- });
457
- stylesheetStyles.push(inlineStylesStyleSheet.inlineStyles);
458
- }
459
- return {
460
- style: shouldInlineStyles ? inlineStylesObject : {},
461
- className: aphrodite.css(...stylesheetStyles)
462
- };
463
- }
464
- const _excluded$2 = ["children", "style", "tag", "testId"];
465
- const isHeaderRegex = /^h[1-6]$/;
466
- const styles$1 = aphrodite.StyleSheet.create({
467
- text: {
468
- WebkitFontSmoothing: "antialiased",
469
- MozOsxFontSmoothing: "grayscale"
470
- },
471
- header: {
472
- marginTop: 0,
473
- marginBottom: 0
474
- }
475
- });
476
- React__namespace.forwardRef(function Text(_ref, ref) {
477
- let {
478
- children,
479
- style,
480
- tag: Tag = "span",
481
- testId
482
- } = _ref,
483
- otherProps = _objectWithoutPropertiesLoose(_ref, _excluded$2);
484
- const isHeader = isHeaderRegex.test(Tag);
485
- const styleAttributes = processStyleList([styles$1.text, isHeader && styles$1.header, style]);
486
- return React__namespace.createElement(Tag, _extends$1({}, otherProps, {
487
- style: styleAttributes.style,
488
- className: styleAttributes.className,
489
- "data-test-id": testId,
490
- ref: ref
491
- }), children);
492
- });
493
- const _excluded$1 = ["className", "style"];
494
- function addStyle(Component, defaultStyle) {
495
- return React__namespace.forwardRef((props, ref) => {
496
- const {
497
- className,
498
- style
499
- } = props,
500
- otherProps = _objectWithoutPropertiesLoose(props, _excluded$1);
501
- const reset = typeof Component === "string" ? overrides[Component] : null;
502
- const {
503
- className: aphroditeClassName,
504
- style: inlineStyles
505
- } = processStyleList([reset, defaultStyle, style]);
506
- return React__namespace.createElement(Component, _extends$1({}, otherProps, {
507
- ref: ref,
508
- className: [aphroditeClassName, className].filter(Boolean).join(" "),
509
- style: inlineStyles
510
- }));
511
- });
512
- }
513
- const overrides = aphrodite.StyleSheet.create({
514
- button: {
515
- margin: 0,
516
- "::-moz-focus-inner": {
517
- border: 0
518
- }
519
- }
520
- });
521
- const _excluded = ["testId", "tag"];
522
- const styles = aphrodite.StyleSheet.create({
523
- default: {
524
- alignItems: "stretch",
525
- borderWidth: 0,
526
- borderStyle: "solid",
527
- boxSizing: "border-box",
528
- display: "flex",
529
- flexDirection: "column",
530
- margin: 0,
531
- padding: 0,
532
- position: "relative",
533
- zIndex: 0,
534
- minHeight: 0,
535
- minWidth: 0
536
- }
537
- });
538
- const StyledDiv = addStyle("div", styles.default);
539
- const StyledArticle = addStyle("article", styles.default);
540
- const StyledAside = addStyle("aside", styles.default);
541
- const StyledNav = addStyle("nav", styles.default);
542
- const StyledSection = addStyle("section", styles.default);
543
- React__namespace.forwardRef(function View(props, ref) {
544
- const {
545
- testId,
546
- tag = "div"
547
- } = props,
548
- restProps = _objectWithoutPropertiesLoose(props, _excluded);
549
- const commonProps = _extends$1({}, restProps, {
550
- "data-test-id": testId
551
- });
552
- switch (tag) {
553
- case "article":
554
- return React__namespace.createElement(StyledArticle, _extends$1({}, commonProps, {
555
- ref: ref
556
- }));
557
- case "aside":
558
- return React__namespace.createElement(StyledAside, _extends$1({}, commonProps, {
559
- ref: ref
560
- }));
561
- case "nav":
562
- return React__namespace.createElement(StyledNav, _extends$1({}, commonProps, {
563
- ref: ref
564
- }));
565
- case "section":
566
- return React__namespace.createElement(StyledSection, _extends$1({}, commonProps, {
567
- ref: ref
568
- }));
569
- case "div":
570
- return React__namespace.createElement(StyledDiv, _extends$1({}, commonProps, {
571
- ref: ref
572
- }));
573
- default:
574
- throw Error(`${tag} is not an allowed value for the 'tag' prop`);
575
- }
576
- });
577
- let RenderState = function (RenderState) {
578
- RenderState["Root"] = "root";
579
- RenderState["Initial"] = "initial";
580
- RenderState["Standard"] = "standard";
581
- return RenderState;
582
- }({});
583
- const RenderStateContext = React__namespace.createContext(RenderState.Root);
584
- RenderStateContext.displayName = "RenderStateContext";
585
- class WithSSRPlaceholder extends React__namespace.Component {
586
- constructor(...args) {
587
- super(...args);
588
- this.state = {
589
- mounted: false
590
- };
591
- this._isTheRootComponent = false;
592
- }
593
- componentDidMount() {
594
- if (this._isTheRootComponent) {
595
- this.setState({
596
- mounted: true
597
- });
598
- }
599
- }
600
- _renderAsRootComponent() {
601
- const {
602
- mounted
603
- } = this.state;
604
- const {
605
- children,
606
- placeholder
607
- } = this.props;
608
- this._isTheRootComponent = true;
609
- if (mounted) {
610
- return React__namespace.createElement(RenderStateContext.Provider, {
611
- value: RenderState.Standard
612
- }, children());
613
- }
614
- if (placeholder) {
615
- return React__namespace.createElement(RenderStateContext.Provider, {
616
- value: RenderState.Initial
617
- }, placeholder());
618
- }
619
- return null;
620
- }
621
- _maybeRender(renderState) {
622
- const {
623
- children,
624
- placeholder
625
- } = this.props;
626
- switch (renderState) {
627
- case RenderState.Root:
628
- return this._renderAsRootComponent();
629
- case RenderState.Initial:
630
- if (placeholder) {
631
- return placeholder();
632
- }
633
- return null;
634
- case RenderState.Standard:
635
- return children();
636
- }
637
- {
638
- var _JSON$stringify;
639
- console.log(`We got a render state we don't understand: "${(_JSON$stringify = JSON.stringify(renderState)) != null ? _JSON$stringify : ""}"`);
640
- return this._maybeRender(RenderState.Root);
641
- }
642
- }
643
- render() {
644
- return React__namespace.createElement(RenderStateContext.Consumer, null, value => this._maybeRender(value));
645
- }
646
- }
647
- class UniqueIDFactory {
648
- constructor(scope) {
649
- this._uniqueFactoryName = void 0;
650
- this.get = key => {
651
- const normalizedKey = key.toLowerCase();
652
- if (!this._hasValidIdChars(key)) {
653
- throw new Error(`Invalid identifier key: ${key}`);
654
- }
655
- return `${this._uniqueFactoryName}-${normalizedKey}`;
656
- };
657
- scope = typeof scope === "string" ? scope : "";
658
- const normalizedScope = scope.toLowerCase();
659
- if (!this._hasValidIdChars(normalizedScope)) {
660
- throw new Error(`Invalid factory scope: ${scope}`);
661
- }
662
- this._uniqueFactoryName = `uid-${normalizedScope}-${UniqueIDFactory._factoryUniquenessCounter++}`;
663
- }
664
- _hasValidIdChars(value) {
665
- if (typeof value !== "string") {
666
- return false;
667
- }
668
- const invalidCharsReplaced = value.replace(/[^\d\w-]/g, "-");
669
- return value === invalidCharsReplaced;
670
- }
671
- }
672
- UniqueIDFactory._factoryUniquenessCounter = 0;
673
- class SsrIDFactory {
674
- get(id) {
675
- return id;
676
- }
677
- }
678
- SsrIDFactory.Default = new SsrIDFactory();
679
- var SsrIDFactory$1 = SsrIDFactory.Default;
680
- class UniqueIDProvider extends React__namespace.Component {
681
- constructor(...args) {
682
- super(...args);
683
- this._idFactory = void 0;
684
- }
685
- _performRender(firstRender) {
686
- const {
687
- children,
688
- mockOnFirstRender,
689
- scope
690
- } = this.props;
691
- if (firstRender) {
692
- if (mockOnFirstRender) {
693
- return children(SsrIDFactory$1);
694
- }
695
- return null;
696
- }
697
- if (!this._idFactory) {
698
- this._idFactory = new UniqueIDFactory(scope);
699
- }
700
- return children(this._idFactory);
701
- }
702
- render() {
703
- return React__namespace.createElement(WithSSRPlaceholder, {
704
- placeholder: () => this._performRender(true)
705
- }, () => this._performRender(false));
706
- }
707
- }
708
- class IDProvider extends React__namespace.Component {
709
- renderChildren(ids) {
710
- const {
711
- id,
712
- children
713
- } = this.props;
714
- const uniqueId = ids ? ids.get(IDProvider.defaultId) : id;
715
- if (!uniqueId) {
716
- throw new Error("Did not get an identifier factory nor a id prop");
717
- }
718
- return children(uniqueId);
719
- }
720
- render() {
721
- const {
722
- id,
723
- scope
724
- } = this.props;
725
- if (id) {
726
- return this.renderChildren();
727
- } else {
728
- return React__namespace.createElement(UniqueIDProvider, {
729
- scope: scope,
730
- mockOnFirstRender: true
731
- }, ids => this.renderChildren(ids));
732
- }
733
- }
734
- }
735
- IDProvider.defaultId = "wb-id";
736
- const useRenderState = () => React.useContext(RenderStateContext);
737
- const {
738
- useEffect,
739
- useState
740
- } = React__namespace;
741
- const RenderStateRoot = ({
742
- children,
743
- throwIfNested: _throwIfNested = true
744
- }) => {
745
- const [firstRender, setFirstRender] = useState(true);
746
- const renderState = useRenderState();
747
- useEffect(() => {
748
- setFirstRender(false);
749
- }, []);
750
- if (renderState !== RenderState.Root) {
751
- if (_throwIfNested) {
752
- throw new Error("There's already a <RenderStateRoot> above this instance in " + "the render tree. This instance should be removed.");
753
- }
754
- return React__namespace.createElement(React__namespace.Fragment, null, children);
755
- }
756
- const value = firstRender ? RenderState.Initial : RenderState.Standard;
757
- return React__namespace.createElement(RenderStateContext.Provider, {
758
- value: value
759
- }, children);
760
- };
761
-
762
387
  const defaultConfig = null;
763
388
  const adapter = (children, config) => {
764
389
  if (config !== true) {
765
- throw new wonderStuffCore.KindError("Unexpected configuraiton", wonderStuffCore.Errors.InvalidInput, {
390
+ throw new wonderStuffCore.KindError("Unexpected configuration: set config to null to turn this adapter off", wonderStuffCore.Errors.InvalidInput, {
766
391
  metadata: {
767
392
  config
768
393
  }
769
394
  });
770
395
  }
771
- return React__namespace.createElement(RenderStateRoot, null, children);
396
+ return React__namespace.createElement(wonderBlocksCore.RenderStateRoot, null, children);
772
397
  };
773
398
 
774
399
  const DefaultAdapters = {
@@ -807,22 +432,29 @@ function _extends() {
807
432
  return _extends.apply(this, arguments);
808
433
  }
809
434
 
810
- const renderAdapters = (adapters, configs, children) => {
811
- let currentChildren = children;
812
- for (const adapterName of Object.keys(adapters)) {
813
- const adapter = adapters[adapterName];
814
- const config = configs[adapterName];
815
- if (config != null) {
816
- currentChildren = adapter(currentChildren, config);
817
- }
818
- }
819
- return React__namespace.createElement(React__namespace.Fragment, null, currentChildren);
820
- };
435
+ const Adapt = ({
436
+ children,
437
+ adapters,
438
+ configs
439
+ }) => Object.entries(adapters).reduce((newChildren, [name, adapter]) => {
440
+ const config = configs[name];
441
+ if (config == null) {
442
+ return newChildren;
443
+ }
444
+ const Adapter = ({
445
+ children
446
+ }) => adapter(children, config);
447
+ Adapter.displayName = `Adapter(${name})`;
448
+ return React__namespace.createElement(Adapter, null, newChildren);
449
+ }, React__namespace.createElement(React__namespace.Fragment, null, children));
821
450
 
822
451
  const makeTestHarness = (adapters, defaultConfigs) => {
823
452
  return (Component, configs) => {
824
453
  const fullConfig = _extends({}, defaultConfigs, configs);
825
- const harnessedComponent = React__namespace.forwardRef((props, ref) => renderAdapters(adapters, fullConfig, React__namespace.createElement(Component, _extends({}, props, {
454
+ const harnessedComponent = React__namespace.forwardRef((props, ref) => React__namespace.createElement(Adapt, {
455
+ adapters: adapters,
456
+ configs: fullConfig
457
+ }, React__namespace.createElement(Component, _extends({}, props, {
826
458
  ref: ref
827
459
  }))));
828
460
  harnessedComponent.displayName = `testHarness(${Component.displayName || Component.name || "Component"})`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-testing",
3
- "version": "9.0.0",
3
+ "version": "9.2.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,7 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@babel/runtime": "^7.18.6",
17
- "@khanacademy/wonder-blocks-data": "^13.0.0"
17
+ "@khanacademy/wonder-blocks-core": "^6.0.2",
18
+ "@khanacademy/wonder-blocks-data": "^13.0.1"
18
19
  },
19
20
  "peerDependencies": {
20
21
  "@khanacademy/wonder-stuff-core": "^1.2.2",
@@ -0,0 +1,200 @@
1
+ import * as React from "react";
2
+ import {render} from "@testing-library/react";
3
+
4
+ import {Adapt} from "../adapt";
5
+
6
+ import type {TestHarnessAdapter, TestHarnessConfigs} from "../types";
7
+
8
+ describe("Adapt", () => {
9
+ it("should render children if no adapters", () => {
10
+ // Arrange
11
+ const children = <div>Adapt me!</div>;
12
+
13
+ // Act
14
+ const {container: result} = render(
15
+ <Adapt adapters={{}} configs={{}}>
16
+ {children}
17
+ </Adapt>,
18
+ );
19
+
20
+ // Assert
21
+ expect(result).toMatchInlineSnapshot(`
22
+ <div>
23
+ <div>
24
+ Adapt me!
25
+ </div>
26
+ </div>
27
+ `);
28
+ });
29
+
30
+ it("should invoke the adapter with its corresponding config", () => {
31
+ // Arrange
32
+ const children = <div>Adapt me!</div>;
33
+ const adapters = {
34
+ adapterA: jest
35
+ .fn()
36
+ .mockReturnValue("ADAPTER A") as TestHarnessAdapter<string>,
37
+ } as const;
38
+ const configs: TestHarnessConfigs<typeof adapters> = {
39
+ adapterA: "APPLY A CONFIG",
40
+ };
41
+
42
+ // Act
43
+ render(
44
+ <Adapt adapters={adapters} configs={configs}>
45
+ {children}
46
+ </Adapt>,
47
+ );
48
+
49
+ // Assert
50
+ expect(adapters.adapterA).toHaveBeenCalledWith(
51
+ expect.anything(),
52
+ "APPLY A CONFIG",
53
+ );
54
+ });
55
+
56
+ it("should render each adapter and the children", () => {
57
+ // Arrange
58
+ const children = "Adapt me!";
59
+ const adapter: TestHarnessAdapter<string> = (c: any, conf: any) => (
60
+ <>
61
+ {conf}
62
+ {c}
63
+ </>
64
+ );
65
+ const adapters = {
66
+ adapterA: adapter,
67
+ adapterB: adapter,
68
+ adapterC: adapter,
69
+ } as const;
70
+ const configs: TestHarnessConfigs<typeof adapters> = {
71
+ adapterA: "A",
72
+ adapterB: "B",
73
+ adapterC: "C",
74
+ };
75
+
76
+ // Act
77
+ const {container: result} = render(
78
+ <Adapt adapters={adapters} configs={configs}>
79
+ {children}
80
+ </Adapt>,
81
+ );
82
+
83
+ // Assert
84
+ expect(result).toMatchInlineSnapshot(`
85
+ <div>
86
+ C
87
+ B
88
+ A
89
+ Adapt me!
90
+ </div>
91
+ `);
92
+ });
93
+
94
+ it("should skip adapters where the corresponding config is null", () => {
95
+ // Arrange
96
+ const children = "Adapt me!";
97
+ const adapter: TestHarnessAdapter<string> = (c: any, conf: any) => (
98
+ <>
99
+ {conf}
100
+ {c}
101
+ </>
102
+ );
103
+ const adapters = {
104
+ adapterA: adapter,
105
+ adapterB: adapter,
106
+ adapterC: adapter,
107
+ } as const;
108
+ const configs: TestHarnessConfigs<typeof adapters> = {
109
+ adapterA: "A",
110
+ adapterB: null,
111
+ adapterC: "C",
112
+ };
113
+
114
+ // Act
115
+ const {container: result} = render(
116
+ <Adapt adapters={adapters} configs={configs}>
117
+ {children}
118
+ </Adapt>,
119
+ );
120
+
121
+ // Assert
122
+ expect(result).toMatchInlineSnapshot(`
123
+ <div>
124
+ C
125
+ A
126
+ Adapt me!
127
+ </div>
128
+ `);
129
+ });
130
+
131
+ it("should render such that contexts are properly setup in order", () => {
132
+ // Arrange
133
+ const MyContext = React.createContext("root");
134
+ const ContextRoot = ({children}: any): React.ReactElement => {
135
+ const value = React.useContext(MyContext);
136
+ if (value !== "root") {
137
+ throw new Error("Don't nest this");
138
+ }
139
+ return (
140
+ <MyContext.Provider value={"other"}>
141
+ {children}
142
+ </MyContext.Provider>
143
+ );
144
+ };
145
+ const adapterNoContext: TestHarnessAdapter<string> = (
146
+ c: any,
147
+ conf: any,
148
+ ) => (
149
+ <>
150
+ {conf}
151
+ {c}
152
+ </>
153
+ );
154
+ const adapterWithContext: TestHarnessAdapter<string> = (
155
+ c: any,
156
+ conf: string,
157
+ ) => (
158
+ <ContextRoot>
159
+ {conf}
160
+ {c}
161
+ </ContextRoot>
162
+ );
163
+ const adapters = {
164
+ adapterA: adapterNoContext,
165
+ adapterB: adapterWithContext,
166
+ adapterC: adapterNoContext,
167
+ } as const;
168
+ const configs: TestHarnessConfigs<typeof adapters> = {
169
+ adapterA: "A",
170
+ adapterB: "B",
171
+ adapterC: "C",
172
+ };
173
+ const ChildComponent = (props: any): React.ReactElement => {
174
+ const value = React.useContext(MyContext);
175
+ if (value === "default") {
176
+ throw new Error("Context not setup properly");
177
+ }
178
+ return <div>{value}</div>;
179
+ };
180
+
181
+ // Act
182
+ const {container: result} = render(
183
+ <Adapt adapters={adapters} configs={configs}>
184
+ <ChildComponent />
185
+ </Adapt>,
186
+ );
187
+
188
+ // Assert
189
+ expect(result).toMatchInlineSnapshot(`
190
+ <div>
191
+ C
192
+ B
193
+ A
194
+ <div>
195
+ other
196
+ </div>
197
+ </div>
198
+ `);
199
+ });
200
+ });