├── .gitignore
├── FUTURE.md
├── README.md
└── webapp
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── src
├── app
│ ├── app.tsx
│ ├── components
│ │ └── components.tsx
│ ├── service
│ │ ├── routing.ts
│ │ └── todoService.ts
│ ├── state
│ │ └── appState.ts
│ └── todomvc
│ │ └── css.ts
├── common
│ └── types.ts
└── server
│ └── server.ts
├── templates
└── index.html
├── tsconfig.json
├── tsconfig.server.json
├── tsconfig.webpack.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE
2 | .alm/
3 | .vscode/
4 | node_modules
5 |
--------------------------------------------------------------------------------
/FUTURE.md:
--------------------------------------------------------------------------------
1 | # Future Ideas
2 |
3 | ## Cypress Runner Dom Snapshots
4 | * Takes snapshots at each command execution
5 | * Takes before and after snapshots for things like XHR requests
6 | * Inspect snapshot with chorme-dev-tools
7 | * Use "select an element" toold in chrome-dev-tools.
8 | * Click a snapshot to pin it
9 |
10 | ## Mock Network requests and responses
11 |
12 | ## Test MobX application state
13 | https://github.com/basarat/typescript-book/issues/441
14 |
15 | ## Breakpoints
16 | * Open devtools
17 |
18 | Two ways
19 | * `debugger` statement in application code
20 | * Debugging test code
21 | * Debug command: https://docs.cypress.io/api/commands/debug.html#
22 | * `.then(()=> debugger)`
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Maintainable web application testing with cypress
2 | > Testing how it should be
3 |
4 | Docs / Code to an accompanying video course on cypress web application testing.
5 |
6 | ## Setup
7 | Setup the `webapp`:
8 | * cd webapp
9 | * `npm install`
10 | * `npm start`
11 |
12 | For any lesson:
13 | * cd `01` (e.g)
14 | * `npm install`
15 | * `npm start`
16 |
17 | ## Course
18 | * https://egghead.io/courses/maintainable-web-application-testing-with-cypress
19 |
20 | ## Lessons
21 | * Set up Cypress and TypeScript - Cypress can easily be integrated into any web application in its own dedicated folder. In this lesson we cover how to add cypress with TypeScript support into an existing application without touching any of the existing code. This provides you with a reproducible pattern that you can carry out in your web applications along with a copy pasteable starting point so you don’t need to repeat these steps again and again.
22 |
23 | * Command - Execution Separation in Cypress - Cypress works on top of commands. In this lesson we look at command / run separation along with best practices for chaining cypress commands. Cypress commands greatly improve your debugging experience with automatic logs and dom snapshots.
24 |
25 | * Implicit Assertions and Automatic Retries in Cypress - We also cover implicit assertions and automatic retries which further decrease the noise in your test code and simultaneously increase test stability.
26 |
27 | * Assert Behaviour with Cypress should Command - The should command is your key way to add assertions to cypress tests. It can be used to carry out chai / chai jquery assertions using chainers. In this lesson we take a look at this `.should` command.
28 |
29 | * Reuse Application Config in a Cypress Test - You can use any of your browser application code in cypress tests. This is because cypress tests run the same way your browser code runs. In this lesson we cover how to create reusable config modules that remove the brittle coupling between conventional E2E tests and application code.
30 |
31 | * Unit Test Application Code with Cypress - Cypress can also be used for unit testing. In this lesson we demonstrate testing a piece of utility application logic in isolation (without navigating to a url) in a cypress test.
32 |
33 | * Create and Interact with Reusable Page Objects in Cypress - A common testing convention is creating objects that provide a convenient handle for all the interactions that various tests need to do with a page. In this lesson we create a page object for cypress and then interact with it in tests.
34 |
35 | * Execute Multiple Cypress Commands in a Loop - In this lesson we look at cypress `each` command which can be used to carry out additional cypress commands in a loop.
36 |
--------------------------------------------------------------------------------
/webapp/.gitignore:
--------------------------------------------------------------------------------
1 | # Node
2 | node_modules
3 |
4 | # Build assets
5 | public/build
6 | public/index.html
7 | lib/
8 | db.json
9 |
10 | # IDE
11 | .alm
12 | .vscode
13 |
--------------------------------------------------------------------------------
/webapp/README.md:
--------------------------------------------------------------------------------
1 | # Webapp
2 |
3 | ## Setup
4 | * `npm install`
5 |
6 | ## Dev
7 | * `npm start`
8 |
9 | And visit `http://localhost:8080`
10 |
11 | # Contributing
12 | Guidance on depdendencies:
13 | * Transpiler:
14 | * typescript
15 | * Dev Frontend:
16 | * webpack
17 | * webpack-dev-server
18 | * ts-loader
19 | * html-webpack-plugin
20 | * clean-webpack-plugin
21 | * Dev Backend:
22 | * ts-node-dev
23 | * Dev both:
24 | * concurrently
25 | * Exec frontend:
26 | * react
27 | * react-dom
28 | * mobx
29 | * mobx-react
30 | * typestyle
31 | * csstips
32 | * Exec backend:
33 | * express
34 | * lowdb
35 | * Exec both:
36 | * axios
37 |
--------------------------------------------------------------------------------
/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-typescript",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/basarat/react-typescript.git"
8 | },
9 | "scripts": {
10 | "clean": "rm -rf ./node_modules package-lock.json && npm install",
11 | "build:check": "tsc -p .",
12 | "build:server": "tsc -p ./tsconfig.server.json",
13 | "build:webpack": "webpack -p",
14 | "build": "npm run build:check && npm run build:server && npm run build:webpack",
15 | "start:server": "ts-node-dev src/server/server.ts",
16 | "start:webpack": "webpack-dev-server -d --content-base ./public",
17 | "start": "concurrently \"npm run start:server\" \"npm run start:webpack\""
18 | },
19 | "dependencies": {
20 | "@types/express": "4.16.0",
21 | "@types/lowdb": "1.0.6",
22 | "@types/react": "16.7.13",
23 | "@types/react-dom": "16.0.11",
24 | "axios": "0.18.0",
25 | "clean-webpack-plugin": "1.0.0",
26 | "concurrently": "4.1.0",
27 | "cors": "2.8.5",
28 | "express": "4.16.4",
29 | "formstate": "1.0.2",
30 | "html-webpack-plugin": "3.2.0",
31 | "lowdb": "1.0.0",
32 | "mobx": "4.3.1",
33 | "mobx-react": "5.4.2",
34 | "react": "16.6.3",
35 | "react-dom": "16.6.3",
36 | "takeme": "0.11.1",
37 | "ts-loader": "5.3.1",
38 | "ts-node-dev": "1.0.0-pre.31",
39 | "typescript": "3.2.1",
40 | "typestyle": "2.0.1",
41 | "uuid": "3.3.2",
42 | "webpack": "4.27.1",
43 | "webpack-cli": "3.1.2",
44 | "webpack-dev-server": "^3.1.14"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/webapp/src/app/app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | import { loadTodoMVCCSS } from './todomvc/css';
5 | import { App } from './components/components';
6 |
7 | loadTodoMVCCSS();
8 |
9 | ReactDOM.render(
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/webapp/src/app/components/components.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { observer } from 'mobx-react';
3 | import { FieldState } from 'formstate';
4 | import { classNames } from '../todomvc/css';
5 | import { appState } from '../state/appState';
6 | import { routerState, link, routes } from '../service/routing';
7 |
8 | export const App: React.SFC<{}> = observer((props) => {
9 | return (
10 | <>
11 |
12 |
13 | {appState.hasTodos && }
14 | {appState.hasTodos && }
15 |
16 |
17 | >
18 | );
19 | });
20 |
21 | export const Header: React.SFC<{
22 | }> = observer(() => {
23 | const fieldState = appState.current;
24 |
25 | return ();
40 | });
41 |
42 | export const Main: React.SFC<{}> = observer(() => {
43 | return (
44 |
86 | );
87 | });
88 |
89 | export const Footer: React.SFC<{}> = observer(() => {
90 | return (
91 |
105 | );
106 | });
107 |
108 | export const Info = () => {
109 | return (
110 |
114 | );
115 | }
--------------------------------------------------------------------------------
/webapp/src/app/service/routing.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export const routes = {
4 | all: '/',
5 | active: '/active',
6 | completed: '/completed'
7 | }
8 |
9 | export type Routes =
10 | |'all'
11 | | 'active'
12 | | 'completed';
13 |
14 | export class RouterState {
15 | @observable
16 | route: Routes = 'all';
17 | }
18 |
19 | export const routerState = new RouterState();
20 |
21 | import { Router } from 'takeme';
22 | export { navigate, link } from 'takeme';
23 |
24 | new Router([
25 | {
26 | $: routes.active,
27 | enter: () => routerState.route = 'active'
28 | },
29 | {
30 | $: routes.completed,
31 | enter: () => routerState.route = 'completed'
32 | },
33 | {
34 | $: routes.all,
35 | enter: () => routerState.route = 'all',
36 | }
37 | ]).init();
38 |
--------------------------------------------------------------------------------
/webapp/src/app/service/todoService.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { TodoItem, API } from '../../common/types';
3 |
4 | export const apiRoot = 'http://localhost:3000/api';
5 |
6 | export const create = (body: API.create.Request) => {
7 | return axios
8 | .post(apiRoot + API.create.endpoint, body)
9 | .then(res => res.data);
10 | }
11 |
12 | export const getAll = () => {
13 | return axios.get(apiRoot + API.getAll.endpoint)
14 | .then(res => res.data);
15 | }
16 |
17 | export const setAll = (body: API.setAll.Request) => {
18 | return axios.put<{}>(apiRoot + API.setAll.endpoint, body)
19 | .then(res => res.data);
20 | }
21 |
--------------------------------------------------------------------------------
/webapp/src/app/state/appState.ts:
--------------------------------------------------------------------------------
1 | import { observable, action, computed } from 'mobx';
2 | import { FieldState } from 'formstate';
3 | import { TodoItem } from '../../common/types';
4 | import { getAll, create, setAll } from '../service/todoService';
5 | import { routerState } from '../service/routing';
6 |
7 |
8 | class AppState {
9 | constructor() {
10 | this.loadItems();
11 | }
12 |
13 | @observable
14 | items: TodoItem[] = [];
15 |
16 | @observable
17 | current = new FieldState('');
18 |
19 | @computed
20 | get hasTodos() {
21 | return this.items.length !== 0;
22 | }
23 |
24 | @computed
25 | get todoCount() {
26 | return this.items.filter(i => i.completed == false).length;
27 | }
28 |
29 | @computed
30 | get visibleList() {
31 | return routerState.route == 'all'
32 | ? this.items
33 | : routerState.route == 'active'
34 | ? this.items.filter(i => i.completed == false)
35 | : this.items.filter(i => i.completed == true)
36 | }
37 |
38 | @action
39 | async addCurrentItem() {
40 | if (this.current.value.trim() === '') return;
41 | const { id } = await create({ message: this.current.value });
42 | this.items.push({
43 | id,
44 | completed: false,
45 | message: this.current.value
46 | });
47 | this.current.onChange('');
48 | }
49 |
50 | @action
51 | async loadItems() {
52 | const { todos } = await getAll();
53 | this.items = todos;
54 | }
55 |
56 | @action
57 | async toggle(item: TodoItem) {
58 | item.completed = !item.completed;
59 | setAll({ todos: this.items });
60 | }
61 |
62 | @action
63 | async destroy(item: TodoItem) {
64 | this.items = this.items.filter(i => i.id !== item.id);
65 | setAll({ todos: this.items });
66 | }
67 |
68 | @action
69 | clearCompleted(): void {
70 | this.items = this.items.filter(i => i.completed == false);
71 | setAll({ todos: this.items });
72 | }
73 |
74 | @observable
75 | editingId: string | null = null;
76 | @observable
77 | editingTodoMessage: null | FieldState = null;
78 |
79 | @action
80 | setEditing(item: TodoItem) {
81 | this.editingId = item.id;
82 | this.editingTodoMessage = new FieldState(item.message);
83 | }
84 |
85 | @action
86 | cancelEditing() {
87 | this.editingId = null;
88 | this.editingTodoMessage = null;
89 | }
90 |
91 | @action
92 | async submitEditing() {
93 | const todo = this.items.find(i => i.id === this.editingId);
94 | todo.message = this.editingTodoMessage.value;
95 | setAll({ todos: this.items });
96 | this.cancelEditing();
97 | }
98 | }
99 |
100 | export const appState = new AppState();
101 |
--------------------------------------------------------------------------------
/webapp/src/app/todomvc/css.ts:
--------------------------------------------------------------------------------
1 |
2 | import { cssRaw } from 'typestyle';
3 | /**
4 | * https://raw.githubusercontent.com/tastejs/todomvc-app-css/master/index.css
5 | */
6 | export function loadTodoMVCCSS() {
7 | cssRaw(`
8 | html,
9 | body {
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | button {
15 | margin: 0;
16 | padding: 0;
17 | border: 0;
18 | background: none;
19 | font-size: 100%;
20 | vertical-align: baseline;
21 | font-family: inherit;
22 | font-weight: inherit;
23 | color: inherit;
24 | -webkit-appearance: none;
25 | appearance: none;
26 | -webkit-font-smoothing: antialiased;
27 | -moz-osx-font-smoothing: grayscale;
28 | }
29 |
30 | body {
31 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
32 | line-height: 1.4em;
33 | background: #f5f5f5;
34 | color: #4d4d4d;
35 | min-width: 230px;
36 | max-width: 550px;
37 | margin: 0 auto;
38 | -webkit-font-smoothing: antialiased;
39 | -moz-osx-font-smoothing: grayscale;
40 | font-weight: 300;
41 | }
42 |
43 | :focus {
44 | outline: 0;
45 | }
46 |
47 | .hidden {
48 | display: none;
49 | }
50 |
51 | .todoapp {
52 | background: #fff;
53 | margin: 130px 0 40px 0;
54 | position: relative;
55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
57 | }
58 |
59 | .todoapp input::-webkit-input-placeholder {
60 | font-style: italic;
61 | font-weight: 300;
62 | color: #e6e6e6;
63 | }
64 |
65 | .todoapp input::-moz-placeholder {
66 | font-style: italic;
67 | font-weight: 300;
68 | color: #e6e6e6;
69 | }
70 |
71 | .todoapp input::input-placeholder {
72 | font-style: italic;
73 | font-weight: 300;
74 | color: #e6e6e6;
75 | }
76 |
77 | .todoapp h1 {
78 | position: absolute;
79 | top: -155px;
80 | width: 100%;
81 | font-size: 100px;
82 | font-weight: 100;
83 | text-align: center;
84 | color: rgba(175, 47, 47, 0.15);
85 | -webkit-text-rendering: optimizeLegibility;
86 | -moz-text-rendering: optimizeLegibility;
87 | text-rendering: optimizeLegibility;
88 | }
89 |
90 | .new-todo,
91 | .edit {
92 | position: relative;
93 | margin: 0;
94 | width: 100%;
95 | font-size: 24px;
96 | font-family: inherit;
97 | font-weight: inherit;
98 | line-height: 1.4em;
99 | border: 0;
100 | color: inherit;
101 | padding: 6px;
102 | border: 1px solid #999;
103 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
104 | box-sizing: border-box;
105 | -webkit-font-smoothing: antialiased;
106 | -moz-osx-font-smoothing: grayscale;
107 | }
108 |
109 | .new-todo {
110 | padding: 16px 16px 16px 60px;
111 | border: none;
112 | background: rgba(0, 0, 0, 0.003);
113 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
114 | }
115 |
116 | .main {
117 | position: relative;
118 | z-index: 2;
119 | border-top: 1px solid #e6e6e6;
120 | }
121 |
122 | .toggle-all {
123 | width: 1px;
124 | height: 1px;
125 | border: none; /* Mobile Safari */
126 | opacity: 0;
127 | position: absolute;
128 | right: 100%;
129 | bottom: 100%;
130 | }
131 |
132 | .toggle-all + label {
133 | width: 60px;
134 | height: 34px;
135 | font-size: 0;
136 | position: absolute;
137 | top: -52px;
138 | left: -13px;
139 | -webkit-transform: rotate(90deg);
140 | transform: rotate(90deg);
141 | }
142 |
143 | .toggle-all + label:before {
144 | content: '❯';
145 | font-size: 22px;
146 | color: #e6e6e6;
147 | padding: 10px 27px 10px 27px;
148 | }
149 |
150 | .toggle-all:checked + label:before {
151 | color: #737373;
152 | }
153 |
154 | .todo-list {
155 | margin: 0;
156 | padding: 0;
157 | list-style: none;
158 | }
159 |
160 | .todo-list li {
161 | position: relative;
162 | font-size: 24px;
163 | border-bottom: 1px solid #ededed;
164 | }
165 |
166 | .todo-list li:last-child {
167 | border-bottom: none;
168 | }
169 |
170 | .todo-list li.editing {
171 | border-bottom: none;
172 | padding: 0;
173 | }
174 |
175 | .todo-list li.editing .edit {
176 | display: block;
177 | width: 506px;
178 | padding: 12px 16px;
179 | margin: 0 0 0 43px;
180 | }
181 |
182 | .todo-list li.editing .view {
183 | display: none;
184 | }
185 |
186 | .todo-list li .toggle {
187 | text-align: center;
188 | width: 40px;
189 | /* auto, since non-WebKit browsers doesn't support input styling */
190 | height: auto;
191 | position: absolute;
192 | top: 0;
193 | bottom: 0;
194 | margin: auto 0;
195 | border: none; /* Mobile Safari */
196 | -webkit-appearance: none;
197 | appearance: none;
198 | }
199 |
200 | .todo-list li .toggle {
201 | opacity: 0;
202 | }
203 |
204 | .todo-list li .toggle + label {
205 | /*
206 | Firefox requires '#' to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
207 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the '#' - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
208 | */
209 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
210 | background-repeat: no-repeat;
211 | background-position: center left;
212 | }
213 |
214 | .todo-list li .toggle:checked + label {
215 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
216 | }
217 |
218 | .todo-list li label {
219 | word-break: break-all;
220 | padding: 15px 15px 15px 60px;
221 | display: block;
222 | line-height: 1.2;
223 | transition: color 0.4s;
224 | }
225 |
226 | .todo-list li.completed label {
227 | color: #d9d9d9;
228 | text-decoration: line-through;
229 | }
230 |
231 | .todo-list li .destroy {
232 | display: none;
233 | position: absolute;
234 | top: 0;
235 | right: 10px;
236 | bottom: 0;
237 | width: 40px;
238 | height: 40px;
239 | margin: auto 0;
240 | font-size: 30px;
241 | color: #cc9a9a;
242 | margin-bottom: 11px;
243 | transition: color 0.2s ease-out;
244 | }
245 |
246 | .todo-list li .destroy:hover {
247 | color: #af5b5e;
248 | }
249 |
250 | .todo-list li .destroy:after {
251 | content: '×';
252 | }
253 |
254 | .todo-list li:hover .destroy {
255 | display: block;
256 | }
257 |
258 | .todo-list li .edit {
259 | display: none;
260 | }
261 |
262 | .todo-list li.editing:last-child {
263 | margin-bottom: -1px;
264 | }
265 |
266 | .footer {
267 | color: #777;
268 | padding: 10px 15px;
269 | height: 20px;
270 | text-align: center;
271 | border-top: 1px solid #e6e6e6;
272 | }
273 |
274 | .footer:before {
275 | content: '';
276 | position: absolute;
277 | right: 0;
278 | bottom: 0;
279 | left: 0;
280 | height: 50px;
281 | overflow: hidden;
282 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
283 | 0 8px 0 -3px #f6f6f6,
284 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
285 | 0 16px 0 -6px #f6f6f6,
286 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
287 | }
288 |
289 | .todo-count {
290 | float: left;
291 | text-align: left;
292 | }
293 |
294 | .todo-count strong {
295 | font-weight: 300;
296 | }
297 |
298 | .filters {
299 | margin: 0;
300 | padding: 0;
301 | list-style: none;
302 | position: absolute;
303 | right: 0;
304 | left: 0;
305 | }
306 |
307 | .filters li {
308 | display: inline;
309 | }
310 |
311 | .filters li a {
312 | color: inherit;
313 | margin: 3px;
314 | padding: 3px 7px;
315 | text-decoration: none;
316 | border: 1px solid transparent;
317 | border-radius: 3px;
318 | }
319 |
320 | .filters li a:hover {
321 | border-color: rgba(175, 47, 47, 0.1);
322 | }
323 |
324 | .filters li a.selected {
325 | border-color: rgba(175, 47, 47, 0.2);
326 | }
327 |
328 | .clear-completed,
329 | html .clear-completed:active {
330 | float: right;
331 | position: relative;
332 | line-height: 20px;
333 | text-decoration: none;
334 | cursor: pointer;
335 | }
336 |
337 | .clear-completed:hover {
338 | text-decoration: underline;
339 | }
340 |
341 | .info {
342 | margin: 65px auto 0;
343 | color: #bfbfbf;
344 | font-size: 10px;
345 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
346 | text-align: center;
347 | }
348 |
349 | .info p {
350 | line-height: 1;
351 | }
352 |
353 | .info a {
354 | color: inherit;
355 | text-decoration: none;
356 | font-weight: 400;
357 | }
358 |
359 | .info a:hover {
360 | text-decoration: underline;
361 | }
362 |
363 | /*
364 | Hack to remove background from Mobile Safari.
365 | Can't use it globally since it destroys checkboxes in Firefox
366 | */
367 | @media screen and (-webkit-min-device-pixel-ratio:0) {
368 | .toggle-all,
369 | .todo-list li .toggle {
370 | background: none;
371 | }
372 |
373 | .todo-list li .toggle {
374 | height: 40px;
375 | }
376 | }
377 |
378 | @media (max-width: 430px) {
379 | .footer {
380 | height: 50px;
381 | }
382 |
383 | .filters {
384 | bottom: 10px;
385 | }
386 | }
387 | `);
388 | }
389 |
390 |
391 | /**
392 | * Provided by the css above
393 | * Relevant html in https://github.com/tastejs/todomvc-app-template/blob/master/index.html
394 | */
395 | export const classNames = {
396 | /** root */
397 | app: 'todoapp',
398 |
399 | /** root:header */
400 | header: 'header',
401 | newTodo: 'new-todo',
402 |
403 | /** root:main */
404 | main: 'main',
405 | toggleAll: 'toggle-all',
406 | todoList: 'todo-list',
407 | completed: 'completed',
408 | editing: 'editing',
409 | view: 'view',
410 | toggle: 'toggle',
411 | destroy: 'destroy',
412 | edit: 'edit',
413 |
414 | /** root:footer */
415 | footer: 'footer',
416 | todoCount: 'todo-count',
417 | clearCompleted: 'clear-completed',
418 |
419 | /** root:footer:filters */
420 | filters: 'filters',
421 | selected: 'selected',
422 |
423 | /** root:info */
424 | info: 'info'
425 | }
426 |
--------------------------------------------------------------------------------
/webapp/src/common/types.ts:
--------------------------------------------------------------------------------
1 | export type TodoItem = {
2 | id: string,
3 | completed: boolean,
4 | message: string,
5 | }
6 |
7 |
8 | export namespace API {
9 | export namespace create {
10 | export const endpoint = '/add';
11 | export type Request = {
12 | message: string
13 | }
14 | export type Response = {
15 | id: string
16 | }
17 | }
18 |
19 | export namespace getAll {
20 | export const endpoint = '/get-all';
21 | export type Response = {
22 | todos: TodoItem[]
23 | }
24 | }
25 |
26 | export namespace setAll {
27 | export const endpoint = '/set-all';
28 | export type Request = {
29 | todos: TodoItem[]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/webapp/src/server/server.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import low from 'lowdb';
4 | import uuid from 'uuid/v4';
5 | import FileSync from 'lowdb/adapters/FileSync';
6 | import { TodoItem, API } from '../common/types';
7 |
8 | /**
9 | * Setup db
10 | */
11 | type DBSchema = {
12 | items: TodoItem[]
13 | }
14 | const adapter = new FileSync('db.json', {
15 | defaultValue: {
16 | items: []
17 | }
18 | });
19 | const db = low(adapter);
20 |
21 | /**
22 | * API server
23 | */
24 | const app = express();
25 | const api = express.Router();
26 |
27 | api.use(cors(), express.json());
28 | api.get(API.getAll.endpoint, (_, res) => {
29 | res.send({ todos: db.get('items') });
30 | });
31 | api.post(API.create.endpoint, (req, res: express.Response) => {
32 | const id = uuid();
33 | const request: API.create.Request = req.body;
34 | db.get('items')
35 | .push({
36 | id: id,
37 | completed: false,
38 | message: request.message
39 | })
40 | .write();
41 | res.send({ id });
42 | });
43 | api.put(API.setAll.endpoint, (req, res: express.Response) => {
44 | const request: API.setAll.Request = req.body;
45 | db.set('items', request.todos)
46 | .write();
47 | res.send({});
48 | });
49 |
50 | app.use('/api', api);
51 |
52 | /** Start */
53 | app.listen(3000, '0.0.0.0');
54 |
--------------------------------------------------------------------------------
/webapp/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/webapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "src"
4 | ],
5 | "compileOnSave": false,
6 | "compilerOptions": {
7 | "noEmit": true,
8 | "sourceMap": true,
9 | "module": "commonjs",
10 | "esModuleInterop": true,
11 | "resolveJsonModule": true,
12 | "experimentalDecorators": true,
13 | "target": "es5",
14 | "jsx": "react",
15 | "lib": [
16 | "dom",
17 | "es6"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/webapp/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "src/app"
5 | ],
6 | "compilerOptions": {
7 | "noEmit": false,
8 | "outDir": "./lib"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/webapp/tsconfig.webpack.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": false
5 | }
6 | }
--------------------------------------------------------------------------------
/webapp/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CleanWebpackPlugin = require('clean-webpack-plugin');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './src/app/app.tsx',
6 | plugins: [
7 | new CleanWebpackPlugin(['public/build']),
8 | new HtmlWebpackPlugin({
9 | template: 'templates/index.html'
10 | }),
11 | ],
12 | output: {
13 | path: __dirname + '/public',
14 | filename: 'build/[name].[contenthash].js'
15 | },
16 | resolve: {
17 | extensions: ['.ts', '.tsx', '.js']
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.tsx?$/,
23 | loader: 'ts-loader',
24 | options: {
25 | configFile: 'tsconfig.webpack.json'
26 | }
27 | }
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------