mutation.spec.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { beVisible, haveText, html, test } from '../utils'
  2. test('element side effects are cleaned up after the elements are removed',
  3. html`
  4. <div x-data="{ foo: 1, bar: 1 }">
  5. <button @click="bar++">bar</button>
  6. <a href="#" @click.prevent="$refs.span.remove()">remove</a>
  7. <span x-text="(() => { foo = foo + 1; return bar })" x-ref="span"></span>
  8. <h1 x-text="foo"></h1>
  9. <h2 x-text="bar"></h2>
  10. </div>
  11. `,
  12. ({ get }) => {
  13. get('h1').should(haveText('2'))
  14. get('h2').should(haveText('1'))
  15. get('button').click()
  16. get('h1').should(haveText('3'))
  17. get('h2').should(haveText('2'))
  18. get('a').click()
  19. get('button').click()
  20. get('h1').should(haveText('3'))
  21. get('h2').should(haveText('3'))
  22. }
  23. )
  24. test('nested element side effects are cleaned up after the parent is removed',
  25. html`
  26. <div x-data="{ foo: 1, bar: 1 }">
  27. <button @click="bar++">bar</button>
  28. <a href="#" @click.prevent="$refs.article.remove()">remove</a>
  29. <article x-ref="article">
  30. <span x-text="(() => { foo = foo + 1; return bar })"></span>
  31. </article>
  32. <h1 x-text="foo"></h1>
  33. <h2 x-text="bar"></h2>
  34. </div>
  35. `,
  36. ({ get }) => {
  37. get('h1').should(haveText('2'))
  38. get('h2').should(haveText('1'))
  39. get('button').click()
  40. get('h1').should(haveText('3'))
  41. get('h2').should(haveText('2'))
  42. get('a').click()
  43. get('button').click()
  44. get('h1').should(haveText('3'))
  45. get('h2').should(haveText('3'))
  46. }
  47. )
  48. test('element magic-based side effects are cleaned up after the element is removed',
  49. html`
  50. <div x-data="{ foo: 1, bar: 1 }">
  51. <button @click="foo++">foo</button>
  52. <a href="#" @click.prevent="$refs.span.remove()">remove</a>
  53. <span x-init="$watch('foo', () => bar++)" x-ref="span"></span>
  54. <h1 x-text="foo"></h1>
  55. <h2 x-text="bar"></h2>
  56. </div>
  57. `,
  58. ({ get }) => {
  59. get('h1').should(haveText('1'))
  60. get('h2').should(haveText('1'))
  61. get('button').click()
  62. get('h1').should(haveText('2'))
  63. get('h2').should(haveText('2'))
  64. get('a').click()
  65. get('button').click()
  66. get('h1').should(haveText('3'))
  67. get('h2').should(haveText('2'))
  68. }
  69. )
  70. test('can mutate directive value',
  71. html`
  72. <div x-data="{ foo: 'bar', bar: 'baz' }">
  73. <button @click="$refs.target.setAttribute('x-text', 'bar')">change text</button>
  74. <span x-text="foo" x-ref="target"></span>
  75. </div>
  76. `,
  77. ({ get }) => {
  78. get('span').should(haveText('bar'))
  79. get('button').click()
  80. get('span').should(haveText('baz'))
  81. }
  82. )
  83. test('can add new directive',
  84. html`
  85. <div x-data="{ foo: 'bar' }">
  86. <button @click="$refs.target.setAttribute('x-text', 'foo')">change text</button>
  87. <span x-ref="target"></span>
  88. </div>
  89. `,
  90. ({ get }) => {
  91. get('span').should(haveText(''))
  92. get('button').click()
  93. get('span').should(haveText('bar'))
  94. }
  95. )
  96. test('can pause and queue mutations for later resuming/flushing',
  97. html`
  98. <div x-data="{ foo: 1 }">
  99. <button x-on:click="setTimeout(() => foo++)" x-ref="btn">foo</button>
  100. <h1 x-text="foo"></h1>
  101. <a href="#" @click="$refs.btn.removeAttribute('x-on:click')" id="remove">remove</a>
  102. <a href="#" @click="$refs.btn.setAttribute('x-on:click', 'foo++')" id="add">add</a>
  103. <a href="#" @click="Alpine.deferMutations()" id="defer">add</a>
  104. <a href="#" @click="Alpine.flushAndStopDeferringMutations()" id="flush">add</a>
  105. </div>
  106. `,
  107. ({ get }) => {
  108. get('h1').should(haveText('1'))
  109. get('button').click()
  110. get('h1').should(haveText('2'))
  111. get('#remove').click()
  112. get('button').click()
  113. get('h1').should(haveText('2'))
  114. get('#defer').click()
  115. get('#add').click()
  116. get('button').click()
  117. get('h1').should(haveText('2'))
  118. get('#flush').click()
  119. get('button').click()
  120. get('h1').should(haveText('3'))
  121. }
  122. )
  123. test('does not initialise components twice when contained in multiple mutations',
  124. html`
  125. <div x-data="{
  126. foo: 0,
  127. bar: 0,
  128. test() {
  129. container = document.createElement('div')
  130. this.$root.appendChild(container)
  131. alpineElement = document.createElement('span')
  132. alpineElement.setAttribute('x-data', '{init() {this.bar++}}')
  133. alpineElement.setAttribute('x-init', 'foo++')
  134. container.appendChild(alpineElement)
  135. }
  136. }">
  137. <span id="one" x-text="foo"></span>
  138. <span id="two" x-text="bar"></span>
  139. <button @click="test">Test</button>
  140. </div>
  141. `,
  142. ({ get }) => {
  143. get('span#one').should(haveText('0'))
  144. get('span#two').should(haveText('0'))
  145. get('button').click()
  146. get('span#one').should(haveText('1'))
  147. get('span#two').should(haveText('1'))
  148. }
  149. )
  150. test('directives keep working when node is moved into a different one',
  151. html`
  152. <div x-data="{
  153. foo: 0,
  154. mutate() {
  155. let button = document.getElementById('one')
  156. button.remove()
  157. let container = document.createElement('p')
  158. container.appendChild(button)
  159. this.$root.appendChild(container)
  160. }
  161. }">
  162. <button id="one" @click="foo++">increment</button>
  163. <button id="two" @click="mutate()">Mutate</button>
  164. <span x-text="foo"></span>
  165. </div>
  166. `,
  167. ({ get }) => {
  168. get('span').should(haveText('0'))
  169. get('button#one').click()
  170. get('span').should(haveText('1'))
  171. get('button#two').click()
  172. get('p').should(beVisible())
  173. get('button#one').click()
  174. get('span').should(haveText('2'))
  175. }
  176. )
  177. test('no side effects when directives are added to an element that is removed afterwards',
  178. html`
  179. <div x-data="{
  180. foo: 0,
  181. mutate() {
  182. let span = document.createElement('span')
  183. span.setAttribute('x-on:keydown.a.window', 'foo = foo+1')
  184. let container = document.getElementById('container')
  185. container.appendChild(span)
  186. container.remove()
  187. }
  188. }">
  189. <button @click="mutate()">Mutate</button>
  190. <p id="container"></p>
  191. <input type="text">
  192. <span x-text="foo"></span>
  193. </div>
  194. `,
  195. ({ get }) => {
  196. get('span').should(haveText('0'))
  197. get('button').click()
  198. get('input').type('a')
  199. get('span').should(haveText('0'))
  200. }
  201. )
  202. test(
  203. "previously initialized elements are not reinitialized on being moved",
  204. html`
  205. <script>
  206. let count = 0;
  207. document.addEventListener('alpine:init', () => {
  208. Alpine.directive('test', el => {
  209. if (count++ > 3) return;
  210. el.textContent = count;
  211. let wrapper = document.createElement('div');
  212. wrapper.setAttribute('class', 'bg-blue-300 p-8');
  213. el.parentNode.replaceChild(wrapper, el);
  214. wrapper.appendChild(el);
  215. });
  216. });
  217. </script>
  218. <div x-data>
  219. <div class="bg-red-300 w-32 h-32" x-test></div>
  220. </div>
  221. `,
  222. ({ get }) => {
  223. get("[x-test]").should(haveText("1"));
  224. }
  225. );
  226. test(
  227. "previously initialized elements are not cleaned up on being moved",
  228. html`
  229. <script>
  230. let count = 0;
  231. document.addEventListener("alpine:init", () => {
  232. Alpine.directive("test", (el, _, { cleanup }) => {
  233. el.textContent = "Initialized";
  234. cleanup(() => {
  235. el.textContent = "Cleaned up";
  236. });
  237. Alpine.nextTick(() => {
  238. el.parentNode.replaceChildren(el);
  239. })
  240. });
  241. });
  242. </script>
  243. <div x-data>
  244. <div class="bg-red-300 w-32 h-32" x-test></div>
  245. </div>
  246. `,
  247. ({ get }) => {
  248. get("[x-test]").should(haveText("Initialized"));
  249. }
  250. );