diff --git a/app/editor/note-editor.tsx b/app/editor/note-editor.tsx index 34bef6a..3c5bc9d 100644 --- a/app/editor/note-editor.tsx +++ b/app/editor/note-editor.tsx @@ -36,10 +36,10 @@ const Metadata = () => { {save !== "idle" && ( - + {save === "saving" && "Saving..."} {save === "saved" && "Saved."} - + )} ); @@ -92,19 +92,20 @@ export const NoteEditor = () => { SystemStore.setState((c) => ({ ...c, save: "saved" })); }, 100); - /** - * Listen to store changes. If the note changes we want to be able to - * debounce the save on Indexed DB. - */ - const { unsubscribe } = NoteStore.subscribe(() => { - SystemStore.setState((c) => ({ ...c, save: "saving" })); - debouncedSave.debouncedFn(); - }); - /** * Mount effect on load. */ useEffect(() => { + /** + * Listen to store changes. If the note changes we want to be able to + * debounce the save on Indexed DB. This store is only mounted once during the + * application's lifecycle. + */ + const { unsubscribe } = NoteStore.subscribe(() => { + SystemStore.setState((c) => ({ ...c, save: "saving" })); + debouncedSave.debouncedFn(); + }); + /** * If the user switched tabs, minimized the browser, or closed the page, * immediately save and unmount the effect. @@ -115,7 +116,6 @@ export const NoteEditor = () => { } await debouncedSave.flush(); - unsubscribe(); }; /** @@ -124,7 +124,6 @@ export const NoteEditor = () => { */ const handlePageHide = async () => { await debouncedSave.flush(); - unsubscribe(); }; /** @@ -141,10 +140,11 @@ export const NoteEditor = () => { * On app unmount, remove all of the event listeners. */ return () => { + unsubscribe(); document.removeEventListener("visibilitychange", handleVisibilityChange); window.removeEventListener("pagehide", handlePageHide); }; - }, [unsubscribe, debouncedSave.flush]); + }, [debouncedSave]); const handleSave = async () => { await debouncedSave.flush(); diff --git a/e2e/Speednote.test.tsx b/e2e/Speednote.test.tsx index 6592f38..ef27c3e 100644 --- a/e2e/Speednote.test.tsx +++ b/e2e/Speednote.test.tsx @@ -621,6 +621,50 @@ test("able to handle unused query parameters", async ({ page }) => { await expect(content).toBeEditable(); }); +test("able to restore autosave after hiding the app", async ({ page }) => { + // Render the app with our new browser context. + await renderPage(page); + + // Write something on the inputs. + const { title, content } = await getAndAssertEditor(page); + await title.fill("Autosave Test Title"); + await content.fill("Autosave Test Content"); + + // Force page visibility. + await page.evaluate(() => { + Object.defineProperty(document, "visibilityState", { + configurable: true, + value: "hidden", + }); + + document.dispatchEvent(new Event("visibilitychange")); + }); + + // Verify that the state is still `Saved.` + await expect(page.getByRole("status")).toContainText("Saved."); + + // Make the page visible again, and then try again. + await page.evaluate(() => { + Object.defineProperty(document, "visibilityState", { + configurable: true, + value: "visible", + }); + + document.dispatchEvent(new Event("visibilitychange")); + }); + + // Fill it out again, and then check if the Autosave still works. + await title.fill("Autosave"); + await content.fill("Autosave"); + await expect(page.getByRole("status")).toContainText("Saving..."); + await expect(page.getByRole("status")).toContainText("Saved."); + + // Reload the page. + await page.reload(); + await expect(title).toHaveValue("Autosave"); + await expect(content).toHaveValue("Autosave"); +}); + // // 404 pages are only testable in integration environments as it's a server-side page. // test('able to view and recover from 404 not found', async ({ page }) => { // await renderPage(page, '/404');