diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..41964e6 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + test-and-build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x, 22.x, 24.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test + + - name: Build + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.node-version }} + path: dist/ diff --git a/jest.config.js b/jest.config.js index a0bfe47..51c61af 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,7 @@ module.exports = { roots: ['/test'], testMatch: [ - '**/__tests__/**/*.+(ts|tsx|js)', - '**/?(*.)+(spec|test).+(ts|tsx|js)', + '**/*.+(spec|test).+(ts|tsx|js)', ], transform: { '^.+\\.(ts|tsx)$': 'ts-jest', diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..9ca5ef3 --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,107 @@ +import { cliArgsParse } from '../src/cli'; +import { CLIArguments } from '../src/types'; + +// Mock the cli module +jest.mock('../src/cli', () => ({ + cliArgsParse: jest.fn(), +})); + +describe('config', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create config object with default values when no CLI args provided', () => { + // Mock empty CLI args + (cliArgsParse as jest.Mock).mockReturnValue({}); + + // Re-import config to get fresh instance + jest.isolateModules(() => { + const { config } = require('../src/config'); + + expect(config.tag).toBe('[NodePostBuild]'); + expect(config.general.npmCommand).toBe('npm install'); + expect(config.general.packageAddEntries).toEqual([]); + expect(config.general.packageKeepEntries).toEqual([ + 'name', + 'version', + 'description', + 'private', + 'main', + 'author', + 'contributors', + 'license', + 'repository', + 'dependencies', + 'engines', + 'config', + ]); + }); + }); + + it('should create config object with provided CLI args', () => { + // Mock CLI args + const mockArgs: Partial = { + pkgSrc: './src', + pkgDst: './dist', + zipDir: './dist', + zipOut: './output', + filesSrcToCopy: ['file1.txt', 'file2.txt'], + filesDstToCopy: ['dist/file1.txt', 'dist/file2.txt'], + remove: ['tmp/'], + commands: ['echo "test"'], + tasks: ['package', 'zip'], + }; + + (cliArgsParse as jest.Mock).mockReturnValue(mockArgs); + + // Re-import config to get fresh instance + jest.isolateModules(() => { + const { config } = require('../src/config'); + + expect(config.resources.packageJson).toEqual({ + src: './src', + dst: './dist', + }); + + expect(config.resources.compressing).toEqual({ + src: './dist', + dst: './output', + }); + + expect(config.resources.files).toEqual([ + { src: 'file1.txt', dst: 'dist/file1.txt' }, + { src: 'file2.txt', dst: 'dist/file2.txt' }, + ]); + + expect(config.resources.remove).toEqual(['tmp/']); + expect(config.resources.commands).toEqual(['echo "test"']); + expect(config.tasks).toEqual(['package', 'zip']); + }); + }); + + it('should handle undefined CLI args gracefully', () => { + // Mock partially undefined CLI args + const mockArgs: Partial = { + pkgSrc: './src', + pkgDst: './dist', + }; + + (cliArgsParse as jest.Mock).mockReturnValue(mockArgs); + + // Re-import config to get fresh instance + jest.isolateModules(() => { + const { config } = require('../src/config'); + + expect(config.resources.packageJson).toEqual({ + src: './src', + dst: './dist', + }); + + expect(config.resources.files).toBeUndefined(); + expect(config.resources.remove).toBeUndefined(); + expect(config.resources.commands).toBeUndefined(); + expect(config.tasks).toBeUndefined(); + }); + }); +}); diff --git a/test/feature.test.ts b/test/feature.test.ts new file mode 100644 index 0000000..c7d1e4a --- /dev/null +++ b/test/feature.test.ts @@ -0,0 +1,201 @@ +import { Feature } from '../src/feature'; +import fs from 'fs'; +import path from 'path'; +import JSZip from 'jszip'; +import { exec } from 'child_process'; +import { Logger } from '../src/logger'; + +// Mock dependencies +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + promises: { + readFile: jest.fn(), + copyFile: jest.fn(), + }, + readdirSync: jest.fn(), + statSync: jest.fn(), + readFileSync: jest.fn(), + existsSync: jest.fn(), + mkdirSync: jest.fn(), + writeFileSync: jest.fn(), + rmSync: jest.fn(), +})); +jest.mock('jszip'); +jest.mock('child_process'); +jest.mock('../src/logger'); + +describe('Feature', () => { + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + }); + + describe('zipDirectory', () => { + it('should log error when paths are undefined', () => { + Feature.zipDirectory(undefined, undefined); + expect(Logger.justlog).toHaveBeenCalledWith( + 'Compress directory path and destination path cannot be empty' + ); + }); + it('should create zip file from directory', async () => { + const mockZip = { + file: jest.fn(), + generateAsync: jest.fn().mockResolvedValue(Buffer.from('test')), + }; + (jest.mocked(JSZip) as unknown as jest.Mock).mockImplementation(() => mockZip); + + const mockFiles = ['file1.txt', 'file2.txt']; + const mockStats = { + isDirectory: () => false, + isFile: () => true, + isSymbolicLink: () => false, + size: 0, + atimeMs: 0, + mtimeMs: 0, + ctimeMs: 0, + birthtimeMs: 0, + atime: new Date(), + mtime: new Date(), + ctime: new Date(), + birthtime: new Date(), + }; + + (fs.readdirSync as jest.Mock).mockReturnValue(mockFiles); + (fs.statSync as jest.Mock).mockReturnValue(mockStats); + (fs.readFileSync as jest.Mock).mockReturnValue('file content'); + (fs.existsSync as jest.Mock).mockReturnValue(false); + (fs.mkdirSync as jest.Mock).mockReturnValue(''); + (fs.writeFileSync as jest.Mock).mockReturnValue(undefined); + + await Feature.zipDirectory('sourceDir', 'destDir'); + + expect(fs.mkdirSync).toHaveBeenCalledWith('destDir'); + expect(mockZip.file).toHaveBeenCalledTimes(2); + expect(fs.writeFileSync).toHaveBeenCalled(); + }); + }); + + describe('packagesProcess', () => { + it('should log error when paths are undefined', async () => { + await Feature.packagesProcess(undefined, undefined); + expect(Logger.justlog).toHaveBeenCalledWith( + 'Package source and destination directory cannot be empty' + ); + }); + + it('should process package.json with keep entries and add entries', async () => { + const mockPackageJson = JSON.stringify({ + name: 'test-package', + version: '1.0.0', + dependencies: {}, + }); + + jest.mocked(fs.promises.readFile).mockResolvedValue(mockPackageJson); + jest.mocked(fs.writeFileSync).mockReturnValue(undefined); + + const keepEntries = ['name', 'version']; + const addEntries = [{ name: 'author', value: 'Test Author' }]; + + await Feature.packagesProcess('sourceDir', 'destDir', keepEntries, addEntries); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('"name": "test-package"') + ); + }); + }); + + describe('copyFiles', () => { + it('should copy files successfully', async () => { + const files = [ + { src: 'src1', dst: 'dst1' }, + { src: 'src2', dst: 'dst2' }, + ]; + + jest.mocked(fs.promises.copyFile).mockResolvedValue(undefined); + + await Feature.copyFiles(files); + + expect(fs.promises.copyFile).toHaveBeenCalledTimes(2); + }); + + it('should handle errors when copying files', async () => { + const files = [{ src: 'src1', dst: 'dst1' }]; + + jest.mocked(fs.promises.copyFile).mockRejectedValue(new Error('Copy failed')); + + await Feature.copyFiles(files); + + expect(Logger.justlog).toHaveBeenCalledWith('Something is wrong with copying files...'); + }); + }); + + describe('installDeps', () => { + it('should execute npm command successfully', async () => { + const mockExec = jest.mocked(exec) as unknown as jest.Mock; + mockExec.mockImplementation((command, options, callback) => { + callback(null, { stdout: 'success', stderr: '' }); + }); + + const consoleSpy = jest.spyOn(console, 'log'); + + await Feature.installDeps('npm install', '/test/path'); + + expect(mockExec).toHaveBeenCalledWith( + 'npm install', + expect.objectContaining({ cwd: '/test/path' }), + expect.any(Function) + ); + expect(consoleSpy).toHaveBeenCalledWith('success'); + }); + }); + + describe('remove', () => { + it('should log message when no paths provided', () => { + Feature.remove([]); + expect(Logger.justlog).toHaveBeenCalledWith('No resources to delete...'); + }); + + it('should remove files/directories', () => { + const paths = ['path1', 'path2']; + (fs.rmSync as jest.Mock).mockImplementation(() => {}); + + Feature.remove(paths); + + expect(fs.rmSync).toHaveBeenCalledTimes(2); + expect(fs.rmSync).toHaveBeenCalledWith('path1', { recursive: true }); + expect(fs.rmSync).toHaveBeenCalledWith('path2', { recursive: true }); + }); + }); + + describe('runCommands', () => { + it('should log message when no commands provided', async () => { + await Feature.runCommands([]); + expect(Logger.justlog).toHaveBeenCalledWith('No commands to run...'); + }); + it('should execute commands successfully', async () => { + const mockExec = jest.mocked(exec) as unknown as jest.Mock; + mockExec.mockImplementation((command, callback) => { + callback(null, { stdout: 'success', stderr: '' }); + }); + + await Feature.runCommands(['command1', 'command2']); + + expect(mockExec).toHaveBeenCalledTimes(2); + expect(Logger.justlog).toHaveBeenCalledWith("Command 'command1' out: ", 'success'); + }); + it('should handle command execution errors', async () => { + const mockExec = jest.mocked(exec) as unknown as jest.Mock; + mockExec.mockImplementation((command, callback) => { + callback(new Error('Command failed'), { stdout: '', stderr: 'error' }); + }); + + await Feature.runCommands(['command1']); + + expect(Logger.justlog).toHaveBeenCalledWith( + 'The command could not be executed: ', + 'Command failed' + ); + }); + }); +});