UNPKG

@moriware/rn-create-template

Version:

Ferramenta CLI da MoriWare, desenvolvida por Caio Mori, para gerar rapidamente componentes, telas, hooks e navegação em projetos React Native com padrões recomendados de código, tipos, estilos e testes integrados.

338 lines (280 loc) 11.4 kB
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import fs from 'node:fs'; import path from 'node:path'; import chalk from 'chalk'; import inquirer from 'inquirer'; const moduleUnderTest = await import('../bin/index.mjs'); const { sleep, progressStep, ensureDirectory, generateFile, createIndexFile, ArtifactGenerator, buildComponentFiles, buildScreenFiles, buildHookFiles, buildNavigationFiles, capitalizeFirstLetter, lowercaseFirstLetter, handleCreationFlow, showWelcome, main, GENERATORS, } = moduleUnderTest; const originalGenerators = { component: GENERATORS.component, screen: GENERATORS.screen, hook: GENERATORS.hook, navigation: GENERATORS.navigation, }; const originalArgv = [...process.argv]; beforeAll(() => { chalk.level = 0; }); afterEach(() => { jest.restoreAllMocks(); jest.useRealTimers(); GENERATORS.component = originalGenerators.component; GENERATORS.screen = originalGenerators.screen; GENERATORS.hook = originalGenerators.hook; GENERATORS.navigation = originalGenerators.navigation; process.argv = [...originalArgv]; }); describe('sleep', () => { it('resolves after the specified delay', async () => { jest.useFakeTimers(); const promise = sleep(500); jest.advanceTimersByTime(500); await expect(promise).resolves.toBeUndefined(); }); }); describe('progressStep', () => { it('logs the styled message and waits for sleep', async () => { jest.useFakeTimers(); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); const styleMock = jest.fn((value) => value.toUpperCase()); const promise = progressStep('fazendo magia', styleMock, 123); expect(styleMock).toHaveBeenCalledWith('› fazendo magia'); expect(logSpy).toHaveBeenCalledWith('› fazendo magia'.toUpperCase()); jest.advanceTimersByTime(123); await promise; }); }); describe('ensureDirectory', () => { it('creates the directory when it does not exist', async () => { const existsSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(false); const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); await ensureDirectory('/tmp/new-dir', chalk.white); expect(logSpy).toHaveBeenCalledWith('› Criando diretório base'); expect(mkdirSpy).toHaveBeenCalledWith('/tmp/new-dir', { recursive: true }); expect(existsSpy).toHaveBeenCalledWith('/tmp/new-dir'); }); it('acknowledges existing directory without recreating it', async () => { jest.spyOn(fs, 'existsSync').mockReturnValue(true); const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); await ensureDirectory('/tmp/existing-dir', chalk.white); expect(logSpy).toHaveBeenCalledWith('› Diretório encontrado, atualizando arquivos'); expect(mkdirSpy).not.toHaveBeenCalled(); }); }); describe('generateFile', () => { it('writes the file after showing progress', async () => { const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); await generateFile('/tmp/file.ts', 'conteúdo', 'Mensagem', chalk.white); expect(logSpy).toHaveBeenCalledWith('› Mensagem'); expect(writeSpy).toHaveBeenCalledWith('/tmp/file.ts', 'conteúdo'); }); }); describe('createIndexFile', () => { it('writes index file according to the artifact type', async () => { const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); await createIndexFile('/tmp/components/sample', 'sample', 'component', chalk.white); expect(logSpy).toHaveBeenCalledWith('› Linkando exports em index.ts'); expect(writeSpy).toHaveBeenCalledWith( path.join('/tmp/components/sample', 'index.ts'), "export * from './sampleComponent';\nexport * from './sampleTypes';\n", ); }); }); describe('ArtifactGenerator', () => { it('creates artifacts and index file using provided strategies', async () => { jest.spyOn(fs, 'existsSync').mockReturnValue(false); jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); const generator = new ArtifactGenerator({ label: 'Test', color: chalk.white, resolveBasePath: (name) => `/tmp/${name}`, buildFiles: (name) => [ { filename: `${name}.ts`, content: 'conteúdo', message: 'criando arquivo' }, ], indexType: 'component', successMessage: (name) => `sucesso ${name}`, }); await generator.generate('demo'); expect(writeSpy).toHaveBeenCalledWith( path.join('/tmp/demo', 'demo.ts'), 'conteúdo', ); expect(writeSpy).toHaveBeenCalledWith( path.join('/tmp/demo', 'index.ts'), "export * from './demoComponent';\nexport * from './demoTypes';\n", ); expect(logSpy).toHaveBeenCalledWith('sucesso demo'); }); it('skips index file when no indexType is provided', async () => { jest.spyOn(fs, 'existsSync').mockReturnValue(false); jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); const generator = new ArtifactGenerator({ label: 'Test', color: chalk.white, resolveBasePath: () => '/tmp/demo', buildFiles: () => [], successMessage: () => 'ok', }); await generator.generate('demo'); const filenames = writeSpy.mock.calls.map(([filename]) => filename); expect(filenames.some((file) => file.endsWith('index.ts'))).toBe(false); }); }); describe('file builders', () => { it('buildComponentFiles returns expected structure', () => { const files = buildComponentFiles('sampleName'); expect(files.map((file) => file.filename)).toEqual([ 'sampleNameComponent.tsx', 'sampleNameStyles.ts', 'sampleNameTypes.ts', 'sampleNameFunctions.ts', 'sampleNameComponent.test.tsx', ]); expect(files[0].content).toContain('export const SampleNameComponent'); }); it('buildScreenFiles returns expected structure', () => { const files = buildScreenFiles('flow'); expect(files.map((file) => file.filename)).toEqual([ 'flowScreen.tsx', 'flowStyles.ts', 'flowTypes.ts', 'flowFunctions.ts', 'flowScreen.test.tsx', ]); expect(files[0].content).toContain('export const FlowScreen'); }); it('buildHookFiles returns expected structure', () => { const files = buildHookFiles('amazing'); expect(files.map((file) => file.filename)).toEqual([ 'amazing.tsx', 'amazingTypes.ts', 'amazing.test.ts', ]); expect(files[0].content).toContain('export function useAmazing()'); }); it('buildNavigationFiles returns navigator template', () => { const files = buildNavigationFiles('journey'); expect(files).toHaveLength(1); expect(files[0].filename).toBe('JourneyNavigation.tsx'); expect(files[0].content).toContain("Stack.Screen name=\"Journey\""); }); }); describe('helpers', () => { it('capitalizeFirstLetter capitalizes correctly', () => { expect(capitalizeFirstLetter('hello')).toBe('Hello'); expect(capitalizeFirstLetter('')).toBe(''); expect(capitalizeFirstLetter()).toBe(''); }); it('lowercaseFirstLetter lowercases correctly', () => { expect(lowercaseFirstLetter('Hello')).toBe('hello'); expect(lowercaseFirstLetter('')).toBe(''); expect(lowercaseFirstLetter()).toBe(''); }); }); describe('handleCreationFlow', () => { it('delegates to the correct generator', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); const generatorMock = { ...originalGenerators.component, generate: jest.fn().mockResolvedValue(), }; GENERATORS.component = generatorMock; await handleCreationFlow('component', ' DemoName '); expect(generatorMock.generate).toHaveBeenCalledWith('demoName'); expect(logSpy).toHaveBeenCalledWith('› Voltando ao menu principal...'); }); it('throws for unsupported generator types', async () => { await expect(handleCreationFlow('unknown', 'name')).rejects.toThrow( 'Tipo "unknown" não suportado.', ); }); }); describe('showWelcome', () => { it('prints formatted welcome text and pauses', async () => { const clearSpy = jest.spyOn(console, 'clear').mockImplementation(() => {}); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); await showWelcome(); expect(clearSpy).toHaveBeenCalled(); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('React Native Create Template CLI')); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Selecione uma opção no menu')); }); }); describe('main', () => { it('executes creation flow when type and name arguments are provided', async () => { process.argv = ['node', 'script', 'component', 'NewItem']; const generateMock = jest.fn().mockResolvedValue(); GENERATORS.component = { ...originalGenerators.component, generate: generateMock, }; await main(); expect(generateMock).toHaveBeenCalledWith('newItem'); }); it('prompts for name when only type is provided', async () => { process.argv = ['node', 'script', 'component']; const generateMock = jest.fn().mockResolvedValue(); GENERATORS.component = { ...originalGenerators.component, generate: generateMock, }; jest.spyOn(inquirer, 'prompt').mockResolvedValue({ name: 'FromPrompt' }); await main(); expect(generateMock).toHaveBeenCalledWith('fromPrompt'); }); it('runs interactive loop when no arguments are provided', async () => { process.argv = ['node', 'script']; const generateMock = jest.fn().mockResolvedValue(); GENERATORS.component = { ...originalGenerators.component, generate: generateMock, }; jest .spyOn(inquirer, 'prompt') .mockResolvedValueOnce({ type: 'component' }) .mockResolvedValueOnce({ name: 'InteractiveItem' }) .mockResolvedValueOnce({ type: 'exit' }); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); await main(); expect(generateMock).toHaveBeenCalledWith('interactiveItem'); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Obrigado por usar o RN Create Template')); }); it('handles user-initiated exit gracefully', async () => { process.argv = ['node', 'script']; const promptError = new Error('saindo'); promptError.name = 'ExitPromptError'; jest.spyOn(inquirer, 'prompt').mockImplementation(() => { throw promptError; }); const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); await main(); expect(logSpy).toHaveBeenCalledWith( expect.stringContaining('Programa interrompido pelo usuário'), ); expect(exitSpy).toHaveBeenCalledWith(0); }); });