diff --git a/src/App.tsx b/src/App.tsx index ddef64a..0de907c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, type ComponentType, type CSSProperties } from 'react' +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom' import { I18nProvider } from './components/I18nProvider' import './i18n/index.js' import './styles/responsive.css' @@ -175,6 +176,7 @@ function NotificationBell({ onClick }: { onClick: () => void }) { } function DashboardLayout() { + const navigate = useNavigate() const { connectedAddress, activeTab, @@ -292,18 +294,14 @@ function DashboardLayout() { const handleSearchResult = (result: SearchResult | null | undefined): void => { if (!result) return if (result.type === 'transaction' || result.type === 'operation') { - setActiveTab('transactions') + navigate('/transactions') return } if (result.type === 'account') { - setActiveTab('account') + navigate('/account') return } - if (result.type === 'contract') { - setActiveTab('contracts') - return - } - setActiveTab('overview') + navigate('/overview') } return ( @@ -383,10 +381,39 @@ function DashboardLayout() { ) } +function RouterSync() { + const navigate = useNavigate() + const location = useLocation() + const { connectedAddress, activeTab, setActiveTab } = useStore() + + const pathTab = location.pathname === '/' ? 'overview' : location.pathname.slice(1) + + useEffect(() => { + if (pathTab === 'connect') return + if (TABS[pathTab] && pathTab !== activeTab) { + setActiveTab(pathTab) + } + }, [location.pathname]) + + useEffect(() => { + if (!connectedAddress && pathTab !== 'connect') { + navigate('/connect', { replace: true }) + } else if (connectedAddress && pathTab === 'connect') { + navigate(`/${activeTab}`, { replace: true }) + } + }, [connectedAddress, location.pathname]) + + return null +} + export default function App() { return ( - + + + } /> + } /> + ) } diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index 5212b9b..a321751 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -1,4 +1,5 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import { useStore } from '../../lib/store' import CopyableValue from '../dashboard/CopyableValue' import { NETWORKS, updateCustomNetworkConfig, switchToCustomProfile, loadCustomNetworkProfiles } from '../../lib/stellar' @@ -43,13 +44,9 @@ const NAV_ITEMS = [ ] export default function Sidebar({ isMobile = false }) { - const initialCustomHeaders = getCustomNetworkAuthHeaders() - const initialHeaderName = Object.keys(initialCustomHeaders)[0] || 'Authorization' - const [customHeaderName, setCustomHeaderName] = useState(initialHeaderName) - const [customHeaderValue, setCustomHeaderValue] = useState(initialCustomHeaders[initialHeaderName] || '') + const navigate = useNavigate() const { activeTab, - setActiveTab, network, setNetwork, connectedAddress, @@ -84,26 +81,8 @@ export default function Sidebar({ isMobile = false }) { }, [network]) const handleNavClick = (tabId) => { - setActiveTab(tabId) - setMobileMenuOpen(false) - } - - const handleSwitchProfile = async (profileId) => { - try { - await switchToCustomProfile(profileId) - setActiveProfileId(profileId) - // Force store update to refresh clients - const profile = customProfiles.find(p => p.id === profileId) - if (profile) { - updateCustomNetworkConfig({ - horizonUrl: profile.horizonUrl, - sorobanUrl: profile.sorobanUrl, - passphrase: profile.passphrase, - }) - } - } catch (err) { - console.error('Failed to switch profile:', err) - } + navigate(`/${tabId}`) + setMobileMenuOpen(false) // Close mobile menu after navigation } // Restore custom API key from sessionStorage on mount diff --git a/src/lib/store.ts b/src/lib/store.ts index 3bfacd2..943360c 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -399,6 +399,11 @@ export const useStore = create((set, get) => ({ setStreamError: (e) => set({ streamError: e }), })) +// ─── Expose store for e2e testing ──────────────────────────────────────────── +if (typeof window !== 'undefined') { + (window as any).__store = useStore +} + // ─── Persistence middleware ─────────────────────────────────────────────────── if (typeof window !== 'undefined') { diff --git a/src/main.jsx b/src/main.jsx index 53055c3..a331ca5 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; import App from "./App"; import "./styles/globals.css"; import { initPerformanceMonitoring } from "./lib/performanceMonitoring"; @@ -9,6 +10,8 @@ initPerformanceMonitoring(); ReactDOM.createRoot(document.getElementById("root")).render( - + + + , ); diff --git a/tests/e2e/navigation.spec.js b/tests/e2e/navigation.spec.js index 54634d9..50d5ab1 100644 --- a/tests/e2e/navigation.spec.js +++ b/tests/e2e/navigation.spec.js @@ -13,16 +13,86 @@ test.describe('Sidebar navigation', () => { test('network toggle switches between testnet and mainnet', async ({ page }) => { const mainBtn = page.locator('button', { hasText: 'Main' }); await mainBtn.click(); - await expect(mainBtn).toHaveCSS('color', /0, 229, 255/); // cyan active color + await expect(mainBtn).toHaveCSS('color', /0, 229, 255/); }); - test('clicking Multisig nav item shows multisig page', async ({ page }) => { - await page.locator('button', { hasText: 'Multisig' }).click(); - await expect(page.locator('text=Multi-Signature')).toBeVisible(); + test('unauthenticated user visiting /transactions is redirected to /connect', async ({ page }) => { + await page.goto('/transactions'); + await expect(page).toHaveURL(/\/connect/); }); - test('clicking Overview nav item shows overview page', async ({ page }) => { - await page.locator('button', { hasText: 'Overview' }).click(); - await expect(page.locator('text=Overview')).toBeVisible(); + test('unauthenticated user visiting /overview is redirected to /connect', async ({ page }) => { + await page.goto('/overview'); + await expect(page).toHaveURL(/\/connect/); + }); + + test('unauthenticated user visiting /contracts is redirected to /connect', async ({ page }) => { + await page.goto('/contracts'); + await expect(page).toHaveURL(/\/connect/); + }); +}); + +test.describe('Authenticated navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.evaluate(() => { + const store = window.__store; + if (store) { + store.getState().setConnectedAddress('GA2C5W4Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5'); + } + }); + }); + + test('sidebar click on Transactions updates URL to /transactions', async ({ page }) => { + await page.locator('aside button', { hasText: 'Transactions' }).click(); + await expect(page).toHaveURL(/\/transactions/); + }); + + test('sidebar click on Overview updates URL to /overview', async ({ page }) => { + await page.locator('aside button', { hasText: 'Overview' }).click(); + await expect(page).toHaveURL(/\/overview/); + }); + + test('sidebar click on Contracts updates URL to /contracts', async ({ page }) => { + await page.locator('aside button', { hasText: 'Contracts' }).click(); + await expect(page).toHaveURL(/\/contracts/); + }); + + test('sidebar click on Assets updates URL to /assets', async ({ page }) => { + await page.locator('aside button', { hasText: 'Assets' }).click(); + await expect(page).toHaveURL(/\/assets/); + }); + + test('sidebar click on Charts updates URL to /charts', async ({ page }) => { + await page.locator('aside button', { hasText: 'Charts' }).click(); + await expect(page).toHaveURL(/\/charts/); + }); + + test('browser back/forward updates active tab', async ({ page }) => { + await page.locator('aside button', { hasText: 'Contracts' }).click(); + await expect(page).toHaveURL(/\/contracts/); + + await page.locator('aside button', { hasText: 'Assets' }).click(); + await expect(page).toHaveURL(/\/assets/); + + await page.goBack(); + await expect(page).toHaveURL(/\/contracts/); + + await page.goBack(); + await expect(page).toHaveURL(/\/overview/); + + await page.goForward(); + await expect(page).toHaveURL(/\/contracts/); + }); + + test('direct navigation to /contracts renders contracts panel when connected', async ({ page }) => { + await page.goto('/contracts'); + await expect(page).toHaveURL(/\/contracts/); + await expect(page.locator('text=Contracts')).toBeVisible(); + }); + + test('direct navigation to /search renders search panel when connected', async ({ page }) => { + await page.goto('/search'); + await expect(page).toHaveURL(/\/search/); }); });