})
describe('async/await detection', () => {
- function assertAwaitDetection(code: string, shouldAsync = true) {
+ function assertAwaitDetection(
+ code: string,
+ expected: string | ((content: string) => boolean),
+ shouldAsync = true
+ ) {
const { content } = compile(`<script setup>${code}</script>`)
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
+ if (typeof expected === 'string') {
+ expect(content).toMatch(expected)
+ } else {
+ expect(expected(content)).toBe(true)
+ }
}
test('expression statement', () => {
- assertAwaitDetection(`await foo`)
+ assertAwaitDetection(`await foo`, `await _withAsyncContext(foo)`)
})
test('variable', () => {
- assertAwaitDetection(`const a = 1 + (await foo)`)
+ assertAwaitDetection(
+ `const a = 1 + (await foo)`,
+ `1 + (await _withAsyncContext(foo))`
+ )
})
test('ref', () => {
- assertAwaitDetection(`ref: a = 1 + (await foo)`)
+ assertAwaitDetection(
+ `ref: a = 1 + (await foo)`,
+ `1 + (await _withAsyncContext(foo))`
+ )
})
test('nested statements', () => {
- assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
+ assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
+ return (
+ code.includes(`await _withAsyncContext(foo)`) &&
+ code.includes(`await _withAsyncContext(bar)`)
+ )
+ })
})
test('should ignore await inside functions', () => {
// function declaration
- assertAwaitDetection(`async function foo() { await bar }`, false)
+ assertAwaitDetection(
+ `async function foo() { await bar }`,
+ `await bar`,
+ false
+ )
// function expression
- assertAwaitDetection(`const foo = async () => { await bar }`, false)
+ assertAwaitDetection(
+ `const foo = async () => { await bar }`,
+ `await bar`,
+ false
+ )
// object method
- assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
+ assertAwaitDetection(
+ `const obj = { async method() { await bar }}`,
+ `await bar`,
+ false
+ )
// class method
assertAwaitDetection(
`const cls = class Foo { async method() { await bar }}`,
+ `await bar`,
false
)
})
import {
+ ComponentInternalInstance,
defineComponent,
+ getCurrentInstance,
h,
nodeOps,
+ onMounted,
render,
- SetupContext
+ SetupContext,
+ Suspense
} from '@vue/runtime-test'
import {
defineEmits,
withDefaults,
useAttrs,
useSlots,
- mergeDefaults
+ mergeDefaults,
+ withAsyncContext
} from '../src/apiSetupHelpers'
describe('SFC <script setup> helpers', () => {
`props default key "foo" has no corresponding declaration`
).toHaveBeenWarned()
})
+
+ test('withAsyncContext', async () => {
+ const spy = jest.fn()
+
+ let beforeInstance: ComponentInternalInstance | null = null
+ let afterInstance: ComponentInternalInstance | null = null
+ let resolve: (msg: string) => void
+
+ const Comp = defineComponent({
+ async setup() {
+ beforeInstance = getCurrentInstance()
+ const msg = await withAsyncContext(
+ new Promise(r => {
+ resolve = r
+ })
+ )
+ // register the lifecycle after an await statement
+ onMounted(spy)
+ afterInstance = getCurrentInstance()
+ return () => msg
+ }
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(() => h(Suspense, () => h(Comp))), root)
+
+ expect(spy).not.toHaveBeenCalled()
+ resolve!('hello')
+ // wait a macro task tick for all micro ticks to resolve
+ await new Promise(r => setTimeout(r))
+ // mount hook should have been called
+ expect(spy).toHaveBeenCalled()
+ // should retain same instance before/after the await call
+ expect(beforeInstance).toBe(afterInstance)
+ })
})
import {
getCurrentInstance,
SetupContext,
- createSetupContext
+ createSetupContext,
+ setCurrentInstance
} from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import {
}
return props
}
+
+/**
+ * Runtime helper for storing and resuming current instance context in
+ * async setup().
+ * @internal
+ */
+export async function withAsyncContext<T>(
+ awaitable: T | Promise<T>
+): Promise<T> {
+ const ctx = getCurrentInstance()
+ const res = await awaitable
+ setCurrentInstance(ctx)
+ return res
+}