mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-29 00:17:48 +01:00
implements #4
This commit is contained in:
@@ -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"],
|
||||
};
|
||||
|
||||
32
frontend/package-lock.json
generated
32
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
99
frontend/src/Documents/tests/DocumentEdit.test.tsx
Normal file
99
frontend/src/Documents/tests/DocumentEdit.test.tsx
Normal 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
2
frontend/src/fileMock.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
module.exports = "test-file-stub";
|
||||
|
||||
1
frontend/src/styleMock.ts
Normal file
1
frontend/src/styleMock.ts
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
Reference in New Issue
Block a user