Skip to content

Commit 7f91f66

Browse files
committed
More auto-save updates
1 parent 3650570 commit 7f91f66

9 files changed

Lines changed: 404 additions & 68 deletions

File tree

client/app/components/Form/__tests__/Form.spec.jsx

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
// @flow
22
import React from 'react';
3-
import { render, screen, fireEvent } from '@testing-library/react';
3+
import {
4+
render, screen, fireEvent, act,
5+
} from '@testing-library/react';
46
import userEvent from '@testing-library/user-event';
57
import { InputMocks } from 'mocks/InputMocks';
68
import Form from 'components/Form';
79

810
// TODO (julianguyen): Include InputTextarea after writing stubs for pell editor
911

12+
const STORAGE_KEY = 'ifme_autosave_fake-action';
13+
14+
const renderMinimalForm = () => render(
15+
<Form
16+
action="/fake-action"
17+
inputs={[
18+
InputMocks.inputTextProps,
19+
InputMocks.inputSwitchProps,
20+
InputMocks.inputSubmitProps,
21+
]}
22+
/>,
23+
);
24+
25+
const renderFullForm = () => render(
26+
<Form
27+
action="/fake-action"
28+
inputs={[
29+
InputMocks.inputTextProps,
30+
InputMocks.inputSelectProps,
31+
InputMocks.inputRadioProps,
32+
InputMocks.inputCheckboxProps,
33+
InputMocks.inputCheckboxGroupProps,
34+
InputMocks.inputMultiSelectProps,
35+
InputMocks.inputTagProps,
36+
InputMocks.inputSwitchProps,
37+
InputMocks.inputNumberProps,
38+
InputMocks.inputSubmitProps,
39+
]}
40+
/>,
41+
);
42+
1043
const getComponent = () => (
1144
<Form
1245
action="/fake-action"
@@ -64,6 +97,224 @@ describe('Form', () => {
6497
});
6598
});
6699

