From c4ad14507d070cb10d75180ba2bce0d56b67845e Mon Sep 17 00:00:00 2001 From: damianpolak Date: Wed, 14 May 2025 12:28:33 +0200 Subject: [PATCH 1/5] chore: add CI workflow and update Jest test matching patterns --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ jest.config.js | 3 +-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..34401e2 --- /dev/null +++ b/.github/workflows/ci.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@v3 + 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', From b0b001e8fe4fcd41cf3e5b19e217abfc031bb5f9 Mon Sep 17 00:00:00 2001 From: damianpolak Date: Wed, 14 May 2025 12:28:57 +0200 Subject: [PATCH 2/5] test: add comprehensive tests for config and feature modules --- test/config.test.ts | 107 +++++++++++++++++++++++ test/feature.test.ts | 201 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 test/config.test.ts create mode 100644 test/feature.test.ts 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' + ); + }); + }); +}); From b09fe84335b66082a5305dcfb5ed46d8d568955c Mon Sep 17 00:00:00 2001 From: Damian Polak Date: Wed, 14 May 2025 12:35:31 +0200 Subject: [PATCH 3/5] Create main.yml Create gha workflows --- .github/workflows/main.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..34401e2 --- /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@v3 + with: + name: dist-${{ matrix.node-version }} + path: dist/ From 5e3febe040b145998217efd292512ee379b39c3e Mon Sep 17 00:00:00 2001 From: damianpolak Date: Wed, 14 May 2025 12:38:22 +0200 Subject: [PATCH 4/5] refactor: delete ci.yml --- .github/workflows/ci.yml | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 34401e2..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,38 +0,0 @@ -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@v3 - with: - name: dist-${{ matrix.node-version }} - path: dist/ From 13b48550b12efda32c4e2fed40b6bd3fd2d167b8 Mon Sep 17 00:00:00 2001 From: damianpolak Date: Wed, 14 May 2025 12:45:02 +0200 Subject: [PATCH 5/5] ci: update upload-artifact action to v4 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 34401e2..41964e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: run: npm run build - name: Upload build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist-${{ matrix.node-version }} path: dist/