├── .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 |
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 |
--------------------------------------------------------------------------------