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 = {};