Skip to content

Commit

Permalink
write to the card before printing, reset on card remove, update e2e t…
Browse files Browse the repository at this point in the history
…ests to use card mock
  • Loading branch information
benadida committed May 23, 2019
1 parent 46f50f9 commit 77a34eb
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 18 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
]
},
"dependencies": {
"@types/fetch-mock": "^7.3.0",
"@types/jest": "24.0.13",
"@types/lodash.camelcase": "^4.3.6",
"@types/mousetrap": "^1.6.2",
Expand All @@ -87,6 +88,7 @@
"@types/react-modal": "^3.8.2",
"@types/react-router-dom": "^4.3.3",
"@types/styled-components": "^4.1.15",
"fetch-mock": "^7.3.3",
"history": "^4.9.0",
"http-proxy-middleware": "^0.19.1",
"lodash.camelcase": "^4.3.0",
Expand All @@ -103,7 +105,8 @@
"react-scripts": "3.0.1",
"react-simple-keyboard": "^1.22.2",
"styled-components": "^4.2.0",
"typescript": "3.4.5"
"typescript": "3.4.5",
"wait-for-expect": "^1.2.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^1.9.0",
Expand Down
45 changes: 43 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export class App extends React.Component<RouteComponentProps, State> {
return
}

// better UI at some point
// don't reuse a card that has been written
if (voterCardData.uz) {
return
}

const ballotStyle = this.state.election.ballotStyles.find(
bs => voterCardData.bs === bs.id
)
Expand Down Expand Up @@ -118,7 +124,10 @@ export class App extends React.Component<RouteComponentProps, State> {
) {
this.setState({ loadingElection: true })
this.fetchElection().then(election => {
this.setElection(JSON.parse(election.longValue))
// setTimeout to prevent tests from going into infinite loops
window.setTimeout(() => {
this.setElection(JSON.parse(election.longValue))
}, 0)
})
}
break
Expand All @@ -134,6 +143,13 @@ export class App extends React.Component<RouteComponentProps, State> {
fetch('/card/read')
.then(result => result.json())
.then(resultJSON => {
// card was just taken out
const { ballotStyleId } = this.getBallotActivation()
if (!resultJSON.present && ballotStyleId) {
this.resetBallot()
return
}

if (resultJSON.shortValue) {
const cardData = JSON.parse(resultJSON.shortValue) as CardData
this.processCardData({
Expand All @@ -146,13 +162,37 @@ export class App extends React.Component<RouteComponentProps, State> {
// if it's an error, aggressively assume there's no backend and stop hammering
this.stopPolling()
})
}, 1000)
}, 200)
}

public stopPolling = () => {
window.clearInterval(checkCardInterval)
}

public markVoterCardUsed = async () => {
const { ballotStyleId, precinctId } = this.getBallotActivation()

const newCardData: VoterCardData = {
bs: ballotStyleId,
pr: precinctId,
t: 'voter',
uz: new Date().getTime(),
}

const newCardDataSerialized = JSON.stringify(newCardData)

await fetch('/card/write', {
method: 'post',
body: newCardDataSerialized,
headers: { 'Content-Type': 'application/json' },
})

const readCheck = await fetch('/card/read')
const readCheckObj = await readCheck.json()

return readCheckObj.shortValue === newCardDataSerialized
}

public componentDidMount = () => {
if (window.location.hash === '#sample') {
this.setState({
Expand Down Expand Up @@ -327,6 +367,7 @@ export class App extends React.Component<RouteComponentProps, State> {
ballotStyleId: this.state.ballotStyleId,
contests: this.state.contests,
election,
markVoterCardUsed: this.markVoterCardUsed,
precinctId: this.state.precinctId,
resetBallot: this.resetBallot,
setUserSettings: this.setUserSettings,
Expand Down
5 changes: 2 additions & 3 deletions src/AppCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ const election = electionSample as Election

beforeEach(() => {
window.localStorage.clear()
window.location.href = '/'
})

it(`App fetches the card data every 1 second`, () => {
it(`App fetches the card data every 200 ms`, () => {
fetchMock.resetMocks()
jest.useFakeTimers()

Expand All @@ -46,7 +45,7 @@ it(`App fetches the card data every 1 second`, () => {

expect(window.setInterval).toHaveBeenCalledTimes(1)

jest.advanceTimersByTime(3000)
jest.advanceTimersByTime(600)

expect(fetchMock.mock.calls.length).toEqual(3)
expect(fetchMock.mock.calls).toEqual([
Expand Down
111 changes: 101 additions & 10 deletions src/AppEndToEnd.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react'
import { fireEvent, render } from 'react-testing-library'
import fetchMock from 'fetch-mock'

import waitForExpect from 'wait-for-expect'
import electionSample from './data/electionSample.json'

import App, { electionKey, mergeWithDefaults } from './App'
import App, { mergeWithDefaults } from './App'
import { CandidateContest, Election, YesNoContest } from './config/types'

const electionSampleAsString = JSON.stringify(
Expand All @@ -28,7 +30,66 @@ beforeEach(() => {
window.location.href = '/'
})

async function sleep(milliseconds: number) {
return new Promise(resolve => {
window.setTimeout(resolve, milliseconds)
})
}

const cardValueVoter = {
present: true,
shortValue: JSON.stringify({
t: 'voter',
pr: '23',
bs: '12',
}),
}

const cardValueVoterUsed = {
present: true,
shortValue: JSON.stringify({
t: 'voter',
pr: '23',
bs: '12',
uz: new Date().getTime(),
}),
}

const cardValueAbsent = {
present: false,
shortValue: '',
}

const cardValueClerk = {
longValueExists: true,
present: true,
shortValue: JSON.stringify({
t: 'clerk',
h: 'abcd',
}),
}

it(`basic end-to-end flow`, async () => {
let cardFunctionsAsExpected = true
let currentCardValue = cardValueAbsent

fetchMock.get('/card/read', () => {
return JSON.stringify(currentCardValue)
})

fetchMock.get('/card/read_long', () => {
return JSON.stringify({ longValue: electionSampleAsString })
})

fetchMock.post('/card/write', (url, options) => {
// if we want to simulate a card that is malfunctioning,
// we don't accept the write
if (cardFunctionsAsExpected) {
currentCardValue = { present: true, shortValue: options.body as string }
}
return ''
})

const eventListenerCallbacksDictionary: any = {} // eslint-disable-line @typescript-eslint/no-explicit-any
window.addEventListener = jest.fn((event, cb) => {
eventListenerCallbacksDictionary[event] = cb
Expand All @@ -37,23 +98,42 @@ it(`basic end-to-end flow`, async () => {
eventListenerCallbacksDictionary.afterprint()
})

window.localStorage.setItem(electionKey, electionSampleAsString)
const { container, getByText, getByTestId, queryByText } = render(<App />)
fireEvent.change(getByTestId('activation-code'), {
target: {
value: 'VX.23.12',
},
})

// TODO: replace next line with "Enter" keyDown on activation code input
fireEvent.click(getByText('Submit'))
// first the clerk card
currentCardValue = cardValueClerk
await sleep(250)

getByText('Scan Your Activation Code')

// first a voter card that's already been used
currentCardValue = cardValueVoterUsed
await sleep(250)
getByText('Scan Your Activation Code')

// then the voter card that is good to go
currentCardValue = cardValueVoter
await sleep(250)

// Get Started Page
expect(container.firstChild).toMatchSnapshot()

// Go to First Contest
fireEvent.click(getByText('Get Started'))

// take out card, should reset
currentCardValue = cardValueAbsent
await sleep(250)

getByTestId('activation-code')

// ok put the card back in
currentCardValue = cardValueVoter
await sleep(250)

// Go to First Contest
fireEvent.click(getByText('Get Started'))

// Vote for President contest
expect(container.firstChild).toMatchSnapshot()
fireEvent.click(
Expand Down Expand Up @@ -113,8 +193,19 @@ it(`basic end-to-end flow`, async () => {
fireEvent.click(getByText('Print Ballot'))
fireEvent.click(getByText('No, go back.'))
fireEvent.click(getByText('Print Ballot'))

// card malfunctions, we should not advance
cardFunctionsAsExpected = false
fireEvent.click(getByText('Yes, print my ballot.'))
await waitForExpect(() => {
expect(window.print).not.toBeCalled()
})

cardFunctionsAsExpected = true
fireEvent.click(getByText('Yes, print my ballot.'))
expect(window.print).toBeCalled()
await waitForExpect(() => {
expect(window.print).toBeCalled()
})

// Review and Cast Instructions
getByText('Verify and Cast Your Ballot')
Expand Down
5 changes: 5 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export interface Dictionary<T> {
[key: string]: T | undefined
}

// AsyncFunction
export type AsyncFunction<O> = () => Promise<O>

// Events
export type InputEvent = React.FormEvent<EventTarget>
export type ButtonEvent = React.MouseEvent<HTMLButtonElement>
Expand Down Expand Up @@ -101,6 +104,7 @@ export type UpdateVoteFunction = (contestId: string, vote: OptionalVote) => void
export interface BallotContextInterface {
contests: Contests
readonly election: Election | undefined
markVoterCardUsed: AsyncFunction<boolean>
resetBallot: (path?: string) => void
activateBallot: (activationData: ActivationData) => void
updateVote: UpdateVoteFunction
Expand All @@ -120,6 +124,7 @@ export interface VoterCardData extends CardData {
readonly t: 'voter'
readonly bs: string
readonly pr: string
readonly uz?: number
}
export interface PollworkerCardData extends CardData {
readonly t: 'pollworker'
Expand Down
1 change: 1 addition & 0 deletions src/contexts/ballotContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const ballot: BallotContextInterface = {
ballotStyleId: '',
contests: [],
election: undefined,
markVoterCardUsed: async () => false,
precinctId: '',
resetBallot: () => undefined,
setUserSettings: () => undefined,
Expand Down
14 changes: 13 additions & 1 deletion src/pages/PrintPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ class SummaryPage extends React.Component<RouteComponentProps, State> {
public showConfirm = () => {
this.setState({ showConfirmModal: true })
}
public print = () => {
this.context.markVoterCardUsed().then((success: boolean) => {
if (success) {
window.print()
}
})
}
public render() {
const {
ballotStyleId,
Expand Down Expand Up @@ -305,7 +312,12 @@ class SummaryPage extends React.Component<RouteComponentProps, State> {
}
actions={
<>
<Button primary onClick={window.print}>
<Button
primary
onClick={() => {
this.print()
}}
>
Yes, print my ballot.
</Button>
<Button onClick={this.hideConfirm}>No, go back.</Button>
Expand Down
2 changes: 2 additions & 0 deletions test/testUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function render(
activateBallot = jest.fn(),
ballotStyleId = '',
contests = electionSample.contests as Contests,
markVoterCardUsed = jest.fn(),
election = electionSample,
history = createMemoryHistory({ initialEntries: [route] }),
precinctId = '',
Expand All @@ -37,6 +38,7 @@ export function render(
ballotStyleId,
contests,
election: mergeWithDefaults(election as Election),
markVoterCardUsed,
precinctId,
resetBallot,
setUserSettings,
Expand Down
Loading

0 comments on commit 77a34eb

Please sign in to comment.