Skip to content

Commit

Permalink
feat: Sites & Applications page (#220)
Browse files Browse the repository at this point in the history
* PageNav component

* Add abbreviated version of Logo

* Add Header component

* Style header

* Add component tests

* More tests

* Footer component

* Create layout for beta pages

* Make layout mobile friendly

* Move CSS fixes into beta layout

* Add Search, styling

* Fix Cypress test

* Working on S&A page

* Styling S&A

* Add content, styling

* Add tests

* Add Cypress spec for Sites and Applications feature

* Add mock Keystone API, fix test

* Open Bookmark links in new window

* Display external link icon on Bookmarks

* Remove external link icon

* Resolve linting errors

* Up test coverage
  • Loading branch information
Suzanne Rozier authored Sep 13, 2021
1 parent 85f4cad commit b0bee3c
Show file tree
Hide file tree
Showing 20 changed files with 197 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ node_modules/
out/

# Keystone output
.keystone
/.keystone

# Storybook build output
storybook-static
Expand Down
16 changes: 14 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,27 @@
"import/order": "warn",
"react/button-has-type": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@next/next/no-img-element": "off"
"@next/next/no-img-element": "off",
"jsx-a11y/anchor-is-valid": [
"error",
{
"components": ["LinkTo"],
"aspects": ["noHref", "invalidHref", "preferButton"]
}
],
"react/jsx-no-target-blank": ["error"]
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"typescript": {} // this loads <rootdir>/tsconfig.json to eslint
}
},
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
{ "name": "LinkTo", "attribute": "href" }
]
},
"overrides": [
{
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ node_modules/
out/

# Keystone output
.keystone
/.keystone

# Storybook build output
storybook-static
Expand Down
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ node_modules/
out/

# Keystone output
.keystone
/.keystone

# Storybook build output
storybook-static
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ The footer should also include `BREAKING CHANGE:` if the commit includes any bre
- We are currently enforcing a minimum Jest test coverage of 95% across the codebase.
- All Jest tests are run in Github CI and must pass before merging.
- Use [Cypress](https://www.cypress.io/) to write integration & end-to-end tests that can be run in a real browser. This allows testing certain browser behaviors that are not reproducible in jsdom.
- All Cypress tests are run in Github CI and must pass before merging. Currently, they are only run against the static site (since that is what is currently in production) at `localhost:5000`.
- All Cypress tests are run in Github CI and must pass before merging. Currently, they are only run against the static site (since that is what is currently in production) at `localhost:5000`. You can test Cypress against the static site on your local machine by running the following commands in order:
- `yarn build:static` (export the static site)
- `yarn start:static` (serve the static site)
- `yarn cypress:static` (start the Cypress runner against the static site)
- You can also run Cypress against the dev server with `yarn cypress:dev`. Just note that the dev server does _not_ match the same behavior as the static site, or even when it is running in production, so Cypress tests should always also be verified against what is going to be deployed to production.

## Releasing

Expand Down
Binary file modified cms.db
Binary file not shown.
14 changes: 14 additions & 0 deletions cypress/integration/sites-and-applications.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
describe('Sites and Applications', () => {
beforeEach(() => {
// Make sure the beta cookie is set
cy.visit('/joinbeta')
})

it('can visit the Sites & Applications page', () => {
// Client-side navigate to the page
cy.contains('All sites & applications').click()

cy.url().should('eq', Cypress.config().baseUrl + '/sites-and-applications')
cy.contains('Sites & Applications')
})
})
13 changes: 7 additions & 6 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ module.exports = {
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
transformIgnorePatterns: ['node_modules/(?!(.keystone)/)'],
moduleNameMapper: {
'\\.(scss|sass|css)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__mocks__/fileMock.js',
'__mocks__/(.*)': '<rootDir>/src/__mocks__/$1',
'components/(.*)': '<rootDir>/src/components/$1',
'hooks/(.*)': '<rootDir>/src/hooks/$1',
'layout/(.*)': '<rootDir>/src/layout/$1',
'pages/(.*)': '<rootDir>/src/pages/$1',
'stores/(.*)': '<rootDir>/src/stores/$1',
'^__mocks__/(.*)': '<rootDir>/src/__mocks__/$1',
'^components/(.*)': '<rootDir>/src/components/$1',
'^hooks/(.*)': '<rootDir>/src/hooks/$1',
'^layout/(.*)': '<rootDir>/src/layout/$1',
'^pages/(.*)': '<rootDir>/src/pages/$1',
'^stores/(.*)': '<rootDir>/src/stores/$1',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
coverageThreshold: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"test": "jest --runInBand --testTimeout=60000",
"test:watch": "jest --watch --runInBand --testTimeout=60000",
"cypress:dev": "cypress open",
"cypress:static": "CYPRESS_BASE_URL=http://localhost:5000 cypress open",
"postinstall": "sh ./scripts/copy_uswds_assets.sh && keystone-next postinstall",
"release": "standard-version -t ''"
},
Expand Down
5 changes: 5 additions & 0 deletions src/__mocks__/.keystone/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const lists = {
Collection: {
findMany: () => Promise.resolve([]),
},
}
61 changes: 61 additions & 0 deletions src/__tests__/pages/beta/sites-and-applications.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @jest-environment jsdom
*/
import { render, screen } from '@testing-library/react'
import SitesAndApplications, {
getStaticProps,
} from 'pages/beta/sites-and-applications'

const mockCollections = [
{
id: 1,
title: 'Example Collection 1',
bookmarks: [
{
id: 1,
url: 'www.example.com',
label: 'Example 1',
},
],
},
{
id: 2,
title: 'Example Collection 2',
bookmarks: [
{
id: 1,
url: 'www.example.com',
label: 'Example 1',
},
{
id: 2,
url: 'www.example2.com',
label: 'Example 2',
},
],
},
]

describe('Sites and Applications page', () => {
it('renders Sites & Applications content', () => {
render(<SitesAndApplications collections={mockCollections} />)

expect(
screen.getByRole('heading', { name: 'Sites & Applications' })
).toBeInTheDocument()

const collections = screen.getAllByRole('heading', { level: 3 })
expect(collections).toHaveLength(mockCollections.length)
collections.forEach((c, i) => {
// eslint-disable-next-line security/detect-object-injection
expect(collections[i]).toHaveTextContent(mockCollections[i].title)
})
})
})

describe('getStaticProps', () => {
it('returns expected props', async () => {
const results = await getStaticProps()
expect(results).toEqual({ props: { collections: [] } })
})
})
2 changes: 1 addition & 1 deletion src/components/Bookmark/Bookmark.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Bookmark component', () => {
render(<Bookmark href="/home">Home</Bookmark>)

const link = screen.getByRole('link', {
name: 'Home',
name: 'Home (opens in a new window)',
})

expect(link).toHaveAttribute('href', '/home')
Expand Down
9 changes: 8 additions & 1 deletion src/components/Bookmark/Bookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ const Bookmark = ({
children,
className,
onDelete,
href,
...linkProps
}: PropTypes) => {
const linkClasses = classnames('usa-link', className)

return (
<div className={styles.bookmark}>
<LinkTo {...linkProps} className={linkClasses}>
<LinkTo
{...linkProps}
href={href}
className={linkClasses}
rel="noreferrer noopener"
target="_blank">
{children}
<span className="usa-sr-only">(opens in a new window)</span>
</LinkTo>

{onDelete && (
Expand Down
1 change: 1 addition & 0 deletions src/components/Collection/Collection.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15);

padding: units(2) units(2) units(3);
margin-bottom: units(3);

h3 {
margin: 0;
Expand Down
3 changes: 3 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import Logo from 'components/Logo/Logo'
import LinkTo from 'components/util/LinkTo/LinkTo'

const Footer = () => {
// PROTOTYPE: Disabling this rule while this component is used for prototyping only!
/* eslint-disable jsx-a11y/anchor-is-valid */

return (
<USWDSFooter
className={styles.footer}
Expand Down
4 changes: 4 additions & 0 deletions src/layout/Beta/DefaultLayout/DefaultLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ const DefaultLayout = ({ children }: { children: React.ReactNode }) => {
}

export default DefaultLayout

export const withBetaLayout = (page: React.ReactNode) => (
<DefaultLayout>{page}</DefaultLayout>
)
2 changes: 1 addition & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import '@fortawesome/fontawesome-svg-core/styles.css'
import 'styles/index.scss'
import '../../public/vendor/fontawesome-pro-5.15.1-web/css/all.min.css'

import 'initIcons'
import '../initIcons'
import DefaultLayout from 'layout/MVP/DefaultLayout/DefaultLayout'
import { BetaContextProvider } from 'stores/betaContext'

Expand Down
57 changes: 57 additions & 0 deletions src/pages/beta/sites-and-applications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { InferGetStaticPropsType } from 'next'
import { Grid } from '@trussworks/react-uswds'

import { lists } from '.keystone/api'

import { withBetaLayout } from 'layout/Beta/DefaultLayout/DefaultLayout'
import Collection from 'components/Collection/Collection'
import Bookmark from 'components/Bookmark/Bookmark'
import styles from 'styles/pages/sitesAndApplications.module.scss'

type Bookmark = {
id: string
url: string
label?: string
description?: string
}

const SitesAndApplications = ({
collections,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
<h2 className={styles.pageTitle}>Sites &amp; Applications</h2>

<div className={styles.widgetContainer}>
<Grid row gap>
{collections.map((collection) => (
<Grid
key={`collection_${collection.id}`}
tablet={{ col: 6 }}
desktop={{ col: 3 }}>
<Collection title={collection.title}>
{collection.bookmarks.map((bookmark: Bookmark) => (
<Bookmark key={`bookmark_${bookmark.id}`} href={bookmark.url}>
{bookmark.label}
</Bookmark>
))}
</Collection>
</Grid>
))}
</Grid>
</div>
</>
)
}

export default SitesAndApplications

SitesAndApplications.getLayout = withBetaLayout

export async function getStaticProps() {
const collections = await lists.Collection.findMany({
query: 'id title bookmarks { id url label }',
})

return { props: { collections } }
}
12 changes: 12 additions & 0 deletions src/styles/pages/sitesAndApplications.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import 'styles/uswdsDependencies';

.pageTitle {
margin-top: 0;
}

.widgetContainer {
border: 1px solid color('base-light');
border-radius: 4px;
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15);
padding: units(4) units(4) units(6);
}
Empty file removed styles/mvp/_global.scss
Empty file.

0 comments on commit b0bee3c

Please sign in to comment.