core.ts 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536
  1. import { assert } from "chai";
  2. import shuffle from "lodash.shuffle";
  3. import {
  4. init,
  5. classModule,
  6. propsModule,
  7. styleModule,
  8. eventListenersModule,
  9. h,
  10. toVNode,
  11. vnode,
  12. VNode,
  13. htmlDomApi,
  14. CreateHook,
  15. InsertHook,
  16. PrePatchHook,
  17. RemoveHook,
  18. InitHook,
  19. DestroyHook,
  20. UpdateHook,
  21. Key,
  22. fragment,
  23. } from "../../src/index";
  24. const hasSvgClassList = "classList" in SVGElement.prototype;
  25. const patch = init(
  26. [classModule, propsModule, eventListenersModule],
  27. undefined,
  28. { experimental: { fragments: true } }
  29. );
  30. function prop<T>(name: string) {
  31. return function (obj: { [index: string]: T }) {
  32. return obj[name];
  33. };
  34. }
  35. function map(fn: any, list: any[]) {
  36. const ret = [];
  37. for (let i = 0; i < list.length; ++i) {
  38. ret[i] = fn(list[i]);
  39. }
  40. return ret;
  41. }
  42. const inner = prop("innerHTML");
  43. describe("snabbdom", function () {
  44. let elm: any, vnode0: any;
  45. beforeEach(function () {
  46. elm = document.createElement("div");
  47. vnode0 = elm;
  48. });
  49. describe("hyperscript", function () {
  50. it("can create vnode with proper tag", function () {
  51. assert.strictEqual(h("div").sel, "div");
  52. assert.strictEqual(h("a").sel, "a");
  53. });
  54. it("can create vnode with children", function () {
  55. const vnode = h("div", [h("span#hello"), h("b.world")]);
  56. assert.strictEqual(vnode.sel, "div");
  57. const children = vnode.children as [VNode, VNode];
  58. assert.strictEqual(children[0].sel, "span#hello");
  59. assert.strictEqual(children[1].sel, "b.world");
  60. });
  61. it("can create vnode with one child vnode", function () {
  62. const vnode = h("div", h("span#hello"));
  63. assert.strictEqual(vnode.sel, "div");
  64. const children = vnode.children as [VNode];
  65. assert.strictEqual(children[0].sel, "span#hello");
  66. });
  67. it("can create vnode with props and one child vnode", function () {
  68. const vnode = h("div", {}, h("span#hello"));
  69. assert.strictEqual(vnode.sel, "div");
  70. const children = vnode.children as [VNode];
  71. assert.strictEqual(children[0].sel, "span#hello");
  72. });
  73. it("can create vnode with text content", function () {
  74. const vnode = h("a", ["I am a string"]);
  75. const children = vnode.children as [VNode];
  76. assert.strictEqual(children[0].text, "I am a string");
  77. });
  78. it("can create vnode with text content in string", function () {
  79. const vnode = h("a", "I am a string");
  80. assert.strictEqual(vnode.text, "I am a string");
  81. });
  82. it("can create vnode with props and text content in string", function () {
  83. const vnode = h("a", {}, "I am a string");
  84. assert.strictEqual(vnode.text, "I am a string");
  85. });
  86. it("can create vnode with String obj content", function () {
  87. const vnode = h("a", new String("b"));
  88. assert.equal(vnode.text, "b");
  89. });
  90. it("can create vnode with props and String obj content", function () {
  91. const vnode = h("a", {}, new String("b"));
  92. assert.equal(vnode.text, "b");
  93. });
  94. it("can create vnode with Number obj content", function () {
  95. const vnode = h("a", new Number(1));
  96. assert.equal(vnode.text, "1");
  97. });
  98. it("can create vnode with null props", function () {
  99. let vnode = h("a", null);
  100. assert.deepEqual(vnode.data, {});
  101. vnode = h("a", null, ["I am a string"]);
  102. const children = vnode.children as [VNode];
  103. assert.strictEqual(children[0].text, "I am a string");
  104. });
  105. it("can create vnode for comment", function () {
  106. const vnode = h("!", "test");
  107. assert.strictEqual(vnode.sel, "!");
  108. assert.strictEqual(vnode.text, "test");
  109. });
  110. });
  111. describe("created element", function () {
  112. it("has tag", function () {
  113. elm = patch(vnode0, h("div")).elm;
  114. assert.strictEqual(elm.tagName, "DIV");
  115. });
  116. it("has different tag and id", function () {
  117. const elm = document.createElement("div");
  118. vnode0.appendChild(elm);
  119. const vnode1 = h("span#id");
  120. const patched = patch(elm, vnode1).elm as HTMLSpanElement;
  121. assert.strictEqual(patched.tagName, "SPAN");
  122. assert.strictEqual(patched.id, "id");
  123. });
  124. it("has id", function () {
  125. elm = patch(vnode0, h("div", [h("div#unique")])).elm;
  126. assert.strictEqual(elm.firstChild.id, "unique");
  127. });
  128. it("has correct namespace", function () {
  129. const SVGNamespace = "http://www.w3.org/2000/svg";
  130. const XHTMLNamespace = "http://www.w3.org/1999/xhtml";
  131. elm = patch(vnode0, h("div", [h("div", { ns: SVGNamespace })])).elm;
  132. assert.strictEqual(elm.firstChild.namespaceURI, SVGNamespace);
  133. // verify that svg tag automatically gets svg namespace
  134. elm = patch(
  135. vnode0,
  136. h("svg", [
  137. h("foreignObject", [h("div", ["I am HTML embedded in SVG"])]),
  138. ])
  139. ).elm;
  140. assert.strictEqual(elm.namespaceURI, SVGNamespace);
  141. assert.strictEqual(elm.firstChild.namespaceURI, SVGNamespace);
  142. assert.strictEqual(
  143. elm.firstChild.firstChild.namespaceURI,
  144. XHTMLNamespace
  145. );
  146. // verify that svg tag with extra selectors gets svg namespace
  147. elm = patch(vnode0, h("svg#some-id")).elm;
  148. assert.strictEqual(elm.namespaceURI, SVGNamespace);
  149. // verify that non-svg tag beginning with 'svg' does NOT get namespace
  150. elm = patch(vnode0, h("svg-custom-el")).elm;
  151. assert.notStrictEqual(elm.namespaceURI, SVGNamespace);
  152. });
  153. it("receives classes in selector", function () {
  154. elm = patch(vnode0, h("div", [h("i.am.a.class")])).elm;
  155. assert(elm.firstChild.classList.contains("am"));
  156. assert(elm.firstChild.classList.contains("a"));
  157. assert(elm.firstChild.classList.contains("class"));
  158. });
  159. it("receives classes in class property", function () {
  160. elm = patch(
  161. vnode0,
  162. h("i", { class: { am: true, a: true, class: true, not: false } })
  163. ).elm;
  164. assert(elm.classList.contains("am"));
  165. assert(elm.classList.contains("a"));
  166. assert(elm.classList.contains("class"));
  167. assert(!elm.classList.contains("not"));
  168. });
  169. it("receives classes in selector when namespaced", function () {
  170. if (!hasSvgClassList) {
  171. this.skip();
  172. } else {
  173. elm = patch(vnode0, h("svg", [h("g.am.a.class.too")])).elm;
  174. assert(elm.firstChild.classList.contains("am"));
  175. assert(elm.firstChild.classList.contains("a"));
  176. assert(elm.firstChild.classList.contains("class"));
  177. }
  178. });
  179. it("receives classes in class property when namespaced", function () {
  180. if (!hasSvgClassList) {
  181. this.skip();
  182. } else {
  183. elm = patch(
  184. vnode0,
  185. h("svg", [
  186. h("g", {
  187. class: { am: true, a: true, class: true, not: false, too: true },
  188. }),
  189. ])
  190. ).elm;
  191. assert(elm.firstChild.classList.contains("am"));
  192. assert(elm.firstChild.classList.contains("a"));
  193. assert(elm.firstChild.classList.contains("class"));
  194. assert(!elm.firstChild.classList.contains("not"));
  195. }
  196. });
  197. it("handles classes from both selector and property", function () {
  198. elm = patch(
  199. vnode0,
  200. h("div", [h("i.has", { class: { classes: true } })])
  201. ).elm;
  202. assert(elm.firstChild.classList.contains("has"), "has `has` class");
  203. assert(
  204. elm.firstChild.classList.contains("classes"),
  205. "has `classes` class"
  206. );
  207. });
  208. it("can create elements with text content", function () {
  209. elm = patch(vnode0, h("div", ["I am a string"])).elm;
  210. assert.strictEqual(elm.innerHTML, "I am a string");
  211. });
  212. it("can create elements with span and text content", function () {
  213. elm = patch(vnode0, h("a", [h("span"), "I am a string"])).elm;
  214. assert.strictEqual(elm.childNodes[0].tagName, "SPAN");
  215. assert.strictEqual(elm.childNodes[1].textContent, "I am a string");
  216. });
  217. it("can create vnode with array String obj content", function () {
  218. elm = patch(vnode0, h("a", ["b", new String("c")])).elm;
  219. assert.strictEqual(elm.innerHTML, "bc");
  220. });
  221. it("can create elements with props", function () {
  222. elm = patch(vnode0, h("a", { props: { src: "http://localhost/" } })).elm;
  223. assert.strictEqual(elm.src, "http://localhost/");
  224. });
  225. it("can create an element created inside an iframe", function (done) {
  226. // Only run if srcdoc is supported.
  227. const frame = document.createElement("iframe");
  228. if (typeof frame.srcdoc !== "undefined") {
  229. frame.srcdoc = "<div>Thing 1</div>";
  230. frame.onload = function () {
  231. const div0 = frame.contentDocument!.body.querySelector(
  232. "div"
  233. ) as HTMLDivElement;
  234. patch(div0, h("div", "Thing 2"));
  235. const div1 = frame.contentDocument!.body.querySelector(
  236. "div"
  237. ) as HTMLDivElement;
  238. assert.strictEqual(div1.textContent, "Thing 2");
  239. frame.remove();
  240. done();
  241. };
  242. document.body.appendChild(frame);
  243. } else {
  244. done();
  245. }
  246. });
  247. it("is a patch of the root element", function () {
  248. const elmWithIdAndClass = document.createElement("div");
  249. elmWithIdAndClass.id = "id";
  250. elmWithIdAndClass.className = "class";
  251. const vnode1 = h("div#id.class", [h("span", "Hi")]);
  252. elm = patch(elmWithIdAndClass, vnode1).elm;
  253. assert.strictEqual(elm, elmWithIdAndClass);
  254. assert.strictEqual(elm.tagName, "DIV");
  255. assert.strictEqual(elm.id, "id");
  256. assert.strictEqual(elm.className, "class");
  257. });
  258. it("can create comments", function () {
  259. elm = patch(vnode0, h("!", "test")).elm;
  260. assert.strictEqual(elm.nodeType, document.COMMENT_NODE);
  261. assert.strictEqual(elm.textContent, "test");
  262. });
  263. });
  264. describe("created document fragment", function () {
  265. it("is an instance of DocumentFragment", function () {
  266. const vnode1 = fragment(["I am", h("span", [" a", " fragment"])]);
  267. elm = patch(vnode0, vnode1).elm;
  268. assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
  269. assert.strictEqual(elm.textContent, "I am a fragment");
  270. });
  271. });
  272. describe("patching an element", function () {
  273. it("changes the elements classes", function () {
  274. const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
  275. const vnode2 = h("i", { class: { i: true, am: true, horse: false } });
  276. patch(vnode0, vnode1);
  277. elm = patch(vnode1, vnode2).elm;
  278. assert(elm.classList.contains("i"));
  279. assert(elm.classList.contains("am"));
  280. assert(!elm.classList.contains("horse"));
  281. });
  282. it("changes classes in selector", function () {
  283. const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
  284. const vnode2 = h("i", { class: { i: true, am: true, horse: false } });
  285. patch(vnode0, vnode1);
  286. elm = patch(vnode1, vnode2).elm;
  287. assert(elm.classList.contains("i"));
  288. assert(elm.classList.contains("am"));
  289. assert(!elm.classList.contains("horse"));
  290. });
  291. it("preserves memoized classes", function () {
  292. const cachedClass = { i: true, am: true, horse: false };
  293. const vnode1 = h("i", { class: cachedClass });
  294. const vnode2 = h("i", { class: cachedClass });
  295. elm = patch(vnode0, vnode1).elm;
  296. assert(elm.classList.contains("i"));
  297. assert(elm.classList.contains("am"));
  298. assert(!elm.classList.contains("horse"));
  299. elm = patch(vnode1, vnode2).elm;
  300. assert(elm.classList.contains("i"));
  301. assert(elm.classList.contains("am"));
  302. assert(!elm.classList.contains("horse"));
  303. });
  304. it("removes missing classes", function () {
  305. const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
  306. const vnode2 = h("i", { class: { i: true, am: true } });
  307. patch(vnode0, vnode1);
  308. elm = patch(vnode1, vnode2).elm;
  309. assert(elm.classList.contains("i"));
  310. assert(elm.classList.contains("am"));
  311. assert(!elm.classList.contains("horse"));
  312. });
  313. it("changes an elements props", function () {
  314. const vnode1 = h("a", { props: { src: "http://other/" } });
  315. const vnode2 = h("a", { props: { src: "http://localhost/" } });
  316. patch(vnode0, vnode1);
  317. elm = patch(vnode1, vnode2).elm;
  318. assert.strictEqual(elm.src, "http://localhost/");
  319. });
  320. it("can set prop value to `0`", function () {
  321. const patch = init([propsModule, styleModule]);
  322. const view = (scrollTop: number) =>
  323. h(
  324. "div",
  325. {
  326. style: { height: "100px", overflowY: "scroll" },
  327. props: { scrollTop },
  328. },
  329. [h("div", { style: { height: "200px" } })]
  330. );
  331. const vnode1 = view(0);
  332. const mountPoint = document.body.appendChild(
  333. document.createElement("div")
  334. );
  335. const { elm } = patch(mountPoint, vnode1);
  336. if (!(elm instanceof HTMLDivElement)) throw new Error();
  337. assert.strictEqual(elm.scrollTop, 0);
  338. const vnode2 = view(20);
  339. patch(vnode1, vnode2);
  340. assert.isAtLeast(elm.scrollTop, 18);
  341. assert.isAtMost(elm.scrollTop, 20);
  342. const vnode3 = view(0);
  343. patch(vnode2, vnode3);
  344. assert.strictEqual(elm.scrollTop, 0);
  345. document.body.removeChild(mountPoint);
  346. });
  347. it("can set prop value to empty string", function () {
  348. const vnode1 = h("p", { props: { textContent: "foo" } });
  349. const { elm } = patch(vnode0, vnode1);
  350. if (!(elm instanceof HTMLParagraphElement)) throw new Error();
  351. assert.strictEqual(elm.textContent, "foo");
  352. const vnode2 = h("p", { props: { textContent: "" } });
  353. patch(vnode1, vnode2);
  354. assert.strictEqual(elm.textContent, "");
  355. });
  356. it("preserves memoized props", function () {
  357. const cachedProps = { src: "http://other/" };
  358. const vnode1 = h("a", { props: cachedProps });
  359. const vnode2 = h("a", { props: cachedProps });
  360. elm = patch(vnode0, vnode1).elm;
  361. assert.strictEqual(elm.src, "http://other/");
  362. elm = patch(vnode1, vnode2).elm;
  363. assert.strictEqual(elm.src, "http://other/");
  364. });
  365. it("removes custom props", function () {
  366. const vnode1 = h("a", { props: { src: "http://other/" } });
  367. const vnode2 = h("a");
  368. patch(vnode0, vnode1);
  369. patch(vnode1, vnode2);
  370. assert.strictEqual(elm.src, undefined);
  371. });
  372. it("cannot remove native props", function () {
  373. const vnode1 = h("a", { props: { href: "http://example.com/" } });
  374. const vnode2 = h("a");
  375. const { elm: elm1 } = patch(vnode0, vnode1);
  376. if (!(elm1 instanceof HTMLAnchorElement)) throw new Error();
  377. assert.strictEqual(elm1.href, "http://example.com/");
  378. const { elm: elm2 } = patch(vnode1, vnode2);
  379. if (!(elm2 instanceof HTMLAnchorElement)) throw new Error();
  380. assert.strictEqual(elm2.href, "http://example.com/");
  381. });
  382. it("does not delete custom props", function () {
  383. const vnode1 = h("p", { props: { a: "foo" } });
  384. const vnode2 = h("p");
  385. const { elm } = patch(vnode0, vnode1);
  386. if (!(elm instanceof HTMLParagraphElement)) throw new Error();
  387. assert.strictEqual((elm as any).a, "foo");
  388. patch(vnode1, vnode2);
  389. assert.strictEqual((elm as any).a, "foo");
  390. });
  391. describe("custom elements", function () {
  392. if ("customElements" in window) {
  393. describe("customized built-in element", function () {
  394. const isSafari = /^((?!chrome|android).)*safari/i.test(
  395. navigator.userAgent
  396. );
  397. if (!isSafari) {
  398. class A extends HTMLParagraphElement {}
  399. class B extends HTMLParagraphElement {}
  400. before(function () {
  401. if ("customElements" in window) {
  402. customElements.define("p-a", A, { extends: "p" });
  403. customElements.define("p-b", B, { extends: "p" });
  404. }
  405. });
  406. it("can create custom elements", function () {
  407. if ("customElements" in window) {
  408. const vnode1 = h("p", { is: "p-a" });
  409. elm = patch(vnode0, vnode1).elm;
  410. assert(elm instanceof A);
  411. } else {
  412. this.skip();
  413. }
  414. });
  415. it("handles changing is attribute", function () {
  416. const vnode1 = h("p", { is: "p-a" });
  417. const vnode2 = h("p", { is: "p-b" });
  418. elm = patch(vnode0, vnode1).elm;
  419. assert(elm instanceof A);
  420. elm = patch(vnode1, vnode2).elm;
  421. assert(elm instanceof B);
  422. });
  423. } else {
  424. it.skip("safari does not support customized built-in elements", () => {
  425. assert(false);
  426. });
  427. }
  428. });
  429. } else {
  430. it.skip("browser does not support custom elements", () => {
  431. assert(false);
  432. });
  433. }
  434. });
  435. describe("using toVNode()", function () {
  436. it("can remove previous children of the root element", function () {
  437. const h2 = document.createElement("h2");
  438. h2.textContent = "Hello";
  439. const prevElm = document.createElement("div");
  440. prevElm.id = "id";
  441. prevElm.className = "class";
  442. prevElm.appendChild(h2);
  443. const nextVNode = h("div#id.class", [h("span", "Hi")]);
  444. elm = patch(toVNode(prevElm), nextVNode).elm;
  445. assert.strictEqual(elm, prevElm);
  446. assert.strictEqual(elm.tagName, "DIV");
  447. assert.strictEqual(elm.id, "id");
  448. assert.strictEqual(elm.className, "class");
  449. assert.strictEqual(elm.childNodes.length, 1);
  450. assert.strictEqual(elm.childNodes[0].tagName, "SPAN");
  451. assert.strictEqual(elm.childNodes[0].textContent, "Hi");
  452. });
  453. it("can support patching in a DocumentFragment", function () {
  454. const prevElm = document.createDocumentFragment();
  455. const nextVNode = vnode(
  456. "",
  457. {},
  458. [h("div#id.class", [h("span", "Hi")])],
  459. undefined,
  460. prevElm as any
  461. );
  462. elm = patch(toVNode(prevElm), nextVNode).elm;
  463. assert.strictEqual(elm, prevElm);
  464. assert.strictEqual(elm.nodeType, 11);
  465. assert.strictEqual(elm.childNodes.length, 1);
  466. assert.strictEqual(elm.childNodes[0].tagName, "DIV");
  467. assert.strictEqual(elm.childNodes[0].id, "id");
  468. assert.strictEqual(elm.childNodes[0].className, "class");
  469. assert.strictEqual(elm.childNodes[0].childNodes.length, 1);
  470. assert.strictEqual(elm.childNodes[0].childNodes[0].tagName, "SPAN");
  471. assert.strictEqual(elm.childNodes[0].childNodes[0].textContent, "Hi");
  472. });
  473. it("can remove some children of the root element", function () {
  474. const h2 = document.createElement("h2");
  475. h2.textContent = "Hello";
  476. const prevElm = document.createElement("div");
  477. prevElm.id = "id";
  478. prevElm.className = "class";
  479. const text = document.createTextNode("Foobar");
  480. const reference = {};
  481. (text as any).testProperty = reference; // ensures we dont recreate the Text Node
  482. prevElm.appendChild(text);
  483. prevElm.appendChild(h2);
  484. const nextVNode = h("div#id.class", ["Foobar"]);
  485. elm = patch(toVNode(prevElm), nextVNode).elm;
  486. assert.strictEqual(elm, prevElm);
  487. assert.strictEqual(elm.tagName, "DIV");
  488. assert.strictEqual(elm.id, "id");
  489. assert.strictEqual(elm.className, "class");
  490. assert.strictEqual(elm.childNodes.length, 1);
  491. assert.strictEqual(elm.childNodes[0].nodeType, 3);
  492. assert.strictEqual(elm.childNodes[0].wholeText, "Foobar");
  493. assert.strictEqual(elm.childNodes[0].testProperty, reference);
  494. });
  495. it("can remove text elements", function () {
  496. const h2 = document.createElement("h2");
  497. h2.textContent = "Hello";
  498. const prevElm = document.createElement("div");
  499. prevElm.id = "id";
  500. prevElm.className = "class";
  501. const text = document.createTextNode("Foobar");
  502. prevElm.appendChild(text);
  503. prevElm.appendChild(h2);
  504. const nextVNode = h("div#id.class", [h("h2", "Hello")]);
  505. elm = patch(toVNode(prevElm), nextVNode).elm;
  506. assert.strictEqual(elm, prevElm);
  507. assert.strictEqual(elm.tagName, "DIV");
  508. assert.strictEqual(elm.id, "id");
  509. assert.strictEqual(elm.className, "class");
  510. assert.strictEqual(elm.childNodes.length, 1);
  511. assert.strictEqual(elm.childNodes[0].nodeType, 1);
  512. assert.strictEqual(elm.childNodes[0].textContent, "Hello");
  513. });
  514. it("can work with domApi", function () {
  515. const domApi = {
  516. ...htmlDomApi,
  517. tagName: function (elm: Element) {
  518. return "x-" + elm.tagName.toUpperCase();
  519. },
  520. };
  521. const h2 = document.createElement("h2");
  522. h2.id = "hx";
  523. h2.setAttribute("data-env", "xyz");
  524. const text = document.createTextNode("Foobar");
  525. const elm = document.createElement("div");
  526. elm.id = "id";
  527. elm.className = "class other";
  528. elm.setAttribute("data", "value");
  529. elm.appendChild(h2);
  530. elm.appendChild(text);
  531. const vnode = toVNode(elm, domApi);
  532. assert.strictEqual(vnode.sel, "x-div#id.class.other");
  533. assert.deepEqual(vnode.data, { attrs: { data: "value" } });
  534. const children = vnode.children as [VNode, VNode];
  535. assert.strictEqual(children[0].sel, "x-h2#hx");
  536. assert.deepEqual(children[0].data, { dataset: { env: "xyz" } });
  537. assert.strictEqual(children[1].text, "Foobar");
  538. });
  539. it("can parsing dataset and attrs", function () {
  540. const onlyAttrs = document.createElement("div");
  541. onlyAttrs.setAttribute("foo", "bar");
  542. assert.deepEqual(toVNode(onlyAttrs).data, { attrs: { foo: "bar" } });
  543. const onlyDataset = document.createElement("div");
  544. onlyDataset.setAttribute("data-foo", "bar");
  545. assert.deepEqual(toVNode(onlyDataset).data, {
  546. dataset: { foo: "bar" },
  547. });
  548. const onlyDatasets2 = document.createElement("div");
  549. onlyDatasets2.dataset.foo = "bar";
  550. assert.deepEqual(toVNode(onlyDatasets2).data, {
  551. dataset: { foo: "bar" },
  552. });
  553. const bothAttrsAndDatasets = document.createElement("div");
  554. bothAttrsAndDatasets.setAttribute("foo", "bar");
  555. bothAttrsAndDatasets.setAttribute("data-foo", "bar");
  556. bothAttrsAndDatasets.dataset.again = "again";
  557. assert.deepEqual(toVNode(bothAttrsAndDatasets).data, {
  558. attrs: { foo: "bar" },
  559. dataset: { foo: "bar", again: "again" },
  560. });
  561. });
  562. });
  563. describe("updating children with keys", function () {
  564. function spanNum(n?: null | Key) {
  565. if (n == null) {
  566. return n;
  567. } else if (typeof n === "string") {
  568. return h("span", {}, n);
  569. } else if (typeof n === "number") {
  570. return h("span", { key: n }, n.toString());
  571. } else {
  572. return h("span", { key: n }, "symbol");
  573. }
  574. }
  575. describe("addition of elements", function () {
  576. it("appends elements", function () {
  577. const vnode1 = h("span", [1].map(spanNum));
  578. const vnode2 = h("span", [1, 2, 3].map(spanNum));
  579. elm = patch(vnode0, vnode1).elm;
  580. assert.strictEqual(elm.children.length, 1);
  581. elm = patch(vnode1, vnode2).elm;
  582. assert.strictEqual(elm.children.length, 3);
  583. assert.strictEqual(elm.children[1].innerHTML, "2");
  584. assert.strictEqual(elm.children[2].innerHTML, "3");
  585. });
  586. it("prepends elements", function () {
  587. const vnode1 = h("span", [4, 5].map(spanNum));
  588. const vnode2 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  589. elm = patch(vnode0, vnode1).elm;
  590. assert.strictEqual(elm.children.length, 2);
  591. elm = patch(vnode1, vnode2).elm;
  592. assert.deepEqual(map(inner, elm.children), ["1", "2", "3", "4", "5"]);
  593. });
  594. it("add elements in the middle", function () {
  595. const vnode1 = h("span", [1, 2, 4, 5].map(spanNum));
  596. const vnode2 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  597. elm = patch(vnode0, vnode1).elm;
  598. assert.strictEqual(elm.children.length, 4);
  599. assert.strictEqual(elm.children.length, 4);
  600. elm = patch(vnode1, vnode2).elm;
  601. assert.deepEqual(map(inner, elm.children), ["1", "2", "3", "4", "5"]);
  602. });
  603. it("add elements at begin and end", function () {
  604. const vnode1 = h("span", [2, 3, 4].map(spanNum));
  605. const vnode2 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  606. elm = patch(vnode0, vnode1).elm;
  607. assert.strictEqual(elm.children.length, 3);
  608. elm = patch(vnode1, vnode2).elm;
  609. assert.deepEqual(map(inner, elm.children), ["1", "2", "3", "4", "5"]);
  610. });
  611. it("adds children to parent with no children", function () {
  612. const vnode1 = h("span", { key: "span" });
  613. const vnode2 = h("span", { key: "span" }, [1, 2, 3].map(spanNum));
  614. elm = patch(vnode0, vnode1).elm;
  615. assert.strictEqual(elm.children.length, 0);
  616. elm = patch(vnode1, vnode2).elm;
  617. assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
  618. });
  619. it("removes all children from parent", function () {
  620. const vnode1 = h("span", { key: "span" }, [1, 2, 3].map(spanNum));
  621. const vnode2 = h("span", { key: "span" });
  622. elm = patch(vnode0, vnode1).elm;
  623. assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
  624. elm = patch(vnode1, vnode2).elm;
  625. assert.strictEqual(elm.children.length, 0);
  626. });
  627. it("update one child with same key but different sel", function () {
  628. const vnode1 = h("span", { key: "span" }, [1, 2, 3].map(spanNum));
  629. const vnode2 = h("span", { key: "span" }, [
  630. spanNum(1),
  631. h("i", { key: 2 }, "2"),
  632. spanNum(3),
  633. ]);
  634. elm = patch(vnode0, vnode1).elm;
  635. assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
  636. elm = patch(vnode1, vnode2).elm;
  637. assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
  638. assert.strictEqual(elm.children.length, 3);
  639. assert.strictEqual(elm.children[1].tagName, "I");
  640. });
  641. });
  642. describe("removal of elements", function () {
  643. it("removes elements from the beginning", function () {
  644. const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  645. const vnode2 = h("span", [3, 4, 5].map(spanNum));
  646. elm = patch(vnode0, vnode1).elm;
  647. assert.strictEqual(elm.children.length, 5);
  648. elm = patch(vnode1, vnode2).elm;
  649. assert.deepEqual(map(inner, elm.children), ["3", "4", "5"]);
  650. });
  651. it("removes elements from the end", function () {
  652. const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  653. const vnode2 = h("span", [1, 2, 3].map(spanNum));
  654. elm = patch(vnode0, vnode1).elm;
  655. assert.strictEqual(elm.children.length, 5);
  656. elm = patch(vnode1, vnode2).elm;
  657. assert.strictEqual(elm.children.length, 3);
  658. assert.strictEqual(elm.children[0].innerHTML, "1");
  659. assert.strictEqual(elm.children[1].innerHTML, "2");
  660. assert.strictEqual(elm.children[2].innerHTML, "3");
  661. });
  662. it("removes elements from the middle", function () {
  663. const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  664. const vnode2 = h("span", [1, 2, 4, 5].map(spanNum));
  665. elm = patch(vnode0, vnode1).elm;
  666. assert.strictEqual(elm.children.length, 5);
  667. elm = patch(vnode1, vnode2).elm;
  668. assert.strictEqual(elm.children.length, 4);
  669. assert.deepEqual(elm.children[0].innerHTML, "1");
  670. assert.strictEqual(elm.children[0].innerHTML, "1");
  671. assert.strictEqual(elm.children[1].innerHTML, "2");
  672. assert.strictEqual(elm.children[2].innerHTML, "4");
  673. assert.strictEqual(elm.children[3].innerHTML, "5");
  674. });
  675. });
  676. describe("element reordering", function () {
  677. it("moves element forward", function () {
  678. const vnode1 = h("span", [1, 2, 3, 4].map(spanNum));
  679. const vnode2 = h("span", [2, 3, 1, 4].map(spanNum));
  680. elm = patch(vnode0, vnode1).elm;
  681. assert.strictEqual(elm.children.length, 4);
  682. elm = patch(vnode1, vnode2).elm;
  683. assert.strictEqual(elm.children.length, 4);
  684. assert.strictEqual(elm.children[0].innerHTML, "2");
  685. assert.strictEqual(elm.children[1].innerHTML, "3");
  686. assert.strictEqual(elm.children[2].innerHTML, "1");
  687. assert.strictEqual(elm.children[3].innerHTML, "4");
  688. });
  689. it("moves element to end", function () {
  690. const vnode1 = h("span", [1, 2, 3].map(spanNum));
  691. const vnode2 = h("span", [2, 3, 1].map(spanNum));
  692. elm = patch(vnode0, vnode1).elm;
  693. assert.strictEqual(elm.children.length, 3);
  694. elm = patch(vnode1, vnode2).elm;
  695. assert.strictEqual(elm.children.length, 3);
  696. assert.strictEqual(elm.children[0].innerHTML, "2");
  697. assert.strictEqual(elm.children[1].innerHTML, "3");
  698. assert.strictEqual(elm.children[2].innerHTML, "1");
  699. });
  700. it("moves element backwards", function () {
  701. const vnode1 = h("span", [1, 2, 3, 4].map(spanNum));
  702. const vnode2 = h("span", [1, 4, 2, 3].map(spanNum));
  703. elm = patch(vnode0, vnode1).elm;
  704. assert.strictEqual(elm.children.length, 4);
  705. elm = patch(vnode1, vnode2).elm;
  706. assert.strictEqual(elm.children.length, 4);
  707. assert.strictEqual(elm.children[0].innerHTML, "1");
  708. assert.strictEqual(elm.children[1].innerHTML, "4");
  709. assert.strictEqual(elm.children[2].innerHTML, "2");
  710. assert.strictEqual(elm.children[3].innerHTML, "3");
  711. });
  712. it("swaps first and last", function () {
  713. const vnode1 = h("span", [1, 2, 3, 4].map(spanNum));
  714. const vnode2 = h("span", [4, 2, 3, 1].map(spanNum));
  715. elm = patch(vnode0, vnode1).elm;
  716. assert.strictEqual(elm.children.length, 4);
  717. elm = patch(vnode1, vnode2).elm;
  718. assert.strictEqual(elm.children.length, 4);
  719. assert.strictEqual(elm.children[0].innerHTML, "4");
  720. assert.strictEqual(elm.children[1].innerHTML, "2");
  721. assert.strictEqual(elm.children[2].innerHTML, "3");
  722. assert.strictEqual(elm.children[3].innerHTML, "1");
  723. });
  724. });
  725. describe("combinations of additions, removals and reorderings", function () {
  726. it("move to left and replace", function () {
  727. const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum));
  728. const vnode2 = h("span", [4, 1, 2, 3, 6].map(spanNum));
  729. elm = patch(vnode0, vnode1).elm;
  730. assert.strictEqual(elm.children.length, 5);
  731. elm = patch(vnode1, vnode2).elm;
  732. assert.strictEqual(elm.children.length, 5);
  733. assert.strictEqual(elm.children[0].innerHTML, "4");
  734. assert.strictEqual(elm.children[1].innerHTML, "1");
  735. assert.strictEqual(elm.children[2].innerHTML, "2");
  736. assert.strictEqual(elm.children[3].innerHTML, "3");
  737. assert.strictEqual(elm.children[4].innerHTML, "6");
  738. });
  739. it("moves to left and leaves hole", function () {
  740. const vnode1 = h("span", [1, 4, 5].map(spanNum));
  741. const vnode2 = h("span", [4, 6].map(spanNum));
  742. elm = patch(vnode0, vnode1).elm;
  743. assert.strictEqual(elm.children.length, 3);
  744. elm = patch(vnode1, vnode2).elm;
  745. assert.deepEqual(map(inner, elm.children), ["4", "6"]);
  746. });
  747. it("handles moved and set to undefined element ending at the end", function () {
  748. const vnode1 = h("span", [2, 4, 5].map(spanNum));
  749. const vnode2 = h("span", [4, 5, 3].map(spanNum));
  750. elm = patch(vnode0, vnode1).elm;
  751. assert.strictEqual(elm.children.length, 3);
  752. elm = patch(vnode1, vnode2).elm;
  753. assert.strictEqual(elm.children.length, 3);
  754. assert.strictEqual(elm.children[0].innerHTML, "4");
  755. assert.strictEqual(elm.children[1].innerHTML, "5");
  756. assert.strictEqual(elm.children[2].innerHTML, "3");
  757. });
  758. it("moves a key in non-keyed nodes with a size up", function () {
  759. const vnode1 = h("span", [1, "a", "b", "c"].map(spanNum));
  760. const vnode2 = h("span", ["d", "a", "b", "c", 1, "e"].map(spanNum));
  761. elm = patch(vnode0, vnode1).elm;
  762. assert.strictEqual(elm.childNodes.length, 4);
  763. assert.strictEqual(elm.textContent, "1abc");
  764. elm = patch(vnode1, vnode2).elm;
  765. assert.strictEqual(elm.childNodes.length, 6);
  766. assert.strictEqual(elm.textContent, "dabc1e");
  767. });
  768. it("accepts symbol as key", function () {
  769. const vnode1 = h("span", [Symbol()].map(spanNum));
  770. const vnode2 = h(
  771. "span",
  772. [Symbol("1"), Symbol("2"), Symbol("3")].map(spanNum)
  773. );
  774. elm = patch(vnode0, vnode1).elm;
  775. assert.equal(elm.children.length, 1);
  776. elm = patch(vnode1, vnode2).elm;
  777. assert.equal(elm.children.length, 3);
  778. assert.equal(elm.children[1].innerHTML, "symbol");
  779. assert.equal(elm.children[2].innerHTML, "symbol");
  780. });
  781. });
  782. it("reverses elements", function () {
  783. const vnode1 = h("span", [1, 2, 3, 4, 5, 6, 7, 8].map(spanNum));
  784. const vnode2 = h("span", [8, 7, 6, 5, 4, 3, 2, 1].map(spanNum));
  785. elm = patch(vnode0, vnode1).elm;
  786. assert.strictEqual(elm.children.length, 8);
  787. elm = patch(vnode1, vnode2).elm;
  788. assert.deepEqual(map(inner, elm.children), [
  789. "8",
  790. "7",
  791. "6",
  792. "5",
  793. "4",
  794. "3",
  795. "2",
  796. "1",
  797. ]);
  798. });
  799. it("something", function () {
  800. const vnode1 = h("span", [0, 1, 2, 3, 4, 5].map(spanNum));
  801. const vnode2 = h("span", [4, 3, 2, 1, 5, 0].map(spanNum));
  802. elm = patch(vnode0, vnode1).elm;
  803. assert.strictEqual(elm.children.length, 6);
  804. elm = patch(vnode1, vnode2).elm;
  805. assert.deepEqual(map(inner, elm.children), [
  806. "4",
  807. "3",
  808. "2",
  809. "1",
  810. "5",
  811. "0",
  812. ]);
  813. });
  814. it("handles random shuffles", function () {
  815. let n;
  816. let i;
  817. const arr = [];
  818. const opacities: string[] = [];
  819. const elms = 14;
  820. const samples = 5;
  821. function spanNumWithOpacity(n: number, o: string) {
  822. return h("span", { key: n, style: { opacity: o } }, n.toString());
  823. }
  824. for (n = 0; n < elms; ++n) {
  825. arr[n] = n;
  826. }
  827. for (n = 0; n < samples; ++n) {
  828. const vnode1 = h(
  829. "span",
  830. arr.map(function (n) {
  831. return spanNumWithOpacity(n, "1");
  832. })
  833. );
  834. const shufArr = shuffle(arr.slice(0));
  835. let elm: HTMLDivElement | HTMLSpanElement =
  836. document.createElement("div");
  837. elm = patch(elm, vnode1).elm as HTMLSpanElement;
  838. for (i = 0; i < elms; ++i) {
  839. assert.strictEqual(elm.children[i].innerHTML, i.toString());
  840. opacities[i] = Math.random().toFixed(5).toString();
  841. }
  842. const vnode2 = h(
  843. "span",
  844. arr.map(function (n) {
  845. return spanNumWithOpacity(shufArr[n], opacities[n]);
  846. })
  847. );
  848. elm = patch(vnode1, vnode2).elm as HTMLSpanElement;
  849. for (i = 0; i < elms; ++i) {
  850. assert.strictEqual(
  851. elm.children[i].innerHTML,
  852. shufArr[i].toString()
  853. );
  854. const opacity = (elm.children[i] as HTMLSpanElement).style.opacity;
  855. assert.strictEqual(opacities[i].indexOf(opacity), 0);
  856. }
  857. }
  858. });
  859. it("supports null/undefined children", function () {
  860. const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
  861. const vnode2 = h(
  862. "i",
  863. [null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(
  864. spanNum
  865. )
  866. );
  867. elm = patch(vnode0, vnode1).elm;
  868. assert.strictEqual(elm.children.length, 6);
  869. elm = patch(vnode1, vnode2).elm;
  870. assert.deepEqual(map(inner, elm.children), [
  871. "2",
  872. "1",
  873. "0",
  874. "5",
  875. "4",
  876. "3",
  877. ]);
  878. });
  879. it("supports all null/undefined children", function () {
  880. const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
  881. const vnode2 = h("i", [null, null, undefined, null, null, undefined]);
  882. const vnode3 = h("i", [5, 4, 3, 2, 1, 0].map(spanNum));
  883. patch(vnode0, vnode1);
  884. elm = patch(vnode1, vnode2).elm;
  885. assert.strictEqual(elm.children.length, 0);
  886. elm = patch(vnode2, vnode3).elm;
  887. assert.deepEqual(map(inner, elm.children), [
  888. "5",
  889. "4",
  890. "3",
  891. "2",
  892. "1",
  893. "0",
  894. ]);
  895. });
  896. it("handles random shuffles with null/undefined children", function () {
  897. let i;
  898. let j;
  899. let r;
  900. let len;
  901. let arr;
  902. const maxArrLen = 15;
  903. const samples = 5;
  904. let vnode1 = vnode0;
  905. let vnode2;
  906. for (i = 0; i < samples; ++i, vnode1 = vnode2) {
  907. len = Math.floor(Math.random() * maxArrLen);
  908. arr = [];
  909. for (j = 0; j < len; ++j) {
  910. if ((r = Math.random()) < 0.5) arr[j] = String(j);
  911. else if (r < 0.75) arr[j] = null;
  912. else arr[j] = undefined;
  913. }
  914. shuffle(arr);
  915. vnode2 = h("div", arr.map(spanNum));
  916. elm = patch(vnode1, vnode2).elm;
  917. assert.deepEqual(
  918. map(inner, elm.children),
  919. arr.filter(function (x) {
  920. return x != null;
  921. })
  922. );
  923. }
  924. });
  925. });
  926. describe("updating children without keys", function () {
  927. it("appends elements", function () {
  928. const vnode1 = h("div", [h("span", "Hello")]);
  929. const vnode2 = h("div", [h("span", "Hello"), h("span", "World")]);
  930. elm = patch(vnode0, vnode1).elm;
  931. assert.deepEqual(map(inner, elm.children), ["Hello"]);
  932. elm = patch(vnode1, vnode2).elm;
  933. assert.deepEqual(map(inner, elm.children), ["Hello", "World"]);
  934. });
  935. it("handles unmoved text nodes", function () {
  936. const vnode1 = h("div", ["Text", h("span", "Span")]);
  937. const vnode2 = h("div", ["Text", h("span", "Span")]);
  938. elm = patch(vnode0, vnode1).elm;
  939. assert.strictEqual(elm.childNodes[0].textContent, "Text");
  940. elm = patch(vnode1, vnode2).elm;
  941. assert.strictEqual(elm.childNodes[0].textContent, "Text");
  942. });
  943. it("handles changing text children", function () {
  944. const vnode1 = h("div", ["Text", h("span", "Span")]);
  945. const vnode2 = h("div", ["Text2", h("span", "Span")]);
  946. elm = patch(vnode0, vnode1).elm;
  947. assert.strictEqual(elm.childNodes[0].textContent, "Text");
  948. elm = patch(vnode1, vnode2).elm;
  949. assert.strictEqual(elm.childNodes[0].textContent, "Text2");
  950. });
  951. it("handles unmoved comment nodes", function () {
  952. const vnode1 = h("div", [h("!", "Text"), h("span", "Span")]);
  953. const vnode2 = h("div", [h("!", "Text"), h("span", "Span")]);
  954. elm = patch(vnode0, vnode1).elm;
  955. assert.strictEqual(elm.childNodes[0].textContent, "Text");
  956. elm = patch(vnode1, vnode2).elm;
  957. assert.strictEqual(elm.childNodes[0].textContent, "Text");
  958. });
  959. it("handles changing comment text", function () {
  960. const vnode1 = h("div", [h("!", "Text"), h("span", "Span")]);
  961. const vnode2 = h("div", [h("!", "Text2"), h("span", "Span")]);
  962. elm = patch(vnode0, vnode1).elm;
  963. assert.strictEqual(elm.childNodes[0].textContent, "Text");
  964. elm = patch(vnode1, vnode2).elm;
  965. assert.strictEqual(elm.childNodes[0].textContent, "Text2");
  966. });
  967. it("handles changing empty comment", function () {
  968. const vnode1 = h("div", [h("!"), h("span", "Span")]);
  969. const vnode2 = h("div", [h("!", "Test"), h("span", "Span")]);
  970. elm = patch(vnode0, vnode1).elm;
  971. assert.strictEqual(elm.childNodes[0].textContent, "");
  972. elm = patch(vnode1, vnode2).elm;
  973. assert.strictEqual(elm.childNodes[0].textContent, "Test");
  974. });
  975. it("prepends element", function () {
  976. const vnode1 = h("div", [h("span", "World")]);
  977. const vnode2 = h("div", [h("span", "Hello"), h("span", "World")]);
  978. elm = patch(vnode0, vnode1).elm;
  979. assert.deepEqual(map(inner, elm.children), ["World"]);
  980. elm = patch(vnode1, vnode2).elm;
  981. assert.deepEqual(map(inner, elm.children), ["Hello", "World"]);
  982. });
  983. it("prepends element of different tag type", function () {
  984. const vnode1 = h("div", [h("span", "World")]);
  985. const vnode2 = h("div", [h("div", "Hello"), h("span", "World")]);
  986. elm = patch(vnode0, vnode1).elm;
  987. assert.deepEqual(map(inner, elm.children), ["World"]);
  988. elm = patch(vnode1, vnode2).elm;
  989. assert.deepEqual(map(prop("tagName"), elm.children), ["DIV", "SPAN"]);
  990. assert.deepEqual(map(inner, elm.children), ["Hello", "World"]);
  991. });
  992. it("removes elements", function () {
  993. const vnode1 = h("div", [
  994. h("span", "One"),
  995. h("span", "Two"),
  996. h("span", "Three"),
  997. ]);
  998. const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
  999. elm = patch(vnode0, vnode1).elm;
  1000. assert.deepEqual(map(inner, elm.children), ["One", "Two", "Three"]);
  1001. elm = patch(vnode1, vnode2).elm;
  1002. assert.deepEqual(map(inner, elm.children), ["One", "Three"]);
  1003. });
  1004. it("removes a single text node", function () {
  1005. const vnode1 = h("div", "One");
  1006. const vnode2 = h("div");
  1007. patch(vnode0, vnode1);
  1008. assert.strictEqual(elm.textContent, "One");
  1009. patch(vnode1, vnode2);
  1010. assert.strictEqual(elm.textContent, "");
  1011. });
  1012. it("removes a single text node when children are updated", function () {
  1013. const vnode1 = h("div", "One");
  1014. const vnode2 = h("div", [h("div", "Two"), h("span", "Three")]);
  1015. patch(vnode0, vnode1);
  1016. assert.strictEqual(elm.textContent, "One");
  1017. patch(vnode1, vnode2);
  1018. assert.deepEqual(map(prop("textContent"), elm.childNodes), [
  1019. "Two",
  1020. "Three",
  1021. ]);
  1022. });
  1023. it("removes a text node among other elements", function () {
  1024. const vnode1 = h("div", ["One", h("span", "Two")]);
  1025. const vnode2 = h("div", [h("div", "Three")]);
  1026. patch(vnode0, vnode1);
  1027. assert.deepEqual(map(prop("textContent"), elm.childNodes), [
  1028. "One",
  1029. "Two",
  1030. ]);
  1031. patch(vnode1, vnode2);
  1032. assert.strictEqual(elm.childNodes.length, 1);
  1033. assert.strictEqual(elm.childNodes[0].tagName, "DIV");
  1034. assert.strictEqual(elm.childNodes[0].textContent, "Three");
  1035. });
  1036. it("reorders elements", function () {
  1037. const vnode1 = h("div", [
  1038. h("span", "One"),
  1039. h("div", "Two"),
  1040. h("b", "Three"),
  1041. ]);
  1042. const vnode2 = h("div", [
  1043. h("b", "Three"),
  1044. h("span", "One"),
  1045. h("div", "Two"),
  1046. ]);
  1047. elm = patch(vnode0, vnode1).elm;
  1048. assert.deepEqual(map(inner, elm.children), ["One", "Two", "Three"]);
  1049. elm = patch(vnode1, vnode2).elm;
  1050. assert.deepEqual(map(prop("tagName"), elm.children), [
  1051. "B",
  1052. "SPAN",
  1053. "DIV",
  1054. ]);
  1055. assert.deepEqual(map(inner, elm.children), ["Three", "One", "Two"]);
  1056. });
  1057. it("supports null/undefined children", function () {
  1058. const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
  1059. const vnode2 = h("i", [
  1060. h("i", "2"),
  1061. undefined,
  1062. undefined,
  1063. h("i", "1"),
  1064. undefined,
  1065. ]);
  1066. const vnode3 = h("i", [
  1067. null,
  1068. h("i", "1"),
  1069. undefined,
  1070. null,
  1071. h("i", "2"),
  1072. undefined,
  1073. null,
  1074. ]);
  1075. elm = patch(vnode0, vnode1).elm;
  1076. assert.deepEqual(map(inner, elm.children), ["1", "2"]);
  1077. elm = patch(vnode1, vnode2).elm;
  1078. assert.deepEqual(map(inner, elm.children), ["2", "1"]);
  1079. elm = patch(vnode2, vnode3).elm;
  1080. assert.deepEqual(map(inner, elm.children), ["1", "2"]);
  1081. });
  1082. it("supports all null/undefined children", function () {
  1083. const vnode1 = h("i", [h("i", "1"), h("i", "2")]);
  1084. const vnode2 = h("i", [null, undefined]);
  1085. const vnode3 = h("i", [h("i", "2"), h("i", "1")]);
  1086. patch(vnode0, vnode1);
  1087. elm = patch(vnode1, vnode2).elm;
  1088. assert.strictEqual(elm.children.length, 0);
  1089. elm = patch(vnode2, vnode3).elm;
  1090. assert.deepEqual(map(inner, elm.children), ["2", "1"]);
  1091. });
  1092. });
  1093. });
  1094. describe("patching a fragment", function () {
  1095. it("can patch on document fragments", function () {
  1096. let firstChild: HTMLElement;
  1097. const root = document.createElement("div");
  1098. const vnode1 = fragment(["I am", h("span", [" a", " fragment"])]);
  1099. const vnode2 = h("div", ["I am an element"]);
  1100. const vnode3 = fragment(["fragment ", "again"]);
  1101. root.appendChild(vnode0);
  1102. firstChild = root.firstChild as HTMLElement;
  1103. assert.strictEqual(firstChild, vnode0);
  1104. elm = patch(vnode0, vnode1).elm;
  1105. firstChild = root.firstChild as HTMLElement;
  1106. assert.strictEqual(firstChild.textContent, "I am");
  1107. assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
  1108. assert.strictEqual(elm.parent, root);
  1109. elm = patch(vnode1, vnode2).elm;
  1110. firstChild = root.firstChild as HTMLElement;
  1111. assert.strictEqual(firstChild.tagName, "DIV");
  1112. assert.strictEqual(firstChild.textContent, "I am an element");
  1113. assert.strictEqual(elm.tagName, "DIV");
  1114. assert.strictEqual(elm.textContent, "I am an element");
  1115. assert.strictEqual(elm.parentNode, root);
  1116. elm = patch(vnode2, vnode3).elm;
  1117. firstChild = root.firstChild as HTMLElement;
  1118. assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
  1119. assert.strictEqual(firstChild.textContent, "fragment ");
  1120. assert.strictEqual(elm.parent, root);
  1121. });
  1122. it("allows a document fragment as a container", function () {
  1123. const vnode0 = document.createDocumentFragment();
  1124. const vnode1 = fragment(["I", "am", "a", h("span", ["fragment"])]);
  1125. const vnode2 = h("div", "I am an element");
  1126. elm = patch(vnode0, vnode1).elm;
  1127. assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
  1128. elm = patch(vnode1, vnode2).elm;
  1129. assert.strictEqual(elm.tagName, "DIV");
  1130. });
  1131. });
  1132. describe("hooks", function () {
  1133. describe("element hooks", function () {
  1134. it("calls `create` listener before inserted into parent but after children", function () {
  1135. const result = [];
  1136. const cb: CreateHook = (empty, vnode) => {
  1137. assert(vnode.elm instanceof Element);
  1138. assert.strictEqual(vnode.elm.children.length, 2);
  1139. assert.strictEqual(vnode.elm.parentNode, null);
  1140. result.push(vnode);
  1141. };
  1142. const vnode1 = h("div", [
  1143. h("span", "First sibling"),
  1144. h("div", { hook: { create: cb } }, [
  1145. h("span", "Child 1"),
  1146. h("span", "Child 2"),
  1147. ]),
  1148. h("span", "Can't touch me"),
  1149. ]);
  1150. patch(vnode0, vnode1);
  1151. assert.strictEqual(1, result.length);
  1152. });
  1153. it("calls `insert` listener after both parents, siblings and children have been inserted", function () {
  1154. const result = [];
  1155. const cb: InsertHook = (vnode) => {
  1156. assert(vnode.elm instanceof Element);
  1157. assert.strictEqual(vnode.elm.children.length, 2);
  1158. assert.strictEqual(vnode.elm.parentNode!.children.length, 3);
  1159. result.push(vnode);
  1160. };
  1161. const vnode1 = h("div", [
  1162. h("span", "First sibling"),
  1163. h("div", { hook: { insert: cb } }, [
  1164. h("span", "Child 1"),
  1165. h("span", "Child 2"),
  1166. ]),
  1167. h("span", "Can touch me"),
  1168. ]);
  1169. patch(vnode0, vnode1);
  1170. assert.strictEqual(1, result.length);
  1171. });
  1172. it("calls `prepatch` listener", function () {
  1173. const result = [];
  1174. const cb: PrePatchHook = (oldVnode, vnode) => {
  1175. assert.strictEqual(oldVnode, vnode1.children![1]);
  1176. assert.strictEqual(vnode, vnode2.children![1]);
  1177. result.push(vnode);
  1178. };
  1179. const vnode1 = h("div", [
  1180. h("span", "First sibling"),
  1181. h("div", { hook: { prepatch: cb } }, [
  1182. h("span", "Child 1"),
  1183. h("span", "Child 2"),
  1184. ]),
  1185. ]);
  1186. const vnode2 = h("div", [
  1187. h("span", "First sibling"),
  1188. h("div", { hook: { prepatch: cb } }, [
  1189. h("span", "Child 1"),
  1190. h("span", "Child 2"),
  1191. ]),
  1192. ]);
  1193. patch(vnode0, vnode1);
  1194. patch(vnode1, vnode2);
  1195. assert.strictEqual(result.length, 1);
  1196. });
  1197. it("calls `postpatch` after `prepatch` listener", function () {
  1198. let pre = 0;
  1199. let post = 0;
  1200. function preCb() {
  1201. pre++;
  1202. }
  1203. function postCb() {
  1204. assert.strictEqual(pre, post + 1);
  1205. post++;
  1206. }
  1207. const vnode1 = h("div", [
  1208. h("span", "First sibling"),
  1209. h("div", { hook: { prepatch: preCb, postpatch: postCb } }, [
  1210. h("span", "Child 1"),
  1211. h("span", "Child 2"),
  1212. ]),
  1213. ]);
  1214. const vnode2 = h("div", [
  1215. h("span", "First sibling"),
  1216. h("div", { hook: { prepatch: preCb, postpatch: postCb } }, [
  1217. h("span", "Child 1"),
  1218. h("span", "Child 2"),
  1219. ]),
  1220. ]);
  1221. patch(vnode0, vnode1);
  1222. patch(vnode1, vnode2);
  1223. assert.strictEqual(pre, 1);
  1224. assert.strictEqual(post, 1);
  1225. });
  1226. it("calls `update` listener", function () {
  1227. const result1: VNode[] = [];
  1228. const result2: VNode[] = [];
  1229. function cb(result: VNode[], oldVnode: VNode, vnode: VNode) {
  1230. if (result.length > 0) {
  1231. console.log(result[result.length - 1]);
  1232. console.log(oldVnode);
  1233. assert.strictEqual(result[result.length - 1], oldVnode);
  1234. }
  1235. result.push(vnode);
  1236. }
  1237. const vnode1 = h("div", [
  1238. h("span", "First sibling"),
  1239. h("div", { hook: { update: cb.bind(null, result1) } }, [
  1240. h("span", "Child 1"),
  1241. h("span", { hook: { update: cb.bind(null, result2) } }, "Child 2"),
  1242. ]),
  1243. ]);
  1244. const vnode2 = h("div", [
  1245. h("span", "First sibling"),
  1246. h("div", { hook: { update: cb.bind(null, result1) } }, [
  1247. h("span", "Child 1"),
  1248. h("span", { hook: { update: cb.bind(null, result2) } }, "Child 2"),
  1249. ]),
  1250. ]);
  1251. patch(vnode0, vnode1);
  1252. patch(vnode1, vnode2);
  1253. assert.strictEqual(result1.length, 1);
  1254. assert.strictEqual(result2.length, 1);
  1255. });
  1256. it("calls `remove` listener", function () {
  1257. const result = [];
  1258. const cb: RemoveHook = (vnode, rm) => {
  1259. const parent = vnode.elm!.parentNode as HTMLDivElement;
  1260. assert(vnode.elm instanceof Element);
  1261. assert.strictEqual((vnode.elm as HTMLDivElement).children.length, 2);
  1262. assert.strictEqual(parent.children.length, 2);
  1263. result.push(vnode);
  1264. rm();
  1265. assert.strictEqual(parent.children.length, 1);
  1266. };
  1267. const vnode1 = h("div", [
  1268. h("span", "First sibling"),
  1269. h("div", { hook: { remove: cb } }, [
  1270. h("span", "Child 1"),
  1271. h("span", "Child 2"),
  1272. ]),
  1273. ]);
  1274. const vnode2 = h("div", [h("span", "First sibling")]);
  1275. patch(vnode0, vnode1);
  1276. patch(vnode1, vnode2);
  1277. assert.strictEqual(1, result.length);
  1278. });
  1279. it("calls `destroy` listener when patching text node over node with children", function () {
  1280. let calls = 0;
  1281. function cb() {
  1282. calls++;
  1283. }
  1284. const vnode1 = h("div", [
  1285. h("div", { hook: { destroy: cb } }, [h("span", "Child 1")]),
  1286. ]);
  1287. const vnode2 = h("div", "Text node");
  1288. patch(vnode0, vnode1);
  1289. patch(vnode1, vnode2);
  1290. assert.strictEqual(calls, 1);
  1291. });
  1292. it("calls `init` and `prepatch` listeners on root", function () {
  1293. let count = 0;
  1294. const init: InitHook = (vnode) => {
  1295. assert.strictEqual(vnode, vnode2);
  1296. count += 1;
  1297. };
  1298. const prepatch: PrePatchHook = (oldVnode, vnode) => {
  1299. assert.strictEqual(vnode, vnode1);
  1300. count += 1;
  1301. };
  1302. const vnode1 = h("div", { hook: { init: init, prepatch: prepatch } });
  1303. patch(vnode0, vnode1);
  1304. assert.strictEqual(1, count);
  1305. const vnode2 = h("span", { hook: { init: init, prepatch: prepatch } });
  1306. patch(vnode1, vnode2);
  1307. assert.strictEqual(2, count);
  1308. });
  1309. it("removes element when all remove listeners are done", function () {
  1310. let rm1, rm2, rm3;
  1311. const patch = init([
  1312. {
  1313. remove: function (_, rm) {
  1314. rm1 = rm;
  1315. },
  1316. },
  1317. {
  1318. remove: function (_, rm) {
  1319. rm2 = rm;
  1320. },
  1321. },
  1322. ]);
  1323. const vnode1 = h("div", [
  1324. h("a", {
  1325. hook: {
  1326. remove: function (_, rm) {
  1327. rm3 = rm;
  1328. },
  1329. },
  1330. }),
  1331. ]);
  1332. const vnode2 = h("div", []);
  1333. elm = patch(vnode0, vnode1).elm;
  1334. assert.strictEqual(elm.children.length, 1);
  1335. elm = patch(vnode1, vnode2).elm;
  1336. assert.strictEqual(elm.children.length, 1);
  1337. (rm1 as any)();
  1338. assert.strictEqual(elm.children.length, 1);
  1339. (rm3 as any)();
  1340. assert.strictEqual(elm.children.length, 1);
  1341. (rm2 as any)();
  1342. assert.strictEqual(elm.children.length, 0);
  1343. });
  1344. it("invokes remove hook on replaced root", function () {
  1345. const result = [];
  1346. const parent = document.createElement("div");
  1347. const vnode0 = document.createElement("div");
  1348. parent.appendChild(vnode0);
  1349. const cb: RemoveHook = (vnode, rm) => {
  1350. result.push(vnode);
  1351. rm();
  1352. };
  1353. const vnode1 = h("div", { hook: { remove: cb } }, [
  1354. h("b", "Child 1"),
  1355. h("i", "Child 2"),
  1356. ]);
  1357. const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
  1358. patch(vnode0, vnode1);
  1359. patch(vnode1, vnode2);
  1360. assert.strictEqual(1, result.length);
  1361. });
  1362. });
  1363. describe("module hooks", function () {
  1364. it("invokes `pre` and `post` hook", function () {
  1365. const result: string[] = [];
  1366. const patch = init([
  1367. {
  1368. pre: function () {
  1369. result.push("pre");
  1370. },
  1371. },
  1372. {
  1373. post: function () {
  1374. result.push("post");
  1375. },
  1376. },
  1377. ]);
  1378. const vnode1 = h("div");
  1379. patch(vnode0, vnode1);
  1380. assert.deepEqual(result, ["pre", "post"]);
  1381. });
  1382. it("invokes global `destroy` hook for all removed children", function () {
  1383. const result = [];
  1384. const cb: DestroyHook = (vnode) => {
  1385. result.push(vnode);
  1386. };
  1387. const vnode1 = h("div", [
  1388. h("span", "First sibling"),
  1389. h("div", [
  1390. h("span", { hook: { destroy: cb } }, "Child 1"),
  1391. h("span", "Child 2"),
  1392. ]),
  1393. ]);
  1394. const vnode2 = h("div");
  1395. patch(vnode0, vnode1);
  1396. patch(vnode1, vnode2);
  1397. assert.strictEqual(result.length, 1);
  1398. });
  1399. it("handles text vnodes with `undefined` `data` property", function () {
  1400. const vnode1 = h("div", [" "]);
  1401. const vnode2 = h("div", []);
  1402. patch(vnode0, vnode1);
  1403. patch(vnode1, vnode2);
  1404. });
  1405. it("invokes `destroy` module hook for all removed children", function () {
  1406. let created = 0;
  1407. let destroyed = 0;
  1408. const patch = init([
  1409. {
  1410. create: function () {
  1411. created++;
  1412. },
  1413. },
  1414. {
  1415. destroy: function () {
  1416. destroyed++;
  1417. },
  1418. },
  1419. ]);
  1420. const vnode1 = h("div", [
  1421. h("span", "First sibling"),
  1422. h("div", [h("span", "Child 1"), h("span", "Child 2")]),
  1423. ]);
  1424. const vnode2 = h("div");
  1425. patch(vnode0, vnode1);
  1426. patch(vnode1, vnode2);
  1427. assert.strictEqual(created, 4);
  1428. assert.strictEqual(destroyed, 4);
  1429. });
  1430. it("does not invoke `create` and `remove` module hook for text nodes", function () {
  1431. let created = 0;
  1432. let removed = 0;
  1433. const patch = init([
  1434. {
  1435. create: function () {
  1436. created++;
  1437. },
  1438. },
  1439. {
  1440. remove: function () {
  1441. removed++;
  1442. },
  1443. },
  1444. ]);
  1445. const vnode1 = h("div", [
  1446. h("span", "First child"),
  1447. "",
  1448. h("span", "Third child"),
  1449. ]);
  1450. const vnode2 = h("div");
  1451. patch(vnode0, vnode1);
  1452. patch(vnode1, vnode2);
  1453. assert.strictEqual(created, 2);
  1454. assert.strictEqual(removed, 2);
  1455. });
  1456. it("does not invoke `destroy` module hook for text nodes", function () {
  1457. let created = 0;
  1458. let destroyed = 0;
  1459. const patch = init([
  1460. {
  1461. create: function () {
  1462. created++;
  1463. },
  1464. },
  1465. {
  1466. destroy: function () {
  1467. destroyed++;
  1468. },
  1469. },
  1470. ]);
  1471. const vnode1 = h("div", [
  1472. h("span", "First sibling"),
  1473. h("div", [h("span", "Child 1"), h("span", ["Text 1", "Text 2"])]),
  1474. ]);
  1475. const vnode2 = h("div");
  1476. patch(vnode0, vnode1);
  1477. patch(vnode1, vnode2);
  1478. assert.strictEqual(created, 4);
  1479. assert.strictEqual(destroyed, 4);
  1480. });
  1481. });
  1482. });
  1483. describe("short circuiting", function () {
  1484. it("does not update strictly equal vnodes", function () {
  1485. const result = [];
  1486. const cb: UpdateHook = (vnode) => {
  1487. result.push(vnode);
  1488. };
  1489. const vnode1 = h("div", [
  1490. h("span", { hook: { update: cb } }, "Hello"),
  1491. h("span", "there"),
  1492. ]);
  1493. patch(vnode0, vnode1);
  1494. patch(vnode1, vnode1);
  1495. assert.strictEqual(result.length, 0);
  1496. });
  1497. it("does not update strictly equal children", function () {
  1498. const result = [];
  1499. function cb(vnode: VNode) {
  1500. result.push(vnode);
  1501. }
  1502. const vnode1 = h("div", [
  1503. h("span", { hook: { patch: cb } as any }, "Hello"),
  1504. h("span", "there"),
  1505. ]);
  1506. const vnode2 = h("div");
  1507. vnode2.children = vnode1.children;
  1508. patch(vnode0, vnode1);
  1509. patch(vnode1, vnode2);
  1510. assert.strictEqual(result.length, 0);
  1511. });
  1512. });
  1513. });