diff --git a/e2e/share.spec.ts b/e2e/share.spec.ts index 55fe344c..f74ab8b6 100644 --- a/e2e/share.spec.ts +++ b/e2e/share.spec.ts @@ -12,38 +12,30 @@ test.describe('Share Functionality', () => { }); test('should copy shareable link to clipboard on Share click', async ({ page, context }) => { - // Grant clipboard permissions await context.grantPermissions(['clipboard-read', 'clipboard-write']); const shareButton = page.getByRole('button', { name: 'Share' }); await shareButton.click(); - // Should show success message await expect(page.getByText('Link copied to clipboard')).toBeVisible({ timeout: 5000 }); - // Verify clipboard contains the link with data parameter const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toContain('data='); }); test('should load template from shared URL', async ({ page }) => { - // First, get a shareable link by clicking share await page.context().grantPermissions(['clipboard-read', 'clipboard-write']); const shareButton = page.getByRole('button', { name: 'Share' }); await shareButton.click(); - // Wait for clipboard to be populated await expect(page.getByText('Link copied to clipboard')).toBeVisible({ timeout: 5000 }); - // Get the shareable link from clipboard const shareableLink = await page.evaluate(() => navigator.clipboard.readText()); - // Validate that we got a non-empty string expect(shareableLink, 'Shareable link should be a non-empty string').toBeTruthy(); expect(typeof shareableLink, 'Shareable link should be a string').toBe('string'); - // Parse URL with validation let url: URL; try { url = new URL(shareableLink); @@ -51,22 +43,17 @@ test.describe('Share Functionality', () => { throw new Error(`Failed to parse shareable link as URL: "${shareableLink}". Error: ${error}`); } - // The app uses hash-based routing (#data=...) not query params (?data=...) - // Extract data from hash fragment - const hashParams = new URLSearchParams(url.hash.slice(1)); // Remove leading # + const hashParams = new URLSearchParams(url.hash.slice(1)); const dataParam = hashParams.get('data'); expect( dataParam, `URL should contain a "data" parameter in hash. Received URL: "${shareableLink}"` ).toBeTruthy(); - // Navigate to the shareable link using the hash await page.goto(`/#data=${dataParam}`); - // Wait for app to load await expect(page.locator('.app-spinner-container')).toBeHidden({ timeout: 30000 }); - // Verify the app loaded successfully with content await expect(page.locator('.main-container-agreement')).toBeVisible(); }); @@ -79,4 +66,16 @@ test.describe('Share Functionality', () => { const settingsButton = page.getByRole('button', { name: 'Settings' }); await expect(settingsButton).toBeVisible(); }); -}); + + test('should display error toast and fallback to default state on corrupted share link', async ({ page }) => { + await page.goto('/#data=invalid_garbage_base64_string'); + + // Updated to 30000 to match the rest of the suite + await expect(page.locator('.app-spinner-container')).toBeHidden({ timeout: 30000 }); + + const errorMessage = page.locator('.ant-message-notice-content', { hasText: 'Failed to load shared workspace. The link data may be corrupted.' }); + await expect(errorMessage).toBeVisible(); + + await expect(page.locator('.agreement')).toContainText('Acme Corp', { timeout: 15000 }); + }); +}); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 291095ae..f3bd3cd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ import { useEffect, useState, lazy, Suspense } from "react"; -import { App as AntdApp, Layout, Spin } from "antd"; +import { App as AntdApp, Layout, Spin, message } from "antd"; import { LoadingOutlined } from "@ant-design/icons"; import { Routes, Route, useSearchParams, useNavigate } from "react-router-dom"; +import { shallow } from "zustand/shallow"; +import { useStoreWithEqualityFn } from "zustand/traditional"; import Navbar from "./components/Navbar"; import tour from "./components/Tour"; import useAppStore from "./store/store"; @@ -19,41 +21,52 @@ const App = () => { const navigate = useNavigate(); const init = useAppStore((state) => state.init); const loadFromLink = useAppStore((state) => state.loadFromLink); - const { isAIConfigOpen, setAIConfigOpen } = - useAppStore((state) => ({ + const globalError = useAppStore((state) => state.error); + + const { isAIConfigOpen, setAIConfigOpen } = useStoreWithEqualityFn( + useAppStore, + (state) => ({ isAIConfigOpen: state.isAIConfigOpen, setAIConfigOpen: state.setAIConfigOpen, - })); + }), + shallow + ); + const backgroundColor = useAppStore((state) => state.backgroundColor); const textColor = useAppStore((state) => state.textColor); const [loading, setLoading] = useState(true); const [searchParams] = useSearchParams(); + useEffect(() => { + if (globalError && globalError.includes("Failed to load shared content:")) { + void message.error("Failed to load shared workspace. The link data may be corrupted."); + // Clear legacy ?data= query param so init() does not re-trigger the same failing loadFromLink + if (window.location.search.includes("data=")) { + navigate(window.location.pathname + window.location.hash, { replace: true }); + } + void init(); + } + }, [globalError, init, navigate]); useEffect(() => { const initializeApp = async () => { - try { - setLoading(true); - // Prioritize hash for new links, fallback to searchParams for old links - let compressedData: string | null = null; - if (window.location.hash.startsWith("#data=")) { - compressedData = window.location.hash.substring(6); - } else { - compressedData = searchParams.get("data"); - } - if (compressedData) { - await loadFromLink(compressedData); - if (window.location.pathname !== "/") { - navigate("/", { replace: true }); - } - } else { - await init(); + setLoading(true); + let compressedData: string | null = null; + if (window.location.hash.startsWith("#data=")) { + compressedData = window.location.hash.substring(6); + } else { + compressedData = searchParams.get("data"); + } + + if (compressedData) { + await loadFromLink(compressedData); + if (window.location.pathname !== "/") { + navigate("/", { replace: true }); } - } catch (error) { - console.error("Initialization error:", error); - } finally { - setLoading(false); + } else { + await init(); } + setLoading(false); }; void initializeApp(); }, [init, loadFromLink, searchParams, navigate]); @@ -94,7 +107,6 @@ const App = () => { } }, [searchParams]); - // Set data-theme attribute on initial load and when theme changes useEffect(() => { const theme = backgroundColor === "#121212" ? "dark" : "light"; document.documentElement.setAttribute("data-theme", theme); @@ -126,7 +138,7 @@ const App = () => { ) : (
-
}> + }> @@ -139,10 +151,10 @@ const App = () => { } /> - }> + }> }