diff --git a/examples/with-jest-typescript/README.md b/examples/with-jest-typescript/README.md new file mode 100644 index 00000000..9bd607c7 --- /dev/null +++ b/examples/with-jest-typescript/README.md @@ -0,0 +1,44 @@ +# Example app with Jest tests inside a NextJS TypeScript app + +## How to use + +### Using `create-next-app` + +Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example: + +```bash +npx create-next-app --example with-jest-typescript with-jest-typescript-app +# or +yarn create next-app --example with-jest-typescript with-jest-typescript-app +``` + +### Download manually + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-jest-typescript +cd with-jest-typescript +``` + +Install it and run: + +```bash +npm install +npm run dev +# or +yarn +yarn dev +``` + +## Run Jest tests + +```bash +npm run test +# or +yarn test +``` + +## The idea behind the example + +This example shows a configuration and several examples for a running Jest tests in a NextJS TypeScript app \ No newline at end of file diff --git a/examples/with-jest-typescript/jest.config.js b/examples/with-jest-typescript/jest.config.js new file mode 100644 index 00000000..a839f1d1 --- /dev/null +++ b/examples/with-jest-typescript/jest.config.js @@ -0,0 +1,21 @@ +const TEST_REGEX = '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$' + +module.exports = { + setupFiles: ['/jest.setup.js'], + globals: { + 'ts-jest': { + 'useBabelrc': true + } + }, + testRegex: TEST_REGEX, + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + testPathIgnorePatterns: [ + '/.next/', '/node_modules/' + ], + moduleFileExtensions: [ + 'ts', 'tsx', 'js', 'jsx' + ], + collectCoverage: true +} diff --git a/examples/with-jest-typescript/jest.setup.js b/examples/with-jest-typescript/jest.setup.js new file mode 100644 index 00000000..c2bb88ff --- /dev/null +++ b/examples/with-jest-typescript/jest.setup.js @@ -0,0 +1,4 @@ +const Enzyme = require('enzyme') +const Adapter = require('enzyme-adapter-react-16') + +Enzyme.configure({adapter: new Adapter()}) diff --git a/examples/with-jest-typescript/next.config.js b/examples/with-jest-typescript/next.config.js new file mode 100644 index 00000000..d8b638bd --- /dev/null +++ b/examples/with-jest-typescript/next.config.js @@ -0,0 +1,2 @@ +const withTypescript = require('@zeit/next-typescript') +module.exports = withTypescript() diff --git a/examples/with-jest-typescript/package.json b/examples/with-jest-typescript/package.json new file mode 100644 index 00000000..8f2b4249 --- /dev/null +++ b/examples/with-jest-typescript/package.json @@ -0,0 +1,29 @@ +{ + "name": "with-jest-typescript", + "version": "1.0.0", + "scripts": { + "test": "NODE_ENV=test jest", + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^5.0.0", + "react": "^16.2.0", + "react-dom": "^16.2.0" + }, + "devDependencies": { + "@types/jest": "^22.2.2", + "@types/next": "^2.4.8", + "@types/react": "^16.0.41", + "@types/react-dom": "^16.0.4", + "@zeit/next-typescript": "^0.0.11", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "jest": "^22.4.3", + "react-addons-test-utils": "^15.6.2", + "react-test-renderer": "^16.2.0", + "ts-jest": "^22.4.2", + "typescript": "^2.7.2" + } +} diff --git a/examples/with-jest-typescript/pages/cars.tsx b/examples/with-jest-typescript/pages/cars.tsx new file mode 100644 index 00000000..6f412087 --- /dev/null +++ b/examples/with-jest-typescript/pages/cars.tsx @@ -0,0 +1,5 @@ +import CarsOverview from './../src/modules/cars/Overview'; + +const CarsPage = () => ; + +export default CarsPage; \ No newline at end of file diff --git a/examples/with-jest-typescript/pages/index.tsx b/examples/with-jest-typescript/pages/index.tsx new file mode 100644 index 00000000..d2f5d88b --- /dev/null +++ b/examples/with-jest-typescript/pages/index.tsx @@ -0,0 +1,3 @@ +const IndexPage = () =>

