Ctrl K
ComunidadGuías…

tests/channel-empty-state.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-environment happy-dom

import type { App } from 'vue'
import { createPinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick, onMounted, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import en from '../messages/en.json'
import ChannelTable from '../src/components/tables/ChannelTable.vue'
import { useDialogV2Store } from '../src/stores/dialogv2'

const state = vi.hoisted(() => ({
  rows: [] as any[],
  failed: false,
  canCreate: true,
  createPermissions: {} as Record<string, Promise<boolean>>,
  inserts: [] as any[],
  delayedApp: '',
  delayedResponse: null as Promise<unknown> | null,
  permissionChecks: [] as Array<{ permission: string, scope: { appId?: string, channelId?: number } }>,
}))

vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('vue-sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
vi.mock('~/stores/main', () => ({ useMainStore: () => ({ user: { id: 'preview-user' } }) }))
vi.mock('~/stores/organization', async () => {
  const { defineStore } = await import('pinia')
  const { ref } = await import('vue')
  return { useOrganizationStore: defineStore('channel-test-org', () => ({ currentOrganization: ref({ gid: 'preview-org' }) })) }
})
vi.mock('~/services/permissions', () => ({
  checkPermissions: async (permission: string, scope: { appId?: string, channelId?: number }) => {
    state.permissionChecks.push({ permission, scope })
    return permission === 'app.create_channel' ? state.createPermissions[scope.appId!] ?? state.canCreate : true
  },
}))
vi.mock('~/components/dashboard/ChannelIntroduction.vue', async () => {
  const { defineComponent, h } = await import('vue')
  return { default: defineComponent({
    props: ['canCreate'],
    emits: ['create'],
    setup: (props, { emit }) => () => h('section', { 'data-test': 'channel-introduction' }, [
      props.canCreate ? h('button', { 'data-test': 'create-channel', 'onClick': () => emit('create') }, 'Create') : null,
    ]),
  }) }
})
vi.mock('~/services/console', () => ({
  useConsole: () => ({
    from(table: string) {
      let appId = ''
      let search = ''
      let insert: any[] | undefined
      const query: any = {
        select: () => query,
        eq: (column: string, value: string) => { if (column === 'app_id') appId = value; return query },
        order: () => query,
        range: () => query,
        like: (_column: string, value: string) => { search = value.slice(1, -1); return query },
        insert: (rows: any[]) => { insert = rows; return query },
        maybeSingle: async () => ({ data: { default_upload_channel: null } }),
        then(resolve: (value: unknown) => unknown, reject: (reason: unknown) => unknown) {
          if (table === 'channels' && appId === state.delayedApp && state.delayedResponse)
            return state.delayedResponse.then(resolve, reject)
          if (insert) {
            state.inserts.push(...insert)
            state.rows.push(...insert.map((row, index) => ({ ...row, id: index + 1 })))
          }
          const rows = state.rows.filter(row => !search || row.name.includes(search))
          return Promise.resolve(table === 'channels'
            ? { data: state.failed ? null : rows, count: state.failed ? null : rows.length }
            : { data: [] }).then(resolve, reject)
        },
      }
      return query
    },
  }),
}))

let app: App | undefined
let container: HTMLElement
let tableEmit: (event: string, value?: unknown) => void
let dialog: ReturnType<typeof useDialogV2Store>
let currentAppId = ref('')

async function settle() {
  for (let i = 0; i < 12; i++)
    await nextTick()
}

async function mountTable(appId = 'com.example.channels') {
  container = document.createElement('div')
  document.body.appendChild(container)
  const target = document.createElement('div')
  target.id = 'dialog-v2-content'
  document.body.appendChild(target)
  const pinia = createPinia()
  currentAppId = ref(appId)
  app = createApp({ setup: () => () => h(ChannelTable, { appId: currentAppId.value }) })
  app.use(pinia)
  app.use(createI18n({ legacy: false, locale: 'en', messages: { en } }))
  app.component('DataTable', defineComponent({
    props: ['isLoading', 'elementList', 'showAdd'],
    emits: ['reload', 'update:search', 'add'],
    setup(props, { emit }) {
      tableEmit = emit
      onMounted(() => emit('reload'))
      return () => h('div', { 'data-test': 'channel-table', 'data-loading': props.isLoading, 'data-show-add': props.showAdd }, props.elementList.map((row: any) => row.name).join(', '))
    },
  }))
  app.mount(container)
  dialog = useDialogV2Store(pinia)
  await settle()
}

beforeEach(() => {
  state.rows = []
  state.inserts = []
  state.failed = false
  state.canCreate = true
  state.createPermissions = {}
  state.delayedApp = ''
  state.delayedResponse = null
  state.permissionChecks = []
})
afterEach(() => {
  app?.unmount()
  document.body.innerHTML = ''
})

describe('Channels page empty state', () => {
  it('shows the introduction after an empty read without automatically opening creation or writing data', async () => {
    await mountTable()
    expect(container.querySelector('[data-test="channel-introduction"]')).not.toBeNull()
    expect(dialog.showDialog).toBe(false)
    expect(state.inserts).toEqual([])
  })

  it('keeps the regular table when channels exist', async () => {
    state.rows = [{ id: 1, name: 'production' }]
    await mountTable()
    expect(container.querySelector('[data-test="channel-introduction"]')).toBeNull()
    expect(container.textContent).toContain('production')
  })

  it('checks delete access with both the requested app and each returned channel', async () => {
    state.rows = [{ id: 17, name: 'production' }, { id: 23, name: 'beta' }]
    await mountTable()
    expect(state.permissionChecks.filter(check => check.permission === 'channel.delete')).toEqual([
      { permission: 'channel.delete', scope: { appId: 'com.example.channels', channelId: 17 } },
      { permission: 'channel.delete', scope: { appId: 'com.example.channels', channelId: 23 } },
    ])
  })

  it('does not mistake an empty search result for an app without channels', async () => {
    state.rows = [{ id: 1, name: 'production' }]
    await mountTable()
    tableEmit('update:search', 'missing')
    await nextTick()
    tableEmit('reload')
    await settle()
    expect(container.querySelector('[data-test="channel-introduction"]')).toBeNull()
    expect(dialog.showDialog).toBe(false)
  })

  it('does not treat a failed read as an empty app and ends the loading state', async () => {
    state.failed = true
    await mountTable()
    expect(container.querySelector('[data-test="channel-introduction"]')).toBeNull()
    expect(container.querySelector('[data-test="channel-table"]')?.getAttribute('data-loading')).toBe('false')
    expect(dialog.showDialog).toBe(false)
  })

  it('keeps a filtered zero count from showing the introduction before clearing search reloads', async () => {
    state.rows = [{ id: 1, name: 'production' }]
    await mountTable()
    tableEmit('update:search', 'missing')
    await nextTick()
    tableEmit('reload')
    await settle()
    tableEmit('update:search', '')
    await settle()
    expect(container.querySelector('[data-test="channel-introduction"]')).toBeNull()
    tableEmit('reload')
    await settle()
    expect(container.textContent).toContain('production')
  })

  it('ignores an obsolete empty response after switching to a populated app', async () => {
    let finishOldRequest!: (response: unknown) => void
    state.delayedApp = 'com.example.empty'
    state.delayedResponse = new Promise(resolve => finishOldRequest = resolve)
    await mountTable(state.delayedApp)
    state.rows = [{ id: 1, name: 'production' }]
    currentAppId.value = 'com.example.populated'
    await settle()
    expect(container.textContent).toContain('production')
    finishOldRequest({ data: [], count: 0 })
    await settle()
    expect(container.querySelector('[data-test="channel-introduction"]')).toBeNull()
    expect(container.textContent).toContain('production')
    expect(container.querySelector('[data-test="channel-table"]')?.getAttribute('data-loading')).toBe('false')
  })

  it('lets readers view the introduction without offering channel creation', async () => {
    state.canCreate = false
    await mountTable()
    expect(container.querySelector('[data-test="channel-introduction"]')).not.toBeNull()
    expect(container.querySelector('[data-test="create-channel"]')).toBeNull()
    expect(dialog.showDialog).toBe(false)
  })

  it.each([false, true])('hides creation while the current app permission is pending (populated: %s)', async (populated) => {
    if (populated)
      state.rows = [{ id: 1, name: 'production' }]
    await mountTable()
    let finishPermission!: (allowed: boolean) => void
    state.createPermissions['com.example.next'] = new Promise(resolve => finishPermission = resolve)
    currentAppId.value = 'com.example.next'
    await settle()
    expect(container.querySelector('[data-test="create-channel"]')).toBeNull()
    expect(container.querySelector('[data-test="channel-table"]')?.getAttribute('data-show-add')).toBe('false')
    tableEmit('add')
    await settle()
    expect(dialog.showDialog).toBe(false)
    finishPermission(true)
    await settle()
    expect(container.querySelector('[data-test="channel-table"]')?.getAttribute('data-show-add')).toBe('true')
  })

  it.each([false, true])('denies creation after the current app permission rejects (populated: %s)', async (populated) => {
    if (populated)
      state.rows = [{ id: 1, name: 'production' }]
    await mountTable()
    let rejectPermission!: (error: Error) => void
    state.createPermissions['com.example.next'] = new Promise((_resolve, reject) => rejectPermission = reject)
    currentAppId.value = 'com.example.next'
    await settle()
    rejectPermission(new Error('permission lookup unavailable'))
    await settle()
    expect(container.querySelector('[data-test="create-channel"]')).toBeNull()
    expect(container.querySelector('[data-test="channel-table"]')?.getAttribute('data-show-add')).toBe('false')
    tableEmit('add')
    await settle()
    expect(dialog.showDialog).toBe(false)
    expect(state.inserts).toEqual([])
  })

  it('ignores an obsolete allow result after a new app denies creation', async () => {
    let finishOldPermission!: (allowed: boolean) => void
    state.createPermissions['com.example.channels'] = new Promise(resolve => finishOldPermission = resolve)
    await mountTable()
    state.canCreate = false
    currentAppId.value = 'com.example.next'
    await settle()
    finishOldPermission(true)
    await settle()
    expect(container.querySelector('[data-test="create-channel"]')).toBeNull()
    expect(container.querySelector('[data-test="channel-table"]')?.getAttribute('data-show-add')).toBe('false')
  })

  it('opens the existing creation dialog only after the explicit action', async () => {
    await mountTable()
    container.querySelector<HTMLButtonElement>('[data-test="create-channel"]')!.click()
    await settle()
    expect(dialog.showDialog).toBe(true)
    expect(dialog.dialogOptions.title).toBe('Create channel')
    expect(state.inserts).toEqual([])
  })

  it('replaces the introduction with the channel table after creating a channel', async () => {
    await mountTable()
    container.querySelector<HTMLButtonElement>('[data-test="create-channel"]')!.click()
    await settle()
    const input = document.querySelector<HTMLInputElement>('#dialog-v2-content input')!
    input.value = 'beta'
    input.dispatchEvent(new Event('input', { bubbles: true }))
    await nextTick()
    await dialog.dialogOptions.buttons!.find(button => button.role === 'primary')!.handler!()
    await settle()
    expect(state.inserts).toHaveLength(1)
    expect(state.inserts[0]).toMatchObject({ name: 'beta', app_id: 'com.example.channels' })
    expect(container.querySelector('[data-test="channel-introduction"]')).toBeNull()
    expect(container.querySelector<HTMLElement>('[data-test="channel-table"]')!.style.display).not.toBe('none')
    expect(container.textContent).toContain('beta')
  })
})