diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 64421b8..258a62d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -5,8 +5,8 @@ import * as path from 'node:path' import process from 'node:process' import { defineCommand } from 'citty' import { consola } from 'consola' -import { name, version } from '../../toon/package.json' with { type: 'json' } import { DEFAULT_DELIMITER, DELIMITERS } from '../../toon/src' +import { name, version } from '../package.json' with { type: 'json' } import { decodeToJson, encodeToToon } from './conversion' import { detectMode } from './utils' diff --git a/packages/cli/test/index.test.ts b/packages/cli/test/index.test.ts index bdae427..1bf9034 100644 --- a/packages/cli/test/index.test.ts +++ b/packages/cli/test/index.test.ts @@ -1,8 +1,8 @@ import process from 'node:process' import { consola } from 'consola' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { version } from '../../toon/package.json' with { type: 'json' } import { DEFAULT_DELIMITER, encode } from '../../toon/src' +import { version } from '../package.json' with { type: 'json' } import { createCliTestContext, runCli } from './utils' describe('toon CLI', () => { @@ -14,113 +14,161 @@ describe('toon CLI', () => { vi.restoreAllMocks() }) - it('prints the version when using --version', async () => { - const consolaLog = vi.spyOn(consola, 'log').mockImplementation(() => undefined) - const consolaError = vi.spyOn(consola, 'error').mockImplementation(() => undefined) + describe('version', () => { + it('prints the version when using --version', async () => { + const consolaLog = vi.spyOn(consola, 'log').mockImplementation(() => undefined) - await runCli({ rawArgs: ['--version'] }) + await runCli({ rawArgs: ['--version'] }) - expect(consolaLog).toHaveBeenCalledWith(version) - expect(consolaError).not.toHaveBeenCalled() + expect(consolaLog).toHaveBeenCalledWith(version) + }) }) - it('encodes a JSON file into a TOON file', async () => { - const data = { - title: 'TOON test', - count: 3, - nested: { ok: true }, - } - const context = await createCliTestContext({ - 'input.json': JSON.stringify(data, undefined, 2), - }) - - const consolaSuccess = vi.spyOn(consola, 'success').mockImplementation(() => undefined) - - try { - await context.run(['input.json', '--output', 'output.toon']) - - const output = await context.read('output.toon') - const expected = encode(data, { - delimiter: DEFAULT_DELIMITER, - indent: 2, - lengthMarker: false, + describe('encode (JSON → TOON)', () => { + it('encodes a JSON file into a TOON file', async () => { + const data = { + title: 'TOON test', + count: 3, + nested: { ok: true }, + } + const context = await createCliTestContext({ + 'input.json': JSON.stringify(data, undefined, 2), }) - expect(output).toBe(expected) - expect(consolaSuccess).toHaveBeenCalledWith('Encoded `input.json` → `output.toon`') - } - finally { - await context.cleanup() - } + const consolaSuccess = vi.spyOn(consola, 'success').mockImplementation(() => undefined) + + try { + await context.run(['input.json', '--output', 'output.toon']) + + const output = await context.read('output.toon') + const expected = encode(data, { + delimiter: DEFAULT_DELIMITER, + indent: 2, + lengthMarker: false, + }) + + expect(output).toBe(expected) + expect(consolaSuccess).toHaveBeenCalledWith(expect.stringMatching(/Encoded .* → .*/)) + } + finally { + await context.cleanup() + } + }) + + it('writes to stdout when output not specified', async () => { + const data = { ok: true } + const context = await createCliTestContext({ + 'input.json': JSON.stringify(data), + }) + + const stdout: string[] = [] + const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown) => { + stdout.push(String(message ?? '')) + }) + + try { + await context.run(['input.json']) + + expect(stdout).toHaveLength(1) + expect(stdout[0]).toBe(encode(data)) + } + finally { + logSpy.mockRestore() + await context.cleanup() + } + }) }) - it('decodes a TOON file into a JSON file', async () => { - const data = { - items: ['alpha', 'beta'], - meta: { done: false }, - } - const toonInput = encode(data) - const context = await createCliTestContext({ - 'input.toon': toonInput, + describe('decode (TOON → JSON)', () => { + it('decodes a TOON file into a JSON file', async () => { + const data = { + items: ['alpha', 'beta'], + meta: { done: false }, + } + const toonInput = encode(data) + const context = await createCliTestContext({ + 'input.toon': toonInput, + }) + + const consolaSuccess = vi.spyOn(consola, 'success').mockImplementation(() => undefined) + + try { + await context.run(['input.toon', '--output', 'output.json']) + + const output = await context.read('output.json') + expect(JSON.parse(output)).toEqual(data) + expect(consolaSuccess).toHaveBeenCalledWith(expect.stringMatching(/Decoded .* → .*/)) + } + finally { + await context.cleanup() + } }) - - const consolaSuccess = vi.spyOn(consola, 'success').mockImplementation(() => undefined) - - try { - await context.run(['input.toon', '--output', 'output.json']) - - const output = await context.read('output.json') - expect(JSON.parse(output)).toEqual(data) - expect(consolaSuccess).toHaveBeenCalledWith('Decoded `input.toon` → `output.json`') - } - finally { - await context.cleanup() - } }) - it('writes encoded TOON to stdout when no output file is provided', async () => { - const data = { ok: true } - const context = await createCliTestContext({ - 'input.json': JSON.stringify(data), + describe('error handling', () => { + it('rejects invalid delimiter', async () => { + const context = await createCliTestContext({ + 'input.json': JSON.stringify({ value: 1 }), + }) + + const consolaError = vi.spyOn(consola, 'error').mockImplementation(() => undefined) + const exitSpy = vi.mocked(process.exit) + + try { + await context.run(['input.json', '--delimiter', ';']) + + expect(exitSpy).toHaveBeenCalledWith(1) + + const errorCall = consolaError.mock.calls.at(0) + expect(errorCall).toBeDefined() + const [error] = errorCall! + expect(error).toBeInstanceOf(Error) + expect(error.message).toContain('Invalid delimiter') + } + finally { + await context.cleanup() + } }) - const stdout: string[] = [] - const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown) => { - stdout.push(String(message ?? '')) + it('rejects invalid indent value', async () => { + const context = await createCliTestContext({ + 'input.json': JSON.stringify({ value: 1 }), + }) + + const consolaError = vi.spyOn(consola, 'error').mockImplementation(() => undefined) + const exitSpy = vi.mocked(process.exit) + + try { + await context.run(['input.json', '--indent', 'abc']) + + expect(exitSpy).toHaveBeenCalledWith(1) + + const errorCall = consolaError.mock.calls.at(0) + expect(errorCall).toBeDefined() + const [error] = errorCall! + expect(error).toBeInstanceOf(Error) + expect(error.message).toContain('Invalid indent value') + } + finally { + await context.cleanup() + } }) - try { - await context.run(['input.json']) + it('handles missing input file', async () => { + const context = await createCliTestContext({}) - expect(stdout).toHaveLength(1) - expect(stdout[0]).toBe(encode(data)) - } - finally { - logSpy.mockRestore() - await context.cleanup() - } - }) + const consolaError = vi.spyOn(consola, 'error').mockImplementation(() => undefined) + const exitSpy = vi.mocked(process.exit) - it('throws on an invalid delimiter argument', async () => { - const context = await createCliTestContext({ - 'input.json': JSON.stringify({ value: 1 }), + try { + await context.run(['nonexistent.json']) + + expect(exitSpy).toHaveBeenCalledWith(1) + expect(consolaError).toHaveBeenCalled() + } + finally { + await context.cleanup() + } }) - - const consolaError = vi.spyOn(consola, 'error').mockImplementation(() => undefined) - - try { - await expect(context.run(['input.json', '--delimiter', ';'])).resolves.toBeUndefined() - - const exitMock = vi.mocked(process.exit) - expect(exitMock).toHaveBeenCalledWith(1) - - const errorCall = consolaError.mock.calls.at(0) - expect(errorCall).toBeDefined() - const [error] = errorCall! - expect(error.message).toContain('Invalid delimiter') - } - finally { - await context.cleanup() - } }) })