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
30 changes: 30 additions & 0 deletions apps/desktop/e2e/workbench.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ test.describe("desktop workbench quality baseline", () => {
expect(layout.scrollHeight).toBeLessThanOrEqual(layout.clientHeight);
});

test("keeps Capture Profile controls usable at minimum size", async ({ page }) => {
await page.setViewportSize(MINIMUM_VIEWPORT);
await page.goto("/");
await page.getByRole("button", { name: "Settings" }).click();
const dialog = page.getByRole("dialog", { name: "Settings & diagnostics" });
await dialog.getByRole("tab", { name: "Profiles" }).click();

await expect(dialog.getByRole("textbox", { name: "Profile name" })).toHaveValue("OpenAI default");
await expect(dialog.getByRole("textbox", { name: "Gateway origin" })).toHaveValue("https://api.openai.com");
await expect(dialog.getByRole("textbox", { name: "Additional capture hosts" })).toBeVisible();
await expect(dialog.getByText("Pause capture", { exact: true })).toBeVisible();

const layout = await dialog.evaluate((element) => {
const content = element.querySelector<HTMLElement>(".profile-content");
return {
dialogClientWidth: element.clientWidth,
dialogScrollWidth: element.scrollWidth,
contentClientWidth: content?.clientWidth ?? 0,
contentScrollWidth: content?.scrollWidth ?? 0,
};
});
expect(layout.dialogScrollWidth).toBe(layout.dialogClientWidth);
expect(layout.contentScrollWidth).toBe(layout.contentClientWidth);

const saveProfile = dialog.getByRole("button", { name: "Save Profile" });
await saveProfile.scrollIntoViewIfNeeded();
await expect(saveProfile).toBeVisible();
await expect(saveProfile).toBeDisabled();
});

