import type { Logger } from '@linktr.ee/messaging-core'
import { render } from '@testing-library/react'
import React from 'react'
import { describe, it, expect, vi, afterEach } from 'vitest'
import {
MessagingLoggerProvider,
consoleLogger,
resolveLogger,
useMessagingLogger,
} from './index'
const ERROR_MESSAGE = '[MessagingShell] Failed to load channels:'
/** Logs one error through the seam the way a call site would. */
const ErrorEmitter: React.FC<{ error: Error }> = ({ error }) => {
const logger = useMessagingLogger()
logger.error(ERROR_MESSAGE, error)
return null
}
/** Emits one line at every level so partial-sink fallback is observable. */
const AllLevelsEmitter: React.FC = () => {
const logger = useMessagingLogger()
logger.debug('debug line', 1)
logger.info('info line', 2)
logger.warn('warn line', 3)
logger.error('error line', 4)
return null
}
afterEach(() => {
vi.restoreAllMocks()
})
describe('logger seam', () => {
it('routes error to an injected sink with the exact message and raw error object', () => {
const error = new Error('boom')
const sink = { error: vi.fn() }
render(
)
expect(sink.error).toHaveBeenCalledTimes(1)
expect(sink.error).toHaveBeenCalledWith(ERROR_MESSAGE, error)
// The raw Error instance, not a serialized copy.
expect(sink.error.mock.calls[0][1]).toBe(error)
})
it('passes identical arguments to console.error when no sink is injected', () => {
const error = new Error('boom')
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
const sink = { error: vi.fn() }
render(
)
const injectedArgs = sink.error.mock.calls[0]
// No provider at all, and a provider with no logger prop: both must produce
// byte-identical console output to what the injected sink received.
render()
render(
)
expect(consoleError).toHaveBeenCalledTimes(2)
expect(consoleError.mock.calls[0]).toEqual(injectedArgs)
expect(consoleError.mock.calls[1]).toEqual(injectedArgs)
expect(consoleError.mock.calls[0][1]).toBe(error)
})
it('falls back to console for the levels a partial sink omits', () => {
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
const sink: Logger = { warn: vi.fn(), error: vi.fn() }
render(
)
expect(sink.warn).toHaveBeenCalledWith('warn line', 3)
expect(sink.error).toHaveBeenCalledWith('error line', 4)
expect(consoleWarn).not.toHaveBeenCalled()
expect(consoleError).not.toHaveBeenCalled()
// debug and info are unset on the sink, so both land on console.log.
expect(consoleLog.mock.calls).toEqual([
['debug line', 1],
['info line', 2],
])
})
it('defaults every level to console when the hook is used with no provider', () => {
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
render()
expect(consoleLog.mock.calls).toEqual([
['debug line', 1],
['info line', 2],
])
expect(consoleWarn).toHaveBeenCalledWith('warn line', 3)
expect(consoleError).toHaveBeenCalledWith('error line', 4)
})
it('resolveLogger keeps provided levels and fills the rest from console', () => {
const warn = vi.fn()
const resolved = resolveLogger({ warn })
expect(resolved.warn).toBe(warn)
expect(resolved.debug).toBe(consoleLogger.debug)
expect(resolved.info).toBe(consoleLogger.info)
expect(resolved.error).toBe(consoleLogger.error)
expect(resolveLogger()).toEqual(consoleLogger)
})
it('consoleLogger dispatches through console at call time, not bind time', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
consoleLogger.error('late spy', 'arg')
expect(consoleError).toHaveBeenCalledWith('late spy', 'arg')
})
})