From cdc9739144997d85862a2f75ddb487c82342bf5c Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 12 Jan 2019 17:58:34 +0100 Subject: [PATCH] Support React.memo Detect memoized elements & dereference their underlying type. Format such elements with a different annotation, similar to function types. Retain backwards compatibility with elements serialized before React.memo support was added. --- index.js | 1 + lib/elementFactory.js | 30 ++++++++++++++++---- test/backcompat.js | 24 ++++++++++++++++ test/compare.js | 6 +++- test/diff.js | 10 ++++++- test/fixtures/react/HelloMessage.jsx | 2 ++ test/format.js | 3 +- test/serialize-and-encode.js | 12 ++------ test/snapshots/diff.js.md | 41 +++++++++++++++++++++++++++ test/snapshots/diff.js.snap | Bin 2082 -> 2186 bytes test/snapshots/format.js.md | 19 +++++++++++++ test/snapshots/format.js.snap | Bin 957 -> 1008 bytes 12 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 test/backcompat.js diff --git a/index.js b/index.js index 30053ed..295ab7a 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ exports.serializerVersion = 2 exports.theme = { react: { functionType: '\u235F', + memoizedType: `\u235D`, openTag: { start: '<', end: '>', diff --git a/lib/elementFactory.js b/lib/elementFactory.js index 2697f87..43b1ef3 100644 --- a/lib/elementFactory.js +++ b/lib/elementFactory.js @@ -5,6 +5,7 @@ const diffShallow = require('./diffShallow') const escapeText = require('./escapeText') const FRAGMENT_NAME = Symbol.for('react.fragment') +const MEMO_TYPE = Symbol.for('react.memo') function factory (api, reactTags) { const tag = Symbol('@concordance/react.ElementValue') @@ -62,7 +63,11 @@ function factory (api, reactTags) { function describe (props) { const element = props.value - const type = element.type + let type = element.type + const hasMemoizedType = type.$$typeof === MEMO_TYPE + // Dereference underlying type if memoized. + if (hasMemoizedType) type = type.type + const hasTypeFn = typeof type === 'function' const typeFn = hasTypeFn ? type : null const name = hasTypeFn ? type.displayName || type.name : type @@ -78,6 +83,7 @@ function factory (api, reactTags) { return new DescribedElementValue(Object.assign({ children, + hasMemoizedType, hasProperties, hasTypeFn, name, @@ -96,6 +102,7 @@ function factory (api, reactTags) { super(props) this.isFragment = props.name === FRAGMENT_NAME this.name = props.name + this.hasMemoizedType = props.hasMemoizedType this.hasProperties = props.hasProperties this.hasTypeFn = props.hasTypeFn @@ -110,13 +117,15 @@ function factory (api, reactTags) { formatName (theme) { const formatted = api.wrapFromTheme(theme.react.tagName, this.isFragment ? 'React.Fragment' : this.name) - return this.hasTypeFn - ? formatted + theme.react.functionType - : formatted + if (this.hasMemoizedType) return formatted + theme.react.memoizedType + if (this.hasTypeFn) return formatted + theme.react.functionType + return formatted } compareNames (expected) { - return this.name === expected.name && this.hasTypeFn === expected.hasTypeFn + return this.name === expected.name && + this.hasMemoizedType === expected.hasMemoizedType && + this.hasTypeFn === expected.hasTypeFn } formatShallow (theme, indent) { @@ -216,7 +225,15 @@ function factory (api, reactTags) { } serialize () { - return [this.isFragment, this.isFragment ? null : this.name, this.hasProperties, this.hasTypeFn, super.serialize()] + // TODO: Reorder hasMemoizedType before next major release. + return [ + this.isFragment, + this.isFragment ? null : this.name, + this.hasProperties, + this.hasTypeFn, + super.serialize(), + this.hasMemoizedType + ] } } Object.defineProperty(ElementValue.prototype, 'tag', {value: tag}) @@ -328,6 +345,7 @@ function factory (api, reactTags) { super(state[4], recursor) this.isFragment = state[0] this.name = this.isFragment ? FRAGMENT_NAME : state[1] + this.hasMemoizedType = state.length === 5 ? false : state[5] this.hasProperties = state[2] this.hasTypeFn = state[3] } diff --git a/test/backcompat.js b/test/backcompat.js new file mode 100644 index 0000000..242b6e5 --- /dev/null +++ b/test/backcompat.js @@ -0,0 +1,24 @@ +import test from 'ava' +import {compareDescriptors, describe, deserialize} from 'concordance' + +import React from 'react' + +import plugin from '..' +import HelloMessage from './fixtures/react/HelloMessage' + +const plugins = [plugin] + +const equalsSerialization = (t, buffer, getValue) => { + const expected = describe(getValue(), {plugins}) + + const deserialized = deserialize(buffer, {plugins}) + t.true( + compareDescriptors(deserialized, expected), + 'the deserialized descriptor equals the expected value') +} + +test('element serialization before React.memo support was added', + equalsSerialization, + Buffer.from('AwAfAAAAAQERARJAY29uY29yZGFuY2UvcmVhY3QBAgEBAQEBAlwAAABiAAAAEwEFEBEBDEhlbGxvTWVzc2FnZQ8PEwEGEQEGT2JqZWN0AQERAQZPYmplY3QQEBAAAQ0AAQEAAQ8AEwECFAEDAAEFEQEEbmFtZRQBAwABBREBBEpvaG4=', 'base64'), // eslint-disable-line max-len + () => +) diff --git a/test/compare.js b/test/compare.js index 91c863f..7e15bd5 100644 --- a/test/compare.js +++ b/test/compare.js @@ -4,7 +4,7 @@ import React from 'react' import renderer from 'react-test-renderer' import plugin from '..' -import HelloMessage from './fixtures/react/HelloMessage' +import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage' const plugins = [plugin] const render = value => renderer.create(value).toJSON() @@ -30,6 +30,10 @@ test('react elements', macros, () => React.createElement('Foo'), () => React.createElement('Bar')) +test('memoized elements', macros, + () => , + () => ) + test('fragments', macros, () => , () => ) diff --git a/test/diff.js b/test/diff.js index 5b02113..5c06c60 100644 --- a/test/diff.js +++ b/test/diff.js @@ -4,7 +4,7 @@ import React from 'react' import renderer from 'react-test-renderer' import plugin from '..' -import HelloMessage from './fixtures/react/HelloMessage' +import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage' const plugins = [plugin] @@ -28,6 +28,14 @@ test('react elements', macros, () => arm, () => arm) +test('memoized elements', macros, + () => , + () => ) + +test('memoized elements against non-memoized elements', macros, + () => , + () => ) + test('fragments', macros, () => , () => ) diff --git a/test/fixtures/react/HelloMessage.jsx b/test/fixtures/react/HelloMessage.jsx index 2606758..a129e80 100644 --- a/test/fixtures/react/HelloMessage.jsx +++ b/test/fixtures/react/HelloMessage.jsx @@ -11,3 +11,5 @@ export default class HelloMessage extends React.Component { return
Hello
} } + +export const MemoizedHelloMessage = React.memo(HelloMessage) diff --git a/test/format.js b/test/format.js index 1c4f404..ce31ce7 100644 --- a/test/format.js +++ b/test/format.js @@ -4,7 +4,7 @@ import React from 'react' import renderer from 'react-test-renderer' import plugin from '..' -import HelloMessage from './fixtures/react/HelloMessage' +import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage' const plugins = [plugin] const format = (value, options) => concordance.format(value, Object.assign({plugins}, options)) @@ -22,6 +22,7 @@ snapshotRendered.title = prefix => `formats rendered ${prefix}` const macros = [snapshot, snapshotRendered] test('react elements', macros, () => ) +test('memoized elements', macros, () => ) test('fragments', macros, () => ) test('object properties', macros, () => { return React.createElement('Foo', {object: {baz: 'thud'}}) diff --git a/test/serialize-and-encode.js b/test/serialize-and-encode.js index ead3b46..03bbc1f 100644 --- a/test/serialize-and-encode.js +++ b/test/serialize-and-encode.js @@ -5,7 +5,7 @@ import React from 'react' import renderer from 'react-test-renderer' import plugin from '..' -import HelloMessage from './fixtures/react/HelloMessage' +import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage' const plugins = [plugin] @@ -46,15 +46,7 @@ useDeserializedRendered.title = prefix => `deserialized rendered ${prefix} is eq const macros = [useDeserialized, useDeserializedRendered] test('react elements', macros, () => ) -// TODO: Combine next two tests with `macros` array -test.failing('memoized react elements', useDeserialized, () => { - const MemoizedHelloMessage = React.memo(HelloMessage) - return -}) -test('memoized react elements', useDeserializedRendered, () => { - const MemoizedHelloMessage = React.memo(HelloMessage) - return -}) +test('memoized elements', macros, () => ) test('fragments', macros, () => ) test('object properties', macros, () => { return React.createElement('Foo', {object: {baz: 'thud'}}) diff --git a/test/snapshots/diff.js.md b/test/snapshots/diff.js.md index 7581e05..cededdb 100644 --- a/test/snapshots/diff.js.md +++ b/test/snapshots/diff.js.md @@ -181,6 +181,24 @@ Generated by [AVA](https://ava.li). ␊ ` +## diffs memoized elements + +> Snapshot 1 + + ` ` + +## diffs memoized elements against non-memoized elements + +> Snapshot 1 + + `- ` + ## diffs multiline string properties > Snapshot 1 @@ -447,6 +465,29 @@ Generated by [AVA](https://ava.li). ␊ ` +## diffs rendered memoized elements + +> Snapshot 1 + + `
␊ + Hello ␊ + ␊ + - John␊ + + Olivia␊ + ␊ +
` + +## diffs rendered memoized elements against non-memoized elements + +> Snapshot 1 + + `
␊ + Hello ␊ + ␊ + John␊ + ␊ +
` + ## diffs rendered multiline string properties > Snapshot 1 diff --git a/test/snapshots/diff.js.snap b/test/snapshots/diff.js.snap index 085e1990475e492e54f8aea0b9ac5a64f46cb8b9..7da6f68bbf8029223b0aafc357a6f7a4ab74616b 100644 GIT binary patch literal 2186 zcmV;52zB>CRzV(iRxjxGkV0e3$IDh~X7Z9)R(mPSF3QNEbpL2Xb{BoI#NqqC;zzb_g4~9H@vv}grW~IRYRufea*72=Y69!6Qgh3>uWm! zF?A=?)n^7A-ROMLd)a06dm;AD5Yumv)XOyodcu>x3jOB6TD`3ae}$M1BQ+?UvP&+| zRJs*;tUzbyhei-n8z}(nv?~-6ZE5tK@mJ-#vl_mGn4Uwb+PuTH*|wmmHguQsp!t0z zh^ZO{0I!v|dlSwS+)b<7w9b59y)ncz9BDy+-V0BdtHGY7@@wO@I#q~iJ5o!pvqcrh zNTJ&3S?ZtYO(_slS7`uLpl0ue`g?1BR4l%{{(|xmi0Kui_8K+WmQckFMbFuTReQ{D zK}>^W061Hj{V$oFEC-JoU*qA5Vk?O0yJ&@UDn`5CbZoZJU{8f5Ys{=x6EaUKF#GRZGVrOpL44NO$|w(BWcO?R#KDa8Vuy!SghRD z_#mHn+9ghU5tUkw})wyUDNScgv!}^Hqu|~6^hkG8;mlQ2mgqZdsl}CLT z6~AhvLf(zG?&m*ms)m>{UL>xmA_382Yvl(_Eyk5z3mH-t=F7QCA(g z-le-2Vmf;^0Oy}=z@AA&a2TebS>4lhWK^%LZZ!-a&H^ zuDa0R`@46Pj4TR*78xKnl758bZ%OR>XUXwh8k_rT_FU6F1u@M>THj$)7ISpR*}rAT zzn5%leivdof;8ANzv}F~^sr`=R!berhYum9)(QY9?C%au>(9wkkN?uHRH}CbV)`Z0 zK__*e{48o>u>pm`Z`4F{P0}Bb66#eoOVhOSbkmx3HYmH#g_x=;0>F{eYuUAfnR_kv zMeWZYTfPr5U5n(GLJg;Mm?Tp?R2yQu_O(Gw`A8k7txA8$UZS;l0nOgxf)468l2#%8 z5&#Az+{O-j72~~6+h|VywZ#zA0HobXJz>4|o-zRr z#&tg2lUpFBHAs7tN(NjUGg3-67T3i**%b>hRd^QwS-xSsjE!Px_uyRqBYuM)#MBX~ zEoouK(yO809U5j1sk1KDK}=JTVp-hwb6=Q(T!Za*T=EmTAf|0d_FG${_Olr3%-zaP zsj>U05L0Dk09Nvvu1K#vWgT$oY2SIR?i`4z6;dK?8O7AB%KAaisVJK{RtF%a+mSBt z4d)#=v$0aD?NHUFatBL@X)#h}*+6Ml{AnAH%Zr96{oQEiBk3U0$vCTvPI9`Nw%l=0 z`B*s^bqGnBDgc~bZVlX;k`-GDX%!Z6kI_>?(qN>AEyjNQ0!_ZVerNRoHC5ChB+WxQ zS6VnS$71o|w}NOX@z)_c#bt_*KC3>(UnzYH-|oCAQ_v96qbK4;7s z8jRqipF)EnraDN`)T;Nhn);%umqzdZ%qERb|Hl9FCuO!^DM9i99`pT{}R^@o_M zssiwNyME+N+u`VMFC8E8Ze0BcV%mp90kZi23Y0TD+@(3I9x#nHjj%u8T2ui=;+!^g^Eu~ z<{xQNR?)=EqlzyhAyTneeazR_N4%gtMgcz>lvA1$L0U?5TiM=I0k>XQH z7K>Afxc(*+?1JdJz(9OspqC$KO9(SUI2{o}SAy_jhp!@ZLwxw&x&}i3Q5eh*Lrg#- zG}#OCG?M!$vKOBV7!RGx&bCHsxG7V9y~&*l4v`1<<>a9$41^Dlvcok{V+@p!R$p2A_E zae7YFQE$PLHM)P0QOE#?pl>oBv(fJeQ{zZ{F~YIB)Kp0eH^X*%Q|m7JOhj2o>;}<^r3yFMNn3b^?d+y% zNA}+|y7O)MuAIP^K4G7dw~MaH%(yI{*hSL(OYFp%@K`*VhiKu`4zQ#NYZ33U$(}!n zT{qKy_7nI?*li zll0h|MVU?9K+|cL8M0YAg@zHGt^XosTD#br#LOlnrk9MRYQfl_->;5@$DKQm2wrc; zK*Yqk7Y`x9iVM%joNPlsihKhDM$gB-WD7o{Eh_lD`aL!Jay@E9ALsP)n)n;||I@hs M1BdoGIaeA00NN!$)c^nh literal 2082 zcmV+-2;KKVRzVhwcQEBKG*U}EGe)ZrP%^G4l8<~2W*Jx#M7|b21*ERa{ zTRw-Feuh-HG5(`=PH|gfOyZhhmq$7f(>|o2rS}HX&K5t&Y1-iFV%TgCG3BTLFmJHv zRUkZK%$=e4ed~=T1BhuUQpdm=*E*h^R=wSe1HWaqWkXCYX#nU!lY!OEU$6L5yZq{^ zOS(rPrUgj18)mC_#AvTBd&wQH+wF1(V#-zp;CyYtztncHeF7Rn?MG|M-65u3NES6? zInLRGDV*l@#Y;Z^_%g(Fp&9^>ybd;IZ>aHcVC?kfxIV(09x7BWob089 zII4c4)v0V~_Y>yAvc=jE)6+=R3BONBT{c#u>Cf=|=8qfdA*Qp`0jNLOXIyf4)!)wW z>uP7PtT_7K_h@%3OziB=+dxc3NYS~C0bzQ=yMzQV z&6hV+Msgsg1~UQZPWND#zQ|L3G^CjF%==L>hY>dIv#)iFS(lUWN};`KuoV74R_3~J8yU>w%wuA&BASH2x4k5 z3jnRX{V_R%g+)fGpLtiR42(fc*CFlqGYUDDPtPc~q0vOGCTOflnunCuY+zEEV^(C9 z({8a^cl{iQ=^Z4V#-fhIc;_QGl3z7`^P$^&5L10E0K&5A+h{!wS+oFymgK(lE{Lf+ zQqLLp%KsEBG+Qu_vC{RD1*$ia{uSvT-&a4I*|v@83E=ZKz&TEZZvraEY)WX{jEydLxU!BOWCBlhJch$#;#na#g_;m?lX zh|SjfYmcR&g-y~ENGmsYChTRijGTAr`W;A4r$bD;k(LVEuBra+v`561=MOKM^%p`+ z^?n0D2IF0tqf?#7cXv-Gc zyaKM8j?f1CPeM#CMt7dOQaTpgy8H>m^qf8bG@y?6 zd#HxCM<;?{ad_e30v}#f6n8VPH@!fUK@da)m(O!H_ZEaim}}BsU;iOIG$xd5u1OF~ z_Bb1FWD^`3!$x-q0{ap8l*Q)?KVf4>f*{?n89|7R#E&q=&yZ)N#W-IEgJ4BP3k4CI z*(j35|T0}hs0{oyLD2%r$+Ic&^Ac*Y}SAq!QZd*oJMTdxjt!yOzQWz*xPEC!vs#7@v(G0)DZHRWgA%v!wpqxI#&UTq|VTLD3SbwkWhlrajUY zNsp51B^&Z9Y93t1P+YQfMMPT2u3?4g_mG)>z9>98G(0qd_qua|e-a3KGgBq~3E~O^ zCEY;%44kN<-hw4tx_^*S$N-0+BN>mG^tj#8J`Ug9?%)(hy=N?&iWkWoobaWiJ+%)< z(oKntvpI?rZ<44d&gSH-ljAMUL)H&Dmvr&@wh8C;?YMr#pmt^Xiejf@&XAbne=R8X z=g8tpIYJCMKN*j4dFk?{^%L1KDMH8hnoc!wO14`xwd!IjBg&Ps8srC-F4<%!Eny|w zc~8}jtiNeg=iBmKI)N{9!a5}%m!A{mxGbL7g=+jMD^VF93nufBZ}_wW+!&I!DAw3y z>yNVPl`h{35|rtwu}UM5{rvvnOmg14␊ ` +## formats memoized elements + +> Snapshot 1 + + `` + ## formats multiline string properties > Snapshot 1 @@ -184,6 +192,17 @@ Generated by [AVA](https://ava.li). ␊ ` +## formats rendered memoized elements + +> Snapshot 1 + + `
␊ + Hello ␊ + ␊ + John␊ + ␊ +
` + ## formats rendered multiline string properties > Snapshot 1 diff --git a/test/snapshots/format.js.snap b/test/snapshots/format.js.snap index 99684ba6eecae5a86b4f75fa3f2d5e97513a19a1..f0c9f5365327509934976d07308d58cc5b54da90 100644 GIT binary patch delta 978 zcmV;@11-D<;Khi7A*$i ztETmH4O+H8b65E={Xx6AA0t?F8xU7IpXX?gy5Rd=Ry-ed%ein1^;oOxuLyz;2H-GmjBRcaeDdKtl@HbA_( zx!#j)dX$0DbV-?M&O1UG!J^4PT>9n5xhrX(zv%zbD}Ecl`79$?bOI0`^SJ+Ox2f)& z(wk;2hbC=%#0VDM4a67tX8RaUG`jlhyWiS17TKbVV1LmUK)mQz`(qxr+i5F*RYVrA z;cI6Ei;A)`FqjMfzCXdDOH$g|_T8pWD=iqoqD?>?d)oi|h6%qXJT;&8{eMgQRz|Ss zdms+GbzN#w+T;1w+a}0&?hF)%!CoXgBQ`H}D~ueom1sg%*?8X!;=W{72D!?wH?&E~EVBcu z0qar#GHh}ai?Tr?3JM?{AQ63tkc~b_EneH^($qFDo1oOhXN4wmXcLo>HDrzA5cGcOftTS8iX{-X(9ToBue60n6CC3+>HdX<1` zL4S&rKw({lkv)l2CJ)OlX<*e!;P9(9F{;IuIb^XZHZj7cR32(QP$e`i*nxFaVGBx< z6vx9H0i&U;M-9s$?tlvcMbJf)ihu&32!-+>OsEPh-k_^xqR?PS%FoZStu84lO~r`( zK$ttA(m;l;0hn?t%}XxH%+FJ(t_AbKmKO%6mcY}#Ixuum${dIb017fD9OnoC08JIs ABLDyZ delta 926 zcmV;P17ZB|2fYU}K~_N^Q*L2!b7*gLAa*he0sw*j8#yC8vK>L6llG_sD>xsE2mk;8 z00003b7Ep(5Mz-tE`N{WX>WJA zF*AZiS%HRLHLag((6arRyUK^@58BQB7{Q_*KwRm3o})eXa*fiXT8Vq78k88pqTN8u zD#`iZ!9o4Ww;V$iWAE-2j9}4+Kzuso!P?Bpc^`_nK2&~-W`D|O1dEyjeH5j9>h8?- z7RzR6O%6J{UmbUzzeADOqN`X~(rgPq81dA>Q;xms-lUE)Ux0|qHvPx}3MlT~+ z^c)bcZm#!an|~fdq;>Y1VRR(zZv8U{PNnzQ8xz$8e(2)nDKJ)~>P07G(sBRsr#%U+s^1+-|3> z{8bTIyoRrx5iGhKh|Pt6-=AR7B`NJ}`)<>xl@^R(QGXFu28P(v{@*uD_&wpN`Lyr< zTiUlWf<+sFIPBJSsYz*%=UZ=^Am6!5n28ZAx&(+HH_l~do%~4nme*Xj>2iq?j9^h_ zHXvqT2d5TBRt7;vMH`RQoSb~$)Z*gA^wdYa^SKlh@)C1XZI!(8GxC(U^zBfT8``8~ zme~Q-fPZx<02wy9iAC8U5d{U14v>gGM94-Tq!vZHmrYP=VseR|TTxwtx7xjPC1FG$I(qygxdbP=rEx5GGUw7H`niGErzSB<1Jl*jASmm8N1u zejv;pP-!4T*8ohpmF6XvWaj57RM&#}U`vBjOWVOWe&sz0BupS3PlJ20F?Q% A(EtDd