test("keeps validated update recovery read-only and exportable", async ({ page }) => {
await page.setViewportSize(MINIMUM_VIEWPORT);
await page.goto("/?recoveryMode=1");
Expand Down
85 changes: 85 additions & 0 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { App } from "./App";
import fixture from "./data/workspace.json";
import type {
BetaMetricsPreview,
CaptureProfile,
ExportPreview,
ExportProfile,
SupportBundlePreview,
Expand Down Expand Up @@ -124,6 +125,9 @@ describe("request workbench", () => {
.toHaveAttribute("aria-selected", "true");
connectionTab.focus();
await user.keyboard("{ArrowRight}");
expect(within(dialog).getByRole("tab", { name: "Profiles" }))
.toHaveAttribute("aria-selected", "true");
await user.keyboard("{ArrowRight}");
expect(within(dialog).getByRole("tab", { name: "Metrics" }))
.toHaveAttribute("aria-selected", "true");
await user.keyboard("{ArrowRight}");
Expand Down Expand Up @@ -237,6 +241,7 @@ describe("request workbench", () => {

await user.click(screen.getByRole("button", { name: "Settings" }));
const settings = screen.getByRole("dialog", { name: "Settings & diagnostics" });
await user.click(within(settings).getByRole("tab", { name: "Profiles" }));
const settingsResults = await axe.run(settings, options);
expect(settingsResults.violations.map((violation) => violation.id)).toEqual([]);
await user.keyboard("{Escape}");
Expand Down Expand Up @@ -416,6 +421,86 @@ describe("request workbench", () => {
.toHaveAttribute("aria-selected", "true");
});

it("saves a normalized Capture Profile only from a paused Gateway", async () => {
const user = userEvent.setup();
window.__TAURI_INTERNALS__ = {};
let workspace = structuredClone(fixture) as unknown as WorkspaceBootstrap;
workspace.capture = {
...workspace.capture,
active: true,
canControl: true,
mode: "gateway",
endpoint: "http://127.0.0.1:8787",
};
vi.mocked(invoke).mockImplementation(async (command, args) => {
if (command === "bootstrap_workspace") return structuredClone(workspace);
if (command === "set_capture_active" && args?.active === false) {
workspace = {
...workspace,
capture: { ...workspace.capture, active: false },
compatibility: {
...workspace.compatibility,
code: "capture_paused",
status: "attention",
title: "Gateway capture paused",
action: "resume_capture",
},
};
return structuredClone(workspace);
}
if (command === "update_capture_profile") {
const profile = args?.profile as CaptureProfile;
workspace = {
...workspace,
captureProfile: {
...profile,
name: profile.name.trim(),
gatewayUpstream: new URL(profile.gatewayUpstream).origin,
additionalHosts: [...profile.additionalHosts].sort(),
},
capture: { ...workspace.capture, profile: `${profile.name.trim()} · Local Gateway` },
};
return structuredClone(workspace);
}
throw new Error(`Unexpected command: ${command}`);
});

render(<App />);
await user.click(await screen.findByRole("button", { name: "Settings" }));
const dialog = screen.getByRole("dialog", { name: "Settings & diagnostics" });
await user.click(within(dialog).getByRole("tab", { name: "Profiles" }));

const name = within(dialog).getByRole("textbox", { name: "Profile name" });
const gateway = within(dialog).getByRole("textbox", { name: "Gateway origin" });
const hosts = within(dialog).getByRole("textbox", { name: "Additional capture hosts" });
await user.clear(name);
await user.type(name, "Private lab");
await user.clear(gateway);
await user.click(gateway);
await user.paste("https://gateway.example.test/");
await user.click(hosts);
await user.paste("proxy.example.test\napi.private.test");

const saveProfile = within(dialog).getByRole("button", { name: "Save Profile" });
expect(within(dialog).queryByRole("alert")).not.toBeInTheDocument();
expect(saveProfile).toBeDisabled();
await user.click(within(dialog).getByRole("button", { name: "Pause" }));
await waitFor(() => expect(saveProfile).toBeEnabled());
await user.click(saveProfile);

expect(invoke).toHaveBeenCalledWith("set_capture_active", { active: false });
expect(invoke).toHaveBeenCalledWith("update_capture_profile", {
profile: {
version: "0.1",
name: "Private lab",
gatewayUpstream: "https://gateway.example.test/",
additionalHosts: ["api.private.test", "proxy.example.test"],
},
});
expect(await within(dialog).findByText("Profile saved")).toBeInTheDocument();
expect(gateway).toHaveValue("https://gateway.example.test");
});

it("returns an active proxy workspace to the safe Gateway from settings", async () => {
const user = userEvent.setup();
window.__TAURI_INTERNALS__ = {};
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { requestSearchText } from "./compare";
import type {
AnatomySection,
CaptureMode,
CaptureProfile,
CapturedRequest,
CertificateAuthority,
EvidenceLocator,
Expand All @@ -55,6 +56,7 @@ import {
setCaptureMode as persistCaptureMode,
subscribeToCaptureEvents,
uninstallCertificateAuthorityTrust,
updateCaptureProfile as persistCaptureProfile,
} from "./workspace";
import { formatRawJson, resolveEvidenceLocator, resolveEvidencePointer } from "./raw-evidence";
import type { ResolvedRawEvidence } from "./raw-evidence";
Expand Down Expand Up @@ -338,6 +340,15 @@ export function App() {
.finally(() => setCertificateChanging(false));
};

const changeCaptureProfile = async (profile: CaptureProfile) => {
const nextWorkspace = await persistCaptureProfile(profile);
setWorkspace(nextWorkspace);
setCaptureActive(nextWorkspace.capture.active);
setCaptureMode(nextWorkspace.capture.mode);
setCaptureError("");
return nextWorkspace.captureProfile;
};

if (loadError) {
return <LoadFailure detail={loadError} onRetry={() => setReloadToken((value) => value + 1)} />;
}
Expand Down Expand Up @@ -440,6 +451,7 @@ export function App() {
certificateChanging={certificateChanging}
onToggleCapture={toggleCapture}
onModeChange={changeCaptureMode}
onCaptureProfileChange={changeCaptureProfile}
onCertificateTrustChange={changeCertificateTrust}
onClose={closeSettings}
/>}
Expand Down
Loading
Loading