From d3c62aa5a21cf3d1657eb81e89f17865cd26286c Mon Sep 17 00:00:00 2001 From: Xiaonan Cui Date: Thu, 23 Jul 2026 22:00:26 +0800 Subject: [PATCH] fix(search-tabs): keep tabs without explicit location across reloads (#108) --- .../search-tabs/useSearchTabs.test.ts | 103 ++++++++++++++++++ .../features/search-tabs/useSearchTabs.ts | 27 ++++- 2 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 src/client/features/search-tabs/useSearchTabs.test.ts diff --git a/src/client/features/search-tabs/useSearchTabs.test.ts b/src/client/features/search-tabs/useSearchTabs.test.ts new file mode 100644 index 0000000..5f5fbfb --- /dev/null +++ b/src/client/features/search-tabs/useSearchTabs.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import { parseStoredState } from "./useSearchTabs"; + +function persistedTab(input: unknown) { + return { + id: "tab-1", + label: "example", + createdAt: 1, + viewedAt: null, + input, + }; +} + +describe("parseStoredState", () => { + it("keeps domain tabs persisted without a locationCode (default location)", () => { + const state = parseStoredState({ + activeTabId: "tab-1", + tabs: [ + persistedTab({ + type: "domain", + domain: "example.com", + subdomains: true, + }), + ], + }); + + expect(state.tabs).toHaveLength(1); + expect(state.activeTabId).toBe("tab-1"); + expect(state.tabs[0].input).toEqual({ + type: "domain", + domain: "example.com", + subdomains: true, + locationCode: undefined, + }); + }); + + it("keeps keyword tabs persisted without a locationCode (default location)", () => { + const state = parseStoredState({ + activeTabId: "tab-1", + tabs: [ + persistedTab({ + type: "keyword", + keyword: "seo tools", + resultLimit: 150, + mode: "auto", + clickstream: false, + }), + ], + }); + + expect(state.tabs).toHaveLength(1); + expect(state.activeTabId).toBe("tab-1"); + expect(state.tabs[0].input).toEqual({ + type: "keyword", + keyword: "seo tools", + locationCode: undefined, + resultLimit: 150, + mode: "auto", + clickstream: false, + }); + }); + + it("keeps tabs persisted with an explicit locationCode", () => { + const state = parseStoredState({ + activeTabId: "tab-1", + tabs: [ + persistedTab({ + type: "domain", + domain: "example.com", + subdomains: false, + locationCode: 2840, + }), + ], + }); + + expect(state.tabs).toHaveLength(1); + expect(state.tabs[0].input).toMatchObject({ locationCode: 2840 }); + }); + + it("still rejects malformed tab inputs", () => { + const state = parseStoredState({ + activeTabId: null, + tabs: [ + persistedTab({ type: "domain", subdomains: true }), + persistedTab({ + type: "keyword", + keyword: "seo tools", + resultLimit: 999, + mode: "auto", + }), + persistedTab({ + type: "domain", + domain: "example.com", + subdomains: true, + locationCode: "us", + }), + persistedTab({ type: "unknown" }), + ], + }); + + expect(state.tabs).toHaveLength(0); + }); +}); diff --git a/src/client/features/search-tabs/useSearchTabs.ts b/src/client/features/search-tabs/useSearchTabs.ts index c3460c7..4815308 100644 --- a/src/client/features/search-tabs/useSearchTabs.ts +++ b/src/client/features/search-tabs/useSearchTabs.ts @@ -44,18 +44,33 @@ function parseTabInput(value: unknown): SearchTabInput | null { if (value.type === "domain") { if (typeof value.domain !== "string" || value.domain === "") return null; if (typeof value.subdomains !== "boolean") return null; - if (typeof value.locationCode !== "number") return null; + // locationCode is optional in DomainSearchTabInput: tabs opened at the + // default location persist no loc param, so accept a missing key. + if ( + value.locationCode !== undefined && + typeof value.locationCode !== "number" + ) { + return null; + } return { type: "domain", domain: value.domain, subdomains: value.subdomains, - locationCode: value.locationCode, + locationCode: + typeof value.locationCode === "number" ? value.locationCode : undefined, }; } if (value.type === "keyword") { if (typeof value.keyword !== "string" || value.keyword === "") return null; - if (typeof value.locationCode !== "number") return null; + // locationCode is optional in KeywordSearchTabInput: tabs opened at the + // default location persist no loc param, so accept a missing key. + if ( + value.locationCode !== undefined && + typeof value.locationCode !== "number" + ) { + return null; + } if ( value.resultLimit !== 150 && value.resultLimit !== 300 && @@ -74,7 +89,8 @@ function parseTabInput(value: unknown): SearchTabInput | null { return { type: "keyword", keyword: value.keyword, - locationCode: value.locationCode, + locationCode: + typeof value.locationCode === "number" ? value.locationCode : undefined, resultLimit: value.resultLimit, mode: value.mode, // Tabs persisted before the clickstream toggle existed default to off. @@ -93,7 +109,8 @@ function tabInputKey(value: unknown): string { return JSON.stringify(value); } -function parseStoredState(value: unknown): TabsState { +// Exported for unit tests. +export function parseStoredState(value: unknown): TabsState { if (!isRecord(value)) return EMPTY_STATE; if (!Array.isArray(value.tabs)) return EMPTY_STATE; const tabs = value.tabs