├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── test │ └── e2e │ ├── imageComparison.ts │ ├── initialize.ts │ ├── jest-puppeteer.config.js │ ├── jest.config.js │ ├── jest.image.ts │ └── tests │ ├── VisualRegression.test.ts │ └── __image_snapshots__ │ └── visual-regression-test-ts-visual-regression-test-1-snap.png ├── tsconfig.json └── yarn.lock /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # IDE 26 | .idea 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2022년 1월 우아한 테크세미나 데모자료 2 | 3 | 이 프로젝트는 react-create-app template --typescript 로 생성된 앱에 jest와 puppeteer를 이용해 시각적 회귀 테스트를 4 | 작성 해 둔 데모 프로젝트입니다. 5 | 6 | 클론 후 아래 명령어들을 실행해서 시각적 회귀 테스트를 수행할 수 있습니다. 7 | 8 | > 본 테스트 과정 수행에는 4321번 포트가 사용됩니다. 9 | 10 | 11 | ### 테스트 12 | ```shell 13 | $ yarn test-update 14 | ``` 15 | 위 스크립트를 실행하면 src/test/e2e/tests/__ image_snapshots __ 폴더 안에 저장된 기존 스크린샷과 현재 렌더링된 페이지를 비교합니다. 16 | 17 | 18 | ### 스냅샷 업데이트 19 | ```shell 20 | $ yarn test-update 21 | ``` 22 | 위 스크립트를 실행하면 src/test/e2e/tests/__ image_snapshots __ 폴더 안의 스크린샷을 무시하고 새 스크린샷을 저장합니다. 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visual-regression-test-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.0.2", 10 | "@types/jest-image-snapshot": "^4.3.1", 11 | "@types/node": "^16.11.21", 12 | "@types/react": "^17.0.38", 13 | "@types/react-dom": "^17.0.11", 14 | "cross-env": "^7.0.3", 15 | "jest": "^27.2.4", 16 | "jest-image-snapshot": "^4.5.1", 17 | "jest-puppeteer": "^6.0.0", 18 | "puppeteer": "^13.1.1", 19 | "ts-jest": "^27.1.3", 20 | "typescript": "^4.5.4" 21 | }, 22 | "dependencies": { 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-scripts": "5.0.0", 26 | "web-vitals": "^2.1.3" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "testdev": "export PORT=4321 && react-scripts start", 31 | "test": "cross-env JEST_PUPPETEER_CONFIG=./src/test/e2e/jest-puppeteer.config.js jest --config=./src/test/e2e/jest.config.js", 32 | "test-update": "cross-env JEST_PUPPETEER_CONFIG=./src/test/e2e/jest-puppeteer.config.js jest --config=./src/test/e2e/jest.config.js --updateSnapshot", 33 | "build": "react-scripts build", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueStragglr/visual-regression-test-demo/cccac0c5cf9e70b497be98fde5e789e11322c61a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueStragglr/visual-regression-test-demo/cccac0c5cf9e70b497be98fde5e789e11322c61a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueStragglr/visual-regression-test-demo/cccac0c5cf9e70b497be98fde5e789e11322c61a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.tsx and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/test/e2e/imageComparison.ts: -------------------------------------------------------------------------------- 1 | // ~src/test/e2e/imageComparison.ts 2 | 3 | import { MatchImageSnapshotOptions } from 'jest-image-snapshot' 4 | 5 | export const getSnapshotConfig: ( 6 | imageName?: string 7 | ) => MatchImageSnapshotOptions = (imageName) => { 8 | return { 9 | // 비교 이미지를 배열할 방향 10 | diffDirection: 'horizontal', 11 | // 콘솔에 발생한 차이를 표시할지 여부: Base64 데이터라 의미 없음 12 | dumpDiffToConsole: false, 13 | // 비교 방법 'pixelmatch' | 'ssim' 14 | comparisonMethod: 'pixelmatch', 15 | // 스냅샷 이름 정의 16 | customSnapshotIdentifier: imageName, 17 | // 비교 이미지 출력 경로 18 | customDiffDir: 'src/test/e2e/tests/__image_snapshots__/__diff_output__/' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/e2e/initialize.ts: -------------------------------------------------------------------------------- 1 | // ~src/test/e2d/initialize.ts 2 | 3 | import puppeteer from 'puppeteer' 4 | const HOST_BASE_URL = 'http://localhost:4321/' 5 | 6 | const initializeTest = async () => { 7 | // 크로미움 브라우저 실행 8 | const browser = await puppeteer.launch() 9 | // 크로미움 페이지 열기 10 | const page = await browser.newPage() 11 | // 페이지 뷰포트 사이즈 고정 12 | await page.setViewport({ 13 | width: 1200, 14 | height: 800, 15 | deviceScaleFactor: 1 16 | }) 17 | 18 | // 테스트용 페이지 로드 19 | const response: any = await page.goto(HOST_BASE_URL) 20 | // 페이지 정상로드 확인 21 | expect(response.status()).toBe(200) 22 | // 페이지 로드 완료 확인 23 | await page.waitForSelector('#root') 24 | 25 | return { 26 | page, 27 | // 종료시 크로미움 프로세스 종료를 위한 콜백을 함께 반환 28 | async cleanUp() { 29 | await page.close() 30 | await browser.close() 31 | } 32 | } 33 | } 34 | 35 | export default initializeTest 36 | -------------------------------------------------------------------------------- /src/test/e2e/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | // ~src/test/e2e/jest-puppeteer.config.js 2 | 3 | module.exports = { 4 | server: { 5 | // Jest 실행 시 서버 서빙을 위해 실행할 커맨드 6 | command: `npm run testdev`, 7 | // 서버 포트 번호 (package.json의 "testdev" 스크립트에 설정됨.) 8 | port: 4321, 9 | // 로컬호스트이므로 https가 아닌 http 사용 10 | protocol: 'http', 11 | // 서버 실행 타임아웃 12 | launchTimeout: 120000, 13 | debug: true 14 | }, 15 | launch: { 16 | // headless 모드 17 | headless: true, 18 | // 브라우저 실행 타임아웃 19 | timeout: 120000, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | // ~src/test/e2e/jest.config.js 2 | 3 | module.exports = { 4 | // 디렉토리 설정 5 | rootDir: '../../', 6 | roots: ['./test/e2e'], 7 | // 타임스크립트 컴파일 8 | transform: { '^.+\\.ts?$': 'ts-jest' }, 9 | // 테스트 코드 특정 10 | testMatch: ['**/?(*.)+(spec|test).ts'], 11 | // 테스트코드를 찾지 않을 경로 12 | testPathIgnorePatterns: ['/node_modules/', 'dist'], 13 | // 테스트 타임아웃 14 | testTimeout: 100000, 15 | // 개별 테스트 결과 표시 16 | verbose: true, 17 | // 프리셋(puppeteer 사용) 18 | preset: 'jest-puppeteer', 19 | // 테스트 셋업 후 실행할 스크립트 20 | setupFilesAfterEnv: ['./test/e2e/jest.image.ts'] 21 | } 22 | -------------------------------------------------------------------------------- /src/test/e2e/jest.image.ts: -------------------------------------------------------------------------------- 1 | // ~src/test/e2e/jest.image.ts 2 | 3 | import { toMatchImageSnapshot } from 'jest-image-snapshot' 4 | 5 | expect.extend({ toMatchImageSnapshot }) 6 | -------------------------------------------------------------------------------- /src/test/e2e/tests/VisualRegression.test.ts: -------------------------------------------------------------------------------- 1 | // ~src/test/e2e/tests.VisualRegression.test.ts 2 | 3 | import { getSnapshotConfig } from '../imageComparison' 4 | import initializeTest from '../initialize' 5 | 6 | it(`Visual Regression Test`, async () => { 7 | // 퍼페티어 페이지 초기화 & 콜백함수 가져오기 8 | const { page, cleanUp } = await initializeTest() 9 | 10 | // 스크린샷을 찍어서 11 | const image = await page.screenshot({ fullPage: true }) 12 | 13 | // Snapshot config 에 정의된 대로 비교 14 | const snapshotConfig = getSnapshotConfig() 15 | expect(image).toMatchImageSnapshot(snapshotConfig) 16 | 17 | await cleanUp() 18 | }) 19 | -------------------------------------------------------------------------------- /src/test/e2e/tests/__image_snapshots__/visual-regression-test-ts-visual-regression-test-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueStragglr/visual-regression-test-demo/cccac0c5cf9e70b497be98fde5e789e11322c61a/src/test/e2e/tests/__image_snapshots__/visual-regression-test-ts-visual-regression-test-1-snap.png -------------------------------------------------------------------------------- /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 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------