Files
PaperClipAI/tests/e2e/sidebar-takeover.spec.ts
T
871f7d1124 fix(ui): polish core navigation and task layout (#12793)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Operators use the main navigation, contextual navigation, and task
chat throughout the product.
> - The recent core UI refactor left uneven spacing and inconsistent
navigation styles.
> - The Apps label also did not match the Connectors product language.
> - The account area did not provide a clear direct path for feedback.
> - This pull request aligns these related core UI surfaces and
preserves their existing behavior.
> - The benefit is a more consistent interface with clearer navigation
and balanced task-chat layout.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

This improves the core sidebar, Settings navigation, Connectors catalog,
task-chat layout, and account controls.

**Subsystem affected**

`ui/` — React and Vite board UI.

**Current behavior**

The task chat had uneven edge treatment. Settings used a separate
contextual-navigation style. Apps used inconsistent product labels. The
account footer did not expose a direct feedback control.

**Proposed behavior**

The task chat keeps balanced content padding while its scrollbar sits at
the properties boundary. Settings replaces the primary sidebar with a
matching navigation surface and a Back to app link. Apps uses Connectors
and Browse labels. The account footer provides a dedicated feedback icon
with a tooltip.

**Reason and benefit**

These changes make related navigation and layout patterns predictable.
They reduce duplicate labels and improve access to feedback.

**Breaking changes**

None. Routes, APIs, and stored data do not change.

## What Changed

- Balanced the task-chat content gutter and moved its scrollbar to the
properties-panel boundary.
- Reworked Settings navigation to replace the main sidebar and use the
shared primary-sidebar style.
- Added a Back to app navigation item to Settings.
- Renamed Apps to Connectors in the main navigation and added the
`Unplug` icon.
- Renamed the Connectors contextual item to Browse.
- Added the Connectors top-level header and aligned the search field
with the connector cards.
- Added account-footer hover states and a direct feedback flag with a
Share feedback tooltip.
- Removed the duplicate Feedback item from the account popover.
- Added regression coverage for each changed UI surface.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/AppsSidebar.test.tsx
src/components/CompanySettingsSidebar.test.tsx
src/components/Layout.test.tsx src/components/Sidebar.test.tsx
src/components/SidebarAccountMenu.test.tsx
src/components/task-chat/TaskMessageScroller.test.tsx
src/pages/apps/Browse.test.tsx` — 90 tests passed.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm --filter @paperclipai/ui build` — passed.
- `pnpm check:token-gates` — passed.
- `git diff --check origin/master...HEAD` — passed.
- `env PAPERCLIP_PLAYWRIGHT_CHANNEL=chrome PAPERCLIP_E2E_PORT=3201 pnpm
exec playwright test --config tests/e2e/playwright.config.ts
tests/e2e/apps-dark-mode-shots.spec.ts
tests/e2e/sidebar-takeover.spec.ts` — 10 tests passed.
- The full workspace typecheck and build reached the Rust runner and
stopped because `cargo` is not installed on this machine.
- The full test suite exposed unrelated server and workspace-runtime
failures and was stopped after the affected suites completed. No changed
UI test failed.
- Manually verified the changed Settings, Connectors, task-chat, and
account-menu surfaces in the running app.

## Risks

- Low risk. The change affects layout and navigation presentation only.
- The Settings sidebar now replaces the main sidebar by design. Users
must use Back to app to return to the application navigation.
- The task scrollbar offset depends on the existing responsive page
gutters. Regression tests cover both narrow and desktop spacing.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, `gpt-5.6-sol`, extended reasoning with tool use and code
execution. The host does not expose the context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-03 15:35:39 -07:00

113 lines
4.6 KiB
TypeScript

import { test, expect, request as pwRequest, type APIRequestContext } from "@playwright/test";
/**
* E2E: contextual sidebar companion model.
*
* Most contextual routes render their navigation beside the stable global
* sidebar. Settings intentionally takes over that sidebar while preserving the
* account menu, and its Back to app link restores the global navigation.
*
* Plugin route sidebars share the same Layout path. A live plugin-route test
* requires a plugin fixture, so that branch remains covered by Layout tests.
*/
const PORT = Number(process.env.PAPERCLIP_E2E_PORT ?? 3199);
const BASE_URL = `http://127.0.0.1:${PORT}`;
const COMPANY_NAME_PREFIX = "E2E-SidebarTakeover";
const COLLAPSED_STORAGE_KEY = "paperclip.sidebar.collapsed";
const APP_SIDEBAR_EXPANDED_MARKER = "Collapse sidebar";
async function createCompany(board: APIRequestContext): Promise<{ id: string; prefix: string }> {
const healthRes = await board.get(`${BASE_URL}/api/health`);
expect(healthRes.ok()).toBe(true);
const health = await healthRes.json();
expect(health.deploymentMode).toBe("local_trusted");
const companyRes = await board.post(`${BASE_URL}/api/companies`, {
data: { name: `${COMPANY_NAME_PREFIX}-${Date.now()}` },
});
if (!companyRes.ok()) {
throw new Error(`POST /api/companies → ${companyRes.status()}: ${await companyRes.text()}`);
}
const company = await companyRes.json();
return {
id: company.id,
prefix: company.issuePrefix ?? company.prefix ?? company.urlKey ?? "E2E",
};
}
test.describe("Contextual sidebar companion", () => {
let board: APIRequestContext;
let companyId: string;
let prefix: string;
test.beforeAll(async () => {
board = await pwRequest.newContext({ baseURL: BASE_URL });
const company = await createCompany(board);
companyId = company.id;
prefix = company.prefix;
});
test.afterAll(async () => {
await board.delete(`${BASE_URL}/api/companies/${companyId}`).catch(() => {});
await board.dispose();
});
test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.removeItem(key);
window.sessionStorage.clear();
}, COLLAPSED_STORAGE_KEY);
});
test("replaces global navigation with Settings navigation", async ({ page }) => {
await page.goto(`/${prefix}/company/settings`);
const contextual = page.locator('[data-contextual-sidebar="settings"]');
await expect(contextual).toBeVisible();
await expect(contextual).toHaveCount(1);
await expect(page.locator("[data-secondary-sidebar]")).toHaveCount(1);
await expect(contextual.getByRole("link", { name: "General" })).toBeVisible();
await expect(contextual.getByText("Environments", { exact: true })).toBeVisible();
await expect(contextual.getByRole("link", { name: "Back to app" })).toBeVisible();
await expect(page.getByRole("button", { name: "Open account menu" })).toBeVisible();
await expect(page.getByRole("link", { name: "Dashboard" })).toHaveCount(0);
await expect(page.getByLabel(APP_SIDEBAR_EXPANDED_MARKER)).toHaveCount(0);
});
test("renders contextual labels at full width", async ({ page }) => {
await page.goto(`/${prefix}/company/settings`);
const contextual = page.locator('[data-contextual-sidebar="settings"]');
const envLabel = contextual.getByText("Environments", { exact: true });
await expect(envLabel).toBeVisible();
const labelBox = await envLabel.boundingBox();
expect(labelBox).not.toBeNull();
expect(labelBox!.width).toBeGreaterThan(20);
});
test("keeps the retired collapse control absent across contextual navigation", async ({ page }) => {
await page.goto(`/${prefix}/company/settings`);
await expect(page.locator('[data-contextual-sidebar="settings"]')).toBeVisible();
await expect(page.getByRole("link", { name: "Back to app" })).toBeVisible();
await expect(page.getByLabel(APP_SIDEBAR_EXPANDED_MARKER)).toHaveCount(0);
await page.goto(`/${prefix}/dashboard`);
await expect(page.locator("[data-contextual-sidebar]")).toHaveCount(0);
await expect(page.getByRole("link", { name: "Dashboard" })).toBeVisible();
await expect(page.getByLabel(APP_SIDEBAR_EXPANDED_MARKER)).toHaveCount(0);
});
test("uses Dashboard as the destination for a direct Settings link", async ({ page }) => {
await page.goto(`/${prefix}/company/settings`);
await page.getByRole("link", { name: "Back to app" }).click();
await expect(page).toHaveURL(new RegExp(`/${prefix}/dashboard$`));
await expect(page.locator("[data-contextual-sidebar]")).toHaveCount(0);
await expect(page.getByRole("link", { name: "Dashboard" })).toBeVisible();
});
});