Skip to content

Commit

Permalink
CHAL-30 #DONE Voter registration form (#30)
Browse files Browse the repository at this point in the history
* added register route

* added checkbox component and story

* added voter registration form and pages

* added phone input

* fixed select menu page overflow issue

* refined eligibility page

* added tests for phone input

* refined names form

* cleaned up addresses form

* added tests for ZipCodeValidator, PhoneValidator, and useRedirectToFirstIncompletePage. Cleaned up modal tests.

* fixed overflow issue on addresses page due to select component input width

* fixed typos, added error alert and loading wheel to other details page, forms focus on first nonvalid input when submitted while invalid

* added persistent fields, rendered voter registration form pages on the the client

* installed latest version of fully-formed and audited npm packages

* added address confirmation components and stories

* added hook to prefetch other details page when home state and zip change

* implemented address confirmation modal

* added address validation service

* added types for google address validation, updated components and stories, added validateAddresses client util

* fixed false positive returned by shouldCreateReviewRecommendedAddressError

* replaced google maps skd with calls to http api as the sdk uses eval internally

* added function to apply caution validity to address form fields

* added warning icons to input elements

* added hook to check if any form elements are invalid

* improved aria

* fixed broken tests, added test for validateAddressesWithGoogleMaps

* added tests for validateAddressesWithGoogleMaps

* added more tests

* added tests for addresses page

* added focus functionality back to select component, fixed missing act() warnings, added tests for usePrefetch

* added more tests for the combobox element

* tested warning icon display in select component

* test coverage outside of register route back up to 100%

* added tests for names form

* added tests for eligibility

* added test for VoterRegistrationForm

* completed tests for src/app/register/eligibility

* added tests for other details page

* added tests for addresses utils

* addeed tests for usePrefetchOtherDetailsWithStateAndZip

* added tests for address forms

* added tests for address forms

* added tests for FormattedAddress

* added final tests, formatted, linted

* small UI updates

* exported PhoneInputGroup from index.tsx

* removed superfluous files

* removed superfluous files

* fixed type errors, linted, and formatted
  • Loading branch information
