/nextjs13-boilerplate

NextJs 13 - Typescript - Prettier - Eslint - Styled Components - Material UI - Vitest - React Testing Library - Storybook - Cypress

Primary LanguageTypeScript

This is a Nextjs boilerplate with

  • NextJs 13
  • Typescript
  • Prettier
  • Eslint
  • Styled Components
  • Vitest
  • React Testing Library
  • Cypress
  • Storybook
  • Material UI

1 - NextJs 13 & Typescript & Eslint

    npx create-next-app --ts

2 - Prettier

Install prettier

yarn add prettier -D

Create .prettierrc.js file

module.exports = {
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  semi: false,
  singleQuote: true,
  trailingComma: 'es5',
  bracketSpacing: true,
  jsxBracketSameLine: false,
  arrowParens: 'avoid',
  proseWrap: 'preserve',
}

3 - Styled Components

    yarn add styled-components
    yarn add -D @swc/plugin-styled-components @swc/core @types/styled-components

Create .swcrc file

{
  "jsc": {
    "experimental": {
      "plugins": [
        [
          "@swc/plugin-styled-components",
          {
            "displayName": true,
            "ssr": true
          }
        ]
      ]
    }
  }
}

Create a page.styled.tsx file

'use client'

import styled from 'styled-components'

export const StyledTitle = styled.h1`
  color: red;
`

Import the styled component in the page

import { StyledTitle } from './page.styled'


export default function Home() {
    (...)
    return (
        (...)
        <StyledTitle className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </StyledTitle>
       (...)
  )
}

It is important to note that since with Next 13 all page are server components, the styled component must be defined in a separate file, with the 'use client' directive at the top of the file. Otherwise, the styled component will be rendered on the server side and will not work.

4 - Vitest & React Testing Library

Install Vitest & React Testing Library

yarn add -D @testing-library/react @types/node @types/react vitest @vitejs/plugin-react jsdom

Edit the package.json file

{
  "scripts": {
    (...)
    "test": "vitest"
  }
}

Create a vitest.config.ts file

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
  },
})

Create a __tests__/Home.test.tsx file

import { expect, test } from 'vitest'
import { render, screen, within } from '@testing-library/react'
import { vi } from 'vitest'
import Home from '../src/app/page'
import { NextFont } from 'next/dist/compiled/@next/font'

vi.mock('next/font/google', () => ({
  Inter: () =>
    ({
      className: 'inter',
      style: {},
    } as NextFont),
}))

test('home', () => {
  render(<Home />)
  const main = within(screen.getByRole('main'))
  expect(
    main.getByRole('heading', { level: 1, name: /welcome to Next.js!/i })
  ).toBeDefined()

  const vercelLogo = within(screen.getByRole('img', { name: /Vercel Logo/i }))
  expect(vercelLogo).toBeDefined()

  const nextJsLogo = within(screen.getByRole('img', { name: /Next.js Logo/i }))
  expect(nextJsLogo).toBeDefined()
})

Run the test

yarn test
 RERUN  __tests__/Home.test.tsx x27

 ✓ __tests__/Home.test.tsx (1)

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  14:43:21
   Duration  266ms


 PASS  Waiting for file changes...
       press h to show help, press q to quit

5 - Cypress

Install Cypress

yarn add -D cypress start-server-and-test

Edit package.json file

{
  "scripts": {
    (...)
    "e2e": "start-server-and-test dev http://localhost:3000 \"cypress open --e2e\"",
    "e2e:headless": "start-server-and-test dev http://localhost:3000 \"cypress run --e2e\"",
    "component": "cypress open --component",
    "component:headless": "cypress run --component"
  }
}

Run the cypress command to initialize the cypress files for e2e and component testing

yarn cypress:open

Edit cypress.config.ts file

import { defineConfig } from 'cypress'

export default defineConfig({
  component: {
    devServer: {
      framework: 'next',
      bundler: 'webpack',
    },
    specPattern: '**/cypress/__tests__/*.spec.tsx',
    viewportHeight: 1280,
    viewportWidth: 800,
  },
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: '**/cypress/e2e/*.spec.ts',
  },
  video: false,
})

Create a /src/app/about/page.tsx file

import Link from 'next/link'

export default function About() {
  return (
    <div>
      <h1>About Page</h1>
      <Link href="/">Homepage</Link>
    </div>
  )
}

Edit src/app/page.tsx file

import Link from 'next/link'

(...)

export default function Home() {
  return (
    (...)
    <Link href="/about">About</Link>
    (...)
  )
}

e2E testing : Create a cypress/e2e/navigation.spec.ts file

describe('Navigation', () => {
  it('should navigate to the about page', () => {
    // Start from the index page
    cy.visit('http://localhost:3000/')

    // Find a link with an href attribute containing "about" and click it
    cy.get('a[href*="about"]').click()

    // The new url should include "/about"
    cy.url().should('include', '/about')

    // The new page should contain an h1 with "About page"
    cy.get('h1').contains('About Page')
  })

  it('should navigate to the home page', () => {
    cy.visit('http://localhost:3000/about')

    // Find a link with an href attribute containing "/" and click it
    cy.get('a[href*="/"]').click()

    // The new url should include "/"
    cy.url().should('include', '/')

    // The new page should contain an h1 with "Home page"
    cy.get('h1').contains('Welcome to Next.js!')
  })
})

export {}

Run the e2e tests

yarn e2e:headless
  (Run Finished)
       Spec                         Tests  Passing  Failing  Pending  Skipped
  ┌───────────────────────────────────────────────────────────────────────────┐
  │ ✔  navigation.spec.ts  00:03     2        2        -        -        -    │
  └───────────────────────────────────────────────────────────────────────────┘
    ✔  All specs passed!   00:03     2        2        -        -        -

✨  Done in 12.32s.

Components testing

Create a cypress/__tests__/pageAbout.spec.tsx file

import About from '../../src/app/about/page'
import React from 'react'

describe('<About />', () => {
  it('renders', () => {
    // see: https://on.cypress.io/mounting-react
    cy.mount(<About />)

    // see: https://on.cypress.io/interacting-with-elements

    // find the link to the homepage
    cy.contains('a', 'Homepage').should('have.attr', 'href', '/')

    // find the heading
    cy.contains('h1', 'About Page').should('be.visible')
  })
})

Create a cypress/__tests__/pageHome.spec.tsx file

import React from 'react'
import Home from '../../src/app/page'

describe('<Home />', () => {
  it('renders', () => {
    // see: https://on.cypress.io/mounting-react
    cy.mount(<Home />)

    // see: https://on.cypress.io/interacting-with-elements

    // find the link to the about page
    cy.contains('a', 'About').should('have.attr', 'href', '/about')

    // find the heading
    cy.contains('h1', 'Welcome to Next.js!').should('be.visible')
  })
})

Run the component tests

yarn component:headless
  (Run Finished)
       Spec                               Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  pageAbout.spec.tsx     114ms        1        1        -        -        -   │
  ├────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  pageHome.spec.tsx      154ms        1        1        -        -        -   │
  └────────────────────────────────────────────────────────────────────────────────┘
    ✔  All specs passed       268ms        2        2        -        -        -

✨  Done in 8.57s.

6 - Storybook

npx storybook@next init

7 - API testing

Install supertest

yarn add -D supertest

Create a tests/api/hello.test.ts file

import request from 'supertest'
import { expect, test } from 'vitest'

test('GET /hello', async () => {
  const response = await request('http://localhost:3000').get('/api/hello')

  expect(response.status).toEqual(200)

  const text = await response.text
  expect(text).toBe('Hello, Next.js!')
})