Skip to content

Commit

Permalink
Handle case when session storage is blocked (#10848)
Browse files Browse the repository at this point in the history
  • Loading branch information
david-bezero committed Sep 14, 2023
1 parent e7a8f10 commit f8194fd
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/soft-forks-cough.md
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Log a warning and fail gracefully in `ScrollRestoration` when `sessionStorage` is unavailable
1 change: 1 addition & 0 deletions contributors.yml
Expand Up @@ -52,6 +52,7 @@
- danielberndt
- daniilguit
- dauletbaev
- david-bezero
- david-crespo
- decadentsavant
- DigitalNaut
Expand Down
148 changes: 148 additions & 0 deletions packages/react-router-dom/__tests__/scroll-restoration-test.tsx
Expand Up @@ -13,6 +13,63 @@ import {
import getHtml from "../../react-router/__tests__/utils/getHtml";

describe(`ScrollRestoration`, () => {
it("restores the scroll position for a page when re-visited", () => {
const consoleWarnMock = jest
.spyOn(console, "warn")
.mockImplementation(() => {});

let testWindow = getWindowImpl("/base");
const mockScroll = jest.fn();
window.scrollTo = mockScroll;

let router = createBrowserRouter(
[
{
path: "/",
Component() {
return (
<>
<Outlet />
<ScrollRestoration
getKey={(location) => "test1-" + location.pathname}
/>
</>
);
},
children: testPages,
},
],
{ basename: "/base", window: testWindow }
);
let { container } = render(<RouterProvider router={router} />);

expect(getHtml(container)).toMatch("On page 1");

// simulate scrolling
Object.defineProperty(window, "scrollY", { writable: true, value: 100 });

// leave page
window.dispatchEvent(new Event("pagehide"));
fireEvent.click(screen.getByText("Go to page 2"));
expect(getHtml(container)).toMatch("On page 2");

// return to page
window.dispatchEvent(new Event("pagehide"));
fireEvent.click(screen.getByText("Go to page 1"));

expect(getHtml(container)).toMatch("On page 1");

// check scroll activity
expect(mockScroll.mock.calls).toEqual([
[0, 0],
[0, 0],
[0, 100], // restored
]);

expect(consoleWarnMock).not.toHaveBeenCalled();
consoleWarnMock.mockRestore();
});

it("removes the basename from the location provided to getKey", () => {
let getKey = jest.fn(() => "mykey");
let testWindow = getWindowImpl("/base");
Expand Down Expand Up @@ -64,8 +121,99 @@ describe(`ScrollRestoration`, () => {
// @ts-expect-error
expect(getKey.mock.calls[2][0].pathname).toBe("/page"); // restore
});

it("fails gracefully if sessionStorage is not available", () => {
const consoleWarnMock = jest
.spyOn(console, "warn")
.mockImplementation(() => {});

let testWindow = getWindowImpl("/base");
const mockScroll = jest.fn();
window.scrollTo = mockScroll;

jest.spyOn(window, "sessionStorage", "get").mockImplementation(() => {
throw new Error("denied");
});

let router = createBrowserRouter(
[
{
path: "/",
Component() {
return (
<>
<Outlet />
<ScrollRestoration
getKey={(location) => "test2-" + location.pathname}
/>
</>
);
},
children: testPages,
},
],
{ basename: "/base", window: testWindow }
);
let { container } = render(<RouterProvider router={router} />);

expect(getHtml(container)).toMatch("On page 1");

// simulate scrolling
Object.defineProperty(window, "scrollY", { writable: true, value: 100 });

// leave page
window.dispatchEvent(new Event("pagehide"));
fireEvent.click(screen.getByText("Go to page 2"));
expect(getHtml(container)).toMatch("On page 2");

// return to page
window.dispatchEvent(new Event("pagehide"));
fireEvent.click(screen.getByText("Go to page 1"));

expect(getHtml(container)).toMatch("On page 1");

// check scroll activity
expect(mockScroll.mock.calls).toEqual([
[0, 0],
[0, 0],
[0, 100], // restored (still possible because the user hasn't left the page)
]);

expect(consoleWarnMock).toHaveBeenCalledWith(
expect.stringContaining(
"Failed to save scroll positions in sessionStorage"
)
);

consoleWarnMock.mockRestore();
});
});

const testPages = [
{
index: true,
Component() {
return (
<p>
On page 1<br />
<Link to="/page">Go to page 2</Link>
</p>
);
},
},
{
path: "page",
Component() {
return (
<p>
On page 2<br />
<Link to="/">Go to page 1</Link>
</p>
);
},
},
];

function getWindowImpl(initialUrl: string): Window {
// Need to use our own custom DOM in order to get a working history
const dom = new JSDOM(`<!DOCTYPE html>`, { url: "http://localhost/" });
Expand Down
15 changes: 11 additions & 4 deletions packages/react-router-dom/index.tsx
Expand Up @@ -1323,10 +1323,17 @@ function useScrollRestoration({
let key = (getKey ? getKey(location, matches) : null) || location.key;
savedScrollPositions[key] = window.scrollY;
}
sessionStorage.setItem(
storageKey || SCROLL_RESTORATION_STORAGE_KEY,
JSON.stringify(savedScrollPositions)
);
try {
sessionStorage.setItem(
storageKey || SCROLL_RESTORATION_STORAGE_KEY,
JSON.stringify(savedScrollPositions)
);
} catch (error) {
warning(
false,
`Failed to save scroll positions in sessionStorage, <ScrollRestoration /> will not work properly (${error}).`
);
}
window.history.scrollRestoration = "auto";
}, [storageKey, getKey, navigation.state, location, matches])
);
Expand Down

0 comments on commit f8194fd

Please sign in to comment.