implements #4

This commit is contained in:
2019-04-13 20:58:07 +03:00
parent 4e5d6fea09
commit 2383f4288c
10 changed files with 219 additions and 50 deletions

View File

@@ -3,8 +3,13 @@ const { compilerOptions } = require("./tsconfig");
module.exports = {
preset: "ts-jest",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: "<rootDir>/",
}),
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/src/fileMock.ts",
"\\.(css|less|scss)$": "<rootDir>/src/styleMock.ts",
...pathsToModuleNameMapper(compilerOptions.paths, {
prefix: "<rootDir>/",
}),
},
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
};

View File

@@ -829,11 +829,11 @@
}
},
"@blueprintjs/core": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.15.0.tgz",
"integrity": "sha512-znXO0UaWBJO7Nm2qEvZzURCDuogsJ9DJlIw3N2XnIi+k/c8mQ1xlvtg0TT2pS88lMG9HUEKky3b0UQaSXBw4LA==",
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.15.1.tgz",
"integrity": "sha512-M8ltbqqlMZuZ6SEuqo/3Fr59ZcUfd8Er7ocbm7EACVfRW7dRhOCd/TKkf2kfICNtCDwznwXk0iAePLXZhUGtQg==",
"requires": {
"@blueprintjs/icons": "^3.7.0",
"@blueprintjs/icons": "^3.8.0",
"@types/dom4": "^2.0.1",
"classnames": "^2.2",
"dom4": "^2.0.1",
@@ -846,9 +846,9 @@
}
},
"@blueprintjs/icons": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.7.0.tgz",
"integrity": "sha512-608lQx1okOo85LNp7LyvsXWlgJjoQAiu29wULcktGuAuMmVXgueaGbMEDpuEMbu+uRlg9yqDhmk1eWm7W7R5ag==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.8.0.tgz",
"integrity": "sha512-yHaRQ3vfV9Gf3foZ4ONtxddz+u5ufkHqHj8Ia5VhPbFgG4el+cPdmsGGIIM72rgKS1KQa5Ay+ggjpByUlXvrKg==",
"requires": {
"classnames": "^2.2",
"tslib": "^1.9.0"
@@ -1347,9 +1347,9 @@
}
},
"@types/react-dom": {
"version": "16.8.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.3.tgz",
"integrity": "sha512-HF5hD5YR3z9Mn6kXcW1VKe4AQ04ZlZj1EdLBae61hzQ3eEWWxMgNLUbIxeZp40BnSxqY1eAYLsH9QopQcxzScA==",
"version": "16.8.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.4.tgz",
"integrity": "sha512-eIRpEW73DCzPIMaNBDP5pPIpK1KXyZwNgfxiVagb5iGiz6da+9A5hslSX6GAQKdO7SayVCS/Fr2kjqprgAvkfA==",
"dev": true,
"requires": {
"@types/react": "*"
@@ -1377,9 +1377,9 @@
}
},
"@types/react-router-dom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.1.tgz",
"integrity": "sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.2.tgz",
"integrity": "sha512-biesHodFxPgDxku2m08XwPeAfUYBcxAnrQG7pwFikuA3L2e3u2OKAb+Sb16bJuU3L5CTHd+Ivap+ke4mmGsHqQ==",
"dev": true,
"requires": {
"@types/history": "*",
@@ -9125,9 +9125,9 @@
}
},
"react-redux": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.0.1.tgz",
"integrity": "sha512-orSiI/QXtGiiJmf8lN/zVTx4hysFo/kGOsce28IUu/mu98AGemBwPTDzf64P4Vf/miRmevO8/w2RSw2awDd21w==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.0.2.tgz",
"integrity": "sha512-uKRuMgQt8dWbcz0U75oFK5tDo3boyAKrqvf/j94vpqRFFZfyDDy4kofUgloFIGyuKTq2Zz51zgK9RzOTFXk5ew==",
"requires": {
"@babel/runtime": "^7.4.3",
"hoist-non-react-statics": "^3.3.0",

View File

@@ -13,10 +13,10 @@
"@types/node-sass": "^4.11.0",
"@types/parcel-bundler": "^1.12.0",
"@types/react": "^16.8.13",
"@types/react-dom": "^16.8.3",
"@types/react-dom": "^16.8.4",
"@types/react-redux": "^7.0.6",
"@types/react-router": "^4.4.5",
"@types/react-router-dom": "^4.3.1",
"@types/react-router-dom": "^4.3.2",
"autoprefixer": "^9.5.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.12.1",
@@ -28,14 +28,14 @@
"ts-jest": "^24.0.2"
},
"dependencies": {
"@blueprintjs/core": "^3.15.0",
"@blueprintjs/icons": "^3.7.0",
"@blueprintjs/core": "^3.15.1",
"@blueprintjs/icons": "^3.8.0",
"http2": "^3.3.7",
"rea": "0.0.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-markdown": "^4.0.6",
"react-redux": "^7.0.1",
"react-redux": "^7.0.2",
"react-router": "^5.0.0",
"react-router-dom": "^5.0.0",
"react-spring": "^8.0.19",

View File

@@ -17,7 +17,7 @@ import {
import { IAppState } from "~redux/reducers";
export interface IDocumentEditComponentProps extends RouteComponentProps {
allDocs: IDocumentJSON[];
allDocs: { [key: number]: IDocumentJSON };
fetching: boolean;
spinner: boolean;
@@ -34,6 +34,11 @@ export interface IDocumentEditComponentState {
id: number | null;
name: string | null;
content: string | null;
savedName: string | null;
savedContent: string | null;
dirty: boolean;
}
const defaultDocumentEditComponentState: IDocumentEditComponentState = {
@@ -42,6 +47,11 @@ const defaultDocumentEditComponentState: IDocumentEditComponentState = {
id: null,
name: null,
content: null,
savedName: null,
savedContent: null,
dirty: false,
};
export class DocumentEditComponent extends React.PureComponent<
@@ -53,6 +63,7 @@ export class DocumentEditComponent extends React.PureComponent<
this.state = defaultDocumentEditComponentState;
this.handleInputChange = this.handleInputChange.bind(this);
this.onUnload = this.onUnload.bind(this);
}
public render() {
@@ -117,6 +128,11 @@ export class DocumentEditComponent extends React.PureComponent<
this.state.name,
this.state.content,
);
this.setState({
savedName: this.state.name,
savedContent: this.state.content,
dirty: false,
} as any);
}
public handleInputChange(
@@ -128,8 +144,21 @@ export class DocumentEditComponent extends React.PureComponent<
const value = target.value;
const name = target.name;
const { savedName, savedContent } = this.state;
const updDoc: { [key: string]: string } = {
name: this.state.name,
content: this.state.content,
};
updDoc[name] = value;
const dirty =
savedName !== updDoc.name || savedContent !== updDoc.content;
this.setState({
[name]: value,
dirty,
} as any);
}
@@ -139,6 +168,18 @@ export class DocumentEditComponent extends React.PureComponent<
public componentDidMount() {
this.tryLoad();
window.addEventListener("beforeunload", this.onUnload);
}
public componentWillUnmount() {
window.removeEventListener("beforeunload", this.onUnload);
}
public onUnload(e: BeforeUnloadEvent) {
if (this.state.dirty) {
e.preventDefault();
e.returnValue = "";
}
}
private tryLoad() {
@@ -159,6 +200,8 @@ export class DocumentEditComponent extends React.PureComponent<
id: doc.id,
name: doc.name,
content: doc.content,
savedContent: doc.content,
savedName: doc.name,
});
}
}

