Newer
Older
navi-1 / webclient / tests / unit / views / sharedChatView.test.js
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'

import { setLocale } from '@/i18n/index.js'

const getSharedChat = vi.fn()
const getSharedHistory = vi.fn()

vi.mock('@/api/index.js', () => ({
  getSharedChat: (...args) => getSharedChat(...args),
  getSharedHistory: (...args) => getSharedHistory(...args),
}))

import SharedChatView from '@/views/SharedChatView.vue'

const TOKEN = 'abcdefghijklmnopqrstuvwxyz012345'

// One user turn, then an assistant turn that both runs a tool carrying a live
// UI component and answers — the two things this page must handle specially:
// the component is dropped, the answer is kept.
function history() {
  return [
    {
      role: 'user',
      content: 'hello',
      created_at: '2026-01-01T10:00:00Z',
      metadata: { display_index: 0 },
    },
    {
      role: 'assistant',
      content: '',
      created_at: '2026-01-01T10:00:05Z',
      tool_calls: [{ id: 't1', name: 'ui_form', arguments: {} }],
      metadata: { display_index: 1 },
    },
    {
      role: 'tool',
      tool_call_id: 't1',
      content: '{}',
      metadata: { ui_component: { component: 'form', payload: {} } },
    },
    {
      role: 'assistant',
      content: 'Here is the answer.',
      created_at: '2026-01-01T10:00:09Z',
      metadata: { display_index: 3 },
    },
  ]
}

function payload(overrides = {}) {
  return {
    name: 'A shared chat',
    profile_id: 'assistant',
    profile_name: 'Assistant',
    created_at: '2026-01-01T10:00:00Z',
    last_active: '2026-01-01T10:05:00Z',
    messages: history(),
    has_more: false,
    next_before_seq: null,
    ...overrides,
  }
}

async function mountView() {
  const wrapper = mount(SharedChatView, { props: { token: TOKEN } })
  await flushPromises()
  return wrapper
}

beforeEach(() => {
  setActivePinia(createPinia())
  setLocale('en')
  getSharedChat.mockReset()
  getSharedHistory.mockReset()
})

describe('SharedChatView', () => {
  it('renders the chat history from the token, without an account', async () => {
    getSharedChat.mockResolvedValue(payload())
    const wrapper = await mountView()

    expect(getSharedChat).toHaveBeenCalledWith(TOKEN)
    expect(wrapper.text()).toContain('A shared chat')
    expect(wrapper.text()).toContain('Assistant')
    expect(wrapper.text()).toContain('hello')
    expect(wrapper.text()).toContain('Here is the answer.')

    wrapper.unmount()
  })

  it('offers no rating controls on a read-only page', async () => {
    getSharedChat.mockResolvedValue(payload())
    const wrapper = await mountView()

    // The copy button stays (a local action); the thumbs must not.
    expect(wrapper.find('.msg-copy-btn').exists()).toBe(true)
    expect(wrapper.find('.msg-rate-btn').exists()).toBe(false)

    wrapper.unmount()
  })

  it('drops interactive UI components, keeping the rest of the turn', async () => {
    getSharedChat.mockResolvedValue(payload())
    const wrapper = await mountView()

    // The tool result still produced its tool card; only the live widget is gone.
    expect(wrapper.findComponent({ name: 'UiComponentCard' }).exists()).toBe(false)
    expect(wrapper.text()).toContain('Here is the answer.')

    wrapper.unmount()
  })

  it('says the chat is gone when the token no longer resolves', async () => {
    getSharedChat.mockRejectedValue(new Error('GET /shared/x → 404: not found'))
    const wrapper = await mountView()

    expect(wrapper.text()).toContain('This chat is no longer available')
    expect(wrapper.find('.shared-scroller').exists()).toBe(false)

    wrapper.unmount()
  })

  it('loads earlier messages above the current ones', async () => {
    getSharedChat.mockResolvedValue(
      payload({
        messages: [
          {
            role: 'user',
            content: 'newer message',
            created_at: '2026-02-01T10:00:00Z',
            metadata: { display_index: 5 },
          },
        ],
        has_more: true,
        next_before_seq: 5,
      })
    )
    getSharedHistory.mockResolvedValue({
      messages: [
        {
          role: 'user',
          content: 'older message',
          created_at: '2026-01-01T10:00:00Z',
          metadata: { display_index: 1 },
        },
      ],
      has_more: false,
      next_before_seq: null,
    })

    const wrapper = await mountView()
    expect(wrapper.text()).toContain('newer message')
    expect(wrapper.text()).not.toContain('older message')

    await wrapper.find('.shared-load-older').trigger('click')
    await flushPromises()

    expect(getSharedHistory).toHaveBeenCalledWith(TOKEN, 5)
    expect(wrapper.text()).toContain('older message')
    expect(wrapper.find('.shared-load-older').exists()).toBe(false)

    wrapper.unmount()
  })
})