Skip to content

Commit 0dcd92e

Browse files
committed
feat: implement useBrowser hook with tab management, navigation, and bookmarking functionalities
1 parent f12ca01 commit 0dcd92e

2 files changed

Lines changed: 423 additions & 0 deletions

File tree

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import { act } from "@testing-library/react-native";
2+
import {
3+
createMockTab,
4+
defaultBrowserState,
5+
renderHookWithStore,
6+
} from "../../../../__tests__/utils/testUtils";
7+
import { useBrowser } from "../useBrowser";
8+
9+
describe("useBrowser Hook", () => {
10+
describe("Initial State", () => {
11+
it("should return initial browser state correctly", () => {
12+
const { result } = renderHookWithStore(() => useBrowser());
13+
14+
expect(result.current.tabs).toHaveLength(1);
15+
expect(result.current.activeTabId).toBe("tab-1");
16+
expect(result.current.showTabs).toBe(false);
17+
expect(result.current.bookmarks).toEqual(["https://bookmark.com"]);
18+
expect(result.current.activeTab).toEqual({
19+
id: "tab-1",
20+
url: "https://example.com",
21+
title: "Example",
22+
isLoading: false,
23+
canGoBack: false,
24+
canGoForward: false,
25+
activeScripts: [],
26+
});
27+
});
28+
29+
it("should return computed values correctly", () => {
30+
const { result } = renderHookWithStore(() => useBrowser(), {
31+
initialState: {
32+
browser: {
33+
...defaultBrowserState,
34+
tabs: [
35+
createMockTab({
36+
id: "tab-1",
37+
url: "https://example.com",
38+
title: "Example",
39+
}),
40+
createMockTab({
41+
id: "tab-2",
42+
url: "https://google.com",
43+
title: "Google",
44+
}),
45+
],
46+
},
47+
},
48+
});
49+
50+
expect(result.current.tabCount).toBe(2);
51+
expect(result.current.hasMultipleTabs).toBe(true);
52+
});
53+
54+
it("should handle no active tab correctly", () => {
55+
const { result } = renderHookWithStore(() => useBrowser(), {
56+
initialState: {
57+
browser: {
58+
...defaultBrowserState,
59+
tabs: [],
60+
activeTabId: "",
61+
},
62+
},
63+
});
64+
65+
expect(result.current.activeTab).toBeUndefined();
66+
expect(result.current.tabCount).toBe(0);
67+
expect(result.current.hasMultipleTabs).toBe(false);
68+
});
69+
});
70+
71+
describe("Tab Management", () => {
72+
it("should create new tab with default URL", () => {
73+
const { result, store } = renderHookWithStore(() => useBrowser());
74+
75+
act(() => {
76+
result.current.createNewTab();
77+
});
78+
79+
const state = store.getState().browser;
80+
expect(state.tabs).toHaveLength(2);
81+
expect(state.tabs[1].url).toBe(
82+
process.env.EXPO_PUBLIC_HOME_PAGE_URL || "https://www.google.com"
83+
);
84+
});
85+
86+
it("should create new tab with custom URL", () => {
87+
const { result, store } = renderHookWithStore(() => useBrowser());
88+
89+
act(() => {
90+
result.current.createNewTab("https://custom.com");
91+
});
92+
93+
const state = store.getState().browser;
94+
expect(state.tabs).toHaveLength(2);
95+
expect(state.tabs[1].url).toBe("https://custom.com");
96+
});
97+
98+
it("should close tab by id", () => {
99+
const { result, store } = renderHookWithStore(() => useBrowser(), {
100+
initialState: {
101+
browser: {
102+
...defaultBrowserState,
103+
tabs: [
104+
createMockTab({
105+
id: "tab-1",
106+
url: "https://example.com",
107+
title: "Example",
108+
}),
109+
createMockTab({
110+
id: "tab-2",
111+
url: "https://google.com",
112+
title: "Google",
113+
}),
114+
],
115+
},
116+
},
117+
});
118+
119+
act(() => {
120+
result.current.closeTabById("tab-1");
121+
});
122+
123+
const state = store.getState().browser;
124+
expect(state.tabs).toHaveLength(1);
125+
expect(state.tabs[0].id).toBe("tab-2");
126+
});
127+
128+
it("should update tab by id", () => {
129+
const { result, store } = renderHookWithStore(() => useBrowser());
130+
131+
act(() => {
132+
result.current.updateTabById("tab-1", {
133+
title: "Updated Title",
134+
isLoading: true,
135+
});
136+
});
137+
138+
const state = store.getState().browser;
139+
const updatedTab = state.tabs.find((tab) => tab.id === "tab-1");
140+
expect(updatedTab?.title).toBe("Updated Title");
141+
expect(updatedTab?.isLoading).toBe(true);
142+
});
143+
144+
it("should switch to tab", () => {
145+
const { result, store } = renderHookWithStore(() => useBrowser(), {
146+
initialState: {
147+
browser: {
148+
...defaultBrowserState,
149+
tabs: [
150+
createMockTab({
151+
id: "tab-1",
152+
url: "https://example.com",
153+
title: "Example",
154+
}),
155+
createMockTab({
156+
id: "tab-2",
157+
url: "https://google.com",
158+
title: "Google",
159+
}),
160+
],
161+
activeTabId: "tab-1",
162+
},
163+
},
164+
});
165+
166+
act(() => {
167+
result.current.switchToTab("tab-2");
168+
});
169+
170+
const state = store.getState().browser;
171+
expect(state.activeTabId).toBe("tab-2");
172+
expect(state.showTabs).toBe(false);
173+
});
174+
175+
it("should toggle tab view", () => {
176+
const { result, store } = renderHookWithStore(() => useBrowser(), {
177+
initialState: {
178+
browser: {
179+
...defaultBrowserState,
180+
showTabs: false,
181+
},
182+
},
183+
});
184+
185+
act(() => {
186+
result.current.toggleTabs();
187+
});
188+
189+
const state = store.getState().browser;
190+
expect(state.showTabs).toBe(true);
191+
});
192+
});
193+
194+
describe("Navigation", () => {
195+
it("should navigate tab to URL", () => {
196+
const { result, store } = renderHookWithStore(() => useBrowser());
197+
198+
act(() => {
199+
result.current.navigateTab("tab-1", "https://newurl.com");
200+
});
201+
202+
const state = store.getState().browser;
203+
const tab = state.tabs.find((t) => t.id === "tab-1");
204+
expect(tab?.url).toBe("https://newurl.com");
205+
expect(tab?.isLoading).toBe(true);
206+
});
207+
});
208+
209+
describe("Bookmarks", () => {
210+
it("should add bookmark when URL is not bookmarked", () => {
211+
const { result, store } = renderHookWithStore(() => useBrowser(), {
212+
initialState: {
213+
browser: {
214+
...defaultBrowserState,
215+
bookmarks: ["https://existing.com"],
216+
},
217+
},
218+
});
219+
220+
act(() => {
221+
result.current.toggleBookmark("https://newbookmark.com");
222+
});
223+
224+
const state = store.getState().browser;
225+
expect(state.bookmarks).toContain("https://newbookmark.com");
226+
expect(state.bookmarks).toHaveLength(2);
227+
});
228+
229+
it("should remove bookmark when URL is already bookmarked", () => {
230+
const { result, store } = renderHookWithStore(() => useBrowser(), {
231+
initialState: {
232+
browser: {
233+
...defaultBrowserState,
234+
bookmarks: ["https://existing.com", "https://tobedeleted.com"],
235+
},
236+
},
237+
});
238+
239+
act(() => {
240+
result.current.toggleBookmark("https://tobedeleted.com");
241+
});
242+
243+
const state = store.getState().browser;
244+
expect(state.bookmarks).not.toContain("https://tobedeleted.com");
245+
expect(state.bookmarks).toHaveLength(1);
246+
});
247+
248+
it("should correctly identify bookmarked URLs", () => {
249+
const { result } = renderHookWithStore(() => useBrowser(), {
250+
initialState: {
251+
browser: {
252+
...defaultBrowserState,
253+
bookmarks: ["https://bookmarked.com"],
254+
},
255+
},
256+
});
257+
258+
expect(result.current.isBookmarked("https://bookmarked.com")).toBe(true);
259+
expect(result.current.isBookmarked("https://notbookmarked.com")).toBe(
260+
false
261+
);
262+
});
263+
});
264+
265+
describe("Function Stability", () => {
266+
it("should maintain function reference stability for memoized functions", () => {
267+
const { result, rerender } = renderHookWithStore(() => useBrowser());
268+
269+
const firstRender = {
270+
toggleBookmark: result.current.toggleBookmark,
271+
};
272+
273+
rerender({});
274+
275+
const secondRender = {
276+
toggleBookmark: result.current.toggleBookmark,
277+
};
278+
279+
// toggleBookmark should maintain reference stability due to useCallback
280+
expect(firstRender.toggleBookmark).toBe(secondRender.toggleBookmark);
281+
});
282+
283+
it("should create new function references for non-memoized functions", () => {
284+
const { result, rerender } = renderHookWithStore(() => useBrowser());
285+
286+
const firstRender = {
287+
createNewTab: result.current.createNewTab,
288+
closeTabById: result.current.closeTabById,
289+
};
290+
291+
rerender({});
292+
293+
const secondRender = {
294+
createNewTab: result.current.createNewTab,
295+
closeTabById: result.current.closeTabById,
296+
};
297+
298+
// These functions are not memoized, so they should be different references
299+
expect(firstRender.createNewTab).not.toBe(secondRender.createNewTab);
300+
expect(firstRender.closeTabById).not.toBe(secondRender.closeTabById);
301+
});
302+
});
303+
304+
describe("Edge Cases", () => {
305+
it("should handle empty tab array", () => {
306+
const { result } = renderHookWithStore(() => useBrowser(), {
307+
initialState: {
308+
browser: {
309+
...defaultBrowserState,
310+
tabs: [],
311+
activeTabId: "",
312+
},
313+
},
314+
});
315+
316+
expect(result.current.tabs).toHaveLength(0);
317+
expect(result.current.activeTab).toBeUndefined();
318+
expect(result.current.tabCount).toBe(0);
319+
expect(result.current.hasMultipleTabs).toBe(false);
320+
});
321+
322+
it("should handle invalid active tab id", () => {
323+
const { result } = renderHookWithStore(() => useBrowser(), {
324+
initialState: {
325+
browser: {
326+
...defaultBrowserState,
327+
activeTabId: "non-existent-tab",
328+
},
329+
},
330+
});
331+
332+
expect(result.current.activeTab).toBeUndefined();
333+
});
334+
});
335+
});

0 commit comments

Comments
 (0)