Skip to content
Open
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
7 changes: 7 additions & 0 deletions packages/oc-docs/e2e/tests/app/app.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';

test('app loads and renders the collection', async ({ page }) => {
await page.goto('/');
await expect(page.getByTestId('app-shell')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Bruno Testbench' }).first()).toBeVisible();
});
75 changes: 75 additions & 0 deletions packages/oc-docs/e2e/tests/routing/routing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { test, expect } from '@playwright/test';

const FIXTURE = '/?fixture=folders';
const page$ = (s: string) => `${FIXTURE}#/${s}`;

test.describe('page-based navigation', () => {
test('deep-link to a nested request renders only that page on fresh load', async ({ page }) => {
await page.goto(page$('bookings/lifecycle/create-booking'));

const active = page.getByTestId('page');
await expect(active).toHaveAttribute('data-page-slug', 'bookings/lifecycle/create-booking');
await expect(active).toHaveAttribute('data-page-type', 'request');
await expect(page.getByRole('heading', { name: 'Create Booking', level: 1 })).toBeVisible();

await expect(page.getByRole('heading', { name: 'Login', level: 1 })).toHaveCount(0);
});

test('breadcrumb reflects the folder hierarchy', async ({ page }) => {
await page.goto(page$('bookings/lifecycle/create-booking'));
const bc = page.getByTestId('breadcrumb');
await expect(bc).toContainText('Hotel API');
await expect(bc).toContainText('Bookings');
await expect(bc).toContainText('Lifecycle');
await expect(page.getByRole('heading', { name: 'Create Booking', level: 1 })).toBeVisible();
});

test('auto-expands ancestor folders so the deep-linked item is visible in the sidebar', async ({ page }) => {
await page.goto(page$('bookings/lifecycle/create-booking'));
await expect(page.getByTestId('sidebar-item').filter({ hasText: 'Cancel Booking' })).toBeVisible();
});

test('prev/next walks the hierarchy in sequence order', async ({ page }) => {
await page.goto(page$('bookings/lifecycle/create-booking'));

const next = page.getByTestId('next-link');
await expect(next).toContainText('Confirm Booking');
await next.click();

await expect(page.getByTestId('page')).toHaveAttribute(
'data-page-slug',
'bookings/lifecycle/confirm-booking'
);
await expect(page.getByTestId('prev-link')).toContainText('Create Booking');
await expect(page.getByTestId('next-link')).toContainText('Cancel Booking');
});

test('slug URL is stable across reload', async ({ page }) => {
await page.goto(page$('authentication/login'));
await expect(page.getByRole('heading', { name: 'Login', level: 1 })).toBeVisible();

await page.reload();
await expect(page).toHaveURL(/#\/authentication\/login$/);
await expect(page.getByRole('heading', { name: 'Login', level: 1 })).toBeVisible();
});

test('a script item renders as its own script page', async ({ page }) => {
await page.goto(page$('setup-script'));
const active = page.getByTestId('page');
await expect(active).toHaveAttribute('data-page-slug', 'setup-script');
await expect(active).toHaveAttribute('data-page-type', 'script');
await expect(page.getByRole('heading', { name: 'Setup Script', level: 1 })).toBeVisible();
});

test('unknown slug redirects to the overview', async ({ page }) => {
await page.goto(page$('does/not/exist'));
await expect(page.getByTestId('page')).toHaveAttribute('data-page-type', 'overview');
});

test('clicking a sidebar item navigates to its slug route', async ({ page }) => {
await page.goto(FIXTURE);
await page.locator('[data-testid="sidebar-item"][data-slug="authentication"]').click();
await expect(page.getByTestId('page')).toHaveAttribute('data-page-slug', 'authentication');
await expect(page).toHaveURL(/#\/authentication$/);
});
});
68 changes: 68 additions & 0 deletions packages/oc-docs/src/components/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useCallback, useEffect, useState } from 'react';
import type { HttpRequest } from '@opencollection/types/requests/http';
import type { Folder } from '@opencollection/types/collection/item';
import Topbar from '../Topbar/Topbar';
import Sidebar from '../Docs/Sidebar/Sidebar';
import PageRouter from '../PageRouter/PageRouter';
import PlaygroundDrawer from '../PlaygroundDrawer/PlaygroundDrawer';
import { useAppSelector } from '../../store/hooks';
import { selectDocsCollection } from '../../store/slices/docs';
import { selectPlaygroundCollection } from '../../store/slices/playground';
import { selectGitCollectionUrl } from '../../store/slices/app';
import { useActiveResolution } from '../../routing/hooks';
import { buildBrunoDeepLink } from '../../utils/buildBrunoDeepLink';
import { StyledWrapper } from './StyledWrapper';

interface AppShellProps {
logo?: React.ReactNode;
}

const AppShell: React.FC<AppShellProps> = ({ logo }) => {
const collection = useAppSelector(selectDocsCollection);
const playgroundCollection = useAppSelector(selectPlaygroundCollection);
const gitCollectionUrl = useAppSelector(selectGitCollectionUrl);
const resolution = useActiveResolution();

const [showDrawer, setShowDrawer] = useState(false);
const [playgroundItem, setPlaygroundItem] = useState<HttpRequest | Folder | null>(null);

const activeItem = resolution?.entry.item ?? null;
const activeType = resolution?.entry.type;
useEffect(() => {
if (activeItem && (activeType === 'request' || activeType === 'folder')) {
setPlaygroundItem(activeItem as HttpRequest | Folder);
}
}, [activeItem, activeType]);

const handleOpenPlayground = useCallback(() => setShowDrawer(true), []);

return (
<StyledWrapper className="appshell" data-testid="app-shell">
<Topbar
collectionName={collection?.info?.name || 'API Collection'}
version={collection?.info?.version}
logo={logo}
openInBrunoHref={buildBrunoDeepLink(gitCollectionUrl)}
/>

<div className="appshell-row">
<aside className="appshell-sidebar">
<Sidebar />
</aside>
<main className="appshell-content">
<PageRouter onOpenPlayground={handleOpenPlayground} />
</main>
</div>

<PlaygroundDrawer
isOpen={showDrawer}
onClose={() => setShowDrawer(false)}
collection={playgroundCollection}
selectedItem={playgroundItem}
onSelectItem={setPlaygroundItem}
/>
</StyledWrapper>
);
};

export default AppShell;
37 changes: 37 additions & 0 deletions packages/oc-docs/src/components/AppShell/StyledWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import styled from '@emotion/styled';

export const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100vh;
box-sizing: border-box;
.appshell-row {
display: flex;
flex: 1;
min-height: 0;
}
.appshell-sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
height: 100%;
overflow: hidden;
border-right: 1px solid var(--oc-border-border1, var(--border-color));
background-color: var(--oc-sidebar-bg);
}
.appshell-content {
flex: 1;
min-width: 0;
height: 100%;
overflow-y: auto;
overscroll-behavior-y: contain;
}
@media (max-width: 768px) {
.appshell-sidebar {
display: none;
}
}
`;
2 changes: 1 addition & 1 deletion packages/oc-docs/src/components/Docs/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const Item = memo(({
const renderBreadcrumb = () => {
if (breadcrumb.length === 0) return null;
return (
<div className="item-breadcrumb">
<div className="item-breadcrumb" data-testid="breadcrumb">
{breadcrumb.map((segment, i) => (
<span key={i}>
{i > 0 && <span className="breadcrumb-sep">/</span>}
Expand Down
Loading
Loading