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') }) })