Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests #5

Merged
merged 21 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.yml]
indent_style = space
indent_size = 2
49 changes: 49 additions & 0 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI
on:
push:
branches:
- "!main"
- "*"
pull_request:
branches:
- "dev"
- "main"

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install PNPM
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false

- name: Install Node 20.x
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: pnpm

- name: Install Dependencies
run: pnpm install

- name: Check Code Style
run: pnpm lint

- name: Run Build
run: pnpm build

- name: Run Unit Tests
run: pnpm test:ci

- name: Summarise Tests
uses: test-summary/action@v2
with:
paths: "__tests__/summary.xml"
show: "fail, skip"
if: always()
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm test
6 changes: 6 additions & 0 deletions .xo-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"import/extensions": "off",
"unicorn/prefer-module": "off"
}
}
47 changes: 47 additions & 0 deletions __tests__/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {simpleFaker as Faker} from '@faker-js/faker';

type Config = {
howMany?: number;
};

type MaybeNumber = number | undefined | unknown;

const defaultDataSize = 100;

const factory = <T>(howMany: number, generator: () => T) => Faker.helpers.multiple<T>(generator, {count: howMany});

export const uuids = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.string.uuid);
};

export const dates = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.date.recent);
};

export const numbers = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.string.numeric).map(n => Number.parseInt(n, 10));
};

export const hexadecimals = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.string.hexadecimal);
};

const mulpleOf = (multiple: number, item: MaybeNumber) => {
if (
!item
|| typeof item !== 'number'
|| item % multiple !== 0
) {
return false;
}

return true;
};

export const isEven = (number_: MaybeNumber) => mulpleOf(2, number_);
export const isMultipleOfFive = (number_: MaybeNumber) => mulpleOf(5, number_);
export const isDate = (date: unknown) => date instanceof Date && !Number.isNaN(date.getTime());
97 changes: 97 additions & 0 deletions __tests__/features.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as _ from 'jest-extended';
import {tuplesFromArray} from '../src/index';
import {
uuids, dates, hexadecimals, numbers, isEven, isDate,
} from './data';

type ParameterDataset<D> = {data: D[]; size: number; type?: string};

describe('nTuple Array', () => {
it('should use default maxItems value', () => {
const tuples = tuplesFromArray({list: numbers()});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(2);
}
});

it('should use provided maxItems value', () => {
const tuples = tuplesFromArray({list: numbers(), maxItems: 5});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(5);
}
});

it('should work without a match function', () => {
const tuples = tuplesFromArray({list: uuids(), maxItems: 10});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(10);
}
});

it('should use match function when provided', () => {
const nums = Array.from({length: 100}, (_, i: number) => i + 1);
const tuples = tuplesFromArray({list: nums, maxItems: 5, match: isEven});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(5);
expect(tuple).toSatisfyAll(isEven);
}
});

it.each<ParameterDataset<string>>([
{data: uuids({howMany: 50}), size: 2},
{data: uuids({howMany: 55}), size: 5},
{data: uuids({howMany: 200}), size: 10},
])('should produce correct quantities when sub-arrays have $size items', ({data, size}) => {
const tuples = tuplesFromArray({list: data, maxItems: size});
const allItemsFlattened: Array<string | undefined > = [];
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(size);
allItemsFlattened.push(...tuple);
}

expect(allItemsFlattened).toBeArrayOfSize(data.length);
});

it.each<ParameterDataset<string>>([
{data: hexadecimals({howMany: 5}), size: 3},
{data: hexadecimals({howMany: 59}), size: 5},
{data: hexadecimals({howMany: 108}), size: 10},
])('should produce correct remainder quantities when sub-arrays are not all maxItems size', ({data, size}) => {
const tuples = tuplesFromArray({list: data, maxItems: size});
const allItemsFlattened: Array<string | undefined > = [];
const localTuples: Array<Array<string | undefined >> = [];
for (const tuple of tuples) {
allItemsFlattened.push(...tuple);
localTuples.push(tuple);
}

expect(allItemsFlattened).toBeArrayOfSize(data.length);

for (const tuple of localTuples) {
expect(tuple).toBeArray();
expect(tuple.length).toBeLessThanOrEqual(size);
expect(tuple.length).toBeGreaterThanOrEqual(data.length % size);
}

const hasRemainder = localTuples.pop();
expect(hasRemainder).toBeArrayOfSize(data.length % size);
});

