Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/app-vitest-browser/components/CustomRandom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
const input = ref(0)

function getRandom() {
return Math.trunc(Math.random() * 101)
}

function random(rand: number) {
input.value = rand * 2
}

function getRandom2() {
return Math.trunc(Math.random() * 101)
}

function random2(rand: number) {
input.value = rand * 2
}

defineExpose({
getRandom2,
random2,
})
</script>

<template>
<div>
<input
v-model="input"
type="number"
>
<button
id="random"
@click="random(getRandom())"
>
Random
</button>
<button
id="random2"
@click="random2(getRandom2())"
>
Random2
</button>
</div>
</template>
36 changes: 32 additions & 4 deletions examples/app-vitest-browser/test/mount-suspended.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'

import { mountSuspended } from '@nuxt/test-utils/runtime'

import { MyCounter } from '#components'
import { MyCounter, CustomRandom } from '#components'

describe('Component (MyCounter)', () => {
it('renders', async () => {
Expand All @@ -12,15 +12,15 @@ describe('Component (MyCounter)', () => {

it('can be interacted with (increment)', async () => {
const component = await mountSuspended(MyCounter)
const incrementButton = component.findAll('button').filter(btn => btn.text().includes('Increment'))[0]
const incrementButton = component.findAll('button').filter(btn => btn.text().includes('Increment'))[0]!
incrementButton.element.click()
await nextTick()
expect(component.text()).toContain('Count: 1')
})

it('can be interacted with (decrement)', async () => {
const component = await mountSuspended(MyCounter)
const decrementButton = component.findAll('button').filter(btn => btn.text().includes('Decrement'))[0]
const decrementButton = component.findAll('button').filter(btn => btn.text().includes('Decrement'))[0]!
decrementButton.element.click()
await nextTick()
expect(component.text()).toContain('Count: -1')
Expand All @@ -30,4 +30,32 @@ describe('Component (MyCounter)', () => {
const component = await mountSuspended(MyCounter)
expect(component.text()).toContain('"buildAssetsDir": "/_nuxt/"')
})

it('can spy component setup state', async () => {
const wrapper = await mountSuspended(CustomRandom, {
spy: true,
})

vi.mocked(wrapper.setupState.getRandom).mockImplementation(() => 200)

await wrapper.find('#random').trigger('click')

expect(wrapper.setupState.getRandom).toHaveBeenCalled()
expect(wrapper.setupState.random).toHaveBeenCalledWith(200)
expect(wrapper.setupState.input.value).toBe(400)
})

it('can spy component exposed via setupState', async () => {
const wrapper = await mountSuspended(CustomRandom, {
spy: true,
})

vi.mocked(wrapper.setupState.getRandom2).mockImplementation(() => 200)

await wrapper.find('#random2').trigger('click')

expect(wrapper.setupState.getRandom2).toHaveBeenCalled()
expect(wrapper.setupState.random2).toHaveBeenCalledWith(200)
expect(wrapper.setupState.input.value).toBe(400)
})
})
45 changes: 45 additions & 0 deletions examples/app-vitest-full/components/CustomRandom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
const input = ref(0)

function getRandom() {
return Math.trunc(Math.random() * 101)
}

function random(rand: number) {
input.value = rand * 2
}

function getRandom2() {
return Math.trunc(Math.random() * 101)
}

function random2(rand: number) {
input.value = rand * 2
}

defineExpose({
getRandom2,
random2,
})
</script>

<template>
<div>
<input
v-model="input"
type="number"
>
<button
id="random"
@click="random(getRandom())"
>
Random
</button>
<button
id="random2"
@click="random2(getRandom2())"
>
Random2
</button>
</div>
</template>
30 changes: 29 additions & 1 deletion examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import ComponentWithCssVar from '~/components/ComponentWithCssVar.vue'
import ComponentWithPluginProvidedValue from '~/components/ComponentWithPluginProvidedValue.vue'
import GenericStateComponent from '~/components/GenericStateComponent.vue'

import { BoundAttrs, TestTeleport } from '#components'
import { BoundAttrs, TestTeleport, CustomRandom } from '#components'
import DirectiveComponent from '~/components/DirectiveComponent.vue'
import CustomComponent from '~/components/CustomComponent.vue'
import WrapperElement from '~/components/WrapperElement.vue'
Expand Down Expand Up @@ -676,4 +676,32 @@ describe('watcher cleanup validation', () => {

expect(watcherCallCount).toBe(1)
})

it('can spy component setup state via setupState', async () => {
const wrapper = await mountSuspended(CustomRandom, {
spy: true,
})

vi.mocked(wrapper.setupState.getRandom).mockImplementation(() => 200)

await wrapper.find('#random').trigger('click')

expect(wrapper.setupState.getRandom).toHaveBeenCalled()
expect(wrapper.setupState.random).toHaveBeenCalledWith(200)
expect(wrapper.setupState.input.value).toBe(400)
})

it('can spy component exposed via setupState', async () => {
const wrapper = await mountSuspended(CustomRandom, {
spy: true,
})

vi.mocked(wrapper.setupState.getRandom2).mockImplementation(() => 200)

await wrapper.find('#random2').trigger('click')

expect(wrapper.setupState.getRandom2).toHaveBeenCalled()
expect(wrapper.setupState.random2).toHaveBeenCalledWith(200)
expect(wrapper.setupState.input.value).toBe(400)
})
})
39 changes: 38 additions & 1 deletion examples/app-vitest-full/tests/nuxt/render-suspended.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ import CompostionApi from '~/components/TestComponentWithCompostionApi.vue'
import OptionsApiWithData from '~/components/TestComponentWithOptionsApiWithData.vue'
import OptionsApiWithSetup from '~/components/TestComponentWithOptionsApiWithSetup.vue'

import { BoundAttrs, OptionsApiComputed, OptionsApiEmits, OptionsApiWatch, ScriptSetupEmits, ScriptSetupWatch, TestTeleport } from '#components'
import {
BoundAttrs,
CustomRandom,
OptionsApiComputed,
OptionsApiEmits,
OptionsApiWatch,
ScriptSetupEmits,
ScriptSetupWatch,
TestTeleport,
} from '#components'

const formats = {
ExportDefaultComponent,
Expand Down Expand Up @@ -408,3 +417,31 @@ it('teleport should work', async () => {
wrapper.unmount()
expect(document.getElementById('teleport-title')).toBeFalsy()
})

it('can spy component setup state via setupState', async () => {
const wrapper = await renderSuspended(CustomRandom, {
spy: true,
})

vi.mocked(wrapper.setupState.getRandom).mockImplementation(() => 200)

await fireEvent.click(wrapper.getByRole('button', { name: 'Random' }))

expect(wrapper.setupState.getRandom).toHaveBeenCalled()
expect(wrapper.setupState.random).toHaveBeenCalledWith(200)
expect(wrapper.setupState.input.value).toBe(400)
})

it('can spy component exposed via setupState', async () => {
const wrapper = await renderSuspended(CustomRandom, {
spy: true,
})

vi.mocked(wrapper.setupState.getRandom2).mockImplementation(() => 200)

await fireEvent.click(wrapper.getByRole('button', { name: 'Random2' }))

expect(wrapper.setupState.getRandom2).toHaveBeenCalled()
expect(wrapper.setupState.random2).toHaveBeenCalledWith(200)
expect(wrapper.setupState.input.value).toBe(400)
})
34 changes: 31 additions & 3 deletions src/runtime-utils/utils/suspended.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { vi } from 'vitest'
import { Suspense, effectScope, h, nextTick, reactive, getCurrentInstance, onErrorCaptured } from 'vue'
import type { App, ComponentInternalInstance, DefineComponent, SetupContext, VNode } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
Expand All @@ -20,11 +21,30 @@ type WrapperFnOption<Fn> = Fn extends (c: WrapperFnComponent<Fn>, o: infer O) =>
type WrapperFnResult<Fn> = Fn extends (c: WrapperFnComponent<Fn>, o: WrapperFnOption<Fn>) => infer R ? R : never

export type WrapperSuspendedOptions<Fn> = WrapperFnOption<Fn> & {
/**
* The initial route, or false to skip the initial route change.
* @default '/'
*/
route?: RouteLocationRaw | false
/**
* Enable spy component setup state.
* @default false
* @example
* ```ts
* const wrapper = await mountSuspended(CustomRandom, { spy: true })
* vi.mocked(wrapper.setupState.getRandom).mockImplementation(() => 200)
* await wrapper.find('#random').trigger('click')
* expect(wrapper.setupState.getRandom).toHaveBeenCalled()
* ```
*/
spy?: boolean
scoped?: boolean
}

export type WrapperSuspendedResult<Fn> = WrapperFnResult<Fn> & {
/**
* The return value of the component setup.
*/
setupState: SetupState
}

Expand Down Expand Up @@ -64,7 +84,7 @@ export function wrapperSuspended<C, Fn extends WrapperFn<C>>(
setProps: (props: object) => void
}> {
const { props = {}, attrs = {} } = options as ComponentMountingOptions<C>
const { route = '/', scoped = false, ...wrapperFnOptions } = options as ComponentMountingOptions<C>
const { route = '/', scoped = false, spy = false, ...wrapperFnOptions } = options as ComponentMountingOptions<C>

const vueApp: App<Element> & Record<string, unknown> = tryUseNuxtApp()?.vueApp
// @ts-expect-error untyped global __unctx__
Expand Down Expand Up @@ -105,15 +125,23 @@ export function wrapperSuspended<C, Fn extends WrapperFn<C>>(

if (!componentSetup) return

const result = scoped
let result = scoped
? await runEffectScope(() => componentSetup(props, setupContext))
: await componentSetup(props, setupContext)

if (wrappedInstance?.exposed) {
instanceContext.expose(wrappedInstance.exposed)
}

setupState = result && typeof result === 'object' ? result : {}
if (result && typeof result === 'object') {
if (spy) {
result = vi.mockObject(result, { spy: true })
}
setupState = result
}
else {
setupState = {}
}

return result
},
Expand Down
Loading