Skip to content
29 changes: 14 additions & 15 deletions e2e/share.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,61 +12,48 @@ 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);
} catch (error) {
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();
});

Expand All @@ -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();
Comment thread
tirth707 marked this conversation as resolved.

await expect(page.locator('.agreement')).toContainText('Acme Corp', { timeout: 15000 });
Comment thread
tirth707 marked this conversation as resolved.
});
});
58 changes: 33 additions & 25 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect, useState } 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 LearnNow from "./pages/LearnNow";
Expand All @@ -18,41 +20,48 @@ 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.");
void init();
}
Comment thread
tirth707 marked this conversation as resolved.
Comment thread
tirth707 marked this conversation as resolved.
}, [globalError, init]);
Comment thread
tirth707 marked this conversation as resolved.
Outdated

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=")) {
Comment thread
tirth707 marked this conversation as resolved.
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();
}
Comment thread
tirth707 marked this conversation as resolved.
setLoading(false);
};
void initializeApp();
}, [init, loadFromLink, searchParams, navigate]);
Expand Down Expand Up @@ -93,7 +102,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);
Expand Down
Loading