Skip to content

Commit

Permalink
Dev Dock Printer (#4591)
Browse files Browse the repository at this point in the history
  • Loading branch information
adghayes authored Feb 12, 2024
1 parent 7449ae3 commit f5dab6b
Show file tree
Hide file tree
Showing 17 changed files with 297 additions and 25 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ REACT_APP_VX_ENABLE_DEV_DOCK=TRUE
REACT_APP_VX_SKIP_PIN_ENTRY=FALSE
REACT_APP_VX_USE_MOCK_CARDS=FALSE
REACT_APP_VX_USE_MOCK_USB_DRIVE=FALSE
REACT_APP_VX_USE_MOCK_PRINTER=FALSE
REACT_APP_VX_SKIP_CVR_ELECTION_HASH_CHECK=FALSE
REACT_APP_VX_SKIP_SCAN_ELECTION_HASH_CHECK=FALSE
REACT_APP_VX_SKIP_ELECTION_PACKAGE_AUTHENTICATION=FALSE
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,6 @@ export function buildApp({
const app: Application = express();
const api = buildApi({ auth, workspace, logger, usbDrive, printer });
app.use('/api', grout.buildRouter(api, express));
useDevDockRouter(app, express);
useDevDockRouter(app, express, 'admin');
return app;
}
2 changes: 1 addition & 1 deletion apps/central-scan/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export function buildCentralScannerApp({
importer,
});
app.use('/api', grout.buildRouter(api, express));
useDevDockRouter(app, express);
useDevDockRouter(app, express, 'central-scan');

const deprecatedApiRouter = express.Router();
deprecatedApiRouter.use(express.raw());
Expand Down
2 changes: 1 addition & 1 deletion apps/mark-scan/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,6 @@ export function buildApp(
const app: Application = express();
const api = buildApi(auth, usbDrive, logger, workspace, stateMachine);
app.use('/api', grout.buildRouter(api, express));
useDevDockRouter(app, express);
useDevDockRouter(app, express, 'mark-scan');
return app;
}
2 changes: 1 addition & 1 deletion apps/mark/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,6 @@ export function buildApp(
const app: Application = express();
const api = buildApi(auth, usbDrive, logger, workspace);
app.use('/api', grout.buildRouter(api, express));
useDevDockRouter(app, express);
useDevDockRouter(app, express, 'mark');
return app;
}
2 changes: 1 addition & 1 deletion apps/scan/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,6 @@ export function buildApp({
const app: Application = express();
const api = buildApi({ auth, machine, workspace, usbDrive, printer, logger });
app.use('/api', grout.buildRouter(api, express));
useDevDockRouter(app, express);
useDevDockRouter(app, express, 'scan');
return app;
}
1 change: 1 addition & 0 deletions libs/dev-dock/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@votingworks/auth": "workspace:*",
"@votingworks/basics": "workspace:*",
"@votingworks/grout": "workspace:*",
"@votingworks/printing": "workspace:*",
"@votingworks/types": "workspace:*",
"@votingworks/usb-drive": "workspace:*",
"@votingworks/utils": "workspace:*"
Expand Down
50 changes: 47 additions & 3 deletions libs/dev-dock/backend/src/dev_dock_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ import {
electionGeneral,
} from '@votingworks/fixtures';
import { Server } from 'http';
import { Api, useDevDockRouter } from './dev_dock_api';
import { typedAs } from '@votingworks/basics';
import { PrinterStatus } from '@votingworks/printing';
import {
Api,
MachineType,
DEFAULT_PRINTERS,
useDevDockRouter,
} from './dev_dock_api';

const TEST_DEV_DOCK_FILE_PATH = '/tmp/dev-dock.test.json';

Expand All @@ -33,12 +40,12 @@ jest.mock('@votingworks/utils', () => {

let server: Server;

function setup() {
function setup(machineType: MachineType = 'scan') {
if (fs.existsSync(TEST_DEV_DOCK_FILE_PATH)) {
fs.unlinkSync(TEST_DEV_DOCK_FILE_PATH);
}
const app = express();
useDevDockRouter(app, express, TEST_DEV_DOCK_FILE_PATH);
useDevDockRouter(app, express, machineType, TEST_DEV_DOCK_FILE_PATH);
server = app.listen();
const { port } = server.address() as AddressInfo;
const baseUrl = `http://localhost:${port}/dock`;
Expand Down Expand Up @@ -163,3 +170,40 @@ test('usb drive mock endpoints', async () => {
await apiClient.clearUsbDrive();
await expect(apiClient.getUsbDriveStatus()).resolves.toEqual('removed');
});

test('machine type', async () => {
const { apiClient: apiClientMark } = setup('mark');
expect(await apiClientMark.getMachineType()).toEqual('mark');

server.close();

const { apiClient: apiClientScan } = setup('scan');
expect(await apiClientScan.getMachineType()).toEqual('scan');
});

test.each(typedAs<MachineType[]>(['admin', 'scan']))(
'printer for machine type %s',
async (machineType) => {
const { apiClient } = setup(machineType);
await expect(apiClient.getPrinterStatus()).resolves.toEqual(
typedAs<PrinterStatus>({
connected: false,
})
);

await apiClient.connectPrinter();
await expect(apiClient.getPrinterStatus()).resolves.toEqual(
typedAs<PrinterStatus>({
connected: true,
config: DEFAULT_PRINTERS[machineType]!,
})
);

await apiClient.disconnectPrinter();
await expect(apiClient.getPrinterStatus()).resolves.toEqual(
typedAs<PrinterStatus>({
connected: false,
})
);
}
);
46 changes: 44 additions & 2 deletions libs/dev-dock/backend/src/dev_dock_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import {
BooleanEnvironmentVariableName,
} from '@votingworks/utils';
import { getMockFileUsbDriveHandler } from '@votingworks/usb-drive';
import {
BROTHER_THERMAL_PRINTER_CONFIG,
HP_LASER_PRINTER_CONFIG,
PrinterConfig,
PrinterStatus,
getMockFilePrinterHandler,
} from '@votingworks/printing';
import { execFile } from './utils';

export type DevDockUserRole = Exclude<UserRole, 'cardless_voter'>;
Expand All @@ -22,6 +29,21 @@ export interface DevDockElectionInfo {
path: string;
}

export type MachineType =
| 'mark'
| 'mark-scan'
| 'scan'
| 'central-scan'
| 'admin';

export const DEFAULT_PRINTERS: Record<MachineType, Optional<PrinterConfig>> = {
admin: HP_LASER_PRINTER_CONFIG,
mark: undefined, // not yet implemented
scan: BROTHER_THERMAL_PRINTER_CONFIG,
'mark-scan': undefined,
'central-scan': undefined,
};

// Convert paths relative to the VxSuite root to absolute paths
function electionPathToAbsolute(path: string) {
return isAbsolute(path)
Expand Down Expand Up @@ -53,8 +75,9 @@ function readDevDockFileContents(devDockFilePath: string): DevDockFileContents {
) as DevDockFileContents;
}

function buildApi(devDockFilePath: string) {
function buildApi(devDockFilePath: string, machineType: MachineType) {
const usbHandler = getMockFileUsbDriveHandler();
const printerHandler = getMockFilePrinterHandler();

return grout.createApi({
setElection(input: { path: string }): void {
Expand Down Expand Up @@ -113,6 +136,24 @@ function buildApi(devDockFilePath: string) {
clearUsbDrive(): void {
usbHandler.clearData();
},

getPrinterStatus(): PrinterStatus {
return printerHandler.getPrinterStatus();
},

connectPrinter(): void {
const config = DEFAULT_PRINTERS[machineType];
assert(config);
printerHandler.connectPrinter(config);
},

disconnectPrinter(): void {
printerHandler.disconnectPrinter();
},

getMachineType(): MachineType {
return machineType;
},
});
}

Expand All @@ -127,6 +168,7 @@ export type Api = ReturnType<typeof buildApi>;
export function useDevDockRouter(
app: Express.Application,
express: typeof Express,
machineType: MachineType,
/* istanbul ignore next */
devDockFilePath: string = DEV_DOCK_FILE_PATH
): void {
Expand All @@ -139,7 +181,7 @@ export function useDevDockRouter(
fs.writeFileSync(devDockFilePath, '{}');
}

const api = buildApi(devDockFilePath);
const api = buildApi(devDockFilePath, machineType);

// Set a default election if one is not already set
if (!api.getElection()) {
Expand Down
1 change: 1 addition & 0 deletions libs/dev-dock/backend/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{ "path": "../../eslint-plugin-vx/tsconfig.build.json" },
{ "path": "../../fixtures/tsconfig.build.json" },
{ "path": "../../grout/tsconfig.build.json" },
{ "path": "../../printing/tsconfig.build.json" },
{ "path": "../../test-utils/tsconfig.build.json" },
{ "path": "../../types/tsconfig.build.json" },
{ "path": "../../usb-drive/tsconfig.build.json" },
Expand Down
1 change: 1 addition & 0 deletions libs/dev-dock/backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
{ "path": "../../eslint-plugin-vx/tsconfig.build.json" },
{ "path": "../../fixtures/tsconfig.build.json" },
{ "path": "../../grout/tsconfig.build.json" },
{ "path": "../../printing/tsconfig.build.json" },
{ "path": "../../test-utils/tsconfig.build.json" },
{ "path": "../../types/tsconfig.build.json" },
{ "path": "../../usb-drive/tsconfig.build.json" },
Expand Down
77 changes: 71 additions & 6 deletions libs/dev-dock/frontend/src/dev_dock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ let mockKiosk!: jest.Mocked<KioskBrowser.Kiosk>;

beforeEach(() => {
mockApiClient = createMockClient<Api>();
mockApiClient.getMachineType.expectCallWith().resolves('central-scan');
mockApiClient.getCardStatus.expectCallWith().resolves(noCardStatus);
mockApiClient.getUsbDriveStatus.expectCallWith().resolves('removed');
mockApiClient.getElection.expectCallWith().resolves({
Expand Down Expand Up @@ -82,6 +83,7 @@ test('renders nothing if dev dock is disabled', () => {
mockApiClient.getCardStatus.reset();
mockApiClient.getElection.reset();
mockApiClient.getUsbDriveStatus.reset();
mockApiClient.getMachineType.reset();
featureFlagMock.disableFeatureFlag(
BooleanEnvironmentVariableName.ENABLE_DEV_DOCK
);
Expand All @@ -93,7 +95,7 @@ test('card mock controls', async () => {
render(<DevDock apiClient={mockApiClient} />);

// Card controls should enable once status loads
const systemAdminControl = screen.getByRole('button', {
const systemAdminControl = await screen.findByRole('button', {
name: 'System Admin',
});
await waitFor(() => {
Expand Down Expand Up @@ -175,7 +177,7 @@ test('disabled card mock controls if card mocks are disabled', async () => {
);
render(<DevDock apiClient={mockApiClient} />);

screen.getByText('Smart card mocks disabled');
await screen.findByText('Smart card mocks disabled');
const systemAdminControl = screen.getByRole('button', {
name: 'System Admin',
});
Expand All @@ -196,7 +198,7 @@ test('disabled card mock controls if card mocks are disabled', async () => {

test('election selector', async () => {
render(<DevDock apiClient={mockApiClient} />);
const electionSelector = screen.getByRole('combobox');
const electionSelector = await screen.findByRole('combobox');
await waitFor(() => {
expect(electionSelector).toHaveValue(
'libs/fixtures/data/electionGeneral/election.json'
Expand Down Expand Up @@ -225,7 +227,7 @@ test('election selector', async () => {

test('USB drive controls', async () => {
render(<DevDock apiClient={mockApiClient} />);
const usbDriveControl = screen.getByRole('button', {
const usbDriveControl = await screen.findByRole('button', {
name: 'USB Drive',
});
await waitFor(() => expect(usbDriveControl).toBeEnabled());
Expand Down Expand Up @@ -257,7 +259,7 @@ test('disabled USB drive controls if USB drive mocks are disabled', async () =>
);
render(<DevDock apiClient={mockApiClient} />);

screen.getByText('USB mock disabled');
await screen.findByText('USB mock disabled');
const usbDriveControl = screen.getByRole('button', {
name: 'USB Drive',
});
Expand All @@ -274,7 +276,7 @@ test('disabled USB drive controls if USB drive mocks are disabled', async () =>

test('screenshot button', async () => {
render(<DevDock apiClient={mockApiClient} />);
const screenshotButton = screen.getByRole('button', {
const screenshotButton = await screen.findByRole('button', {
name: 'Capture Screenshot',
});

Expand All @@ -290,3 +292,66 @@ test('screenshot button', async () => {
expect(fileWriter.write).toHaveBeenCalledWith(screenshotBuffer);
});
});

test('printer mock control', async () => {
featureFlagMock.enableFeatureFlag(
BooleanEnvironmentVariableName.USE_MOCK_PRINTER
);

mockApiClient.getMachineType.reset();
mockApiClient.getMachineType.expectCallWith().resolves('admin');
mockApiClient.getPrinterStatus.expectCallWith().resolves({
connected: false,
});

render(<DevDock apiClient={mockApiClient} />);
const printerButton = await screen.findByRole('button', {
name: 'Printer',
});

mockApiClient.connectPrinter.expectCallWith().resolves();
mockApiClient.getPrinterStatus.expectCallWith().resolves({
connected: true,
config: {
label: 'mock',
vendorId: 0,
productId: 0,
baseDeviceUri: 'mock://',
ppd: 'mock.ppd',
},
});
userEvent.click(printerButton);
await waitFor(() => mockApiClient.assertComplete());

mockApiClient.disconnectPrinter.expectCallWith().resolves();
mockApiClient.getPrinterStatus.expectCallWith().resolves({
connected: false,
});
userEvent.click(printerButton);
await waitFor(() => mockApiClient.assertComplete());

featureFlagMock.disableFeatureFlag(
BooleanEnvironmentVariableName.USE_MOCK_PRINTER
);
});

test('printer mock when disabled', async () => {
featureFlagMock.disableFeatureFlag(
BooleanEnvironmentVariableName.USE_MOCK_PRINTER
);

mockApiClient.getMachineType.reset();
mockApiClient.getMachineType.expectCallWith().resolves('admin');
mockApiClient.getPrinterStatus.expectCallWith().resolves({
connected: false,
});

render(<DevDock apiClient={mockApiClient} />);
const printerButton = await screen.findByRole('button', {
name: 'Printer',
});

expect(printerButton).toBeDisabled();
screen.getByText('Printer mock disabled');
userEvent.click(printerButton);
});
Loading

0 comments on commit f5dab6b

Please sign in to comment.