Fix clear localstorage after comment sending

* add functions for saving json to localstorage and tests
* remove comment form localstorage after posting
This commit is contained in:
Pavel Mineev
2020-04-02 16:25:37 -05:00
committed by Umputun
parent 10c1aa939f
commit 7de82e0fef
5 changed files with 260 additions and 33 deletions
+95
View File
@@ -0,0 +1,95 @@
import { setJsonItem, getJsonItem, updateJsonItem } from './local-storage';
const LS_KEY = 'test';
describe('getJsonItem', () => {
afterAll(() => {
localStorage.clear();
});
it('should set json to empty localStorage', () => {
setJsonItem<Record<string, string>>(LS_KEY, {});
expect(localStorage.getItem(LS_KEY)).toBe('{}');
});
it('should update json in localStoeage', () => {
setJsonItem<any[]>(LS_KEY, []);
expect(localStorage.getItem(LS_KEY)).toBe('[]');
});
});
describe('setJsonItem', () => {
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
consoleSpy = jest.spyOn(console, 'error').mockImplementation();
});
afterEach(() => {
localStorage.clear();
});
it('should return null when localStorage is empty', () => {
expect(getJsonItem(LS_KEY)).toBe(null);
});
it('should return value of key', () => {
localStorage.setItem(LS_KEY, JSON.stringify({}));
expect(getJsonItem(LS_KEY)).toEqual({});
localStorage.setItem(LS_KEY, JSON.stringify([]));
expect(getJsonItem(LS_KEY)).toEqual([]);
localStorage.setItem(LS_KEY, JSON.stringify(null));
expect(getJsonItem(LS_KEY)).toBe(null);
localStorage.setItem(LS_KEY, JSON.stringify(1));
expect(getJsonItem(LS_KEY)).toBe(1);
localStorage.setItem(LS_KEY, JSON.stringify(1));
expect(getJsonItem(LS_KEY)).toBe(1);
});
it('should return `null` if value in localStorage is not JSON', () => {
localStorage.setItem(LS_KEY, '"{:"""');
expect(getJsonItem(LS_KEY)).toBe(null);
expect(consoleSpy).toHaveBeenCalled();
localStorage.setItem(LS_KEY, 'asdas');
expect(getJsonItem(LS_KEY)).toBe(null);
expect(consoleSpy).toHaveBeenCalled();
});
});
describe('updateJsonItem', () => {
afterEach(() => {
localStorage.clear();
});
it('should set data to empty localStorage', () => {
updateJsonItem<Record<string, string>>(LS_KEY, {});
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({}));
});
it('should update object in localStorage', () => {
localStorage.setItem(LS_KEY, JSON.stringify({ x: 1 }));
updateJsonItem(LS_KEY, { y: 1 });
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({ x: 1, y: 1 }));
});
it('should update array in localStorage', () => {
localStorage.setItem(LS_KEY, JSON.stringify([1, 2, 3]));
updateJsonItem(LS_KEY, [4, 5, 6]);
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify([1, 2, 3, 4, 5, 6]));
});
it('should update data in localStorage with merge', () => {
localStorage.setItem(LS_KEY, JSON.stringify([3, 4, 5]));
updateJsonItem<any[]>(LS_KEY, data => [1, 2, ...data]);
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify([1, 2, 3, 4, 5]));
});
});
+49
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { IS_STORAGE_AVAILABLE } from './constants';
const failMessage = 'remark42: localStorage access denied, check browser preferences';
@@ -20,3 +21,51 @@ export const removeItem = IS_STORAGE_AVAILABLE
: () => {
console.error(failMessage); // eslint-disable-line no-console
};
export function getJsonItem<T = any>(key: string): T | null {
try {
const json = getItem(key);
if (json === null) {
return null;
}
const data = JSON.parse(json);
return data;
} catch (e) {
console.error(`remark42: error on read JSON from ${key} in localStorage`, e); // eslint-disable-line no-console
return null;
}
}
export function setJsonItem<T = any>(key: string, data: T) {
try {
setItem(key, JSON.stringify(data));
} catch (e) {
console.error(`remark42: error on parse JSON from ${key} in localStorage`, e); // eslint-disable-line no-console
}
}
export function updateJsonItem<T = Record<string, any> | any[]>(key: string, value: (data: T) => T): void;
export function updateJsonItem<T = any[]>(key: string, value: T): void;
export function updateJsonItem<T = Record<string, any>>(key: string, value: T) {
const savedData = getJsonItem<any>(key);
if (Array.isArray(value)) {
setJsonItem(key, [...savedData, ...value]);
return;
}
if (value !== null && typeof value === 'object') {
setJsonItem(key, { ...savedData, ...value });
return;
}
if (typeof value === 'function') {
setJsonItem(key, value(savedData));
return;
}
throw new Error(`remark42: error on update JSON for ${key} in localStorage`);
}
@@ -4,9 +4,20 @@ import { shallow } from 'enzyme';
import { user } from '@app/testUtils/mocks/user';
import { StaticStore } from '@app/common/static_store';
import { LS_SAVED_COMMENT_VALUE } from '@app/common/constants';
import * as localStorageModule from '@app/common/local-storage';
import { CommentForm, Props } from './comment-form';
import { SubscribeByEmail } from './__subscribe-by-email';
import TextareaAutosize from './textarea-autosize';
function createEvent<T = any>(type: string, value: T) {
const event = new Event(type);
Object.defineProperty(event, 'target', { value });
return event;
}
const DEFAULT_PROPS: Readonly<Omit<Props, 'intl'>> = {
mode: 'main',
@@ -50,4 +61,74 @@ describe('<CommentForm />', () => {
expect(wrapper.exists(SubscribeByEmail)).toEqual(false);
});
describe('initial value of comment', () => {
afterEach(() => {
localStorage.clear();
});
it('should has empty value', () => {
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 2: 'text' }));
const props = { ...DEFAULT_PROPS, user, intl };
const wrapper = shallow(<CommentForm {...props} />);
expect(wrapper.state('text')).toBe('');
expect(wrapper.find(TextareaAutosize).prop('value')).toBe('');
});
it('should get initial value from localStorage', () => {
const COMMENT_VALUE = 'text';
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: COMMENT_VALUE }));
const props = { ...DEFAULT_PROPS, user, intl };
const wrapper = shallow(<CommentForm {...props} />);
expect(wrapper.state('text')).toBe(COMMENT_VALUE);
expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE);
});
it('should get initial value from props instead localStorage', () => {
const COMMENT_VALUE = 'text from props';
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'text from localStorage' }));
const props = { ...DEFAULT_PROPS, user, intl, value: COMMENT_VALUE };
const wrapper = shallow(<CommentForm {...props} />);
expect(wrapper.state('text')).toBe(COMMENT_VALUE);
expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE);
});
});
describe('update value of comment in localStorage', () => {
afterEach(() => {
localStorage.clear();
});
it('should update value', () => {
const props = { ...DEFAULT_PROPS, user, intl };
const wrapper = shallow(<CommentForm {...props} />);
// @ts-ignore
const instance: CommentForm = wrapper.instance();
instance.onInput(createEvent('input', { value: '1' }));
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"1"}');
instance.onInput(createEvent('input', { value: '11' }));
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"11"}');
});
it('should clear value after send', async () => {
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ '1': 'asd' }));
const updateJsonItemSpy = jest.spyOn(localStorageModule, 'updateJsonItem');
const props = { ...DEFAULT_PROPS, user, intl };
const wrapper = shallow(<CommentForm {...props} />);
// @ts-ignore
const instance: CommentForm = wrapper.instance();
await instance.send(createEvent('send', { preventDefault: () => undefined }));
expect(updateJsonItemSpy).toHaveBeenCalled();
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe(JSON.stringify({}));
});
});
});
@@ -11,7 +11,7 @@ import { sleep } from '@app/utils/sleep';
import { replaceSelection } from '@app/utils/replaceSelection';
import { Button } from '@app/components/button';
import Auth from '@app/components/auth';
import { getItem, setItem } from '@app/common/local-storage';
import { getJsonItem, updateJsonItem } from '@app/common/local-storage';
import { LS_SAVED_COMMENT_VALUE } from '@app/common/constants';
import { SubscribeByEmail } from './__subscribe-by-email';
@@ -42,7 +42,7 @@ export interface Props {
intl: IntlShape;
}
interface State {
export interface State {
preview: string | null;
isErrorShown: boolean;
/** error message, if contains newlines, it will be splitted to multiple errors */
@@ -104,13 +104,16 @@ export class CommentForm extends Component<Props, State> {
textareaId = textareaId + 1;
this.textareaId = `textarea_${textareaId}`;
const savedCommentsJSON = getItem(LS_SAVED_COMMENT_VALUE);
let savedValue = '';
try {
if (typeof savedCommentsJSON === 'string') {
savedValue = JSON.parse(savedCommentsJSON)[this.props.id] || '';
}
} catch (e) {}
const savedComments = getJsonItem(LS_SAVED_COMMENT_VALUE);
let text = '';
if (savedComments !== null && savedComments[props.id]) {
text = savedComments[props.id];
}
if (props.value) {
text = props.value;
}
this.state = {
preview: null,
@@ -119,13 +122,11 @@ export class CommentForm extends Component<Props, State> {
errorLock: false,
isDisabled: false,
maxLength: StaticStore.config.max_comment_size,
text: props.value || savedValue,
text,
buttonText: null,
};
this.send = this.send.bind(this);
this.getPreview = this.getPreview.bind(this);
this.onInput = this.onInput.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.onDrop = this.onDrop.bind(this);
@@ -169,12 +170,10 @@ export class CommentForm extends Component<Props, State> {
}
}
onInput(e: Event) {
onInput = (e: Event) => {
const { value } = e.target as HTMLInputElement;
try {
setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ [this.props.id]: value }));
} catch (e) {}
updateJsonItem(LS_SAVED_COMMENT_VALUE, { [this.props.id]: value });
if (this.state.errorLock) {
this.setState({
@@ -183,13 +182,14 @@ export class CommentForm extends Component<Props, State> {
});
return;
}
this.setState({
isErrorShown: false,
errorMessage: null,
preview: null,
text: value,
});
}
};
async onPaste(e: ClipboardEvent) {
if (!(e.clipboardData && e.clipboardData.files.length > 0)) {
@@ -200,32 +200,35 @@ export class CommentForm extends Component<Props, State> {
await this.uploadImages(files);
}
send(e: Event) {
const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text;
const props = this.props;
send = async (e: Event) => {
const { text } = this.state;
if (e) e.preventDefault();
if (!text || !text.trim()) return;
if (text === this.props.value) {
this.props.onCancel && this.props.onCancel();
this.setState({ preview: null, text: '' });
}
this.setState({ isDisabled: true, isErrorShown: false, text });
try {
await this.props.onSubmit(text, pageTitle || document.title);
updateJsonItem<Record<string, string>>(LS_SAVED_COMMENT_VALUE, data => {
delete data[this.props.id];
props
.onSubmit(text, pageTitle || document.title)
.then(() => {
this.setState({ preview: null, text: '' });
})
.catch(e => {
const errorMessage = extractErrorMessageFromResponse(e, this.props.intl);
this.setState({ isErrorShown: true, errorMessage });
})
.finally(() => this.setState({ isDisabled: false }));
}
return data;
});
this.setState({ preview: null, text: '' });
} catch (e) {
this.setState({
isErrorShown: true,
errorMessage: extractErrorMessageFromResponse(e, this.props.intl),
});
}
this.setState({ isDisabled: false });
};
getPreview() {
const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text;
@@ -835,7 +835,6 @@ class Comment extends Component<Props, State> {
intl={this.props.intl}
user={props.user}
theme={props.theme}
value=""
mode="reply"
mix="comment__input"
onSubmit={(text, title) => this.addComment(text, title, o.id)}