From f9ad0cc1217c8b97c6dc99f834c6db550277509e Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 11 Feb 2018 11:18:16 -0500 Subject: [PATCH 01/10] Add optional React 16.2.0 by defining REACT_VERSION for package --- .eslintignore | 4 +- dash_renderer/__init__.py | 28 ++- .../react-dom@16.2.0.production.min.js | 193 ++++++++++++++++++ dash_renderer/react@16.2.0.production.min.js | 21 ++ dash_renderer/version.py | 2 +- 5 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 dash_renderer/react-dom@16.2.0.production.min.js create mode 100644 dash_renderer/react@16.2.0.production.min.js diff --git a/.eslintignore b/.eslintignore index d5c24ef..a2d6c16 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,5 +14,5 @@ dist npm-debug* dash_renderer/bundle.js dash_renderer/bundle.js.map -dash_renderer/react-dom@15.4.2.min.js -dash_renderer/react@15.4.2.min.js +dash_renderer/react@*.min.js +dash_renderer/react-dom@*.min.js diff --git a/dash_renderer/__init__.py b/dash_renderer/__init__.py index d9005d0..7cca24c 100644 --- a/dash_renderer/__init__.py +++ b/dash_renderer/__init__.py @@ -10,10 +10,11 @@ from .version import __version__ __file__ -# Dash renderer's dependencies get loaded in a special order by the server: -# React bundles first, the renderer bundle at the very end. -_js_dist_dependencies = [ - { +REACT_VERSION = '15.4.2' +_REACT_VERSION_TYPES = {'15.4.2', '16.2.0'} +assert REACT_VERSION in _REACT_VERSION_TYPES +_REACT_VERSION_TO_URLS = { + '15.4.2': { 'external_url': [ 'https://unpkg.com/react@15.4.2/dist/react.min.js', 'https://unpkg.com/react-dom@15.4.2/dist/react-dom.min.js' @@ -22,6 +23,25 @@ 'react@15.4.2.min.js', 'react-dom@15.4.2.min.js' ], + }, + '16.2.0': { + 'external_url': [ + 'https://unpkg.com/react@16.2.0/umd/react.production.min.js', + 'https://unpkg.com/react@16.2.0/umd/react-dom.production.min.js' + ], + 'relative_package_path': [ + 'react@16.2.0.production.min.js', + 'react-dom@16.2.0.production.min.js' + ], + } +} + +# Dash renderer's dependencies get loaded in a special order by the server: +# React bundles first, the renderer bundle at the very end. +_js_dist_dependencies = [ + { + 'external_url': _REACT_VERSION_TO_URLS[REACT_VERSION]['external_url'], + 'relative_package_path': _REACT_VERSION_TO_URLS[REACT_VERSION]['relative_package_path'], 'namespace': 'dash_renderer' } ] diff --git a/dash_renderer/react-dom@16.2.0.production.min.js b/dash_renderer/react-dom@16.2.0.production.min.js new file mode 100644 index 0000000..7528a54 --- /dev/null +++ b/dash_renderer/react-dom@16.2.0.production.min.js @@ -0,0 +1,193 @@ +/** @license React v16.2.0 + * react-dom.production.min.js + * + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + Modernizr 3.0.0pre (Custom Build) | MIT +*/ +'use strict';(function(na,l){"object"===typeof exports&&"undefined"!==typeof module?module.exports=l(require("react")):"function"===typeof define&&define.amd?define(["react"],l):na.ReactDOM=l(na.React)})(this,function(na){function l(a){for(var b=arguments.length-1,c="Minified React error #"+a+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant\x3d"+a,d=0;dthis.eventPool.length&&this.eventPool.push(a)}function md(a){a.eventPool=[];a.getPooled=ef;a.release=ff}function nd(a,b,c,d){return n.call(this,a,b,c,d)}function od(a,b,c,d){return n.call(this,a,b,c,d)}function gf(){var a=window.opera;return"object"===typeof a&&"function"===typeof a.version&&12>=parseInt(a.version(),10)}function pd(a,b){switch(a){case "topKeyUp":return-1!==hf.indexOf(b.keyCode);case "topKeyDown":return 229!==b.keyCode;case "topKeyPress":case "topMouseDown":case "topBlur":return!0; +default:return!1}}function qd(a){a=a.detail;return"object"===typeof a&&"data"in a?a.data:null}function jf(a,b){switch(a){case "topCompositionEnd":return qd(b);case "topKeyPress":if(32!==b.which)return null;rd=!0;return sd;case "topTextInput":return a=b.data,a===sd&&rd?null:a;default:return null}}function kf(a,b){if(za)return"topCompositionEnd"===a||!bc&&pd(a,b)?(a=kd(),H._root=null,H._startText=null,H._fallbackText=null,za=!1,a):null;switch(a){case "topPaste":return null;case "topKeyPress":if(!(b.ctrlKey|| +b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1qb.length&&qb.push(a)}}}function rb(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;c["ms"+a]="MS"+b;c["O"+a]="o"+b.toLowerCase();return c}function sb(a){if(kc[a])return kc[a];if(!U[a])return a;var b=U[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Jd)return kc[a]=b[c];return""}function Kd(a){Object.prototype.hasOwnProperty.call(a,tb)||(a[tb]=zf++,Ld[a[tb]]= +{});return Ld[a[tb]]}function Md(a,b){return a===b?0!==a||0!==b||1/a===1/b:a!==a&&b!==b}function Nd(a,b){return a&&b?a===b?!0:Od(a)?!1:Od(b)?Nd(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function Pd(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function Qd(a,b){var c=Pd(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c= +c.parentNode}c=void 0}c=Pd(c)}}function lc(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&"text"===a.type||"textarea"===b||"true"===a.contentEditable)}function Rd(a,b){if(mc||null==X||X!==nc())return null;var c=X;"selectionStart"in c&&lc(c)?c={start:c.selectionStart,end:c.selectionEnd}:window.getSelection?(c=window.getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset}):c=void 0;return Sa&&oc(Sa,c)?null:(Sa= +c,a=n.getPooled(Sd.select,pc,a,b),a.type="select",a.target=X,ya(a),a)}function Td(a,b,c,d){return n.call(this,a,b,c,d)}function Ud(a,b,c,d){return n.call(this,a,b,c,d)}function Vd(a,b,c,d){return n.call(this,a,b,c,d)}function ub(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;return 32<=a||13===a?a:0}function Wd(a,b,c,d){return n.call(this,a,b,c,d)}function Xd(a,b,c,d){return n.call(this,a,b,c,d)}function Yd(a,b,c,d){return n.call(this,a,b,c,d)}function Zd(a,b,c,d){return n.call(this, +a,b,c,d)}function $d(a,b,c,d){return n.call(this,a,b,c,d)}function I(a,b){0>ra||(a.current=vb[ra],vb[ra]=null,ra--)}function M(a,b,c){ra++;vb[ra]=a.current;a.current=b}function Ta(a){return Ua(a)?wb:ia.current}function Va(a,b){var c=a.type.contextTypes;if(!c)return ja;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext= +e);return e}function Ua(a){return 2===a.tag&&null!=a.type.childContextTypes}function ae(a){Ua(a)&&(I(J,a),I(ia,a))}function be(a,b,c){null!=ia.cursor?l("168"):void 0;M(ia,b,a);M(J,c,a)}function ce(a,b){var c=a.stateNode,d=a.type.childContextTypes;if("function"!==typeof c.getChildContext)return b;c=c.getChildContext();for(var e in c)e in d?void 0:l("108",Pa(a)||"Unknown",e);return C({},b,c)}function xb(a){if(!Ua(a))return!1;var b=a.stateNode;b=b&&b.__reactInternalMemoizedMergedChildContext||ja;wb= +ia.current;M(ia,b,a);M(J,J.current,a);return!0}function de(a,b){var c=a.stateNode;c?void 0:l("169");if(b){var d=ce(a,wb);c.__reactInternalMemoizedMergedChildContext=d;I(J,a);I(ia,a);M(ia,d,a)}else I(J,a);M(J,b,a)}function Q(a,b,c){this.tag=a;this.key=b;this.stateNode=this.type=null;this.sibling=this.child=this["return"]=null;this.index=0;this.memoizedState=this.updateQueue=this.memoizedProps=this.pendingProps=this.ref=null;this.internalContextTag=c;this.effectTag=0;this.lastEffect=this.firstEffect= +this.nextEffect=null;this.expirationTime=0;this.alternate=null}function yb(a,b,c){var d=a.alternate;null===d?(d=new Q(a.tag,a.key,a.internalContextTag),d.type=a.type,d.stateNode=a.stateNode,d.alternate=a,a.alternate=d):(d.effectTag=0,d.nextEffect=null,d.firstEffect=null,d.lastEffect=null);d.expirationTime=c;d.pendingProps=b;d.child=a.child;d.memoizedProps=a.memoizedProps;d.memoizedState=a.memoizedState;d.updateQueue=a.updateQueue;d.sibling=a.sibling;d.index=a.index;d.ref=a.ref;return d}function qc(a, +b,c){var d=void 0,e=a.type,f=a.key;"function"===typeof e?(d=e.prototype&&e.prototype.isReactComponent?new Q(2,f,b):new Q(0,f,b),d.type=e,d.pendingProps=a.props):"string"===typeof e?(d=new Q(5,f,b),d.type=e,d.pendingProps=a.props):"object"===typeof e&&null!==e&&"number"===typeof e.tag?(d=e,d.pendingProps=a.props):l("130",null==e?e:typeof e,"");d.expirationTime=c;return d}function zb(a,b,c,d){b=new Q(10,d,b);b.pendingProps=a;b.expirationTime=c;return b}function rc(a,b,c){b=new Q(6,null,b);b.pendingProps= +a;b.expirationTime=c;return b}function sc(a,b,c){b=new Q(7,a.key,b);b.type=a.handler;b.pendingProps=a;b.expirationTime=c;return b}function tc(a,b,c){a=new Q(9,null,b);a.expirationTime=c;return a}function uc(a,b,c){b=new Q(4,a.key,b);b.pendingProps=a.children||[];b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function ee(a){return function(b){try{return a(b)}catch(c){}}}function Af(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1; +var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=b.inject(a);vc=ee(function(a){return b.onCommitFiberRoot(c,a)});wc=ee(function(a){return b.onCommitFiberUnmount(c,a)})}catch(d){}return!0}function fe(a){"function"===typeof vc&&vc(a)}function ge(a){"function"===typeof wc&&wc(a)}function he(a){return{baseState:a,expirationTime:0,first:null,last:null,callbackList:null,hasForceUpdate:!1,isInitialized:!1}}function Ab(a,b){null===a.last?a.first=a.last=b:(a.last.next= +b,a.last=b);if(0===a.expirationTime||a.expirationTime>b.expirationTime)a.expirationTime=b.expirationTime}function Bb(a,b){var c=a.alternate,d=a.updateQueue;null===d&&(d=a.updateQueue=he(null));null!==c?(a=c.updateQueue,null===a&&(a=c.updateQueue=he(null))):a=null;a=a!==d?a:null;null===a?Ab(d,b):null===d.last||null===a.last?(Ab(d,b),Ab(a,b)):(Ab(d,b),a.last=b)}function ie(a,b,c,d){a=a.partialState;return"function"===typeof a?a.call(b,c,d):a}function xc(a,b,c,d,e,f){null!==a&&a.updateQueue===c&&(c= +b.updateQueue={baseState:c.baseState,expirationTime:c.expirationTime,first:c.first,last:c.last,isInitialized:c.isInitialized,callbackList:null,hasForceUpdate:!1});c.expirationTime=0;c.isInitialized?a=c.baseState:(a=c.baseState=b.memoizedState,c.isInitialized=!0);for(var g=!0,h=c.first,k=!1;null!==h;){var l=h.expirationTime;if(l>f){var D=c.expirationTime;if(0===D||D>l)c.expirationTime=l;k||(k=!0,c.baseState=a)}else{k||(c.first=h.next,null===c.first&&(c.last=null));if(h.isReplace)a=ie(h,d,a,e),g=!0; +else if(l=ie(h,d,a,e))a=g?C({},a,l):C(a,l),g=!1;h.isForced&&(c.hasForceUpdate=!0);null!==h.callback&&(l=c.callbackList,null===l&&(l=c.callbackList=[]),l.push(h))}h=h.next}null!==c.callbackList?b.effectTag|=32:null!==c.first||c.hasForceUpdate||(b.updateQueue=null);k||(c.baseState=a);return a}function je(a,b){var c=a.callbackList;if(null!==c)for(a.callbackList=null,a=0;ax?(k=p,p=null):k=p.sibling;var l=L(e,p,h[x],z);if(null===l){null===p&&(p=k);break}a&&p&&null===l.alternate&& +b(e,p);g=f(l,g,x);null===q?t=l:q.sibling=l;q=l;p=k}if(x===h.length)return c(e,p),t;if(null===p){for(;xx?(k=p,p=null):k=p.sibling;var La=L(e,p,m.value,z);if(null===La){p||(p=k);break}a&&p&&null===La.alternate&&b(e,p);g=f(La,g,x);null===q?t=La:q.sibling=La;q=La;p=k}if(m.done)return c(e,p),t;if(null===p){for(;!m.done;x++,m=h.next())m=K(e,m.value,z),null!==m&&(g=f(m,g,x),null===q?t=m:q.sibling=m,q=m);return t}for(p=d(e,p);!m.done;x++,m=h.next())if(m=R(p,e,x,m.value,z),null!==m){if(a&&null!==m.alternate)p["delete"](null===m.key?x:m.key); +g=f(m,g,x);null===q?t=m:q.sibling=m;q=m}a&&p.forEach(function(a){return b(e,a)});return t}return function(a,d,f,h){"object"===typeof f&&null!==f&&f.type===sa&&null===f.key&&(f=f.props.children);var k="object"===typeof f&&null!==f;if(k)switch(f.$$typeof){case Db:a:{var q=f.key;for(k=d;null!==k;){if(k.key===q)if(10===k.tag?f.type===sa:k.type===f.type){c(a,k.sibling);d=e(k,f.type===sa?f.props.children:f.props,h);d.ref=Xa(k,f);d["return"]=a;a=d;break a}else{c(a,k);break}else b(a,k);k=k.sibling}f.type=== +sa?(d=zb(f.props.children,a.internalContextTag,h,f.key),d["return"]=a,a=d):(h=qc(f,a.internalContextTag,h),h.ref=Xa(d,f),h["return"]=a,a=h)}return g(a);case Eb:a:{for(k=f.key;null!==d;){if(d.key===k)if(7===d.tag){c(a,d.sibling);d=e(d,f,h);d["return"]=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=sc(f,a.internalContextTag,h);d["return"]=a;a=d}return g(a);case Fb:a:{if(null!==d)if(9===d.tag){c(a,d.sibling);d=e(d,null,h);d.type=f.value;d["return"]=a;a=d;break a}else c(a,d);d=tc(f,a.internalContextTag, +h);d.type=f.value;d["return"]=a;a=d}return g(a);case Ya:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[],h);d["return"]=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=uc(f,a.internalContextTag,h);d["return"]=a;a=d}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f,h)):(c(a,d),d=rc(f,a.internalContextTag, +h)),d["return"]=a,a=d,g(a);if(Gb(f))return n(a,d,f,h);if(Wa(f))return r(a,d,f,h);k&&Cb(a,f);if("undefined"===typeof f)switch(a.tag){case 2:case 1:h=a.type,l("152",h.displayName||h.name||"Component")}return c(a,d)}}function Bf(a,b,c){var d=3c||d.hasOverloadedBooleanValue&&!1===c?oe(a,b):d.mustUseProperty?a[d.propertyName]=c:(b=d.attributeName,(e=d.attributeNamespace)?a.setAttributeNS(e,b,""+c):d.hasBooleanValue||d.hasOverloadedBooleanValue&&!0===c?a.setAttribute(b,""):a.setAttribute(b,""+c))}else Ac(a,b,Xc(b,c)?c:null)}function Ac(a,b,c){Cf(b)&&(null==c?a.removeAttribute(b): +a.setAttribute(b,""+c))}function oe(a,b){var c=Ub(b);c?(b=c.mutationMethod)?b(a,void 0):c.mustUseProperty?a[c.propertyName]=c.hasBooleanValue?!1:"":a.removeAttribute(c.attributeName):a.removeAttribute(b)}function Bc(a,b){var c=b.value,d=b.checked;return C({type:void 0,step:void 0,min:void 0,max:void 0},b,{defaultChecked:void 0,defaultValue:void 0,value:null!=c?c:a._wrapperState.initialValue,checked:null!=d?d:a._wrapperState.initialChecked})}function pe(a,b){var c=b.defaultValue;a._wrapperState={initialChecked:null!= +b.checked?b.checked:b.defaultChecked,initialValue:null!=b.value?b.value:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function qe(a,b){b=b.checked;null!=b&&zc(a,"checked",b)}function Cc(a,b){qe(a,b);var c=b.value;if(null!=c)if(0===c&&""===a.value)a.value="0";else if("number"===b.type){if(b=parseFloat(a.value)||0,c!=b||c==b&&a.value!=c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else null==b.value&&null!=b.defaultValue&&a.defaultValue!==""+b.defaultValue&&(a.defaultValue= +""+b.defaultValue),null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function re(a,b){switch(b.type){case "submit":case "reset":break;case "color":case "date":case "datetime":case "datetime-local":case "month":case "time":case "week":a.value="";a.value=a.defaultValue;break;default:a.value=a.value}b=a.name;""!==b&&(a.name="");a.defaultChecked=!a.defaultChecked;a.defaultChecked=!a.defaultChecked;""!==b&&(a.name=b)}function Ef(a){var b="";na.Children.forEach(a,function(a){null== +a||"string"!==typeof a&&"number"!==typeof a||(b+=a)});return b}function Dc(a,b){a=C({children:void 0},b);if(b=Ef(b.children))a.children=b;return a}function ka(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e=b.length?void 0:l("93"),b=b[0]),c=""+b),null==c&&(c=""));a._wrapperState={initialValue:""+ +c}}function ue(a,b){var c=b.value;null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&(a.defaultValue=c));null!=b.defaultValue&&(a.defaultValue=b.defaultValue)}function ve(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Fc(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?ve(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml": +a}function we(a,b,c){a=a.style;for(var d in b)if(b.hasOwnProperty(d)){c=0===d.indexOf("--");var e=d;var f=b[d];e=null==f||"boolean"===typeof f||""===f?"":c||"number"!==typeof f||0===f||Za.hasOwnProperty(e)&&Za[e]?(""+f).trim():f+"px";"float"===d&&(d="cssFloat");c?a.setProperty(d,e):a[d]=e}}function Gc(a,b,c){b&&(Ff[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML?l("137",a,c()):void 0),null!=b.dangerouslySetInnerHTML&&(null!=b.children?l("60"):void 0,"object"===typeof b.dangerouslySetInnerHTML&& +"__html"in b.dangerouslySetInnerHTML?void 0:l("61")),null!=b.style&&"object"!==typeof b.style?l("62",c()):void 0)}function Hc(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;default:return!0}}function Y(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=Kd(a);b=kb[b];for(var d=0;d=g.hasBooleanValue+g.hasNumericValue+g.hasOverloadedBooleanValue?void 0:l("50",f);e.hasOwnProperty(f)&&(g.attributeName=e[f]);d.hasOwnProperty(f)&&(g.attributeNamespace=d[f]);a.hasOwnProperty(f)&&(g.mutationMethod=a[f]);ib[f]=g}}},ib={},aa=Ie,Ib=aa.MUST_USE_PROPERTY,w=aa.HAS_BOOLEAN_VALUE,Je=aa.HAS_NUMERIC_VALUE,Jb=aa.HAS_POSITIVE_NUMERIC_VALUE,Ke=aa.HAS_OVERLOADED_BOOLEAN_VALUE, +Kb=aa.HAS_STRING_BOOLEAN_VALUE,Hf={Properties:{allowFullScreen:w,async:w,autoFocus:w,autoPlay:w,capture:Ke,checked:Ib|w,cols:Jb,contentEditable:Kb,controls:w,"default":w,defer:w,disabled:w,download:Ke,draggable:Kb,formNoValidate:w,hidden:w,loop:w,multiple:Ib|w,muted:Ib|w,noValidate:w,open:w,playsInline:w,readOnly:w,required:w,reversed:w,rows:Jb,rowSpan:Je,scoped:w,seamless:w,selected:Ib|w,size:Jb,start:Je,span:Jb,spellCheck:Kb,style:0,tabIndex:0,itemScope:w,acceptCharset:0,className:0,htmlFor:0,httpEquiv:0, +value:Kb},DOMAttributeNames:{acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},DOMMutationMethods:{value:function(a,b){if(null==b)return a.removeAttribute("value");"number"!==a.type||!1===a.hasAttribute("value")?a.setAttribute("value",""+b):a.validity&&!a.validity.badInput&&a.ownerDocument.activeElement!==a&&a.setAttribute("value",""+b)}}},Kc=aa.HAS_STRING_BOOLEAN_VALUE,Lc={Properties:{autoReverse:Kc,externalResourcesRequired:Kc,preserveAlpha:Kc},DOMAttributeNames:{autoReverse:"autoReverse", +externalResourcesRequired:"externalResourcesRequired",preserveAlpha:"preserveAlpha"},DOMAttributeNamespaces:{xlinkActuate:"http://www.w3.org/1999/xlink",xlinkArcrole:"http://www.w3.org/1999/xlink",xlinkHref:"http://www.w3.org/1999/xlink",xlinkRole:"http://www.w3.org/1999/xlink",xlinkShow:"http://www.w3.org/1999/xlink",xlinkTitle:"http://www.w3.org/1999/xlink",xlinkType:"http://www.w3.org/1999/xlink",xmlBase:"http://www.w3.org/XML/1998/namespace",xmlLang:"http://www.w3.org/XML/1998/namespace",xmlSpace:"http://www.w3.org/XML/1998/namespace"}}, +If=/[\-\:]([a-z])/g,Jf=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode x-height xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xmlns:xlink xml:lang xml:space".split(" ").forEach(function(a){var b= +a.replace(If,Jf);Lc.Properties[b]=0;Lc.DOMAttributeNames[b]=a});aa.injectDOMPropertyConfig(Hf);aa.injectDOMPropertyConfig(Lc);var y={_caughtError:null,_hasCaughtError:!1,_rethrowError:null,_hasRethrowError:!1,injection:{injectErrorUtils:function(a){"function"!==typeof a.invokeGuardedCallback?l("197"):void 0;Le=a.invokeGuardedCallback}},invokeGuardedCallback:function(a,b,c,d,e,f,g,h,k){Le.apply(y,arguments)},invokeGuardedCallbackAndCatchFirstError:function(a,b,c,d,e,f,g,h,k){y.invokeGuardedCallback.apply(this, +arguments);if(y.hasCaughtError()){var l=y.clearCaughtError();y._hasRethrowError||(y._hasRethrowError=!0,y._rethrowError=l)}},rethrowCaughtError:function(){return Kf.apply(y,arguments)},hasCaughtError:function(){return y._hasCaughtError},clearCaughtError:function(){if(y._hasCaughtError){var a=y._caughtError;y._caughtError=null;y._hasCaughtError=!1;return a}l("198")}},Le=function(a,b,c,d,e,f,g,h,k){y._hasCaughtError=!1;y._caughtError=null;var l=Array.prototype.slice.call(arguments,3);try{b.apply(c, +l)}catch(D){y._caughtError=D,y._hasCaughtError=!0}},Kf=function(){if(y._hasRethrowError){var a=y._rethrowError;y._rethrowError=null;y._hasRethrowError=!1;throw a;}},jb=null,ba={},oa=[],Vb={},ca={},kb={},Lf=Object.freeze({plugins:oa,eventNameDispatchConfigs:Vb,registrationNameModules:ca,registrationNameDependencies:kb,possibleRegistrationNames:null,injectEventPluginOrder:ad,injectEventPluginsByName:bd}),ta=function(){};ta.thatReturns=lb;ta.thatReturnsFalse=lb(!1);ta.thatReturnsTrue=lb(!0);ta.thatReturnsNull= +lb(null);ta.thatReturnsThis=function(){return this};ta.thatReturnsArgument=function(a){return a};var G=ta,Xb=null,vd=null,dd=null,pa=null,Me=function(a,b){if(a){var c=a._dispatchListeners,d=a._dispatchInstances;if(Array.isArray(c))for(var e=0;e=ab),sd=String.fromCharCode(32), +V={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["topCompositionEnd","topKeyPress","topTextInput","topPaste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"topBlur topCompositionEnd topKeyDown topKeyPress topKeyUp topMouseDown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"topBlur topCompositionStart topKeyDown topKeyPress topKeyUp topMouseDown".split(" ")}, +compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"topBlur topCompositionUpdate topKeyDown topKeyPress topKeyUp topMouseDown".split(" ")}},rd=!1,za=!1,Qf={eventTypes:V,extractEvents:function(a,b,c,d){var e;if(bc)b:{switch(a){case "topCompositionStart":var f=V.compositionStart;break b;case "topCompositionEnd":f=V.compositionEnd;break b;case "topCompositionUpdate":f=V.compositionUpdate;break b}f=void 0}else za?pd(a,c)&&(f=V.compositionEnd): +"topKeyDown"===a&&229===c.keyCode&&(f=V.compositionStart);f?(td&&(za||f!==V.compositionStart?f===V.compositionEnd&&za&&(e=kd()):(H._root=d,H._startText=ld(),za=!0)),f=nd.getPooled(f,b,c,d),e?f.data=e:(e=qd(c),null!==e&&(f.data=e)),ya(f),e=f):e=null;(a=Pf?jf(a,c):kf(a,c))?(b=od.getPooled(V.beforeInput,b,c,d),b.data=a,ya(b)):b=null;return[e,b]}},mb=null,Ga=null,fa=null,Qe={injectFiberControlledHostComponent:function(a){mb=a}},Rf=Object.freeze({injection:Qe,enqueueStateRestore:wd,restoreStateIfNeeded:xd}), +ec=function(a,b){return a(b)},dc=!1,lf={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},zd;P.canUseDOM&&(zd=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("",""));var Dd={change:{phasedRegistrationNames:{bubbled:"onChange",captured:"onChangeCapture"},dependencies:"topBlur topChange topClick topFocus topInput topKeyDown topKeyUp topSelectionChange".split(" ")}}, +Ha=null,Oa=null,Nc=!1;P.canUseDOM&&(Nc=gc("input")&&(!document.documentMode||9=document.documentMode,Sd={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},dependencies:"topBlur topContextMenu topFocus topKeyDown topKeyUp topMouseDown topMouseUp topSelectionChange".split(" ")}}, +X=null,pc=null,Sa=null,mc=!1,Xf={eventTypes:Sd,extractEvents:function(a,b,c,d){var e=d.window===d?d.document:9===d.nodeType?d:d.ownerDocument,f;if(!(f=!e)){a:{e=Kd(e);f=kb.onSelect;for(var g=0;gc)return D(a,b);switch(b.tag){case 0:null!== +a?l("155"):void 0;var d=b.type,e=b.pendingProps,q=Ta(b);q=Va(b,q);d=d(e,q);b.effectTag|=1;"object"===typeof d&&null!==d&&"function"===typeof d.render?(b.tag=2,e=xb(b),x(b,d),z(b,c),b=h(a,b,!0,e)):(b.tag=1,f(a,b,d),b.memoizedProps=e,b=b.child);return b;case 1:a:{e=b.type;c=b.pendingProps;d=b.memoizedProps;if(J.current)null===c&&(c=d);else if(null===c||d===c){b=m(a,b);break a}d=Ta(b);d=Va(b,d);e=e(c,d);b.effectTag|=1;f(a,b,e);b.memoizedProps=c;b=b.child}return b;case 2:return e=xb(b),d=void 0,null=== +a?b.stateNode?l("153"):(t(b,b.pendingProps),z(b,c),d=!0):d=yc(a,b,c),h(a,b,d,e);case 3:return k(b),e=b.updateQueue,null!==e?(d=b.memoizedState,e=xc(a,b,e,null,null,c),d===e?(w(),b=m(a,b)):(d=e.element,q=b.stateNode,(null===a||null===a.child)&&q.hydrate&&r(b)?(b.effectTag|=2,b.child=Mb(b,null,d,c)):(w(),f(a,b,d)),b.memoizedState=e,b=b.child)):(w(),b=m(a,b)),b;case 5:L(b);null===a&&y(b);e=b.type;var p=b.memoizedProps;d=b.pendingProps;null===d&&(d=p,null===d?l("154"):void 0);q=null!==a?a.memoizedProps: +null;J.current||null!==d&&p!==d?(p=d.children,A(e,d)?p=null:q&&A(e,q)&&(b.effectTag|=16),g(a,b),2147483647!==c&&!v&&n(e,d)?(b.expirationTime=2147483647,b=null):(f(a,b,p),b.memoizedProps=d,b=b.child)):b=m(a,b);return b;case 6:return null===a&&y(b),a=b.pendingProps,null===a&&(a=b.memoizedProps),b.memoizedProps=a,null;case 8:b.tag=7;case 7:e=b.pendingProps;if(J.current)null===e&&(e=a&&a.memoizedProps,null===e?l("154"):void 0);else if(null===e||b.memoizedProps===e)e=b.memoizedProps;d=e.children;b.stateNode= +null===a?Mb(b,b.stateNode,d,c):db(b,b.stateNode,d,c);b.memoizedProps=e;return b.stateNode;case 9:return null;case 4:a:{R(b,b.stateNode.containerInfo);e=b.pendingProps;if(J.current)null===e&&(e=a&&a.memoizedProps,null==e?l("154"):void 0);else if(null===e||b.memoizedProps===e){b=m(a,b);break a}null===a?b.child=db(b,null,e,c):f(a,b,e);b.memoizedProps=e;b=b.child}return b;case 10:a:{c=b.pendingProps;if(J.current)null===c&&(c=b.memoizedProps);else if(null===c||b.memoizedProps===c){b=m(a,b);break a}f(a, +b,c);b.memoizedProps=c;b=b.child}return b;default:l("156")}},beginFailedWork:function(a,b,c){switch(b.tag){case 2:xb(b);break;case 3:k(b);break;default:l("157")}b.effectTag|=64;null===a?b.child=null:b.child!==a.child&&(b.child=a.child);if(0===b.expirationTime||b.expirationTime>c)return D(a,b);b.firstEffect=null;b.lastEffect=null;b.child=null===a?Mb(b,null,null,c):db(b,a.child,null,c);2===b.tag&&(a=b.stateNode,b.memoizedProps=a.props,b.memoizedState=a.state);return b.child}}},cg=function(a,b,c){function d(a){a.effectTag|= +4}var e=a.createInstance,f=a.createTextInstance,g=a.appendInitialChild,h=a.finalizeInitialChildren,k=a.prepareUpdate,m=a.persistence,D=b.getRootHostContainer,A=b.popHostContext,v=b.getHostContext,n=b.popHostContainer,L=c.prepareToHydrateHostInstance,R=c.prepareToHydrateHostTextInstance,r=c.popHydrationState,w=void 0,y=void 0,x=void 0;a.mutation?(w=function(a){},y=function(a,b,c,e,f,g,h){(b.updateQueue=c)&&d(b)},x=function(a,b,c,e){c!==e&&d(b)}):m?l("235"):l("236");return{completeWork:function(a,b, +c){var t=b.pendingProps;if(null===t)t=b.memoizedProps;else if(2147483647!==b.expirationTime||2147483647===c)b.pendingProps=null;switch(b.tag){case 1:return null;case 2:return ae(b),null;case 3:n(b);I(J,b);I(ia,b);t=b.stateNode;t.pendingContext&&(t.context=t.pendingContext,t.pendingContext=null);if(null===a||null===a.child)r(b),b.effectTag&=-3;w(b);return null;case 5:A(b);c=D();var z=b.type;if(null!==a&&null!=b.stateNode){var m=a.memoizedProps,K=b.stateNode,yc=v();K=k(K,z,m,t,c,yc);y(a,b,K,z,m,t,c); +a.ref!==b.ref&&(b.effectTag|=128)}else{if(!t)return null===b.stateNode?l("166"):void 0,null;a=v();if(r(b))L(b,c,a)&&d(b);else{a=e(z,t,c,a,b);a:for(m=b.child;null!==m;){if(5===m.tag||6===m.tag)g(a,m.stateNode);else if(4!==m.tag&&null!==m.child){m.child["return"]=m;m=m.child;continue}if(m===b)break;for(;null===m.sibling;){if(null===m["return"]||m["return"]===b)break a;m=m["return"]}m.sibling["return"]=m["return"];m=m.sibling}h(a,z,t,c)&&d(b);b.stateNode=a}null!==b.ref&&(b.effectTag|=128)}return null; +case 6:if(a&&null!=b.stateNode)x(a,b,a.memoizedProps,t);else{if("string"!==typeof t)return null===b.stateNode?l("166"):void 0,null;a=D();c=v();r(b)?R(b)&&d(b):b.stateNode=f(t,a,c,b)}return null;case 7:(t=b.memoizedProps)?void 0:l("165");b.tag=8;z=[];a:for((m=b.stateNode)&&(m["return"]=b);null!==m;){if(5===m.tag||6===m.tag||4===m.tag)l("247");else if(9===m.tag)z.push(m.type);else if(null!==m.child){m.child["return"]=m;m=m.child;continue}for(;null===m.sibling;){if(null===m["return"]||m["return"]=== +b)break a;m=m["return"]}m.sibling["return"]=m["return"];m=m.sibling}m=t.handler;t=m(t.props,z);b.child=db(b,null!==a?a.child:null,t,c);return b.child;case 8:return b.tag=7,null;case 9:return null;case 10:return null;case 4:return n(b),w(b),null;case 0:l("167");default:l("156")}}}},dg=function(a,b){function c(a){var c=a.ref;if(null!==c)try{c(null)}catch(z){b(a,z)}}function d(a){"function"===typeof ge&&ge(a);switch(a.tag){case 2:c(a);var d=a.stateNode;if("function"===typeof d.componentWillUnmount)try{d.props= +a.memoizedProps,d.state=a.memoizedState,d.componentWillUnmount()}catch(z){b(a,z)}break;case 5:c(a);break;case 7:e(a.stateNode);break;case 4:k&&g(a)}}function e(a){for(var b=a;;)if(d(b),null===b.child||k&&4===b.tag){if(b===a)break;for(;null===b.sibling;){if(null===b["return"]||b["return"]===a)return;b=b["return"]}b.sibling["return"]=b["return"];b=b.sibling}else b.child["return"]=b,b=b.child}function f(a){return 5===a.tag||3===a.tag||4===a.tag}function g(a){for(var b=a,c=!1,f=void 0,g=void 0;;){if(!c){c= +b["return"];a:for(;;){null===c?l("160"):void 0;switch(c.tag){case 5:f=c.stateNode;g=!1;break a;case 3:f=c.stateNode.containerInfo;g=!0;break a;case 4:f=c.stateNode.containerInfo;g=!0;break a}c=c["return"]}c=!0}if(5===b.tag||6===b.tag)e(b),g?y(f,b.stateNode):w(f,b.stateNode);else if(4===b.tag?f=b.stateNode.containerInfo:d(b),null!==b.child){b.child["return"]=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b["return"]||b["return"]===a)return;b=b["return"];4===b.tag&&(c=!1)}b.sibling["return"]= +b["return"];b=b.sibling}}var h=a.getPublicInstance,k=a.mutation;a=a.persistence;k||(a?l("235"):l("236"));var m=k.commitMount,D=k.commitUpdate,A=k.resetTextContent,v=k.commitTextUpdate,n=k.appendChild,L=k.appendChildToContainer,R=k.insertBefore,r=k.insertInContainerBefore,w=k.removeChild,y=k.removeChildFromContainer;return{commitResetTextContent:function(a){A(a.stateNode)},commitPlacement:function(a){a:{for(var b=a["return"];null!==b;){if(f(b)){var c=b;break a}b=b["return"]}l("160");c=void 0}var d= +b=void 0;switch(c.tag){case 5:b=c.stateNode;d=!1;break;case 3:b=c.stateNode.containerInfo;d=!0;break;case 4:b=c.stateNode.containerInfo;d=!0;break;default:l("161")}c.effectTag&16&&(A(b),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c["return"]||f(c["return"])){c=null;break a}c=c["return"]}c.sibling["return"]=c["return"];for(c=c.sibling;5!==c.tag&&6!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;else c.child["return"]=c,c=c.child}if(!(c.effectTag& +2)){c=c.stateNode;break a}}for(var e=a;;){if(5===e.tag||6===e.tag)c?d?r(b,e.stateNode,c):R(b,e.stateNode,c):d?L(b,e.stateNode):n(b,e.stateNode);else if(4!==e.tag&&null!==e.child){e.child["return"]=e;e=e.child;continue}if(e===a)break;for(;null===e.sibling;){if(null===e["return"]||e["return"]===a)return;e=e["return"]}e.sibling["return"]=e["return"];e=e.sibling}},commitDeletion:function(a){g(a);a["return"]=null;a.child=null;a.alternate&&(a.alternate.child=null,a.alternate["return"]=null)},commitWork:function(a, +b){switch(b.tag){case 2:break;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps;a=null!==a?a.memoizedProps:d;var e=b.type,f=b.updateQueue;b.updateQueue=null;null!==f&&D(c,f,e,a,d,b)}break;case 6:null===b.stateNode?l("162"):void 0;c=b.memoizedProps;v(b.stateNode,null!==a?a.memoizedProps:c,c);break;case 3:break;default:l("163")}},commitLifeCycles:function(a,b){switch(b.tag){case 2:var c=b.stateNode;if(b.effectTag&4)if(null===a)c.props=b.memoizedProps,c.state=b.memoizedState,c.componentDidMount(); +else{var d=a.memoizedProps;a=a.memoizedState;c.props=b.memoizedProps;c.state=b.memoizedState;c.componentDidUpdate(d,a)}b=b.updateQueue;null!==b&&je(b,c);break;case 3:c=b.updateQueue;null!==c&&je(c,null!==b.child?b.child.stateNode:null);break;case 5:c=b.stateNode;null===a&&b.effectTag&4&&m(c,b.type,b.memoizedProps,b);break;case 6:break;case 4:break;default:l("163")}},commitAttachRef:function(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:b(h(c));break;default:b(c)}}},commitDetachRef:function(a){a= +a.ref;null!==a&&a(null)}}},la={},eg=function(a){function b(a){a===la?l("174"):void 0;return a}var c=a.getChildHostContext,d=a.getRootHostContext,e={current:la},f={current:la},g={current:la};return{getHostContext:function(){return b(e.current)},getRootHostContainer:function(){return b(g.current)},popHostContainer:function(a){I(e,a);I(f,a);I(g,a)},popHostContext:function(a){f.current===a&&(I(e,a),I(f,a))},pushHostContainer:function(a,b){M(g,b,a);b=d(b);M(f,a,a);M(e,b,a)},pushHostContext:function(a){var d= +b(g.current),h=b(e.current);d=c(h,a.type,d);h!==d&&(M(f,a,a),M(e,d,a))},resetHostContainer:function(){e.current=la;g.current=la}}},fg=function(a){function b(a,b){var c=new Q(5,null,0);c.type="DELETED";c.stateNode=b;c["return"]=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function c(a,b){switch(a.tag){case 5:return b=f(b,a.type,a.pendingProps),null!==b?(a.stateNode=b,!0):!1;case 6:return b=g(b,a.pendingProps),null!==b?(a.stateNode=b,!0): +!1;default:return!1}}function d(a){for(a=a["return"];null!==a&&5!==a.tag&&3!==a.tag;)a=a["return"];A=a}var e=a.shouldSetTextContent;a=a.hydration;if(!a)return{enterHydrationState:function(){return!1},resetHydrationState:function(){},tryToClaimNextHydratableInstance:function(){},prepareToHydrateHostInstance:function(){l("175")},prepareToHydrateHostTextInstance:function(){l("176")},popHydrationState:function(a){return!1}};var f=a.canHydrateInstance,g=a.canHydrateTextInstance,h=a.getNextHydratableSibling, +k=a.getFirstHydratableChild,m=a.hydrateInstance,D=a.hydrateTextInstance,A=null,v=null,n=!1;return{enterHydrationState:function(a){v=k(a.stateNode.containerInfo);A=a;return n=!0},resetHydrationState:function(){v=A=null;n=!1},tryToClaimNextHydratableInstance:function(a){if(n){var d=v;if(d){if(!c(a,d)){d=h(d);if(!d||!c(a,d)){a.effectTag|=2;n=!1;A=a;return}b(A,v)}A=a;v=k(d)}else a.effectTag|=2,n=!1,A=a}},prepareToHydrateHostInstance:function(a,b,c){b=m(a.stateNode,a.type,a.memoizedProps,b,c,a);a.updateQueue= +b;return null!==b?!0:!1},prepareToHydrateHostTextInstance:function(a){return D(a.stateNode,a.memoizedProps,a)},popHydrationState:function(a){if(a!==A)return!1;if(!n)return d(a),n=!0,!1;var c=a.type;if(5!==a.tag||"head"!==c&&"body"!==c&&!e(c,a.memoizedProps))for(c=v;c;)b(a,c),c=h(c);d(a);v=A?h(a.stateNode):null;return!0}}},gg=function(a){function b(a){X=Ba=!0;var b=a.stateNode;b.current===a?l("177"):void 0;b.isReadyForCommit=!1;bb.current=null;if(1g.expirationTime)&&(f=g.expirationTime),g=g.sibling;e.expirationTime=f}if(null!== +b)return b;null!==c&&(null===c.firstEffect&&(c.firstEffect=a.firstEffect),null!==a.lastEffect&&(null!==c.lastEffect&&(c.lastEffect.nextEffect=a.firstEffect),c.lastEffect=a.lastEffect),1a))if(F<=ha)for(;null!==B;)B=k(B)?e(B):d(B);else for(;null!==B&&!z();)B=k(B)?e(B):d(B)}else if(!(0===F||F>a))if(F<=ha)for(;null!==B;)B=d(B);else for(;null!==B&&!z();)B=d(B)}function g(a,b){Ba?l("243"):void 0;Ba=!0;a.isReadyForCommit=!1;if(a!==Ja||b!==F||null===B){for(;-1b)a.expirationTime=b;null!==a.alternate&&(0===a.alternate.expirationTime||a.alternate.expirationTime>b)&&(a.alternate.expirationTime=b);if(null=== +a["return"])if(3===a.tag){c=a.stateNode;!Ba&&c===Ja&&bGa&&l("185");if(null===d.nextScheduledRoot)d.remainingExpirationTime=e,null===N?(Ka=N=d,d.nextScheduledRoot=d):(N=N.nextScheduledRoot=d,N.nextScheduledRoot=Ka);else{var f=d.remainingExpirationTime;if(0===f||eZ)return;xa(la)}var b=ba()-qa;Z=a;la=wa(H,{timeout:10*(a-2)-b})}function E(){var a=0,b=null;if(null!==N)for(var c=N,d=Ka;null!==d;){var e=d.remainingExpirationTime;if(0===e){null===c||null===N?l("244"):void 0;if(d===d.nextScheduledRoot){Ka=N=d.nextScheduledRoot=null;break}else if(d===Ka)Ka=e=d.nextScheduledRoot,N.nextScheduledRoot=e,d.nextScheduledRoot=null;else if(d===N){N=c;N.nextScheduledRoot=Ka;d.nextScheduledRoot=null;break}else c.nextScheduledRoot=d.nextScheduledRoot, +d.nextScheduledRoot=null;d=c.nextScheduledRoot}else{if(0===a||eHa?!1:oa=!0}function G(a){null===Ea?l("246"):void 0;Ea.remainingExpirationTime=0;da||(da=!0,pa=a)}var q=eg(a),p=fg(a),I=q.popHostContainer,O=q.popHostContext,P=q.resetHostContainer,M=bg(a,q,p,v,A),Q=M.beginWork,T=M.beginFailedWork, +Y=cg(a,q,p).completeWork;q=dg(a,h);var aa=q.commitResetTextContent,V=q.commitPlacement,na=q.commitDeletion,ca=q.commitWork,sa=q.commitLifeCycles,ta=q.commitAttachRef,va=q.commitDetachRef,ba=a.now,wa=a.scheduleDeferredCallback,xa=a.cancelDeferredCallback,ya=a.useSyncScheduling,za=a.prepareForCommit,Aa=a.resetAfterCommit,qa=ba(),ha=2,Ca=0,Ba=!1,B=null,Ja=null,F=0,u=null,S=null,Ia=null,ua=null,ma=null,U=!1,X=!1,ka=!1,Ka=null,N=null,Z=0,la=-1,Na=!1,Ea=null,Fa=0,oa=!1,da=!1,pa=null,W=null,Da=!1,ea=!1, +Ga=1E3,fa=0,Ha=1;return{computeAsyncExpiration:n,computeExpirationForFiber:A,scheduleWork:v,batchedUpdates:function(a,b){var c=Da;Da=!0;try{return a(b)}finally{(Da=c)||Na||x(1,null)}},unbatchedUpdates:function(a){if(Da&&!ea){ea=!0;try{return a()}finally{ea=!1}}return a()},flushSync:function(a){var b=Da;Da=!0;try{a:{var c=Ca;Ca=1;try{var d=a();break a}finally{Ca=c}d=void 0}return d}finally{Da=b,Na?l("187"):void 0,x(1,null)}},deferredUpdates:function(a){var b=Ca;Ca=n();try{return a()}finally{Ca=b}}}}, +Te=function(a){function b(a){a=wf(a);return null===a?null:a.stateNode}var c=a.getPublicInstance;a=gg(a);var d=a.computeAsyncExpiration,e=a.computeExpirationForFiber,f=a.scheduleWork;return{createContainer:function(a,b){var c=new Q(3,null,0);a={current:c,containerInfo:a,pendingChildren:null,remainingExpirationTime:0,isReadyForCommit:!1,finishedWork:null,context:null,pendingContext:null,hydrate:b,nextScheduledRoot:null};return c.stateNode=a},updateContainer:function(a,b,c,m){var g=b.current;if(c){c= +c._reactInternalFiber;var h;b:{2===Qa(c)&&2===c.tag?void 0:l("170");for(h=c;3!==h.tag;){if(Ua(h)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}(h=h["return"])?void 0:l("171")}h=h.stateNode.context}c=Ua(c)?ce(c,h):h}else c=ja;null===b.context?b.context=c:b.pendingContext=c;b=m;b=void 0===b?null:b;m=null!=a&&null!=a.type&&null!=a.type.prototype&&!0===a.type.prototype.unstable_isAsyncReactComponent?d():e(g);Bb(g,{expirationTime:m,partialState:{element:a},callback:b,isReplace:!1,isForced:!1, +nextCallback:null,next:null});f(g,m)},batchedUpdates:a.batchedUpdates,unbatchedUpdates:a.unbatchedUpdates,deferredUpdates:a.deferredUpdates,flushSync:a.flushSync,getPublicRootInstance:function(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return c(a.child.stateNode);default:return a.child.stateNode}},findHostInstance:b,findHostInstanceWithNoPortals:function(a){a=xf(a);return null===a?null:a.stateNode},injectIntoDevTools:function(a){var c=a.findFiberByHostInstance;return Af(C({}, +a,{findHostInstanceByFiber:function(a){return b(a)},findFiberByHostInstance:function(a){return c?c(a):null}}))}}},Ue=Object.freeze({default:Te}),Sc=Ue&&Te||Ue,hg=Sc["default"]?Sc["default"]:Sc,Ve="object"===typeof performance&&"function"===typeof performance.now,Nb=void 0;Nb=Ve?function(){return performance.now()}:function(){return Date.now()};var Ob=void 0,Pb=void 0;if(P.canUseDOM)if("function"!==typeof requestIdleCallback||"function"!==typeof cancelIdleCallback){var Qb=null,Rb=!1,eb=-1,fb=!1,gb= +0,Sb=33,hb=33;var Tc=Ve?{didTimeout:!1,timeRemaining:function(){var a=gb-performance.now();return 0=gb-a)if(-1!==eb&&eb<=a)Tc.didTimeout=!0;else{fb||(fb=!0,requestAnimationFrame(Xe));return}else Tc.didTimeout=!1;eb=-1;a=Qb;Qb=null;null!==a&&a(Tc)}},!1); +var Xe=function(a){fb=!1;var b=a-gb+hb;bb&&(b=8),hb=bd&&(e=d,d=a,a=e);e=Qd(c,a);var f=Qd(c,d);if(e&&f&&(1!==b.rangeCount||b.anchorNode!==e.node||b.anchorOffset!==e.offset||b.focusNode!==f.node|| +b.focusOffset!==f.offset)){var g=document.createRange();g.setStart(e.node,e.offset);b.removeAllRanges();a>d?(b.addRange(g),b.extend(f.node,f.offset)):(g.setEnd(f.node,f.offset),b.addRange(g))}}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});try{c.focus()}catch(h){}for(c=0;cu.length&&u.push(a)}function t(a,b,c,d){var f=typeof a;if("undefined"===f||"boolean"===f)a=null;var l=!1;if(null===a)l=!0;else switch(f){case "string":case "number":l=!0;break;case "object":switch(a.$$typeof){case r:case P:case Q:case R:l=!0}}if(l)return c(d,a,""===b?"."+D(a,0):b),1;l=0;b=""===b?".":b+":";if(Array.isArray(a))for(var e= +0;ea;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(d){return!1}}()?Object.assign: +function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var d,f=1;f Date: Sun, 11 Feb 2018 11:22:49 -0500 Subject: [PATCH 02/10] 0.12.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 031d8cc..61c967f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dash-renderer", - "version": "0.11.2", + "version": "0.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 74167a5..447f7be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dash-renderer", - "version": "0.11.3", + "version": "0.12.0", "description": "render dash components in react", "main": "src/index.js", "scripts": { From ac17b2f30b04424be91d2e369df915e0c421404b Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 11 Feb 2018 11:23:57 -0500 Subject: [PATCH 03/10] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d95d7e..99ca102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.12.0] - 2018-02-11 +### Added +- `dash_renderer.REACT_VERSION` allows user to now choose between '15.4.2' and '16.2.0' for the React version used by Dash. + ## [0.11.3] - 2018-02-01 ### Fixed - Fixed #41 in #42. In some cases, during initialization, callbacks may fired multiple times instead of just once. This only happens in certain scenarios where outputs have overlapping inputs and those inputs are leaves (they don't have any inputs of their own). See #41 for a simple example and #42 for some more extensive test cases. From d32aabe802f383ca7626599e8d6f1a226773737f Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 17 Feb 2018 09:23:20 -0500 Subject: [PATCH 04/10] Provide _set_react_version method to allow for dynamically updating React version from a dash application. Adds test for scripts versions served by Dash. Updates CHANGELOG to reflect new usage --- CHANGELOG.md | 10 +- dash_renderer/__init__.py | 45 +- tests/test_render.py | 3444 +++++++++++++++++++------------------ 3 files changed, 1798 insertions(+), 1701 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ca102..a6ab41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,15 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [0.12.0] - 2018-02-11 ### Added -- `dash_renderer.REACT_VERSION` allows user to now choose between '15.4.2' and '16.2.0' for the React version used by Dash. +- Allows user to now choose between '15.4.2' and '16.2.0' for React versions +```python +import dash_renderer + +# Set the react version before setting up the Dash application +dash_renderer._set_react_version('16.2.0') + +app = dash.Dash(...) +``` ## [0.11.3] - 2018-02-01 ### Fixed diff --git a/dash_renderer/__init__.py b/dash_renderer/__init__.py index 7cca24c..7bcbc19 100644 --- a/dash_renderer/__init__.py +++ b/dash_renderer/__init__.py @@ -1,3 +1,5 @@ +import sys + # For reasons that I don't fully understand, # unless I include __file__ in here, the packaged version # of this module will just be a .egg file, not a .egg folder. @@ -10,9 +12,8 @@ from .version import __version__ __file__ -REACT_VERSION = '15.4.2' +_DEFAULT_REACT_VERSION = '15.4.2' _REACT_VERSION_TYPES = {'15.4.2', '16.2.0'} -assert REACT_VERSION in _REACT_VERSION_TYPES _REACT_VERSION_TO_URLS = { '15.4.2': { 'external_url': [ @@ -36,15 +37,39 @@ } } -# Dash renderer's dependencies get loaded in a special order by the server: -# React bundles first, the renderer bundle at the very end. -_js_dist_dependencies = [ - { - 'external_url': _REACT_VERSION_TO_URLS[REACT_VERSION]['external_url'], - 'relative_package_path': _REACT_VERSION_TO_URLS[REACT_VERSION]['relative_package_path'], + +def _set_react_version(react_version): + """ + Update the version of React in _js_dist_dependencies served by dash-renderer to the client + + Example: + ``` + import dash_renderer + + # Set the react version before setting up the Dash application + dash_renderer._set_react_version('16.2.0') + + app = dash.Dash(...) + ``` + + :param str react_version: Version of React + + """ + assert react_version in _REACT_VERSION_TYPES + + _this_module = sys.modules[__name__] + + # Dash renderer's dependencies get loaded in a special order by the server: + # React bundles first, the renderer bundle at the very end. + setattr(_this_module, '_js_dist_dependencies', [{ + 'external_url': _REACT_VERSION_TO_URLS[react_version]['external_url'], + 'relative_package_path': _REACT_VERSION_TO_URLS[react_version]['relative_package_path'], 'namespace': 'dash_renderer' - } -] + }]) + + +_js_dist_dependencies = [] +_set_react_version(_DEFAULT_REACT_VERSION) _js_dist = [ { diff --git a/tests/test_render.py b/tests/test_render.py index 7f3afff..3a044d8 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -1,6 +1,7 @@ from dash import Dash from dash.dependencies import Input, Output, State, Event import dash +from dash.development.base_component import Component import dash_html_components as html import dash_core_components as dcc from .IntegrationTests import IntegrationTests @@ -66,1709 +67,1772 @@ def request_queue_assertions( if expected_length is not None: self.assertEqual(len(request_queue), expected_length) - def test_initial_state(self): - app = Dash(__name__) - app.layout = html.Div([ - 'Basic string', - 3.14, - None, - html.Div('Child div with basic string', - id='p.c.3', - className="my-class", - title='tooltip', - style={'color': 'red', 'fontSize': 30} - ), - html.Div(id='p.c.4'), - html.Div([ - html.Div('Grandchild div', id='p.c.5.p.c.0'), - html.Div([ - html.Div('Great grandchild', id='p.c.5.p.c.1.p.c.0'), - 3.14159, - 'another basic string' - ], id='p.c.5.p.c.1'), - html.Div([ - html.Div( - html.Div([ - html.Div([ - html.Div( - id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0' - ), - '', - html.Div( - id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2' - ) - ], id='p.c.5.p.c.2.p.c.0.p.c.p.c.0') - ], id='p.c.5.p.c.2.p.c.0.p.c'), - id='p.c.5.p.c.2.p.c.0' - ) - ], id='p.c.5.p.c.2') - ], id='p.c.5') - ]) + # def test_initial_state(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # 'Basic string', + # 3.14, + # None, + # html.Div('Child div with basic string', + # id='p.c.3', + # className="my-class", + # title='tooltip', + # style={'color': 'red', 'fontSize': 30} + # ), + # html.Div(id='p.c.4'), + # html.Div([ + # html.Div('Grandchild div', id='p.c.5.p.c.0'), + # html.Div([ + # html.Div('Great grandchild', id='p.c.5.p.c.1.p.c.0'), + # 3.14159, + # 'another basic string' + # ], id='p.c.5.p.c.1'), + # html.Div([ + # html.Div( + # html.Div([ + # html.Div([ + # html.Div( + # id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0' + # ), + # '', + # html.Div( + # id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2' + # ) + # ], id='p.c.5.p.c.2.p.c.0.p.c.p.c.0') + # ], id='p.c.5.p.c.2.p.c.0.p.c'), + # id='p.c.5.p.c.2.p.c.0' + # ) + # ], id='p.c.5.p.c.2') + # ], id='p.c.5') + # ]) + # + # self.startServer(app) + # + # el = self.wait_for_element_by_css_selector('#_dash-app-content') + # + # # TODO - Make less fragile with http://lxml.de/lxmlhtml.html#html-diff + # rendered_dom = ''' + #
+ # Basic string + # + # 3.14 + # + #
+ # Child div with basic string + #
+ # + #
+ #
+ # + #
+ #
+ # Grandchild div + #
+ # + #
+ #
+ # Great grandchild + #
+ # + # 3.14159 + # + # another basic string + #
+ # + #
+ #
+ #
+ #
+ # + #
+ #
+ # + # + #
+ #
+ # + #
+ #
+ #
+ #
+ #
+ # + #
+ # ''' + # # React wraps text and numbers with e.g. + # # Remove those + # comment_regex = '' + # + # # Somehow the html attributes are unordered. + # # Try different combinations (they're all valid html) + # style_permutations = [ + # 'style="color: red; font-size: 30px;"', + # 'style="font-size: 30px; color: red;"' + # ] + # permutations = itertools.permutations([ + # 'id="p.c.3"', + # 'class="my-class"', + # 'title="tooltip"', + # ], 3) + # passed = False + # for permutation in permutations: + # for style in style_permutations: + # actual_cleaned = re.sub(comment_regex, '', el.get_attribute('innerHTML')) + # expected_cleaned = re.sub( + # comment_regex, + # '', + # rendered_dom.replace('\n', '') + # .replace(' ', '') + # .replace('PERMUTE', ' '.join(list(permutation) + [style])) + # ) + # passed = passed or (actual_cleaned == expected_cleaned) + # if not passed: + # raise Exception( + # 'HTML does not match\nActual:\n{}\n\nExpected:\n{}'.format( + # actual_cleaned, + # expected_cleaned + # ) + # ) + # + # # Check that no errors or warnings were displayed + # self.assertEqual( + # self.driver.execute_script( + # 'return window.tests.console.error.length' + # ), + # 0 + # ) + # self.assertEqual( + # self.driver.execute_script( + # 'return window.tests.console.warn.length' + # ), + # 0 + # ) + # + # # Check the initial stores + # + # # layout should just be the JSON-ified app.layout + # self.assertEqual( + # self.driver.execute_script( + # 'return JSON.parse(JSON.stringify(' + # 'window.store.getState().layout' + # '))' + # ), + # { + # "namespace": "dash_html_components", + # "props": { + # "children": [ + # "Basic string", + # 3.14, + # None, + # { + # "namespace": "dash_html_components", + # "props": { + # "children": "Child div with basic string", + # "id": "p.c.3", + # 'className': "my-class", + # 'title': 'tooltip', + # 'style': { + # 'color': 'red', 'fontSize': 30 + # } + # }, + # "type": "Div" + # }, + # { + # "namespace": "dash_html_components", + # "props": { + # "children": None, + # "id": "p.c.4" + # }, + # "type": "Div" + # }, + # { + # "namespace": "dash_html_components", + # "props": { + # "children": [ + # { + # "namespace": "dash_html_components", + # "props": { + # "children": "Grandchild div", + # "id": "p.c.5.p.c.0" + # }, + # "type": "Div" + # }, + # { + # "namespace": "dash_html_components", + # "props": { + # "children": [ + # { + # "namespace": "dash_html_components", + # "props": { + # "children": "Great grandchild", + # "id": "p.c.5.p.c.1.p.c.0" + # }, + # "type": "Div" + # }, + # 3.14159, + # "another basic string" + # ], + # "id": "p.c.5.p.c.1" + # }, + # "type": "Div" + # }, + # { + # "namespace": "dash_html_components", + # "props": { + # "children": [ + # { + # "namespace": "dash_html_components", + # "props": { + # "children": { + # "namespace": "dash_html_components", + # "props": { + # "children": [ + # { + # "namespace": "dash_html_components", + # "props": { + # "children": [ + # { + # "namespace": "dash_html_components", + # "props": { + # "children": None, + # "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0" + # }, + # "type": "Div" + # }, + # "", + # { + # "namespace": "dash_html_components", + # "props": { + # "children": None, + # "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2" + # }, + # "type": "Div" + # } + # ], + # "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0" + # }, + # "type": "Div" + # } + # ], + # "id": "p.c.5.p.c.2.p.c.0.p.c" + # }, + # "type": "Div" + # }, + # "id": "p.c.5.p.c.2.p.c.0" + # }, + # "type": "Div" + # } + # ], + # "id": "p.c.5.p.c.2" + # }, + # "type": "Div" + # } + # ], + # "id": "p.c.5" + # }, + # "type": "Div" + # } + # ] + # }, + # "type": "Div" + # } + # ) + # + # # graphs should just be empty since there are no dependencies + # self.assertEqual( + # self.driver.execute_script( + # 'return JSON.parse(JSON.stringify(' + # 'window.store.getState().graphs' + # '))' + # ), + # { + # "InputGraph": { + # "nodes": {}, + # "outgoingEdges": {}, + # "incomingEdges": {} + # }, + # "EventGraph": { + # "nodes": {}, + # "outgoingEdges": {}, + # "incomingEdges": {} + # } + # } + # ) + # + # # paths is just a lookup table of the components's IDs and their + # # placement in the tree. + # # in this case the IDs are just abbreviations of the path to make + # # things easy to verify. + # self.assertEqual( + # self.driver.execute_script( + # 'return window.store.getState().paths' + # ), + # { + # "p.c.3": [ + # "props", "children", 3 + # ], + # "p.c.4": [ + # "props", "children", 4 + # ], + # "p.c.5": [ + # "props", "children", 5 + # ], + # "p.c.5.p.c.0": [ + # "props", "children", 5, + # "props", "children", 0 + # ], + # "p.c.5.p.c.1": [ + # "props", "children", 5, + # "props", "children", 1 + # ], + # "p.c.5.p.c.1.p.c.0": [ + # "props", "children", 5, + # "props", "children", 1, + # "props", "children", 0 + # ], + # "p.c.5.p.c.2": [ + # "props", "children", 5, + # "props", "children", 2 + # ], + # "p.c.5.p.c.2.p.c.0": [ + # "props", "children", 5, + # "props", "children", 2, + # "props", "children", 0 + # ], + # "p.c.5.p.c.2.p.c.0.p.c": [ + # "props", "children", 5, + # "props", "children", 2, + # "props", "children", 0, + # "props", "children" + # ], + # "p.c.5.p.c.2.p.c.0.p.c.p.c.0": [ + # "props", "children", 5, + # "props", "children", 2, + # "props", "children", 0, + # "props", "children", + # "props", "children", 0 + # ], + # "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0": [ + # "props", "children", 5, + # "props", "children", 2, + # "props", "children", 0, + # "props", "children", + # "props", "children", 0, + # "props", "children", 0 + # ], + # "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2": [ + # "props", "children", 5, + # "props", "children", 2, + # "props", "children", 0, + # "props", "children", + # "props", "children", 0, + # "props", "children", 2 + # ] + # } + # ) + # + # self.request_queue_assertions(0) + # + # self.percy_snapshot(name='layout') + # + # assert_clean_console(self) + # + # def test_simple_callback(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input( + # id='input', + # value='initial value' + # ), + # html.Div( + # html.Div([ + # 1.5, + # None, + # 'string', + # html.Div(id='output-1') + # ]) + # ) + # ]) + # + # call_count = Value('i', 0) + # + # @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + # def update_output(value): + # call_count.value = call_count.value + 1 + # return value + # + # self.startServer(app) + # + # self.wait_for_text_to_equal('#output-1', 'initial value') + # self.percy_snapshot(name='simple-callback-1') + # + # input1 = self.wait_for_element_by_css_selector('#input') + # input1.clear() + # + # input1.send_keys('hello world') + # + # self.wait_for_text_to_equal('#output-1', 'hello world') + # self.percy_snapshot(name='simple-callback-2') + # + # self.assertEqual( + # call_count.value, + # # an initial call to retrieve the first value + # 1 + + # # one for each hello world character + # len('hello world') + # ) + # + # self.request_queue_assertions( + # expected_length=1, + # check_rejected=False) + # + # assert_clean_console(self) + # + # def test_callbacks_generating_children(self): + # """ Modify the DOM tree by adding new + # components in the callbacks + # """ + # + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input( + # id='input', + # value='initial value' + # ), + # html.Div(id='output') + # ]) + # + # @app.callback(Output('output', 'children'), [Input('input', 'value')]) + # def pad_output(input): + # return html.Div([ + # dcc.Input( + # id='sub-input-1', + # value='sub input initial value' + # ), + # html.Div(id='sub-output-1') + # ]) + # + # call_count = Value('i', 0) + # + # # these components don't exist in the initial render + # app.config.supress_callback_exceptions = True + # + # @app.callback( + # Output('sub-output-1', 'children'), + # [Input('sub-input-1', 'value')] + # ) + # def update_input(value): + # call_count.value = call_count.value + 1 + # return value + # + # self.startServer(app) + # + # output = self.driver.find_element_by_id('output') + # output_html = output.get_attribute('innerHTML') + # + # wait_for(lambda: call_count.value == 1) + # + # # Adding new children to the layout should + # # call the callbacks immediately to set + # # the correct initial state + # wait_for( + # lambda: ( + # self.driver.find_element_by_id('output') + # .get_attribute('innerHTML') in [''' + #
+ # {} + #
+ # sub input initial value + #
+ #
'''.replace('\n', '').replace(' ', '').format(input) + # for input in [ + # # html attributes are unordered, so include both versions + # '', + # '' + # ] + # ] + # ) + # ) + # self.percy_snapshot(name='callback-generating-function-1') + # + # # the paths should include these new output IDs + # self.assertEqual( + # self.driver.execute_script('return window.store.getState().paths'), + # { + # 'input': [ + # 'props', 'children', 0 + # ], + # 'output': ['props', 'children', 1], + # 'sub-input-1': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 0 + # ], + # 'sub-output-1': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 1 + # ] + # } + # ) + # + # # editing the input should modify the sub output + # sub_input = self.driver.find_element_by_id('sub-input-1') + # sub_input.send_keys('a') + # self.wait_for_text_to_equal( + # '#sub-output-1', + # 'sub input initial valuea') + # + # self.assertEqual(call_count.value, 2) + # + # self.request_queue_assertions(call_count.value + 1) + # self.percy_snapshot(name='callback-generating-function-2') + # + # assert_clean_console(self) + # + # def test_radio_buttons_callbacks_generating_children(self): + # self.maxDiff = 100 * 1000 + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.RadioItems( + # options=[ + # {'label': 'Chapter 1', 'value': 'chapter1'}, + # {'label': 'Chapter 2', 'value': 'chapter2'}, + # {'label': 'Chapter 3', 'value': 'chapter3'}, + # {'label': 'Chapter 4', 'value': 'chapter4'}, + # {'label': 'Chapter 5', 'value': 'chapter5'} + # ], + # value='chapter1', + # id='toc' + # ), + # html.Div(id='body') + # ]) + # for script in dcc._js_dist: + # app.scripts.append_script(script) + # + # chapters = { + # 'chapter1': html.Div([ + # html.H1('Chapter 1', id='chapter1-header'), + # dcc.Dropdown( + # options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'SF']], + # value='NYC', + # id='chapter1-controls' + # ), + # html.Label(id='chapter1-label'), + # dcc.Graph(id='chapter1-graph') + # ]), + # # Chapter 2 has the some of the same components in the same order + # # as Chapter 1. This means that they won't get remounted + # # unless they set their own keys are differently. + # # Switching back and forth between 1 and 2 implicitly + # # tests how components update when they aren't remounted. + # 'chapter2': html.Div([ + # html.H1('Chapter 2', id='chapter2-header'), + # dcc.RadioItems( + # options=[{'label': i, 'value': i} + # for i in ['USA', 'Canada']], + # value='USA', + # id='chapter2-controls' + # ), + # html.Label(id='chapter2-label'), + # dcc.Graph(id='chapter2-graph') + # ]), + # # Chapter 3 has a different layout and so the components + # # should get rewritten + # 'chapter3': [html.Div( + # html.Div([ + # html.H3('Chapter 3', id='chapter3-header'), + # html.Label(id='chapter3-label'), + # dcc.Graph(id='chapter3-graph'), + # dcc.RadioItems( + # options=[{'label': i, 'value': i} + # for i in ['Summer', 'Winter']], + # value='Winter', + # id='chapter3-controls' + # ) + # ]) + # )], + # + # # Chapter 4 doesn't have an object to recursively + # # traverse + # 'chapter4': 'Just a string', + # + # # Chapter 5 contains elements that are bound with events + # 'chapter5': [html.Div([ + # html.Button(id='chapter5-button'), + # html.Div(id='chapter5-output') + # ])] + # } + # + # call_counts = { + # 'body': Value('i', 0), + # 'chapter1-graph': Value('i', 0), + # 'chapter1-label': Value('i', 0), + # 'chapter2-graph': Value('i', 0), + # 'chapter2-label': Value('i', 0), + # 'chapter3-graph': Value('i', 0), + # 'chapter3-label': Value('i', 0), + # 'chapter5-output': Value('i', 0) + # } + # + # @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + # def display_chapter(toc_value): + # call_counts['body'].value += 1 + # return chapters[toc_value] + # + # app.config.supress_callback_exceptions = True + # + # def generate_graph_callback(counterId): + # def callback(value): + # call_counts[counterId].value += 1 + # return { + # 'data': [{ + # 'x': ['Call Counter'], + # 'y': [call_counts[counterId].value], + # 'type': 'bar' + # }], + # 'layout': {'title': value} + # } + # return callback + # + # def generate_label_callback(id): + # def update_label(value): + # call_counts[id].value += 1 + # return value + # return update_label + # + # for chapter in ['chapter1', 'chapter2', 'chapter3']: + # app.callback( + # Output('{}-graph'.format(chapter), 'figure'), + # [Input('{}-controls'.format(chapter), 'value')] + # )(generate_graph_callback('{}-graph'.format(chapter))) + # + # app.callback( + # Output('{}-label'.format(chapter), 'children'), + # [Input('{}-controls'.format(chapter), 'value')] + # )(generate_label_callback('{}-label'.format(chapter))) + # + # chapter5_output_children = 'Button clicked' + # + # @app.callback(Output('chapter5-output', 'children'), + # events=[Event('chapter5-button', 'click')]) + # def display_output(): + # call_counts['chapter5-output'].value += 1 + # return chapter5_output_children + # + # self.startServer(app) + # + # time.sleep(0.5) + # wait_for(lambda: call_counts['body'].value == 1) + # wait_for(lambda: call_counts['chapter1-graph'].value == 1) + # wait_for(lambda: call_counts['chapter1-label'].value == 1) + # self.assertEqual(call_counts['chapter2-graph'].value, 0) + # self.assertEqual(call_counts['chapter2-label'].value, 0) + # self.assertEqual(call_counts['chapter3-graph'].value, 0) + # self.assertEqual(call_counts['chapter3-label'].value, 0) + # + # def generic_chapter_assertions(chapter): + # # each element should exist in the dom + # paths = self.driver.execute_script( + # 'return window.store.getState().paths' + # ) + # for key in paths: + # self.driver.find_element_by_id(key) + # + # if chapter == 'chapter3': + # value = chapters[chapter][0][ + # '{}-controls'.format(chapter) + # ].value + # else: + # value = chapters[chapter]['{}-controls'.format(chapter)].value + # # check the actual values + # self.wait_for_text_to_equal('#{}-label'.format(chapter), value) + # wait_for( + # lambda: ( + # self.driver.execute_script( + # 'return document.' + # 'getElementById("{}-graph").'.format(chapter) + + # 'layout.title' + # ) == value + # ) + # ) + # self.request_queue_assertions() + # + # def chapter1_assertions(): + # paths = self.driver.execute_script( + # 'return window.store.getState().paths' + # ) + # self.assertEqual(paths, { + # 'toc': ['props', 'children', 0], + # 'body': ['props', 'children', 1], + # 'chapter1-header': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 0 + # ], + # 'chapter1-controls': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 1 + # ], + # 'chapter1-label': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 2 + # ], + # 'chapter1-graph': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 3 + # ] + # }) + # generic_chapter_assertions('chapter1') + # + # chapter1_assertions() + # self.percy_snapshot(name='chapter-1') + # + # # switch chapters + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[1]).click() + # + # # sleep just to make sure that no calls happen after our check + # time.sleep(2) + # self.percy_snapshot(name='chapter-2') + # wait_for(lambda: call_counts['body'].value == 2) + # wait_for(lambda: call_counts['chapter2-graph'].value == 1) + # wait_for(lambda: call_counts['chapter2-label'].value == 1) + # self.assertEqual(call_counts['chapter1-graph'].value, 1) + # self.assertEqual(call_counts['chapter1-label'].value, 1) + # + # def chapter2_assertions(): + # paths = self.driver.execute_script( + # 'return window.store.getState().paths' + # ) + # self.assertEqual(paths, { + # 'toc': ['props', 'children', 0], + # 'body': ['props', 'children', 1], + # 'chapter2-header': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 0 + # ], + # 'chapter2-controls': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 1 + # ], + # 'chapter2-label': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 2 + # ], + # 'chapter2-graph': [ + # 'props', 'children', 1, + # 'props', 'children', + # 'props', 'children', 3 + # ] + # }) + # generic_chapter_assertions('chapter2') + # + # chapter2_assertions() + # + # # switch to 3 + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[2]).click() + # # sleep just to make sure that no calls happen after our check + # time.sleep(2) + # self.percy_snapshot(name='chapter-3') + # wait_for(lambda: call_counts['body'].value == 3) + # wait_for(lambda: call_counts['chapter3-graph'].value == 1) + # wait_for(lambda: call_counts['chapter3-label'].value == 1) + # self.assertEqual(call_counts['chapter2-graph'].value, 1) + # self.assertEqual(call_counts['chapter2-label'].value, 1) + # self.assertEqual(call_counts['chapter1-graph'].value, 1) + # self.assertEqual(call_counts['chapter1-label'].value, 1) + # + # def chapter3_assertions(): + # paths = self.driver.execute_script( + # 'return window.store.getState().paths' + # ) + # self.assertEqual(paths, { + # 'toc': ['props', 'children', 0], + # 'body': ['props', 'children', 1], + # 'chapter3-header': [ + # 'props', 'children', 1, + # 'props', 'children', 0, + # 'props', 'children', + # 'props', 'children', 0 + # ], + # 'chapter3-label': [ + # 'props', 'children', 1, + # 'props', 'children', 0, + # 'props', 'children', + # 'props', 'children', 1 + # ], + # 'chapter3-graph': [ + # 'props', 'children', 1, + # 'props', 'children', 0, + # 'props', 'children', + # 'props', 'children', 2 + # ], + # 'chapter3-controls': [ + # 'props', 'children', 1, + # 'props', 'children', 0, + # 'props', 'children', + # 'props', 'children', 3 + # ] + # }) + # generic_chapter_assertions('chapter3') + # + # chapter3_assertions() + # + # # switch to 4 + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[3]).click() + # self.wait_for_text_to_equal('#body', 'Just a string') + # self.percy_snapshot(name='chapter-4') + # + # # each element should exist in the dom + # paths = self.driver.execute_script( + # 'return window.store.getState().paths' + # ) + # for key in paths: + # self.driver.find_element_by_id(key) + # self.assertEqual(paths, { + # 'toc': ['props', 'children', 0], + # 'body': ['props', 'children', 1] + # }) + # + # # switch back to 1 + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[0]).click() + # time.sleep(0.5) + # chapter1_assertions() + # self.percy_snapshot(name='chapter-1-again') + # + # # switch to 5 + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[4]).click() + # time.sleep(1) + # # click on the button and check the output div before and after + # chapter5_div = lambda: self.driver.find_element_by_id( + # 'chapter5-output' + # ) + # chapter5_button = lambda: self.driver.find_element_by_id( + # 'chapter5-button' + # ) + # self.assertEqual(chapter5_div().text, '') + # chapter5_button().click() + # wait_for(lambda: chapter5_div().text == chapter5_output_children) + # time.sleep(0.5) + # self.percy_snapshot(name='chapter-5') + # self.assertEqual(call_counts['chapter5-output'].value, 1) + # + # def test_dependencies_on_components_that_dont_exist(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input(id='input', value='initial value'), + # html.Div(id='output-1') + # ]) + # + # # standard callback + # output_1_call_count = Value('i', 0) + # + # @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + # def update_output(value): + # output_1_call_count.value += 1 + # return value + # + # # callback for component that doesn't yet exist in the dom + # # in practice, it might get added by some other callback + # app.config.supress_callback_exceptions = True + # output_2_call_count = Value('i', 0) + # + # @app.callback( + # Output('output-2', 'children'), + # [Input('input', 'value')] + # ) + # def update_output_2(value): + # output_2_call_count.value += 1 + # return value + # + # self.startServer(app) + # + # self.wait_for_text_to_equal('#output-1', 'initial value') + # self.percy_snapshot(name='dependencies') + # time.sleep(1.0) + # self.assertEqual(output_1_call_count.value, 1) + # self.assertEqual(output_2_call_count.value, 0) + # + # input = self.driver.find_element_by_id('input') + # + # input.send_keys('a') + # self.wait_for_text_to_equal('#output-1', 'initial valuea') + # time.sleep(1.0) + # self.assertEqual(output_1_call_count.value, 2) + # self.assertEqual(output_2_call_count.value, 0) + # + # self.request_queue_assertions(2) + # + # assert_clean_console(self) + # + # def test_events(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # html.Button('Click Me', id='button'), + # html.Div(id='output') + # ]) + # + # call_count = Value('i', 0) + # + # @app.callback(Output('output', 'children'), + # events=[Event('button', 'click')]) + # def update_output(): + # call_count.value += 1 + # return 'Click' + # + # self.startServer(app) + # btn = self.driver.find_element_by_id('button') + # output = lambda: self.driver.find_element_by_id('output') + # self.assertEqual(call_count.value, 0) + # self.assertEqual(output().text, '') + # + # btn.click() + # wait_for(lambda: output().text == 'Click') + # self.assertEqual(call_count.value, 1) + # + # def test_events_and_state(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # html.Button('Click Me', id='button'), + # dcc.Input(value='Initial State', id='state'), + # html.Div(id='output') + # ]) + # + # call_count = Value('i', 0) + # + # @app.callback(Output('output', 'children'), + # state=[State('state', 'value')], + # events=[Event('button', 'click')]) + # def update_output(value): + # call_count.value += 1 + # return value + # + # self.startServer(app) + # btn = self.driver.find_element_by_id('button') + # output = lambda: self.driver.find_element_by_id('output') + # + # self.assertEqual(call_count.value, 0) + # self.assertEqual(output().text, '') + # + # btn.click() + # wait_for(lambda: output().text == 'Initial State') + # self.assertEqual(call_count.value, 1) + # + # # Changing state shouldn't fire the callback + # state = self.driver.find_element_by_id('state') + # state.send_keys('x') + # time.sleep(0.75) + # self.assertEqual(output().text, 'Initial State') + # self.assertEqual(call_count.value, 1) + # + # btn.click() + # wait_for(lambda: output().text == 'Initial Statex') + # self.assertEqual(call_count.value, 2) + # + # def test_events_state_and_inputs(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # html.Button('Click Me', id='button'), + # dcc.Input(value='Initial Input', id='input'), + # dcc.Input(value='Initial State', id='state'), + # html.Div(id='output') + # ]) + # + # call_count = Value('i', 0) + # + # @app.callback(Output('output', 'children'), + # inputs=[Input('input', 'value')], + # state=[State('state', 'value')], + # events=[Event('button', 'click')]) + # def update_output(input, state): + # call_count.value += 1 + # return 'input="{}", state="{}"'.format(input, state) + # + # self.startServer(app) + # btn = lambda: self.driver.find_element_by_id('button') + # output = lambda: self.driver.find_element_by_id('output') + # input = lambda: self.driver.find_element_by_id('input') + # state = lambda: self.driver.find_element_by_id('state') + # + # # callback gets called with initial input + # self.assertEqual(call_count.value, 1) + # self.assertEqual( + # output().text, + # 'input="Initial Input", state="Initial State"' + # ) + # + # btn().click() + # wait_for(lambda: call_count.value == 2) + # self.assertEqual( + # output().text, + # 'input="Initial Input", state="Initial State"') + # + # input().send_keys('x') + # wait_for(lambda: call_count.value == 3) + # self.assertEqual( + # output().text, + # 'input="Initial Inputx", state="Initial State"') + # + # state().send_keys('x') + # time.sleep(0.75) + # self.assertEqual(call_count.value, 3) + # self.assertEqual( + # output().text, + # 'input="Initial Inputx", state="Initial State"') + # + # btn().click() + # wait_for(lambda: call_count.value == 4) + # self.assertEqual( + # output().text, + # 'input="Initial Inputx", state="Initial Statex"') + # + # def test_state_and_inputs(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input(value='Initial Input', id='input'), + # dcc.Input(value='Initial State', id='state'), + # html.Div(id='output') + # ]) + # + # call_count = Value('i', 0) + # + # @app.callback(Output('output', 'children'), + # inputs=[Input('input', 'value')], + # state=[State('state', 'value')]) + # def update_output(input, state): + # call_count.value += 1 + # return 'input="{}", state="{}"'.format(input, state) + # + # self.startServer(app) + # output = lambda: self.driver.find_element_by_id('output') + # input = lambda: self.driver.find_element_by_id('input') + # state = lambda: self.driver.find_element_by_id('state') + # + # # callback gets called with initial input + # self.assertEqual(call_count.value, 1) + # self.assertEqual( + # output().text, + # 'input="Initial Input", state="Initial State"' + # ) + # + # input().send_keys('x') + # wait_for(lambda: call_count.value == 2) + # self.assertEqual( + # output().text, + # 'input="Initial Inputx", state="Initial State"') + # + # state().send_keys('x') + # time.sleep(0.75) + # self.assertEqual(call_count.value, 2) + # self.assertEqual( + # output().text, + # 'input="Initial Inputx", state="Initial State"') + # + # input().send_keys('y') + # wait_for(lambda: call_count.value == 3) + # self.assertEqual( + # output().text, + # 'input="Initial Inputxy", state="Initial Statex"') + # + # def test_event_creating_inputs(self): + # app = Dash(__name__) + # + # ids = { + # k: k for k in ['button', 'button-output', 'input', 'input-output'] + # } + # app.layout = html.Div([ + # html.Button(id=ids['button']), + # html.Div(id=ids['button-output']) + # ]) + # for script in dcc._js_dist: + # script['namespace'] = 'dash_core_components' + # app.scripts.append_script(script) + # + # app.config.supress_callback_exceptions = True + # call_counts = { + # ids['input-output']: Value('i', 0), + # ids['button-output']: Value('i', 0) + # } + # + # @app.callback( + # Output(ids['button-output'], 'children'), + # events=[Event(ids['button'], 'click')]) + # def display(): + # call_counts['button-output'].value += 1 + # return html.Div([ + # dcc.Input(id=ids['input'], value='initial state'), + # html.Div(id=ids['input-output']) + # ]) + # + # @app.callback( + # Output(ids['input-output'], 'children'), + # [Input(ids['input'], 'value')]) + # def update_input(value): + # call_counts['input-output'].value += 1 + # return 'Input is equal to "{}"'.format(value) + # + # self.startServer(app) + # time.sleep(1) + # self.assertEqual(call_counts[ids['button-output']].value, 0) + # self.assertEqual(call_counts[ids['input-output']].value, 0) + # + # btn = lambda: self.driver.find_element_by_id(ids['button']) + # output = lambda: self.driver.find_element_by_id(ids['input-output']) + # with self.assertRaises(Exception): + # output() + # + # btn().click() + # wait_for(lambda: call_counts[ids['input-output']].value == 1) + # self.assertEqual(call_counts[ids['button-output']].value, 1) + # self.assertEqual(output().text, 'Input is equal to "initial state"') + # + # def test_chained_dependencies_direct_lineage(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input(id='input-1', value='input 1'), + # dcc.Input(id='input-2'), + # html.Div('test', id='output') + # ]) + # input1 = lambda: self.driver.find_element_by_id('input-1') + # input2 = lambda: self.driver.find_element_by_id('input-2') + # output = lambda: self.driver.find_element_by_id('output') + # + # call_counts = { + # 'output': Value('i', 0), + # 'input-2': Value('i', 0) + # } + # + # @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) + # def update_input(input1): + # call_counts['input-2'].value += 1 + # return '<<{}>>'.format(input1) + # + # @app.callback(Output('output', 'children'), [ + # Input('input-1', 'value'), + # Input('input-2', 'value') + # ]) + # def update_output(input1, input2): + # call_counts['output'].value += 1 + # return '{} + {}'.format(input1, input2) + # + # self.startServer(app) + # + # wait_for(lambda: call_counts['output'].value == 1) + # wait_for(lambda: call_counts['input-2'].value == 1) + # self.assertEqual(input1().get_attribute('value'), 'input 1') + # self.assertEqual(input2().get_attribute('value'), '<>') + # self.assertEqual(output().text, 'input 1 + <>') + # + # input1().send_keys('x') + # wait_for(lambda: call_counts['output'].value == 2) + # wait_for(lambda: call_counts['input-2'].value == 2) + # self.assertEqual(input1().get_attribute('value'), 'input 1x') + # self.assertEqual(input2().get_attribute('value'), '<>') + # self.assertEqual(output().text, 'input 1x + <>') + # + # input2().send_keys('y') + # wait_for(lambda: call_counts['output'].value == 3) + # wait_for(lambda: call_counts['input-2'].value == 2) + # self.assertEqual(input1().get_attribute('value'), 'input 1x') + # self.assertEqual(input2().get_attribute('value'), '<>y') + # self.assertEqual(output().text, 'input 1x + <>y') + # + # + # def test_chained_dependencies_branched_lineage(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input(id='grandparent', value='input 1'), + # dcc.Input(id='parent-a'), + # dcc.Input(id='parent-b'), + # html.Div(id='child-a'), + # html.Div(id='child-b') + # ]) + # grandparent = lambda: self.driver.find_element_by_id('grandparent') + # parenta = lambda: self.driver.find_element_by_id('parent-a') + # parentb = lambda: self.driver.find_element_by_id('parent-b') + # childa = lambda: self.driver.find_element_by_id('child-a') + # childb = lambda: self.driver.find_element_by_id('child-b') + # + # call_counts = { + # 'parent-a': Value('i', 0), + # 'parent-b': Value('i', 0), + # 'child-a': Value('i', 0), + # 'child-b': Value('i', 0) + # } + # + # @app.callback(Output('parent-a', 'value'), + # [Input('grandparent', 'value')]) + # def update_parenta(value): + # call_counts['parent-a'].value += 1 + # return 'a: {}'.format(value) + # + # @app.callback(Output('parent-b', 'value'), + # [Input('grandparent', 'value')]) + # def update_parentb(value): + # time.sleep(0.5) + # call_counts['parent-b'].value += 1 + # return 'b: {}'.format(value) + # + # @app.callback(Output('child-a', 'children'), + # [Input('parent-a', 'value'), + # Input('parent-b', 'value')]) + # def update_childa(parenta_value, parentb_value): + # time.sleep(1) + # call_counts['child-a'].value += 1 + # return '{} + {}'.format(parenta_value, parentb_value) + # + # @app.callback(Output('child-b', 'children'), + # [Input('parent-a', 'value'), + # Input('parent-b', 'value'), + # Input('grandparent', 'value')]) + # def update_childb(parenta_value, parentb_value, grandparent_value): + # call_counts['child-b'].value += 1 + # return '{} + {} + {}'.format( + # parenta_value, + # parentb_value, + # grandparent_value + # ) + # + # self.startServer(app) + # + # wait_for(lambda: childa().text == 'a: input 1 + b: input 1') + # wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') + # time.sleep(1) # wait for potential requests of app to settle down + # self.assertEqual(parenta().get_attribute('value'), 'a: input 1') + # self.assertEqual(parentb().get_attribute('value'), 'b: input 1') + # self.assertEqual(call_counts['parent-a'].value, 1) + # self.assertEqual(call_counts['parent-b'].value, 1) + # self.assertEqual(call_counts['child-a'].value, 1) + # self.assertEqual(call_counts['child-b'].value, 1) + # + # def test_removing_component_while_its_getting_updated(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.RadioItems( + # id='toc', + # options=[ + # {'label': i, 'value': i} for i in ['1', '2'] + # ], + # value='1' + # ), + # html.Div(id='body') + # ]) + # app.config.supress_callback_exceptions = True + # + # call_counts = { + # 'body': Value('i', 0), + # 'button-output': Value('i', 0) + # } + # + # @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + # def update_body(chapter): + # call_counts['body'].value += 1 + # if chapter == '1': + # return [ + # html.Div('Chapter 1'), + # html.Button( + # 'clicking this button takes forever', + # id='button' + # ), + # html.Div(id='button-output') + # ] + # elif chapter == '2': + # return 'Chapter 2' + # else: + # raise Exception('chapter is {}'.format(chapter)) + # + # @app.callback( + # Output('button-output', 'children'), + # events=[Event('button', 'click')]) + # def this_callback_takes_forever(): + # time.sleep(5) + # call_counts['button-output'].value += 1 + # return 'New value!' + # + # body = lambda: self.driver.find_element_by_id('body') + # self.startServer(app) + # + # wait_for(lambda: call_counts['body'].value == 1) + # time.sleep(0.5) + # self.driver.find_element_by_id('button').click() + # + # # while that callback is resolving, switch the chapter, + # # hiding the `button-output` tag + # def chapter2_assertions(): + # wait_for(lambda: body().text == 'Chapter 2') + # self.assertEqual( + # self.driver.execute_script( + # 'return JSON.parse(JSON.stringify(' + # 'window.store.getState().layout' + # '))' + # ), + # { + # "namespace": "dash_html_components", + # "type": "Div", + # "props": { + # "children": [ + # { + # "namespace": "dash_core_components", + # "type": "RadioItems", + # "props": { + # "value": "2", + # "options": app.layout['toc'].options, + # "id": app.layout['toc'].id, + # } + # }, + # { + # "namespace": "dash_html_components", + # "type": "Div", + # "props": { + # "id": "body", + # "children": "Chapter 2" + # } + # } + # ] + # } + # } + # ) + # self.assertEqual( + # self.driver.execute_script( + # 'return JSON.parse(JSON.stringify(' + # 'window.store.getState().paths' + # '))' + # ), + # { + # "toc": ["props", "children", 0], + # "body": ["props", "children", 1] + # } + # ) + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[1]).click() + # chapter2_assertions() + # time.sleep(5) + # wait_for(lambda: call_counts['button-output'].value == 1) + # time.sleep(2) # liberally wait for the front-end to process request + # chapter2_assertions() + # assert_clean_console(self) + # + # def test_rendering_layout_calls_callback_once_per_output(self): + # app = Dash(__name__) + # call_count = Value('i', 0) + # + # app.config['suppress_callback_exceptions'] = True + # app.layout = html.Div([ + # html.Div([ + # dcc.Input( + # value='Input {}'.format(i), + # id='input-{}'.format(i) + # ) + # for i in range(10) + # ]), + # html.Div(id='container'), + # dcc.RadioItems() + # ]) + # + # @app.callback( + # Output('container', 'children'), + # [Input('input-{}'.format(i), 'value') for i in range(10)]) + # def dynamic_output(*args): + # call_count.value += 1 + # return json.dumps(args, indent=2) + # + # self.startServer(app) + # + # time.sleep(5) + # + # self.percy_snapshot( + # name='test_rendering_layout_calls_callback_once_per_output' + # ) + # + # self.assertEqual(call_count.value, 1) + # + # def test_rendering_new_content_calls_callback_once_per_output(self): + # app = Dash(__name__) + # call_count = Value('i', 0) + # + # app.config['suppress_callback_exceptions'] = True + # app.layout = html.Div([ + # html.Button( + # id='display-content', + # children='Display Content', + # n_clicks=0 + # ), + # html.Div(id='container'), + # dcc.RadioItems() + # ]) + # + # @app.callback( + # Output('container', 'children'), + # [Input('display-content', 'n_clicks')]) + # def display_output(n_clicks): + # if n_clicks == 0: + # return '' + # return html.Div([ + # html.Div([ + # dcc.Input( + # value='Input {}'.format(i), + # id='input-{}'.format(i) + # ) + # for i in range(10) + # ]), + # html.Div(id='dynamic-output') + # ]) + # + # @app.callback( + # Output('dynamic-output', 'children'), + # [Input('input-{}'.format(i), 'value') for i in range(10)]) + # def dynamic_output(*args): + # call_count.value += 1 + # return json.dumps(args, indent=2) + # + # self.startServer(app) + # + # self.wait_for_element_by_css_selector('#display-content').click() + # + # time.sleep(5) + # + # self.percy_snapshot( + # name='test_rendering_new_content_calls_callback_once_per_output' + # ) + # + # self.assertEqual(call_count.value, 1) + # + # def test_callbacks_called_multiple_times_and_out_of_order(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # html.Button(id='input', n_clicks=0), + # html.Div(id='output') + # ]) + # + # call_count = Value('i', 0) + # + # @app.callback( + # Output('output', 'children'), + # [Input('input', 'n_clicks')]) + # def update_output(n_clicks): + # call_count.value = call_count.value + 1 + # if n_clicks == 1: + # time.sleep(4) + # return n_clicks + # + # self.startServer(app) + # button = self.wait_for_element_by_css_selector('#input') + # button.click() + # button.click() + # time.sleep(8) + # self.percy_snapshot( + # name='test_callbacks_called_multiple_times_and_out_of_order' + # ) + # self.assertEqual(call_count.value, 3) + # self.assertEqual( + # self.driver.find_element_by_id('output').text, + # '2' + # ) + # request_queue = self.driver.execute_script( + # 'return window.store.getState().requestQueue' + # ) + # self.assertFalse(request_queue[0]['rejected']) + # self.assertEqual(len(request_queue), 1) + # + # + # def test_callbacks_with_shared_grandparent(self): + # app = dash.Dash() + # + # app.layout = html.Div([ + # html.Div(id='session-id', children='id'), + # dcc.Dropdown(id='dropdown-1'), + # dcc.Dropdown(id='dropdown-2'), + # ]) + # + # options = [{'value': 'a', 'label': 'a'}] + # + # call_counts = { + # 'dropdown_1': Value('i', 0), + # 'dropdown_2': Value('i', 0) + # } + # + # @app.callback( + # Output('dropdown-1', 'options'), + # [Input('dropdown-1', 'value'), + # Input('session-id', 'children')]) + # def dropdown_1(value, session_id): + # call_counts['dropdown_1'].value += 1 + # return options + # + # @app.callback( + # Output('dropdown-2', 'options'), + # [Input('dropdown-2', 'value'), + # Input('session-id', 'children')]) + # def dropdown_2(value, session_id): + # call_counts['dropdown_2'].value += 1 + # return options + # + # self.startServer(app) + # + # self.wait_for_element_by_css_selector('#session-id') + # time.sleep(2) + # self.assertEqual(call_counts['dropdown_1'].value, 1) + # self.assertEqual(call_counts['dropdown_2'].value, 1) + # + # assert_clean_console(self) + # + # def test_callbacks_triggered_on_generated_output(self): + # app = dash.Dash() + # app.config['suppress_callback_exceptions'] = True + # + # call_counts = { + # 'tab1': Value('i', 0), + # 'tab2': Value('i', 0) + # } + # + # app.layout = html.Div([ + # dcc.Dropdown( + # id='outer-controls', + # options=[{'label': i, 'value': i} for i in ['a', 'b']], + # value='a' + # ), + # dcc.RadioItems( + # options=[ + # {'label': 'Tab 1', 'value': 1}, + # {'label': 'Tab 2', 'value': 2} + # ], + # value=1, + # id='tabs', + # ), + # html.Div(id='tab-output') + # ]) + # + # @app.callback(Output('tab-output', 'children'), + # [Input('tabs', 'value')]) + # def display_content(value): + # return html.Div([ + # html.Div(id='tab-{}-output'.format(value)) + # ]) + # + # @app.callback(Output('tab-1-output', 'children'), + # [Input('outer-controls', 'value')]) + # def display_tab1_output(value): + # call_counts['tab1'].value += 1 + # return 'You have selected "{}"'.format(value) + # + # @app.callback(Output('tab-2-output', 'children'), + # [Input('outer-controls', 'value')]) + # def display_tab2_output(value): + # call_counts['tab2'].value += 1 + # return 'You have selected "{}"'.format(value) + # + # self.startServer(app) + # self.wait_for_element_by_css_selector('#tab-output') + # time.sleep(2) + # + # self.assertEqual(call_counts['tab1'].value, 1) + # self.assertEqual(call_counts['tab2'].value, 0) + # self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') + # self.wait_for_text_to_equal('#tab-1-output', 'You have selected "a"') + # + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[1]).click() + # time.sleep(2) + # + # self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') + # self.wait_for_text_to_equal('#tab-2-output', 'You have selected "a"') + # self.assertEqual(call_counts['tab1'].value, 1) + # self.assertEqual(call_counts['tab2'].value, 1) + # + # assert_clean_console(self) + # + # def test_initialization_with_overlapping_outputs(self): + # app = dash.Dash() + # app.layout = html.Div([ + # + # html.Div(id='input-1', children='input-1'), + # html.Div(id='input-2', children='input-2'), + # html.Div(id='input-3', children='input-3'), + # html.Div(id='input-4', children='input-4'), + # html.Div(id='input-5', children='input-5'), + # + # html.Div(id='output-1'), + # html.Div(id='output-2'), + # html.Div(id='output-3'), + # html.Div(id='output-4'), + # + # ]) + # call_counts = { + # 'output-1': Value('i', 0), + # 'output-2': Value('i', 0), + # 'output-3': Value('i', 0), + # 'output-4': Value('i', 0), + # } + # + # def generate_callback(outputid): + # def callback(*args): + # call_counts[outputid].value += 1 + # return '{}, {}'.format(*args) + # return callback + # + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # app.callback( + # Output(outputid, 'children'), + # [Input('input-{}'.format(i), 'children'), + # Input('input-{}'.format(i+1), 'children')] + # )(generate_callback(outputid)) + # + # self.startServer(app) + # + # self.wait_for_element_by_css_selector('#output-1') + # time.sleep(5) + # + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # self.assertEqual(call_counts[outputid].value, 1) + # self.wait_for_text_to_equal( + # '#{}'.format(outputid), + # "input-{}, input-{}".format(i, i+1) + # ) + # + # def test_generate_overlapping_outputs(self): + # app = dash.Dash() + # app.config['suppress_callback_exceptions'] = True + # block = html.Div([ + # + # html.Div(id='input-1', children='input-1'), + # html.Div(id='input-2', children='input-2'), + # html.Div(id='input-3', children='input-3'), + # html.Div(id='input-4', children='input-4'), + # html.Div(id='input-5', children='input-5'), + # + # html.Div(id='output-1'), + # html.Div(id='output-2'), + # html.Div(id='output-3'), + # html.Div(id='output-4'), + # + # ]) + # app.layout = html.Div([ + # html.Div(id='input'), + # html.Div(id='container') + # ]) + # + # call_counts = { + # 'container': Value('i', 0), + # 'output-1': Value('i', 0), + # 'output-2': Value('i', 0), + # 'output-3': Value('i', 0), + # 'output-4': Value('i', 0), + # } + # + # @app.callback(Output('container', 'children'), + # [Input('input', 'children')]) + # def display_output(*args): + # call_counts['container'].value += 1 + # return block + # + # def generate_callback(outputid): + # def callback(*args): + # call_counts[outputid].value += 1 + # return '{}, {}'.format(*args) + # return callback + # + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # app.callback( + # Output(outputid, 'children'), + # [Input('input-{}'.format(i), 'children'), + # Input('input-{}'.format(i+1), 'children')] + # )(generate_callback(outputid)) + # + # self.startServer(app) + # + # wait_for(lambda: call_counts['container'].value == 1) + # self.wait_for_element_by_css_selector('#output-1') + # time.sleep(5) + # + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # self.assertEqual(call_counts[outputid].value, 1) + # self.wait_for_text_to_equal( + # '#{}'.format(outputid), + # "input-{}, input-{}".format(i, i+1) + # ) + # self.assertEqual(call_counts['container'].value, 1) + + def test_update_react_version(self): + import dash_renderer - self.startServer(app) - - el = self.wait_for_element_by_css_selector('#_dash-app-content') - - # TODO - Make less fragile with http://lxml.de/lxmlhtml.html#html-diff - rendered_dom = ''' -
- Basic string - - 3.14 - -
- Child div with basic string -
- -
-
- -
-
- Grandchild div -
- -
-
- Great grandchild -
- - 3.14159 - - another basic string -
- -
-
-
-
- -
-
- - -
-
- -
-
-
-
-
- -
- ''' - # React wraps text and numbers with e.g. - # Remove those - comment_regex = '' - - # Somehow the html attributes are unordered. - # Try different combinations (they're all valid html) - style_permutations = [ - 'style="color: red; font-size: 30px;"', - 'style="font-size: 30px; color: red;"' - ] - permutations = itertools.permutations([ - 'id="p.c.3"', - 'class="my-class"', - 'title="tooltip"', - ], 3) - passed = False - for permutation in permutations: - for style in style_permutations: - actual_cleaned = re.sub(comment_regex, '', el.get_attribute('innerHTML')) - expected_cleaned = re.sub( - comment_regex, - '', - rendered_dom.replace('\n', '') - .replace(' ', '') - .replace('PERMUTE', ' '.join(list(permutation) + [style])) - ) - passed = passed or (actual_cleaned == expected_cleaned) - if not passed: - raise Exception( - 'HTML does not match\nActual:\n{}\n\nExpected:\n{}'.format( - actual_cleaned, - expected_cleaned - ) - ) - - # Check that no errors or warnings were displayed self.assertEqual( - self.driver.execute_script( - 'return window.tests.console.error.length' - ), - 0 - ) - self.assertEqual( - self.driver.execute_script( - 'return window.tests.console.warn.length' - ), - 0 - ) - - # Check the initial stores - - # layout should just be the JSON-ified app.layout - self.assertEqual( - self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().layout' - '))' - ), - { - "namespace": "dash_html_components", - "props": { - "children": [ - "Basic string", - 3.14, - None, - { - "namespace": "dash_html_components", - "props": { - "children": "Child div with basic string", - "id": "p.c.3", - 'className': "my-class", - 'title': 'tooltip', - 'style': { - 'color': 'red', 'fontSize': 30 - } - }, - "type": "Div" - }, - { - "namespace": "dash_html_components", - "props": { - "children": None, - "id": "p.c.4" - }, - "type": "Div" - }, - { - "namespace": "dash_html_components", - "props": { - "children": [ - { - "namespace": "dash_html_components", - "props": { - "children": "Grandchild div", - "id": "p.c.5.p.c.0" - }, - "type": "Div" - }, - { - "namespace": "dash_html_components", - "props": { - "children": [ - { - "namespace": "dash_html_components", - "props": { - "children": "Great grandchild", - "id": "p.c.5.p.c.1.p.c.0" - }, - "type": "Div" - }, - 3.14159, - "another basic string" - ], - "id": "p.c.5.p.c.1" - }, - "type": "Div" - }, - { - "namespace": "dash_html_components", - "props": { - "children": [ - { - "namespace": "dash_html_components", - "props": { - "children": { - "namespace": "dash_html_components", - "props": { - "children": [ - { - "namespace": "dash_html_components", - "props": { - "children": [ - { - "namespace": "dash_html_components", - "props": { - "children": None, - "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0" - }, - "type": "Div" - }, - "", - { - "namespace": "dash_html_components", - "props": { - "children": None, - "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2" - }, - "type": "Div" - } - ], - "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0" - }, - "type": "Div" - } - ], - "id": "p.c.5.p.c.2.p.c.0.p.c" - }, - "type": "Div" - }, - "id": "p.c.5.p.c.2.p.c.0" - }, - "type": "Div" - } - ], - "id": "p.c.5.p.c.2" - }, - "type": "Div" - } - ], - "id": "p.c.5" - }, - "type": "Div" - } - ] - }, - "type": "Div" - } - ) - - # graphs should just be empty since there are no dependencies - self.assertEqual( - self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().graphs' - '))' - ), - { - "InputGraph": { - "nodes": {}, - "outgoingEdges": {}, - "incomingEdges": {} - }, - "EventGraph": { - "nodes": {}, - "outgoingEdges": {}, - "incomingEdges": {} - } - } - ) - - # paths is just a lookup table of the components's IDs and their - # placement in the tree. - # in this case the IDs are just abbreviations of the path to make - # things easy to verify. - self.assertEqual( - self.driver.execute_script( - 'return window.store.getState().paths' - ), - { - "p.c.3": [ - "props", "children", 3 - ], - "p.c.4": [ - "props", "children", 4 - ], - "p.c.5": [ - "props", "children", 5 - ], - "p.c.5.p.c.0": [ - "props", "children", 5, - "props", "children", 0 - ], - "p.c.5.p.c.1": [ - "props", "children", 5, - "props", "children", 1 - ], - "p.c.5.p.c.1.p.c.0": [ - "props", "children", 5, - "props", "children", 1, - "props", "children", 0 - ], - "p.c.5.p.c.2": [ - "props", "children", 5, - "props", "children", 2 - ], - "p.c.5.p.c.2.p.c.0": [ - "props", "children", 5, - "props", "children", 2, - "props", "children", 0 - ], - "p.c.5.p.c.2.p.c.0.p.c": [ - "props", "children", 5, - "props", "children", 2, - "props", "children", 0, - "props", "children" + dash_renderer._js_dist_dependencies, + [{ + 'external_url': [ + 'https://unpkg.com/react@15.4.2/dist/react.min.js', + 'https://unpkg.com/react-dom@15.4.2/dist/react-dom.min.js', ], - "p.c.5.p.c.2.p.c.0.p.c.p.c.0": [ - "props", "children", 5, - "props", "children", 2, - "props", "children", 0, - "props", "children", - "props", "children", 0 + 'relative_package_path': [ + 'react@15.4.2.min.js', + 'react-dom@15.4.2.min.js', ], - "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0": [ - "props", "children", 5, - "props", "children", 2, - "props", "children", 0, - "props", "children", - "props", "children", 0, - "props", "children", 0 - ], - "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2": [ - "props", "children", 5, - "props", "children", 2, - "props", "children", 0, - "props", "children", - "props", "children", 0, - "props", "children", 2 - ] - } - ) - - self.request_queue_assertions(0) - - self.percy_snapshot(name='layout') - - assert_clean_console(self) - - def test_simple_callback(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div( - html.Div([ - 1.5, - None, - 'string', - html.Div(id='output-1') - ]) - ) - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - call_count.value = call_count.value + 1 - return value - - self.startServer(app) - - self.wait_for_text_to_equal('#output-1', 'initial value') - self.percy_snapshot(name='simple-callback-1') - - input1 = self.wait_for_element_by_css_selector('#input') - input1.clear() - - input1.send_keys('hello world') - - self.wait_for_text_to_equal('#output-1', 'hello world') - self.percy_snapshot(name='simple-callback-2') - - self.assertEqual( - call_count.value, - # an initial call to retrieve the first value - 1 + - # one for each hello world character - len('hello world') - ) + 'namespace': 'dash_renderer', + }]) - self.request_queue_assertions( - expected_length=1, - check_rejected=False) + dash_renderer._set_react_version('16.2.0') - assert_clean_console(self) - - def test_callbacks_generating_children(self): - """ Modify the DOM tree by adding new - components in the callbacks - """ - - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div(id='output') - ]) - - @app.callback(Output('output', 'children'), [Input('input', 'value')]) - def pad_output(input): - return html.Div([ - dcc.Input( - id='sub-input-1', - value='sub input initial value' - ), - html.Div(id='sub-output-1') - ]) - - call_count = Value('i', 0) - - # these components don't exist in the initial render - app.config.supress_callback_exceptions = True - - @app.callback( - Output('sub-output-1', 'children'), - [Input('sub-input-1', 'value')] - ) - def update_input(value): - call_count.value = call_count.value + 1 - return value - - self.startServer(app) - - output = self.driver.find_element_by_id('output') - output_html = output.get_attribute('innerHTML') - - wait_for(lambda: call_count.value == 1) - - # Adding new children to the layout should - # call the callbacks immediately to set - # the correct initial state - wait_for( - lambda: ( - self.driver.find_element_by_id('output') - .get_attribute('innerHTML') in [''' -
- {} -
- sub input initial value -
-
'''.replace('\n', '').replace(' ', '').format(input) - for input in [ - # html attributes are unordered, so include both versions - '', - '' - ] - ] - ) - ) - self.percy_snapshot(name='callback-generating-function-1') - - # the paths should include these new output IDs + # Check that the _js_dist_dependencies updated self.assertEqual( - self.driver.execute_script('return window.store.getState().paths'), - { - 'input': [ - 'props', 'children', 0 - ], - 'output': ['props', 'children', 1], - 'sub-input-1': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 0 - ], - 'sub-output-1': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 1 - ] - } - ) - - # editing the input should modify the sub output - sub_input = self.driver.find_element_by_id('sub-input-1') - sub_input.send_keys('a') - self.wait_for_text_to_equal( - '#sub-output-1', - 'sub input initial valuea') - - self.assertEqual(call_count.value, 2) - - self.request_queue_assertions(call_count.value + 1) - self.percy_snapshot(name='callback-generating-function-2') - - assert_clean_console(self) - - def test_radio_buttons_callbacks_generating_children(self): - self.maxDiff = 100 * 1000 - app = Dash(__name__) - app.layout = html.Div([ - dcc.RadioItems( - options=[ - {'label': 'Chapter 1', 'value': 'chapter1'}, - {'label': 'Chapter 2', 'value': 'chapter2'}, - {'label': 'Chapter 3', 'value': 'chapter3'}, - {'label': 'Chapter 4', 'value': 'chapter4'}, - {'label': 'Chapter 5', 'value': 'chapter5'} + dash_renderer._js_dist_dependencies, + [{ + 'external_url': [ + 'https://unpkg.com/react@16.2.0/umd/react.production.min.js', + 'https://unpkg.com/react@16.2.0/umd/react-dom.production.min.js', ], - value='chapter1', - id='toc' - ), - html.Div(id='body') - ]) - for script in dcc._js_dist: - app.scripts.append_script(script) - - chapters = { - 'chapter1': html.Div([ - html.H1('Chapter 1', id='chapter1-header'), - dcc.Dropdown( - options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'SF']], - value='NYC', - id='chapter1-controls' - ), - html.Label(id='chapter1-label'), - dcc.Graph(id='chapter1-graph') - ]), - # Chapter 2 has the some of the same components in the same order - # as Chapter 1. This means that they won't get remounted - # unless they set their own keys are differently. - # Switching back and forth between 1 and 2 implicitly - # tests how components update when they aren't remounted. - 'chapter2': html.Div([ - html.H1('Chapter 2', id='chapter2-header'), - dcc.RadioItems( - options=[{'label': i, 'value': i} - for i in ['USA', 'Canada']], - value='USA', - id='chapter2-controls' - ), - html.Label(id='chapter2-label'), - dcc.Graph(id='chapter2-graph') - ]), - # Chapter 3 has a different layout and so the components - # should get rewritten - 'chapter3': [html.Div( - html.Div([ - html.H3('Chapter 3', id='chapter3-header'), - html.Label(id='chapter3-label'), - dcc.Graph(id='chapter3-graph'), - dcc.RadioItems( - options=[{'label': i, 'value': i} - for i in ['Summer', 'Winter']], - value='Winter', - id='chapter3-controls' - ) - ]) - )], - - # Chapter 4 doesn't have an object to recursively - # traverse - 'chapter4': 'Just a string', - - # Chapter 5 contains elements that are bound with events - 'chapter5': [html.Div([ - html.Button(id='chapter5-button'), - html.Div(id='chapter5-output') - ])] - } - - call_counts = { - 'body': Value('i', 0), - 'chapter1-graph': Value('i', 0), - 'chapter1-label': Value('i', 0), - 'chapter2-graph': Value('i', 0), - 'chapter2-label': Value('i', 0), - 'chapter3-graph': Value('i', 0), - 'chapter3-label': Value('i', 0), - 'chapter5-output': Value('i', 0) - } - - @app.callback(Output('body', 'children'), [Input('toc', 'value')]) - def display_chapter(toc_value): - call_counts['body'].value += 1 - return chapters[toc_value] - - app.config.supress_callback_exceptions = True - - def generate_graph_callback(counterId): - def callback(value): - call_counts[counterId].value += 1 - return { - 'data': [{ - 'x': ['Call Counter'], - 'y': [call_counts[counterId].value], - 'type': 'bar' - }], - 'layout': {'title': value} - } - return callback - - def generate_label_callback(id): - def update_label(value): - call_counts[id].value += 1 - return value - return update_label - - for chapter in ['chapter1', 'chapter2', 'chapter3']: - app.callback( - Output('{}-graph'.format(chapter), 'figure'), - [Input('{}-controls'.format(chapter), 'value')] - )(generate_graph_callback('{}-graph'.format(chapter))) - - app.callback( - Output('{}-label'.format(chapter), 'children'), - [Input('{}-controls'.format(chapter), 'value')] - )(generate_label_callback('{}-label'.format(chapter))) - - chapter5_output_children = 'Button clicked' - - @app.callback(Output('chapter5-output', 'children'), - events=[Event('chapter5-button', 'click')]) - def display_output(): - call_counts['chapter5-output'].value += 1 - return chapter5_output_children - - self.startServer(app) - - time.sleep(0.5) - wait_for(lambda: call_counts['body'].value == 1) - wait_for(lambda: call_counts['chapter1-graph'].value == 1) - wait_for(lambda: call_counts['chapter1-label'].value == 1) - self.assertEqual(call_counts['chapter2-graph'].value, 0) - self.assertEqual(call_counts['chapter2-label'].value, 0) - self.assertEqual(call_counts['chapter3-graph'].value, 0) - self.assertEqual(call_counts['chapter3-label'].value, 0) - - def generic_chapter_assertions(chapter): - # each element should exist in the dom - paths = self.driver.execute_script( - 'return window.store.getState().paths' - ) - for key in paths: - self.driver.find_element_by_id(key) - - if chapter == 'chapter3': - value = chapters[chapter][0][ - '{}-controls'.format(chapter) - ].value - else: - value = chapters[chapter]['{}-controls'.format(chapter)].value - # check the actual values - self.wait_for_text_to_equal('#{}-label'.format(chapter), value) - wait_for( - lambda: ( - self.driver.execute_script( - 'return document.' - 'getElementById("{}-graph").'.format(chapter) + - 'layout.title' - ) == value - ) - ) - self.request_queue_assertions() - - def chapter1_assertions(): - paths = self.driver.execute_script( - 'return window.store.getState().paths' - ) - self.assertEqual(paths, { - 'toc': ['props', 'children', 0], - 'body': ['props', 'children', 1], - 'chapter1-header': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 0 - ], - 'chapter1-controls': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 1 + 'relative_package_path': [ + 'react@16.2.0.production.min.js', + 'react-dom@16.2.0.production.min.js' ], - 'chapter1-label': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 2 - ], - 'chapter1-graph': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 3 - ] - }) - generic_chapter_assertions('chapter1') - - chapter1_assertions() - self.percy_snapshot(name='chapter-1') - - # switch chapters - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() - - # sleep just to make sure that no calls happen after our check - time.sleep(2) - self.percy_snapshot(name='chapter-2') - wait_for(lambda: call_counts['body'].value == 2) - wait_for(lambda: call_counts['chapter2-graph'].value == 1) - wait_for(lambda: call_counts['chapter2-label'].value == 1) - self.assertEqual(call_counts['chapter1-graph'].value, 1) - self.assertEqual(call_counts['chapter1-label'].value, 1) - - def chapter2_assertions(): - paths = self.driver.execute_script( - 'return window.store.getState().paths' - ) - self.assertEqual(paths, { - 'toc': ['props', 'children', 0], - 'body': ['props', 'children', 1], - 'chapter2-header': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 0 - ], - 'chapter2-controls': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 1 - ], - 'chapter2-label': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 2 - ], - 'chapter2-graph': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 3 - ] - }) - generic_chapter_assertions('chapter2') - - chapter2_assertions() - - # switch to 3 - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[2]).click() - # sleep just to make sure that no calls happen after our check - time.sleep(2) - self.percy_snapshot(name='chapter-3') - wait_for(lambda: call_counts['body'].value == 3) - wait_for(lambda: call_counts['chapter3-graph'].value == 1) - wait_for(lambda: call_counts['chapter3-label'].value == 1) - self.assertEqual(call_counts['chapter2-graph'].value, 1) - self.assertEqual(call_counts['chapter2-label'].value, 1) - self.assertEqual(call_counts['chapter1-graph'].value, 1) - self.assertEqual(call_counts['chapter1-label'].value, 1) - - def chapter3_assertions(): - paths = self.driver.execute_script( - 'return window.store.getState().paths' - ) - self.assertEqual(paths, { - 'toc': ['props', 'children', 0], - 'body': ['props', 'children', 1], - 'chapter3-header': [ - 'props', 'children', 1, - 'props', 'children', 0, - 'props', 'children', - 'props', 'children', 0 - ], - 'chapter3-label': [ - 'props', 'children', 1, - 'props', 'children', 0, - 'props', 'children', - 'props', 'children', 1 - ], - 'chapter3-graph': [ - 'props', 'children', 1, - 'props', 'children', 0, - 'props', 'children', - 'props', 'children', 2 - ], - 'chapter3-controls': [ - 'props', 'children', 1, - 'props', 'children', 0, - 'props', 'children', - 'props', 'children', 3 - ] - }) - generic_chapter_assertions('chapter3') - - chapter3_assertions() - - # switch to 4 - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[3]).click() - self.wait_for_text_to_equal('#body', 'Just a string') - self.percy_snapshot(name='chapter-4') - - # each element should exist in the dom - paths = self.driver.execute_script( - 'return window.store.getState().paths' - ) - for key in paths: - self.driver.find_element_by_id(key) - self.assertEqual(paths, { - 'toc': ['props', 'children', 0], - 'body': ['props', 'children', 1] - }) - - # switch back to 1 - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[0]).click() - time.sleep(0.5) - chapter1_assertions() - self.percy_snapshot(name='chapter-1-again') - - # switch to 5 - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[4]).click() - time.sleep(1) - # click on the button and check the output div before and after - chapter5_div = lambda: self.driver.find_element_by_id( - 'chapter5-output' - ) - chapter5_button = lambda: self.driver.find_element_by_id( - 'chapter5-button' - ) - self.assertEqual(chapter5_div().text, '') - chapter5_button().click() - wait_for(lambda: chapter5_div().text == chapter5_output_children) - time.sleep(0.5) - self.percy_snapshot(name='chapter-5') - self.assertEqual(call_counts['chapter5-output'].value, 1) - - def test_dependencies_on_components_that_dont_exist(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='input', value='initial value'), - html.Div(id='output-1') - ]) - - # standard callback - output_1_call_count = Value('i', 0) - - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - output_1_call_count.value += 1 - return value - - # callback for component that doesn't yet exist in the dom - # in practice, it might get added by some other callback - app.config.supress_callback_exceptions = True - output_2_call_count = Value('i', 0) - - @app.callback( - Output('output-2', 'children'), - [Input('input', 'value')] - ) - def update_output_2(value): - output_2_call_count.value += 1 - return value - - self.startServer(app) - - self.wait_for_text_to_equal('#output-1', 'initial value') - self.percy_snapshot(name='dependencies') - time.sleep(1.0) - self.assertEqual(output_1_call_count.value, 1) - self.assertEqual(output_2_call_count.value, 0) - - input = self.driver.find_element_by_id('input') - - input.send_keys('a') - self.wait_for_text_to_equal('#output-1', 'initial valuea') - time.sleep(1.0) - self.assertEqual(output_1_call_count.value, 2) - self.assertEqual(output_2_call_count.value, 0) - - self.request_queue_assertions(2) - - assert_clean_console(self) - - def test_events(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - events=[Event('button', 'click')]) - def update_output(): - call_count.value += 1 - return 'Click' - - self.startServer(app) - btn = self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') - self.assertEqual(call_count.value, 0) - self.assertEqual(output().text, '') - - btn.click() - wait_for(lambda: output().text == 'Click') - self.assertEqual(call_count.value, 1) - - def test_events_and_state(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - dcc.Input(value='Initial State', id='state'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - state=[State('state', 'value')], - events=[Event('button', 'click')]) - def update_output(value): - call_count.value += 1 - return value - - self.startServer(app) - btn = self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') - - self.assertEqual(call_count.value, 0) - self.assertEqual(output().text, '') + 'namespace': 'dash_renderer', + }]) - btn.click() - wait_for(lambda: output().text == 'Initial State') - self.assertEqual(call_count.value, 1) - - # Changing state shouldn't fire the callback - state = self.driver.find_element_by_id('state') - state.send_keys('x') - time.sleep(0.75) - self.assertEqual(output().text, 'Initial State') - self.assertEqual(call_count.value, 1) - - btn.click() - wait_for(lambda: output().text == 'Initial Statex') - self.assertEqual(call_count.value, 2) - - def test_events_state_and_inputs(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - dcc.Input(value='Initial Input', id='input'), - dcc.Input(value='Initial State', id='state'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - inputs=[Input('input', 'value')], - state=[State('state', 'value')], - events=[Event('button', 'click')]) - def update_output(input, state): - call_count.value += 1 - return 'input="{}", state="{}"'.format(input, state) - - self.startServer(app) - btn = lambda: self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') - input = lambda: self.driver.find_element_by_id('input') - state = lambda: self.driver.find_element_by_id('state') - - # callback gets called with initial input - self.assertEqual(call_count.value, 1) - self.assertEqual( - output().text, - 'input="Initial Input", state="Initial State"' - ) - - btn().click() - wait_for(lambda: call_count.value == 2) - self.assertEqual( - output().text, - 'input="Initial Input", state="Initial State"') - - input().send_keys('x') - wait_for(lambda: call_count.value == 3) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - state().send_keys('x') - time.sleep(0.75) - self.assertEqual(call_count.value, 3) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - btn().click() - wait_for(lambda: call_count.value == 4) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial Statex"') - - def test_state_and_inputs(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(value='Initial Input', id='input'), - dcc.Input(value='Initial State', id='state'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - inputs=[Input('input', 'value')], - state=[State('state', 'value')]) - def update_output(input, state): - call_count.value += 1 - return 'input="{}", state="{}"'.format(input, state) - - self.startServer(app) - output = lambda: self.driver.find_element_by_id('output') - input = lambda: self.driver.find_element_by_id('input') - state = lambda: self.driver.find_element_by_id('state') - - # callback gets called with initial input - self.assertEqual(call_count.value, 1) - self.assertEqual( - output().text, - 'input="Initial Input", state="Initial State"' - ) - - input().send_keys('x') - wait_for(lambda: call_count.value == 2) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - state().send_keys('x') - time.sleep(0.75) - self.assertEqual(call_count.value, 2) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - input().send_keys('y') - wait_for(lambda: call_count.value == 3) - self.assertEqual( - output().text, - 'input="Initial Inputxy", state="Initial Statex"') - - def test_event_creating_inputs(self): - app = Dash(__name__) - - ids = { - k: k for k in ['button', 'button-output', 'input', 'input-output'] - } - app.layout = html.Div([ - html.Button(id=ids['button']), - html.Div(id=ids['button-output']) - ]) - for script in dcc._js_dist: - script['namespace'] = 'dash_core_components' - app.scripts.append_script(script) - - app.config.supress_callback_exceptions = True - call_counts = { - ids['input-output']: Value('i', 0), - ids['button-output']: Value('i', 0) - } - - @app.callback( - Output(ids['button-output'], 'children'), - events=[Event(ids['button'], 'click')]) - def display(): - call_counts['button-output'].value += 1 - return html.Div([ - dcc.Input(id=ids['input'], value='initial state'), - html.Div(id=ids['input-output']) - ]) - - @app.callback( - Output(ids['input-output'], 'children'), - [Input(ids['input'], 'value')]) - def update_input(value): - call_counts['input-output'].value += 1 - return 'Input is equal to "{}"'.format(value) - - self.startServer(app) - time.sleep(1) - self.assertEqual(call_counts[ids['button-output']].value, 0) - self.assertEqual(call_counts[ids['input-output']].value, 0) - - btn = lambda: self.driver.find_element_by_id(ids['button']) - output = lambda: self.driver.find_element_by_id(ids['input-output']) - with self.assertRaises(Exception): - output() - - btn().click() - wait_for(lambda: call_counts[ids['input-output']].value == 1) - self.assertEqual(call_counts[ids['button-output']].value, 1) - self.assertEqual(output().text, 'Input is equal to "initial state"') - - def test_chained_dependencies_direct_lineage(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='input-1', value='input 1'), - dcc.Input(id='input-2'), - html.Div('test', id='output') - ]) - input1 = lambda: self.driver.find_element_by_id('input-1') - input2 = lambda: self.driver.find_element_by_id('input-2') - output = lambda: self.driver.find_element_by_id('output') - - call_counts = { - 'output': Value('i', 0), - 'input-2': Value('i', 0) - } - - @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) - def update_input(input1): - call_counts['input-2'].value += 1 - return '<<{}>>'.format(input1) - - @app.callback(Output('output', 'children'), [ - Input('input-1', 'value'), - Input('input-2', 'value') - ]) - def update_output(input1, input2): - call_counts['output'].value += 1 - return '{} + {}'.format(input1, input2) - - self.startServer(app) - - wait_for(lambda: call_counts['output'].value == 1) - wait_for(lambda: call_counts['input-2'].value == 1) - self.assertEqual(input1().get_attribute('value'), 'input 1') - self.assertEqual(input2().get_attribute('value'), '<>') - self.assertEqual(output().text, 'input 1 + <>') - - input1().send_keys('x') - wait_for(lambda: call_counts['output'].value == 2) - wait_for(lambda: call_counts['input-2'].value == 2) - self.assertEqual(input1().get_attribute('value'), 'input 1x') - self.assertEqual(input2().get_attribute('value'), '<>') - self.assertEqual(output().text, 'input 1x + <>') - - input2().send_keys('y') - wait_for(lambda: call_counts['output'].value == 3) - wait_for(lambda: call_counts['input-2'].value == 2) - self.assertEqual(input1().get_attribute('value'), 'input 1x') - self.assertEqual(input2().get_attribute('value'), '<>y') - self.assertEqual(output().text, 'input 1x + <>y') - - - def test_chained_dependencies_branched_lineage(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='grandparent', value='input 1'), - dcc.Input(id='parent-a'), - dcc.Input(id='parent-b'), - html.Div(id='child-a'), - html.Div(id='child-b') - ]) - grandparent = lambda: self.driver.find_element_by_id('grandparent') - parenta = lambda: self.driver.find_element_by_id('parent-a') - parentb = lambda: self.driver.find_element_by_id('parent-b') - childa = lambda: self.driver.find_element_by_id('child-a') - childb = lambda: self.driver.find_element_by_id('child-b') - - call_counts = { - 'parent-a': Value('i', 0), - 'parent-b': Value('i', 0), - 'child-a': Value('i', 0), - 'child-b': Value('i', 0) - } - - @app.callback(Output('parent-a', 'value'), - [Input('grandparent', 'value')]) - def update_parenta(value): - call_counts['parent-a'].value += 1 - return 'a: {}'.format(value) - - @app.callback(Output('parent-b', 'value'), - [Input('grandparent', 'value')]) - def update_parentb(value): - time.sleep(0.5) - call_counts['parent-b'].value += 1 - return 'b: {}'.format(value) - - @app.callback(Output('child-a', 'children'), - [Input('parent-a', 'value'), - Input('parent-b', 'value')]) - def update_childa(parenta_value, parentb_value): - time.sleep(1) - call_counts['child-a'].value += 1 - return '{} + {}'.format(parenta_value, parentb_value) - - @app.callback(Output('child-b', 'children'), - [Input('parent-a', 'value'), - Input('parent-b', 'value'), - Input('grandparent', 'value')]) - def update_childb(parenta_value, parentb_value, grandparent_value): - call_counts['child-b'].value += 1 - return '{} + {} + {}'.format( - parenta_value, - parentb_value, - grandparent_value - ) - - self.startServer(app) - - wait_for(lambda: childa().text == 'a: input 1 + b: input 1') - wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') - time.sleep(1) # wait for potential requests of app to settle down - self.assertEqual(parenta().get_attribute('value'), 'a: input 1') - self.assertEqual(parentb().get_attribute('value'), 'b: input 1') - self.assertEqual(call_counts['parent-a'].value, 1) - self.assertEqual(call_counts['parent-b'].value, 1) - self.assertEqual(call_counts['child-a'].value, 1) - self.assertEqual(call_counts['child-b'].value, 1) - - def test_removing_component_while_its_getting_updated(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.RadioItems( - id='toc', - options=[ - {'label': i, 'value': i} for i in ['1', '2'] - ], - value='1' - ), - html.Div(id='body') - ]) - app.config.supress_callback_exceptions = True - - call_counts = { - 'body': Value('i', 0), - 'button-output': Value('i', 0) - } - - @app.callback(Output('body', 'children'), [Input('toc', 'value')]) - def update_body(chapter): - call_counts['body'].value += 1 - if chapter == '1': - return [ - html.Div('Chapter 1'), - html.Button( - 'clicking this button takes forever', - id='button' - ), - html.Div(id='button-output') - ] - elif chapter == '2': - return 'Chapter 2' - else: - raise Exception('chapter is {}'.format(chapter)) - - @app.callback( - Output('button-output', 'children'), - events=[Event('button', 'click')]) - def this_callback_takes_forever(): - time.sleep(5) - call_counts['button-output'].value += 1 - return 'New value!' - - body = lambda: self.driver.find_element_by_id('body') - self.startServer(app) - - wait_for(lambda: call_counts['body'].value == 1) - time.sleep(0.5) - self.driver.find_element_by_id('button').click() - - # while that callback is resolving, switch the chapter, - # hiding the `button-output` tag - def chapter2_assertions(): - wait_for(lambda: body().text == 'Chapter 2') - self.assertEqual( - self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().layout' - '))' - ), - { - "namespace": "dash_html_components", - "type": "Div", - "props": { - "children": [ - { - "namespace": "dash_core_components", - "type": "RadioItems", - "props": { - "value": "2", - "options": app.layout['toc'].options, - "id": app.layout['toc'].id, - } - }, - { - "namespace": "dash_html_components", - "type": "Div", - "props": { - "id": "body", - "children": "Chapter 2" - } - } - ] - } - } - ) - self.assertEqual( - self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().paths' - '))' - ), - { - "toc": ["props", "children", 0], - "body": ["props", "children", 1] - } - ) - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() - chapter2_assertions() - time.sleep(5) - wait_for(lambda: call_counts['button-output'].value == 1) - time.sleep(2) # liberally wait for the front-end to process request - chapter2_assertions() - assert_clean_console(self) - - def test_rendering_layout_calls_callback_once_per_output(self): - app = Dash(__name__) - call_count = Value('i', 0) - - app.config['suppress_callback_exceptions'] = True - app.layout = html.Div([ - html.Div([ - dcc.Input( - value='Input {}'.format(i), - id='input-{}'.format(i) - ) - for i in range(10) - ]), - html.Div(id='container'), - dcc.RadioItems() - ]) - - @app.callback( - Output('container', 'children'), - [Input('input-{}'.format(i), 'value') for i in range(10)]) - def dynamic_output(*args): - call_count.value += 1 - return json.dumps(args, indent=2) - - self.startServer(app) - - time.sleep(5) - - self.percy_snapshot( - name='test_rendering_layout_calls_callback_once_per_output' - ) - - self.assertEqual(call_count.value, 1) - - def test_rendering_new_content_calls_callback_once_per_output(self): - app = Dash(__name__) - call_count = Value('i', 0) - - app.config['suppress_callback_exceptions'] = True - app.layout = html.Div([ - html.Button( - id='display-content', - children='Display Content', - n_clicks=0 - ), - html.Div(id='container'), - dcc.RadioItems() - ]) - - @app.callback( - Output('container', 'children'), - [Input('display-content', 'n_clicks')]) - def display_output(n_clicks): - if n_clicks == 0: - return '' - return html.Div([ - html.Div([ - dcc.Input( - value='Input {}'.format(i), - id='input-{}'.format(i) - ) - for i in range(10) - ]), - html.Div(id='dynamic-output') - ]) - - @app.callback( - Output('dynamic-output', 'children'), - [Input('input-{}'.format(i), 'value') for i in range(10)]) - def dynamic_output(*args): - call_count.value += 1 - return json.dumps(args, indent=2) - - self.startServer(app) - - self.wait_for_element_by_css_selector('#display-content').click() - - time.sleep(5) - - self.percy_snapshot( - name='test_rendering_new_content_calls_callback_once_per_output' - ) - - self.assertEqual(call_count.value, 1) - - def test_callbacks_called_multiple_times_and_out_of_order(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button(id='input', n_clicks=0), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback( - Output('output', 'children'), - [Input('input', 'n_clicks')]) - def update_output(n_clicks): - call_count.value = call_count.value + 1 - if n_clicks == 1: - time.sleep(4) - return n_clicks - - self.startServer(app) - button = self.wait_for_element_by_css_selector('#input') - button.click() - button.click() - time.sleep(8) - self.percy_snapshot( - name='test_callbacks_called_multiple_times_and_out_of_order' - ) - self.assertEqual(call_count.value, 3) - self.assertEqual( - self.driver.find_element_by_id('output').text, - '2' - ) - request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' - ) - self.assertFalse(request_queue[0]['rejected']) - self.assertEqual(len(request_queue), 1) - - - def test_callbacks_with_shared_grandparent(self): - app = dash.Dash() - - app.layout = html.Div([ - html.Div(id='session-id', children='id'), - dcc.Dropdown(id='dropdown-1'), - dcc.Dropdown(id='dropdown-2'), - ]) - - options = [{'value': 'a', 'label': 'a'}] - - call_counts = { - 'dropdown_1': Value('i', 0), - 'dropdown_2': Value('i', 0) - } - - @app.callback( - Output('dropdown-1', 'options'), - [Input('dropdown-1', 'value'), - Input('session-id', 'children')]) - def dropdown_1(value, session_id): - call_counts['dropdown_1'].value += 1 - return options - - @app.callback( - Output('dropdown-2', 'options'), - [Input('dropdown-2', 'value'), - Input('session-id', 'children')]) - def dropdown_2(value, session_id): - call_counts['dropdown_2'].value += 1 - return options - - self.startServer(app) - - self.wait_for_element_by_css_selector('#session-id') - time.sleep(2) - self.assertEqual(call_counts['dropdown_1'].value, 1) - self.assertEqual(call_counts['dropdown_2'].value, 1) - - assert_clean_console(self) - - def test_callbacks_triggered_on_generated_output(self): - app = dash.Dash() - app.config['suppress_callback_exceptions'] = True - - call_counts = { - 'tab1': Value('i', 0), - 'tab2': Value('i', 0) - } - - app.layout = html.Div([ - dcc.Dropdown( - id='outer-controls', - options=[{'label': i, 'value': i} for i in ['a', 'b']], - value='a' - ), - dcc.RadioItems( - options=[ - {'label': 'Tab 1', 'value': 1}, - {'label': 'Tab 2', 'value': 2} - ], - value=1, - id='tabs', - ), - html.Div(id='tab-output') - ]) - - @app.callback(Output('tab-output', 'children'), - [Input('tabs', 'value')]) - def display_content(value): - return html.Div([ - html.Div(id='tab-{}-output'.format(value)) - ]) - - @app.callback(Output('tab-1-output', 'children'), - [Input('outer-controls', 'value')]) - def display_tab1_output(value): - call_counts['tab1'].value += 1 - return 'You have selected "{}"'.format(value) - - @app.callback(Output('tab-2-output', 'children'), - [Input('outer-controls', 'value')]) - def display_tab2_output(value): - call_counts['tab2'].value += 1 - return 'You have selected "{}"'.format(value) - - self.startServer(app) - self.wait_for_element_by_css_selector('#tab-output') - time.sleep(2) - - self.assertEqual(call_counts['tab1'].value, 1) - self.assertEqual(call_counts['tab2'].value, 0) - self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') - self.wait_for_text_to_equal('#tab-1-output', 'You have selected "a"') - - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() - time.sleep(2) - - self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') - self.wait_for_text_to_equal('#tab-2-output', 'You have selected "a"') - self.assertEqual(call_counts['tab1'].value, 1) - self.assertEqual(call_counts['tab2'].value, 1) - - assert_clean_console(self) - - def test_initialization_with_overlapping_outputs(self): - app = dash.Dash() - app.layout = html.Div([ - - html.Div(id='input-1', children='input-1'), - html.Div(id='input-2', children='input-2'), - html.Div(id='input-3', children='input-3'), - html.Div(id='input-4', children='input-4'), - html.Div(id='input-5', children='input-5'), - - html.Div(id='output-1'), - html.Div(id='output-2'), - html.Div(id='output-3'), - html.Div(id='output-4'), - - ]) - call_counts = { - 'output-1': Value('i', 0), - 'output-2': Value('i', 0), - 'output-3': Value('i', 0), - 'output-4': Value('i', 0), - } - - def generate_callback(outputid): - def callback(*args): - call_counts[outputid].value += 1 - return '{}, {}'.format(*args) - return callback - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - app.callback( - Output(outputid, 'children'), - [Input('input-{}'.format(i), 'children'), - Input('input-{}'.format(i+1), 'children')] - )(generate_callback(outputid)) - - self.startServer(app) - - self.wait_for_element_by_css_selector('#output-1') - time.sleep(5) - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - self.assertEqual(call_counts[outputid].value, 1) - self.wait_for_text_to_equal( - '#{}'.format(outputid), - "input-{}, input-{}".format(i, i+1) - ) - - def test_generate_overlapping_outputs(self): app = dash.Dash() - app.config['suppress_callback_exceptions'] = True - block = html.Div([ - - html.Div(id='input-1', children='input-1'), - html.Div(id='input-2', children='input-2'), - html.Div(id='input-3', children='input-3'), - html.Div(id='input-4', children='input-4'), - html.Div(id='input-5', children='input-5'), - - html.Div(id='output-1'), - html.Div(id='output-2'), - html.Div(id='output-3'), - html.Div(id='output-4'), - - ]) - app.layout = html.Div([ - html.Div(id='input'), - html.Div(id='container') - ]) - - call_counts = { - 'container': Value('i', 0), - 'output-1': Value('i', 0), - 'output-2': Value('i', 0), - 'output-3': Value('i', 0), - 'output-4': Value('i', 0), - } - @app.callback(Output('container', 'children'), - [Input('input', 'children')]) - def display_output(*args): - call_counts['container'].value += 1 - return block + # Create a dummy component with no props + # (dash-html-components may not support tested React version) + class TestComponent(Component): + def __init__(self, _namespace): + self._prop_names = [] + self._namespace = _namespace + super(TestComponent, self).__init__() - def generate_callback(outputid): - def callback(*args): - call_counts[outputid].value += 1 - return '{}, {}'.format(*args) - return callback - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - app.callback( - Output(outputid, 'children'), - [Input('input-{}'.format(i), 'children'), - Input('input-{}'.format(i+1), 'children')] - )(generate_callback(outputid)) + _test_component = TestComponent(_namespace='test-namespace') + app.layout = _test_component self.startServer(app) - wait_for(lambda: call_counts['container'].value == 1) - self.wait_for_element_by_css_selector('#output-1') - time.sleep(5) - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - self.assertEqual(call_counts[outputid].value, 1) - self.wait_for_text_to_equal( - '#{}'.format(outputid), - "input-{}, input-{}".format(i, i+1) - ) - self.assertEqual(call_counts['container'].value, 1) + # Make sure that the Dash application is generating the right React scripts + self.assertEqual( + app._generate_scripts_html(), + '\n' + '\n' + ''.format( + dash_renderer.__version__)) + + # Reset react version + dash_renderer._set_react_version(dash_renderer._DEFAULT_REACT_VERSION) From 2ca1467330b36676bc6ef036234fb299a29659b7 Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 17 Feb 2018 09:34:11 -0500 Subject: [PATCH 05/10] Uncomment tests --- tests/test_render.py | 3422 +++++++++++++++++++++--------------------- 1 file changed, 1712 insertions(+), 1710 deletions(-) diff --git a/tests/test_render.py b/tests/test_render.py index 3a044d8..b3f6a0d 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -67,1712 +67,1712 @@ def request_queue_assertions( if expected_length is not None: self.assertEqual(len(request_queue), expected_length) - # def test_initial_state(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # 'Basic string', - # 3.14, - # None, - # html.Div('Child div with basic string', - # id='p.c.3', - # className="my-class", - # title='tooltip', - # style={'color': 'red', 'fontSize': 30} - # ), - # html.Div(id='p.c.4'), - # html.Div([ - # html.Div('Grandchild div', id='p.c.5.p.c.0'), - # html.Div([ - # html.Div('Great grandchild', id='p.c.5.p.c.1.p.c.0'), - # 3.14159, - # 'another basic string' - # ], id='p.c.5.p.c.1'), - # html.Div([ - # html.Div( - # html.Div([ - # html.Div([ - # html.Div( - # id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0' - # ), - # '', - # html.Div( - # id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2' - # ) - # ], id='p.c.5.p.c.2.p.c.0.p.c.p.c.0') - # ], id='p.c.5.p.c.2.p.c.0.p.c'), - # id='p.c.5.p.c.2.p.c.0' - # ) - # ], id='p.c.5.p.c.2') - # ], id='p.c.5') - # ]) - # - # self.startServer(app) - # - # el = self.wait_for_element_by_css_selector('#_dash-app-content') - # - # # TODO - Make less fragile with http://lxml.de/lxmlhtml.html#html-diff - # rendered_dom = ''' - #
- # Basic string - # - # 3.14 - # - #
- # Child div with basic string - #
- # - #
- #
- # - #
- #
- # Grandchild div - #
- # - #
- #
- # Great grandchild - #
- # - # 3.14159 - # - # another basic string - #
- # - #
- #
- #
- #
- # - #
- #
- # - # - #
- #
- # - #
- #
- #
- #
- #
- # - #
- # ''' - # # React wraps text and numbers with e.g. - # # Remove those - # comment_regex = '' - # - # # Somehow the html attributes are unordered. - # # Try different combinations (they're all valid html) - # style_permutations = [ - # 'style="color: red; font-size: 30px;"', - # 'style="font-size: 30px; color: red;"' - # ] - # permutations = itertools.permutations([ - # 'id="p.c.3"', - # 'class="my-class"', - # 'title="tooltip"', - # ], 3) - # passed = False - # for permutation in permutations: - # for style in style_permutations: - # actual_cleaned = re.sub(comment_regex, '', el.get_attribute('innerHTML')) - # expected_cleaned = re.sub( - # comment_regex, - # '', - # rendered_dom.replace('\n', '') - # .replace(' ', '') - # .replace('PERMUTE', ' '.join(list(permutation) + [style])) - # ) - # passed = passed or (actual_cleaned == expected_cleaned) - # if not passed: - # raise Exception( - # 'HTML does not match\nActual:\n{}\n\nExpected:\n{}'.format( - # actual_cleaned, - # expected_cleaned - # ) - # ) - # - # # Check that no errors or warnings were displayed - # self.assertEqual( - # self.driver.execute_script( - # 'return window.tests.console.error.length' - # ), - # 0 - # ) - # self.assertEqual( - # self.driver.execute_script( - # 'return window.tests.console.warn.length' - # ), - # 0 - # ) - # - # # Check the initial stores - # - # # layout should just be the JSON-ified app.layout - # self.assertEqual( - # self.driver.execute_script( - # 'return JSON.parse(JSON.stringify(' - # 'window.store.getState().layout' - # '))' - # ), - # { - # "namespace": "dash_html_components", - # "props": { - # "children": [ - # "Basic string", - # 3.14, - # None, - # { - # "namespace": "dash_html_components", - # "props": { - # "children": "Child div with basic string", - # "id": "p.c.3", - # 'className': "my-class", - # 'title': 'tooltip', - # 'style': { - # 'color': 'red', 'fontSize': 30 - # } - # }, - # "type": "Div" - # }, - # { - # "namespace": "dash_html_components", - # "props": { - # "children": None, - # "id": "p.c.4" - # }, - # "type": "Div" - # }, - # { - # "namespace": "dash_html_components", - # "props": { - # "children": [ - # { - # "namespace": "dash_html_components", - # "props": { - # "children": "Grandchild div", - # "id": "p.c.5.p.c.0" - # }, - # "type": "Div" - # }, - # { - # "namespace": "dash_html_components", - # "props": { - # "children": [ - # { - # "namespace": "dash_html_components", - # "props": { - # "children": "Great grandchild", - # "id": "p.c.5.p.c.1.p.c.0" - # }, - # "type": "Div" - # }, - # 3.14159, - # "another basic string" - # ], - # "id": "p.c.5.p.c.1" - # }, - # "type": "Div" - # }, - # { - # "namespace": "dash_html_components", - # "props": { - # "children": [ - # { - # "namespace": "dash_html_components", - # "props": { - # "children": { - # "namespace": "dash_html_components", - # "props": { - # "children": [ - # { - # "namespace": "dash_html_components", - # "props": { - # "children": [ - # { - # "namespace": "dash_html_components", - # "props": { - # "children": None, - # "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0" - # }, - # "type": "Div" - # }, - # "", - # { - # "namespace": "dash_html_components", - # "props": { - # "children": None, - # "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2" - # }, - # "type": "Div" - # } - # ], - # "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0" - # }, - # "type": "Div" - # } - # ], - # "id": "p.c.5.p.c.2.p.c.0.p.c" - # }, - # "type": "Div" - # }, - # "id": "p.c.5.p.c.2.p.c.0" - # }, - # "type": "Div" - # } - # ], - # "id": "p.c.5.p.c.2" - # }, - # "type": "Div" - # } - # ], - # "id": "p.c.5" - # }, - # "type": "Div" - # } - # ] - # }, - # "type": "Div" - # } - # ) - # - # # graphs should just be empty since there are no dependencies - # self.assertEqual( - # self.driver.execute_script( - # 'return JSON.parse(JSON.stringify(' - # 'window.store.getState().graphs' - # '))' - # ), - # { - # "InputGraph": { - # "nodes": {}, - # "outgoingEdges": {}, - # "incomingEdges": {} - # }, - # "EventGraph": { - # "nodes": {}, - # "outgoingEdges": {}, - # "incomingEdges": {} - # } - # } - # ) - # - # # paths is just a lookup table of the components's IDs and their - # # placement in the tree. - # # in this case the IDs are just abbreviations of the path to make - # # things easy to verify. - # self.assertEqual( - # self.driver.execute_script( - # 'return window.store.getState().paths' - # ), - # { - # "p.c.3": [ - # "props", "children", 3 - # ], - # "p.c.4": [ - # "props", "children", 4 - # ], - # "p.c.5": [ - # "props", "children", 5 - # ], - # "p.c.5.p.c.0": [ - # "props", "children", 5, - # "props", "children", 0 - # ], - # "p.c.5.p.c.1": [ - # "props", "children", 5, - # "props", "children", 1 - # ], - # "p.c.5.p.c.1.p.c.0": [ - # "props", "children", 5, - # "props", "children", 1, - # "props", "children", 0 - # ], - # "p.c.5.p.c.2": [ - # "props", "children", 5, - # "props", "children", 2 - # ], - # "p.c.5.p.c.2.p.c.0": [ - # "props", "children", 5, - # "props", "children", 2, - # "props", "children", 0 - # ], - # "p.c.5.p.c.2.p.c.0.p.c": [ - # "props", "children", 5, - # "props", "children", 2, - # "props", "children", 0, - # "props", "children" - # ], - # "p.c.5.p.c.2.p.c.0.p.c.p.c.0": [ - # "props", "children", 5, - # "props", "children", 2, - # "props", "children", 0, - # "props", "children", - # "props", "children", 0 - # ], - # "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0": [ - # "props", "children", 5, - # "props", "children", 2, - # "props", "children", 0, - # "props", "children", - # "props", "children", 0, - # "props", "children", 0 - # ], - # "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2": [ - # "props", "children", 5, - # "props", "children", 2, - # "props", "children", 0, - # "props", "children", - # "props", "children", 0, - # "props", "children", 2 - # ] - # } - # ) - # - # self.request_queue_assertions(0) - # - # self.percy_snapshot(name='layout') - # - # assert_clean_console(self) - # - # def test_simple_callback(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input( - # id='input', - # value='initial value' - # ), - # html.Div( - # html.Div([ - # 1.5, - # None, - # 'string', - # html.Div(id='output-1') - # ]) - # ) - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - # def update_output(value): - # call_count.value = call_count.value + 1 - # return value - # - # self.startServer(app) - # - # self.wait_for_text_to_equal('#output-1', 'initial value') - # self.percy_snapshot(name='simple-callback-1') - # - # input1 = self.wait_for_element_by_css_selector('#input') - # input1.clear() - # - # input1.send_keys('hello world') - # - # self.wait_for_text_to_equal('#output-1', 'hello world') - # self.percy_snapshot(name='simple-callback-2') - # - # self.assertEqual( - # call_count.value, - # # an initial call to retrieve the first value - # 1 + - # # one for each hello world character - # len('hello world') - # ) - # - # self.request_queue_assertions( - # expected_length=1, - # check_rejected=False) - # - # assert_clean_console(self) - # - # def test_callbacks_generating_children(self): - # """ Modify the DOM tree by adding new - # components in the callbacks - # """ - # - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input( - # id='input', - # value='initial value' - # ), - # html.Div(id='output') - # ]) - # - # @app.callback(Output('output', 'children'), [Input('input', 'value')]) - # def pad_output(input): - # return html.Div([ - # dcc.Input( - # id='sub-input-1', - # value='sub input initial value' - # ), - # html.Div(id='sub-output-1') - # ]) - # - # call_count = Value('i', 0) - # - # # these components don't exist in the initial render - # app.config.supress_callback_exceptions = True - # - # @app.callback( - # Output('sub-output-1', 'children'), - # [Input('sub-input-1', 'value')] - # ) - # def update_input(value): - # call_count.value = call_count.value + 1 - # return value - # - # self.startServer(app) - # - # output = self.driver.find_element_by_id('output') - # output_html = output.get_attribute('innerHTML') - # - # wait_for(lambda: call_count.value == 1) - # - # # Adding new children to the layout should - # # call the callbacks immediately to set - # # the correct initial state - # wait_for( - # lambda: ( - # self.driver.find_element_by_id('output') - # .get_attribute('innerHTML') in [''' - #
- # {} - #
- # sub input initial value - #
- #
'''.replace('\n', '').replace(' ', '').format(input) - # for input in [ - # # html attributes are unordered, so include both versions - # '', - # '' - # ] - # ] - # ) - # ) - # self.percy_snapshot(name='callback-generating-function-1') - # - # # the paths should include these new output IDs - # self.assertEqual( - # self.driver.execute_script('return window.store.getState().paths'), - # { - # 'input': [ - # 'props', 'children', 0 - # ], - # 'output': ['props', 'children', 1], - # 'sub-input-1': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 0 - # ], - # 'sub-output-1': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 1 - # ] - # } - # ) - # - # # editing the input should modify the sub output - # sub_input = self.driver.find_element_by_id('sub-input-1') - # sub_input.send_keys('a') - # self.wait_for_text_to_equal( - # '#sub-output-1', - # 'sub input initial valuea') - # - # self.assertEqual(call_count.value, 2) - # - # self.request_queue_assertions(call_count.value + 1) - # self.percy_snapshot(name='callback-generating-function-2') - # - # assert_clean_console(self) - # - # def test_radio_buttons_callbacks_generating_children(self): - # self.maxDiff = 100 * 1000 - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.RadioItems( - # options=[ - # {'label': 'Chapter 1', 'value': 'chapter1'}, - # {'label': 'Chapter 2', 'value': 'chapter2'}, - # {'label': 'Chapter 3', 'value': 'chapter3'}, - # {'label': 'Chapter 4', 'value': 'chapter4'}, - # {'label': 'Chapter 5', 'value': 'chapter5'} - # ], - # value='chapter1', - # id='toc' - # ), - # html.Div(id='body') - # ]) - # for script in dcc._js_dist: - # app.scripts.append_script(script) - # - # chapters = { - # 'chapter1': html.Div([ - # html.H1('Chapter 1', id='chapter1-header'), - # dcc.Dropdown( - # options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'SF']], - # value='NYC', - # id='chapter1-controls' - # ), - # html.Label(id='chapter1-label'), - # dcc.Graph(id='chapter1-graph') - # ]), - # # Chapter 2 has the some of the same components in the same order - # # as Chapter 1. This means that they won't get remounted - # # unless they set their own keys are differently. - # # Switching back and forth between 1 and 2 implicitly - # # tests how components update when they aren't remounted. - # 'chapter2': html.Div([ - # html.H1('Chapter 2', id='chapter2-header'), - # dcc.RadioItems( - # options=[{'label': i, 'value': i} - # for i in ['USA', 'Canada']], - # value='USA', - # id='chapter2-controls' - # ), - # html.Label(id='chapter2-label'), - # dcc.Graph(id='chapter2-graph') - # ]), - # # Chapter 3 has a different layout and so the components - # # should get rewritten - # 'chapter3': [html.Div( - # html.Div([ - # html.H3('Chapter 3', id='chapter3-header'), - # html.Label(id='chapter3-label'), - # dcc.Graph(id='chapter3-graph'), - # dcc.RadioItems( - # options=[{'label': i, 'value': i} - # for i in ['Summer', 'Winter']], - # value='Winter', - # id='chapter3-controls' - # ) - # ]) - # )], - # - # # Chapter 4 doesn't have an object to recursively - # # traverse - # 'chapter4': 'Just a string', - # - # # Chapter 5 contains elements that are bound with events - # 'chapter5': [html.Div([ - # html.Button(id='chapter5-button'), - # html.Div(id='chapter5-output') - # ])] - # } - # - # call_counts = { - # 'body': Value('i', 0), - # 'chapter1-graph': Value('i', 0), - # 'chapter1-label': Value('i', 0), - # 'chapter2-graph': Value('i', 0), - # 'chapter2-label': Value('i', 0), - # 'chapter3-graph': Value('i', 0), - # 'chapter3-label': Value('i', 0), - # 'chapter5-output': Value('i', 0) - # } - # - # @app.callback(Output('body', 'children'), [Input('toc', 'value')]) - # def display_chapter(toc_value): - # call_counts['body'].value += 1 - # return chapters[toc_value] - # - # app.config.supress_callback_exceptions = True - # - # def generate_graph_callback(counterId): - # def callback(value): - # call_counts[counterId].value += 1 - # return { - # 'data': [{ - # 'x': ['Call Counter'], - # 'y': [call_counts[counterId].value], - # 'type': 'bar' - # }], - # 'layout': {'title': value} - # } - # return callback - # - # def generate_label_callback(id): - # def update_label(value): - # call_counts[id].value += 1 - # return value - # return update_label - # - # for chapter in ['chapter1', 'chapter2', 'chapter3']: - # app.callback( - # Output('{}-graph'.format(chapter), 'figure'), - # [Input('{}-controls'.format(chapter), 'value')] - # )(generate_graph_callback('{}-graph'.format(chapter))) - # - # app.callback( - # Output('{}-label'.format(chapter), 'children'), - # [Input('{}-controls'.format(chapter), 'value')] - # )(generate_label_callback('{}-label'.format(chapter))) - # - # chapter5_output_children = 'Button clicked' - # - # @app.callback(Output('chapter5-output', 'children'), - # events=[Event('chapter5-button', 'click')]) - # def display_output(): - # call_counts['chapter5-output'].value += 1 - # return chapter5_output_children - # - # self.startServer(app) - # - # time.sleep(0.5) - # wait_for(lambda: call_counts['body'].value == 1) - # wait_for(lambda: call_counts['chapter1-graph'].value == 1) - # wait_for(lambda: call_counts['chapter1-label'].value == 1) - # self.assertEqual(call_counts['chapter2-graph'].value, 0) - # self.assertEqual(call_counts['chapter2-label'].value, 0) - # self.assertEqual(call_counts['chapter3-graph'].value, 0) - # self.assertEqual(call_counts['chapter3-label'].value, 0) - # - # def generic_chapter_assertions(chapter): - # # each element should exist in the dom - # paths = self.driver.execute_script( - # 'return window.store.getState().paths' - # ) - # for key in paths: - # self.driver.find_element_by_id(key) - # - # if chapter == 'chapter3': - # value = chapters[chapter][0][ - # '{}-controls'.format(chapter) - # ].value - # else: - # value = chapters[chapter]['{}-controls'.format(chapter)].value - # # check the actual values - # self.wait_for_text_to_equal('#{}-label'.format(chapter), value) - # wait_for( - # lambda: ( - # self.driver.execute_script( - # 'return document.' - # 'getElementById("{}-graph").'.format(chapter) + - # 'layout.title' - # ) == value - # ) - # ) - # self.request_queue_assertions() - # - # def chapter1_assertions(): - # paths = self.driver.execute_script( - # 'return window.store.getState().paths' - # ) - # self.assertEqual(paths, { - # 'toc': ['props', 'children', 0], - # 'body': ['props', 'children', 1], - # 'chapter1-header': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 0 - # ], - # 'chapter1-controls': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 1 - # ], - # 'chapter1-label': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 2 - # ], - # 'chapter1-graph': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 3 - # ] - # }) - # generic_chapter_assertions('chapter1') - # - # chapter1_assertions() - # self.percy_snapshot(name='chapter-1') - # - # # switch chapters - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[1]).click() - # - # # sleep just to make sure that no calls happen after our check - # time.sleep(2) - # self.percy_snapshot(name='chapter-2') - # wait_for(lambda: call_counts['body'].value == 2) - # wait_for(lambda: call_counts['chapter2-graph'].value == 1) - # wait_for(lambda: call_counts['chapter2-label'].value == 1) - # self.assertEqual(call_counts['chapter1-graph'].value, 1) - # self.assertEqual(call_counts['chapter1-label'].value, 1) - # - # def chapter2_assertions(): - # paths = self.driver.execute_script( - # 'return window.store.getState().paths' - # ) - # self.assertEqual(paths, { - # 'toc': ['props', 'children', 0], - # 'body': ['props', 'children', 1], - # 'chapter2-header': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 0 - # ], - # 'chapter2-controls': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 1 - # ], - # 'chapter2-label': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 2 - # ], - # 'chapter2-graph': [ - # 'props', 'children', 1, - # 'props', 'children', - # 'props', 'children', 3 - # ] - # }) - # generic_chapter_assertions('chapter2') - # - # chapter2_assertions() - # - # # switch to 3 - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[2]).click() - # # sleep just to make sure that no calls happen after our check - # time.sleep(2) - # self.percy_snapshot(name='chapter-3') - # wait_for(lambda: call_counts['body'].value == 3) - # wait_for(lambda: call_counts['chapter3-graph'].value == 1) - # wait_for(lambda: call_counts['chapter3-label'].value == 1) - # self.assertEqual(call_counts['chapter2-graph'].value, 1) - # self.assertEqual(call_counts['chapter2-label'].value, 1) - # self.assertEqual(call_counts['chapter1-graph'].value, 1) - # self.assertEqual(call_counts['chapter1-label'].value, 1) - # - # def chapter3_assertions(): - # paths = self.driver.execute_script( - # 'return window.store.getState().paths' - # ) - # self.assertEqual(paths, { - # 'toc': ['props', 'children', 0], - # 'body': ['props', 'children', 1], - # 'chapter3-header': [ - # 'props', 'children', 1, - # 'props', 'children', 0, - # 'props', 'children', - # 'props', 'children', 0 - # ], - # 'chapter3-label': [ - # 'props', 'children', 1, - # 'props', 'children', 0, - # 'props', 'children', - # 'props', 'children', 1 - # ], - # 'chapter3-graph': [ - # 'props', 'children', 1, - # 'props', 'children', 0, - # 'props', 'children', - # 'props', 'children', 2 - # ], - # 'chapter3-controls': [ - # 'props', 'children', 1, - # 'props', 'children', 0, - # 'props', 'children', - # 'props', 'children', 3 - # ] - # }) - # generic_chapter_assertions('chapter3') - # - # chapter3_assertions() - # - # # switch to 4 - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[3]).click() - # self.wait_for_text_to_equal('#body', 'Just a string') - # self.percy_snapshot(name='chapter-4') - # - # # each element should exist in the dom - # paths = self.driver.execute_script( - # 'return window.store.getState().paths' - # ) - # for key in paths: - # self.driver.find_element_by_id(key) - # self.assertEqual(paths, { - # 'toc': ['props', 'children', 0], - # 'body': ['props', 'children', 1] - # }) - # - # # switch back to 1 - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[0]).click() - # time.sleep(0.5) - # chapter1_assertions() - # self.percy_snapshot(name='chapter-1-again') - # - # # switch to 5 - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[4]).click() - # time.sleep(1) - # # click on the button and check the output div before and after - # chapter5_div = lambda: self.driver.find_element_by_id( - # 'chapter5-output' - # ) - # chapter5_button = lambda: self.driver.find_element_by_id( - # 'chapter5-button' - # ) - # self.assertEqual(chapter5_div().text, '') - # chapter5_button().click() - # wait_for(lambda: chapter5_div().text == chapter5_output_children) - # time.sleep(0.5) - # self.percy_snapshot(name='chapter-5') - # self.assertEqual(call_counts['chapter5-output'].value, 1) - # - # def test_dependencies_on_components_that_dont_exist(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input(id='input', value='initial value'), - # html.Div(id='output-1') - # ]) - # - # # standard callback - # output_1_call_count = Value('i', 0) - # - # @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - # def update_output(value): - # output_1_call_count.value += 1 - # return value - # - # # callback for component that doesn't yet exist in the dom - # # in practice, it might get added by some other callback - # app.config.supress_callback_exceptions = True - # output_2_call_count = Value('i', 0) - # - # @app.callback( - # Output('output-2', 'children'), - # [Input('input', 'value')] - # ) - # def update_output_2(value): - # output_2_call_count.value += 1 - # return value - # - # self.startServer(app) - # - # self.wait_for_text_to_equal('#output-1', 'initial value') - # self.percy_snapshot(name='dependencies') - # time.sleep(1.0) - # self.assertEqual(output_1_call_count.value, 1) - # self.assertEqual(output_2_call_count.value, 0) - # - # input = self.driver.find_element_by_id('input') - # - # input.send_keys('a') - # self.wait_for_text_to_equal('#output-1', 'initial valuea') - # time.sleep(1.0) - # self.assertEqual(output_1_call_count.value, 2) - # self.assertEqual(output_2_call_count.value, 0) - # - # self.request_queue_assertions(2) - # - # assert_clean_console(self) - # - # def test_events(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # html.Button('Click Me', id='button'), - # html.Div(id='output') - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback(Output('output', 'children'), - # events=[Event('button', 'click')]) - # def update_output(): - # call_count.value += 1 - # return 'Click' - # - # self.startServer(app) - # btn = self.driver.find_element_by_id('button') - # output = lambda: self.driver.find_element_by_id('output') - # self.assertEqual(call_count.value, 0) - # self.assertEqual(output().text, '') - # - # btn.click() - # wait_for(lambda: output().text == 'Click') - # self.assertEqual(call_count.value, 1) - # - # def test_events_and_state(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # html.Button('Click Me', id='button'), - # dcc.Input(value='Initial State', id='state'), - # html.Div(id='output') - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback(Output('output', 'children'), - # state=[State('state', 'value')], - # events=[Event('button', 'click')]) - # def update_output(value): - # call_count.value += 1 - # return value - # - # self.startServer(app) - # btn = self.driver.find_element_by_id('button') - # output = lambda: self.driver.find_element_by_id('output') - # - # self.assertEqual(call_count.value, 0) - # self.assertEqual(output().text, '') - # - # btn.click() - # wait_for(lambda: output().text == 'Initial State') - # self.assertEqual(call_count.value, 1) - # - # # Changing state shouldn't fire the callback - # state = self.driver.find_element_by_id('state') - # state.send_keys('x') - # time.sleep(0.75) - # self.assertEqual(output().text, 'Initial State') - # self.assertEqual(call_count.value, 1) - # - # btn.click() - # wait_for(lambda: output().text == 'Initial Statex') - # self.assertEqual(call_count.value, 2) - # - # def test_events_state_and_inputs(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # html.Button('Click Me', id='button'), - # dcc.Input(value='Initial Input', id='input'), - # dcc.Input(value='Initial State', id='state'), - # html.Div(id='output') - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback(Output('output', 'children'), - # inputs=[Input('input', 'value')], - # state=[State('state', 'value')], - # events=[Event('button', 'click')]) - # def update_output(input, state): - # call_count.value += 1 - # return 'input="{}", state="{}"'.format(input, state) - # - # self.startServer(app) - # btn = lambda: self.driver.find_element_by_id('button') - # output = lambda: self.driver.find_element_by_id('output') - # input = lambda: self.driver.find_element_by_id('input') - # state = lambda: self.driver.find_element_by_id('state') - # - # # callback gets called with initial input - # self.assertEqual(call_count.value, 1) - # self.assertEqual( - # output().text, - # 'input="Initial Input", state="Initial State"' - # ) - # - # btn().click() - # wait_for(lambda: call_count.value == 2) - # self.assertEqual( - # output().text, - # 'input="Initial Input", state="Initial State"') - # - # input().send_keys('x') - # wait_for(lambda: call_count.value == 3) - # self.assertEqual( - # output().text, - # 'input="Initial Inputx", state="Initial State"') - # - # state().send_keys('x') - # time.sleep(0.75) - # self.assertEqual(call_count.value, 3) - # self.assertEqual( - # output().text, - # 'input="Initial Inputx", state="Initial State"') - # - # btn().click() - # wait_for(lambda: call_count.value == 4) - # self.assertEqual( - # output().text, - # 'input="Initial Inputx", state="Initial Statex"') - # - # def test_state_and_inputs(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input(value='Initial Input', id='input'), - # dcc.Input(value='Initial State', id='state'), - # html.Div(id='output') - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback(Output('output', 'children'), - # inputs=[Input('input', 'value')], - # state=[State('state', 'value')]) - # def update_output(input, state): - # call_count.value += 1 - # return 'input="{}", state="{}"'.format(input, state) - # - # self.startServer(app) - # output = lambda: self.driver.find_element_by_id('output') - # input = lambda: self.driver.find_element_by_id('input') - # state = lambda: self.driver.find_element_by_id('state') - # - # # callback gets called with initial input - # self.assertEqual(call_count.value, 1) - # self.assertEqual( - # output().text, - # 'input="Initial Input", state="Initial State"' - # ) - # - # input().send_keys('x') - # wait_for(lambda: call_count.value == 2) - # self.assertEqual( - # output().text, - # 'input="Initial Inputx", state="Initial State"') - # - # state().send_keys('x') - # time.sleep(0.75) - # self.assertEqual(call_count.value, 2) - # self.assertEqual( - # output().text, - # 'input="Initial Inputx", state="Initial State"') - # - # input().send_keys('y') - # wait_for(lambda: call_count.value == 3) - # self.assertEqual( - # output().text, - # 'input="Initial Inputxy", state="Initial Statex"') - # - # def test_event_creating_inputs(self): - # app = Dash(__name__) - # - # ids = { - # k: k for k in ['button', 'button-output', 'input', 'input-output'] - # } - # app.layout = html.Div([ - # html.Button(id=ids['button']), - # html.Div(id=ids['button-output']) - # ]) - # for script in dcc._js_dist: - # script['namespace'] = 'dash_core_components' - # app.scripts.append_script(script) - # - # app.config.supress_callback_exceptions = True - # call_counts = { - # ids['input-output']: Value('i', 0), - # ids['button-output']: Value('i', 0) - # } - # - # @app.callback( - # Output(ids['button-output'], 'children'), - # events=[Event(ids['button'], 'click')]) - # def display(): - # call_counts['button-output'].value += 1 - # return html.Div([ - # dcc.Input(id=ids['input'], value='initial state'), - # html.Div(id=ids['input-output']) - # ]) - # - # @app.callback( - # Output(ids['input-output'], 'children'), - # [Input(ids['input'], 'value')]) - # def update_input(value): - # call_counts['input-output'].value += 1 - # return 'Input is equal to "{}"'.format(value) - # - # self.startServer(app) - # time.sleep(1) - # self.assertEqual(call_counts[ids['button-output']].value, 0) - # self.assertEqual(call_counts[ids['input-output']].value, 0) - # - # btn = lambda: self.driver.find_element_by_id(ids['button']) - # output = lambda: self.driver.find_element_by_id(ids['input-output']) - # with self.assertRaises(Exception): - # output() - # - # btn().click() - # wait_for(lambda: call_counts[ids['input-output']].value == 1) - # self.assertEqual(call_counts[ids['button-output']].value, 1) - # self.assertEqual(output().text, 'Input is equal to "initial state"') - # - # def test_chained_dependencies_direct_lineage(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input(id='input-1', value='input 1'), - # dcc.Input(id='input-2'), - # html.Div('test', id='output') - # ]) - # input1 = lambda: self.driver.find_element_by_id('input-1') - # input2 = lambda: self.driver.find_element_by_id('input-2') - # output = lambda: self.driver.find_element_by_id('output') - # - # call_counts = { - # 'output': Value('i', 0), - # 'input-2': Value('i', 0) - # } - # - # @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) - # def update_input(input1): - # call_counts['input-2'].value += 1 - # return '<<{}>>'.format(input1) - # - # @app.callback(Output('output', 'children'), [ - # Input('input-1', 'value'), - # Input('input-2', 'value') - # ]) - # def update_output(input1, input2): - # call_counts['output'].value += 1 - # return '{} + {}'.format(input1, input2) - # - # self.startServer(app) - # - # wait_for(lambda: call_counts['output'].value == 1) - # wait_for(lambda: call_counts['input-2'].value == 1) - # self.assertEqual(input1().get_attribute('value'), 'input 1') - # self.assertEqual(input2().get_attribute('value'), '<>') - # self.assertEqual(output().text, 'input 1 + <>') - # - # input1().send_keys('x') - # wait_for(lambda: call_counts['output'].value == 2) - # wait_for(lambda: call_counts['input-2'].value == 2) - # self.assertEqual(input1().get_attribute('value'), 'input 1x') - # self.assertEqual(input2().get_attribute('value'), '<>') - # self.assertEqual(output().text, 'input 1x + <>') - # - # input2().send_keys('y') - # wait_for(lambda: call_counts['output'].value == 3) - # wait_for(lambda: call_counts['input-2'].value == 2) - # self.assertEqual(input1().get_attribute('value'), 'input 1x') - # self.assertEqual(input2().get_attribute('value'), '<>y') - # self.assertEqual(output().text, 'input 1x + <>y') - # - # - # def test_chained_dependencies_branched_lineage(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input(id='grandparent', value='input 1'), - # dcc.Input(id='parent-a'), - # dcc.Input(id='parent-b'), - # html.Div(id='child-a'), - # html.Div(id='child-b') - # ]) - # grandparent = lambda: self.driver.find_element_by_id('grandparent') - # parenta = lambda: self.driver.find_element_by_id('parent-a') - # parentb = lambda: self.driver.find_element_by_id('parent-b') - # childa = lambda: self.driver.find_element_by_id('child-a') - # childb = lambda: self.driver.find_element_by_id('child-b') - # - # call_counts = { - # 'parent-a': Value('i', 0), - # 'parent-b': Value('i', 0), - # 'child-a': Value('i', 0), - # 'child-b': Value('i', 0) - # } - # - # @app.callback(Output('parent-a', 'value'), - # [Input('grandparent', 'value')]) - # def update_parenta(value): - # call_counts['parent-a'].value += 1 - # return 'a: {}'.format(value) - # - # @app.callback(Output('parent-b', 'value'), - # [Input('grandparent', 'value')]) - # def update_parentb(value): - # time.sleep(0.5) - # call_counts['parent-b'].value += 1 - # return 'b: {}'.format(value) - # - # @app.callback(Output('child-a', 'children'), - # [Input('parent-a', 'value'), - # Input('parent-b', 'value')]) - # def update_childa(parenta_value, parentb_value): - # time.sleep(1) - # call_counts['child-a'].value += 1 - # return '{} + {}'.format(parenta_value, parentb_value) - # - # @app.callback(Output('child-b', 'children'), - # [Input('parent-a', 'value'), - # Input('parent-b', 'value'), - # Input('grandparent', 'value')]) - # def update_childb(parenta_value, parentb_value, grandparent_value): - # call_counts['child-b'].value += 1 - # return '{} + {} + {}'.format( - # parenta_value, - # parentb_value, - # grandparent_value - # ) - # - # self.startServer(app) - # - # wait_for(lambda: childa().text == 'a: input 1 + b: input 1') - # wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') - # time.sleep(1) # wait for potential requests of app to settle down - # self.assertEqual(parenta().get_attribute('value'), 'a: input 1') - # self.assertEqual(parentb().get_attribute('value'), 'b: input 1') - # self.assertEqual(call_counts['parent-a'].value, 1) - # self.assertEqual(call_counts['parent-b'].value, 1) - # self.assertEqual(call_counts['child-a'].value, 1) - # self.assertEqual(call_counts['child-b'].value, 1) - # - # def test_removing_component_while_its_getting_updated(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.RadioItems( - # id='toc', - # options=[ - # {'label': i, 'value': i} for i in ['1', '2'] - # ], - # value='1' - # ), - # html.Div(id='body') - # ]) - # app.config.supress_callback_exceptions = True - # - # call_counts = { - # 'body': Value('i', 0), - # 'button-output': Value('i', 0) - # } - # - # @app.callback(Output('body', 'children'), [Input('toc', 'value')]) - # def update_body(chapter): - # call_counts['body'].value += 1 - # if chapter == '1': - # return [ - # html.Div('Chapter 1'), - # html.Button( - # 'clicking this button takes forever', - # id='button' - # ), - # html.Div(id='button-output') - # ] - # elif chapter == '2': - # return 'Chapter 2' - # else: - # raise Exception('chapter is {}'.format(chapter)) - # - # @app.callback( - # Output('button-output', 'children'), - # events=[Event('button', 'click')]) - # def this_callback_takes_forever(): - # time.sleep(5) - # call_counts['button-output'].value += 1 - # return 'New value!' - # - # body = lambda: self.driver.find_element_by_id('body') - # self.startServer(app) - # - # wait_for(lambda: call_counts['body'].value == 1) - # time.sleep(0.5) - # self.driver.find_element_by_id('button').click() - # - # # while that callback is resolving, switch the chapter, - # # hiding the `button-output` tag - # def chapter2_assertions(): - # wait_for(lambda: body().text == 'Chapter 2') - # self.assertEqual( - # self.driver.execute_script( - # 'return JSON.parse(JSON.stringify(' - # 'window.store.getState().layout' - # '))' - # ), - # { - # "namespace": "dash_html_components", - # "type": "Div", - # "props": { - # "children": [ - # { - # "namespace": "dash_core_components", - # "type": "RadioItems", - # "props": { - # "value": "2", - # "options": app.layout['toc'].options, - # "id": app.layout['toc'].id, - # } - # }, - # { - # "namespace": "dash_html_components", - # "type": "Div", - # "props": { - # "id": "body", - # "children": "Chapter 2" - # } - # } - # ] - # } - # } - # ) - # self.assertEqual( - # self.driver.execute_script( - # 'return JSON.parse(JSON.stringify(' - # 'window.store.getState().paths' - # '))' - # ), - # { - # "toc": ["props", "children", 0], - # "body": ["props", "children", 1] - # } - # ) - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[1]).click() - # chapter2_assertions() - # time.sleep(5) - # wait_for(lambda: call_counts['button-output'].value == 1) - # time.sleep(2) # liberally wait for the front-end to process request - # chapter2_assertions() - # assert_clean_console(self) - # - # def test_rendering_layout_calls_callback_once_per_output(self): - # app = Dash(__name__) - # call_count = Value('i', 0) - # - # app.config['suppress_callback_exceptions'] = True - # app.layout = html.Div([ - # html.Div([ - # dcc.Input( - # value='Input {}'.format(i), - # id='input-{}'.format(i) - # ) - # for i in range(10) - # ]), - # html.Div(id='container'), - # dcc.RadioItems() - # ]) - # - # @app.callback( - # Output('container', 'children'), - # [Input('input-{}'.format(i), 'value') for i in range(10)]) - # def dynamic_output(*args): - # call_count.value += 1 - # return json.dumps(args, indent=2) - # - # self.startServer(app) - # - # time.sleep(5) - # - # self.percy_snapshot( - # name='test_rendering_layout_calls_callback_once_per_output' - # ) - # - # self.assertEqual(call_count.value, 1) - # - # def test_rendering_new_content_calls_callback_once_per_output(self): - # app = Dash(__name__) - # call_count = Value('i', 0) - # - # app.config['suppress_callback_exceptions'] = True - # app.layout = html.Div([ - # html.Button( - # id='display-content', - # children='Display Content', - # n_clicks=0 - # ), - # html.Div(id='container'), - # dcc.RadioItems() - # ]) - # - # @app.callback( - # Output('container', 'children'), - # [Input('display-content', 'n_clicks')]) - # def display_output(n_clicks): - # if n_clicks == 0: - # return '' - # return html.Div([ - # html.Div([ - # dcc.Input( - # value='Input {}'.format(i), - # id='input-{}'.format(i) - # ) - # for i in range(10) - # ]), - # html.Div(id='dynamic-output') - # ]) - # - # @app.callback( - # Output('dynamic-output', 'children'), - # [Input('input-{}'.format(i), 'value') for i in range(10)]) - # def dynamic_output(*args): - # call_count.value += 1 - # return json.dumps(args, indent=2) - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#display-content').click() - # - # time.sleep(5) - # - # self.percy_snapshot( - # name='test_rendering_new_content_calls_callback_once_per_output' - # ) - # - # self.assertEqual(call_count.value, 1) - # - # def test_callbacks_called_multiple_times_and_out_of_order(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # html.Button(id='input', n_clicks=0), - # html.Div(id='output') - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback( - # Output('output', 'children'), - # [Input('input', 'n_clicks')]) - # def update_output(n_clicks): - # call_count.value = call_count.value + 1 - # if n_clicks == 1: - # time.sleep(4) - # return n_clicks - # - # self.startServer(app) - # button = self.wait_for_element_by_css_selector('#input') - # button.click() - # button.click() - # time.sleep(8) - # self.percy_snapshot( - # name='test_callbacks_called_multiple_times_and_out_of_order' - # ) - # self.assertEqual(call_count.value, 3) - # self.assertEqual( - # self.driver.find_element_by_id('output').text, - # '2' - # ) - # request_queue = self.driver.execute_script( - # 'return window.store.getState().requestQueue' - # ) - # self.assertFalse(request_queue[0]['rejected']) - # self.assertEqual(len(request_queue), 1) - # - # - # def test_callbacks_with_shared_grandparent(self): - # app = dash.Dash() - # - # app.layout = html.Div([ - # html.Div(id='session-id', children='id'), - # dcc.Dropdown(id='dropdown-1'), - # dcc.Dropdown(id='dropdown-2'), - # ]) - # - # options = [{'value': 'a', 'label': 'a'}] - # - # call_counts = { - # 'dropdown_1': Value('i', 0), - # 'dropdown_2': Value('i', 0) - # } - # - # @app.callback( - # Output('dropdown-1', 'options'), - # [Input('dropdown-1', 'value'), - # Input('session-id', 'children')]) - # def dropdown_1(value, session_id): - # call_counts['dropdown_1'].value += 1 - # return options - # - # @app.callback( - # Output('dropdown-2', 'options'), - # [Input('dropdown-2', 'value'), - # Input('session-id', 'children')]) - # def dropdown_2(value, session_id): - # call_counts['dropdown_2'].value += 1 - # return options - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#session-id') - # time.sleep(2) - # self.assertEqual(call_counts['dropdown_1'].value, 1) - # self.assertEqual(call_counts['dropdown_2'].value, 1) - # - # assert_clean_console(self) - # - # def test_callbacks_triggered_on_generated_output(self): - # app = dash.Dash() - # app.config['suppress_callback_exceptions'] = True - # - # call_counts = { - # 'tab1': Value('i', 0), - # 'tab2': Value('i', 0) - # } - # - # app.layout = html.Div([ - # dcc.Dropdown( - # id='outer-controls', - # options=[{'label': i, 'value': i} for i in ['a', 'b']], - # value='a' - # ), - # dcc.RadioItems( - # options=[ - # {'label': 'Tab 1', 'value': 1}, - # {'label': 'Tab 2', 'value': 2} - # ], - # value=1, - # id='tabs', - # ), - # html.Div(id='tab-output') - # ]) - # - # @app.callback(Output('tab-output', 'children'), - # [Input('tabs', 'value')]) - # def display_content(value): - # return html.Div([ - # html.Div(id='tab-{}-output'.format(value)) - # ]) - # - # @app.callback(Output('tab-1-output', 'children'), - # [Input('outer-controls', 'value')]) - # def display_tab1_output(value): - # call_counts['tab1'].value += 1 - # return 'You have selected "{}"'.format(value) - # - # @app.callback(Output('tab-2-output', 'children'), - # [Input('outer-controls', 'value')]) - # def display_tab2_output(value): - # call_counts['tab2'].value += 1 - # return 'You have selected "{}"'.format(value) - # - # self.startServer(app) - # self.wait_for_element_by_css_selector('#tab-output') - # time.sleep(2) - # - # self.assertEqual(call_counts['tab1'].value, 1) - # self.assertEqual(call_counts['tab2'].value, 0) - # self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') - # self.wait_for_text_to_equal('#tab-1-output', 'You have selected "a"') - # - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[1]).click() - # time.sleep(2) - # - # self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') - # self.wait_for_text_to_equal('#tab-2-output', 'You have selected "a"') - # self.assertEqual(call_counts['tab1'].value, 1) - # self.assertEqual(call_counts['tab2'].value, 1) - # - # assert_clean_console(self) - # - # def test_initialization_with_overlapping_outputs(self): - # app = dash.Dash() - # app.layout = html.Div([ - # - # html.Div(id='input-1', children='input-1'), - # html.Div(id='input-2', children='input-2'), - # html.Div(id='input-3', children='input-3'), - # html.Div(id='input-4', children='input-4'), - # html.Div(id='input-5', children='input-5'), - # - # html.Div(id='output-1'), - # html.Div(id='output-2'), - # html.Div(id='output-3'), - # html.Div(id='output-4'), - # - # ]) - # call_counts = { - # 'output-1': Value('i', 0), - # 'output-2': Value('i', 0), - # 'output-3': Value('i', 0), - # 'output-4': Value('i', 0), - # } - # - # def generate_callback(outputid): - # def callback(*args): - # call_counts[outputid].value += 1 - # return '{}, {}'.format(*args) - # return callback - # - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # app.callback( - # Output(outputid, 'children'), - # [Input('input-{}'.format(i), 'children'), - # Input('input-{}'.format(i+1), 'children')] - # )(generate_callback(outputid)) - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#output-1') - # time.sleep(5) - # - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # self.assertEqual(call_counts[outputid].value, 1) - # self.wait_for_text_to_equal( - # '#{}'.format(outputid), - # "input-{}, input-{}".format(i, i+1) - # ) - # - # def test_generate_overlapping_outputs(self): - # app = dash.Dash() - # app.config['suppress_callback_exceptions'] = True - # block = html.Div([ - # - # html.Div(id='input-1', children='input-1'), - # html.Div(id='input-2', children='input-2'), - # html.Div(id='input-3', children='input-3'), - # html.Div(id='input-4', children='input-4'), - # html.Div(id='input-5', children='input-5'), - # - # html.Div(id='output-1'), - # html.Div(id='output-2'), - # html.Div(id='output-3'), - # html.Div(id='output-4'), - # - # ]) - # app.layout = html.Div([ - # html.Div(id='input'), - # html.Div(id='container') - # ]) - # - # call_counts = { - # 'container': Value('i', 0), - # 'output-1': Value('i', 0), - # 'output-2': Value('i', 0), - # 'output-3': Value('i', 0), - # 'output-4': Value('i', 0), - # } - # - # @app.callback(Output('container', 'children'), - # [Input('input', 'children')]) - # def display_output(*args): - # call_counts['container'].value += 1 - # return block - # - # def generate_callback(outputid): - # def callback(*args): - # call_counts[outputid].value += 1 - # return '{}, {}'.format(*args) - # return callback - # - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # app.callback( - # Output(outputid, 'children'), - # [Input('input-{}'.format(i), 'children'), - # Input('input-{}'.format(i+1), 'children')] - # )(generate_callback(outputid)) - # - # self.startServer(app) - # - # wait_for(lambda: call_counts['container'].value == 1) - # self.wait_for_element_by_css_selector('#output-1') - # time.sleep(5) - # - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # self.assertEqual(call_counts[outputid].value, 1) - # self.wait_for_text_to_equal( - # '#{}'.format(outputid), - # "input-{}, input-{}".format(i, i+1) - # ) - # self.assertEqual(call_counts['container'].value, 1) + def test_initial_state(self): + app = Dash(__name__) + app.layout = html.Div([ + 'Basic string', + 3.14, + None, + html.Div('Child div with basic string', + id='p.c.3', + className="my-class", + title='tooltip', + style={'color': 'red', 'fontSize': 30} + ), + html.Div(id='p.c.4'), + html.Div([ + html.Div('Grandchild div', id='p.c.5.p.c.0'), + html.Div([ + html.Div('Great grandchild', id='p.c.5.p.c.1.p.c.0'), + 3.14159, + 'another basic string' + ], id='p.c.5.p.c.1'), + html.Div([ + html.Div( + html.Div([ + html.Div([ + html.Div( + id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0' + ), + '', + html.Div( + id='p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2' + ) + ], id='p.c.5.p.c.2.p.c.0.p.c.p.c.0') + ], id='p.c.5.p.c.2.p.c.0.p.c'), + id='p.c.5.p.c.2.p.c.0' + ) + ], id='p.c.5.p.c.2') + ], id='p.c.5') + ]) + + self.startServer(app) + + el = self.wait_for_element_by_css_selector('#_dash-app-content') + + # TODO - Make less fragile with http://lxml.de/lxmlhtml.html#html-diff + rendered_dom = ''' +
+ Basic string + + 3.14 + +
+ Child div with basic string +
+ +
+
+ +
+
+ Grandchild div +
+ +
+
+ Great grandchild +
+ + 3.14159 + + another basic string +
+ +
+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+ +
+ ''' + # React wraps text and numbers with e.g. + # Remove those + comment_regex = '' + + # Somehow the html attributes are unordered. + # Try different combinations (they're all valid html) + style_permutations = [ + 'style="color: red; font-size: 30px;"', + 'style="font-size: 30px; color: red;"' + ] + permutations = itertools.permutations([ + 'id="p.c.3"', + 'class="my-class"', + 'title="tooltip"', + ], 3) + passed = False + for permutation in permutations: + for style in style_permutations: + actual_cleaned = re.sub(comment_regex, '', el.get_attribute('innerHTML')) + expected_cleaned = re.sub( + comment_regex, + '', + rendered_dom.replace('\n', '') + .replace(' ', '') + .replace('PERMUTE', ' '.join(list(permutation) + [style])) + ) + passed = passed or (actual_cleaned == expected_cleaned) + if not passed: + raise Exception( + 'HTML does not match\nActual:\n{}\n\nExpected:\n{}'.format( + actual_cleaned, + expected_cleaned + ) + ) + + # Check that no errors or warnings were displayed + self.assertEqual( + self.driver.execute_script( + 'return window.tests.console.error.length' + ), + 0 + ) + self.assertEqual( + self.driver.execute_script( + 'return window.tests.console.warn.length' + ), + 0 + ) + + # Check the initial stores + + # layout should just be the JSON-ified app.layout + self.assertEqual( + self.driver.execute_script( + 'return JSON.parse(JSON.stringify(' + 'window.store.getState().layout' + '))' + ), + { + "namespace": "dash_html_components", + "props": { + "children": [ + "Basic string", + 3.14, + None, + { + "namespace": "dash_html_components", + "props": { + "children": "Child div with basic string", + "id": "p.c.3", + 'className': "my-class", + 'title': 'tooltip', + 'style': { + 'color': 'red', 'fontSize': 30 + } + }, + "type": "Div" + }, + { + "namespace": "dash_html_components", + "props": { + "children": None, + "id": "p.c.4" + }, + "type": "Div" + }, + { + "namespace": "dash_html_components", + "props": { + "children": [ + { + "namespace": "dash_html_components", + "props": { + "children": "Grandchild div", + "id": "p.c.5.p.c.0" + }, + "type": "Div" + }, + { + "namespace": "dash_html_components", + "props": { + "children": [ + { + "namespace": "dash_html_components", + "props": { + "children": "Great grandchild", + "id": "p.c.5.p.c.1.p.c.0" + }, + "type": "Div" + }, + 3.14159, + "another basic string" + ], + "id": "p.c.5.p.c.1" + }, + "type": "Div" + }, + { + "namespace": "dash_html_components", + "props": { + "children": [ + { + "namespace": "dash_html_components", + "props": { + "children": { + "namespace": "dash_html_components", + "props": { + "children": [ + { + "namespace": "dash_html_components", + "props": { + "children": [ + { + "namespace": "dash_html_components", + "props": { + "children": None, + "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0" + }, + "type": "Div" + }, + "", + { + "namespace": "dash_html_components", + "props": { + "children": None, + "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2" + }, + "type": "Div" + } + ], + "id": "p.c.5.p.c.2.p.c.0.p.c.p.c.0" + }, + "type": "Div" + } + ], + "id": "p.c.5.p.c.2.p.c.0.p.c" + }, + "type": "Div" + }, + "id": "p.c.5.p.c.2.p.c.0" + }, + "type": "Div" + } + ], + "id": "p.c.5.p.c.2" + }, + "type": "Div" + } + ], + "id": "p.c.5" + }, + "type": "Div" + } + ] + }, + "type": "Div" + } + ) + + # graphs should just be empty since there are no dependencies + self.assertEqual( + self.driver.execute_script( + 'return JSON.parse(JSON.stringify(' + 'window.store.getState().graphs' + '))' + ), + { + "InputGraph": { + "nodes": {}, + "outgoingEdges": {}, + "incomingEdges": {} + }, + "EventGraph": { + "nodes": {}, + "outgoingEdges": {}, + "incomingEdges": {} + } + } + ) + + # paths is just a lookup table of the components's IDs and their + # placement in the tree. + # in this case the IDs are just abbreviations of the path to make + # things easy to verify. + self.assertEqual( + self.driver.execute_script( + 'return window.store.getState().paths' + ), + { + "p.c.3": [ + "props", "children", 3 + ], + "p.c.4": [ + "props", "children", 4 + ], + "p.c.5": [ + "props", "children", 5 + ], + "p.c.5.p.c.0": [ + "props", "children", 5, + "props", "children", 0 + ], + "p.c.5.p.c.1": [ + "props", "children", 5, + "props", "children", 1 + ], + "p.c.5.p.c.1.p.c.0": [ + "props", "children", 5, + "props", "children", 1, + "props", "children", 0 + ], + "p.c.5.p.c.2": [ + "props", "children", 5, + "props", "children", 2 + ], + "p.c.5.p.c.2.p.c.0": [ + "props", "children", 5, + "props", "children", 2, + "props", "children", 0 + ], + "p.c.5.p.c.2.p.c.0.p.c": [ + "props", "children", 5, + "props", "children", 2, + "props", "children", 0, + "props", "children" + ], + "p.c.5.p.c.2.p.c.0.p.c.p.c.0": [ + "props", "children", 5, + "props", "children", 2, + "props", "children", 0, + "props", "children", + "props", "children", 0 + ], + "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.0": [ + "props", "children", 5, + "props", "children", 2, + "props", "children", 0, + "props", "children", + "props", "children", 0, + "props", "children", 0 + ], + "p.c.5.p.c.2.p.c.0.p.c.p.c.0.p.c.2": [ + "props", "children", 5, + "props", "children", 2, + "props", "children", 0, + "props", "children", + "props", "children", 0, + "props", "children", 2 + ] + } + ) + + self.request_queue_assertions(0) + + self.percy_snapshot(name='layout') + + assert_clean_console(self) + + def test_simple_callback(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input( + id='input', + value='initial value' + ), + html.Div( + html.Div([ + 1.5, + None, + 'string', + html.Div(id='output-1') + ]) + ) + ]) + + call_count = Value('i', 0) + + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + call_count.value = call_count.value + 1 + return value + + self.startServer(app) + + self.wait_for_text_to_equal('#output-1', 'initial value') + self.percy_snapshot(name='simple-callback-1') + + input1 = self.wait_for_element_by_css_selector('#input') + input1.clear() + + input1.send_keys('hello world') + + self.wait_for_text_to_equal('#output-1', 'hello world') + self.percy_snapshot(name='simple-callback-2') + + self.assertEqual( + call_count.value, + # an initial call to retrieve the first value + 1 + + # one for each hello world character + len('hello world') + ) + + self.request_queue_assertions( + expected_length=1, + check_rejected=False) + + assert_clean_console(self) + + def test_callbacks_generating_children(self): + """ Modify the DOM tree by adding new + components in the callbacks + """ + + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input( + id='input', + value='initial value' + ), + html.Div(id='output') + ]) + + @app.callback(Output('output', 'children'), [Input('input', 'value')]) + def pad_output(input): + return html.Div([ + dcc.Input( + id='sub-input-1', + value='sub input initial value' + ), + html.Div(id='sub-output-1') + ]) + + call_count = Value('i', 0) + + # these components don't exist in the initial render + app.config.supress_callback_exceptions = True + + @app.callback( + Output('sub-output-1', 'children'), + [Input('sub-input-1', 'value')] + ) + def update_input(value): + call_count.value = call_count.value + 1 + return value + + self.startServer(app) + + output = self.driver.find_element_by_id('output') + output_html = output.get_attribute('innerHTML') + + wait_for(lambda: call_count.value == 1) + + # Adding new children to the layout should + # call the callbacks immediately to set + # the correct initial state + wait_for( + lambda: ( + self.driver.find_element_by_id('output') + .get_attribute('innerHTML') in [''' +
+ {} +
+ sub input initial value +
+
'''.replace('\n', '').replace(' ', '').format(input) + for input in [ + # html attributes are unordered, so include both versions + '', + '' + ] + ] + ) + ) + self.percy_snapshot(name='callback-generating-function-1') + + # the paths should include these new output IDs + self.assertEqual( + self.driver.execute_script('return window.store.getState().paths'), + { + 'input': [ + 'props', 'children', 0 + ], + 'output': ['props', 'children', 1], + 'sub-input-1': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 0 + ], + 'sub-output-1': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 1 + ] + } + ) + + # editing the input should modify the sub output + sub_input = self.driver.find_element_by_id('sub-input-1') + sub_input.send_keys('a') + self.wait_for_text_to_equal( + '#sub-output-1', + 'sub input initial valuea') + + self.assertEqual(call_count.value, 2) + + self.request_queue_assertions(call_count.value + 1) + self.percy_snapshot(name='callback-generating-function-2') + + assert_clean_console(self) + + def test_radio_buttons_callbacks_generating_children(self): + self.maxDiff = 100 * 1000 + app = Dash(__name__) + app.layout = html.Div([ + dcc.RadioItems( + options=[ + {'label': 'Chapter 1', 'value': 'chapter1'}, + {'label': 'Chapter 2', 'value': 'chapter2'}, + {'label': 'Chapter 3', 'value': 'chapter3'}, + {'label': 'Chapter 4', 'value': 'chapter4'}, + {'label': 'Chapter 5', 'value': 'chapter5'} + ], + value='chapter1', + id='toc' + ), + html.Div(id='body') + ]) + for script in dcc._js_dist: + app.scripts.append_script(script) + + chapters = { + 'chapter1': html.Div([ + html.H1('Chapter 1', id='chapter1-header'), + dcc.Dropdown( + options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'SF']], + value='NYC', + id='chapter1-controls' + ), + html.Label(id='chapter1-label'), + dcc.Graph(id='chapter1-graph') + ]), + # Chapter 2 has the some of the same components in the same order + # as Chapter 1. This means that they won't get remounted + # unless they set their own keys are differently. + # Switching back and forth between 1 and 2 implicitly + # tests how components update when they aren't remounted. + 'chapter2': html.Div([ + html.H1('Chapter 2', id='chapter2-header'), + dcc.RadioItems( + options=[{'label': i, 'value': i} + for i in ['USA', 'Canada']], + value='USA', + id='chapter2-controls' + ), + html.Label(id='chapter2-label'), + dcc.Graph(id='chapter2-graph') + ]), + # Chapter 3 has a different layout and so the components + # should get rewritten + 'chapter3': [html.Div( + html.Div([ + html.H3('Chapter 3', id='chapter3-header'), + html.Label(id='chapter3-label'), + dcc.Graph(id='chapter3-graph'), + dcc.RadioItems( + options=[{'label': i, 'value': i} + for i in ['Summer', 'Winter']], + value='Winter', + id='chapter3-controls' + ) + ]) + )], + + # Chapter 4 doesn't have an object to recursively + # traverse + 'chapter4': 'Just a string', + + # Chapter 5 contains elements that are bound with events + 'chapter5': [html.Div([ + html.Button(id='chapter5-button'), + html.Div(id='chapter5-output') + ])] + } + + call_counts = { + 'body': Value('i', 0), + 'chapter1-graph': Value('i', 0), + 'chapter1-label': Value('i', 0), + 'chapter2-graph': Value('i', 0), + 'chapter2-label': Value('i', 0), + 'chapter3-graph': Value('i', 0), + 'chapter3-label': Value('i', 0), + 'chapter5-output': Value('i', 0) + } + + @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + def display_chapter(toc_value): + call_counts['body'].value += 1 + return chapters[toc_value] + + app.config.supress_callback_exceptions = True + + def generate_graph_callback(counterId): + def callback(value): + call_counts[counterId].value += 1 + return { + 'data': [{ + 'x': ['Call Counter'], + 'y': [call_counts[counterId].value], + 'type': 'bar' + }], + 'layout': {'title': value} + } + return callback + + def generate_label_callback(id): + def update_label(value): + call_counts[id].value += 1 + return value + return update_label + + for chapter in ['chapter1', 'chapter2', 'chapter3']: + app.callback( + Output('{}-graph'.format(chapter), 'figure'), + [Input('{}-controls'.format(chapter), 'value')] + )(generate_graph_callback('{}-graph'.format(chapter))) + + app.callback( + Output('{}-label'.format(chapter), 'children'), + [Input('{}-controls'.format(chapter), 'value')] + )(generate_label_callback('{}-label'.format(chapter))) + + chapter5_output_children = 'Button clicked' + + @app.callback(Output('chapter5-output', 'children'), + events=[Event('chapter5-button', 'click')]) + def display_output(): + call_counts['chapter5-output'].value += 1 + return chapter5_output_children + + self.startServer(app) + + time.sleep(0.5) + wait_for(lambda: call_counts['body'].value == 1) + wait_for(lambda: call_counts['chapter1-graph'].value == 1) + wait_for(lambda: call_counts['chapter1-label'].value == 1) + self.assertEqual(call_counts['chapter2-graph'].value, 0) + self.assertEqual(call_counts['chapter2-label'].value, 0) + self.assertEqual(call_counts['chapter3-graph'].value, 0) + self.assertEqual(call_counts['chapter3-label'].value, 0) + + def generic_chapter_assertions(chapter): + # each element should exist in the dom + paths = self.driver.execute_script( + 'return window.store.getState().paths' + ) + for key in paths: + self.driver.find_element_by_id(key) + + if chapter == 'chapter3': + value = chapters[chapter][0][ + '{}-controls'.format(chapter) + ].value + else: + value = chapters[chapter]['{}-controls'.format(chapter)].value + # check the actual values + self.wait_for_text_to_equal('#{}-label'.format(chapter), value) + wait_for( + lambda: ( + self.driver.execute_script( + 'return document.' + 'getElementById("{}-graph").'.format(chapter) + + 'layout.title' + ) == value + ) + ) + self.request_queue_assertions() + + def chapter1_assertions(): + paths = self.driver.execute_script( + 'return window.store.getState().paths' + ) + self.assertEqual(paths, { + 'toc': ['props', 'children', 0], + 'body': ['props', 'children', 1], + 'chapter1-header': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 0 + ], + 'chapter1-controls': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 1 + ], + 'chapter1-label': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 2 + ], + 'chapter1-graph': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 3 + ] + }) + generic_chapter_assertions('chapter1') + + chapter1_assertions() + self.percy_snapshot(name='chapter-1') + + # switch chapters + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[1]).click() + + # sleep just to make sure that no calls happen after our check + time.sleep(2) + self.percy_snapshot(name='chapter-2') + wait_for(lambda: call_counts['body'].value == 2) + wait_for(lambda: call_counts['chapter2-graph'].value == 1) + wait_for(lambda: call_counts['chapter2-label'].value == 1) + self.assertEqual(call_counts['chapter1-graph'].value, 1) + self.assertEqual(call_counts['chapter1-label'].value, 1) + + def chapter2_assertions(): + paths = self.driver.execute_script( + 'return window.store.getState().paths' + ) + self.assertEqual(paths, { + 'toc': ['props', 'children', 0], + 'body': ['props', 'children', 1], + 'chapter2-header': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 0 + ], + 'chapter2-controls': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 1 + ], + 'chapter2-label': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 2 + ], + 'chapter2-graph': [ + 'props', 'children', 1, + 'props', 'children', + 'props', 'children', 3 + ] + }) + generic_chapter_assertions('chapter2') + + chapter2_assertions() + + # switch to 3 + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[2]).click() + # sleep just to make sure that no calls happen after our check + time.sleep(2) + self.percy_snapshot(name='chapter-3') + wait_for(lambda: call_counts['body'].value == 3) + wait_for(lambda: call_counts['chapter3-graph'].value == 1) + wait_for(lambda: call_counts['chapter3-label'].value == 1) + self.assertEqual(call_counts['chapter2-graph'].value, 1) + self.assertEqual(call_counts['chapter2-label'].value, 1) + self.assertEqual(call_counts['chapter1-graph'].value, 1) + self.assertEqual(call_counts['chapter1-label'].value, 1) + + def chapter3_assertions(): + paths = self.driver.execute_script( + 'return window.store.getState().paths' + ) + self.assertEqual(paths, { + 'toc': ['props', 'children', 0], + 'body': ['props', 'children', 1], + 'chapter3-header': [ + 'props', 'children', 1, + 'props', 'children', 0, + 'props', 'children', + 'props', 'children', 0 + ], + 'chapter3-label': [ + 'props', 'children', 1, + 'props', 'children', 0, + 'props', 'children', + 'props', 'children', 1 + ], + 'chapter3-graph': [ + 'props', 'children', 1, + 'props', 'children', 0, + 'props', 'children', + 'props', 'children', 2 + ], + 'chapter3-controls': [ + 'props', 'children', 1, + 'props', 'children', 0, + 'props', 'children', + 'props', 'children', 3 + ] + }) + generic_chapter_assertions('chapter3') + + chapter3_assertions() + + # switch to 4 + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[3]).click() + self.wait_for_text_to_equal('#body', 'Just a string') + self.percy_snapshot(name='chapter-4') + + # each element should exist in the dom + paths = self.driver.execute_script( + 'return window.store.getState().paths' + ) + for key in paths: + self.driver.find_element_by_id(key) + self.assertEqual(paths, { + 'toc': ['props', 'children', 0], + 'body': ['props', 'children', 1] + }) + + # switch back to 1 + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[0]).click() + time.sleep(0.5) + chapter1_assertions() + self.percy_snapshot(name='chapter-1-again') + + # switch to 5 + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[4]).click() + time.sleep(1) + # click on the button and check the output div before and after + chapter5_div = lambda: self.driver.find_element_by_id( + 'chapter5-output' + ) + chapter5_button = lambda: self.driver.find_element_by_id( + 'chapter5-button' + ) + self.assertEqual(chapter5_div().text, '') + chapter5_button().click() + wait_for(lambda: chapter5_div().text == chapter5_output_children) + time.sleep(0.5) + self.percy_snapshot(name='chapter-5') + self.assertEqual(call_counts['chapter5-output'].value, 1) + + def test_dependencies_on_components_that_dont_exist(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='input', value='initial value'), + html.Div(id='output-1') + ]) + + # standard callback + output_1_call_count = Value('i', 0) + + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + output_1_call_count.value += 1 + return value + + # callback for component that doesn't yet exist in the dom + # in practice, it might get added by some other callback + app.config.supress_callback_exceptions = True + output_2_call_count = Value('i', 0) + + @app.callback( + Output('output-2', 'children'), + [Input('input', 'value')] + ) + def update_output_2(value): + output_2_call_count.value += 1 + return value + + self.startServer(app) + + self.wait_for_text_to_equal('#output-1', 'initial value') + self.percy_snapshot(name='dependencies') + time.sleep(1.0) + self.assertEqual(output_1_call_count.value, 1) + self.assertEqual(output_2_call_count.value, 0) + + input = self.driver.find_element_by_id('input') + + input.send_keys('a') + self.wait_for_text_to_equal('#output-1', 'initial valuea') + time.sleep(1.0) + self.assertEqual(output_1_call_count.value, 2) + self.assertEqual(output_2_call_count.value, 0) + + self.request_queue_assertions(2) + + assert_clean_console(self) + + def test_events(self): + app = Dash(__name__) + app.layout = html.Div([ + html.Button('Click Me', id='button'), + html.Div(id='output') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('output', 'children'), + events=[Event('button', 'click')]) + def update_output(): + call_count.value += 1 + return 'Click' + + self.startServer(app) + btn = self.driver.find_element_by_id('button') + output = lambda: self.driver.find_element_by_id('output') + self.assertEqual(call_count.value, 0) + self.assertEqual(output().text, '') + + btn.click() + wait_for(lambda: output().text == 'Click') + self.assertEqual(call_count.value, 1) + + def test_events_and_state(self): + app = Dash(__name__) + app.layout = html.Div([ + html.Button('Click Me', id='button'), + dcc.Input(value='Initial State', id='state'), + html.Div(id='output') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('output', 'children'), + state=[State('state', 'value')], + events=[Event('button', 'click')]) + def update_output(value): + call_count.value += 1 + return value + + self.startServer(app) + btn = self.driver.find_element_by_id('button') + output = lambda: self.driver.find_element_by_id('output') + + self.assertEqual(call_count.value, 0) + self.assertEqual(output().text, '') + + btn.click() + wait_for(lambda: output().text == 'Initial State') + self.assertEqual(call_count.value, 1) + + # Changing state shouldn't fire the callback + state = self.driver.find_element_by_id('state') + state.send_keys('x') + time.sleep(0.75) + self.assertEqual(output().text, 'Initial State') + self.assertEqual(call_count.value, 1) + + btn.click() + wait_for(lambda: output().text == 'Initial Statex') + self.assertEqual(call_count.value, 2) + + def test_events_state_and_inputs(self): + app = Dash(__name__) + app.layout = html.Div([ + html.Button('Click Me', id='button'), + dcc.Input(value='Initial Input', id='input'), + dcc.Input(value='Initial State', id='state'), + html.Div(id='output') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('output', 'children'), + inputs=[Input('input', 'value')], + state=[State('state', 'value')], + events=[Event('button', 'click')]) + def update_output(input, state): + call_count.value += 1 + return 'input="{}", state="{}"'.format(input, state) + + self.startServer(app) + btn = lambda: self.driver.find_element_by_id('button') + output = lambda: self.driver.find_element_by_id('output') + input = lambda: self.driver.find_element_by_id('input') + state = lambda: self.driver.find_element_by_id('state') + + # callback gets called with initial input + self.assertEqual(call_count.value, 1) + self.assertEqual( + output().text, + 'input="Initial Input", state="Initial State"' + ) + + btn().click() + wait_for(lambda: call_count.value == 2) + self.assertEqual( + output().text, + 'input="Initial Input", state="Initial State"') + + input().send_keys('x') + wait_for(lambda: call_count.value == 3) + self.assertEqual( + output().text, + 'input="Initial Inputx", state="Initial State"') + + state().send_keys('x') + time.sleep(0.75) + self.assertEqual(call_count.value, 3) + self.assertEqual( + output().text, + 'input="Initial Inputx", state="Initial State"') + + btn().click() + wait_for(lambda: call_count.value == 4) + self.assertEqual( + output().text, + 'input="Initial Inputx", state="Initial Statex"') + + def test_state_and_inputs(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(value='Initial Input', id='input'), + dcc.Input(value='Initial State', id='state'), + html.Div(id='output') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('output', 'children'), + inputs=[Input('input', 'value')], + state=[State('state', 'value')]) + def update_output(input, state): + call_count.value += 1 + return 'input="{}", state="{}"'.format(input, state) + + self.startServer(app) + output = lambda: self.driver.find_element_by_id('output') + input = lambda: self.driver.find_element_by_id('input') + state = lambda: self.driver.find_element_by_id('state') + + # callback gets called with initial input + self.assertEqual(call_count.value, 1) + self.assertEqual( + output().text, + 'input="Initial Input", state="Initial State"' + ) + + input().send_keys('x') + wait_for(lambda: call_count.value == 2) + self.assertEqual( + output().text, + 'input="Initial Inputx", state="Initial State"') + + state().send_keys('x') + time.sleep(0.75) + self.assertEqual(call_count.value, 2) + self.assertEqual( + output().text, + 'input="Initial Inputx", state="Initial State"') + + input().send_keys('y') + wait_for(lambda: call_count.value == 3) + self.assertEqual( + output().text, + 'input="Initial Inputxy", state="Initial Statex"') + + def test_event_creating_inputs(self): + app = Dash(__name__) + + ids = { + k: k for k in ['button', 'button-output', 'input', 'input-output'] + } + app.layout = html.Div([ + html.Button(id=ids['button']), + html.Div(id=ids['button-output']) + ]) + for script in dcc._js_dist: + script['namespace'] = 'dash_core_components' + app.scripts.append_script(script) + + app.config.supress_callback_exceptions = True + call_counts = { + ids['input-output']: Value('i', 0), + ids['button-output']: Value('i', 0) + } + + @app.callback( + Output(ids['button-output'], 'children'), + events=[Event(ids['button'], 'click')]) + def display(): + call_counts['button-output'].value += 1 + return html.Div([ + dcc.Input(id=ids['input'], value='initial state'), + html.Div(id=ids['input-output']) + ]) + + @app.callback( + Output(ids['input-output'], 'children'), + [Input(ids['input'], 'value')]) + def update_input(value): + call_counts['input-output'].value += 1 + return 'Input is equal to "{}"'.format(value) + + self.startServer(app) + time.sleep(1) + self.assertEqual(call_counts[ids['button-output']].value, 0) + self.assertEqual(call_counts[ids['input-output']].value, 0) + + btn = lambda: self.driver.find_element_by_id(ids['button']) + output = lambda: self.driver.find_element_by_id(ids['input-output']) + with self.assertRaises(Exception): + output() + + btn().click() + wait_for(lambda: call_counts[ids['input-output']].value == 1) + self.assertEqual(call_counts[ids['button-output']].value, 1) + self.assertEqual(output().text, 'Input is equal to "initial state"') + + def test_chained_dependencies_direct_lineage(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='input-1', value='input 1'), + dcc.Input(id='input-2'), + html.Div('test', id='output') + ]) + input1 = lambda: self.driver.find_element_by_id('input-1') + input2 = lambda: self.driver.find_element_by_id('input-2') + output = lambda: self.driver.find_element_by_id('output') + + call_counts = { + 'output': Value('i', 0), + 'input-2': Value('i', 0) + } + + @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) + def update_input(input1): + call_counts['input-2'].value += 1 + return '<<{}>>'.format(input1) + + @app.callback(Output('output', 'children'), [ + Input('input-1', 'value'), + Input('input-2', 'value') + ]) + def update_output(input1, input2): + call_counts['output'].value += 1 + return '{} + {}'.format(input1, input2) + + self.startServer(app) + + wait_for(lambda: call_counts['output'].value == 1) + wait_for(lambda: call_counts['input-2'].value == 1) + self.assertEqual(input1().get_attribute('value'), 'input 1') + self.assertEqual(input2().get_attribute('value'), '<>') + self.assertEqual(output().text, 'input 1 + <>') + + input1().send_keys('x') + wait_for(lambda: call_counts['output'].value == 2) + wait_for(lambda: call_counts['input-2'].value == 2) + self.assertEqual(input1().get_attribute('value'), 'input 1x') + self.assertEqual(input2().get_attribute('value'), '<>') + self.assertEqual(output().text, 'input 1x + <>') + + input2().send_keys('y') + wait_for(lambda: call_counts['output'].value == 3) + wait_for(lambda: call_counts['input-2'].value == 2) + self.assertEqual(input1().get_attribute('value'), 'input 1x') + self.assertEqual(input2().get_attribute('value'), '<>y') + self.assertEqual(output().text, 'input 1x + <>y') + + + def test_chained_dependencies_branched_lineage(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='grandparent', value='input 1'), + dcc.Input(id='parent-a'), + dcc.Input(id='parent-b'), + html.Div(id='child-a'), + html.Div(id='child-b') + ]) + grandparent = lambda: self.driver.find_element_by_id('grandparent') + parenta = lambda: self.driver.find_element_by_id('parent-a') + parentb = lambda: self.driver.find_element_by_id('parent-b') + childa = lambda: self.driver.find_element_by_id('child-a') + childb = lambda: self.driver.find_element_by_id('child-b') + + call_counts = { + 'parent-a': Value('i', 0), + 'parent-b': Value('i', 0), + 'child-a': Value('i', 0), + 'child-b': Value('i', 0) + } + + @app.callback(Output('parent-a', 'value'), + [Input('grandparent', 'value')]) + def update_parenta(value): + call_counts['parent-a'].value += 1 + return 'a: {}'.format(value) + + @app.callback(Output('parent-b', 'value'), + [Input('grandparent', 'value')]) + def update_parentb(value): + time.sleep(0.5) + call_counts['parent-b'].value += 1 + return 'b: {}'.format(value) + + @app.callback(Output('child-a', 'children'), + [Input('parent-a', 'value'), + Input('parent-b', 'value')]) + def update_childa(parenta_value, parentb_value): + time.sleep(1) + call_counts['child-a'].value += 1 + return '{} + {}'.format(parenta_value, parentb_value) + + @app.callback(Output('child-b', 'children'), + [Input('parent-a', 'value'), + Input('parent-b', 'value'), + Input('grandparent', 'value')]) + def update_childb(parenta_value, parentb_value, grandparent_value): + call_counts['child-b'].value += 1 + return '{} + {} + {}'.format( + parenta_value, + parentb_value, + grandparent_value + ) + + self.startServer(app) + + wait_for(lambda: childa().text == 'a: input 1 + b: input 1') + wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') + time.sleep(1) # wait for potential requests of app to settle down + self.assertEqual(parenta().get_attribute('value'), 'a: input 1') + self.assertEqual(parentb().get_attribute('value'), 'b: input 1') + self.assertEqual(call_counts['parent-a'].value, 1) + self.assertEqual(call_counts['parent-b'].value, 1) + self.assertEqual(call_counts['child-a'].value, 1) + self.assertEqual(call_counts['child-b'].value, 1) + + def test_removing_component_while_its_getting_updated(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.RadioItems( + id='toc', + options=[ + {'label': i, 'value': i} for i in ['1', '2'] + ], + value='1' + ), + html.Div(id='body') + ]) + app.config.supress_callback_exceptions = True + + call_counts = { + 'body': Value('i', 0), + 'button-output': Value('i', 0) + } + + @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + def update_body(chapter): + call_counts['body'].value += 1 + if chapter == '1': + return [ + html.Div('Chapter 1'), + html.Button( + 'clicking this button takes forever', + id='button' + ), + html.Div(id='button-output') + ] + elif chapter == '2': + return 'Chapter 2' + else: + raise Exception('chapter is {}'.format(chapter)) + + @app.callback( + Output('button-output', 'children'), + events=[Event('button', 'click')]) + def this_callback_takes_forever(): + time.sleep(5) + call_counts['button-output'].value += 1 + return 'New value!' + + body = lambda: self.driver.find_element_by_id('body') + self.startServer(app) + + wait_for(lambda: call_counts['body'].value == 1) + time.sleep(0.5) + self.driver.find_element_by_id('button').click() + + # while that callback is resolving, switch the chapter, + # hiding the `button-output` tag + def chapter2_assertions(): + wait_for(lambda: body().text == 'Chapter 2') + self.assertEqual( + self.driver.execute_script( + 'return JSON.parse(JSON.stringify(' + 'window.store.getState().layout' + '))' + ), + { + "namespace": "dash_html_components", + "type": "Div", + "props": { + "children": [ + { + "namespace": "dash_core_components", + "type": "RadioItems", + "props": { + "value": "2", + "options": app.layout['toc'].options, + "id": app.layout['toc'].id, + } + }, + { + "namespace": "dash_html_components", + "type": "Div", + "props": { + "id": "body", + "children": "Chapter 2" + } + } + ] + } + } + ) + self.assertEqual( + self.driver.execute_script( + 'return JSON.parse(JSON.stringify(' + 'window.store.getState().paths' + '))' + ), + { + "toc": ["props", "children", 0], + "body": ["props", "children", 1] + } + ) + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[1]).click() + chapter2_assertions() + time.sleep(5) + wait_for(lambda: call_counts['button-output'].value == 1) + time.sleep(2) # liberally wait for the front-end to process request + chapter2_assertions() + assert_clean_console(self) + + def test_rendering_layout_calls_callback_once_per_output(self): + app = Dash(__name__) + call_count = Value('i', 0) + + app.config['suppress_callback_exceptions'] = True + app.layout = html.Div([ + html.Div([ + dcc.Input( + value='Input {}'.format(i), + id='input-{}'.format(i) + ) + for i in range(10) + ]), + html.Div(id='container'), + dcc.RadioItems() + ]) + + @app.callback( + Output('container', 'children'), + [Input('input-{}'.format(i), 'value') for i in range(10)]) + def dynamic_output(*args): + call_count.value += 1 + return json.dumps(args, indent=2) + + self.startServer(app) + + time.sleep(5) + + self.percy_snapshot( + name='test_rendering_layout_calls_callback_once_per_output' + ) + + self.assertEqual(call_count.value, 1) + + def test_rendering_new_content_calls_callback_once_per_output(self): + app = Dash(__name__) + call_count = Value('i', 0) + + app.config['suppress_callback_exceptions'] = True + app.layout = html.Div([ + html.Button( + id='display-content', + children='Display Content', + n_clicks=0 + ), + html.Div(id='container'), + dcc.RadioItems() + ]) + + @app.callback( + Output('container', 'children'), + [Input('display-content', 'n_clicks')]) + def display_output(n_clicks): + if n_clicks == 0: + return '' + return html.Div([ + html.Div([ + dcc.Input( + value='Input {}'.format(i), + id='input-{}'.format(i) + ) + for i in range(10) + ]), + html.Div(id='dynamic-output') + ]) + + @app.callback( + Output('dynamic-output', 'children'), + [Input('input-{}'.format(i), 'value') for i in range(10)]) + def dynamic_output(*args): + call_count.value += 1 + return json.dumps(args, indent=2) + + self.startServer(app) + + self.wait_for_element_by_css_selector('#display-content').click() + + time.sleep(5) + + self.percy_snapshot( + name='test_rendering_new_content_calls_callback_once_per_output' + ) + + self.assertEqual(call_count.value, 1) + + def test_callbacks_called_multiple_times_and_out_of_order(self): + app = Dash(__name__) + app.layout = html.Div([ + html.Button(id='input', n_clicks=0), + html.Div(id='output') + ]) + + call_count = Value('i', 0) + + @app.callback( + Output('output', 'children'), + [Input('input', 'n_clicks')]) + def update_output(n_clicks): + call_count.value = call_count.value + 1 + if n_clicks == 1: + time.sleep(4) + return n_clicks + + self.startServer(app) + button = self.wait_for_element_by_css_selector('#input') + button.click() + button.click() + time.sleep(8) + self.percy_snapshot( + name='test_callbacks_called_multiple_times_and_out_of_order' + ) + self.assertEqual(call_count.value, 3) + self.assertEqual( + self.driver.find_element_by_id('output').text, + '2' + ) + request_queue = self.driver.execute_script( + 'return window.store.getState().requestQueue' + ) + self.assertFalse(request_queue[0]['rejected']) + self.assertEqual(len(request_queue), 1) + + + def test_callbacks_with_shared_grandparent(self): + app = dash.Dash() + + app.layout = html.Div([ + html.Div(id='session-id', children='id'), + dcc.Dropdown(id='dropdown-1'), + dcc.Dropdown(id='dropdown-2'), + ]) + + options = [{'value': 'a', 'label': 'a'}] + + call_counts = { + 'dropdown_1': Value('i', 0), + 'dropdown_2': Value('i', 0) + } + + @app.callback( + Output('dropdown-1', 'options'), + [Input('dropdown-1', 'value'), + Input('session-id', 'children')]) + def dropdown_1(value, session_id): + call_counts['dropdown_1'].value += 1 + return options + + @app.callback( + Output('dropdown-2', 'options'), + [Input('dropdown-2', 'value'), + Input('session-id', 'children')]) + def dropdown_2(value, session_id): + call_counts['dropdown_2'].value += 1 + return options + + self.startServer(app) + + self.wait_for_element_by_css_selector('#session-id') + time.sleep(2) + self.assertEqual(call_counts['dropdown_1'].value, 1) + self.assertEqual(call_counts['dropdown_2'].value, 1) + + assert_clean_console(self) + + def test_callbacks_triggered_on_generated_output(self): + app = dash.Dash() + app.config['suppress_callback_exceptions'] = True + + call_counts = { + 'tab1': Value('i', 0), + 'tab2': Value('i', 0) + } + + app.layout = html.Div([ + dcc.Dropdown( + id='outer-controls', + options=[{'label': i, 'value': i} for i in ['a', 'b']], + value='a' + ), + dcc.RadioItems( + options=[ + {'label': 'Tab 1', 'value': 1}, + {'label': 'Tab 2', 'value': 2} + ], + value=1, + id='tabs', + ), + html.Div(id='tab-output') + ]) + + @app.callback(Output('tab-output', 'children'), + [Input('tabs', 'value')]) + def display_content(value): + return html.Div([ + html.Div(id='tab-{}-output'.format(value)) + ]) + + @app.callback(Output('tab-1-output', 'children'), + [Input('outer-controls', 'value')]) + def display_tab1_output(value): + call_counts['tab1'].value += 1 + return 'You have selected "{}"'.format(value) + + @app.callback(Output('tab-2-output', 'children'), + [Input('outer-controls', 'value')]) + def display_tab2_output(value): + call_counts['tab2'].value += 1 + return 'You have selected "{}"'.format(value) + + self.startServer(app) + self.wait_for_element_by_css_selector('#tab-output') + time.sleep(2) + + self.assertEqual(call_counts['tab1'].value, 1) + self.assertEqual(call_counts['tab2'].value, 0) + self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') + self.wait_for_text_to_equal('#tab-1-output', 'You have selected "a"') + + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[1]).click() + time.sleep(2) + + self.wait_for_text_to_equal('#tab-output', 'You have selected "a"') + self.wait_for_text_to_equal('#tab-2-output', 'You have selected "a"') + self.assertEqual(call_counts['tab1'].value, 1) + self.assertEqual(call_counts['tab2'].value, 1) + + assert_clean_console(self) + + def test_initialization_with_overlapping_outputs(self): + app = dash.Dash() + app.layout = html.Div([ + + html.Div(id='input-1', children='input-1'), + html.Div(id='input-2', children='input-2'), + html.Div(id='input-3', children='input-3'), + html.Div(id='input-4', children='input-4'), + html.Div(id='input-5', children='input-5'), + + html.Div(id='output-1'), + html.Div(id='output-2'), + html.Div(id='output-3'), + html.Div(id='output-4'), + + ]) + call_counts = { + 'output-1': Value('i', 0), + 'output-2': Value('i', 0), + 'output-3': Value('i', 0), + 'output-4': Value('i', 0), + } + + def generate_callback(outputid): + def callback(*args): + call_counts[outputid].value += 1 + return '{}, {}'.format(*args) + return callback + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + app.callback( + Output(outputid, 'children'), + [Input('input-{}'.format(i), 'children'), + Input('input-{}'.format(i+1), 'children')] + )(generate_callback(outputid)) + + self.startServer(app) + + self.wait_for_element_by_css_selector('#output-1') + time.sleep(5) + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + self.assertEqual(call_counts[outputid].value, 1) + self.wait_for_text_to_equal( + '#{}'.format(outputid), + "input-{}, input-{}".format(i, i+1) + ) + + def test_generate_overlapping_outputs(self): + app = dash.Dash() + app.config['suppress_callback_exceptions'] = True + block = html.Div([ + + html.Div(id='input-1', children='input-1'), + html.Div(id='input-2', children='input-2'), + html.Div(id='input-3', children='input-3'), + html.Div(id='input-4', children='input-4'), + html.Div(id='input-5', children='input-5'), + + html.Div(id='output-1'), + html.Div(id='output-2'), + html.Div(id='output-3'), + html.Div(id='output-4'), + + ]) + app.layout = html.Div([ + html.Div(id='input'), + html.Div(id='container') + ]) + + call_counts = { + 'container': Value('i', 0), + 'output-1': Value('i', 0), + 'output-2': Value('i', 0), + 'output-3': Value('i', 0), + 'output-4': Value('i', 0), + } + + @app.callback(Output('container', 'children'), + [Input('input', 'children')]) + def display_output(*args): + call_counts['container'].value += 1 + return block + + def generate_callback(outputid): + def callback(*args): + call_counts[outputid].value += 1 + return '{}, {}'.format(*args) + return callback + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + app.callback( + Output(outputid, 'children'), + [Input('input-{}'.format(i), 'children'), + Input('input-{}'.format(i+1), 'children')] + )(generate_callback(outputid)) + + self.startServer(app) + + wait_for(lambda: call_counts['container'].value == 1) + self.wait_for_element_by_css_selector('#output-1') + time.sleep(5) + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + self.assertEqual(call_counts[outputid].value, 1) + self.wait_for_text_to_equal( + '#{}'.format(outputid), + "input-{}, input-{}".format(i, i+1) + ) + self.assertEqual(call_counts['container'].value, 1) def test_update_react_version(self): import dash_renderer @@ -1823,16 +1823,18 @@ def __init__(self, _namespace): self.startServer(app) + dr_path = '/_dash-component-suites/dash_renderer' # Make sure that the Dash application is generating the right React scripts self.assertEqual( app._generate_scripts_html(), '\n' + 'src="{dr_path}/react@16.2.0.production.min.js?v={version}">\n' '\n' + 'src="{dr_path}/react-dom@16.2.0.production.min.js?v={version}">\n' ''.format( - dash_renderer.__version__)) + 'src="{dr_path}/bundle.js?v={version}">'.format( + dr_path=dr_path, + version=dash_renderer.__version__)) # Reset react version dash_renderer._set_react_version(dash_renderer._DEFAULT_REACT_VERSION) From ae8a8fdd4a1db81e1d0c80240945024ac799a41f Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 23 Feb 2018 21:06:23 -0500 Subject: [PATCH 06/10] Fix link for react-dom --- dash_renderer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash_renderer/__init__.py b/dash_renderer/__init__.py index 7bcbc19..38dbbdd 100644 --- a/dash_renderer/__init__.py +++ b/dash_renderer/__init__.py @@ -28,7 +28,7 @@ '16.2.0': { 'external_url': [ 'https://unpkg.com/react@16.2.0/umd/react.production.min.js', - 'https://unpkg.com/react@16.2.0/umd/react-dom.production.min.js' + 'https://unpkg.com/react-dom@16.2.0/umd/react-dom.production.min.js' ], 'relative_package_path': [ 'react@16.2.0.production.min.js', From e01f9cc46d971684063a1353ffd8b3be40454d3d Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 23 Feb 2018 21:16:07 -0500 Subject: [PATCH 07/10] Split out PropTypes into prop-types package for new React --- package.json | 1 + src/APIController.react.js | 5 +++-- src/AccessDenied.react.js | 3 ++- src/Authentication.react.js | 3 ++- src/TreeContainer.js | 3 ++- src/components/core/DocumentTitle.react.js | 5 +++-- src/components/core/Loading.react.js | 5 +++-- src/components/core/NotifyObservers.react.js | 3 ++- src/components/core/Toolbar.react.js | 5 +++-- 9 files changed, 21 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 447f7be..e68bead 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependency-graph": "^0.5.0", "es6-promise": "^4.1.0", "json-loader": "^0.5.4", + "prop-types": "^15.6.0", "query-string": "^4.3.2", "radium": "^0.18.2", "ramda": "^0.23.0", diff --git a/src/APIController.react.js b/src/APIController.react.js index 36a5754..57db63b 100644 --- a/src/APIController.react.js +++ b/src/APIController.react.js @@ -1,6 +1,7 @@ import {connect} from 'react-redux' import {contains, isEmpty, isNil} from 'ramda' -import React, {Component, PropTypes} from 'react'; +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; import TreeContainer from './TreeContainer'; import { computeGraphs, @@ -112,7 +113,7 @@ UnconnectedContainer.propTypes = { APP_STATES('STARTED'), APP_STATES('HYDRATED') ]), - dispatch: PropTypes.function, + dispatch: PropTypes.func, dependenciesRequest: PropTypes.object, layoutRequest: PropTypes.object, layout: PropTypes.object, diff --git a/src/AccessDenied.react.js b/src/AccessDenied.react.js index 8334530..59bbf0e 100644 --- a/src/AccessDenied.react.js +++ b/src/AccessDenied.react.js @@ -1,6 +1,7 @@ /* global window:true, document:true */ -import React, {PropTypes} from 'react'; +import React from 'react'; import {merge} from 'ramda'; +import PropTypes from 'prop-types'; import * as styles from './styles/styles.js'; import * as constants from './constants/constants.js'; diff --git a/src/Authentication.react.js b/src/Authentication.react.js index 47c2432..c71db80 100644 --- a/src/Authentication.react.js +++ b/src/Authentication.react.js @@ -1,5 +1,6 @@ /* global window:true, document:true */ -import React, {Component, PropTypes} from 'react'; +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; import {connect} from 'react-redux' import queryString from 'query-string'; import {login} from './actions/api'; diff --git a/src/TreeContainer.js b/src/TreeContainer.js index 40a7295..5c827d2 100644 --- a/src/TreeContainer.js +++ b/src/TreeContainer.js @@ -1,7 +1,8 @@ 'use strict' import R from 'ramda'; -import React, {Component, PropTypes} from 'react'; +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; import Registry from './registry'; import NotifyObservers from './components/core/NotifyObservers.react'; diff --git a/src/components/core/DocumentTitle.react.js b/src/components/core/DocumentTitle.react.js index a96087e..03afa8a 100644 --- a/src/components/core/DocumentTitle.react.js +++ b/src/components/core/DocumentTitle.react.js @@ -2,7 +2,8 @@ import {connect} from 'react-redux' import {any} from 'ramda' -import {Component, PropTypes} from 'react' +import {Component} from 'react' +import PropTypes from 'prop-types'; class DocumentTitle extends Component { constructor(props) { @@ -31,7 +32,7 @@ class DocumentTitle extends Component { DocumentTitle.propTypes = { requestQueue: PropTypes.array.required -} +}; export default connect( state => ({ diff --git a/src/components/core/Loading.react.js b/src/components/core/Loading.react.js index 22aaa1b..037d723 100644 --- a/src/components/core/Loading.react.js +++ b/src/components/core/Loading.react.js @@ -1,6 +1,7 @@ import {connect} from 'react-redux' import {any} from 'ramda' -import React, {PropTypes} from 'react' +import React from 'react' +import PropTypes from 'prop-types'; function Loading(props) { if (any(r => r.status === 'loading', props.requestQueue)) { @@ -14,7 +15,7 @@ function Loading(props) { Loading.propTypes = { requestQueue: PropTypes.array.required -} +}; export default connect( state => ({ diff --git a/src/components/core/NotifyObservers.react.js b/src/components/core/NotifyObservers.react.js index 492b9e7..182ac84 100644 --- a/src/components/core/NotifyObservers.react.js +++ b/src/components/core/NotifyObservers.react.js @@ -1,7 +1,8 @@ import {connect} from 'react-redux'; import {isEmpty} from 'ramda'; import {notifyObservers, updateProps} from '../../actions'; -import React, {PropTypes} from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; /* * NotifyObservers passes a connected `setProps` handler down to diff --git a/src/components/core/Toolbar.react.js b/src/components/core/Toolbar.react.js index c7ec325..892491b 100644 --- a/src/components/core/Toolbar.react.js +++ b/src/components/core/Toolbar.react.js @@ -1,5 +1,6 @@ import {connect} from 'react-redux'; -import React, {PropTypes} from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; import {merge} from 'ramda'; import {redo, undo} from '../../actions/index.js'; import Radium from 'radium'; @@ -91,7 +92,7 @@ function UnconnectedToolbar(props) { UnconnectedToolbar.propTypes = { history: PropTypes.object, - dispatch: PropTypes.function + dispatch: PropTypes.func, }; const Toolbar = connect( From eafd871f725bb14911924c3d7936fe04a7b04563 Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 24 Feb 2018 07:54:50 -0500 Subject: [PATCH 08/10] Update requirements and fix PropTypes from .required to .isRequired in DocumentTitle and Loading --- package-lock.json | 40 ++++++++++++---------- package.json | 6 ++-- src/components/core/DocumentTitle.react.js | 2 +- src/components/core/Loading.react.js | 2 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61c967f..518438d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,11 +106,6 @@ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" }, - "array-find": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-find/-/array-find-1.0.0.tgz", - "integrity": "sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=" - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -890,9 +885,9 @@ } }, "bowser": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.1.tgz", - "integrity": "sha512-UXti1JB6oK8hO983AImunnV6j/fqAEeDlPXh99zhsP5g32oLbxJJ6qcOaUesR+tqqhnUVQHlRJyD0dfiV0Hxaw==" + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.2.tgz", + "integrity": "sha512-fuiANC1Bqbqa/S4gmvfCt7bGBmNELMsGZj4Wg3PrP6esP66Ttoj1JSlzFlXtHyduMv07kDNmDsX6VsMWT/MLGg==" }, "brace-expansion": { "version": "1.1.8", @@ -1240,6 +1235,14 @@ "sha.js": "2.2.6" } }, + "css-in-js-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.0.tgz", + "integrity": "sha512-yuWmPMD9FLi50Xf3k8W8oO3WM1eVnxEGCldCLyfusQ+CgivFk0s23yst4ooW6tfxMuSa03S6uUEga9UhX6GRrA==", + "requires": { + "hyphenate-style-name": "1.0.2" + } + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -3001,12 +3004,12 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "inline-style-prefixer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz", - "integrity": "sha1-wVPH6I/YT+9cYC6VqBaLJ3BnH+c=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-4.0.0.tgz", + "integrity": "sha1-MKA98bNGumsfuKgSvDydq+9IAi0=", "requires": { - "bowser": "1.9.1", - "hyphenate-style-name": "1.0.2" + "bowser": "1.9.2", + "css-in-js-utils": "2.0.0" } }, "inquirer": { @@ -4038,14 +4041,13 @@ "dev": true }, "radium": { - "version": "0.18.4", - "resolved": "https://registry.npmjs.org/radium/-/radium-0.18.4.tgz", - "integrity": "sha1-pdqVc63Woq+ZtSjQe0/UA+rCylg=", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/radium/-/radium-0.22.0.tgz", + "integrity": "sha512-9zOYegr4gXfgDiVcf02Qyj8zzupmYSTtIhxvSU/42Ls1Q/+r1cisuGUUJ5m06Ha2cc/f3e5vMM80rj2l34Slew==", "requires": { - "array-find": "1.0.0", "exenv": "1.2.2", - "inline-style-prefixer": "2.0.5", - "rimraf": "2.6.2" + "inline-style-prefixer": "4.0.0", + "prop-types": "15.6.0" } }, "ramda": { diff --git a/package.json b/package.json index e68bead..4041e4d 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ "json-loader": "^0.5.4", "prop-types": "^15.6.0", "query-string": "^4.3.2", - "radium": "^0.18.2", + "radium": "^0.22.0", "ramda": "^0.23.0", - "react": "^15.4.2", - "react-dom": "^15.0.1", + "react": "^15.6.2", + "react-dom": "^15.6.2", "react-redux": "^4.4.5", "redux": "^3.4.0", "redux-actions": "^0.9.1", diff --git a/src/components/core/DocumentTitle.react.js b/src/components/core/DocumentTitle.react.js index 03afa8a..72bba80 100644 --- a/src/components/core/DocumentTitle.react.js +++ b/src/components/core/DocumentTitle.react.js @@ -31,7 +31,7 @@ class DocumentTitle extends Component { } DocumentTitle.propTypes = { - requestQueue: PropTypes.array.required + requestQueue: PropTypes.array.isRequired }; export default connect( diff --git a/src/components/core/Loading.react.js b/src/components/core/Loading.react.js index 037d723..f8686e1 100644 --- a/src/components/core/Loading.react.js +++ b/src/components/core/Loading.react.js @@ -14,7 +14,7 @@ function Loading(props) { } Loading.propTypes = { - requestQueue: PropTypes.array.required + requestQueue: PropTypes.array.isRequired }; export default connect( From b1b03ad1a49c19e8b30db3056fb4339cd070debf Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 24 Feb 2018 08:17:05 -0500 Subject: [PATCH 09/10] Update react-dom@16 url in test --- tests/test_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_render.py b/tests/test_render.py index b3f6a0d..afeee5d 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -1799,7 +1799,7 @@ def test_update_react_version(self): [{ 'external_url': [ 'https://unpkg.com/react@16.2.0/umd/react.production.min.js', - 'https://unpkg.com/react@16.2.0/umd/react-dom.production.min.js', + 'https://unpkg.com/react-dom@16.2.0/umd/react-dom.production.min.js', ], 'relative_package_path': [ 'react@16.2.0.production.min.js', From a705929e215cb1138a558d7ef6128dbe91fbe4a5 Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 24 Feb 2018 08:53:14 -0500 Subject: [PATCH 10/10] Remove url check in src --- tests/test_render.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test_render.py b/tests/test_render.py index afeee5d..174ff85 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -1823,18 +1823,5 @@ def __init__(self, _namespace): self.startServer(app) - dr_path = '/_dash-component-suites/dash_renderer' - # Make sure that the Dash application is generating the right React scripts - self.assertEqual( - app._generate_scripts_html(), - '\n' - '\n' - ''.format( - dr_path=dr_path, - version=dash_renderer.__version__)) - # Reset react version dash_renderer._set_react_version(dash_renderer._DEFAULT_REACT_VERSION)