View File

@@ -12,7 +12,7 @@ import { fetchDocsStart } from "~redux/docs/actions";
import { IAppState } from "~redux/reducers";
export interface IDocumentViewComponentProps extends RouteComponentProps {
allDocs: IDocumentJSON[];
allDocs: { [key: number]: IDocumentJSON };
fetching: boolean;
spinner: boolean;

View File

@@ -0,0 +1,99 @@
import { mount } from "enzyme";
import * as React from "react";
import { DocumentEditComponent } from "../DocumentEdit";
const mock: any = jest.fn();
const onUnloadSpy = jest.spyOn(DocumentEditComponent.prototype, "onUnload");
const testDoc = {
1: {
name: "not changed",
content: "not changed",
id: 1,
user: 1,
createdAt: 0,
editedAt: 0,
},
};
afterEach(() => {
jest.restoreAllMocks();
});
describe("<DocumentEdit />", () => {
it("should warn before exiting with unsaved changes", () => {
// https://medium.com/@DavideRama/testing-global-event-listener-within-a-react-component-b9d661e59953
const map: { [key: string]: any } = {};
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
});
const wrapper = mount(
<DocumentEditComponent
allDocs={testDoc}
fetching={false}
spinner={false}
fetchDocs={mock}
cancelDelete={mock}
deleteDoc={mock}
updateDoc={mock}
history={mock}
location={mock}
match={{ params: { id: 1 } } as any}
/>,
);
const content = wrapper.find("textarea");
expect(content).toHaveLength(1);
(content.instance() as any).value = "changed";
content.simulate("change");
const preventDefault = jest.fn();
const event = { preventDefault };
map.beforeunload(event);
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onUnloadSpy).toHaveBeenCalledTimes(1);
});
it("shouldn't warn before exiting with no changes", () => {
const map: { [key: string]: any } = {};
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
});
const wrapper = mount(
<DocumentEditComponent
allDocs={testDoc}
fetching={false}
spinner={false}
fetchDocs={mock}
cancelDelete={mock}
deleteDoc={mock}
updateDoc={mock}
history={mock}
location={mock}
match={{ params: { id: 1 } } as any}
/>,
);
const content = wrapper.find("textarea");
expect(content).toHaveLength(1);
(content.instance() as any).value = "changed";
content.simulate("change");
(content.instance() as any).value = "not changed";
content.simulate("change");
const preventDefault = jest.fn();
const event = { preventDefault };
map.beforeunload(event);
expect(onUnloadSpy).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(0);
});
});