Testing Next.js App written in TypeScript with Jest

; + +export default IndexPage; \ No newline at end of file diff --git a/examples/with-jest-typescript/pages/login.tsx b/examples/with-jest-typescript/pages/login.tsx new file mode 100644 index 00000000..5b78a62f --- /dev/null +++ b/examples/with-jest-typescript/pages/login.tsx @@ -0,0 +1,5 @@ +import Login from './../src/modules/auth/Login'; + +const LoginPage = () => ; + +export default LoginPage; \ No newline at end of file diff --git a/examples/with-jest-typescript/src/components/NiceCheckbox/index.tsx b/examples/with-jest-typescript/src/components/NiceCheckbox/index.tsx new file mode 100644 index 00000000..562a19f4 --- /dev/null +++ b/examples/with-jest-typescript/src/components/NiceCheckbox/index.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +interface NiceCheckboxProps { + rootID : string; + id : string; + name : string; + value : string; + label : string; +} + +const NiceCheckbox : React.SFC < NiceCheckboxProps > = (props : NiceCheckboxProps) : JSX.Element => { + return ( +
+ + +
+ ); +}; + +export default NiceCheckbox; \ No newline at end of file diff --git a/examples/with-jest-typescript/src/components/NiceCheckbox/test.tsx b/examples/with-jest-typescript/src/components/NiceCheckbox/test.tsx new file mode 100644 index 00000000..04c87a52 --- /dev/null +++ b/examples/with-jest-typescript/src/components/NiceCheckbox/test.tsx @@ -0,0 +1,17 @@ +/* eslint-env jest */ +import React from 'react'; +import {shallow} from 'enzyme'; + +import NiceCheckbox from './index'; + +describe('NiceCheckbox', () => { + it('renders the checkbox with correct label', () => { + const wrapper = shallow(); + expect(wrapper.find('#NiceCarCheckbox').find('label').text()).toEqual('Is this car available?'); + }); +}); \ No newline at end of file diff --git a/examples/with-jest-typescript/src/modules/auth/Login.tsx b/examples/with-jest-typescript/src/modules/auth/Login.tsx new file mode 100644 index 00000000..28d03548 --- /dev/null +++ b/examples/with-jest-typescript/src/modules/auth/Login.tsx @@ -0,0 +1,70 @@ +import React, {ChangeEvent} from 'react'; +import Router from 'next/router'; + +import * as T from './types'; + +export interface LoginProps {} + +export interface LoginState { + credentials : T.LoginCredentials; + isLoginLoading : boolean; +} + +export default class Login extends React.Component < LoginProps, +LoginState > { + constructor(props : LoginProps) { + super(props); + + this.state = { + isLoginLoading: false, + credentials: { + email: null, + password: null + } + } + } + + handleCredentialsChange = (e : ChangeEvent < HTMLInputElement >) => { + let {credentials} = this.state; + credentials[e.target.name] = e.target.value; + + this.setState({credentials}); + } + + handleLoginSubmit = (e : React.MouseEvent < HTMLElement >) => { + e.preventDefault(); + this.setState({isLoginLoading: true}); + + setTimeout(() => { + this.setState({isLoginLoading: false}); + Router.replace('/cars'); + }, 500); + } + + render() { + const {credentials} = this.state; + + return ( +
+

Login

+
+ + + + +
+
+ ); + } +} diff --git a/examples/with-jest-typescript/src/modules/auth/__tests__/Login.test.tsx b/examples/with-jest-typescript/src/modules/auth/__tests__/Login.test.tsx new file mode 100644 index 00000000..6d51c9eb --- /dev/null +++ b/examples/with-jest-typescript/src/modules/auth/__tests__/Login.test.tsx @@ -0,0 +1,38 @@ +/* eslint-env jest */ +import React from 'react'; +import {shallow} from 'enzyme'; + +import Login from './../Login'; + +describe('Login', () => { + it('renders the h1 title', () => { + const login = shallow(); + expect(login.find('h1').text()).toEqual('Login'); + }); + + it('renders the form', () => { + const login = shallow(); + expect(login.find('form')).toHaveLength(1); + }); + + it('changes the text of email', () => { + const login = shallow(); + login + .find('#formEmail') + .simulate('change', { + target: { + name: 'email', + value: 'some@test.com' + } + }); + expect(login.update().find('#formEmail').props().value).toEqual('some@test.com'); + }); + + it('changes the text of login button after clicking it', () => { + const login = shallow(); + login + .find('#loginSubmit') + .simulate('click', {preventDefault() {}}); + expect(login.update().find('#loginSubmit').text()).toEqual('Logging in...'); + }); +}); \ No newline at end of file diff --git a/examples/with-jest-typescript/src/modules/auth/types.ts b/examples/with-jest-typescript/src/modules/auth/types.ts new file mode 100644 index 00000000..c234113c --- /dev/null +++ b/examples/with-jest-typescript/src/modules/auth/types.ts @@ -0,0 +1,4 @@ +export interface LoginCredentials { + email: string; + password: string; +} \ No newline at end of file diff --git a/examples/with-jest-typescript/src/modules/cars/Detail.tsx b/examples/with-jest-typescript/src/modules/cars/Detail.tsx new file mode 100644 index 00000000..9cbab8ea --- /dev/null +++ b/examples/with-jest-typescript/src/modules/cars/Detail.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import * as T from './types'; + +interface DetailProps { + car : T.Car; +} + +const Detail : React.SFC < DetailProps > = ({car} : DetailProps) => { + return ( +
+

{`${car.make} ${car.model}`}

+

Engine : {car.engine}

+

Year : {car.year}

+

Mileage : {car.mileage}

+

Equipment : +

+
    {car.equipment && car + .equipment + .map((e : string, index : number) =>
  • {e}
  • )}
+
+ ); +}; + +export default Detail; \ No newline at end of file diff --git a/examples/with-jest-typescript/src/modules/cars/Overview.tsx b/examples/with-jest-typescript/src/modules/cars/Overview.tsx new file mode 100644 index 00000000..27114c82 --- /dev/null +++ b/examples/with-jest-typescript/src/modules/cars/Overview.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; + +import * as T from './types'; + +export interface CarsOverviewProps { + cars : T.CarList; +} + +export interface CarsOverviewState { + selectedCar : T.Car; +} + +export default class CarsOverview extends React.Component < CarsOverviewProps, +CarsOverviewState > { + constructor(props : CarsOverviewProps) { + super(props); + + this.state = { + selectedCar: null + } + } + + handleSelectCar = (car : T.Car) : void => { + this.setState({selectedCar: car}); + } + + renderCarsList = (cars : T.CarList) : JSX.Element => { + if (!cars || cars.length === 0) { + return ( +

No cars

+ ); + } + + return ( +
    {cars.map((car : T.Car, index : number) : JSX.Element =>
  • this.handleSelectCar(car)}>{car.make} {car.model}
  • )}
+ ); + } + + renderCarInfo = (car : T.Car) : JSX.Element => { + if (!car) { + return null; + } + + return ( +
+

{`${car.make} ${car.model}`}

+
{car.engine}
+
+ ); + } + + render() { + return ( +
+

Cars Overview

+ +
+ {this.renderCarsList(this.props.cars)} +
+ + {this.renderCarInfo(this.state.selectedCar)} +
+ ); + } +} diff --git a/examples/with-jest-typescript/src/modules/cars/__tests__/Detail.test.tsx b/examples/with-jest-typescript/src/modules/cars/__tests__/Detail.test.tsx new file mode 100644 index 00000000..d0744601 --- /dev/null +++ b/examples/with-jest-typescript/src/modules/cars/__tests__/Detail.test.tsx @@ -0,0 +1,31 @@ +/* eslint-env jest */ +import React from 'react'; +import {shallow} from 'enzyme'; + +import Detail from './../Detail'; + +const __CAR__ = { + make: 'Volvo', + model: 'XC60', + engine: 'T5', + year: 2018, + mileage: 123, + equipment: ['Leather', 'Seat heating', 'City Safety'] +}; + +describe('Car detail', () => { + it('renders correct title', () => { + const detail = shallow(); + expect(detail.find('h1').text()).toEqual('Volvo XC60'); + }); + + it('renders correct list item', () => { + const detail = shallow(); + expect(detail.childAt(1).text()).toEqual('Engine : T5'); + }); + + it('renders correct equipment list item', () => { + const detail = shallow(); + expect(detail.children().find('ul').childAt(1).text()).toEqual('Seat heating'); + }); +}); \ No newline at end of file diff --git a/examples/with-jest-typescript/src/modules/cars/__tests__/Overview.test.tsx b/examples/with-jest-typescript/src/modules/cars/__tests__/Overview.test.tsx new file mode 100644 index 00000000..f54787e4 --- /dev/null +++ b/examples/with-jest-typescript/src/modules/cars/__tests__/Overview.test.tsx @@ -0,0 +1,64 @@ +/* eslint-env jest */ +import React from 'react'; +import {shallow} from 'enzyme'; + +import Overview from './../Overview'; + +const __CARS__ = [ + { + make: 'Volvo', + model: 'C30', + engine: 'T5', + year: 2018, + mileage: 123, + equipment: ['Leather', 'Seat heating', 'City Safety'] + }, { + make: 'Volvo', + model: 'XC60', + engine: 'D5', + year: 2018, + mileage: 456, + equipment: ['Leather', 'Seat heating', 'City Safety'] + }, { + make: 'Volvo', + model: 'XC90', + engine: 'T6', + year: 2018, + mileage: 789, + equipment: ['Leather', 'Seat heating', 'City Safety'] + } +]; + +describe('Cars overview', () => { + it('renders the h1 title', () => { + const overview = shallow(); + expect(overview.find('h1').text()).toEqual('Cars Overview'); + }); + + it('renders empty cars list when no cars are provided', () => { + const overview = shallow(); + expect(overview.find('.Cars__List').children().find('p').text()).toEqual('No cars'); + }); + + it('renders cars list with 3 items when 3 cars are provided', () => { + const overview = shallow(); + expect(overview.find('.Cars__List').children().find('ul').children()).toHaveLength(3); + }); + + it('renders cars list with the expected item on third place', () => { + const overview = shallow(); + expect(overview.find('.Cars__List').children().find('ul').childAt(2).text()).toEqual('Volvo XC90'); + }); + + it('renders car detail after clicking on an item in cars list', () => { + const overview = shallow(); + overview + .find('.Cars__List') + .children() + .find('ul') + .childAt(1) + .simulate('click', {preventDefault() {}}); + + expect(overview.update().find('.CarInfo').find('h2').text()).toEqual('Volvo XC60'); + }); +}); \ No newline at end of file diff --git a/examples/with-jest-typescript/src/modules/cars/types.ts b/examples/with-jest-typescript/src/modules/cars/types.ts new file mode 100644 index 00000000..e0f20427 --- /dev/null +++ b/examples/with-jest-typescript/src/modules/cars/types.ts @@ -0,0 +1,10 @@ +export interface Car { + make : string; + model : string; + engine : string; + year : number; + mileage : number; + equipment : string[]; +} + +export type CarList = Array < Car >; \ No newline at end of file diff --git a/examples/with-jest-typescript/tsconfig.json b/examples/with-jest-typescript/tsconfig.json new file mode 100644 index 00000000..ace24fb1 --- /dev/null +++ b/examples/with-jest-typescript/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "jsx": "preserve", + "allowJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true, + "skipLibCheck": true, + "baseUrl": ".", + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "dom", + "es2015", + "es2016" + ] + }, + "exclude": [ + "node_modules", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + ] +} \ No newline at end of file