146 lines
5.0 KiB
TypeScript
146 lines
5.0 KiB
TypeScript
// 02-07: the AIUI embed URL must be byte-stable across re-renders, a
|
|||
|
|
// simulated viewport resize, and a KeepAlive deactivate/reactivate cycle —
|
||
|
|
// any runtime-varying input would change the iframe `src` and force a full
|
||
|
|
// AIUI reload on the next tab switch, giving back the entire benefit 02-04
|
||
|
|
// established for the Chat tab. Also covers the two D-14 presentation
|
||
|
|
// flags (chatExpanded, mobileChat), origin validation being unchanged, and
|
||
|
|
// aiuiConnected surviving deactivation (AIUI's 'ready' message is not
|
||
|
|
// re-sent on re-entry).
|
||
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
||
|
|
import { KeepAlive, defineComponent, h, ref } from 'vue'
|
||
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||
|
|
import Chat from '../Chat.vue'
|
||
|
|
|
||
|
|
const routerBackMock = vi.fn()
|
||
|
|
const routerPushMock = vi.fn()
|
||
|
|
|
||
|
|
vi.mock('vue-router', () => ({
|
||
|
|
useRouter: () => ({ back: routerBackMock, push: routerPushMock }),
|
||
|
|
}))
|
||
|
|
|
||
|
|
vi.mock('vue-i18n', () => ({
|
||
|
|
useI18n: () => ({ t: (key: string) => key }),
|
||
|
|
}))
|
||
|
|
|
||
|
|
// IS_DEMO is a build-time constant in the real module; mock it so these
|
||
|
|
// tests exercise the plain VITE_AIUI_URL branch deterministically.
|
||
|
|
vi.mock('@/composables/useDemoIntro', () => ({ IS_DEMO: false }))
|
||
|
|
|
||
|
|
// ContextBroker pulls in several Pinia stores (app/container/aiPermissions)
|
||
|
|
// unrelated to this test's concern (URL stability + origin validation) —
|
||
|
|
// mocked at the module boundary, mirroring MarketplaceRefresh.test.ts's
|
||
|
|
// convention for isolating a view from its heavier dependencies.
|
||
|
|
vi.mock('@/services/contextBroker', () => ({
|
||
|
|
ContextBroker: vi.fn().mockImplementation(() => ({
|
||
|
|
start: vi.fn(),
|
||
|
|
stop: vi.fn(),
|
||
|
|
})),
|
||
|
|
}))
|
||
|
|
|
||
|
|
/** Mount Chat.vue behind a real <KeepAlive> so onActivated/onDeactivated fire. */
|
||
|
|
function mountChatInKeepAlive() {
|
||
|
|
const show = ref(true)
|
||
|
|
const Host = defineComponent({
|
||
|
|
setup() {
|
||
|
|
return () => h(KeepAlive, null, {
|
||
|
|
default: () => (show.value ? h(Chat) : h('div', 'other-tab')),
|
||
|
|
})
|
||
|
|
},
|
||
|
|
})
|
||
|
|
const wrapper = mount(Host)
|
||
|
|
return { wrapper, show }
|
||
|
|
}
|
||
|
|
|
||
|
|
function iframeSrc(wrapper: ReturnType<typeof mount>): string | undefined {
|
||
|
|
return wrapper.find('iframe').attributes('src')
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Chat / AIUI embed URL stability + D-14 defaults (02-07)', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.stubEnv('VITE_AIUI_URL', 'http://localhost:5173')
|
||
|
|
})
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
vi.unstubAllEnvs()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('carries embedded=true, hideClose=true, and both D-14 flags', () => {
|
||
|
|
const { wrapper } = mountChatInKeepAlive()
|
||
|
|
const src = iframeSrc(wrapper)
|
||
|
|
expect(src).toBeTruthy()
|
||
|
|
expect(src).toContain('embedded=true')
|
||
|
|
expect(src).toContain('hideClose=true')
|
||
|
|
expect(src).toContain('chatExpanded=true')
|
||
|
|
expect(src).toContain('mobileChat=true')
|
||
|
|
wrapper.unmount()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('is string-equal before and after a simulated viewport resize across the mobile breakpoint', async () => {
|
||
|
|
const { wrapper } = mountChatInKeepAlive()
|
||
|
|
const before = iframeSrc(wrapper)
|
||
|
|
|
||
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 375 })
|
||
|
|
window.dispatchEvent(new Event('resize'))
|
||
|
|
await wrapper.vm.$nextTick()
|
||
|
|
|
||
|
|
const after = iframeSrc(wrapper)
|
||
|
|
expect(after).toBe(before)
|
||
|
|
wrapper.unmount()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('is string-equal before and after a deactivate/reactivate cycle', async () => {
|
||
|
|
const { wrapper, show } = mountChatInKeepAlive()
|
||
|
|
const before = iframeSrc(wrapper)
|
||
|
|
|
||
|
|
show.value = false
|
||
|
|
await wrapper.vm.$nextTick()
|
||
|
|
show.value = true
|
||
|
|
await wrapper.vm.$nextTick()
|
||
|
|
|
||
|
|
const after = iframeSrc(wrapper)
|
||
|
|
expect(after).toBe(before)
|
||
|
|
wrapper.unmount()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('does not set aiuiConnected for a message from a foreign origin', async () => {
|
||
|
|
const { wrapper } = mountChatInKeepAlive()
|
||
|
|
|
||
|
|
window.dispatchEvent(new MessageEvent('message', {
|
||
|
|
data: { type: 'ready' },
|
||
|
|
origin: 'http://evil.example',
|
||
|
|
}))
|
||
|
|
await flushPromises()
|
||
|
|
|
||
|
|
// aiuiConnected stays false: the loading overlay is still shown and the
|
||
|
|
// connected indicator (title="chat.aiuiConnected") is absent.
|
||
|
|
expect(wrapper.find('.chat-loading').exists()).toBe(true)
|
||
|
|
expect(wrapper.find('[title="chat.aiuiConnected"]').exists()).toBe(false)
|
||
|
|
wrapper.unmount()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('aiuiConnected survives a deactivate/reactivate cycle once set by a same-origin ready message', async () => {
|
||
|
|
const { wrapper, show } = mountChatInKeepAlive()
|
||
|
|
|
||
|
|
window.dispatchEvent(new MessageEvent('message', {
|
||
|
|
data: { type: 'ready' },
|
||
|
|
origin: 'http://localhost:5173',
|
||
|
|
}))
|
||
|
|
await flushPromises()
|
||
|
|
|
||
|
|
expect(wrapper.find('[title="chat.aiuiConnected"]').exists()).toBe(true)
|
||
|
|
expect(wrapper.find('.chat-loading').exists()).toBe(false)
|
||
|
|
|
||
|
|
show.value = false
|
||
|
|
await wrapper.vm.$nextTick()
|
||
|
|
show.value = true
|
||
|
|
await wrapper.vm.$nextTick()
|
||
|
|
await flushPromises()
|
||
|
|
|
||
|
|
// No second 'ready' message is sent on reactivation — aiuiConnected must
|
||
|
|
// not have been reset to false by the deactivate/reactivate cycle.
|
||
|
|
expect(wrapper.find('[title="chat.aiuiConnected"]').exists()).toBe(true)
|
||
|
|
expect(wrapper.find('.chat-loading').exists()).toBe(false)
|
||
|
|
wrapper.unmount()
|
||
|
|
})
|
||
|
|
})
|