Skip to content

Commit

Permalink
✅ Added Unit Tests (and fixed some discovered bugs) (#4)
Browse files Browse the repository at this point in the history
* 🔧 Switched from outdated extension

* ➕ Added Jest and related Deps

* 🔧 Added Jest config

* ♻️ Reworked input code

Error handling is no longer required

* 🔥 Removed dead code and uneeded code

* 🐛 Adjust names to be proper names

* 🐛 Also using getEmojiName on extracted code

* ✅ Added Tests for shared

* ✅ Added tests for gitmoji

* 🐛 Better Tag to version mapping

Based on supported prefix v/V

* ✅ Added tests for missing history functions

* 🐛 Improved Emoji Name extraction for variation emojis

* 🐛 Handling link not existing

* ♻️ Using emoji name now hence no transform needed

* ✅ Added test cases for special chars

* ✅ Added tests for extracting

* ✅ Added tests for generating
  • Loading branch information
Templum authored May 2, 2024
1 parent e56ea95 commit 64ef768
Show file tree
Hide file tree
Showing 17 changed files with 5,187 additions and 998 deletions.
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
"dbaeumer.vscode-eslint",
"Gruntfuggly.todo-tree",
"rvest.vs-code-prettier-eslint",
"kavod-io.vscode-jest-test-adapter",
"ms-vscode.test-adapter-converter",
"hbenl.vscode-test-explorer",
"pflannery.vscode-versionlens",
"me-dutour-mathieu.vscode-github-actions"
"me-dutour-mathieu.vscode-github-actions",
"Orta.vscode-jest"
]
}
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This action generates a `CHANGELOG` based on the git history using the [Gitmoji](https://gitmoji.dev/about) convention. If no CHANGELOG has been created yet, it will go ahead and create one based on the current version. Currently, only package.json is supported (feel free to request other tooling). If a `CHANGELOG.md` exists, it will append to it based on the commits between the last changelog entry and `HEAD`. This action will provide via outputs the place of the created/updated `CHANGELOG.md` so it can be committed or uploaded. For example, flows have a look at the [usage examples](#example-usage)

> Please be aware that this expects you to create Tags for the versions that may be prefixed with 'v'. e.g. Version=1.0.0 => TAG=1.0.0 or TAG=v1.0.0
> Please be aware that this expects you to create Tags for the versions that may be prefixed with 'v' or 'V'. e.g. Version=1.0.0 => TAG=1.0.0 or TAG=v1.0.0 or TAG=V1.0.0
## Inputs

Expand Down
137 changes: 137 additions & 0 deletions __tests__/changelog/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import fs from 'node:fs/promises';
import { extractEmojiFromMessage, getLastChangelogVersion, getPackageVersion } from '../../src/changelog/extract.js';

describe('Extract', () => {
const basePath = '/workspaces/gitmoji-changelog/';
const changelogFile = 'CHANGELOG.md';
const packageFile = 'package.json';

describe('getLastChangelogVersion', () => {
let readFileMock: typeof jest;

beforeAll(() => {
readFileMock = jest.mock('node:fs/promises');
});

afterEach(() => {
(fs.readFile as unknown as jest.SpyInstance).mockRestore();
});

afterAll(() => {
readFileMock.restoreAllMocks();
});

it('should obtain version from changelog', async () => {
fs.readFile = jest.fn().mockResolvedValue('<a name="0.0.1"></a>');

const version = await getLastChangelogVersion(`${basePath}${changelogFile}`);

expect(version).toEqual('0.0.1');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${changelogFile}`, { encoding: 'utf-8' });
});

it('should return empty string if changelog is empty', async () => {
fs.readFile = jest.fn().mockResolvedValue('');

const version = await getLastChangelogVersion(`${basePath}${changelogFile}`);

expect(version).toEqual('');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${changelogFile}`, { encoding: 'utf-8' });
});

it('should return empty string if anchor is not in changelog', async () => {
fs.readFile = jest
.fn()
.mockResolvedValue(
'- ➕ Added Jest and related Deps [[1dc0987](https://github.com/Templum/gitmoji-changelog/commit/1dc09876a225128aa63c4cba4e65d4bd6d820ba7)] (by Templum)',
);

const version = await getLastChangelogVersion(`${basePath}${changelogFile}`);

expect(version).toEqual('');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${changelogFile}`, { encoding: 'utf-8' });
});

it('should return empty string if error occured', async () => {
fs.readFile = jest.fn().mockRejectedValue(new Error('Failed to read file'));

const version = await getLastChangelogVersion(`${basePath}${changelogFile}`);

expect(version).toEqual('');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${changelogFile}`, { encoding: 'utf-8' });
});
});

describe('getPackageVersion', () => {
let readFileMock: typeof jest;

beforeAll(() => {
readFileMock = jest.mock('node:fs/promises');
});

afterEach(() => {
(fs.readFile as unknown as jest.SpyInstance).mockRestore();
});

afterAll(() => {
readFileMock.restoreAllMocks();
});

it('should return version obtained from package.json', async () => {
fs.readFile = jest.fn().mockResolvedValue('{ "version": "0.0.1" }');

const version = await getPackageVersion(basePath);

expect(version).toEqual('0.0.1');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${packageFile}`, { encoding: 'utf-8' });
});

it('should return 0.0.0 if package.json has no version field', async () => {
fs.readFile = jest.fn().mockResolvedValue('{ "name": "test" }');

const version = await getPackageVersion(basePath);

expect(version).toEqual('0.0.0');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${packageFile}`, { encoding: 'utf-8' });
});

it('should rethrow any error encountered', async () => {
fs.readFile = jest.fn().mockRejectedValue(new Error('Oops'));

expect(async () => await getPackageVersion(basePath)).rejects.toThrow('Oops');
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(`${basePath}${packageFile}`, { encoding: 'utf-8' });
});
});

