ssrWatch.spec.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import {
  2. createSSRApp,
  3. defineComponent,
  4. h,
  5. nextTick,
  6. onScopeDispose,
  7. ref,
  8. watch,
  9. watchEffect,
  10. withAsyncContext,
  11. } from 'vue'
  12. import { type SSRContext, renderToString } from '../src'
  13. const gc = () =>
  14. new Promise<void>(resolve => {
  15. setTimeout(() => {
  16. global.gc!()
  17. resolve()
  18. })
  19. })
  20. describe('ssr: watch', () => {
  21. // #6013
  22. test('should work w/ flush:sync', async () => {
  23. const App = defineComponent(() => {
  24. const count = ref(0)
  25. let msg = ''
  26. watch(
  27. count,
  28. () => {
  29. msg = 'hello world'
  30. },
  31. { flush: 'sync' },
  32. )
  33. count.value = 1
  34. expect(msg).toBe('hello world')
  35. return () => h('div', null, msg)
  36. })
  37. const app = createSSRApp(App)
  38. const ctx: SSRContext = {}
  39. const html = await renderToString(app, ctx)
  40. expect(ctx.__watcherHandles!.length).toBe(1)
  41. expect(html).toMatch('hello world')
  42. })
  43. test('should work with flush: sync and immediate: true', async () => {
  44. const text = ref('start')
  45. let msg = 'unchanged'
  46. const App = defineComponent(() => {
  47. watch(
  48. text,
  49. () => {
  50. msg = text.value
  51. },
  52. { flush: 'sync', immediate: true },
  53. )
  54. expect(msg).toBe('start')
  55. text.value = 'changed'
  56. expect(msg).toBe('changed')
  57. text.value = 'changed again'
  58. expect(msg).toBe('changed again')
  59. return () => h('div', null, msg)
  60. })
  61. const app = createSSRApp(App)
  62. const ctx: SSRContext = {}
  63. const html = await renderToString(app, ctx)
  64. expect(ctx.__watcherHandles!.length).toBe(1)
  65. expect(html).toMatch('changed again')
  66. await nextTick()
  67. expect(msg).toBe('changed again')
  68. })
  69. test('should run once with immediate: true', async () => {
  70. const text = ref('start')
  71. let msg = 'unchanged'
  72. const App = defineComponent(() => {
  73. watch(
  74. text,
  75. () => {
  76. msg = String(text.value)
  77. },
  78. { immediate: true },
  79. )
  80. text.value = 'changed'
  81. expect(msg).toBe('start')
  82. return () => h('div', null, msg)
  83. })
  84. const app = createSSRApp(App)
  85. const ctx: SSRContext = {}
  86. const html = await renderToString(app, ctx)
  87. expect(ctx.__watcherHandles).toBeUndefined()
  88. expect(html).toMatch('start')
  89. await nextTick()
  90. expect(msg).toBe('start')
  91. })
  92. test('should run once with immediate: true and flush: post', async () => {
  93. const text = ref('start')
  94. let msg = 'unchanged'
  95. const App = defineComponent(() => {
  96. watch(
  97. text,
  98. () => {
  99. msg = String(text.value)
  100. },
  101. { immediate: true, flush: 'post' },
  102. )
  103. text.value = 'changed'
  104. expect(msg).toBe('start')
  105. return () => h('div', null, msg)
  106. })
  107. const app = createSSRApp(App)
  108. const ctx: SSRContext = {}
  109. const html = await renderToString(app, ctx)
  110. expect(ctx.__watcherHandles).toBeUndefined()
  111. expect(html).toMatch('start')
  112. await nextTick()
  113. expect(msg).toBe('start')
  114. })
  115. test('should not run non-immediate watchers registered after async context restore', async () => {
  116. const text = ref('start')
  117. let beforeAwaitTriggered = false
  118. let afterAwaitTriggered = false
  119. const App = defineComponent({
  120. async setup() {
  121. let __temp: any, __restore: any
  122. watch(text, () => {
  123. beforeAwaitTriggered = true
  124. })
  125. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  126. __temp = await __temp
  127. __restore()
  128. watch(text, () => {
  129. afterAwaitTriggered = true
  130. })
  131. text.value = 'changed'
  132. expect(beforeAwaitTriggered).toBe(false)
  133. expect(afterAwaitTriggered).toBe(false)
  134. return () => h('div', null, text.value)
  135. },
  136. })
  137. const app = createSSRApp(App)
  138. const ctx: SSRContext = {}
  139. const html = await renderToString(app, ctx)
  140. expect(ctx.__watcherHandles).toBeUndefined()
  141. expect(html).toMatch('changed')
  142. await nextTick()
  143. expect(beforeAwaitTriggered).toBe(false)
  144. expect(afterAwaitTriggered).toBe(false)
  145. })
  146. test('should not run non-immediate watchers registered after async context restore on rejection', async () => {
  147. const text = ref('start')
  148. let beforeAwaitTriggered = false
  149. let afterAwaitTriggered = false
  150. const App = defineComponent({
  151. async setup() {
  152. let __temp: any, __restore: any
  153. watch(text, () => {
  154. beforeAwaitTriggered = true
  155. })
  156. try {
  157. ;[__temp, __restore] = withAsyncContext(() =>
  158. Promise.reject(new Error('failed')),
  159. )
  160. __temp = await __temp
  161. __restore()
  162. } catch {}
  163. watch(text, () => {
  164. afterAwaitTriggered = true
  165. })
  166. text.value = 'changed'
  167. expect(beforeAwaitTriggered).toBe(false)
  168. expect(afterAwaitTriggered).toBe(false)
  169. return () => h('div', null, text.value)
  170. },
  171. })
  172. const app = createSSRApp(App)
  173. const ctx: SSRContext = {}
  174. const html = await renderToString(app, ctx)
  175. expect(ctx.__watcherHandles).toBeUndefined()
  176. expect(html).toMatch('changed')
  177. await nextTick()
  178. expect(beforeAwaitTriggered).toBe(false)
  179. expect(afterAwaitTriggered).toBe(false)
  180. })
  181. })
  182. describe('ssr: watchEffect', () => {
  183. test('should run with flush: sync', async () => {
  184. const text = ref('start')
  185. let msg = 'unchanged'
  186. const App = defineComponent(() => {
  187. watchEffect(
  188. () => {
  189. msg = text.value
  190. },
  191. { flush: 'sync' },
  192. )
  193. expect(msg).toBe('start')
  194. text.value = 'changed'
  195. expect(msg).toBe('changed')
  196. text.value = 'changed again'
  197. expect(msg).toBe('changed again')
  198. return () => h('div', null, msg)
  199. })
  200. const app = createSSRApp(App)
  201. const ctx: SSRContext = {}
  202. const html = await renderToString(app, ctx)
  203. expect(ctx.__watcherHandles!.length).toBe(1)
  204. expect(html).toMatch('changed again')
  205. await nextTick()
  206. expect(msg).toBe('changed again')
  207. })
  208. test('should run once with default flush (pre)', async () => {
  209. const text = ref('start')
  210. let msg = 'unchanged'
  211. const App = defineComponent(() => {
  212. watchEffect(() => {
  213. msg = text.value
  214. })
  215. text.value = 'changed'
  216. expect(msg).toBe('start')
  217. return () => h('div', null, msg)
  218. })
  219. const app = createSSRApp(App)
  220. const ctx: SSRContext = {}
  221. const html = await renderToString(app, ctx)
  222. expect(ctx.__watcherHandles).toBeUndefined()
  223. expect(html).toMatch('start')
  224. await nextTick()
  225. expect(msg).toBe('start')
  226. })
  227. test('should not run for flush: post', async () => {
  228. const text = ref('start')
  229. let msg = 'unchanged'
  230. const App = defineComponent(() => {
  231. watchEffect(
  232. () => {
  233. msg = text.value
  234. },
  235. { flush: 'post' },
  236. )
  237. text.value = 'changed'
  238. expect(msg).toBe('unchanged')
  239. return () => h('div', null, msg)
  240. })
  241. const app = createSSRApp(App)
  242. const ctx: SSRContext = {}
  243. const html = await renderToString(app, ctx)
  244. expect(ctx.__watcherHandles).toBeUndefined()
  245. expect(html).toMatch('unchanged')
  246. await nextTick()
  247. expect(msg).toBe('unchanged')
  248. })
  249. })
  250. describe.skipIf(!global.gc)('ssr: watch gc', () => {
  251. test('should not retain apps when a watcher stop handle is registered with onScopeDispose after async context restore', async () => {
  252. const weakRefs: { deref(): unknown | undefined }[] = []
  253. const ComponentA = defineComponent({
  254. async setup() {
  255. let __temp: any, __restore: any
  256. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve(false))
  257. const enabled = await __temp
  258. __restore()
  259. const el = ref(null)
  260. const stop = watch(
  261. () => el.value,
  262. () => {},
  263. { immediate: true },
  264. )
  265. onScopeDispose(stop)
  266. return () => h('div', { ref: el }, `Component A ${enabled}`)
  267. },
  268. })
  269. const ComponentB = defineComponent({
  270. async setup() {
  271. let __temp: any, __restore: any
  272. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve(false))
  273. const enabled = await __temp
  274. __restore()
  275. return () => h('div', `Component B ${enabled}`)
  276. },
  277. })
  278. async function renderOnce() {
  279. const app = createSSRApp({
  280. render: () => h('div', [h(ComponentA), h(ComponentB)]),
  281. })
  282. // @ts-expect-error ES2021 API
  283. weakRefs.push(new WeakRef(app))
  284. const html = await renderToString(app)
  285. expect(html).toContain('Component A false')
  286. expect(html).toContain('Component B false')
  287. }
  288. for (let i = 0; i < 10; i++) {
  289. await renderOnce()
  290. }
  291. for (let i = 0; i < 5; i++) {
  292. await gc()
  293. }
  294. expect(weakRefs.filter(ref => ref.deref()).length).toBe(0)
  295. })
  296. })