Skip to content

Commit

Permalink
test(utils): split out transform helper (#240)
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton authored Nov 13, 2023
1 parent 96c4d07 commit e778553
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 125 deletions.
18 changes: 10 additions & 8 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,21 @@ export { reportToMd } from './lib/report-to-md';
export { reportToStdout } from './lib/report-to-stdout';
export { ScoredReport, scoreReport } from './lib/scoring';
export {
countOccurrences,
distinct,
objectToEntries,
objectToKeys,
pluralize,
readJsonFile,
readTextFile,
toArray,
toUnixPath,
ensureDirectoryExists,
FileResult,
MultipleFileResults,
logMultipleFileResults,
slugify,
} from './lib/utils';
} from './lib/file-system';
export { verboseUtils } from './lib/verbose-utils';
export {
pluralize,
toArray,
objectToKeys,
objectToEntries,
countOccurrences,
distinct,
slugify,
} from './lib/transformation';
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MEMFS_VOLUME } from '@code-pushup/models/testing';
import { mockConsole, unmockConsole } from '../../test/console.mock';
import {
countOccurrences,
distinct,
ensureDirectoryExists,
logMultipleFileResults,
pluralize,
slugify,
toArray,
toUnixPath,
} from './utils';
} from './file-system';

// Mock file system API's
vi.mock('fs', async () => {
Expand All @@ -27,48 +22,6 @@ vi.mock('fs/promises', async () => {

const outputDir = MEMFS_VOLUME;

describe('slugify', () => {
it.each([
['Largest Contentful Paint', 'largest-contentful-paint'],
['cumulative-layout-shift', 'cumulative-layout-shift'],
['max-lines-200', 'max-lines-200'],
['rxjs/finnish', 'rxjs-finnish'],
['@typescript-eslint/no-explicit-any', 'typescript-eslint-no-explicit-any'],
['Code PushUp ', 'code-pushup'],
])('should transform "%s" to valid slug "%s"', (text, slug) => {
expect(slugify(text)).toBe(slug);
});
});

describe('pluralize', () => {
it.each([
['warning', 'warnings'],
['error', 'errors'],
['category', 'categories'],
['status', 'statuses'],
])('should pluralize "%s" as "%s"', (singular, plural) => {
expect(pluralize(singular)).toBe(plural);
});
});

describe('toArray', () => {
it('should transform non-array value into array with single value', () => {
expect(toArray('src/**/*.ts')).toEqual(['src/**/*.ts']);
});

it('should leave array value unchanged', () => {
expect(toArray(['*.ts', '*.js'])).toEqual(['*.ts', '*.js']);
});
});

describe('countOccurrences', () => {
it('should return record with counts for each item', () => {
expect(
countOccurrences(['error', 'warning', 'error', 'error', 'warning']),
).toEqual({ error: 3, warning: 2 });
});
});

describe('toUnixPath', () => {
it.each([
['main.ts', 'main.ts'],
Expand All @@ -91,24 +44,6 @@ describe('toUnixPath', () => {
});
});

describe('distinct', () => {
it('should remove duplicate strings from array', () => {
expect(
distinct([
'no-unused-vars',
'no-invalid-regexp',
'no-unused-vars',
'no-invalid-regexp',
'@typescript-eslint/no-unused-vars',
]),
).toEqual([
'no-unused-vars',
'no-invalid-regexp',
'@typescript-eslint/no-unused-vars',
]);
});
});

describe('ensureDirectoryExists', () => {
beforeEach(() => {
vol.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,6 @@ import chalk from 'chalk';
import { mkdir, readFile } from 'fs/promises';
import { formatBytes } from './report';

// === Transform

export function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/\s+|\//g, '-')
.replace(/[^a-z0-9-]/g, '');
}

export function pluralize(text: string): string {
if (text.endsWith('y')) {
return text.slice(0, -1) + 'ies';
}
if (text.endsWith('s')) {
return `${text}es`;
}
return `${text}s`;
}

export function toArray<T>(val: T | T[]): T[] {
return Array.isArray(val) ? val : [val];
}

export function objectToKeys<T extends object>(obj: T) {
return Object.keys(obj) as (keyof T)[];
}

export function objectToEntries<T extends object>(obj: T) {
return Object.entries(obj) as [keyof T, T[keyof T]][];
}

export function countOccurrences<T extends PropertyKey>(
values: T[],
): Partial<Record<T, number>> {
return values.reduce<Partial<Record<T, number>>>(
(acc, value) => ({ ...acc, [value]: (acc[value] ?? 0) + 1 }),
{},
);
}

export function distinct<T extends string | number | boolean>(array: T[]): T[] {
return Array.from(new Set(array));
}

// === Filesystem @TODO move to fs-utils.ts

export function toUnixPath(
path: string,
options?: { toRelative?: boolean },
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/lib/report-to-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
reportOverviewTableHeaders,
} from './report';
import { EnrichedScoredAuditGroup, ScoredReport } from './scoring';
import { slugify } from './utils';
import { slugify } from './transformation';

export function reportToMd(
report: ScoredReport,
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/lib/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
Report,
reportSchema,
} from '@code-pushup/models';
import { ScoredReport } from './scoring';
import {
ensureDirectoryExists,
pluralize,
readJsonFile,
readTextFile,
} from './utils';
} from './file-system';
import { ScoredReport } from './scoring';
import { pluralize } from './transformation';

export const FOOTER_PREFIX = 'Made with ❤️ by';
export const CODE_PUSHUP_DOMAIN = 'code-pushup.dev';
Expand Down
98 changes: 98 additions & 0 deletions packages/utils/src/lib/transformation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it } from 'vitest';
import {
countOccurrences,
distinct,
objectToEntries,
objectToKeys,
pluralize,
slugify,
toArray,
} from './transformation';

describe('slugify', () => {
it.each([
['Largest Contentful Paint', 'largest-contentful-paint'],
['cumulative-layout-shift', 'cumulative-layout-shift'],
['max-lines-200', 'max-lines-200'],
['rxjs/finnish', 'rxjs-finnish'],
['@typescript-eslint/no-explicit-any', 'typescript-eslint-no-explicit-any'],
['Code PushUp ', 'code-pushup'],
])('should transform "%s" to valid slug "%s"', (text, slug) => {
expect(slugify(text)).toBe(slug);
});
});

describe('pluralize', () => {
it.each([
['warning', 'warnings'],
['error', 'errors'],
['category', 'categories'],
['status', 'statuses'],
])('should pluralize "%s" as "%s"', (singular, plural) => {
expect(pluralize(singular)).toBe(plural);
});
});

describe('toArray', () => {
it('should transform non-array value into array with single value', () => {
expect(toArray('src/**/*.ts')).toEqual(['src/**/*.ts']);
});

it('should leave array value unchanged', () => {
expect(toArray(['*.ts', '*.js'])).toEqual(['*.ts', '*.js']);
});
});

describe('objectToKeys', () => {
it('should transform object into array of keys', () => {
const keys: 'prop1'[] = objectToKeys({ prop1: 1 });
expect(keys).toEqual(['prop1']);
});

it('should transform empty object into empty array', () => {
const keys: never[] = objectToKeys({});
expect(keys).toEqual([]);
});
});

describe('objectToEntries', () => {
it('should transform object into array of entries', () => {
const keys: ['prop1', number][] = objectToEntries({ prop1: 1 });
expect(keys).toEqual([['prop1', 1]]);
});

it('should transform empty object into empty array', () => {
const keys: [never, never][] = objectToEntries({});
expect(keys).toEqual([]);
});
});

describe('countOccurrences', () => {
it('should return record with counts for each item', () => {
expect(
countOccurrences(['error', 'warning', 'error', 'error', 'warning']),
).toEqual({ error: 3, warning: 2 });
});

it('should return empty record for no matches', () => {
expect(countOccurrences([])).toEqual({});
});
});

describe('distinct', () => {
it('should remove duplicate strings from array', () => {
expect(
distinct([
'no-unused-vars',
'no-invalid-regexp',
'no-unused-vars',
'no-invalid-regexp',
'@typescript-eslint/no-unused-vars',
]),
).toEqual([
'no-unused-vars',
'no-invalid-regexp',
'@typescript-eslint/no-unused-vars',
]);
});
});
42 changes: 42 additions & 0 deletions packages/utils/src/lib/transformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/\s+|\//g, '-')
.replace(/[^a-z0-9-]/g, '');
}

export function pluralize(text: string): string {
if (text.endsWith('y')) {
return text.slice(0, -1) + 'ies';
}
if (text.endsWith('s')) {
return `${text}es`;
}
return `${text}s`;
}

export function toArray<T>(val: T | T[]): T[] {
return Array.isArray(val) ? val : [val];
}

export function objectToKeys<T extends object>(obj: T) {
return Object.keys(obj) as (keyof T)[];
}

export function objectToEntries<T extends object>(obj: T) {
return Object.entries(obj) as [keyof T, T[keyof T]][];
}

export function countOccurrences<T extends PropertyKey>(
values: T[],
): Partial<Record<T, number>> {
return values.reduce<Partial<Record<T, number>>>(
(acc, value) => ({ ...acc, [value]: (acc[value] ?? 0) + 1 }),
{},
);
}

export function distinct<T extends string | number | boolean>(array: T[]): T[] {
return Array.from(new Set(array));
}

0 comments on commit e778553

Please sign in to comment.