2
frontend/src/fileMock.ts Normal file
View File

@@ -0,0 +1,2 @@
module.exports = "test-file-stub";

View File

@@ -0,0 +1 @@
module.exports = {};

59
package-lock.json generated
View File

@@ -1569,11 +1569,12 @@
}
},
"eslint-plugin-import": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz",
"integrity": "sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==",
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.17.0.tgz",
"integrity": "sha512-JCsOtNwPYUoeZPlSr8t0+uCU5OVlHh+dIBn8Rw7FiOPjCECG+QzDIKDqshbyJE6CYoj9wpcstEl8vUY7rXkqVA==",
"dev": true,
"requires": {
"array-includes": "^3.0.3",
"contains-path": "^0.1.0",
"debug": "^2.6.9",
"doctrine": "1.5.0",
@@ -1583,7 +1584,7 @@
"lodash": "^4.17.11",
"minimatch": "^3.0.4",
"read-pkg-up": "^2.0.0",
"resolve": "^1.9.0"
"resolve": "^1.10.0"
},
"dependencies": {
"debug": {
@@ -1597,7 +1598,7 @@
},
"doctrine": {
"version": "1.5.0",
"resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
"integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
"dev": true,
"requires": {
@@ -1616,7 +1617,7 @@
},
"load-json-file": {
"version": "2.0.0",
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
"dev": true,
"requires": {
@@ -1662,6 +1663,15 @@
"read-pkg": "^2.0.0"
}
},
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
"integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -3104,9 +3114,9 @@
}
},
"mocha": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.2.tgz",
"integrity": "sha512-BgD2/RozoSC3uQK5R0isDcxjqaWw2n5HWdk8njYUyZf2NC79ErO5FtYVX52+rfqGoEgMfJf4fuG0IWh2TMzFoA==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.3.tgz",
"integrity": "sha512-QdE/w//EPHrqgT5PNRUjRVHy6IJAzAf1R8n2O8W8K2RZ+NbPfOD5cBDp+PGa2Gptep37C/TdBiaNwakppEzEbg==",
"dev": true,
"requires": {
"ansi-colors": "3.2.3",
@@ -3123,7 +3133,7 @@
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-environment-flags": "1.0.4",
"node-environment-flags": "1.0.5",
"object.assign": "4.1.0",
"strip-json-comments": "2.0.1",
"supports-color": "6.0.0",
@@ -3308,12 +3318,21 @@
}
},
"node-environment-flags": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz",
"integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
"integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
"dev": true,
"requires": {
"object.getownpropertydescriptors": "^2.0.3"
"object.getownpropertydescriptors": "^2.0.3",
"semver": "^5.7.0"
},
"dependencies": {
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
}
}
},
"node-notifier": {
@@ -3739,9 +3758,9 @@
"dev": true
},
"prettier": {
"version": "1.16.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz",
"integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==",
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz",
"integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==",
"dev": true
},
"prettier-linter-helpers": {
@@ -4565,9 +4584,9 @@
}
},
"typescript": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.2.tgz",
"integrity": "sha512-Og2Vn6Mk7JAuWA1hQdDQN/Ekm/SchX80VzLhjKN9ETYrIepBFAd8PkOdOTK2nKt0FCkmMZKBJvQ1dV1gIxPu/A==",
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
"integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==",
"dev": true
},
"unpipe": {

View File

@@ -23,12 +23,12 @@
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-import": "^2.17.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"mocha": "^6.1.2",
"prettier": "^1.16.4",
"mocha": "^6.1.3",
"prettier": "^1.17.0",
"supertest": "^4.0.2",
"ts-node": "8.0.3",
"ts-node-dev": "^1.0.0-pre.32",
@@ -37,7 +37,7 @@
"tslint-config-prettier": "^1.18.0",
"tslint-no-unused-expression-chai": "^0.1.4",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "3.4.2"
"typescript": "3.4.3"
},
"dependencies": {
"@koa/cors": "^3.0.0",