Skip to content

Commit

Permalink
chore: migrate to vitest
Browse files Browse the repository at this point in the history
  • Loading branch information
GriffinSauce committed Jul 15, 2022
1 parent e3653cd commit e0b3a10
Show file tree
Hide file tree
Showing 12 changed files with 1,282 additions and 119 deletions.
1,174 changes: 1,148 additions & 26 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"lint": "eslint ./src/ --fix",
"prepare": "husky install && wds ./scripts/download-device-descriptors.ts",
"semantic-release": "semantic-release",
"test:watch": "jest --watch",
"test": "jest --coverage",
"test:watch": "vitest --watch --config test/vitest.config.ts",
"test": "vitest --coverage --config test/vitest.config.ts",
"typecheck": "tsc --noEmit"
},
"repository": {
Expand Down Expand Up @@ -85,6 +85,7 @@
"@types/w3c-web-serial": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"c8": "^7.11.3",
"codecov": "^3.8.3",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
Expand All @@ -101,6 +102,7 @@
"ts-jest": "^27.0.5",
"ts-node": "^10.2.1",
"typescript": "^4.2.4",
"vitest": "^0.18.0",
"wds": "^0.12.0",
"xstate": "^4.32.1"
},
Expand All @@ -118,6 +120,8 @@
]
},
"dependencies": {
"@serialport/bindings-cpp": "^10.7.0",
"@serialport/bindings-interface": "^1.2.2",
"@serialport/parser-regex": "^10.3.0",
"debug": "^4.3.4",
"serialport": "^10.4.0"
Expand Down
4 changes: 2 additions & 2 deletions src/BaseDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class BaseDevice {
const timeout = setTimeout(() => {
debug(`Timeout executing "${formattedCommand}"`);
this.#port.off('data', handleResponse);
_reject('command timed out');
_reject(new Error('command timed out'));
}, TIMEOUT_MS);

const cleanup = () => {
Expand All @@ -81,7 +81,7 @@ export class BaseDevice {
const reject = (errorMessage: string) => {
debug(`Error executing "${formattedCommand}" - ${errorMessage}`);
cleanup();
_reject(errorMessage);
_reject(new Error(errorMessage));
};

const handleResponse = (rawData: string) => {
Expand Down
4 changes: 3 additions & 1 deletion src/PirateMidiDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { WebSerialPort } from './serial/WebSerialPort';
import EventEmitter from 'events';
import { DevicePortMock } from '../test/mocks/DevicePortMock';

type DeviceTypes = keyof typeof deviceDescriptors;

export class PirateMidiDevice extends EventEmitter {
deviceInfo?: DeviceInfo;
baseDevice: BaseDevice;
Expand All @@ -25,7 +27,7 @@ export class PirateMidiDevice extends EventEmitter {
});
}

getDeviceDescription() {
getDeviceDescription(): typeof deviceDescriptors[DeviceTypes] {
if (!this.deviceInfo) throw new Error('No device info available');
return deviceDescriptors[this.deviceInfo.deviceModel];
}
Expand Down
19 changes: 9 additions & 10 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { getDevices } from '.';
import * as SerialPortModule from 'serialport';
import { PirateMidiDevice } from './PirateMidiDevice';
import * as DevicePortMockModule from '../test/mocks/DevicePortMock';
import { MockBinding } from '@serialport/binding-mock';
import { vi } from 'vitest';

jest.mock('serialport', () => {
const actual = jest.requireActual('serialport');
const { SerialPortMock } = actual;
vi.mock('./serial/NodeSerialPort', async () => {
const { DevicePortMock } = await vi.importActual<typeof DevicePortMockModule>(
'../test/mocks/DevicePortMock'
);
return {
...actual,
SerialPort: SerialPortMock,
} as typeof SerialPortModule;
NodeSerialPort: DevicePortMock,
};
});

jest.mock('./PirateMidiDevice');

describe('index', () => {
describe('getDevices', () => {
describe('no devices available', () => {
Expand All @@ -32,7 +31,7 @@ describe('index', () => {
productId: '5740',
});

const devices = await getDevices();
const devices = await getDevices({ binding: MockBinding });

expect(devices).toHaveLength(1);
expect(devices[0]).toBeInstanceOf(PirateMidiDevice);
Expand Down
37 changes: 35 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,50 @@ import { PirateMidiDevice } from './PirateMidiDevice';
import { NodeSerialPort } from './serial/NodeSerialPort';
import { GetDevices } from './types';

import { SerialPortStream } from '@serialport/stream';
import { autoDetect } from '@serialport/bindings-cpp';
import { BindingInterface } from '@serialport/bindings-interface';

export * from './types';
export { PirateMidiDevice } from './PirateMidiDevice';
export { ValidationError } from './ValidationError';

type Binding = BindingInterface;

/**
* Device manufacturer key, use to filter USB devices
*/
const MANUFACTURER = 'Pirate MIDI';

const getPorts = async (
binding: Binding = autoDetect()
): Promise<Array<NodeSerialPort>> => {
// TODO: error handling
const portsInfo = await binding.list();
return Promise.all(
portsInfo
.filter(({ manufacturer }) => manufacturer === MANUFACTURER)
.map(portInfo => {
const port = new SerialPortStream({
binding,
path: portInfo.path,
baudRate: 9600,
autoOpen: false,
});
return new NodeSerialPort(port);
})
);
};

/**
* Get any available Pirate Midi devices with device info set
* @param options.binding - On NodeJS optionally provide an OS binding for serialport (mostly useful for testing)
*/
export const getDevices: GetDevices = async () => {
export const getDevices: GetDevices = async ({ binding } = {}) => {
// TODO: error handling

const ports = await NodeSerialPort.list();
const ports = await getPorts(binding);

return Promise.all(
ports.map(async port => {
await port.connect();
Expand Down
29 changes: 3 additions & 26 deletions src/serial/NodeSerialPort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import EventEmitter from 'events';
*/
const DELIMITER = '~';

/**
* Device manufacturer key, use to filter USB devices
*/
const MANUFACTURER = 'Pirate MIDI';

/**
* Maintain node serial port for device
*/
Expand All @@ -31,29 +26,11 @@ export class NodeSerialPort extends EventEmitter {
this.#parser.on('data', data => this.emit('data', data));
}

static async list(): Promise<Array<NodeSerialPort>> {
// TODO: error handling
const portsInfo = await SerialPort.list();
return Promise.all(
portsInfo
.filter(({ manufacturer }) => manufacturer === MANUFACTURER)
.map(portInfo => {
const port = new SerialPort({
path: portInfo.path,
baudRate: 9600,
autoOpen: false,
});
return new NodeSerialPort(port);
})
);
}

async connect(): Promise<void> {
// Auto open doesn't wait so manually open
const error = await new Promise(resolve => {
this.#port.open(resolve);
// Auto open is async so instead we'll manually open it so we can wait for it
return new Promise<void>((resolve, reject) => {
this.#port.open(error => (error ? reject(error) : resolve()));
});
if (error) throw error;
}

write(data: string): void {
Expand Down
9 changes: 8 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { PirateMidiDevice } from '../PirateMidiDevice';
import { BindingInterface } from '@serialport/bindings-interface';

export * from './Commands';
export * from './DeviceInfo';
export * from './GlobalSettings';
export * from './BankSettings';

interface Options {
binding?: BindingInterface;
}

/**
* Default export
*/
export type GetDevices = () => Promise<Array<PirateMidiDevice>>;
export type GetDevices = (
options?: Options
) => Promise<Array<PirateMidiDevice>>;
4 changes: 4 additions & 0 deletions test/mocks/DevicePortMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class DevicePortMock extends EventEmitter {
this.emit('data', response);
};

connect(): Promise<void> {
return new Promise(resolve => setTimeout(resolve));
}

write(buffer: Buffer): boolean {
const rawData = Buffer.from(buffer).toString();

Expand Down
102 changes: 55 additions & 47 deletions test/mocks/device.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createDevice } from './device';
* Collects responses and check them once the expected amount is reached
* Caveats: does not check for superfluous responses, will hang when responses are missing
*/
const assertResponses = (expected: string[], done: () => void) => {
const assertResponses = (expected: string[], done: (_?: any) => void) => {
const responses: string[] = [];
return (response: string) => {
responses.push(response);
Expand All @@ -21,51 +21,55 @@ const assertResponses = (expected: string[], done: () => void) => {

describe('deviceMachine', () => {
describe('CHCK', () => {
it('should return deviceInfo immediately', done => {
const device = createDevice({
onResponse: assertResponses(['0,{"deviceName":"Bridge 6"}~'], done),
});

device.send('0,CHCK~');
});
it('should return deviceInfo immediately', () =>
new Promise(done => {
const device = createDevice({
onResponse: assertResponses(['0,{"deviceName":"Bridge 6"}~'], done),
});

device.send('0,CHCK~');
}));
});

describe('CTRL', () => {
it('should return ok for command and args', done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,ok~'], done),
});

device.send('0,CTRL~');
device.send('1,bankUp~');
});
it('should return ok for command and args', () =>
new Promise(done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,ok~'], done),
});

device.send('0,CTRL~');
device.send('1,bankUp~');
}));
});

describe('DREQ', () => {
it('should return ok and requested data', done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,{"currentBank":0}~'], done),
});

device.send('0,DREQ~');
device.send('1,globalSettings~');
});
it('should return ok and requested data', () =>
new Promise(done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,{"currentBank":0}~'], done),
});

device.send('0,DREQ~');
device.send('1,globalSettings~');
}));
});

describe('DTXR', () => {
it('should return ok for each step', done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,ok~', '2,ok~'], done),
});

device.send('0,DTXR~');
device.send('1,globalSettings~');
device.send(
`2,${JSON.stringify({
name: 'Bridge 8',
})}~`
);
});
it('should return ok for each step', () =>
new Promise(done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,ok~', '2,ok~'], done),
});

device.send('0,DTXR~');
device.send('1,globalSettings~');
device.send(
`2,${JSON.stringify({
name: 'Bridge 8',
})}~`
);
}));
});

describe('RSET', () => {
Expand All @@ -82,15 +86,19 @@ describe('deviceMachine', () => {
expect(device.state.context.response).toBeUndefined();
});

it('should behave as expected after reset', done => {
const device = createDevice({
onResponse: assertResponses(['0,ok~', '1,ok~', '2,ok~', '3,ok~'], done),
});

device.send('0,DTXR~');
device.send('1,RSET~');
device.send('2,CTRL~');
device.send('3,bankUp~');
});
it('should behave as expected after reset', () =>
new Promise(done => {
const device = createDevice({
onResponse: assertResponses(
['0,ok~', '1,ok~', '2,ok~', '3,ok~'],
done
),
});

device.send('0,DTXR~');
device.send('1,RSET~');
device.send('2,CTRL~');
device.send('3,bankUp~');
}));
});
});
7 changes: 7 additions & 0 deletions test/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
},
});
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"types": [
"node",
"w3c-web-serial",
"jest"
"vitest/globals"
]
},
"include": ["src/**/*.ts", "scripts/**/*.ts"],
"include": ["src/**/*.ts", "scripts/**/*.ts", "test/**/*.ts"],
"exclude": ["lib"]
}

0 comments on commit e0b3a10

Please sign in to comment.