dvorakjt committed Sep 19, 2024
1 parent 82dd0bb commit 3243b0a
Show file tree
Hide file tree
Showing 264 changed files with 13,468 additions and 259 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ NEXT_PUBLIC_TURNSTILE_SITE_KEY=cloudflare_turnstile_site_key_goes_here
TURNSTILE_SECRET_KEY=cloudflare_turnstile_secret_key_goes_here
NEXT_PUBLIC_SUPABASE_URL=supabase_api_url_goes_here
NEXT_PUBLIC_SUPABASE_ANON_KEY=supabase_anon_key_goes_here
SUPABASE_SERVICE_ROLE_KEY=supabase_service_role_key_goes_here
SUPABASE_SERVICE_ROLE_KEY=supabase_service_role_key_goes_here
GOOGLE_MAPS_API_KEY=google_maps_api_key_here
VOTER_REGISTRATION_REPO_ENCRYPTION_KEY="result of npm run create-cryptokey"
1 change: 1 addition & 0 deletions .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
NEXT_PUBLIC_SUPABASE_URL: http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SUPABASE_SERVICE_ROLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
GOOGLE_MAPS_API_KEY: ''
VOTER_REGISTRATION_REPO_ENCRYPTION_KEY: 'DvViBWSQfwFGSetPOVbIWZrMXYJh4wTVSE/+1QI/VTI='
steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 7 additions & 1 deletion jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ const config = {
'<rootDir>/src/constants/',
'fonts',
'<rootDir>/src/model/',
'user-context/user-context-provider.tsx',
'<rootDir>/src/app/register/progress-bar',
// ignore async server components as the test environment doesn't support rendering them at this time
'<rootDir>/src/contexts/user-context/user-context-provider.tsx',
'<rootDir>/src/app/register/addresses/page.tsx',
'<rootDir>/src/app/register/eligibility/page.tsx',
'<rootDir>/src/app/register/names/page.tsx',
'<rootDir>/src/app/register/other-details/page.tsx',
],
//require 100% code coverage for the tests to pass
coverageThreshold: {
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@paralleldrive/cuid2": "^2.2.2",
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.44.0",
"fully-formed": "^1.0.1",
"fully-formed": "^1.2.1",
"luxon": "^3.4.4",
"next": "^14.2.4",
"react": "^18.3.1",
Expand Down
4 changes: 4 additions & 0 deletions public/static/images/components/checkbox/checkmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/static/images/components/shared/warning-icon-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/static/images/components/shared/warning-icon-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
271 changes: 271 additions & 0 deletions src/__tests__/unit/app/api/validate-addresses/route.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { POST } from '@/app/api/validate-addresses/route';
import { serverContainer } from '@/services/server/container';
import { SERVER_SERVICE_KEYS } from '@/services/server/keys';
import { saveActualImplementation } from '@/utils/test/save-actual-implementation';
import { NextRequest } from 'next/server';
import { AddressErrorTypes } from '@/model/types/addresses/address-error-types';
import { ServerError } from '@/errors/server-error';
import type { ValidateAddresses } from '@/services/server/validate-addresses/validate-addresses';
import type { AddressErrors } from '@/model/types/addresses/address-errors';

describe('POST', () => {
const getActualService = saveActualImplementation(serverContainer, 'get');

it(`returns a response with a result.errors property containing an empty array
if all addresses were valid.`, async () => {
const containerSpy = jest
.spyOn(serverContainer, 'get')
.mockImplementation(key => {
if (key.name === SERVER_SERVICE_KEYS.validateAddresses.name) {
const validateAddresses: ValidateAddresses = () => {
return Promise.resolve([] as AddressErrors[]);
};

return validateAddresses;
}

return getActualService(key);
});

const request = new NextRequest(
'https://challenge.8by8.us/api/validate-addresses',
{
method: 'POST',
body: JSON.stringify({
homeAddress: {
streetLine1: '1600 Amphitheatre Pkwy',
city: 'Mountain View',
state: 'CA',
zip: '94043',
},
}),
},
);

const response = await POST(request);
const body = await response.json();
expect(body).toEqual({
result: {
errors: [],
},
});

containerSpy.mockRestore();
});

it(`returns a response with a result.errors property containing errors if any
were detected.`, async () => {
const addresses = {
homeAddress: {
streetLine1: '2930 Pearl Street',
streetLine2: 'Suite 100',
city: 'Boulder',
state: 'CO',
zip: '80301',
},
mailingAddress: {
streetLine1: '1600 Amphitheatre Pkwy',
city: 'Montan View',
state: 'CA',
zip: '94043',
},
previousAddress: {
streetLine1: '500 W 2nd St',
city: 'Austin',
state: 'TX',
zip: '78701',
},
};

const errors: AddressErrors[] = [
{
type: AddressErrorTypes.UnconfirmedComponents,
form: 'homeAddress',
unconfirmedAddressComponents: {
streetLine1: {
value: addresses.homeAddress.streetLine1,
hasIssue: false,
},
streetLine2: {
value: addresses.homeAddress.streetLine2,
hasIssue: true,
},
city: {
value: addresses.homeAddress.city,
hasIssue: false,
},
state: {
value: addresses.homeAddress.state,
hasIssue: false,
},
zip: {
value: addresses.homeAddress.zip,
hasIssue: false,
},
},
},
{
type: AddressErrorTypes.ReviewRecommendedAddress,
form: 'mailingAddress',
enteredAddress: {
streetLine1: {
value: addresses.mailingAddress.streetLine1,
hasIssue: false,
},
city: {
value: addresses.mailingAddress.city,
hasIssue: true,
},
state: {
value: addresses.mailingAddress.state,
hasIssue: false,
},
zip: {
value: addresses.mailingAddress.zip,
hasIssue: false,
},
},
recommendedAddress: {
streetLine1: {
value: addresses.mailingAddress.streetLine1,
hasIssue: false,
},
city: {
value: addresses.mailingAddress.city,
hasIssue: true,
},
state: {
value: addresses.mailingAddress.state,
hasIssue: false,
},
zip: {
value: addresses.mailingAddress.zip,
hasIssue: false,
},
},
},
{
type: AddressErrorTypes.MissingSubpremise,
form: 'previousAddress',
},
];

const containerSpy = jest
.spyOn(serverContainer, 'get')
.mockImplementation(key => {
if (key.name === SERVER_SERVICE_KEYS.validateAddresses.name) {
const validateAddresses: ValidateAddresses = () => {
return Promise.resolve(errors);
};

return validateAddresses;
}

return getActualService(key);
});

const request = new NextRequest(
'https://challenge.8by8.us/api/validate-addresses',
{
method: 'POST',
body: JSON.stringify(addresses),
},
);

const response = await POST(request);
const body = await response.json();
expect(body).toEqual({
result: {
errors,
},
});

containerSpy.mockRestore();
});

it(`returns a response with a status of 400 if the data sent in the body
could not be parsed.`, async () => {
const request = new NextRequest(
'https://challenge.8by8.us/api/validate-addresses',
{
method: 'POST',
body: JSON.stringify({}),
},
);

const response = await POST(request);
expect(response.status).toBe(400);
});

it('returns a response with a status matching that of a caught ServerError.', async () => {
const containerSpy = jest
.spyOn(serverContainer, 'get')
.mockImplementation(key => {
if (key.name === SERVER_SERVICE_KEYS.validateAddresses.name) {
const validateAddresses: ValidateAddresses = () => {
throw new ServerError('Too many requests.', 429);
};

return validateAddresses;
}

return getActualService(key);
});

const request = new NextRequest(
'https://challenge.8by8.us/api/validate-addresses',
{
method: 'POST',
body: JSON.stringify({
homeAddress: {
streetLine1: '1600 Amphitheatre Pkwy',
city: 'Mountain View',
state: 'CA',
zip: '94043',
},
}),
},
);

const response = await POST(request);
expect(response.status).toBe(429);

containerSpy.mockRestore();
});

it(`returns a response with a status of 500 when an unknown error is encountered.`, async () => {
const containerSpy = jest
.spyOn(serverContainer, 'get')
.mockImplementation(key => {
if (key.name === SERVER_SERVICE_KEYS.validateAddresses.name) {
const validateAddresses: ValidateAddresses = () => {
throw new Error();
};

return validateAddresses;
}

return getActualService(key);
});

const request = new NextRequest(
'https://challenge.8by8.us/api/validate-addresses',
{
method: 'POST',
body: JSON.stringify({
homeAddress: {
streetLine1: '1600 Amphitheatre Pkwy',
city: 'Mountain View',
state: 'CA',
zip: '94043',
},
}),
},
);

const response = await POST(request);
expect(response.status).toBe(500);

containerSpy.mockRestore();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ exports[`Progress renders progress page unchanged 1`] = `
Not registered to vote yet?
<br />
<a
href="/voterreg"
href="/register/eligibility"
>
Register now
</a>
and earn a badge!
and earn a badge!
</p>
</div>
</section>
Expand Down Expand Up @@ -355,11 +356,12 @@ exports[`Progress renders progress page unchanged 1`] = `
Not registered to vote yet?
<br />
<a
href="/voterreg"
href="/register/eligibility"
>
Register now
</a>
and earn a badge!
and earn a badge!
</p>
</div>
</section>
Expand All @@ -375,6 +377,7 @@ exports[`Progress renders progress page unchanged 1`] = `
<button
aria-label="close dialog"
class="close_btn"
type="button"
>
<svg
class="close_btn_icon"
Expand Down
Loading

0 comments on commit 3243b0a

Please sign in to comment.