| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import {
- createSSRApp,
- defineComponent,
- h,
- nextTick,
- onScopeDispose,
- ref,
- watch,
- watchEffect,
- withAsyncContext,
- } from 'vue'
- import { type SSRContext, renderToString } from '../src'
- const gc = () =>
- new Promise<void>(resolve => {
- setTimeout(() => {
- global.gc!()
- resolve()
- })
- })
- describe('ssr: watch', () => {
- // #6013
- test('should work w/ flush:sync', async () => {
- const App = defineComponent(() => {
- const count = ref(0)
- let msg = ''
- watch(
- count,
- () => {
- msg = 'hello world'
- },
- { flush: 'sync' },
- )
- count.value = 1
- expect(msg).toBe('hello world')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles!.length).toBe(1)
- expect(html).toMatch('hello world')
- })
- test('should work with flush: sync and immediate: true', async () => {
- const text = ref('start')
- let msg = 'unchanged'
- const App = defineComponent(() => {
- watch(
- text,
- () => {
- msg = text.value
- },
- { flush: 'sync', immediate: true },
- )
- expect(msg).toBe('start')
- text.value = 'changed'
- expect(msg).toBe('changed')
- text.value = 'changed again'
- expect(msg).toBe('changed again')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles!.length).toBe(1)
- expect(html).toMatch('changed again')
- await nextTick()
- expect(msg).toBe('changed again')
- })
- test('should run once with immediate: true', async () => {
- const text = ref('start')
- let msg = 'unchanged'
- const App = defineComponent(() => {
- watch(
- text,
- () => {
- msg = String(text.value)
- },
- { immediate: true },
- )
- text.value = 'changed'
- expect(msg).toBe('start')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles).toBeUndefined()
- expect(html).toMatch('start')
- await nextTick()
- expect(msg).toBe('start')
- })
- test('should run once with immediate: true and flush: post', async () => {
- const text = ref('start')
- let msg = 'unchanged'
- const App = defineComponent(() => {
- watch(
- text,
- () => {
- msg = String(text.value)
- },
- { immediate: true, flush: 'post' },
- )
- text.value = 'changed'
- expect(msg).toBe('start')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles).toBeUndefined()
- expect(html).toMatch('start')
- await nextTick()
- expect(msg).toBe('start')
- })
- test('should not run non-immediate watchers registered after async context restore', async () => {
- const text = ref('start')
- let beforeAwaitTriggered = false
- let afterAwaitTriggered = false
- const App = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- watch(text, () => {
- beforeAwaitTriggered = true
- })
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- watch(text, () => {
- afterAwaitTriggered = true
- })
- text.value = 'changed'
- expect(beforeAwaitTriggered).toBe(false)
- expect(afterAwaitTriggered).toBe(false)
- return () => h('div', null, text.value)
- },
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles).toBeUndefined()
- expect(html).toMatch('changed')
- await nextTick()
- expect(beforeAwaitTriggered).toBe(false)
- expect(afterAwaitTriggered).toBe(false)
- })
- test('should not run non-immediate watchers registered after async context restore on rejection', async () => {
- const text = ref('start')
- let beforeAwaitTriggered = false
- let afterAwaitTriggered = false
- const App = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- watch(text, () => {
- beforeAwaitTriggered = true
- })
- try {
- ;[__temp, __restore] = withAsyncContext(() =>
- Promise.reject(new Error('failed')),
- )
- __temp = await __temp
- __restore()
- } catch {}
- watch(text, () => {
- afterAwaitTriggered = true
- })
- text.value = 'changed'
- expect(beforeAwaitTriggered).toBe(false)
- expect(afterAwaitTriggered).toBe(false)
- return () => h('div', null, text.value)
- },
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles).toBeUndefined()
- expect(html).toMatch('changed')
- await nextTick()
- expect(beforeAwaitTriggered).toBe(false)
- expect(afterAwaitTriggered).toBe(false)
- })
- })
- describe('ssr: watchEffect', () => {
- test('should run with flush: sync', async () => {
- const text = ref('start')
- let msg = 'unchanged'
- const App = defineComponent(() => {
- watchEffect(
- () => {
- msg = text.value
- },
- { flush: 'sync' },
- )
- expect(msg).toBe('start')
- text.value = 'changed'
- expect(msg).toBe('changed')
- text.value = 'changed again'
- expect(msg).toBe('changed again')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles!.length).toBe(1)
- expect(html).toMatch('changed again')
- await nextTick()
- expect(msg).toBe('changed again')
- })
- test('should run once with default flush (pre)', async () => {
- const text = ref('start')
- let msg = 'unchanged'
- const App = defineComponent(() => {
- watchEffect(() => {
- msg = text.value
- })
- text.value = 'changed'
- expect(msg).toBe('start')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles).toBeUndefined()
- expect(html).toMatch('start')
- await nextTick()
- expect(msg).toBe('start')
- })
- test('should not run for flush: post', async () => {
- const text = ref('start')
- let msg = 'unchanged'
- const App = defineComponent(() => {
- watchEffect(
- () => {
- msg = text.value
- },
- { flush: 'post' },
- )
- text.value = 'changed'
- expect(msg).toBe('unchanged')
- return () => h('div', null, msg)
- })
- const app = createSSRApp(App)
- const ctx: SSRContext = {}
- const html = await renderToString(app, ctx)
- expect(ctx.__watcherHandles).toBeUndefined()
- expect(html).toMatch('unchanged')
- await nextTick()
- expect(msg).toBe('unchanged')
- })
- })
- describe.skipIf(!global.gc)('ssr: watch gc', () => {
- test('should not retain apps when a watcher stop handle is registered with onScopeDispose after async context restore', async () => {
- const weakRefs: { deref(): unknown | undefined }[] = []
- const ComponentA = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve(false))
- const enabled = await __temp
- __restore()
- const el = ref(null)
- const stop = watch(
- () => el.value,
- () => {},
- { immediate: true },
- )
- onScopeDispose(stop)
- return () => h('div', { ref: el }, `Component A ${enabled}`)
- },
- })
- const ComponentB = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve(false))
- const enabled = await __temp
- __restore()
- return () => h('div', `Component B ${enabled}`)
- },
- })
- async function renderOnce() {
- const app = createSSRApp({
- render: () => h('div', [h(ComponentA), h(ComponentB)]),
- })
- // @ts-expect-error ES2021 API
- weakRefs.push(new WeakRef(app))
- const html = await renderToString(app)
- expect(html).toContain('Component A false')
- expect(html).toContain('Component B false')
- }
- for (let i = 0; i < 10; i++) {
- await renderOnce()
- }
- for (let i = 0; i < 5; i++) {
- await gc()
- }
- expect(weakRefs.filter(ref => ref.deref()).length).toBe(0)
- })
- })
|