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

chore: migrate to vitest #4

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
27,551 changes: 11,356 additions & 16,195 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 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,22 +85,21 @@
"@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",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"husky": "^6.0.0",
"jest": "^27.2.0",
"lint-staged": "^10.5.4",
"node-fetch": "^2.6.7",
"prettier": "^2.2.1",
"rollup": "^2.76.0",
"rollup-plugin-polyfill-node": "^0.10.1",
"semantic-release": "^19.0.2",
"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 +117,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 @@ -13,9 +13,9 @@
"types": [
"node",
"w3c-web-serial",
"jest"
"vitest/globals"
]
},
"include": ["src/**/*.ts", "scripts/**/*.ts"],
"include": ["src/**/*.ts", "scripts/**/*.ts", "test/**/*.ts"],
"exclude": ["lib"]
}