100+
describe('auto-save', () => {
101+
beforeEach(() => { localStorage.clear(); });
102+
afterEach(() => { localStorage.clear(); });
103+
104+
describe('interval', () => {
105+
beforeEach(() => { jest.useFakeTimers(); });
106+
afterEach(() => { jest.useRealTimers(); });
107+
108+
it('does not write to localStorage when nothing has changed from the initial form state', () => {
109+
renderMinimalForm();
110+
act(() => { jest.advanceTimersByTime(2000); });
111+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
112+
});
113+
114+
it('does not write to localStorage when only default/pre-filled values are present', () => {
115+
render(
116+
<Form
117+
action="/fake-action"
118+
inputs={[
119+
InputMocks.inputSelectProps,
120+
{ ...InputMocks.inputCheckboxProps, checked: true },
121+
InputMocks.inputSwitchProps,
122+
InputMocks.inputSubmitProps,
123+
]}
124+
/>,
125+
);
126+
act(() => { jest.advanceTimersByTime(2000); });
127+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
128+
});
129+
130+
it('saves to localStorage after the user changes a text field', () => {
131+
const { container } = renderMinimalForm();
132+
fireEvent.change(
133+
container.querySelector('input[name="some-text-name"]'),
134+
{ target: { value: 'Hello world' } },
135+
);
136+
act(() => { jest.advanceTimersByTime(2000); });
137+
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
138+
expect(saved?.values?.['some-text-name']).toBe('Hello world');
139+
});
140+
141+
it('saves to localStorage after the user changes a select', () => {
142+
const { container } = render(
143+
<Form
144+
action="/fake-action"
145+
inputs={[
146+
InputMocks.inputSelectProps,
147+
InputMocks.inputSubmitProps,
148+
]}
149+
/>,
150+
);
151+
fireEvent.change(
152+
container.querySelector('select[name="some-select-name"]'),
153+
{ target: { value: '1' } },
154+
);
155+
act(() => { jest.advanceTimersByTime(2000); });
156+
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
157+
expect(saved?.values?.['some-select-name']).toBe('1');
158+
});
159+
160+
it('does not re-save to localStorage after the form is submitted', () => {
161+
const { container } = renderMinimalForm();
162+
fireEvent.change(
163+
container.querySelector('input[name="some-text-name"]'),
164+
{ target: { value: 'Hello world' } },
165+
);
166+
fireEvent.submit(container.querySelector('form'));
167+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
168+
act(() => { jest.advanceTimersByTime(2000); });
169+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
170+
});
171+
});
172+
173+
describe('restore banner', () => {
174+
it('shows when localStorage contains a draft', () => {
175+
localStorage.setItem(
176+
STORAGE_KEY,
177+
JSON.stringify({ timestamp: Date.now(), values: { 'some-text-name': 'Saved text' } }),
178+
);
179+
renderMinimalForm();
180+
expect(screen.getByRole('status')).toBeInTheDocument();
181+
expect(screen.getByRole('button', { name: 'Restore draft' })).toBeInTheDocument();
182+
});
183+
184+
it('does not show when localStorage is empty', () => {
185+
renderMinimalForm();
186+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
187+
});
188+
189+
it('populates text fields with saved values when Restore draft is clicked', async () => {
190+
localStorage.setItem(
191+
STORAGE_KEY,
192+
JSON.stringify({ timestamp: Date.now(), values: { 'some-text-name': 'My restored text' } }),
193+
);
194+
renderMinimalForm();
195+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
196+
expect(screen.getByPlaceholderText('Some Text Placeholder')).toHaveValue('My restored text');
197+
});
198+
199+
it('hides the banner and removes the draft when Dismiss is clicked', async () => {
200+
localStorage.setItem(
201+
STORAGE_KEY,
202+
JSON.stringify({ timestamp: Date.now(), values: { 'some-text-name': 'Saved text' } }),
203+
);
204+
renderMinimalForm();
205+
await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
206+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
207+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
208+
});
209+
210+
it('removes the draft from localStorage when the form is submitted without errors', () => {
211+
localStorage.setItem(
212+
STORAGE_KEY,
213+
JSON.stringify({ timestamp: Date.now(), values: { 'some-text-name': 'Old draft' } }),
214+
);
215+
const { container } = renderMinimalForm();
216+
fireEvent.submit(container.querySelector('form'));
217+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
218+
});
219+
});
220+
221+
describe('restore: all input types', () => {
222+
it('restores a select to its saved option', async () => {
223+
localStorage.setItem(
224+
STORAGE_KEY,
225+
JSON.stringify({ timestamp: Date.now(), values: { 'some-select-name': '1' } }),
226+
);
227+
const { container } = renderFullForm();
228+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
229+
expect(container.querySelector('select[name="some-select-name"]').value).toBe('1');
230+
});
231+
232+
it('restores a radio group to its saved selection', async () => {
233+
localStorage.setItem(
234+
STORAGE_KEY,
235+
JSON.stringify({ timestamp: Date.now(), values: { 'some-radio-name': '2' } }),
236+
);
237+
const { container } = renderFullForm();
238+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
239+
const checkedRadio = container.querySelector('input[type="radio"][name="some-radio-name"]:checked');
240+
expect(checkedRadio).not.toBeNull();
241+
expect(checkedRadio.value).toBe('2');
242+
});
243+
244+
it('restores a single checkbox to its saved checked state', async () => {
245+
localStorage.setItem(
246+
STORAGE_KEY,
247+
JSON.stringify({ timestamp: Date.now(), values: { 'some-checkbox-name': '1' } }),
248+
);
249+
const { container } = renderFullForm();
250+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
251+
expect(container.querySelector('input[type="checkbox"][name="some-checkbox-name"]').checked).toBe(true);
252+
});
253+
254+
it('restores a checkbox group to its saved checked states', async () => {
255+
localStorage.setItem(
256+
STORAGE_KEY,
257+
JSON.stringify({
258+
timestamp: Date.now(),
259+
values: {
260+
'some-checkbox-one-name': '1',
261+
'some-checkbox-two-name': '0',
262+
},
263+
}),
264+
);
265+
const { container } = renderFullForm();
266+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
267+
expect(container.querySelector('input[type="checkbox"][name="some-checkbox-one-name"]').checked).toBe(true);
268+
expect(container.querySelector('input[type="checkbox"][name="some-checkbox-two-name"]').checked).toBe(false);
269+
});
270+
271+
it('restores a switch toggle to its saved on state', async () => {
272+
localStorage.setItem(
273+
STORAGE_KEY,
274+
JSON.stringify({ timestamp: Date.now(), values: { 'some-switch-name': 'true' } }),
275+
);
276+
const { container } = renderFullForm();
277+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
278+
expect(container.querySelector('input[type="checkbox"][name="some-switch-name"]').checked).toBe(true);
279+
});
280+
281+
it('restores a quickCreate widget to its saved selections', async () => {
282+
const quickCreateInput = {
283+
id: 'some-qc-id',
284+
type: 'quickCreate',
285+
name: 'some-qc-name[]',
286+
label: 'Some QC Label',
287+
placeholder: 'Search',
288+
checkboxes: [
289+
{
290+
id: 'qc-one', label: 'Option One', value: 1, checked: false,
291+
},
292+
{
293+
id: 'qc-two', label: 'Option Two', value: 2, checked: false,
294+
},
295+
],
296+
formProps: { action: '/test', inputs: [] },
297+
};
298+
localStorage.setItem(
299+
STORAGE_KEY,
300+
JSON.stringify({ timestamp: Date.now(), values: { 'some-qc-name[]': ['1'] } }),
301+
);
302+
const { container } = render(
303+
<Form
304+
action="/fake-action"
305+
inputs={[quickCreateInput, InputMocks.inputSubmitProps]}
306+
/>,
307+
);
308+
await userEvent.click(screen.getByRole('button', { name: 'Restore draft' }));
309+
const checkbox = container.querySelector(
310+
'input[type="checkbox"][name="some-qc-name[]"][value="1"]',
311+
);
312+
expect(checkbox).not.toBeNull();
313+
expect(checkbox.checked).toBe(true);
314+
});
315+
});
316+
});
317+
67318
describe('for changes on the input with number type', () => {
68319
it('has no errors when submit is clicked', async () => {
69320
render(getComponent());

0 commit comments

Comments
 (0)