it.each<ParameterDataset<number | Date>>([
{data: dates({howMany: 50}), size: 10, type: 'date'},
{data: Array.from({length: 50}, (_, i: number) => i + 1), size: 5, type: 'number'},
])('should produce sub-arrays with the correct data types', ({data, size, type}) => {
const matcher = type === 'number' ? isEven : undefined;
const tuples = tuplesFromArray({list: data, maxItems: size, match: matcher});

for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(size);

const predicate = type === 'number'
? isEven
: (type === 'date' ? isDate : () => false);

expect(tuple).toSatisfyAll(predicate);
}
});
});
3 changes: 3 additions & 0 deletions __tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {toBeArray, toBeArrayOfSize, toSatisfyAll} from 'jest-extended';

expect.extend({toBeArray, toBeArrayOfSize, toSatisfyAll});
60 changes: 60 additions & 0 deletions __tests__/smoke.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const {tuplesFromArray, InvalidInvocationParameterError} = require('../dist/index.js');

describe('Smoke Tests', () => {
it('should throw if input array is not specified', () => {
function functionInClientCode() {
tuplesFromArray({});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if input array is undefined', () => {
function functionInClientCode() {
tuplesFromArray({list: undefined});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if input array is not an array', () => {
function functionInClientCode() {
tuplesFromArray({list: 'some iterable object'});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if maxItems param not a number', () => {
function functionInClientCode() {
tuplesFromArray({list: [], maxItems: {}});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if maxItems param is 0', () => {
function functionInClientCode() {
tuplesFromArray({list: [], maxItems: 0});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if maxItems param is < 0', () => {
function functionInClientCode() {
tuplesFromArray({list: [], maxItems: -2});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if the match param not a function', () => {
function functionInClientCode() {
tuplesFromArray({list: [], match: {}});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});
});

17 changes: 17 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type IterationResult<T> = {
done: boolean;
value: Array<T | undefined>;
};
type Matcher<T> = (item: T | undefined) => boolean;
export type TupleConfig<T> = {
list: T[];
maxItems?: number;
match?: Matcher<T>;
};
export declare class InvalidInvocationParameterError extends Error {
}
export declare const tuplesFromArray: <T>(config: TupleConfig<T>) => {
[Symbol.iterator](): any;
next(): IterationResult<T>;
};
export {};
59 changes: 59 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tuplesFromArray = exports.InvalidInvocationParameterError = void 0;
class InvalidInvocationParameterError extends Error {
}
exports.InvalidInvocationParameterError = InvalidInvocationParameterError;
const validateParametersOrThrow = (list, maxItems, match) => {
if (!list || !Array.isArray(list)) {
throw new InvalidInvocationParameterError('expected list to be an array');
}
if (typeof maxItems !== 'number'
|| (typeof maxItems === 'number' && maxItems <= 0)) {
const message = 'expected maxItems (when provided) to be a positive integer (1 and above)';
throw new InvalidInvocationParameterError(message);
}
if (match !== undefined && typeof match !== 'function') {
const message = 'expected match (when provided) to be a function';
throw new InvalidInvocationParameterError(message);
}
};
const tuplesFromArray = (config) => {
const { list, match, maxItems = 2 } = config;
validateParametersOrThrow(list, maxItems, match);
let cursor = 0;
const maxItemSize = Number.parseInt(`${maxItems}`, 10);
const iterable = {
[Symbol.iterator]() {
return this;
},
next() {
if (cursor >= list.length) {
return { done: true, value: [] };
}
const items = [];
const endIndex = match === undefined
// A match funtion was provided. Okay to run to array end
// or until we've matched maxItemSize elements
? Math.min(cursor + maxItemSize, list.length)
// No match function was provided. We should run till we are
// out of items (list.length) or till we gotten the next set
// of maxItemSize items
: list.length;
while (cursor < endIndex) {
const item = list[cursor];
cursor += 1;
if (match && !match(item)) {
continue;
}
items.push(item);
if (match && items.length === maxItemSize) {
break;
}
}
return { done: false, value: items };
},
};
return iterable;
};
exports.tuplesFromArray = tuplesFromArray;
22 changes: 22 additions & 0 deletions jest.config.ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {JestConfigWithTsJest} from 'ts-jest';

const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: [
'/**/*.test.*',
],
transformIgnorePatterns: [
'/node_modules/',
'\\.pnp\\.[^\\/]+$',
],
reporters: [
'default',
['jest-junit', {
outputFile: '__tests__/summary.xml',
}],
],
setupFilesAfterEnv: ['./__tests__/setup.ts'],
};

export default jestConfig;
Loading
Loading