import { View as RouterView } from '../src/components/View'
import { components, RouteLocationNormalizedLoose } from './utils'
import { START_LOCATION_NORMALIZED } from '../src/types'
-import { ref, markNonReactive } from 'vue'
-import { mount, tick } from './mount'
+import { markNonReactive } from 'vue'
+import { mount, createMockedRoute } from './mount2'
import { mockWarn } from 'jest-mock-warn'
// to have autocompletion
describe('RouterView', () => {
mockWarn()
- function factory(route: RouteLocationNormalizedLoose, props: any = {}) {
- const router = {
- currentRoute: ref(
- markNonReactive({
- ...route,
- // reset the instances every time
- matched: route.matched.map(match => ({ ...match, instances: {} })),
- })
- ),
- }
-
- const { app, el } = mount(
- router as any,
- {
- template: `<RouterView :name="name"></RouterView>`,
- components: { RouterView },
- setup() {
- const name = ref(props.name)
-
- return {
- name,
- }
- },
- } as any
- )
+ async function factory(
+ initialRoute: RouteLocationNormalizedLoose,
+ propsData: any = {}
+ ) {
+ const route = createMockedRoute(initialRoute)
+ const wrapper = await mount(RouterView, {
+ propsData,
+ provide: route.provides,
+ components: { RouterView },
+ })
- return { app, router, el }
+ return { route, wrapper }
}
- it('displays current route component', () => {
- const { el } = factory(routes.root)
- expect(el.innerHTML).toBe(`<div>Home</div>`)
+ it('displays current route component', async () => {
+ const { wrapper } = await factory(routes.root)
+ expect(wrapper.html()).toBe(`<div>Home</div>`)
})
- it('displays named views', () => {
- const { el } = factory(routes.named, { name: 'foo' })
- expect(el.innerHTML).toBe(`<div>Foo</div>`)
+ it('displays named views', async () => {
+ const { wrapper } = await factory(routes.named, { name: 'foo' })
+ expect(wrapper.html()).toBe(`<div>Foo</div>`)
})
- it('displays nothing when route is unmatched', () => {
- const { el } = factory(START_LOCATION_NORMALIZED as any)
+ it('displays nothing when route is unmatched', async () => {
+ const { wrapper } = await factory(START_LOCATION_NORMALIZED as any)
// NOTE: I wonder if this will stay stable in future releases
expect('Router').not.toHaveBeenWarned()
- expect(el.childElementCount).toBe(0)
+ expect(wrapper.rootEl.childElementCount).toBe(0)
})
- it('displays nested views', () => {
- const { el } = factory(routes.nested)
- expect(el.innerHTML).toBe(`<div><h2>Nested</h2><div>Foo</div></div>`)
+ it('displays nested views', async () => {
+ const { wrapper } = await factory(routes.nested)
+ expect(wrapper.html()).toBe(`<div><h2>Nested</h2><div>Foo</div></div>`)
})
- it('displays deeply nested views', () => {
- const { el } = factory(routes.nestedNested)
- expect(el.innerHTML).toBe(
+ it('displays deeply nested views', async () => {
+ const { wrapper } = await factory(routes.nestedNested)
+ expect(wrapper.html()).toBe(
`<div><h2>Nested</h2><div><h2>Nested</h2><div>Foo</div></div></div>`
)
})
it('renders when the location changes', async () => {
- const { el, router } = factory(routes.root)
- expect(el.innerHTML).toBe(`<div>Home</div>`)
- router.currentRoute.value = routes.foo
- await tick()
- expect(el.innerHTML).toBe(`<div>Foo</div>`)
+ const { route, wrapper } = await factory(routes.root)
+ expect(wrapper.html()).toBe(`<div>Home</div>`)
+ await route.set(routes.foo)
+ expect(wrapper.html()).toBe(`<div>Foo</div>`)
})
it('does not pass params as props by default', async () => {
...routes.withParams,
matched: [{ ...routes.withParams.matched[0], props: false }],
}
- const { el, router } = factory(noPropsWithParams)
- expect(el.innerHTML).toBe(`<div>User: default</div>`)
- router.currentRoute.value = markNonReactive({
+ const { wrapper, route } = await factory(noPropsWithParams)
+ expect(wrapper.html()).toBe(`<div>User: default</div>`)
+ await route.set({
...noPropsWithParams,
params: { id: '4' },
})
- await tick()
- expect(el.innerHTML).toBe(`<div>User: default</div>`)
+ expect(wrapper.html()).toBe(`<div>User: default</div>`)
})
it('passes params as props with props: true', async () => {
- const { el, router } = factory(routes.withParams)
- expect(el.innerHTML).toBe(`<div>User: 1</div>`)
- router.currentRoute.value = markNonReactive({
+ const { wrapper, route } = await factory(routes.withParams)
+ expect(wrapper.html()).toBe(`<div>User: 1</div>`)
+ await route.set({
+ ...routes.withParams,
+ params: { id: '4' },
+ })
+ expect(wrapper.html()).toBe(`<div>User: 4</div>`)
+ })
+
+ it('passes params as props with props: true', async () => {
+ const { wrapper, route } = await factory(routes.withParams)
+
+ expect(wrapper.html()).toBe(`<div>User: 1</div>`)
+
+ await route.set({
...routes.withParams,
params: { id: '4' },
})
- await tick()
- expect(el.innerHTML).toBe(`<div>User: 4</div>`)
+ expect(wrapper.html()).toBe(`<div>User: 4</div>`)
})
it('can pass an object as props', async () => {
- const { el } = factory(routes.withIdAndOther)
- expect(el.innerHTML).toBe(`<div>id:foo;other:fixed</div>`)
+ const { wrapper } = await factory(routes.withIdAndOther)
+ expect(wrapper.html()).toBe(`<div>id:foo;other:fixed</div>`)
})
it('can pass a function as props', async () => {
- const { el } = factory(routes.withFnProps)
- expect(el.innerHTML).toBe(`<div>id:2;other:page</div>`)
+ const { wrapper } = await factory(routes.withFnProps)
+ expect(wrapper.html()).toBe(`<div>id:2;other:page</div>`)
})
})
--- /dev/null
+import {
+ Component,
+ createApp,
+ defineComponent,
+ h,
+ ref,
+ ComponentPublicInstance,
+ reactive,
+ nextTick,
+ ComponentObjectPropsOptions,
+ ComputedRef,
+ computed,
+ markNonReactive,
+ App,
+ ComponentOptionsWithProps,
+} from 'vue'
+import { RouteLocationNormalizedLoose } from './utils'
+import { routeLocationKey } from '../src/utils/injectionSymbols'
+
+interface MountOptions {
+ propsData: Record<string, any>
+ provide: Record<string | symbol, any>
+ components: ComponentOptionsWithProps['components']
+}
+// { app, vm: instance!, el: rootEl, setProps, provide }
+interface Wrapper {
+ app: App
+ vm: ComponentPublicInstance
+ rootEl: HTMLDivElement
+ setProps(props: MountOptions['propsData']): Promise<void>
+ html(): string
+}
+
+function initialProps<P>(propsOption: ComponentObjectPropsOptions<P>) {
+ let copy = {} as ComponentPublicInstance<typeof propsOption>['$props']
+
+ for (let key in propsOption) {
+ const prop = propsOption[key]!
+ // @ts-ignore
+ if (!prop.required && prop.default)
+ // TODO: function value
+ // @ts-ignore
+ copy[key] = prop.default
+ }
+
+ return copy
+}
+
+export function mount(
+ // TODO: generic?
+ targetComponent: Parameters<typeof createApp>[0],
+ options: Partial<MountOptions> = {}
+): Promise<Wrapper> {
+ const TargetComponent = targetComponent as Component
+ return new Promise(resolve => {
+ // TODO: props can only be an object
+ const propsData = reactive(
+ Object.assign(
+ initialProps(TargetComponent.props || {}),
+ options.propsData
+ )
+ )
+
+ function setProps(partialProps: Record<string, any>) {
+ Object.assign(propsData, partialProps)
+ return nextTick()
+ }
+
+ const Wrapper = defineComponent({
+ setup(_props, { emit }) {
+ const componentInstanceRef = ref<ComponentPublicInstance>()
+
+ return () => {
+ return h(TargetComponent, {
+ ref: componentInstanceRef,
+ onVnodeMounted() {
+ emit('ready', componentInstanceRef.value)
+ },
+ ...propsData,
+ })
+ }
+ },
+ })
+
+ const app = createApp(Wrapper, {
+ onReady: (instance: ComponentPublicInstance) => {
+ resolve({ app, vm: instance!, rootEl, setProps, html })
+ },
+ })
+
+ if (options.provide) {
+ const keys = getKeys(options.provide)
+
+ for (let key of keys) {
+ app.provide(key, options.provide[key as any])
+ }
+ }
+
+ if (options.components) {
+ for (let key in options.components) {
+ app.component(key, options.components[key])
+ }
+ }
+
+ // TODO: how to cleanup?
+ const rootEl = document.createElement('div')
+ document.body.appendChild(rootEl)
+
+ function html() {
+ return rootEl.innerHTML
+ }
+
+ app.mount(rootEl)
+ })
+}
+
+function getKeys(object: Record<string | symbol, any>): Array<symbol | string> {
+ return (Object.getOwnPropertyNames(object) as Array<string | symbol>).concat(
+ Object.getOwnPropertySymbols(object)
+ )
+}
+
+export function createMockedRoute(initialValue: RouteLocationNormalizedLoose) {
+ const route = {} as {
+ [k in keyof RouteLocationNormalizedLoose]: ComputedRef<
+ RouteLocationNormalizedLoose[k]
+ >
+ }
+
+ const routeRef = ref(markNonReactive(initialValue))
+
+ function set(newRoute: RouteLocationNormalizedLoose) {
+ routeRef.value = markNonReactive(newRoute)
+ return nextTick()
+ }
+
+ for (let key in initialValue) {
+ // @ts-ignore
+ route[key] =
+ // new line to still get errors here
+ computed(() => routeRef.value[key as keyof RouteLocationNormalizedLoose])
+ }
+
+ const value = reactive(route)
+
+ return {
+ value,
+ set,
+ provides: {
+ [routeLocationKey as symbol]: value,
+ },
+ }
+}