{t("api.workspace.deleteConfirm")}
+ )} +{selected.prefix}{t("api.newKeyNote")}
+{newKey}
+
+
+ {endpoint}
+ {t("api.endpointNote")}
+{`curl ${endpoint} \\
+ -H "Authorization: Bearer ocx_YOUR_KEY_HERE" \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "model": "gpt-5.4",
+ "input": ${JSON.stringify(t("api.usageSampleInput"))}
+ }'`}
+ chips; split the localized string on both tokens.
const subtitleParts = t("api.subtitle").split(/\{authHeader\}|\{altHeader\}/);
+ if (workspaceView) {
+ return (
+
+
+ {t("api.title")}
+
+ handleCreate(name)}
+ onDismissNewKey={() => setNewKey(null)}
+ onCopyNewKey={copyKey}
+ onDelete={id => { void handleDelete(id); }}
+ />
+
+ );
+ }
+
return (
@@ -131,7 +164,7 @@ export default function ApiKeys({ apiBase }: { apiBase: string }) {
onChange={e => setNewName(e.target.value)}
className="input"
/>
-
@@ -178,7 +211,7 @@ export default function ApiKeys({ apiBase }: { apiBase: string }) {
-H "Content-Type: application/json" \\
-d '{
"model": "gpt-5.4",
- "input": "Hello, world!"
+ "input": ${JSON.stringify(t("api.usageSampleInput"))}
}'`}
diff --git a/gui/src/styles-apikeys-workspace.css b/gui/src/styles-apikeys-workspace.css
new file mode 100644
index 000000000..2eddf9fe1
--- /dev/null
+++ b/gui/src/styles-apikeys-workspace.css
@@ -0,0 +1,346 @@
+/* ============================================================================
+ apikeys-workspace — isolated stylesheet (mirrors providers-workspace layout DNA)
+ Namespace: apikeys-workspace- | widgets: awi-
+ Uses only design tokens from styles.css. No gradients.
+ ============================================================================ */
+
+.main-inner:has(.apikeys-workspace-shell) {
+ max-width: 1200px;
+}
+
+.apikeys-workspace-shell {
+ width: 100%;
+ min-width: 0;
+ container-type: inline-size;
+ container-name: apikeys-workspace;
+}
+
+.apikeys-workspace-root {
+ display: grid;
+ grid-template-columns: minmax(280px, 320px) minmax(0, 1fr);
+ gap: var(--space-4);
+ width: 100%;
+ max-width: 100%;
+ min-height: 480px;
+}
+
+/* ── Rail ─────────────────────────────────────────────── */
+
+.apikeys-workspace-rail {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+ border-right: 1px solid var(--border);
+ padding-right: var(--space-3);
+ min-width: 0;
+}
+
+.apikeys-workspace-rail-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.apikeys-workspace-rail-title {
+ font-size: var(--text-body);
+ font-weight: var(--weight-semibold);
+ color: var(--text);
+}
+
+.apikeys-workspace-rail-count {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--faint);
+ font-variant-numeric: tabular-nums;
+}
+
+.apikeys-workspace-rail-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ overflow-y: auto;
+ max-height: 560px;
+ min-height: 0;
+}
+
+.apikeys-workspace-rail-row {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-0-5);
+ width: 100%;
+ min-height: 46px;
+ appearance: none;
+ background: none;
+ border: none;
+ border-radius: var(--radius-sm);
+ padding: var(--space-1-5) var(--space-2);
+ cursor: pointer;
+ text-align: left;
+ color: inherit;
+ font: inherit;
+ overflow: hidden;
+ transition: background var(--motion-fast), box-shadow var(--motion-fast);
+}
+
+.apikeys-workspace-rail-row:hover {
+ background: var(--surface);
+}
+
+.apikeys-workspace-rail-row--selected {
+ background: var(--accent-soft);
+ box-shadow: inset 0 0 0 1px var(--border);
+}
+
+.apikeys-workspace-rail-row:focus-visible {
+ outline: 2px solid var(--accent-ring);
+ outline-offset: -2px;
+}
+
+.apikeys-workspace-rail-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: var(--text-body);
+ font-weight: var(--weight-medium);
+ line-height: var(--leading-ui);
+}
+
+.apikeys-workspace-rail-meta {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: var(--text-caption);
+ line-height: var(--leading-ui);
+ color: var(--muted);
+ font-family: var(--mono);
+}
+
+.apikeys-workspace-rail-empty {
+ font-size: 12px;
+ padding: 8px 4px;
+ color: var(--muted);
+}
+
+/* ── Main ─────────────────────────────────────────────── */
+
+.apikeys-workspace-main {
+ min-width: 0;
+ max-width: 100%;
+ overflow: hidden;
+ padding: 0 var(--space-2);
+}
+
+.awi-section {
+ margin-bottom: 20px;
+}
+
+.awi-section-title {
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--muted);
+ margin: 0 0 8px 0;
+}
+
+.awi-endpoint {
+ display: block;
+ font-family: var(--mono);
+ font-size: 0.82rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 10px 12px;
+ word-break: break-all;
+ color: var(--text);
+}
+
+.awi-endpoint-note {
+ font-size: 12px;
+ color: var(--muted);
+ margin-top: 6px;
+}
+
+.awi-generate-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.awi-generate-row .input {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.awi-newkey {
+ border: 1px solid var(--accent);
+ background: var(--accent-soft);
+ border-radius: var(--radius-sm);
+ padding: 12px;
+ margin-bottom: 20px;
+}
+
+.awi-newkey-title {
+ font-weight: var(--weight-semibold);
+ margin: 0 0 4px 0;
+ font-size: var(--text-body);
+}
+
+.awi-newkey-note {
+ font-size: 12px;
+ color: var(--muted);
+ margin: 0 0 10px 0;
+}
+
+.awi-newkey-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.awi-newkey-code {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-family: var(--mono);
+ font-size: 0.8rem;
+ word-break: break-all;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 8px 10px;
+}
+
+.awi-kv {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+}
+
+.awi-kv-row {
+ display: flex;
+ gap: 12px;
+ padding: 5px 0;
+ font-size: 0.84rem;
+ line-height: 1.4;
+}
+
+.awi-kv-row dt {
+ flex-shrink: 0;
+ width: 130px;
+ color: var(--muted);
+ font-weight: 400;
+}
+
+.awi-kv-row dd {
+ margin: 0;
+ flex: 1;
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.awi-kv-row dd code {
+ font-size: 0.8rem;
+}
+
+.awi-detail-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.awi-back {
+ margin-bottom: 8px;
+}
+
+.awi-detail-title {
+ font-size: 1.15rem;
+ font-weight: 600;
+ margin: 0;
+ min-width: 0;
+ word-break: break-all;
+}
+
+.awi-detail-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.awi-usage-pre {
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ line-height: 1.5;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 12px;
+ overflow-x: auto;
+ color: var(--text);
+ margin: 0;
+}
+
+.awi-empty {
+ border: 1px dashed var(--border);
+ border-radius: var(--radius-sm);
+ padding: 32px 20px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.awi-hint {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 24px 8px;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+/* ── Responsive ───────────────────────────────────────── */
+/* Keep @container and @media stacking rules in sync when changing breakpoints. */
+
+@container apikeys-workspace (max-width: 720px) {
+ .apikeys-workspace-root {
+ grid-template-columns: minmax(0, 1fr);
+ min-height: auto;
+ }
+ .apikeys-workspace-rail {
+ border-right: none;
+ padding-right: 0;
+ border-bottom: 1px solid var(--border);
+ padding-bottom: var(--space-3);
+ }
+ .apikeys-workspace-rail-list {
+ max-height: 320px;
+ }
+ .apikeys-workspace-main {
+ padding: var(--space-2) 0 0;
+ }
+}
+
+@media (max-width: 768px) {
+ .apikeys-workspace-root {
+ grid-template-columns: minmax(0, 1fr);
+ min-height: auto;
+ }
+ .apikeys-workspace-rail {
+ border-right: none;
+ padding-right: 0;
+ border-bottom: 1px solid var(--border);
+ padding-bottom: var(--space-3);
+ }
+ .apikeys-workspace-rail-list {
+ max-height: 320px;
+ }
+ .apikeys-workspace-main {
+ padding: var(--space-2) 0 0;
+ }
+}
diff --git a/gui/src/styles.css b/gui/src/styles.css
index dc4b7bff6..89e4d020c 100644
--- a/gui/src/styles.css
+++ b/gui/src/styles.css
@@ -17,6 +17,7 @@
@import "./styles-dashboard-workspace.css";
@import "./styles-subagents-workspace.css";
@import "./styles-usage-workspace.css";
+@import "./styles-apikeys-workspace.css";
:root {
/* default: follow the OS; [data-theme] below pins color-scheme so light-dark() obeys it */
diff --git a/gui/tests/apikeys-workspace.test.ts b/gui/tests/apikeys-workspace.test.ts
new file mode 100644
index 000000000..5add98107
--- /dev/null
+++ b/gui/tests/apikeys-workspace.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from "bun:test";
+
+test("ApiKeys uses global viewMode (no per-page toggle) and workspace shell", async () => {
+ const page = await Bun.file(new URL("../src/pages/ApiKeys.tsx", import.meta.url)).text();
+ const app = await Bun.file(new URL("../src/App.tsx", import.meta.url)).text();
+ const css = await Bun.file(new URL("../src/styles.css", import.meta.url)).text();
+
+ expect(page).toContain("viewMode");
+ expect(page).toContain("readViewMode");
+ expect(page).toContain("ApiKeysWorkspace");
+ expect(page).toContain('workspaceView = (viewMode ?? readViewMode()) === "workspace"');
+ expect(page).not.toContain("ocx-apikeys-view");
+ expect(page).not.toContain("pws.workspaceToggle");
+ expect(page).not.toContain("pws.classicToggle");
+
+ expect(app).toContain(" ");
+ expect(css).toContain('@import "./styles-apikeys-workspace.css"');
+});
+
+test("ApiKeys workspace avoids nested main and stacks via container query", async () => {
+ const src = await Bun.file(new URL("../src/components/apikeys-workspace/ApiKeysWorkspace.tsx", import.meta.url)).text();
+ const page = await Bun.file(new URL("../src/pages/ApiKeys.tsx", import.meta.url)).text();
+ const css = await Bun.file(new URL("../src/styles-apikeys-workspace.css", import.meta.url)).text();
+
+ expect(src).toContain('");
+ expect(src).toContain("e.nativeEvent.isComposing");
+ expect(page).toContain("creatingRef");
+ expect(page).toContain("if (creatingRef.current) return false");
+
+ expect(css).toContain("container-name: apikeys-workspace");
+ expect(css).toContain("container-type: inline-size");
+ expect(css).toContain("@container apikeys-workspace (max-width: 720px)");
+ expect(css).toContain("@media (max-width: 768px)");
+ expect(css).toContain("overflow-wrap: anywhere");
+ expect(css).not.toContain("word-break: break-word");
+});
diff --git a/gui/tests/apikeys-workspace.test.tsx b/gui/tests/apikeys-workspace.test.tsx
new file mode 100644
index 000000000..dc075db0c
--- /dev/null
+++ b/gui/tests/apikeys-workspace.test.tsx
@@ -0,0 +1,355 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act, useState } from "react";
+import type { Root } from "react-dom/client";
+import { LanguageProvider } from "../src/i18n/provider";
+import ApiKeysWorkspace, {
+ type ApiKeyEntry,
+ type ApiKeysWorkspaceProps,
+} from "../src/components/apikeys-workspace/ApiKeysWorkspace";
+
+const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previousGlobals: Record<(typeof globals)[number], unknown>;
+let testWindow: Window;
+
+const sampleKeys: ApiKeyEntry[] = [
+ { id: "k1", name: "alpha", prefix: "ocx_aaa", createdAt: "2026-01-01T00:00:00.000Z" },
+ { id: "k2", name: "beta", prefix: "ocx_bbb", createdAt: "2026-01-02T00:00:00.000Z" },
+];
+
+const FULL_SECRET = "ocx_FULL_SECRET_ONLY_ONCE";
+
+beforeEach(() => {
+ previousGlobals = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals;
+ testWindow = new Window({ url: "http://localhost/" });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ localStorage: { configurable: true, value: testWindow.localStorage },
+ });
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+});
+
+afterEach(() => {
+ testWindow.close();
+ for (const key of globals) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+function setInputValue(input: HTMLInputElement, value: string): void {
+ const proto = Object.getPrototypeOf(input) as HTMLInputElement;
+ const protoSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
+ protoSetter?.call(input, value);
+ input.dispatchEvent(new testWindow.Event("input", { bubbles: true }));
+}
+
+async function mountWorkspace(
+ props: Partial & Pick,
+): Promise<{ root: Root; container: HTMLElement; rerender: (next: Partial) => Promise }> {
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+
+ let latest: ApiKeysWorkspaceProps = {
+ keys: sampleKeys,
+ endpoint: "http://127.0.0.1:10100/v1/responses",
+ creating: false,
+ newKey: null,
+ copied: false,
+ onDismissNewKey: () => {},
+ onCopyNewKey: () => {},
+ onDelete: () => {},
+ ...props,
+ };
+
+ function Harness({ value }: { value: ApiKeysWorkspaceProps }) {
+ return (
+
+
+
+ );
+ }
+
+ let root!: Root;
+ await act(async () => {
+ root = createRoot(container);
+ root.render( );
+ });
+
+ return {
+ root,
+ container,
+ async rerender(next) {
+ latest = { ...latest, ...next };
+ await act(async () => {
+ root.render( );
+ });
+ },
+ };
+}
+
+function overviewButton(container: HTMLElement): HTMLButtonElement {
+ return [...container.querySelectorAll(".apikeys-workspace-rail-row")]
+ .find(el => el.textContent?.includes("Overview"))!;
+}
+
+function keyButton(container: HTMLElement, name: string): HTMLButtonElement {
+ return [...container.querySelectorAll(".apikeys-workspace-rail-row")]
+ .find(el => el.querySelector(".apikeys-workspace-rail-name")?.textContent === name)!;
+}
+
+function nameInput(container: HTMLElement): HTMLInputElement {
+ return container.querySelector('input[aria-label="Key name (optional)"]')!;
+}
+
+function generateButton(container: HTMLElement): HTMLButtonElement {
+ return [...container.querySelectorAll("button")]
+ .find(el => /Generate|Creating/.test(el.textContent ?? ""))!;
+}
+
+test("workspace overview navigation preserves pending secret and resets delete confirm", async () => {
+ const { root, container } = await mountWorkspace({
+ newKey: FULL_SECRET,
+ onCreate: async () => false,
+ });
+
+ expect(container.textContent).toContain("Endpoint");
+ expect(container.textContent).toContain("Generate key");
+ expect(container.textContent).toContain("Usage example");
+ expect(container.querySelector(".awi-newkey-code")?.textContent).toBe(FULL_SECRET);
+ expect(container.querySelector("main")).toBeNull();
+ expect(container.querySelector('[role="main"]')).toBeNull();
+ expect(container.querySelector("section.apikeys-workspace-main")?.getAttribute("aria-label")).toBe("API key details");
+
+ await act(async () => { keyButton(container, "alpha").click(); });
+ expect(container.textContent).toContain("Key details");
+ expect(container.textContent).toContain("ocx_aaa");
+ expect(container.textContent).not.toContain(FULL_SECRET);
+
+ await act(async () => {
+ [...container.querySelectorAll("button")].find(b => b.textContent?.includes("Delete key"))!.click();
+ });
+ expect(container.textContent).toContain("Are you sure you want to delete this key?");
+
+ await act(async () => { overviewButton(container).click(); });
+ expect(container.textContent).toContain("Endpoint");
+ expect(container.textContent).toContain("Generate key");
+ expect(container.querySelector(".awi-newkey-code")?.textContent).toBe(FULL_SECRET);
+ expect(container.textContent).not.toContain("Are you sure you want to delete this key?");
+
+ await act(async () => { root.unmount(); });
+});
+
+test("workspace single-flight: repeated Enter and Generate while creating call onCreate once", async () => {
+ let calls = 0;
+ let resolveCreate!: (ok: boolean) => void;
+ const pending = new Promise(resolve => { resolveCreate = resolve; });
+
+ const { root, container, rerender } = await mountWorkspace({
+ creating: false,
+ onCreate: async () => {
+ calls += 1;
+ return pending;
+ },
+ });
+
+ await act(async () => {
+ setInputValue(nameInput(container), "once");
+ nameInput(container).dispatchEvent(new testWindow.KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
+ });
+ expect(calls).toBe(1);
+
+ await rerender({ creating: true });
+ expect(generateButton(container).disabled).toBe(true);
+ expect(nameInput(container).disabled).toBe(true);
+
+ await act(async () => {
+ nameInput(container).dispatchEvent(new testWindow.KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
+ generateButton(container).click();
+ });
+ expect(calls).toBe(1);
+
+ await act(async () => {
+ const event = new testWindow.KeyboardEvent("keydown", { key: "Enter", bubbles: true });
+ Object.defineProperty(event, "isComposing", { configurable: true, value: true });
+ nameInput(container).dispatchEvent(event);
+ });
+ // Even if composing flag is ignored by React's synthetic path here, creating/disabled still blocks.
+ expect(calls).toBe(1);
+
+ await act(async () => {
+ resolveCreate(true);
+ await pending;
+ });
+
+ await act(async () => { root.unmount(); });
+});
+
+test("workspace clears draft name only after successful create", async () => {
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+
+ let createImpl: (name: string) => Promise = async () => false;
+ const createCalls: string[] = [];
+
+ function Host() {
+ const [creating, setCreating] = useState(false);
+ return (
+
+ {}}
+ onCopyNewKey={() => {}}
+ onDelete={() => {}}
+ onCreate={async (name) => {
+ createCalls.push(name);
+ setCreating(true);
+ try {
+ return await createImpl(name);
+ } finally {
+ setCreating(false);
+ }
+ }}
+ />
+
+ );
+ }
+
+ let root!: Root;
+ await act(async () => {
+ root = createRoot(container);
+ root.render( );
+ });
+
+ await act(async () => {
+ setInputValue(nameInput(container), "keep-me");
+ });
+ expect(nameInput(container).value).toBe("keep-me");
+
+ createImpl = async () => false;
+ await act(async () => { generateButton(container).click(); });
+ await act(async () => { await Promise.resolve(); await Promise.resolve(); });
+ expect(createCalls.at(-1)).toBe("keep-me");
+ expect(nameInput(container).value).toBe("keep-me");
+
+ createImpl = async () => true;
+ await act(async () => { generateButton(container).click(); });
+ await act(async () => { await Promise.resolve(); await Promise.resolve(); });
+ expect(createCalls.at(-1)).toBe("keep-me");
+ expect(nameInput(container).value).toBe("");
+
+ await act(async () => { root.unmount(); });
+});
+
+test("IME composition Enter does not submit", async () => {
+ let calls = 0;
+ const { root, container } = await mountWorkspace({
+ onCreate: async () => {
+ calls += 1;
+ return true;
+ },
+ });
+
+ await act(async () => {
+ setInputValue(nameInput(container), "ime");
+ const event = new testWindow.KeyboardEvent("keydown", {
+ key: "Enter",
+ bubbles: true,
+ cancelable: true,
+ });
+ Object.defineProperty(event, "isComposing", { configurable: true, get: () => true });
+ Object.defineProperty(event, "keyCode", { configurable: true, value: 229 });
+ nameInput(container).dispatchEvent(event);
+ });
+
+ // React reads event.nativeEvent.isComposing from the dispatched KeyboardEvent when available.
+ // If happy-dom doesn't populate nativeEvent, the component still checks e.nativeEvent.isComposing —
+ // verify the handler path by also ensuring creating/disabled paths work above.
+ // Force through React's onKeyDown by constructing an event React will treat as composing:
+ await act(async () => {
+ const input = nameInput(container);
+ const handler = (input as unknown as { onclick?: unknown }).onclick;
+ void handler;
+ const reactPropsKey = Object.keys(input).find(k => k.startsWith("__reactProps$"));
+ if (reactPropsKey) {
+ const props = (input as unknown as Record void }>)[reactPropsKey];
+ props?.onKeyDown?.({
+ key: "Enter",
+ preventDefault() {},
+ nativeEvent: { isComposing: true },
+ });
+ }
+ });
+ expect(calls).toBe(0);
+
+ await act(async () => { root.unmount(); });
+});
+
+test("parent creatingRef blocks duplicate submissions before rerender", async () => {
+ const creatingRef = { current: false };
+ let posts = 0;
+ const handleCreate = async (): Promise => {
+ if (creatingRef.current) return false;
+ creatingRef.current = true;
+ try {
+ posts += 1;
+ await Promise.resolve();
+ return true;
+ } finally {
+ creatingRef.current = false;
+ }
+ };
+
+ const first = handleCreate();
+ const second = handleCreate();
+ expect(await Promise.all([first, second])).toEqual([true, false]);
+ expect(posts).toBe(1);
+});
+
+test("App keeps the sole main landmark; workspace never nests main", async () => {
+ const app = await Bun.file(new URL("../src/App.tsx", import.meta.url)).text();
+ const workspace = await Bun.file(new URL("../src/components/apikeys-workspace/ApiKeysWorkspace.tsx", import.meta.url)).text();
+ const page = await Bun.file(new URL("../src/pages/ApiKeys.tsx", import.meta.url)).text();
+
+ expect(app).toContain(' {
+ const { root, container } = await mountWorkspace({
+ newKey: FULL_SECRET,
+ onCreate: async () => false,
+ });
+ const rail = container.querySelector(".apikeys-workspace-rail")!;
+ expect(rail.textContent).not.toContain(FULL_SECRET);
+ expect(rail.textContent).toContain("ocx_aaa");
+ expect(container.querySelector(".awi-newkey-code")?.textContent).toBe(FULL_SECRET);
+
+ await act(async () => { keyButton(container, "beta").click(); });
+ expect(container.querySelector(".awi-detail")?.textContent).not.toContain(FULL_SECRET);
+ expect(container.querySelector(".awi-detail")?.textContent).toContain("ocx_bbb");
+
+ await act(async () => { overviewButton(container).click(); });
+ expect(container.querySelector(".awi-newkey-code")?.textContent).toBe(FULL_SECRET);
+
+ await act(async () => { root.unmount(); });
+});