├── .dockerignore
├── .eslintignore
├── .eslintrc.yml
├── .github
└── workflows
│ └── CI.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── package-lock.json
├── package.json
├── playwright.config.js
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ ├── TodoPromo.gif
│ ├── application.scss
│ └── todoPreview.jpg
├── components
│ ├── App.jsx
│ ├── Footer.jsx
│ ├── Header.jsx
│ ├── Navbar.jsx
│ ├── ToDoItem.jsx
│ └── ToDoList.jsx
├── index.css
├── index.js
├── init.js
├── reportWebVitals.js
├── setupTests.js
└── slices
│ ├── filterInfoSlice.js
│ ├── index.js
│ └── tasksSlice.js
└── tests
├── app.test.js
└── example.spec.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 | Dockerfile
4 | logs
5 | tmp
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | dist
3 | build
4 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | es2021: true
4 |
5 | extends:
6 | - airbnb
7 | - plugin:react/recommended
8 | - plugin:functional/recommended
9 | - plugin:react-hooks/recommended
10 |
11 | parserOptions:
12 | ecmaFeatures:
13 | jsx: true
14 | ecmaVersion: latest
15 | sourceType: module
16 |
17 | plugins:
18 | - react
19 | - functional
20 |
21 | rules:
22 | import/extensions: 0
23 | import/no-unresolved: 0
24 | react/prop-types: 0
25 | no-console: 0
26 |
27 | react/react-in-jsx-scope: 0
28 | linebreak-style: ["error", "windows"]
29 | functional/no-conditional-statement: 0
30 | functional/no-expression-statement: 0
31 | functional/immutable-data: 0
32 | functional/functional-parameters: 0
33 | functional/no-try-statement: 0
34 | functional/no-throw-statement: 0
35 | no-underscore-dangle: [2, { "allow": ["__filename", "__dirname"] }]
36 | react/function-component-definition:
37 | [2, { "namedComponents": "arrow-function" }]
38 | testing-library/no-debug: 0
39 | react/jsx-filename-extension: [1, { "extensions": [".js", ".jsx"] }]
40 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: [ main, master ]
5 | pull_request:
6 | branches: [ main, master ]
7 | jobs:
8 | test:
9 | timeout-minutes: 60
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 16
16 | - name: Install dependencies
17 | run: npm ci
18 | - name: App setup
19 | run: make app-setup
20 | - name: Run linter & tests
21 | run: make app-check
22 | - uses: actions/upload-artifact@v3
23 | if: always()
24 | with:
25 | name: playwright-report
26 | path: playwright-report/
27 | retention-days: 30
28 |
--------------------------------------------------------------------------------
/.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 | /playwright-report
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | /test-results/
26 | /playwright-report/
27 | /playwright/.cache/
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json .
6 |
7 | COPY package-lock.json .
8 |
9 | RUN npm ci
10 |
11 | COPY . .
12 |
13 | # RUN npm build
14 |
15 | EXPOSE 3000
16 |
17 | # CMD ["npm", "start"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Misa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | npm ci
3 |
4 | start:
5 | npm start
6 |
7 | build:
8 | docker-compose build
9 |
10 | deploy:
11 | npm run deploy
12 |
13 | up:
14 | docker-compose up -d
15 |
16 | down:
17 | docker-compose down
18 |
19 | lint:
20 | npx eslint .
21 |
22 | linter-fix:
23 | npx eslint . --fix
24 |
25 | test:
26 | npx playwright test
27 |
28 | test-install:
29 | npx playwright install --with-deps
30 |
31 | app-install:
32 | docker-compose run --rm web make install
33 |
34 | app-lint:
35 | docker-compose run --rm --no-deps web make lint
36 |
37 | app-test:
38 | docker-compose run --rm web make test-install test
39 |
40 | app-check: app-lint app-test
41 |
42 | app-setup: build app-install
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React App
2 |
3 | 
4 |
5 | ### CI, tests and linter status:
6 | [](https://github.com/bogdan-ho/todo-list/actions/workflows/CI.yml)
7 | [](https://codeclimate.com/github/bogdan-ho/todo-list/maintainability)
8 |
9 |
10 | ## About
11 | ✅React Todo application built with React (with hooks), Redux (@redux/toolkit) with adaptive layout using Bootstrap + Formik + Playwright tests + Docker
12 |
13 |
14 | ## Getting Started
15 | - Clone a repository
16 | - Go to the working directory of the project `cd playwright-ts-redux-toolkit`
17 | - Run `make app-setup`
18 | - Run tests `make app-test`
19 | - Start the server `make up`
20 |
21 | ## Features
22 | - Adding, Editing, Deleting and marking Completed tasks
23 | - Filter tasks by All, Active, Completed
24 | - Clear all completed tasks by one click
25 | - Displaying the remaining tasks counter
26 |
27 | ## Demo
28 | Check out the demo project for a quick example of how React Todo application works.
29 |
30 | 
31 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | web:
5 | build: .
6 |
7 | image: bogdanho/todo-list
8 |
9 | container_name: todo-list
10 |
11 | restart: always
12 |
13 | stdin_open: true
14 |
15 | tty: true
16 |
17 | volumes:
18 | - ".:/app"
19 |
20 | ports:
21 | - 3000:3000
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-list",
3 | "version": "0.1.0",
4 | "description": "React ToDo list",
5 | "main": "src/index.js",
6 | "homepage": "https://bogdan-ho.github.io/todo-list/",
7 | "dependencies": {
8 | "@playwright/test": "^1.29.2",
9 | "@reduxjs/toolkit": "^1.9.1",
10 | "@testing-library/jest-dom": "^5.16.5",
11 | "@testing-library/react": "^13.4.0",
12 | "@testing-library/user-event": "^13.5.0",
13 | "bootstrap": "^5.2.3",
14 | "formik": "^2.2.9",
15 | "lodash": "^4.17.21",
16 | "react": "^18.2.0",
17 | "react-bootstrap": "^2.7.0",
18 | "react-dom": "^18.2.0",
19 | "react-redux": "^8.0.5",
20 | "react-scripts": "5.0.1",
21 | "sass": "^1.57.1",
22 | "web-vitals": "^2.1.4",
23 | "yup": "^0.32.11"
24 | },
25 | "scripts": {
26 | "predeploy" : "npm run build",
27 | "deploy" : "gh-pages -d build",
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | },
45 | "devDependencies": {
46 | "eslint": "^8.31.0",
47 | "eslint-config-airbnb": "^19.0.4",
48 | "eslint-plugin-functional": "^4.4.1",
49 | "eslint-plugin-import": "^2.26.0",
50 | "eslint-plugin-jsx-a11y": "^6.6.1",
51 | "eslint-plugin-react": "^7.31.11",
52 | "eslint-plugin-react-hooks": "^4.6.0",
53 | "gh-pages": "^4.0.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { devices } from '@playwright/test';
3 |
4 | /**
5 | * Read environment variables from file.
6 | * https://github.com/motdotla/dotenv
7 | */
8 | // require('dotenv').config();
9 |
10 | /**
11 | * @see https://playwright.dev/docs/test-configuration
12 | * @type {import('@playwright/test').PlaywrightTestConfig}
13 | */
14 | const config = {
15 | testDir: './tests',
16 | /* Maximum time one test can run for. */
17 | timeout: 30 * 1000,
18 | expect: {
19 | /**
20 | * Maximum time expect() should wait for the condition to be met.
21 | * For example in `await expect(locator).toHaveText();`
22 | */
23 | timeout: 5000,
24 | },
25 | /* Run tests in files in parallel */
26 | fullyParallel: true,
27 | /* Fail the build on CI if you accidentally left test.only in the source code. */
28 | forbidOnly: !!process.env.CI,
29 | /* Retry on CI only */
30 | retries: process.env.CI ? 2 : 0,
31 | /* Opt out of parallel tests on CI. */
32 | workers: process.env.CI ? 1 : undefined,
33 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
34 | reporter: 'html',
35 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
36 | use: {
37 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
38 | actionTimeout: 0,
39 | /* Base URL to use in actions like `await page.goto('/')`. */
40 | baseURL: 'http://localhost:3000',
41 |
42 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
43 | trace: 'on-first-retry',
44 | },
45 |
46 | /* Configure projects for major browsers */
47 | projects: [
48 | {
49 | name: 'chromium',
50 | use: {
51 | ...devices['Desktop Chrome'],
52 | },
53 | },
54 |
55 | // {
56 | // name: 'firefox',
57 | // use: {
58 | // ...devices['Desktop Firefox'],
59 | // },
60 | // },
61 |
62 | // {
63 | // name: 'webkit',
64 | // use: {
65 | // ...devices['Desktop Safari'],
66 | // },
67 | // },
68 |
69 | /* Test against mobile viewports. */
70 | // {
71 | // name: 'Mobile Chrome',
72 | // use: {
73 | // ...devices['Pixel 5'],
74 | // },
75 | // },
76 | // {
77 | // name: 'Mobile Safari',
78 | // use: {
79 | // ...devices['iPhone 12'],
80 | // },
81 | // },
82 |
83 | /* Test against branded browsers. */
84 | // {
85 | // name: 'Microsoft Edge',
86 | // use: {
87 | // channel: 'msedge',
88 | // },
89 | // },
90 | // {
91 | // name: 'Google Chrome',
92 | // use: {
93 | // channel: 'chrome',
94 | // },
95 | // },
96 | ],
97 |
98 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */
99 | // outputDir: 'test-results/',
100 |
101 | /* Run your local dev server before starting the tests */
102 | webServer: {
103 | command: 'npm run start',
104 | port: 3000,
105 | timeout: 120000,
106 | },
107 | };
108 |
109 | export default config;
110 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mm0hammadi/playwright-ts-redux-toolkit/3a42087a9f0dcbd0e318d8b0fb7bb1081cbedc39/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | sample
11 |
12 |
13 |
14 |
15 |
16 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/assets/TodoPromo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mm0hammadi/playwright-ts-redux-toolkit/3a42087a9f0dcbd0e318d8b0fb7bb1081cbedc39/src/assets/TodoPromo.gif
--------------------------------------------------------------------------------
/src/assets/application.scss:
--------------------------------------------------------------------------------
1 | @import '~bootstrap/scss/bootstrap';
2 |
--------------------------------------------------------------------------------
/src/assets/todoPreview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mm0hammadi/playwright-ts-redux-toolkit/3a42087a9f0dcbd0e318d8b0fb7bb1081cbedc39/src/assets/todoPreview.jpg
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 | import React from "react";
3 | import "../assets/application.scss";
4 | import Header from "./Header";
5 | import ToDoList from "./ToDoList";
6 | import Footer from "./Footer";
7 | // import Navbar from "./Navbar";
8 |
9 | const App = () => (
10 |
11 | {/* */}
12 |
13 |
14 |
15 |
16 | );
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from 'react-bootstrap/Button';
3 | import ButtonGroup from 'react-bootstrap/ButtonGroup';
4 | import { useDispatch, useSelector } from 'react-redux';
5 |
6 | import { actions as filterInfoActions, filterInfoSelector } from '../slices/filterInfoSlice';
7 | import { actions as tasksActions, selectCompletedTasks, selectActiveTasks } from '../slices/tasksSlice';
8 |
9 | const Footer = () => {
10 | const completedTasks = useSelector(selectCompletedTasks);
11 | const activeTasks = useSelector(selectActiveTasks);
12 |
13 | const { nowShowing } = useSelector(filterInfoSelector);
14 | const dispatch = useDispatch();
15 |
16 |
17 | const handleShowAll = () => {
18 | dispatch(filterInfoActions.showAll());
19 | };
20 | const handleShowActive = () => {
21 | dispatch(filterInfoActions.showActive());
22 | };
23 | const handleShowCompleted = () => {
24 | dispatch(filterInfoActions.showCompleted());
25 | };
26 | const handleClearCompletedTasks = () => {
27 | const completedTasksIds = completedTasks.map((task) => task.id);
28 | dispatch(tasksActions.removeAllCompleted(completedTasksIds));
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 | {activeTasks.length}
38 | {' '}
39 | items left
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default Footer;
55 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { FloatingLabel, Form } from 'react-bootstrap/esm';
3 | import { useDispatch } from 'react-redux';
4 | import _uniqueId from 'lodash/uniqueId';
5 | import { useFormik } from 'formik';
6 | import * as yup from 'yup';
7 | import { actions } from '../slices/tasksSlice';
8 |
9 | const Header = () => {
10 | const dispatch = useDispatch();
11 |
12 | const inputRef = useRef();
13 | useEffect(() => {
14 | inputRef.current.focus();
15 | }, []);
16 |
17 | const handleSubmit = (value, { resetForm }) => {
18 | const task = value.body;
19 |
20 | dispatch(actions.addTask({ id: _uniqueId(), task, completed: false }));
21 | resetForm();
22 | };
23 |
24 | const schema = yup.object().shape({
25 | body: yup.string().min(3, 'Must be at least 3 characters'),
26 | });
27 |
28 | const formik = useFormik({ onSubmit: handleSubmit, validationSchema: schema, initialValues: { body: '' } });
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
todos
38 |
39 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default Header;
68 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavItem, Nav } from 'react-bootstrap';
3 |
4 | // eslint-disable-next-line react/function-component-definition
5 | export default function Navbar() {
6 | return (
7 |
8 |
9 |
10 |
11 | React-Bootstrap
12 |
13 |
14 |
15 |
16 |
24 |
32 |
33 |
34 | ;
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ToDoItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import InputGroup from 'react-bootstrap/InputGroup';
5 | import { useDispatch } from 'react-redux';
6 | import { actions } from '../slices/tasksSlice';
7 |
8 | const ToDoItem = ({ value }) => {
9 | const [removeButton, setRemoveButton] = useState(false);
10 | const { id, task, completed } = value;
11 | const [currentTask, setCurrentTask] = useState(task);
12 |
13 | const dispatch = useDispatch();
14 | const handleCompleteTask = (taskId) => () => {
15 | dispatch(actions.updateTask({ id: taskId, changes: { completed: !completed } }));
16 | };
17 | const handleChange = (event) => {
18 | setCurrentTask(event.target.value);
19 | };
20 | const handleChangeTask = (taskId) => (event) => {
21 | dispatch(actions.updateTask({ id: taskId, changes: { task: event.target.value } }));
22 | };
23 | const handleRemoveTask = (taskId) => () => {
24 | dispatch(actions.removeTask(taskId));
25 | };
26 |
27 | return (
28 | setRemoveButton(true)} onMouseOut={() => setRemoveButton(false)}>
29 |
35 |
42 | {removeButton
43 | ? (
44 |
47 | )
48 | : null}
49 |
50 | );
51 | };
52 |
53 | export default ToDoItem;
54 |
--------------------------------------------------------------------------------
/src/components/ToDoList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ListGroup } from 'react-bootstrap/esm';
3 | import { useSelector } from 'react-redux';
4 | import _uniqueId from 'lodash/uniqueId';
5 |
6 | import { tasksSelector } from '../slices/tasksSlice';
7 | import ToDoItem from './ToDoItem';
8 | import { filterInfoSelector } from '../slices/filterInfoSlice';
9 |
10 | const ToDoList = () => {
11 | const { nowShowing } = useSelector(filterInfoSelector);
12 | const tasksForRender = useSelector(tasksSelector.selectAll)
13 | .filter((task) => {
14 | switch (nowShowing) {
15 | case 'completed':
16 | return task.completed === true;
17 | case 'active':
18 | return task.completed === false;
19 | default:
20 | return true;
21 | }
22 | });
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | {tasksForRender.map((task) => (
30 |
31 | ))}
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default ToDoList;
41 |
--------------------------------------------------------------------------------
/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.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 |
3 | import './index.css';
4 | import init from './init';
5 |
6 | const root = ReactDOM.createRoot(document.getElementById('root'));
7 | root.render(init());
8 |
--------------------------------------------------------------------------------
/src/init.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import App from './components/App';
4 |
5 | import store from './slices/index';
6 |
7 | const init = () => (
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default init;
16 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({
4 | getCLS, getFID, getFCP, getLCP, getTTFB,
5 | }) => {
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.js:
--------------------------------------------------------------------------------
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/slices/filterInfoSlice.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { createSlice } from '@reduxjs/toolkit';
3 |
4 | const initialState = { nowShowing: 'all' };
5 |
6 | const filterInfoSlice = createSlice({
7 | name: 'filterInfo',
8 | initialState,
9 | reducers: {
10 | showAll: (state) => {
11 | state.nowShowing = 'all';
12 | },
13 | showCompleted: (state) => {
14 | state.nowShowing = 'completed';
15 | },
16 | showActive: (state) => {
17 | state.nowShowing = 'active';
18 | },
19 | },
20 | });
21 |
22 | export const filterInfoSelector = (state) => state.filterInfo;
23 |
24 | export const { actions } = filterInfoSlice;
25 | export default filterInfoSlice.reducer;
26 |
--------------------------------------------------------------------------------
/src/slices/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import filterInfoSlice from './filterInfoSlice';
3 | import tasksSlice from './tasksSlice';
4 |
5 | const store = configureStore({
6 | reducer: {
7 | tasks: tasksSlice,
8 | filterInfo: filterInfoSlice,
9 | },
10 | });
11 |
12 | export default store;
13 |
--------------------------------------------------------------------------------
/src/slices/tasksSlice.js:
--------------------------------------------------------------------------------
1 | import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
2 |
3 | const tasksAdapter = createEntityAdapter();
4 | const initialState = tasksAdapter.getInitialState();
5 |
6 | const tasksSlice = createSlice({
7 | name: 'tasks',
8 | initialState,
9 | reducers: {
10 | addTask: tasksAdapter.addOne,
11 | removeTask: tasksAdapter.removeOne,
12 | updateTask: tasksAdapter.updateOne,
13 | removeAllCompleted: tasksAdapter.removeMany,
14 | },
15 | });
16 |
17 | export const tasksSelector = tasksAdapter.getSelectors((state) => state.tasks);
18 | export const selectCompletedTasks = (state) => tasksSelector
19 | .selectAll(state)
20 | .filter((task) => task.completed === true);
21 | export const selectActiveTasks = (state) => tasksSelector
22 | .selectAll(state)
23 | .filter((task) => task.completed === false);
24 |
25 | export const { actions } = tasksSlice;
26 | export default tasksSlice.reducer;
27 |
--------------------------------------------------------------------------------
/tests/app.test.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 |
5 | test.describe('todo', () => {
6 | test.beforeEach(async ({ page }) => {
7 | await page.goto('http://localhost:3000');
8 | await page.waitForTimeout(300);
9 | await page.locator('[aria-label="new task"]').first().type('hello');
10 | await page.keyboard.press('Enter');
11 | expect(await page.$('input[value="hello"]')).not.toBeNull();
12 | });
13 |
14 | test('add task', async ({ page }) => {
15 | await page.locator('[aria-label="new task"]').first().type('new task');
16 | await page.keyboard.press('Enter');
17 | expect(await page.$('input[value="new task"]')).not.toBeNull();
18 | });
19 |
20 | test('complete task', async ({ page }) => {
21 | await page.locator('.form-check-input').first().check();
22 | expect(await page.$('input[disabled]')).not.toBeNull();
23 | });
24 |
25 | test('delete task', async ({ page }) => {
26 | await page.locator('input[value="hello"]').hover();
27 | await page.locator('.btn-danger').first().click();
28 | expect(await page.$('input[value="hello"]')).toBeNull();
29 | });
30 |
31 | test('update task', async ({ page }) => {
32 | await page.locator('input[value="hello"]').fill('new task');
33 | expect(await page.$('input[value="new task"]')).not.toBeNull();
34 | });
35 |
36 | test('add task less 3 characters', async ({ page }) => {
37 | await page.locator('[aria-label="new task"]').first().type('ta');
38 | await page.keyboard.press('Enter');
39 | expect(await page.$('.invalid-feedback')).not.toBeNull();
40 | });
41 |
42 | test('tasks left', async ({ page }) => {
43 | await page.locator('[aria-label="new task"]').first().type('new task');
44 | await page.keyboard.press('Enter');
45 | await expect(page.locator('[aria-label="tasks left"]')).toContainText('2 items left');
46 | });
47 |
48 | test('clear completed', async ({ page }) => {
49 | await page.locator('[aria-label="new task"]').first().type('new task');
50 | await page.keyboard.press('Enter');
51 |
52 | await page.locator('.form-check-input').first().check();
53 | await page.locator('.form-check-input').locator('nth=1').check();
54 |
55 | expect(await page.$('input[value="hello"]')).not.toBeNull();
56 | expect(await page.$('input[value="new task"]')).not.toBeNull();
57 |
58 | await page.locator('text=Clear completed').first().click();
59 |
60 | expect(await page.$('input[value="hello"]')).toBeNull();
61 | expect(await page.$('input[value="new task"]')).toBeNull();
62 | });
63 |
64 | test('show only active tasks', async ({ page }) => {
65 | await page.locator('[aria-label="new task"]').first().type('new task');
66 | await page.keyboard.press('Enter');
67 |
68 | await page.locator('.form-check-input').first().check();
69 | await page.locator('text=Active').first().click();
70 |
71 | expect(await page.$('input[value="hello"]')).toBeNull();
72 | });
73 |
74 | test('show only completed tasks', async ({ page }) => {
75 | await page.locator('[aria-label="new task"]').first().type('new task');
76 | await page.keyboard.press('Enter');
77 |
78 | await page.locator('.form-check-input').first().check();
79 | await page.locator('text=Completed').first().click();
80 |
81 | expect(await page.$('input[value="hello"]')).not.toBeNull();
82 | expect(await page.$('input[value="new task"]')).toBeNull();
83 | });
84 |
85 | test('show all tasks', async ({ page }) => {
86 | await page.locator('[aria-label="new task"]').first().type('new task');
87 | await page.keyboard.press('Enter');
88 |
89 | await page.locator('.form-check-input').first().check();
90 | await page.locator('text=Completed').first().click();
91 |
92 | expect(await page.$('input[value="hello"]')).not.toBeNull();
93 | expect(await page.$('input[value="new task"]')).toBeNull();
94 |
95 | await page.locator('text=All').first().click();
96 | expect(await page.$('input[value="new task"]')).not.toBeNull();
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/tests/example.spec.js:
--------------------------------------------------------------------------------
1 | // // @ts-check
2 | // import { test, expect } from '@playwright/test';
3 |
4 |
5 |
6 | // test('has title', async ({ page }) => {
7 | // await page.goto('https://playwright.dev/');
8 |
9 | // // Expect a title "to contain" a substring.
10 | // await expect(page).toHaveTitle(/Playwright/);
11 | // });
12 |
13 | // test('get started link', async ({ page }) => {
14 | // await page.goto('https://playwright.dev/');
15 |
16 | // // Click the get started link.
17 | // await page.getByRole('link', { name: 'Get started' }).click();
18 |
19 | // // Expects the URL to contain intro.
20 | // await expect(page).toHaveURL(/.*intro/);
21 | // });
22 |
--------------------------------------------------------------------------------