diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 000000000..0051b4b26
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,4 @@
+# don't ever lint node_modules
+node_modules
+# don't lint build output (make sure it's set to your correct build folder name)
+dist
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 000000000..5ab990a65
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,36 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "jest": true,
+ "node": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "prettier",
+ "plugin:cypress/recommended"
+ ],
+ "plugins": [
+ "prettier"
+ ],
+ "parser": "@babel/eslint-parser",
+ "parserOptions": {
+ "requireConfigFile": false
+ },
+ "rules": {
+ "prettier/prettier": [
+ "error",
+ {
+ "singleQuote": true,
+ "semi": true,
+ "useTabs": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "bracketSpacing": true,
+ "arrowParens": "avoid",
+ "endOfLine": "auto"
+ }
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..75e8ef465
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,108 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# dependencies
+package-lock.json
+yarn-lock.json
\ No newline at end of file
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 000000000..0e7b77ba9
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,5 @@
+{
+ "testFiles": "*.test.js",
+ "screenshotOnRunFailure": false,
+ "video": false
+}
diff --git a/cypress/integration/app.test.js b/cypress/integration/app.test.js
new file mode 100644
index 000000000..5cca1524b
--- /dev/null
+++ b/cypress/integration/app.test.js
@@ -0,0 +1,137 @@
+import { ERROR_MESSAGE, MAX_GAME_TRY_COUNT } from '../../src/js/constants.js';
+import { inputCarNamesParsing } from '../../src/js/infrastructure/actions/inputSection.action.js';
+
+const BASE_URL = '../../index.html';
+
+// TODO: https://glebbahmutov.com/blog/form-validation-in-cypress/
+
+describe('Racing Car Game', () => {
+ beforeEach(() => {
+ cy.visit(BASE_URL);
+ });
+
+ describe('최초 렌더링 시', () => {
+ it('자동차 이름 입력창만 보여야 한다.', () => {
+ cy.$('[name="car-names-field"]').should('be.visible');
+ cy.$('[name="game-try-count-field"]').should('not.be.visible');
+ cy.$('[name="game-section"]').should('not.be.visible');
+ cy.$('[name="result-section"]').should('not.be.visible');
+ });
+ it('자동차 이름 입력창의 값은 비어 있어야 한다.', () => {
+ cy.$('[name="car-names"]').should('have.text', '');
+ });
+ });
+
+ describe('자동차 이름을 입력한 뒤 확인 버튼을 누른다.', () => {
+ it('자동차 이름 입력창이 비어 있다면 "자동차 이름을 입력해주세요!" 경고창을 출력한다.', () => {
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+ cy.get('[name="car-names-confirm"]')
+ .click()
+ .then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.REQUIRED_NAME);
+ });
+ });
+ it('자동차 이름은 쉼표로 구분된다.', () => {
+ const carNames = 'EAST, WEST, SOUTH, NORTH';
+ cy.inputCarNames(carNames).then(() => {
+ expect(inputCarNamesParsing(carNames)).to.have.length(4);
+ });
+ });
+ it('자동차 이름의 처음과 마지막에 쉼표가 존재하면 제거한다.', () => {
+ const carNames = ',EAST, WEST, SOUTH, NORTH,';
+ cy.inputCarNames(carNames).then(() => {
+ expect(inputCarNamesParsing(carNames)).to.have.length(4);
+ });
+ });
+
+ describe('입력된 자동차 이름들이 유효하지 않으면 에러를 출력한다.', () => {
+ it('자동차 이름이 하나라도 비어 있다면 "자동차 이름을 입력해주세요!" 경고창을 출력한다.', () => {
+ const carNames = 'EAST,, SOUTH, NORTH';
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.inputCarNames(carNames).then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.REQUIRED_NAME);
+ });
+ });
+ it('자동차 이름이 5자를 초과하면 "자동차 이름은 5자 이하여야만 해요!" 경고창을 출력한다.', () => {
+ const carNames = 'EAST, WEST, SOUTH, NORTH2222';
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.inputCarNames(carNames).then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.MUST_LESS_THAN);
+ });
+ });
+ it('중복된 자동차 이름이 존재하면 "자동차 이름은 중복될 수 없어요!" 경고창을 출력한다.', () => {
+ const carNames = 'EAST, EAST, SOUTH, NORTH';
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.inputCarNames(carNames).then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.NOT_ACCEPT_DUPLICATED);
+ });
+ });
+ });
+
+ describe('입력된 자동차 이름들이 유효한 경우 시도 횟수 입력창을 표시한다.', () => {
+ it('시도 횟수 입력창이 보여야 한다.', () => {
+ cy.inputCarNames('EAST, WEST, SOUTH, NORTH');
+ cy.$('[name="game-try-count-field"]').should('be.visible');
+ });
+ });
+ });
+
+ describe('시도 횟수를 입력한 뒤 확인 버튼을 누른다.', () => {
+ beforeEach(() => {
+ cy.inputCarNames('EAST, WEST, SOUTH, NORTH');
+ });
+
+ describe('입력된 시도 횟수가 유효하지 않으면 에러를 출력한다.', () => {
+ it('시도 횟수가 공백일 경우 "숫자를 입력해주세요!" 경고창을 출력한다.', () => {
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.get('[name="game-try-count-confirm"]')
+ .click()
+ .then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.REQUIRED_DIGIT);
+ });
+ });
+ it('시도 횟수가 음수일 경우 "시도 횟수는 0보다 커야 해요!" 경고창을 출력한다.', () => {
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.inputGameTryCount(0).then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.MUST_MORE_THAN_ONE);
+ });
+ });
+ it('시도 횟수가 문자일 경우 "숫자를 입력해주세요!" 경고창을 출력한다.', () => {
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.inputGameTryCount('오잉?!😳').then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.REQUIRED_DIGIT);
+ });
+ });
+ it(`시도 횟수가 ${MAX_GAME_TRY_COUNT}를 초과하면 "시도 횟수는 ${MAX_GAME_TRY_COUNT}보다 낮아야 해요!" 경고창을 출력한다.`, () => {
+ const alertStub = cy.stub();
+ cy.on('window:alert', alertStub);
+
+ cy.inputGameTryCount(MAX_GAME_TRY_COUNT + 1).then(() => {
+ expect(alertStub).to.be.calledWith(ERROR_MESSAGE.MUST_LESS_THAN_MAX_GAME_TRY_COUNT);
+ });
+ });
+ });
+ });
+
+ describe('주어진 시도 횟수 동안 n대의 자동차는 난수 값에 따라 전진/또는 멈출 수 있다.', () => {
+ it('난수 값이 4 이상인 경우 전진하고 3 이하의 값이라면 움직이지 않는다.', () => {});
+ });
+
+ describe('주어진 시도 횟수가 소진된 경우 가장 많이 전진한 자동차가 우승한다.', () => {
+ it('자동차가 중복인 경우 공동 우승한다.', () => {});
+ it('자동차가 하나인 경우 단독 우승한다.', () => {});
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 000000000..8229063ad
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+///