2024-01-19 12:21:42 +00:00
|
|
|
import { beforeEach } from 'node:test'
|
2023-09-10 08:06:11 +00:00
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
2024-06-10 22:20:27 +00:00
|
|
|
import { defineComponent, h, nextTick, popScopeId, pushScopeId } from 'vue'
|
2023-12-11 18:20:11 +00:00
|
|
|
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
2023-09-10 08:06:11 +00:00
|
|
|
import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component'
|
2023-10-20 08:38:51 +00:00
|
|
|
import { createSimpleRemoteIslandProvider } from '../fixtures/remote-provider'
|
|
|
|
import NuxtIsland from '../../packages/nuxt/src/app/components/nuxt-island'
|
|
|
|
|
|
|
|
vi.mock('#build/nuxt.config.mjs', async (original) => {
|
|
|
|
return {
|
|
|
|
// @ts-expect-error virtual file
|
|
|
|
...(await original()),
|
2023-12-19 12:21:29 +00:00
|
|
|
remoteComponentIslands: true,
|
2024-04-05 18:08:32 +00:00
|
|
|
selectiveClient: true,
|
2023-10-20 08:38:51 +00:00
|
|
|
}
|
|
|
|
})
|
2023-09-10 08:06:11 +00:00
|
|
|
|
|
|
|
vi.mock('vue', async (original) => {
|
|
|
|
const vue = await original<typeof import('vue')>()
|
|
|
|
return {
|
|
|
|
...vue,
|
2024-04-05 18:08:32 +00:00
|
|
|
h: vi.fn(vue.h),
|
2023-09-10 08:06:11 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2024-01-19 12:21:42 +00:00
|
|
|
const consoleError = vi.spyOn(console, 'error')
|
|
|
|
const consoleWarn = vi.spyOn(console, 'warn')
|
|
|
|
|
2024-03-06 15:26:19 +00:00
|
|
|
function expectNoConsoleIssue () {
|
2024-01-19 12:21:42 +00:00
|
|
|
expect(consoleError).not.toHaveBeenCalled()
|
|
|
|
expect(consoleWarn).not.toHaveBeenCalled()
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
consoleError.mockClear()
|
|
|
|
consoleWarn.mockClear()
|
|
|
|
})
|
|
|
|
|
2023-09-10 08:06:11 +00:00
|
|
|
describe('runtime server component', () => {
|
2024-04-23 12:53:11 +00:00
|
|
|
it('expect no data-v- attributes #23051', () => {
|
2023-09-10 08:06:11 +00:00
|
|
|
// @ts-expect-error mock
|
|
|
|
vi.mocked(h).mockImplementation(() => null)
|
|
|
|
|
|
|
|
// @ts-expect-error test setup
|
|
|
|
createServerComponent('DummyName').setup!({
|
2024-04-05 18:08:32 +00:00
|
|
|
lazy: false,
|
2023-09-10 08:06:11 +00:00
|
|
|
}, {
|
|
|
|
attrs: {
|
|
|
|
'data-v-123': '',
|
2024-04-05 18:08:32 +00:00
|
|
|
'test': 1,
|
2023-09-10 08:06:11 +00:00
|
|
|
},
|
|
|
|
slots: {},
|
|
|
|
emit: vi.fn(),
|
2024-04-05 18:08:32 +00:00
|
|
|
expose: vi.fn(),
|
2023-09-10 08:06:11 +00:00
|
|
|
})()
|
|
|
|
|
|
|
|
expect(h).toHaveBeenCalledOnce()
|
|
|
|
if (!vi.mocked(h).mock.lastCall) { throw new Error('no last call') }
|
|
|
|
expect(vi.mocked(h).mock.lastCall![1]?.props).toBeTypeOf('object')
|
|
|
|
expect(vi.mocked(h).mock.lastCall![1]?.props).toMatchInlineSnapshot(`
|
|
|
|
{
|
2023-09-28 07:36:13 +00:00
|
|
|
"data-v-123": "",
|
2023-09-10 08:06:11 +00:00
|
|
|
"test": 1,
|
|
|
|
}
|
|
|
|
`)
|
2023-10-20 08:38:51 +00:00
|
|
|
vi.mocked(h).mockRestore()
|
|
|
|
})
|
|
|
|
|
|
|
|
it('expect remote island to be rendered', async () => {
|
|
|
|
const server = createSimpleRemoteIslandProvider()
|
|
|
|
|
|
|
|
const wrapper = await mountSuspended(NuxtIsland, {
|
|
|
|
props: {
|
|
|
|
name: 'Test',
|
2024-04-05 18:08:32 +00:00
|
|
|
source: 'http://localhost:3001',
|
|
|
|
},
|
2023-10-20 08:38:51 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(wrapper.html()).toMatchInlineSnapshot('"<div>hello world from another server</div>"')
|
|
|
|
|
|
|
|
await server.close()
|
2023-09-10 08:06:11 +00:00
|
|
|
})
|
2023-12-14 11:07:54 +00:00
|
|
|
|
|
|
|
it('force refresh', async () => {
|
|
|
|
let count = 0
|
|
|
|
const stubFetch = vi.fn(() => {
|
|
|
|
count++
|
|
|
|
return {
|
|
|
|
id: '123',
|
|
|
|
html: `<div>${count}</div>`,
|
|
|
|
state: {},
|
|
|
|
head: {
|
|
|
|
link: [],
|
2024-04-05 18:08:32 +00:00
|
|
|
style: [],
|
2023-12-14 11:07:54 +00:00
|
|
|
},
|
2024-03-06 15:26:19 +00:00
|
|
|
json () {
|
2023-12-14 11:07:54 +00:00
|
|
|
return this
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2023-12-14 11:07:54 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
vi.stubGlobal('fetch', stubFetch)
|
|
|
|
|
|
|
|
const component = await mountSuspended(createServerComponent('dummyName'))
|
|
|
|
expect(fetch).toHaveBeenCalledOnce()
|
|
|
|
|
|
|
|
expect(component.html()).toBe('<div>1</div>')
|
|
|
|
|
|
|
|
await component.vm.$.exposed!.refresh()
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(2)
|
|
|
|
await nextTick()
|
|
|
|
expect(component.html()).toBe('<div>2</div>')
|
2024-01-19 12:21:42 +00:00
|
|
|
vi.mocked(fetch).mockReset()
|
2023-12-14 11:07:54 +00:00
|
|
|
})
|
2024-03-06 16:45:43 +00:00
|
|
|
|
|
|
|
it('expect NuxtIsland to emit an error', async () => {
|
|
|
|
const stubFetch = vi.fn(() => {
|
|
|
|
throw new Error('fetch error')
|
|
|
|
})
|
|
|
|
|
|
|
|
vi.stubGlobal('fetch', stubFetch)
|
|
|
|
|
|
|
|
const wrapper = await mountSuspended(createServerComponent('ErrorServerComponent'), {
|
|
|
|
props: {
|
|
|
|
name: 'Error',
|
|
|
|
props: {
|
2024-04-05 18:08:32 +00:00
|
|
|
force: true,
|
|
|
|
},
|
2024-03-06 16:45:43 +00:00
|
|
|
},
|
2024-04-05 18:08:32 +00:00
|
|
|
attachTo: 'body',
|
2024-03-06 16:45:43 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(fetch).toHaveBeenCalledOnce()
|
|
|
|
expect(wrapper.emitted('error')).toHaveLength(1)
|
|
|
|
vi.mocked(fetch).mockReset()
|
|
|
|
})
|
2024-06-10 22:20:27 +00:00
|
|
|
|
|
|
|
it('expect NuxtIsland to have parent scopeId', async () => {
|
|
|
|
const wrapper = await mountSuspended(defineComponent({
|
|
|
|
render () {
|
|
|
|
pushScopeId('data-v-654e2b21')
|
|
|
|
const vnode = h(createServerComponent('dummyName'))
|
|
|
|
popScopeId()
|
|
|
|
return vnode
|
|
|
|
},
|
|
|
|
}))
|
|
|
|
|
|
|
|
expect(wrapper.find('*').attributes()).toHaveProperty('data-v-654e2b21')
|
|
|
|
})
|
2023-09-10 08:06:11 +00:00
|
|
|
})
|
2024-01-19 12:21:42 +00:00
|
|
|
|
|
|
|
describe('client components', () => {
|
|
|
|
it('expect swapping nuxt-client should not trigger errors #25289', async () => {
|
|
|
|
const mockPath = '/nuxt-client.js'
|
|
|
|
const componentId = 'Client-12345'
|
|
|
|
|
|
|
|
vi.doMock(mockPath, () => ({
|
|
|
|
default: {
|
|
|
|
name: 'ClientComponent',
|
2024-03-06 15:26:19 +00:00
|
|
|
setup () {
|
2024-01-19 12:21:42 +00:00
|
|
|
return () => h('div', 'client component')
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
|
|
|
},
|
2024-01-19 12:21:42 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
const stubFetch = vi.fn(() => {
|
|
|
|
return {
|
|
|
|
id: '123',
|
|
|
|
html: `<div data-island-uid>hello<div data-island-uid data-island-component="${componentId}"></div></div>`,
|
|
|
|
state: {},
|
|
|
|
head: {
|
|
|
|
link: [],
|
2024-04-05 18:08:32 +00:00
|
|
|
style: [],
|
2024-01-19 12:21:42 +00:00
|
|
|
},
|
|
|
|
components: {
|
|
|
|
[componentId]: {
|
|
|
|
html: '<div>fallback</div>',
|
|
|
|
props: {},
|
2024-04-05 18:08:32 +00:00
|
|
|
chunk: mockPath,
|
|
|
|
},
|
2024-01-19 12:21:42 +00:00
|
|
|
},
|
2024-03-06 15:26:19 +00:00
|
|
|
json () {
|
2024-01-19 12:21:42 +00:00
|
|
|
return this
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2024-01-19 12:21:42 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
vi.stubGlobal('fetch', stubFetch)
|
|
|
|
|
|
|
|
const wrapper = await mountSuspended(NuxtIsland, {
|
|
|
|
props: {
|
|
|
|
name: 'NuxtClient',
|
|
|
|
props: {
|
2024-04-05 18:08:32 +00:00
|
|
|
force: true,
|
|
|
|
},
|
2024-01-19 12:21:42 +00:00
|
|
|
},
|
2024-04-05 18:08:32 +00:00
|
|
|
attachTo: 'body',
|
2024-01-19 12:21:42 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(fetch).toHaveBeenCalledOnce()
|
|
|
|
|
|
|
|
expect(wrapper.html()).toMatchInlineSnapshot(`
|
2024-06-10 22:20:27 +00:00
|
|
|
"<div data-island-uid="5">hello<div data-island-uid="5" data-island-component="Client-12345">
|
2024-01-19 12:21:42 +00:00
|
|
|
<div>client component</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!--teleport start-->
|
|
|
|
<!--teleport end-->"
|
|
|
|
`)
|
|
|
|
|
|
|
|
// @ts-expect-error mock
|
|
|
|
vi.mocked(fetch).mockImplementation(() => ({
|
|
|
|
id: '123',
|
2024-03-09 06:48:15 +00:00
|
|
|
html: '<div data-island-uid>hello<div><div>fallback</div></div></div>',
|
2024-01-19 12:21:42 +00:00
|
|
|
state: {},
|
|
|
|
head: {
|
|
|
|
link: [],
|
2024-04-05 18:08:32 +00:00
|
|
|
style: [],
|
2024-01-19 12:21:42 +00:00
|
|
|
},
|
|
|
|
components: {},
|
2024-03-06 15:26:19 +00:00
|
|
|
json () {
|
2024-01-19 12:21:42 +00:00
|
|
|
return this
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2024-01-19 12:21:42 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
await wrapper.vm.$.exposed!.refresh()
|
|
|
|
await nextTick()
|
2024-03-06 15:26:19 +00:00
|
|
|
expect(wrapper.html()).toMatchInlineSnapshot(`
|
2024-06-10 22:20:27 +00:00
|
|
|
"<div data-island-uid="5">hello<div>
|
2024-01-19 12:21:42 +00:00
|
|
|
<div>fallback</div>
|
|
|
|
</div>
|
|
|
|
</div>"
|
|
|
|
`)
|
|
|
|
|
|
|
|
vi.mocked(fetch).mockReset()
|
|
|
|
expectNoConsoleIssue()
|
|
|
|
})
|
2024-01-21 11:30:54 +00:00
|
|
|
|
|
|
|
it('should not replace nested client components data-island-uid', async () => {
|
|
|
|
const componentId = 'Client-12345'
|
2024-03-06 15:26:19 +00:00
|
|
|
|
2024-01-21 11:30:54 +00:00
|
|
|
const stubFetch = vi.fn(() => {
|
|
|
|
return {
|
|
|
|
id: '1234',
|
|
|
|
html: `<div data-island-uid>hello<div data-island-uid="not-to-be-replaced" data-island-component="${componentId}"></div></div>`,
|
|
|
|
state: {},
|
|
|
|
head: {
|
|
|
|
link: [],
|
2024-04-05 18:08:32 +00:00
|
|
|
style: [],
|
2024-01-21 11:30:54 +00:00
|
|
|
},
|
2024-03-06 15:26:19 +00:00
|
|
|
json () {
|
2024-01-21 11:30:54 +00:00
|
|
|
return this
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2024-01-21 11:30:54 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
vi.stubGlobal('fetch', stubFetch)
|
|
|
|
|
|
|
|
const wrapper = await mountSuspended(NuxtIsland, {
|
|
|
|
props: {
|
|
|
|
name: 'WithNestedClient',
|
|
|
|
props: {
|
2024-04-05 18:08:32 +00:00
|
|
|
force: true,
|
|
|
|
},
|
2024-01-21 11:30:54 +00:00
|
|
|
},
|
2024-04-05 18:08:32 +00:00
|
|
|
attachTo: 'body',
|
2024-01-21 11:30:54 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(fetch).toHaveBeenCalledOnce()
|
|
|
|
expect(wrapper.html()).toContain('data-island-uid="not-to-be-replaced"')
|
|
|
|
vi.mocked(fetch).mockReset()
|
|
|
|
expectNoConsoleIssue()
|
|
|
|
})
|
2024-03-06 15:26:19 +00:00
|
|
|
|
|
|
|
it('pass a slot to a client components within islands', async () => {
|
|
|
|
const mockPath = '/nuxt-client-with-slot.js'
|
|
|
|
const componentId = 'ClientWithSlot-12345'
|
|
|
|
|
|
|
|
vi.doMock(mockPath, () => ({
|
2024-06-07 15:57:37 +00:00
|
|
|
default: defineComponent({
|
2024-03-06 15:26:19 +00:00
|
|
|
name: 'ClientWithSlot',
|
|
|
|
setup (_, { slots }) {
|
2024-06-07 15:57:37 +00:00
|
|
|
return () => h('div', { class: 'client-component' }, slots.default?.())
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2024-06-07 15:57:37 +00:00
|
|
|
}),
|
2024-03-06 15:26:19 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
const stubFetch = vi.fn(() => {
|
|
|
|
return {
|
|
|
|
id: '123',
|
|
|
|
html: `<div data-island-uid>hello<div data-island-uid data-island-component="${componentId}"></div></div>`,
|
|
|
|
state: {},
|
|
|
|
head: {
|
|
|
|
link: [],
|
2024-04-05 18:08:32 +00:00
|
|
|
style: [],
|
2024-03-06 15:26:19 +00:00
|
|
|
},
|
|
|
|
components: {
|
|
|
|
[componentId]: {
|
|
|
|
html: '<div>fallback</div>',
|
|
|
|
props: {},
|
|
|
|
chunk: mockPath,
|
|
|
|
slots: {
|
2024-04-05 18:08:32 +00:00
|
|
|
default: '<div>slot in client component</div>',
|
|
|
|
},
|
|
|
|
},
|
2024-03-06 15:26:19 +00:00
|
|
|
},
|
|
|
|
json () {
|
|
|
|
return this
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2024-03-06 15:26:19 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
vi.stubGlobal('fetch', stubFetch)
|
|
|
|
const wrapper = await mountSuspended(NuxtIsland, {
|
|
|
|
props: {
|
2024-04-05 18:08:32 +00:00
|
|
|
name: 'NuxtClientWithSlot',
|
2024-03-06 15:26:19 +00:00
|
|
|
},
|
2024-04-05 18:08:32 +00:00
|
|
|
attachTo: 'body',
|
2024-03-06 15:26:19 +00:00
|
|
|
})
|
|
|
|
expect(fetch).toHaveBeenCalledOnce()
|
|
|
|
expect(wrapper.html()).toMatchInlineSnapshot(`
|
2024-06-10 22:20:27 +00:00
|
|
|
"<div data-island-uid="7">hello<div data-island-uid="7" data-island-component="ClientWithSlot-12345">
|
2024-03-06 15:26:19 +00:00
|
|
|
<div class="client-component">
|
|
|
|
<div style="display: contents" data-island-uid="" data-island-slot="default">
|
|
|
|
<div>slot in client component</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!--teleport start-->
|
|
|
|
<!--teleport end-->"
|
|
|
|
`)
|
|
|
|
|
|
|
|
expectNoConsoleIssue()
|
|
|
|
})
|
|
|
|
})
|