├── .babelrc ├── .gitignore ├── README.md ├── __tests__ ├── BlogDetail.test.tsx ├── BlogPage.test.tsx ├── CommentPage.test.tsx ├── Context.test.tsx ├── Home.test.tsx ├── NavBar.test.tsx ├── Props.test.tsx ├── TaskPageSWR.test.tsx └── TaskPageStatic.test.tsx ├── components ├── Comment.tsx ├── ContextA.tsx ├── ContextB.tsx ├── Layout.tsx └── Post.tsx ├── context └── StateProvider.tsx ├── lib └── fetch.ts ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── blog-page.tsx ├── comment-page.tsx ├── context-page.tsx ├── index.tsx ├── posts │ └── [id].tsx └── task-page.tsx ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types └── Types.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Project setup : Nextjs+React-testing-library+TypeScript+Tailwind CSS 4 | 5 | ## 1. Nextjs Project 新規作成 6 | ### 1-1. create-next-app 7 | npx create-next-app@11.1.2 . --use-npm 8 | #### Node.js version 10.13以降が必要です。 -> ターミナル `node -v`でver確認出来ます。 9 | ### 1-2. 必要 module のインストール 10 | npm i axios@0.21.1 msw@0.35.0 swr 11 | ### 1-3. prettierの設定 : package.json 12 | ~~~ 13 | "prettier": { 14 | "singleQuote": true, 15 | "semi": false 16 | } 17 | ~~~ 18 | 19 | ## 2. React-testing-library の導入 20 | 21 | ### 2-1. 必要 module のインストール 22 | npm i react@17.0.2 react-dom@17.0.2 23 | npm i next@11.1.2 24 | npm i -D jest@26.6.3 @testing-library/react@11.2.3 @types/jest@26.0.20 @testing-library/jest-dom@5.11.8 @testing-library/dom@7.29.2 babel-jest@26.6.3 @testing-library/user-event@12.6.0 jest-css-modules@2.1.0 25 | ### 2-2. Project folder 直下に".babelrc"ファイルを作成して下記設定を追加 26 | touch .babelrc 27 | ~~~ 28 | { 29 | "presets": ["next/babel"] 30 | } 31 | ~~~ 32 | ### 2-3. package.json に jest の設定を追記 33 | ~~~ 34 | "jest": { 35 | "testPathIgnorePatterns": [ 36 | "/.next/", 37 | "/node_modules/" 38 | ], 39 | "moduleNameMapper": { 40 | "\\.(css)$": "/node_modules/jest-css-modules" 41 | } 42 | } 43 | ~~~ 44 | ### 2-4. package.jsonに test scriptを追記 45 | ~~~ 46 | "scripts": { 47 | ... 48 | "test": "jest --env=jsdom --verbose" 49 | }, 50 | ~~~ 51 | 52 | ## 3. TypeScript の導入 53 | https://nextjs.org/learn/excel/typescript/create-tsconfig 54 | ### 3-1. 空のtsconfig.json作成 55 | touch tsconfig.json 56 | ### 3-2. 必要moduleのインストール 57 | npm i -D typescript @types/react@17.0.41 @types/node@14.14.41 58 | ### 3-3. 開発server起動 59 | npm run dev 60 | ### 3-4. _app.js, index.js -> tsx へ拡張子変更 61 | ### 3-5. AppProps型追記 62 | ~~~ 63 | import { AppProps } from 'next/app' 64 | 65 | function MyApp({ Component, pageProps }: AppProps) { 66 | return 67 | } 68 | 69 | export default MyApp 70 | ~~~ 71 | 72 | ## 4. Tailwind CSS の導入 73 | https://tailwindcss.com/docs/guides/nextjs 74 | ### 4-1. 必要moduleのインストール 75 | npm i tailwindcss@latest postcss@latest autoprefixer@latest 76 | ### 4-2. tailwind.config.js, postcss.config.jsの生成 77 | npx tailwindcss init -p 78 | ### 4-3. tailwind.config.jsのpurge設定追加 79 | ~~~ 80 | module.exports = { 81 | content: [ 82 | "./pages/**/*.{js,ts,jsx,tsx}", 83 | "./components/**/*.{js,ts,jsx,tsx}", 84 | ], 85 | darkMode: false, 86 | theme: { 87 | extend: {}, 88 | }, 89 | variants: { 90 | extend: {}, 91 | }, 92 | plugins: [], 93 | } 94 | ~~~ 95 | ### 4-4. globals.cssの編集 96 | ~~~ 97 | @tailwind base; 98 | @tailwind components; 99 | @tailwind utilities; 100 | ~~~ 101 | ## 5. 動作確認 102 | ### 5-1. index.tsxの編集 103 | ~~~ 104 | const Home: React.FC = () => { 105 | return ( 106 |
107 | Hello Nextjs 108 |
109 | ) 110 | } 111 | export default Home 112 | ~~~ 113 | #### npm run dev -> Tailwind CSSが効いているかブラウザで確認 114 | ### 5-2. `__tests__`フォルダと`Home.test.tsx`ファイルの作成 115 | ~~~ 116 | import { render, screen } from '@testing-library/react' 117 | import '@testing-library/jest-dom/extend-expect' 118 | import Home from '../pages/index' 119 | 120 | it('Should render hello text', () => { 121 | render() 122 | expect(screen.getByText('Hello Nextjs')).toBeInTheDocument() 123 | }) 124 | ~~~ 125 | #### npm test -> テストがPASSするか確認 126 | ~~~ 127 | PASS __tests__/Home.test.tsx 128 | ✓ Should render hello text (20 ms) 129 | 130 | Test Suites: 1 passed, 1 total 131 | Tests: 1 passed, 1 total 132 | Snapshots: 0 total 133 | Time: 1.728 s, estimated 2 s 134 | ~~~ 135 | -------------------------------------------------------------------------------- /__tests__/BlogDetail.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import '@testing-library/jest-dom/extend-expect' 5 | import { render, screen, cleanup } from '@testing-library/react' 6 | import { getPage } from 'next-page-tester' 7 | import { initTestHelpers } from 'next-page-tester' 8 | import { rest } from 'msw' 9 | import { setupServer } from 'msw/node' 10 | import userEvent from '@testing-library/user-event' 11 | import 'setimmediate' 12 | 13 | initTestHelpers() 14 | const handlers = [ 15 | rest.get('https://jsonplaceholder.typicode.com/posts/', (req, res, ctx) => { 16 | const query = req.url.searchParams 17 | const _limit = query.get('_limit') 18 | if (_limit === '10') { 19 | return res( 20 | ctx.status(200), 21 | ctx.json([ 22 | { 23 | userId: 1, 24 | id: 1, 25 | title: 'dummy title 1', 26 | body: 'dummy body 1', 27 | }, 28 | { 29 | userId: 2, 30 | id: 2, 31 | title: 'dummy title 2', 32 | body: 'dummy body 2', 33 | }, 34 | ]) 35 | ) 36 | } 37 | }), 38 | // rest.get( 39 | // 'https://jsonplaceholder.typicode.com/posts/?_limit=10', 40 | // (req, res, ctx) => { 41 | // return res( 42 | // ctx.status(200), 43 | // ctx.json([ 44 | // { 45 | // userId: 1, 46 | // id: 1, 47 | // title: 'dummy title 1', 48 | // body: 'dummy body 1', 49 | // }, 50 | // { 51 | // userId: 2, 52 | // id: 2, 53 | // title: 'dummy title 2', 54 | // body: 'dummy body 2', 55 | // }, 56 | // ]) 57 | // ) 58 | // } 59 | // ), 60 | rest.get('https://jsonplaceholder.typicode.com/posts/1', (req, res, ctx) => { 61 | return res( 62 | ctx.status(200), 63 | ctx.json({ 64 | userId: 1, 65 | id: 1, 66 | title: 'dummy title 1', 67 | body: 'dummy body 1', 68 | }) 69 | ) 70 | }), 71 | rest.get('https://jsonplaceholder.typicode.com/posts/2', (req, res, ctx) => { 72 | return res( 73 | ctx.status(200), 74 | ctx.json({ 75 | userId: 2, 76 | id: 2, 77 | title: 'dummy title 2', 78 | body: 'dummy body 2', 79 | }) 80 | ) 81 | }), 82 | ] 83 | const server = setupServer(...handlers) 84 | beforeAll(() => { 85 | server.listen() 86 | }) 87 | afterEach(() => { 88 | server.resetHandlers() 89 | cleanup() 90 | }) 91 | afterAll(() => { 92 | server.close() 93 | }) 94 | describe(`Blog detail page`, () => { 95 | it('Should render detailed content of ID 1', async () => { 96 | const { page } = await getPage({ 97 | route: '/posts/1', 98 | }) 99 | render(page) 100 | expect(await screen.findByText('dummy title 1')).toBeInTheDocument() 101 | expect(screen.getByText('dummy body 1')).toBeInTheDocument() 102 | //screen.debug() 103 | }) 104 | it('Should render detailed content of ID 2', async () => { 105 | const { page } = await getPage({ 106 | route: '/posts/2', 107 | }) 108 | render(page) 109 | expect(await screen.findByText('dummy title 2')).toBeInTheDocument() 110 | expect(screen.getByText('dummy body 2')).toBeInTheDocument() 111 | }) 112 | it('Should route back to blog-page from detail page', async () => { 113 | const { page } = await getPage({ 114 | route: '/posts/2', 115 | }) 116 | render(page) 117 | await screen.findByText('dummy title 2') 118 | userEvent.click(screen.getByTestId('back-blog')) 119 | expect(await screen.findByText('blog page')).toBeInTheDocument() 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /__tests__/BlogPage.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import '@testing-library/jest-dom/extend-expect' 5 | import { render, screen, cleanup } from '@testing-library/react' 6 | import { getPage } from 'next-page-tester' 7 | import { initTestHelpers } from 'next-page-tester' 8 | import { rest } from 'msw' 9 | import { setupServer } from 'msw/node' 10 | import 'setimmediate' 11 | 12 | initTestHelpers() 13 | 14 | const handlers = [ 15 | rest.get('https://jsonplaceholder.typicode.com/posts/', (req, res, ctx) => { 16 | const query = req.url.searchParams 17 | const _limit = query.get('_limit') 18 | if (_limit === '10') { 19 | return res( 20 | ctx.status(200), 21 | ctx.json([ 22 | { 23 | userId: 1, 24 | id: 1, 25 | title: 'dummy title 1', 26 | body: 'dummy body 1', 27 | }, 28 | { 29 | userId: 2, 30 | id: 2, 31 | title: 'dummy title 2', 32 | body: 'dummy body 2', 33 | }, 34 | ]) 35 | ) 36 | } 37 | }), 38 | // rest.get( 39 | // 'https://jsonplaceholder.typicode.com/posts/?_limit=10', 40 | // (req, res, ctx) => { 41 | // return res( 42 | // ctx.status(200), 43 | // ctx.json([ 44 | // { 45 | // userId: 1, 46 | // id: 1, 47 | // title: 'dummy title 1', 48 | // body: 'dummy body 1', 49 | // }, 50 | // { 51 | // userId: 2, 52 | // id: 2, 53 | // title: 'dummy title 2', 54 | // body: 'dummy body 2', 55 | // }, 56 | // ]) 57 | // ) 58 | // } 59 | // ), 60 | ] 61 | const server = setupServer(...handlers) 62 | beforeAll(() => { 63 | server.listen() 64 | }) 65 | afterEach(() => { 66 | server.resetHandlers() 67 | cleanup() 68 | }) 69 | afterAll(() => { 70 | server.close() 71 | }) 72 | 73 | describe(`Blog page`, () => { 74 | it('Should render the list of blogs pre-fetched by getStaticProps', async () => { 75 | const { page } = await getPage({ 76 | route: '/blog-page', 77 | }) 78 | render(page) 79 | expect(await screen.findByText('blog page')).toBeInTheDocument() 80 | expect(screen.getByText('dummy title 1')).toBeInTheDocument() 81 | expect(screen.getByText('dummy title 2')).toBeInTheDocument() 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /__tests__/CommentPage.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { render, screen, cleanup } from '@testing-library/react' 5 | import '@testing-library/jest-dom/extend-expect' 6 | import { SWRConfig } from 'swr' 7 | import { rest } from 'msw' 8 | import { setupServer } from 'msw/node' 9 | import CommentPage from '../pages/comment-page' 10 | import 'setimmediate' 11 | 12 | const server = setupServer( 13 | rest.get( 14 | 'https://jsonplaceholder.typicode.com/comments/', 15 | (req, res, ctx) => { 16 | const query = req.url.searchParams 17 | const _limit = query.get('_limit') 18 | if (_limit === '10') { 19 | return res( 20 | ctx.status(200), 21 | ctx.json([ 22 | { 23 | postId: 1, 24 | id: 1, 25 | name: 'A', 26 | email: 'dummya@gmail.com', 27 | body: 'test body a', 28 | }, 29 | { 30 | postId: 2, 31 | id: 2, 32 | name: 'B', 33 | email: 'dummyb@gmail.com', 34 | body: 'test body b', 35 | }, 36 | ]) 37 | ) 38 | } 39 | } 40 | ) 41 | // rest.get( 42 | // 'https://jsonplaceholder.typicode.com/comments/?_limit=10', 43 | // (req, res, ctx) => { 44 | // return res( 45 | // ctx.status(200), 46 | // ctx.json([ 47 | // { 48 | // postId: 1, 49 | // id: 1, 50 | // name: 'A', 51 | // email: 'dummya@gmail.com', 52 | // body: 'test body a', 53 | // }, 54 | // { 55 | // postId: 2, 56 | // id: 2, 57 | // name: 'B', 58 | // email: 'dummyb@gmail.com', 59 | // body: 'test body b', 60 | // }, 61 | // ]) 62 | // ) 63 | // } 64 | // ) 65 | ) 66 | beforeAll(() => server.listen()) 67 | afterEach(() => { 68 | server.resetHandlers() 69 | cleanup() 70 | }) 71 | afterAll(() => server.close()) 72 | 73 | describe('Comment page with useSWR / Success+Error', () => { 74 | it('Should render the value fetched by useSWR ', async () => { 75 | render( 76 | 77 | 78 | 79 | ) 80 | expect(await screen.findByText('1: test body a')).toBeInTheDocument() 81 | expect(screen.getByText('2: test body b')).toBeInTheDocument() 82 | }) 83 | it('Should render Error text when fetch failed', async () => { 84 | server.use( 85 | rest.get( 86 | 'https://jsonplaceholder.typicode.com/comments/', 87 | (req, res, ctx) => { 88 | const query = req.url.searchParams 89 | const _limit = query.get('_limit') 90 | if (_limit === '10') { 91 | return res(ctx.status(400)) 92 | } 93 | } 94 | ) 95 | // rest.get( 96 | // 'https://jsonplaceholder.typicode.com/comments/?_limit=10', 97 | // (req, res, ctx) => { 98 | // return res(ctx.status(400)) 99 | // } 100 | // ) 101 | ) 102 | render( 103 | 104 | 105 | 106 | ) 107 | expect(await screen.findByText('Error!')).toBeInTheDocument() 108 | //screen.debug() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /__tests__/Context.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import '@testing-library/jest-dom/extend-expect' 5 | import { render, screen } from '@testing-library/react' 6 | import userEvent from '@testing-library/user-event' 7 | import { StateProvider } from '../context/StateProvider' 8 | import ContextA from '../components/ContextA' 9 | import ContextB from '../components/ContextB' 10 | import 'setimmediate' 11 | 12 | describe('Global state management (useContext)', () => { 13 | it('Should change the toggle state globally', () => { 14 | render( 15 | 16 | 17 | 18 | 19 | ) 20 | expect(screen.getByTestId('toggle-a').textContent).toBe('false') 21 | expect(screen.getByTestId('toggle-b').textContent).toBe('false') 22 | userEvent.click(screen.getByRole('button')) 23 | expect(screen.getByTestId('toggle-a').textContent).toBe('true') 24 | expect(screen.getByTestId('toggle-b').textContent).toBe('true') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /__tests__/Home.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { render, screen } from '@testing-library/react' 6 | import '@testing-library/jest-dom/extend-expect' 7 | import Home from '../pages/index' 8 | import 'setimmediate' 9 | 10 | it('Should render hello text', () => { 11 | render() 12 | expect(screen.getByText('Welcome to Nextjs')).toBeInTheDocument() 13 | }) 14 | -------------------------------------------------------------------------------- /__tests__/NavBar.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { render, screen } from '@testing-library/react' 5 | import '@testing-library/jest-dom/extend-expect' 6 | import userEvent from '@testing-library/user-event' 7 | import { getPage } from 'next-page-tester' 8 | import { initTestHelpers } from 'next-page-tester' 9 | import 'setimmediate' 10 | 11 | initTestHelpers() 12 | 13 | describe('Navigation by Link', () => { 14 | it('Should route to selected page in navbar', async () => { 15 | const { page } = await getPage({ 16 | route: '/index', 17 | }) 18 | render(page) 19 | 20 | userEvent.click(screen.getByTestId('blog-nav')) 21 | expect(await screen.findByText('blog page')).toBeInTheDocument() 22 | //screen.debug() 23 | userEvent.click(screen.getByTestId('comment-nav')) 24 | expect(await screen.findByText('comment page')).toBeInTheDocument() 25 | userEvent.click(screen.getByTestId('context-nav')) 26 | expect(await screen.findByText('context page')).toBeInTheDocument() 27 | userEvent.click(screen.getByTestId('task-nav')) 28 | expect(await screen.findByText('todos page')).toBeInTheDocument() 29 | userEvent.click(screen.getByTestId('home-nav')) 30 | expect(await screen.findByText('Welcome to Nextjs')).toBeInTheDocument() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /__tests__/Props.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { render, screen } from '@testing-library/react' 5 | import '@testing-library/jest-dom/extend-expect' 6 | import Post from '../components/Post' 7 | import { POST } from '../types/Types' 8 | import 'setimmediate' 9 | 10 | describe('Post component with given props', () => { 11 | let dummyProps: POST 12 | beforeEach(() => { 13 | dummyProps = { 14 | userId: 1, 15 | id: 1, 16 | title: 'dummy title 1', 17 | body: 'dummy body 1', 18 | } 19 | }) 20 | it('Should render correctly with given props value', () => { 21 | render() 22 | expect(screen.getByText(dummyProps.id)).toBeInTheDocument() 23 | expect(screen.getByText(dummyProps.title)).toBeInTheDocument() 24 | //screen.debug() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /__tests__/TaskPageSWR.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import '@testing-library/jest-dom/extend-expect' 5 | import { render, screen, cleanup } from '@testing-library/react' 6 | import { SWRConfig } from 'swr' 7 | import { rest } from 'msw' 8 | import { setupServer } from 'msw/node' 9 | import TaskPage from '../pages/task-page' 10 | import { TASK } from '../types/Types' 11 | import 'setimmediate' 12 | 13 | const server = setupServer( 14 | rest.get('https://jsonplaceholder.typicode.com/todos/', (req, res, ctx) => { 15 | const query = req.url.searchParams 16 | const _limit = query.get('_limit') 17 | if (_limit === '10') { 18 | return res( 19 | ctx.status(200), 20 | ctx.json([ 21 | { 22 | userId: 1, 23 | id: 1, 24 | title: 'Task A', 25 | completed: false, 26 | }, 27 | { 28 | userId: 1, 29 | id: 2, 30 | title: 'Task B', 31 | completed: true, 32 | }, 33 | ]) 34 | ) 35 | } 36 | }) 37 | // rest.get( 38 | // 'https://jsonplaceholder.typicode.com/todos/?_limit=10', 39 | // (req, res, ctx) => { 40 | // return res( 41 | // ctx.status(200), 42 | // ctx.json([ 43 | // { 44 | // userId: 1, 45 | // id: 1, 46 | // title: 'Task A', 47 | // completed: false, 48 | // }, 49 | // { 50 | // userId: 1, 51 | // id: 2, 52 | // title: 'Task B', 53 | // completed: true, 54 | // }, 55 | // ]) 56 | // ) 57 | // } 58 | // ) 59 | ) 60 | beforeAll(() => { 61 | server.listen() 62 | }) 63 | afterEach(() => { 64 | server.resetHandlers() 65 | cleanup() 66 | }) 67 | afterAll(() => { 68 | server.close() 69 | }) 70 | describe(`Todos page / useSWR`, () => { 71 | let staticProps: TASK[] 72 | staticProps = [ 73 | { 74 | userId: 3, 75 | id: 3, 76 | title: 'Static task C', 77 | completed: true, 78 | }, 79 | { 80 | userId: 4, 81 | id: 4, 82 | title: 'Static task D', 83 | completed: false, 84 | }, 85 | ] 86 | it('Should render CSF data after pre-rendered data', async () => { 87 | render( 88 | 89 | 90 | 91 | ) 92 | expect(await screen.findByText('Static task C')).toBeInTheDocument() 93 | expect(screen.getByText('Static task D')).toBeInTheDocument() 94 | //screen.debug() 95 | expect(await screen.findByText('Task A')).toBeInTheDocument() 96 | expect(screen.getByText('Task B')).toBeInTheDocument() 97 | //screen.debug() 98 | }) 99 | it('Should render Error text when fetch failed', async () => { 100 | server.use( 101 | rest.get( 102 | 'https://jsonplaceholder.typicode.com/todos/', 103 | (req, res, ctx) => { 104 | const query = req.url.searchParams 105 | const _limit = query.get('_limit') 106 | if (_limit === '10') { 107 | return res(ctx.status(400)) 108 | } 109 | } 110 | ) 111 | // rest.get( 112 | // 'https://jsonplaceholder.typicode.com/todos/?_limit=10', 113 | // (req, res, ctx) => { 114 | // return res(ctx.status(400)) 115 | // } 116 | // ) 117 | ) 118 | render( 119 | 120 | 121 | 122 | ) 123 | expect(await screen.findByText('Error!')).toBeInTheDocument() 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /__tests__/TaskPageStatic.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import '@testing-library/jest-dom/extend-expect' 5 | import { render, screen, cleanup } from '@testing-library/react' 6 | import { getPage } from 'next-page-tester' 7 | import { initTestHelpers } from 'next-page-tester' 8 | import { rest } from 'msw' 9 | import { setupServer } from 'msw/node' 10 | import 'setimmediate' 11 | 12 | initTestHelpers() 13 | const server = setupServer( 14 | rest.get('https://jsonplaceholder.typicode.com/todos/', (req, res, ctx) => { 15 | const query = req.url.searchParams 16 | const _limit = query.get('_limit') 17 | if (_limit === '10') { 18 | return res( 19 | ctx.status(200), 20 | ctx.json([ 21 | { 22 | userId: 3, 23 | id: 3, 24 | title: 'Static task C', 25 | completed: true, 26 | }, 27 | { 28 | userId: 4, 29 | id: 4, 30 | title: 'Static task D', 31 | completed: false, 32 | }, 33 | ]) 34 | ) 35 | } 36 | }) 37 | // rest.get( 38 | // 'https://jsonplaceholder.typicode.com/todos/?_limit=10', 39 | // (req, res, ctx) => { 40 | // return res( 41 | // ctx.status(200), 42 | // ctx.json([ 43 | // { 44 | // userId: 3, 45 | // id: 3, 46 | // title: 'Static task C', 47 | // completed: true, 48 | // }, 49 | // { 50 | // userId: 4, 51 | // id: 4, 52 | // title: 'Static task D', 53 | // completed: false, 54 | // }, 55 | // ]) 56 | // ) 57 | // } 58 | // ) 59 | ) 60 | beforeAll(() => { 61 | server.listen() 62 | }) 63 | afterEach(() => { 64 | server.resetHandlers() 65 | cleanup() 66 | }) 67 | afterAll(() => { 68 | server.close() 69 | }) 70 | describe(`Todo page / getStaticProps`, () => { 71 | it('Should render the list of tasks pre-fetched by getStaticProps', async () => { 72 | const { page } = await getPage({ 73 | route: '/task-page', 74 | }) 75 | render(page) 76 | expect(await screen.findByText('todos page')).toBeInTheDocument() 77 | expect(screen.getByText('Static task C')).toBeInTheDocument() 78 | expect(screen.getByText('Static task D')).toBeInTheDocument() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /components/Comment.tsx: -------------------------------------------------------------------------------- 1 | import { COMMENT } from '../types/Types' 2 | 3 | const Comment: React.FC = ({ id, name, body }) => { 4 | return ( 5 |
  • 6 |

    7 | {id} 8 | {': '} 9 | {body} 10 |

    11 |

    12 | {'by '} 13 | {name} 14 |

    15 |
  • 16 | ) 17 | } 18 | export default Comment 19 | -------------------------------------------------------------------------------- /components/ContextA.tsx: -------------------------------------------------------------------------------- 1 | import { useStateContext } from '../context/StateProvider' 2 | 3 | const ContextA: React.FC = () => { 4 | const { toggle, setToggle } = useStateContext() 5 | return ( 6 | <> 7 | 15 |

    Context A

    16 |

    17 | {toggle ? 'true' : 'false'} 18 |

    19 | 20 | ) 21 | } 22 | export default ContextA 23 | -------------------------------------------------------------------------------- /components/ContextB.tsx: -------------------------------------------------------------------------------- 1 | import { useStateContext } from '../context/StateProvider' 2 | 3 | const ContextB: React.FC = () => { 4 | const { toggle } = useStateContext() 5 | return ( 6 | <> 7 |

    Context B

    8 |

    9 | {toggle ? 'true' : 'false'} 10 |

    11 | 12 | ) 13 | } 14 | export default ContextB 15 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Link from 'next/link' 3 | import Image from 'next/image' 4 | 5 | interface TITLE { 6 | title: string 7 | } 8 | const Layout: React.FC = ({ children, title = 'Nextjs' }) => { 9 | return ( 10 | <div className="flex justify-center items-center flex-col min-h-screen font-mono"> 11 | <Head> 12 | <title>{title} 13 | 14 |
    15 | 61 |
    62 |
    63 | {children} 64 |
    65 | 77 | 78 | ) 79 | } 80 | export default Layout 81 | -------------------------------------------------------------------------------- /components/Post.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { POST } from '../types/Types' 3 | 4 | const Post: React.FC = ({ id, title }) => { 5 | return ( 6 |
    7 | {id} 8 | {' : '} 9 | 10 | 11 | {title} 12 | 13 | 14 |
    15 | ) 16 | } 17 | export default Post 18 | -------------------------------------------------------------------------------- /context/StateProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, createContext } from 'react' 2 | 3 | const StateContext = createContext( 4 | {} as { 5 | toggle: boolean 6 | setToggle: React.Dispatch> 7 | } 8 | ) 9 | 10 | export const StateProvider: React.FC = ({ children }) => { 11 | const [toggle, setToggle] = useState(false) 12 | 13 | return ( 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | export const useStateContext = () => useContext(StateContext) 20 | -------------------------------------------------------------------------------- /lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | export const getAllPostsData = async () => { 4 | const res = await fetch( 5 | new URL('https://jsonplaceholder.typicode.com/posts/?_limit=10') 6 | ) 7 | const posts = await res.json() 8 | return posts 9 | } 10 | 11 | export const getAllTasksData = async () => { 12 | const res = await fetch( 13 | new URL('https://jsonplaceholder.typicode.com/todos/?_limit=10') 14 | ) 15 | const tasks = await res.json() 16 | return tasks 17 | } 18 | 19 | export const getAllPostIds = async () => { 20 | const res = await fetch( 21 | new URL('https://jsonplaceholder.typicode.com/posts/?_limit=10') 22 | ) 23 | const posts = await res.json() 24 | return posts.map((post) => { 25 | return { 26 | params: { 27 | id: String(post.id), 28 | }, 29 | } 30 | }) 31 | } 32 | 33 | export const getPostData = async (id: string) => { 34 | const res = await fetch( 35 | new URL(`https://jsonplaceholder.typicode.com/posts/${id}`) 36 | ) 37 | const post = await res.json() 38 | // return { 39 | // post, 40 | // } 41 | return post 42 | } 43 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-testing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "jest --env=jsdom --verbose" 10 | }, 11 | "dependencies": { 12 | "autoprefixer": "^10.2.1", 13 | "axios": "^0.21.1", 14 | "msw": "^0.25.0", 15 | "next": "10.0.5", 16 | "next-page-tester": "^0.16.0", 17 | "postcss": "^8.2.4", 18 | "react": "17.0.1", 19 | "react-dom": "17.0.1", 20 | "setimmediate": "^1.0.5", 21 | "swr": "^0.4.0", 22 | "tailwindcss": "^2.0.2" 23 | }, 24 | "prettier": { 25 | "singleQuote": true, 26 | "semi": false 27 | }, 28 | "jest": { 29 | "testPathIgnorePatterns": [ 30 | "/.next/", 31 | "/node_modules/" 32 | ], 33 | "moduleNameMapper": { 34 | "\\.(css)$": "/node_modules/jest-css-modules" 35 | } 36 | }, 37 | "devDependencies": { 38 | "@testing-library/dom": "^7.29.2", 39 | "@testing-library/jest-dom": "^5.11.8", 40 | "@testing-library/react": "^11.2.3", 41 | "@testing-library/user-event": "^12.6.0", 42 | "@types/jest": "^26.0.20", 43 | "@types/node": "^14.14.20", 44 | "@types/react": "^17.0.0", 45 | "babel-jest": "^26.6.3", 46 | "jest": "^26.6.3", 47 | "jest-css-modules": "^2.1.0", 48 | "typescript": "^4.1.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import { AppProps } from 'next/app' 3 | function MyApp({ Component, pageProps }: AppProps) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/blog-page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import { getAllPostsData } from '../lib/fetch' 3 | import Post from '../components/Post' 4 | import { GetStaticProps } from 'next' 5 | import { POST } from '../types/Types' 6 | 7 | interface STATICPROPS { 8 | posts: POST[] 9 | } 10 | 11 | const BlogPage: React.FC = ({ posts }) => { 12 | return ( 13 | 14 |

    blog page

    15 |
      {posts && posts.map((post) => )}
    16 |
    17 | ) 18 | } 19 | export default BlogPage 20 | 21 | export const getStaticProps: GetStaticProps = async () => { 22 | const posts = await getAllPostsData() 23 | return { 24 | props: { posts }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pages/comment-page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import useSWR from 'swr' 3 | import axios from 'axios' 4 | import Comment from '../components/Comment' 5 | import { COMMENT } from '../types/Types' 6 | 7 | const axiosFetcher = async () => { 8 | const result = await axios.get( 9 | 'https://jsonplaceholder.typicode.com/comments/?_limit=10' 10 | ) 11 | return result.data 12 | } 13 | 14 | const CommentPage: React.FC = () => { 15 | const { data: comments, error } = useSWR('commentsFetch', axiosFetcher) 16 | 17 | if (error) return Error! 18 | 19 | return ( 20 | 21 |

    comment page

    22 |
      23 | {comments && 24 | comments.map((comment) => )} 25 |
    26 |
    27 | ) 28 | } 29 | export default CommentPage 30 | -------------------------------------------------------------------------------- /pages/context-page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import { StateProvider } from '../context/StateProvider' 3 | import ContextA from '../components/ContextA' 4 | import ContextB from '../components/ContextB' 5 | 6 | const ContextPage: React.FC = () => { 7 | return ( 8 | 9 |

    context page

    10 | 11 | 12 | 13 | 14 |
    15 | ) 16 | } 17 | export default ContextPage 18 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | const Home: React.FC = () => { 3 | return ( 4 | 5 |

    Welcome to Nextjs

    6 |
    7 | ) 8 | } 9 | export default Home 10 | -------------------------------------------------------------------------------- /pages/posts/[id].tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Layout from '../../components/Layout' 3 | import { getAllPostIds, getPostData } from '../../lib/fetch' 4 | import { POST } from '../../types/Types' 5 | import { GetStaticProps, GetStaticPaths } from 'next' 6 | 7 | const PostDetail: React.FC = ({ id, title, body }) => { 8 | return ( 9 | 10 |

    11 | {'ID : '} 12 | {id} 13 |

    14 |

    {title}

    15 |

    {body}

    16 | 17 |
    18 | 25 | 31 | 32 | Back to blog-page 33 |
    34 | 35 |
    36 | ) 37 | } 38 | export default PostDetail 39 | 40 | export const getStaticPaths: GetStaticPaths = async () => { 41 | const paths = await getAllPostIds() 42 | return { 43 | paths, 44 | fallback: false, 45 | } 46 | } 47 | 48 | export const getStaticProps: GetStaticProps = async (ctx) => { 49 | //const { post: post } = await getPostData(ctx.params.id as string) 50 | const post = await getPostData(ctx.params.id as string) 51 | return { 52 | props: { 53 | ...post, 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pages/task-page.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import { GetStaticProps } from 'next' 3 | import { getAllTasksData } from '../lib/fetch' 4 | import useSWR from 'swr' 5 | import axios from 'axios' 6 | import { TASK } from '../types/Types' 7 | 8 | interface STATICPROPS { 9 | staticTasks: TASK[] 10 | } 11 | 12 | const axiosFetcher = async () => { 13 | const result = await axios.get( 14 | 'https://jsonplaceholder.typicode.com/todos/?_limit=10' 15 | ) 16 | return result.data 17 | } 18 | 19 | const TaskPage: React.FC = ({ staticTasks }) => { 20 | const { data: tasks, error } = useSWR('todosFetch', axiosFetcher, { 21 | fallbackData: staticTasks, 22 | revalidateOnMount: true, 23 | }) 24 | if (error) return Error! 25 | return ( 26 | 27 |

    todos page

    28 |
      29 | {tasks && 30 | tasks.map((task) => ( 31 |
    • 32 | {task.id} 33 | {': '} 34 | {task.title} 35 |
    • 36 | ))} 37 |
    38 |
    39 | ) 40 | } 41 | export default TaskPage 42 | 43 | export const getStaticProps: GetStaticProps = async () => { 44 | const staticTasks = await getAllTasksData() 45 | return { 46 | props: { staticTasks }, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GomaGoma676/nextjs-testing/3b010f4519390762a47d5c064fc5c6b0e3e61c12/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./pages/**/*.tsx', './components/**/*.tsx'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": [ 22 | "next-env.d.ts", 23 | "**/*.ts", 24 | "**/*.tsx" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /types/Types.ts: -------------------------------------------------------------------------------- 1 | export interface POST { 2 | userId: number 3 | id: number 4 | title: string 5 | body: string 6 | } 7 | export interface COMMENT { 8 | postId: number 9 | id: number 10 | name: string 11 | email: string 12 | body: string 13 | } 14 | export interface TASK { 15 | userId: number 16 | id: number 17 | title: string 18 | completed: boolean 19 | } 20 | --------------------------------------------------------------------------------