diff --git a/package.json b/package.json index b582033e..cc75a890 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.183", + "version": "0.18.184", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/app.js b/src/js/app.js index 9adceb0d..28d79496 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -255,10 +255,12 @@ export async function main() { router.addRoute(["/", "/intent/compose"], () => homeView, { layoutOptions: { activeNavItem: "home" }, + scrollRestore: "always", }); router.addRoute("/login", () => loginView, { layout: false }); router.addRoute("/notifications", () => notificationsView, { layoutOptions: { activeNavItem: "notifications" }, + scrollRestore: "always", }); router.addRoute("/messages/inbox", () => chatRequestsView, { layoutOptions: { activeNavItem: "chat" }, @@ -268,6 +270,7 @@ export async function main() { }); router.addRoute("/messages/:convoId", () => chatDetailView, { layoutOptions: { activeNavItem: "chat" }, + scrollRestore: "manual", }); router.addRoute("/messages", () => chatView, { layoutOptions: { activeNavItem: "chat" }, @@ -283,6 +286,7 @@ export async function main() { }); router.addRoute("/search", () => searchView, { layoutOptions: { activeNavItem: "search" }, + scrollRestore: "manual", }); router.addRoute("/hashtag/:tag", () => hashtagView); router.addRoute("/profile/:handleOrDid/feed/:rkey", () => feedDetailView); @@ -299,7 +303,9 @@ export async function main() { "/profile/:handleOrDid/post/:rkey/reposts", () => postRepostsView, ); - router.addRoute("/profile/:handleOrDid/post/:rkey", () => postThreadView); + router.addRoute("/profile/:handleOrDid/post/:rkey", () => postThreadView, { + scrollRestore: "manual", + }); router.addRoute( "/profile/:handleOrDid/known-followers", () => profileKnownFollowersView, diff --git a/src/js/router.js b/src/js/router.js index 08250ced..9f273eeb 100644 --- a/src/js/router.js +++ b/src/js/router.js @@ -272,6 +272,20 @@ export class Router extends EventEmitter { const scrollY = this.scrollStates.get(path) ?? 0; this.currentPage.classList.remove("page-hidden"); this.currentPage.classList.add("page-visible"); + // Scroll before dispatching so a "manual" view's own scroll wins + const scrollRestore = routeInfo.options.scrollRestore ?? "back"; + switch (scrollRestore) { + case "always": + window.scrollTo(0, scrollY); + break; + case "back": + window.scrollTo(0, isBack ? scrollY : 0); + break; + case "manual": + break; + default: + console.warn(`unknown scrollRestore type: ${scrollRestore}`); + } this.currentPage.dispatchEvent( new CustomEvent("page-restore", { detail: { diff --git a/src/js/views/bookmarks.view.js b/src/js/views/bookmarks.view.js index 81a3db6a..03f4e102 100644 --- a/src/js/views/bookmarks.view.js +++ b/src/js/views/bookmarks.view.js @@ -66,14 +66,8 @@ class BookmarksView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadBookmarks({ reload: true }); - } + if (e.detail?.isBack) return; + await loadBookmarks({ reload: true }); }); } } diff --git a/src/js/views/chat.view.js b/src/js/views/chat.view.js index 6aef6ced..0f218525 100644 --- a/src/js/views/chat.view.js +++ b/src/js/views/chat.view.js @@ -283,14 +283,8 @@ class ChatView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadConvoList({ reload: true }); - } + if (e.detail?.isBack) return; + await loadConvoList({ reload: true }); }); } } diff --git a/src/js/views/chatRequests.view.js b/src/js/views/chatRequests.view.js index 7fc4530c..631c35f5 100644 --- a/src/js/views/chatRequests.view.js +++ b/src/js/views/chatRequests.view.js @@ -261,14 +261,8 @@ class ChatRequestsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadConvoRequestList({ reload: true }); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadConvoRequestList({ reload: true }); }); } } diff --git a/src/js/views/communityPluginListing.view.js b/src/js/views/communityPluginListing.view.js index c76ff5ff..e167eaa6 100644 --- a/src/js/views/communityPluginListing.view.js +++ b/src/js/views/communityPluginListing.view.js @@ -285,10 +285,6 @@ class CommunityPluginListingView extends View { await loadListings(); await loadDetails(); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/communityPlugins.view.js b/src/js/views/communityPlugins.view.js index 9f61c77b..239d9dd3 100644 --- a/src/js/views/communityPlugins.view.js +++ b/src/js/views/communityPlugins.view.js @@ -152,7 +152,6 @@ class CommunityPluginsView extends View { }); root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); loadListings(); }); } diff --git a/src/js/views/feedDetail.view.js b/src/js/views/feedDetail.view.js index eb02ec45..5cd446e8 100644 --- a/src/js/views/feedDetail.view.js +++ b/src/js/views/feedDetail.view.js @@ -145,14 +145,8 @@ class FeedDetailView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadFeed({ reload: true }); - } + if (e.detail?.isBack) return; + await loadFeed({ reload: true }); }); } } diff --git a/src/js/views/feeds.view.js b/src/js/views/feeds.view.js index 9ce672d6..460b46a6 100644 --- a/src/js/views/feeds.view.js +++ b/src/js/views/feeds.view.js @@ -303,14 +303,8 @@ class FeedsView extends View { root.addEventListener("page-restore", async (e) => { resetEditingState(); - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPinnedItems(); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPinnedItems(); }); } } diff --git a/src/js/views/groupChatDetails.view.js b/src/js/views/groupChatDetails.view.js index e424fee7..adec59be 100644 --- a/src/js/views/groupChatDetails.view.js +++ b/src/js/views/groupChatDetails.view.js @@ -340,16 +340,8 @@ class GroupChatDetailsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await loadConvoDetails({ reload: true }); - } + if (e.detail?.isBack) return; + await loadConvoDetails({ reload: true }); }); } } diff --git a/src/js/views/hashtag.view.js b/src/js/views/hashtag.view.js index bde96bcd..20ddada4 100644 --- a/src/js/views/hashtag.view.js +++ b/src/js/views/hashtag.view.js @@ -119,11 +119,6 @@ class HashtagView extends View { root.addEventListener("page-enter", async () => { await loadCurrentFeed(); }); - - root.addEventListener("page-restore", (e) => { - const scrollY = e.detail?.scrollY ?? 0; - window.scrollTo(0, scrollY); - }); } } diff --git a/src/js/views/home.view.js b/src/js/views/home.view.js index 1cf4bade..fd933638 100644 --- a/src/js/views/home.view.js +++ b/src/js/views/home.view.js @@ -414,9 +414,7 @@ class HomeView extends View { await loadCurrentFeed(); }); - root.addEventListener("page-restore", (e) => { - const scrollY = e.detail?.scrollY ?? 0; - window.scrollTo(0, scrollY); + root.addEventListener("page-restore", () => { for (const observer of postSeenObservers.values()) { observer.connect(); } diff --git a/src/js/views/installedPlugins.view.js b/src/js/views/installedPlugins.view.js index d5f572b1..993758b8 100644 --- a/src/js/views/installedPlugins.view.js +++ b/src/js/views/installedPlugins.view.js @@ -344,18 +344,6 @@ class InstalledPluginsView extends View { root, ); }); - - root.addEventListener("page-restore", (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - } - }); } } diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js index 474d0dfd..23c1a1fa 100644 --- a/src/js/views/listDetail.view.js +++ b/src/js/views/listDetail.view.js @@ -431,14 +431,8 @@ class ListDetailView extends View { root.addEventListener("page-restore", async (e) => { userHasScrolled = false; - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadListAndFeeds({ reload: true }); - } + if (e.detail?.isBack) return; + await loadListAndFeeds({ reload: true }); }); } } diff --git a/src/js/views/lists.view.js b/src/js/views/lists.view.js index 22d9a000..479c8ee5 100644 --- a/src/js/views/lists.view.js +++ b/src/js/views/lists.view.js @@ -92,14 +92,8 @@ class ListsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadLists({ reload: true }); - } + if (e.detail?.isBack) return; + await loadLists({ reload: true }); }); } } diff --git a/src/js/views/notifications.view.js b/src/js/views/notifications.view.js index ea657d15..9bc506b5 100644 --- a/src/js/views/notifications.view.js +++ b/src/js/views/notifications.view.js @@ -841,7 +841,6 @@ class NotificationsView extends View { root.addEventListener("page-restore", async (e) => { const scrollY = e.detail?.scrollY ?? 0; - window.scrollTo(0, scrollY); if (scrollY <= 200) { await loadNotifications({ reload: true }); } diff --git a/src/js/views/pluginPage.view.js b/src/js/views/pluginPage.view.js index 9ff70f50..4b327c57 100644 --- a/src/js/views/pluginPage.view.js +++ b/src/js/views/pluginPage.view.js @@ -100,10 +100,6 @@ class PluginPageView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/pluginSettings.view.js b/src/js/views/pluginSettings.view.js index fd7502b0..b87169b7 100644 --- a/src/js/views/pluginSettings.view.js +++ b/src/js/views/pluginSettings.view.js @@ -99,10 +99,6 @@ class PluginSettingsView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/postLikes.view.js b/src/js/views/postLikes.view.js index 07b5ce07..923e8e0a 100644 --- a/src/js/views/postLikes.view.js +++ b/src/js/views/postLikes.view.js @@ -100,16 +100,8 @@ class PostLikesView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPostLikes(postUri); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPostLikes(postUri); }); } } diff --git a/src/js/views/postQuotes.view.js b/src/js/views/postQuotes.view.js index 8ab37509..f2d9268c 100644 --- a/src/js/views/postQuotes.view.js +++ b/src/js/views/postQuotes.view.js @@ -103,16 +103,8 @@ class PostQuotesView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPostQuotes(postUri); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPostQuotes(postUri); }); } } diff --git a/src/js/views/postReposts.view.js b/src/js/views/postReposts.view.js index 2320b86b..2ecfd88f 100644 --- a/src/js/views/postReposts.view.js +++ b/src/js/views/postReposts.view.js @@ -99,16 +99,8 @@ class PostRepostsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPostReposts(postUri); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPostReposts(postUri); }); } } diff --git a/src/js/views/profile.view.js b/src/js/views/profile.view.js index 5d8c7b1b..ed329c12 100644 --- a/src/js/views/profile.view.js +++ b/src/js/views/profile.view.js @@ -581,15 +581,6 @@ class ProfileView extends View { dataLayer.requests.loadProfileChatStatus(profile.did); } }); - - root.addEventListener("page-restore", (e) => { - const { isBack, scrollY } = e.detail; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - } - }); } } diff --git a/src/js/views/profileFollowers.view.js b/src/js/views/profileFollowers.view.js index 89cfaaad..76430194 100644 --- a/src/js/views/profileFollowers.view.js +++ b/src/js/views/profileFollowers.view.js @@ -110,16 +110,8 @@ class ProfileFollowersView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadProfileFollowers(profileDid); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadProfileFollowers(profileDid); }); } } diff --git a/src/js/views/profileFollowing.view.js b/src/js/views/profileFollowing.view.js index ad0bb90b..368f05ef 100644 --- a/src/js/views/profileFollowing.view.js +++ b/src/js/views/profileFollowing.view.js @@ -108,16 +108,8 @@ class ProfileFollowingView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadProfileFollows(profileDid); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadProfileFollows(profileDid); }); } } diff --git a/src/js/views/profileKnownFollowers.view.js b/src/js/views/profileKnownFollowers.view.js index 52848b11..1ddcd33f 100644 --- a/src/js/views/profileKnownFollowers.view.js +++ b/src/js/views/profileKnownFollowers.view.js @@ -96,16 +96,8 @@ class ProfileKnownFollowersView extends View { }); root.addEventListener("page-restore", async (event) => { - const scrollY = event.detail?.scrollY ?? 0; - const isBack = event.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadKnownFollowers(profileDid); - } + if (event.detail?.isBack) return; + await dataLayer.requests.loadKnownFollowers(profileDid); }); } } diff --git a/src/js/views/settings.view.js b/src/js/views/settings.view.js index b8291fa0..a2792b79 100644 --- a/src/js/views/settings.view.js +++ b/src/js/views/settings.view.js @@ -396,10 +396,6 @@ class SettingsView extends View { loadOtherAccounts(); }); - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); - // Account actions navigate away with the pending spinner showing; if the // user comes back via the back/forward cache the document is restored // as-is, so reset the stuck pending state. diff --git a/src/js/views/settings/advanced.view.js b/src/js/views/settings/advanced.view.js index bc3c3007..2f0ac0b4 100644 --- a/src/js/views/settings/advanced.view.js +++ b/src/js/views/settings/advanced.view.js @@ -277,10 +277,6 @@ class SettingsAdvancedView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/appearance.view.js b/src/js/views/settings/appearance.view.js index d22a0385..d2bbc1ce 100644 --- a/src/js/views/settings/appearance.view.js +++ b/src/js/views/settings/appearance.view.js @@ -147,10 +147,6 @@ class SettingsAppearanceView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/blockedAccounts.view.js b/src/js/views/settings/blockedAccounts.view.js index 01737c2d..89c599d5 100644 --- a/src/js/views/settings/blockedAccounts.view.js +++ b/src/js/views/settings/blockedAccounts.view.js @@ -81,10 +81,6 @@ class SettingsBlockedAccountsView extends View { root.addEventListener("page-enter", async () => { await loadMore(); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/mutedAccounts.view.js b/src/js/views/settings/mutedAccounts.view.js index 708f8dce..f469709b 100644 --- a/src/js/views/settings/mutedAccounts.view.js +++ b/src/js/views/settings/mutedAccounts.view.js @@ -78,10 +78,6 @@ class SettingsMutedAccountsView extends View { root.addEventListener("page-enter", async () => { await loadMore(); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/mutedWords.view.js b/src/js/views/settings/mutedWords.view.js index f3ef7c0a..c571705b 100644 --- a/src/js/views/settings/mutedWords.view.js +++ b/src/js/views/settings/mutedWords.view.js @@ -358,10 +358,6 @@ class SettingsMutedWordsView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/notifications.view.js b/src/js/views/settings/notifications.view.js index 88799b20..6d560de5 100644 --- a/src/js/views/settings/notifications.view.js +++ b/src/js/views/settings/notifications.view.js @@ -103,10 +103,6 @@ class SettingsNotificationsView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/tests/e2e/specs/concerns/scroll.test.js b/tests/e2e/specs/concerns/scroll.test.js index cf5e17b2..a99add73 100644 --- a/tests/e2e/specs/concerns/scroll.test.js +++ b/tests/e2e/specs/concerns/scroll.test.js @@ -55,4 +55,64 @@ test.describe("Scroll position restoration", () => { // Verify the post we scrolled to is still visible (scroll position restored) await expect(targetPost).toBeVisible({ timeout: 10000 }); }); + + test.describe("on a settings subpage", () => { + test.use({ viewport: { width: 375, height: 667 } }); + + test("should restore on back and reset to the top on a forward visit", async ({ + page, + }) => { + const mockServer = new MockServer(); + mockServer.mutedWords = Array.from({ length: 40 }, (_, i) => ({ + value: `mutedword${i + 1}`, + targets: ["content"], + })); + await mockServer.setup(page); + + await login(page); + await page.goto("/settings"); + + const openMutedWords = () => + page.locator('[data-testid="settings-nav-muted-words"]').click(); + const view = page.locator("#settings-muted-words-view"); + + await openMutedWords(); + await expect(view.locator('[data-testid="muted-word-list"]')).toBeVisible( + { + timeout: 10000, + }, + ); + + await view + .locator('[data-testid="muted-word-item"]') + .last() + .scrollIntoViewIfNeeded(); + const scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBeGreaterThan(0); + + await page.goBack(); + await expect(page.locator("#settings-view")).toBeVisible({ + timeout: 10000, + }); + + // Forward navigation to the cached page starts at the top + await openMutedWords(); + await expect(view).toBeVisible({ timeout: 10000 }); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); + + // ...and the position saved for that visit is restored on back + await view + .locator('[data-testid="muted-word-item"]') + .last() + .scrollIntoViewIfNeeded(); + await page.goBack(); + await expect(page.locator("#settings-view")).toBeVisible({ + timeout: 10000, + }); + await page.goForward(); + await expect + .poll(() => page.evaluate(() => window.scrollY)) + .toBe(scrollY); + }); + }); }); diff --git a/tests/unit/specs/router.test.js b/tests/unit/specs/router.test.js index cb417aa6..92af5eaa 100644 --- a/tests/unit/specs/router.test.js +++ b/tests/unit/specs/router.test.js @@ -28,6 +28,27 @@ function mountRouter(router, { layout = null } = {}) { }; } +// JSDOM's window.scrollY is a read-only getter, so temporarily override it to +// simulate the page being scrolled before we navigate away. +function withScrollY(value, callback) { + const original = Object.getOwnPropertyDescriptor(window, "scrollY"); + Object.defineProperty(window, "scrollY", { + value, + configurable: true, + }); + return (async () => { + try { + return await callback(); + } finally { + if (original) { + Object.defineProperty(window, "scrollY", original); + } else { + delete window.scrollY; + } + } + })(); +} + describe("constructor and initialization", () => { it("should initialize with empty routes", () => { const router = new Router(); @@ -1088,27 +1109,6 @@ describe("page cache", () => { }); describe("scroll position persistence", () => { - // JSDOM's window.scrollY is a read-only getter, so temporarily override it to - // simulate the page being scrolled before we navigate away. - function withScrollY(value, callback) { - const original = Object.getOwnPropertyDescriptor(window, "scrollY"); - Object.defineProperty(window, "scrollY", { - value, - configurable: true, - }); - return (async () => { - try { - return await callback(); - } finally { - if (original) { - Object.defineProperty(window, "scrollY", original); - } else { - delete window.scrollY; - } - } - })(); - } - function createRouter() { const router = new Router(); mountRouter(router); @@ -1168,3 +1168,94 @@ describe("scroll position persistence", () => { assert.deepEqual(restoredScrollY, 175); }); }); + +describe("scrollRestore", () => { + let scrollTo; + + beforeEach(() => { + scrollTo = mock.method(window, "scrollTo", () => {}); + }); + + afterEach(() => { + scrollTo.mock.restore(); + }); + + function scrollCalls() { + return scrollTo.mock.calls.map((call) => call.arguments); + } + + function createRouter(scrollRestore) { + const router = new Router(); + mountRouter(router); + router.addRoute( + "/a", + () => Promise.resolve({}), + scrollRestore ? { scrollRestore } : {}, + ); + router.addRoute("/b", () => Promise.resolve({})); + router.renderRoute(() => {}); + return router; + } + + // Returns to /a after leaving it scrolled to 175 + async function revisit(router, { isBack }) { + await router.load("/a"); + await withScrollY(175, () => router.load("/b")); + scrollTo.mock.resetCalls(); + await router.load("/a", { isBack }); + } + + it("restores the saved position on back navigation by default", async () => { + const router = createRouter(); + + await revisit(router, { isBack: true }); + + assert.deepEqual(scrollCalls(), [[0, 175]]); + }); + + it("scrolls to the top on forward navigation by default", async () => { + const router = createRouter(); + + await revisit(router, { isBack: false }); + + assert.deepEqual(scrollCalls(), [[0, 0]]); + }); + + it("restores the saved position on forward navigation when set to always", async () => { + const router = createRouter("always"); + + await revisit(router, { isBack: false }); + + assert.deepEqual(scrollCalls(), [[0, 175]]); + }); + + it("still restores the saved position on back navigation when set to always", async () => { + const router = createRouter("always"); + + await revisit(router, { isBack: true }); + + assert.deepEqual(scrollCalls(), [[0, 175]]); + }); + + it("does not touch the scroll position when set to manual", async () => { + const router = createRouter("manual"); + + await revisit(router, { isBack: false }); + await revisit(router, { isBack: true }); + + assert.deepEqual(scrollCalls(), []); + }); + + it("scrolls before dispatching page-restore so a view can override it", async () => { + const router = createRouter(); + await router.load("/a"); + let scrolledBeforeDispatch = null; + router.pages.get("/a").el.addEventListener("page-restore", () => { + scrolledBeforeDispatch = scrollCalls(); + }); + + await revisit(router, { isBack: true }); + + assert.deepEqual(scrolledBeforeDispatch, [[0, 175]]); + }); +});