describe('extractEmojiFromMessage', () => {
test.each([
{
message: '⚡️ Improved Performance',
emojiName: 'zap',
cleanedMessage: 'Improved Performance',
},
{
message: ':fire: Removed unused files',
emojiName: 'fire',
cleanedMessage: 'Removed unused files',
},
{
message: 'feat: allow provided config object to extend other configs',
emojiName: '',
cleanedMessage: 'at: allow provided config object to extend other configs',
},
])('extractEmojiFromMessage should extract "$emojiName" from message', ({ message, emojiName, cleanedMessage }) => {
const [extractedEmoji, extractedMessage] = extractEmojiFromMessage(message);

expect(extractedEmoji).toEqual(emojiName);
expect(extractedMessage).toEqual(cleanedMessage);
});
});
});
183 changes: 183 additions & 0 deletions __tests__/changelog/generate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { GitCommit } from '../../src/git/history.js';
import { generateChangelog, writeChangelog } from '../../src/changelog/generate.js';
import fs from 'node:fs/promises';

describe('Generate', () => {
describe('generateChangelog', () => {
const name = 'Templum';
const email = '[email protected]';

const short = 'e2cfc89';
const long = 'e2cfc89fae5b43594b2c649fd4c05bcc6d2d12ac';
const hash = { short, long };

const author = {
name,
email,
};

it('should create a changelog using provided history', () => {
const history: GitCommit[] = [
{
message: ':sparkles: Added Enterprise Logging', // Added
author,
hash,
},
{
message: ':fire: Removed Transition Code', // Removed
author,
hash,
},
{
message: '⚡️ Doubled Performance', // Changed
author,
hash,
},
{
message: '🔐 Secrets added', // Miscellaneous
author,
hash,
},
];

const changelog = generateChangelog(history, { addAuthors: true }, '1.0.0');

const entries = changelog.split('\n').filter((current) => current.length > 0);

const expectedCategories = ['Added', 'Removed', 'Changed', 'Miscellaneous'];
const actualCategories = entries.filter((current) => current.startsWith('###')).map((current) => current.replace('###', '').trim());

for (const expectedCategory of expectedCategories) {
expect(actualCategories).toContain(expectedCategory);
}

const commits = entries.filter((current) => current.startsWith('-'));

for (const commit of commits) {
expect(commit).toContain(name);
expect(commit).toContain(hash.short);
expect(commit).toContain(hash.long);
}
});

it('should ignore history entry that dont fit a category', () => {
const history: GitCommit[] = [
{
message: ':sparkles: Added Enterprise Logging',
author,
hash,
},
{
message: ':children_crossing: Improved UX',
author,
hash,
},
{
message: 'feat: Wrong commit convention',
author,
hash,
},
];

const changelog = generateChangelog(history, { addAuthors: true }, '1.0.0');

const commits = changelog
.split('\n')
.filter((current) => current.length > 0)
.filter((current) => current.startsWith('-'));

for (const commit of commits) {
expect(commit).not.toContain('Improved UX');
expect(commit).not.toContain('Wrong commit convention');
}
});

it('should not add authors if addAuthors is set to false', () => {
const history: GitCommit[] = [
{
message: ':sparkles: Added Enterprise Logging',
author: { name: 'HideMe', email },
hash,
},
];

const changelog = generateChangelog(history, { addAuthors: false }, '1.0.0');

const commits = changelog
.split('\n')
.filter((current) => current.length > 0)
.filter((current) => current.startsWith('-'));

for (const commit of commits) {
expect(commit).not.toContain('HideMe');
}
});

it('should embbed version in link', () => {
const history: GitCommit[] = [
{
message: ':sparkles: Added Enterprise Logging',
author: { name: 'HideMe', email },
hash,
},
];

const changelog = generateChangelog(history, { addAuthors: false }, '1.0.0');

const [link, heading] = changelog.split('\n').filter((current) => current.length > 0);
expect(link).toContain('1.0.0');
expect(heading).toContain('1.0.0');
});
});

describe('writeChangelog', () => {
const changelogPath = '/workspaces/gitmoji-changelog/CHANGELOG.md';

let fsMock: typeof jest;

beforeAll(() => {
fsMock = jest.mock('node:fs/promises');
});

afterEach(() => {
(fs.writeFile as unknown as jest.SpyInstance).mockRestore();
(fs.readFile as unknown as jest.SpyInstance).mockRestore();
});

afterAll(() => {
fsMock.restoreAllMocks();
});

it('should write initial changelog containing heading Changelog', async () => {
fs.writeFile = jest.fn().mockResolvedValue(Promise.resolve());
fs.readFile = jest.fn().mockRejectedValue(new Error('Should not be called'));

await writeChangelog(changelogPath, 'Initial', true);

expect(fs.writeFile).toHaveBeenCalledTimes(1);
expect(fs.writeFile).toHaveBeenCalledWith(changelogPath, `# Changelog\n\nInitial`, { encoding: 'utf-8' });
});

it('should append changelog', async () => {
fs.writeFile = jest.fn().mockResolvedValue(Promise.resolve());
fs.readFile = jest.fn().mockResolvedValue('# Changelog\n\nInitial');

await writeChangelog(changelogPath, 'Added', false);

expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith(changelogPath, { encoding: 'utf-8' });
expect(fs.writeFile).toHaveBeenCalledTimes(1);
expect(fs.writeFile).toHaveBeenCalledWith(changelogPath, `# Changelog\n\nAdded\n\nInitial`, { encoding: 'utf-8' });
});

it('should simply log encountered errors', async () => {
fs.writeFile = jest.fn().mockResolvedValue(Promise.resolve());
fs.readFile = jest.fn().mockRejectedValue(new Error('Oops'));

await writeChangelog(changelogPath, 'Initial');

expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.writeFile).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 64ef768

Please sign in to comment.