Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions INTEGRATION_TESTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Integration Test Outlines for Public View System

This document outlines key integration tests for the public view system, including the `PublicHome` page and routing behaviors. These tests would typically be implemented using a framework like Cypress or Playwright, combined with React Testing Library for component interactions where appropriate.

## I. PublicHome Page (`client/src/pages/PublicHome.tsx`)

**Setup:**
* Ensure the backend server is running and accessible.
* Mock or seed the database with a known set of public and private snippets. Include snippets with various languages and tags.
* Use a testing utility or direct API calls to `/api/public/snippets` to verify data setup.

**Test Scenarios:**

1. **Initial Load and Display:**
* **Description:** Verify that `PublicHome` fetches and displays public snippets correctly on initial load.
* **Steps:**
1. Navigate to the `/` route as an unauthenticated user.
2. Observe that the `PublicHome` component renders.
3. Check that a list/grid of snippets is displayed.
4. Verify that only snippets marked `isPublic: true` in the database are shown.
5. Confirm that elements like the header, tagline, and "Sign In" button are present.
6. Check for loading states while data is being fetched.
* **Assertions:**
* Correct number of public snippets displayed.
* Snippet cards show appropriate information (title, description snippet, language, "Public" badge).
* No private snippets are visible.
* Header and sign-in elements are visible.

2. **Search Functionality:**
* **Description:** Test if searching filters the displayed public snippets.
* **Steps:**
1. On `PublicHome`, type a search term (e.g., a keyword from a known public snippet's title or description) into the search bar.
2. Observe the list of snippets updating.
* **Assertions:**
* Only snippets matching the search term are displayed.
* If the search term matches no public snippets, an appropriate "No public snippets found" message is shown.
* The filtering should be reasonably fast (client-side or server-side depending on implementation).

3. **Filter Functionality (Language/Tags):**
* **Description:** Test if filtering by language (and tags, if implemented) works correctly.
* **Steps:**
1. On `PublicHome`, select a language from the language filter dropdown.
2. Observe the list of snippets updating.
3. (If applicable) Select a tag from a tag filter dropdown and observe further filtering.
* **Assertions:**
* Only snippets matching the selected language (and/or tag) are displayed.
* Combining search and filters works as expected.
* If no snippets match the filter criteria, an appropriate message is shown.

4. **Empty State:**
* **Description:** Verify the behavior when no public snippets are available or match filters.
* **Steps:**
1. Ensure the database has no snippets marked `isPublic: true`.
2. Navigate to `PublicHome`.
3. OR: Apply filters that result in no matches.
* **Assertions:**
* The correct "No public snippets found" (or similar) message is displayed.
* The layout remains intact.

5. **Sign-In Button Navigation:**
* **Description:** Ensure the "Sign In" button navigates to the login flow.
* **Steps:**
1. On `PublicHome`, click the "Sign In / Sign Up" button.
* **Assertions:**
* The user is redirected to the application's login page/mechanism (e.g., `/login` or triggers the Firebase auth flow).

## II. Routing and Authentication State

**Setup:**
* As above, backend running with mixed public/private data.
* Ability to simulate user login/logout within the test environment.

**Test Scenarios:**

1. **Unauthenticated User Access:**
* **Description:** Verify routes accessible to unauthenticated users.
* **Steps:**
1. As an unauthenticated user, navigate to `/`.
2. Navigate to `/shared/:shareId` (using a share ID of a public snippet).
3. Navigate to `/shared/:shareId` (using a share ID of a private snippet).
4. Attempt to navigate to an authenticated route (e.g., `/snippets` or `/settings`).
* **Assertions:**
* `/` loads `PublicHome`.
* `/shared/:shareId` for a public snippet loads the `SharedSnippet` page and displays the snippet.
* `/shared/:shareId` for a private snippet either shows a "not found/access denied" message within `SharedSnippet` or redirects (behavior depends on `SharedSnippet` implementation).
* Access to `/snippets` or `/settings` redirects to `PublicHome` (or the login page).

2. **Authenticated User Access:**
* **Description:** Verify routes and UI changes for authenticated users.
* **Steps:**
1. Log in as a user.
2. Navigate to `/`.
3. Navigate to other authenticated routes like `/snippets`, `/collections`.
4. Navigate to `/shared/:shareId` (using a share ID of a snippet they own, and one they don't but is public).
* **Assertions:**
* `/` loads the authenticated dashboard (e.g., `Home.tsx` for authenticated users, not `PublicHome`).
* Authenticated routes are accessible and render correctly.
* Layout for authenticated users includes the sidebar and full header.
* Snippet cards on authenticated pages show owner controls for owned snippets.
* `SharedSnippet` page works correctly for owned and public shared snippets.

3. **Navigation from Public to Authenticated:**
* **Description:** Test the transition when a user signs in from `PublicHome`.
* **Steps:**
1. Start on `PublicHome` as an unauthenticated user.
2. Click "Sign In" and complete the login process.
* **Assertions:**
* After successful login, the user is redirected to the authenticated dashboard (e.g., `/`).
* The UI updates to the authenticated layout (sidebar appears, etc.).

## III. Shared Snippet Page (`client/src/pages/SharedSnippet.tsx`)

**Note:** These depend heavily on `SharedSnippet.tsx`'s internal logic, which also needs to be context-aware.

1. **Public Shared Snippet (Unauthenticated User):**
* **Description:** An unauthenticated user views a publicly shared snippet.
* **Assertions:** Snippet content is visible. No owner controls.
2. **Private Shared Snippet (Unauthenticated User):**
* **Description:** An unauthenticated user attempts to view a private shared snippet.
* **Assertions:** Snippet content is NOT visible. A "not found" or "access denied" message is shown.
3. **Private Shared Snippet (Authenticated Owner):**
* **Description:** The owner views their own private shared snippet.
* **Assertions:** Snippet content is visible. Owner controls might be visible (depends on design).
4. **Private Shared Snippet (Authenticated Non-Owner):**
* **Description:** An authenticated user (not the owner) attempts to view a private shared snippet.
* **Assertions:** Snippet content is NOT visible. A "not found" or "access denied" message.
5. **Public Shared Snippet (Authenticated User):**
* **Description:** An authenticated user views a public snippet (owned by someone else).
* **Assertions:** Snippet content is visible. No owner controls.
```
55 changes: 23 additions & 32 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import { SnippetProvider } from "@/contexts/SnippetContext";
import { CollectionProvider } from "@/contexts/CollectionContext";
import { useAuthContext } from "@/contexts/AuthContext";
import NotFound from "@/pages/not-found";
import Home from "@/pages/Home";
import Home from "@/pages/Home"; // This is the authenticated dashboard
import PublicHome from '@/pages/PublicHome';
import Snippets from "@/pages/Snippets";
import Collections from "@/pages/Collections";
import CollectionDetail from "@/pages/CollectionDetail";
import Tags from "@/pages/Tags";
import Settings from "@/pages/Settings";
import SharedSnippet from "@/pages/SharedSnippet";

function Router() {
// Renamed from Router to AuthenticatedRouter
function AuthenticatedRouter() {
return (
<Switch>
<Route path="/" component={Home} />
<Route path="/" component={Home} /> {/* Authenticated home/dashboard */}
<Route path="/snippets" component={Snippets} />
<Route path="/collections" component={Collections} />
<Route path="/collections/:id" component={CollectionDetail} />
Expand All @@ -32,6 +34,17 @@ function Router() {
);
}

function PublicRouter() {
return (
<Switch>
<Route path="/" component={PublicHome} />
<Route path="/shared/:shareId" component={SharedSnippet} />
{/* For non-matched routes, redirect to PublicHome */}
<Route component={PublicHome} />
</Switch>
);
}

// Added debug component to show authentication state
function AuthDebug({ user, loading }: { user: any, loading: boolean }) {
return (
Expand All @@ -48,7 +61,7 @@ function AuthDebug({ user, loading }: { user: any, loading: boolean }) {
}

export default function App() {
const { user, loading, signIn } = useAuthContext();
const { user, loading } = useAuthContext(); // Removed signIn from here as it's not used directly for button anymore
const [showDebug, setShowDebug] = useState(false);

// Add explicit debugging to track auth state
Expand Down Expand Up @@ -105,38 +118,16 @@ export default function App() {
);
}

// 2) Not signed in → show Google button
if (!user) {
console.log("[App] No user detected, showing login button");
return (
<div className="h-screen flex flex-col items-center justify-center">
<div className="mb-4">
<h1 className="text-xl font-bold mb-2">CodePatchwork</h1>
<p>Please sign in to access your snippets</p>
</div>
<button
onClick={() => {
console.log("[App] Sign in button clicked");
signIn();
}}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Sign in with Google
</button>
{showDebug && <AuthDebug user={user} loading={loading} />}
</div>
);
}

// 3) Signed in → render the full app
console.log("[App] User authenticated, rendering full app:", user);
// 2) Routing logic based on authentication state
// PublicHome will now handle the sign-in prompt.
console.log(`[App] Rendering routers. User: ${user ? user.id : 'null'}, Loading: ${loading}`);
return (
<ThemeProvider>
<CodeThemeProvider>
<SnippetProvider>
<CollectionProvider>
<SnippetProvider> {/* SnippetProvider might be needed by SharedSnippet too */}
<CollectionProvider> {/* CollectionProvider might be needed by SharedSnippet too */}
<TooltipProvider>
<Router />
{user ? <AuthenticatedRouter /> : <PublicRouter />}
{showDebug && <AuthDebug user={user} loading={loading} />}
</TooltipProvider>
</CollectionProvider>
Expand Down
35 changes: 20 additions & 15 deletions client/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import Header from "./Header";

interface LayoutProps {
children: ReactNode;
isPublicView?: boolean;
}

export default function Layout({ children }: LayoutProps) {
export default function Layout({ children, isPublicView = false }: LayoutProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

const toggleMobileMenu = () => {
Expand All @@ -19,25 +20,29 @@ export default function Layout({ children }: LayoutProps) {

return (
<div className="flex flex-col md:flex-row h-screen overflow-hidden">
{/* Sidebar - hidden on mobile, visible on larger screens */}
<Sidebar className="w-full md:w-64 bg-white dark:bg-gray-900 border-r border-slate-200 dark:border-slate-700 flex-shrink-0 hidden md:block h-full overflow-y-auto" />
{/* Sidebar - hidden on mobile, visible on larger screens, hidden in public view */}
{!isPublicView && (
<Sidebar className="w-full md:w-64 bg-white dark:bg-gray-900 border-r border-slate-200 dark:border-slate-700 flex-shrink-0 hidden md:block h-full overflow-y-auto" />
)}

{/* Mobile Sidebar */}
<div className={`fixed inset-0 z-40 ${mobileMenuOpen ? "" : "hidden"}`}>
<div
className="absolute inset-0 bg-black/50 dark:bg-black/70"
onClick={closeMobileMenu}
></div>
<div className={`absolute top-0 left-0 h-full w-64 bg-white dark:bg-gray-900 border-r border-slate-200 dark:border-slate-700 z-50 transform transition-transform duration-300 ${mobileMenuOpen ? "" : "-translate-x-full"}`}>
<Sidebar onClose={closeMobileMenu} />
{/* Mobile Sidebar - Entire mechanism hidden in public view */}
{!isPublicView && (
<div className={`fixed inset-0 z-40 ${mobileMenuOpen ? "" : "hidden"}`}>
<div
className="absolute inset-0 bg-black/50 dark:bg-black/70"
onClick={closeMobileMenu}
></div>
<div className={`absolute top-0 left-0 h-full w-64 bg-white dark:bg-gray-900 border-r border-slate-200 dark:border-slate-700 z-50 transform transition-transform duration-300 ${mobileMenuOpen ? "" : "-translate-x-full"}`}>
<Sidebar onClose={closeMobileMenu} />
</div>
</div>
</div>
)}

<div className="flex-grow flex flex-col overflow-hidden">
{/* Header */}
<Header toggleMobileMenu={toggleMobileMenu} />
{/* Header - Pass isPublicView to handle mobile menu toggle visibility */}
<Header toggleMobileMenu={toggleMobileMenu} isPublicView={isPublicView} />

{/* Main Content Area - FIXED */}
{/* Main Content Area - Should expand if sidebar is not present */}
<main className="flex-1 overflow-y-auto p-4 bg-slate-50 dark:bg-gray-800 min-h-0">
{children}
</main>
Expand Down
Loading
Loading