mirror of
https://github.com/block/buzz.git
synced 2026-11-03 07:51:22 +00:00
🤖 ## Requested rebase published —ef40744bRebased onto fetched main **47d068e2109d077414cbf2f4f1c927f6d051037a**, published **ef40744b3aeb4baaf8c81416e1a644fb5b315f91** with the exact expected-old `df7fad6a` force-with-lease. No merge. Manual conflicts were additive: preserve main's exact-key identity documentation alongside the availability contract, and retain both Bestie props and the shared availability reader in `UnifiedAgentsSection`. Range-diff confirms unchanged lifecycle policy: exact-key action-time authority, Unknown versus Offline, rejected shutdown retains record/memberships, and separate local/provider/owner gates. Main's exact-key profile routing survives. Both test-only CI synchronization repairs (natural toast expiry and bounded stderr wait) are byte-identical to the prior head. All ten original authors/messages/DCO/material coauthor trailers are preserved; configured signing policy was not changed. Fresh checks on the rebased candidate: - TypeScript, Biome on 26 changed TypeScript files, differential file-size gate, and diff whitespace: pass. - Focused production-hook/card/profile units: **58/58**. - Fresh E2E build, availability/deletion browser: **11/11**, no retries. - Main exact-key profile cases plus failed-DM send/startup retries: **6/6**, no retries. Previously reviewed full Desktop/buzz-agent package and mutation evidence is reused for unchanged behavior; no ceremonial full suite, new native/provider test, or `just ci` pass is claimed. Local configs, dependency links, and historical artifacts are preserved. Hosted observation: **MERGEABLE**, **BLOCKED / REVIEW_REQUIRED**, no new-head formal review. [CI 33699735990](https://github.com/block/buzz/actions/runs/33699735990) is running (including Rust and Desktop lanes), not a completed success. DCO and required Security aggregate passed at the observation; the separate Codex advisory review was skipped. No completed failing check or new inline feedback observed. Historical approvals are not new-head approvals. No reviewer/security authorization or merge action was performed. --- ## Feature summary and retained pre-rebase evidence ## Summary In Buzz Desktop, an agent could look online just because it had been started or deployed, even when there was no current sign it was connected. Cards and profiles now show availability from the agent's relay presence rather than a saved launch record, so you can distinguish an online agent from one that was merely deployed. Agents cards and profiles use presence reported through the shared server (the relay). A successful presence read with no online agent shows Offline; failed/disconnected evidence shows unknown, rather than retaining a misleading cached Online state. Lifecycle actions remain separate. An offline agent may still have a Shutdown action because the deployment record exists. Shutdown reports a **request**, not proof the process stopped. Offline does not imply that starting a duplicate agent is safe, and Online does not promise a response. ### Related issue Independent base: `main`; no stack parent or child among the replacements. Extracted from [#7114](https://github.com/block/buzz/pull/7114), retained as historical source (`98fe33ec`). [Behavior contract](f4bb2ed44e/docs/agent-availability.md). Originating [Buzz discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848) · channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`. ### Testing The same saved provider-backed agent, with only authored presence changing. These are mock-browser states, not a before/after deployment or live relay transport test; production UI is unchanged by the later fixture repairs. **No online presence:** gray dot, existing Shutdown control retained.  **Online presence:** green dot, same lifecycle control.  [Capture details](https://github.com/block/buzz/pull/7127#issuecomment-5482264824). To check manually, compare runtime-only transitions with presence updates, then disconnect/fail the presence read and verify it does not stay Online. A Shutdown request should not immediately claim confirmed termination. #### Historical pre-rebase evidence and limitations (df7fad6a) Lifecycle production source remains **`f4bb2ed44e5a989d93c5f51e93c0bbd2dca941be`**. Current published head is **`df7fad6ae65dda78508317186a95522d1bb22ed9`**: the prior browser synchronization at `b78d093e` plus an additive two-file Rust test-harness synchronization described below. No production bytes, dependency/configuration files, or prior commits were changed; no rebase. Current live main `0dbd036f5bff33e7ade75e7639f3218d424a6e73` has identical failing-test/toaster/send-flow source; the causal browser comparison used latest successfully tested main `04babf02655440b4dfd37f2e2df605ead0a030d8`. **Lifecycle/deletion correction:** both Agents and actual profile deletion now pass the shared exact-key availability reader, not raw cached data. It reads the canonical query state and connection at action time, including after awaited channel discovery. Failed/disconnected/pending evidence and unqueried persona siblings are unknown; successful missing means Offline only for a requested key. Successful background refetch cache remains usable; settled failure revokes it. No second cache or per-row polling was added. Provider record + channel + Online/Away/**unknown** awaits shutdown submission before local removal; rejection preserves record/membership for retry. Established Offline preserves intentional no-request removal. No route preserves warned local removal. Local agents retain native stop-before-remove, independent of presence. Profile consent now describes a shutdown **request**, not remote deletion or guaranteed termination. Existing ownership and force gates are unchanged. **Verified, reused exact-candidate validation:** the independently approved eleven-file patch (SHA-256 `2f69fe12ef0420e62dea1fd8db28cfa22cde5eecaf8080e656310a3e60d0cf86`) was committed without byte changes. Desktop **5,921 passed, 0 failed/skipped**, including **26 new mounted production hook/IPC regressions**; rebuilt availability browser suite **11/11 passed, no retries**, including four actual profile Delete journeys. Desktop check (existing 4 warnings/5 infos), typecheck, production/protected-feature artifact matrix, differential file-size/policy and diff checks passed. No blanket rerun or new full-repository `just ci` is claimed for this frontend correction. Production regressions cover cached Online **and Offline** failure/disconnection, genuine missing/Offline, pending, successful inflight refetch versus settled error, retained reader, error during awaited channel discovery, unqueried persona sibling, shutdown rejection/order/cancel, no route and local authority. Browser fixtures use safe mock IPC and a retained provider receipt, not a real deployment. Three restored mutation controls fail: unknown → skip shutdown (**15** regressions), Agents raw-cache reader (**6**), actual profile raw-cache caller (**1 browser journey**, false removal on failed cached Offline). Independent review approved the exact frozen bytes and added **4/4 cached-empty failure/disconnection probes** across both callers. This is local independent approval, not formal GitHub/A Team clearance. The prior native propagation/poll-count defects remain closed ([earlier response](https://github.com/block/buzz/pull/7127#issuecomment-5512278755)). The prior hover-popover correction at `b55423f6` remains covered by the full 11-journey browser run: pending/failed/disconnected means no badge or accessible status, genuine missing/Offline retains an Offline badge. Its earlier fallback-restoration mutation failed as expected (badge count 1 rather than 0); that historical witness is reused, not rerun. **Reused unchanged native/system boundary:** local `just ci` at `c59067d8` passed workspace/Tauri fmt/clippy, static/policy checks, Rust unit recipe, native workspace **3,159 passed / 20 ignored**, Web build and **2,019 mobile tests**. No native implementation changed in this lifecycle correction. These are historical boundary results, not new-head native/live certification. [Parent CI](https://github.com/block/buzz/actions/runs/33650549130) passed with **14 retry-recovered browser flakes**, not retry-free. Old-head CI/reviews are not current-head clearance. **Hosted gates:** [CI33662151103](https://github.com/block/buzz/actions/runs/33662151103) on `f4bb2ed4` **FAILED**: smoke shard1 had 322 pass, one failure, one retry-recovered flaky, two skipped. The failed first-DM retry test timed out on all three attempts because the error toast intercepted Send. That failure is preserved, not waived; the scoped test repair below is published as `b78d093e`. [CI33668171165](https://github.com/block/buzz/actions/runs/33668171165) on `b78d093e` subsequently **FAILED** the Rust unit budget regression described below. Both original failures remain visible; neither was retried to green. Exact `f4bb2ed4` and `b78d093e` APPROVED reviews cover unchanged reviewed bytes, not formal approval of the new head. Fresh exact-head CI and the established automated technical rereview are the next gates for `df7fad6a`. Historical deletion responses remain ([5092381800](https://github.com/block/buzz/pull/7127#issuecomment-5513759316), [5092391193](https://github.com/block/buzz/pull/7127#issuecomment-5513763497)). No formal review dismissed. The [security notice](https://github.com/block/buzz/pull/7127#issuecomment-5482231278) and latest-push maintainer/codeowner policy remain separate actionable gates: eligible Block organization members own current-range authorization. No merge/security authority exercised. **CI causal repair (`b78d093e`, test only):** the error `Message failed to send: Mock first DM send failed.` is deliberately injected by the existing fixture. CI screenshot and retry trace show the bottom-right Sonner notification over the actual enabled Send button. `fill()` leaves the pointer parked there; Sonner pauses its 4-second lifetime while hovered. A fast run can click before animation settles (unchanged local test passed in 2.7s; two actual tested-main CI cases passed first attempt in 3.3s), which does not disprove the failure. Independent controlled browser runs on `f4bb2ed4` and tested main `04babf` both reproduced the same toast hit-test at Send `(1203,627,32,32)`, persistent hover beyond 4s, and intercepted ordinary click with no second send. Moving the real pointer to the editor allows natural expiry and successful ordinary retry, preserving all original DM-channel/recipient assertions. This same synchronization already exists in the neighboring agent-startup-failure test. The one-file correction keeps the visible error assertion, scopes its toast locator, moves the pointer back to the editor and observes normal toast removal (bounded 10s) before retry. No forced click, direct toast dismissal, mocked clock, CSS override, skipped test, production behavior change, or new backend mock. Six focused browser executions pass (first-send/startup-failure, three repeats each, no retries); the held-toast control fails on original bytes at the Send click while the exact repaired test passes. Biome and diff checks pass. Reuse unchanged 5,921 Desktop / 11 availability browser / four independent probes above; no semantic production change warrants repeating those suites. Original failed CI attempt/retries, local fast pass, deliberate failing control and all traces remain in `WORK_LOGS/AVAILABILITY_CI_B9210A40`. Browser evidence is mock-IPC Chromium, not native/live-relay certification. The UI still temporarily overlays Send while a notification is hovered; the test exercises its real move-away/expiry recovery, not immediate click-through. **Rust CI causal repair (`df7fad6a`, test only):** [original Rust / Unit Tests failure, job100375291370](https://github.com/block/buzz/actions/runs/33668171165/job/100375291370) tested GitHub merge `223dee91a396d8cb4ebf18b9b8559e5a54951235`. `context_recovery_budget_exhaustion_surfaces_the_error` failed at `regressions.rs:2756` in **0.091s** because its immediate stderr snapshot lacked `context recovery budget spent`. ACP context-error assertions had already passed. The captured prefix shows all three budgets **32768 → 16384 → 8192 bytes**, above the 4096-byte floor, and ends during the third attempt. This is **not evidence of floor exhaustion**. The collector is an independent Tokio task; a stdout response is not a stderr barrier. Recovery, harness and test blobs were identical across the compared base/head/merge parents; no production regression was implicated. The shared test Harness now provides a bounded event/condition wait, registering for collector notifications before reading the buffer to avoid lost wakeups. The budget and adjacent terminal floor assertions wait for their own diagnostic and retain the matching snapshot. The budget test still requires the provider's ACP context error and exactly three recovery rungs, now corroborated by **exactly four provider calls** and no floor diagnostic. Timeout remains a real failure with captured stderr. No fixed sleep, weaker assertion, skip, provider-limit/logging change, dependency/config edit, or production change. **Deterministic causal control:** the same real agent/HTTP-provider/ACP scenario holds only stderr collection behind a one-shot gate until after stdout responds. The old immediate snapshot fails the original budget assertion (intentional exit101); the repaired wait explicitly remains Pending while held, then passes after release. No scheduler-speed assumption or fixed sleep. This reproduces the observation race under controlled delay, **not the exact historical CI schedule**. A missing-diagnostic test proves the wait actually times out. Original failure and deliberate failing-control logs/patch remain in `WORK_LOGS/RUST_TRIAGE_06E32D2D` and `WORK_LOGS/RUST_SYNC_BC5758B9`. **Final candidate validation:** focused recovery **9/9**, floor **1/1**, absent-diagnostic timeout **1/1** pass. One full touched-package run, `cargo test --locked -p buzz-agent`: **695 passed, 0 failed, 1 existing ignored**, including all **54 regressions**. Local nextest was unavailable, so this uses the repository-supported cargo-test fallback, not a claim of nextest reproduction. `cargo fmt --check`, package-scoped Clippy all-targets with `-D warnings`, differential file-size/policy and diff checks pass. Previously reviewed availability production and the Desktop/browser evidence above are unchanged and reused; no all-native blanket rerun. The test-only delta was self-reviewed against collector ordering, timeout and falsification evidence. Existing production approval remains valid for those bytes; exact-new-head technical/CI clearance is not assumed. The required **Security aggregate** is distinct from optional Codex advisory feedback; no security authorization, human review contact, or merge was requested. **Remaining policy limits:** shutdown submission is not harness acceptance or process termination; confirmed Offline/no-route local removal may leave a remote process; route discovery is best effort, membership cleanup uses `Promise.allSettled`, and multi-instance deletion is sequential/non-atomic. No distributed singleton, provider-health, tenant-switch cancellation, live relay TTL or packaged WebView/VoiceOver certification is claimed. The pre-existing DM-header raw-presence fallback (`ChannelScreenHeader`/`useActiveChannelHeader`) remains outside this repair and uncertified. Screenshots above remain historical mock-browser illustrations, not new deletion or native transport evidence. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2771 lines
95 KiB
TypeScript
2771 lines
95 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
||
|
||
import {
|
||
createMockAgentMemoryListing,
|
||
installMockBridge,
|
||
TEST_IDENTITIES,
|
||
} from "../helpers/bridge";
|
||
import { waitForAnimations } from "../helpers/animations";
|
||
import { expectEmojiMartStylesInstalled } from "../helpers/css";
|
||
import { openProfileMenu, openSettings } from "../helpers/settings";
|
||
|
||
async function expectHomeView(page: import("@playwright/test").Page) {
|
||
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
||
}
|
||
|
||
async function expandIdentity(page: import("@playwright/test").Page) {
|
||
const identity = page.getByTestId("profile-identity-card");
|
||
const isOpen = await identity.evaluate(
|
||
(element) => element instanceof HTMLDetailsElement && element.open,
|
||
);
|
||
if (!isOpen) {
|
||
await page.getByTestId("profile-identity-toggle").click();
|
||
}
|
||
}
|
||
|
||
async function selectFirstEmojiFromPicker(page: Page) {
|
||
const picker = page.locator("em-emoji-picker");
|
||
await expect(picker).toBeVisible();
|
||
await expect
|
||
.poll(() =>
|
||
picker.evaluate((element) =>
|
||
Boolean(element.shadowRoot?.querySelector(".scroll button")),
|
||
),
|
||
)
|
||
.toBe(true);
|
||
await picker.evaluate((element) => {
|
||
const button = element.shadowRoot?.querySelector(".scroll button");
|
||
if (!(button instanceof HTMLElement)) {
|
||
throw new Error("Emoji picker did not render an emoji button.");
|
||
}
|
||
button.click();
|
||
});
|
||
}
|
||
|
||
async function waitForAvatarEditorToClose(page: Page) {
|
||
await expect(page.getByTestId("profile-avatar-editor-shell")).toHaveCount(0);
|
||
}
|
||
|
||
async function waitForReactEffects(page: Page) {
|
||
await page.evaluate(
|
||
() =>
|
||
new Promise<void>((resolve) => {
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => resolve());
|
||
});
|
||
}),
|
||
);
|
||
}
|
||
|
||
function getHashSearchParam(page: Page, name: string) {
|
||
const hash = new URL(page.url()).hash.replace(/^#/, "");
|
||
const queryStart = hash.indexOf("?");
|
||
if (queryStart === -1) {
|
||
return null;
|
||
}
|
||
return new URLSearchParams(hash.slice(queryStart + 1)).get(name);
|
||
}
|
||
|
||
async function expectHashSearchParam(
|
||
page: Page,
|
||
name: string,
|
||
value: string | null,
|
||
) {
|
||
await expect.poll(() => getHashSearchParam(page, name)).toBe(value);
|
||
}
|
||
|
||
async function readVisibleProfileSurface(page: Page) {
|
||
const panel = page.getByTestId("user-profile-panel");
|
||
await expect(panel).toBeVisible();
|
||
|
||
return panel.evaluate((element) => {
|
||
const isVisible = (candidate: Element) => {
|
||
if (!(candidate instanceof HTMLElement)) return false;
|
||
const style = getComputedStyle(candidate);
|
||
const rect = candidate.getBoundingClientRect();
|
||
return (
|
||
style.display !== "none" &&
|
||
style.visibility !== "hidden" &&
|
||
rect.width > 0 &&
|
||
rect.height > 0
|
||
);
|
||
};
|
||
const visibleTestIds = Array.from(
|
||
element.querySelectorAll<HTMLElement>("[data-testid]"),
|
||
)
|
||
.filter(isVisible)
|
||
.map((candidate) => candidate.dataset.testid)
|
||
.filter((value): value is string => Boolean(value))
|
||
.filter(
|
||
(value) =>
|
||
(value.startsWith("user-profile-") ||
|
||
value.startsWith("agent-config-")) &&
|
||
!value.endsWith("resize-handle"),
|
||
)
|
||
.sort();
|
||
const visibleControls = Array.from(
|
||
element.querySelectorAll<HTMLElement>(
|
||
'button, [role="button"], [role="tab"], [role="switch"]',
|
||
),
|
||
)
|
||
.filter(isVisible)
|
||
.map((candidate) => ({
|
||
label:
|
||
candidate.getAttribute("aria-label") ??
|
||
candidate.textContent?.replace(/\s+/g, " ").trim() ??
|
||
"",
|
||
testId: candidate.dataset.testid ?? null,
|
||
}))
|
||
.filter(({ label, testId }) => label.length > 0 || testId !== null)
|
||
.filter(({ testId }) => !testId?.endsWith("resize-handle"))
|
||
.sort((left, right) =>
|
||
`${left.testId}:${left.label}`.localeCompare(
|
||
`${right.testId}:${right.label}`,
|
||
),
|
||
);
|
||
|
||
const activityChannelLabel = element
|
||
.querySelector<HTMLElement>(
|
||
'[data-testid="user-profile-activity-channel-label"]',
|
||
)
|
||
?.textContent?.replace(/\s+/g, " ")
|
||
.trim();
|
||
|
||
return {
|
||
activityChannelLabel: activityChannelLabel ?? null,
|
||
visibleControls,
|
||
visibleTestIds,
|
||
};
|
||
});
|
||
}
|
||
|
||
async function readOwnedAgentProfileContract(page: Page) {
|
||
const tabs = ["info", "runtime", "channels", "memories"] as const;
|
||
const contract: Partial<
|
||
Record<
|
||
(typeof tabs)[number],
|
||
Awaited<ReturnType<typeof readVisibleProfileSurface>>
|
||
>
|
||
> = {};
|
||
|
||
for (const tab of tabs) {
|
||
const trigger = page.getByTestId(`user-profile-tab-${tab}`);
|
||
await expect(trigger).toBeVisible();
|
||
if ((await trigger.getAttribute("data-state")) !== "active") {
|
||
await trigger.click();
|
||
}
|
||
await expect(trigger).toHaveAttribute("data-state", "active");
|
||
if (tab === "memories") {
|
||
await expect(page.getByTestId("agent-memory-section")).toBeVisible();
|
||
}
|
||
await waitForAnimations(page);
|
||
contract[tab] = await readVisibleProfileSurface(page);
|
||
}
|
||
|
||
await page.getByTestId("user-profile-tab-info").click();
|
||
await expect(page.getByTestId("user-profile-tab-info")).toHaveAttribute(
|
||
"data-state",
|
||
"active",
|
||
);
|
||
return contract;
|
||
}
|
||
|
||
async function addGenericAgent(
|
||
page: Page,
|
||
channelName: string,
|
||
agentName: string,
|
||
systemPrompt = "Watch the channel and help when asked.",
|
||
): Promise<string> {
|
||
await page.getByTestId(`channel-${channelName}`).click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
||
const channelId = await page
|
||
.getByTestId(`channel-${channelName}`)
|
||
.getAttribute("data-channel-id");
|
||
if (!channelId) {
|
||
throw new Error(`Channel ${channelName} is missing a data-channel-id.`);
|
||
}
|
||
|
||
await page.waitForFunction(() => {
|
||
return Boolean(
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
||
}
|
||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__,
|
||
);
|
||
});
|
||
return page.evaluate(
|
||
async ({ agentName, channelId, systemPrompt }) => {
|
||
const invoke = (
|
||
window as Window & {
|
||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
) => Promise<{ agent?: { pubkey: string }; id?: string }>;
|
||
}
|
||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
||
if (!invoke) {
|
||
throw new Error("Mock bridge is not installed.");
|
||
}
|
||
|
||
const persona = await invoke("create_persona", {
|
||
input: {
|
||
displayName: agentName,
|
||
systemPrompt,
|
||
},
|
||
});
|
||
if (!persona.id) {
|
||
throw new Error("Mock persona creation did not return an id.");
|
||
}
|
||
|
||
const created = await invoke("create_managed_agent", {
|
||
input: {
|
||
name: agentName,
|
||
personaId: persona.id,
|
||
spawnAfterCreate: true,
|
||
systemPrompt,
|
||
},
|
||
});
|
||
const pubkey = created.agent?.pubkey;
|
||
if (!pubkey) {
|
||
throw new Error("Mock managed agent creation did not return a pubkey.");
|
||
}
|
||
|
||
await invoke("add_channel_members", {
|
||
channelId,
|
||
pubkeys: [pubkey],
|
||
role: "bot",
|
||
});
|
||
|
||
await (
|
||
window as Window & {
|
||
__BUZZ_E2E_QUERY_CLIENT__?: {
|
||
invalidateQueries: () => Promise<void>;
|
||
};
|
||
}
|
||
).__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries();
|
||
|
||
return pubkey;
|
||
},
|
||
{ agentName, channelId, systemPrompt },
|
||
);
|
||
}
|
||
|
||
async function waitForMockLiveSubscription(
|
||
page: Page,
|
||
channelName: string,
|
||
kind?: number,
|
||
) {
|
||
await expect
|
||
.poll(async () => {
|
||
return page.evaluate(
|
||
({ channelName, kind }) => {
|
||
return (
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
|
||
channelName: string;
|
||
kind?: number;
|
||
}) => boolean;
|
||
}
|
||
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
|
||
channelName,
|
||
kind,
|
||
}) ?? false
|
||
);
|
||
},
|
||
{ channelName, kind },
|
||
);
|
||
})
|
||
.toBe(true);
|
||
}
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
await installMockBridge(page);
|
||
});
|
||
|
||
test("profile panel shows communication actions as quick action tiles", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
await page.getByTestId("channel-general").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||
await waitForMockLiveSubscription(page, "general");
|
||
|
||
await page.evaluate((bobPubkey) => {
|
||
const emit = (
|
||
window as Window & {
|
||
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
||
channelName: string;
|
||
content: string;
|
||
pubkey: string;
|
||
}) => unknown;
|
||
}
|
||
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
|
||
if (!emit) {
|
||
throw new Error("Mock message emitter is unavailable.");
|
||
}
|
||
emit({
|
||
channelName: "general",
|
||
content: "Profile action tile check",
|
||
pubkey: bobPubkey,
|
||
});
|
||
}, TEST_IDENTITIES.bob.pubkey);
|
||
|
||
const messageRow = page
|
||
.getByTestId("message-row")
|
||
.filter({ hasText: "Profile action tile check" });
|
||
await expect(messageRow).toBeVisible();
|
||
await messageRow.locator("button").first().click();
|
||
|
||
const panel = page.getByTestId("user-profile-panel");
|
||
await expect(panel).toBeVisible();
|
||
await expect(panel.getByTestId("user-profile-header-edit-agent")).toHaveCount(
|
||
0,
|
||
);
|
||
const actionGroup = panel.getByTestId("user-profile-primary-actions");
|
||
await expect(actionGroup).toBeVisible();
|
||
await expect(actionGroup).toHaveClass(/grid-flow-col/);
|
||
await expect(actionGroup).toHaveClass(/auto-cols-fr/);
|
||
const waveAction = panel.getByTestId("user-profile-wave");
|
||
const messageAction = panel.getByTestId("user-profile-message");
|
||
const huddleAction = panel.getByTestId("user-profile-huddle");
|
||
await expect(waveAction).toHaveClass(/flex-col/);
|
||
await expect(messageAction).toHaveClass(/flex-col/);
|
||
await expect(huddleAction).toHaveClass(/flex-col/);
|
||
const communicationActionOrder = await actionGroup
|
||
.locator("button[data-testid]")
|
||
.evaluateAll((buttons) =>
|
||
buttons.map((button) => button.getAttribute("data-testid")),
|
||
);
|
||
expect(communicationActionOrder).toEqual([
|
||
"user-profile-message",
|
||
"user-profile-huddle",
|
||
"user-profile-wave",
|
||
]);
|
||
await expect(panel.getByTestId("user-profile-copy-pubkey")).toBeVisible();
|
||
|
||
await waveAction.click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
|
||
await expect(page.getByTestId("message-wave-attachment")).toBeVisible();
|
||
});
|
||
|
||
test("owned agent profile stays in parity between Agents and its DM", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
agentMemory: createMockAgentMemoryListing(),
|
||
oaOwnerIsMe: true,
|
||
});
|
||
await page.goto("/");
|
||
const agentName = "Parity Bot";
|
||
const agentPubkey = await addGenericAgent(
|
||
page,
|
||
"general",
|
||
agentName,
|
||
"Keep every profile entry point in sync.",
|
||
);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page
|
||
.getByRole("button", { name: `${agentName} agent profile` })
|
||
.click();
|
||
await page.getByTestId("user-profile-message").click();
|
||
await expect(page.getByTestId("chat-header-dm-avatar")).toBeVisible();
|
||
const dmChannelId = await page.evaluate(() => {
|
||
const match = window.location.hash.match(/\/channels\/([^?]+)/);
|
||
if (!match?.[1]) {
|
||
throw new Error("Could not resolve the agent DM channel id.");
|
||
}
|
||
return decodeURIComponent(match[1]);
|
||
});
|
||
await page.evaluate(
|
||
({ dmChannelId, pubkey }) => {
|
||
const seed = (
|
||
window as Window & {
|
||
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
|
||
agentPubkey: string;
|
||
channelId: string;
|
||
turnId: string;
|
||
}) => void;
|
||
}
|
||
).__BUZZ_E2E_SEED_ACTIVE_TURNS__;
|
||
if (!seed) {
|
||
throw new Error("Active-turn test bridge is unavailable.");
|
||
}
|
||
seed({
|
||
agentPubkey: pubkey,
|
||
channelId: "00000000-0000-0000-0000-000000000001",
|
||
turnId: "profile-parity-other-channel",
|
||
});
|
||
seed({
|
||
agentPubkey: pubkey,
|
||
channelId: dmChannelId,
|
||
turnId: "profile-parity-dm-channel",
|
||
});
|
||
},
|
||
{ dmChannelId, pubkey: agentPubkey },
|
||
);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page
|
||
.getByRole("button", { name: `${agentName} agent profile` })
|
||
.click();
|
||
const agentsSurface = await readOwnedAgentProfileContract(page);
|
||
|
||
await page.getByTestId("user-profile-message").click();
|
||
await expect(page.getByTestId("chat-header-dm-avatar")).toBeVisible();
|
||
await page
|
||
.getByTestId("chat-header")
|
||
.getByRole("button", { name: `Open profile for ${agentName}` })
|
||
.click();
|
||
await expect(page.getByTestId("user-profile-public-key")).toContainText(
|
||
agentPubkey.slice(0, 8),
|
||
);
|
||
const dmSurface = await readOwnedAgentProfileContract(page);
|
||
|
||
expect(dmSurface).toEqual(agentsSurface);
|
||
});
|
||
|
||
test("keeps the saved profile description after a community round trip", async ({
|
||
page,
|
||
}) => {
|
||
const communities = [
|
||
{
|
||
id: "profile-community-a",
|
||
name: "Alpha",
|
||
relayUrl: "ws://localhost:3000",
|
||
addedAt: "2026-01-01T00:00:00.000Z",
|
||
},
|
||
{
|
||
id: "profile-community-b",
|
||
name: "Bravo",
|
||
relayUrl: "ws://localhost:3001",
|
||
addedAt: "2026-01-02T00:00:00.000Z",
|
||
},
|
||
];
|
||
await page.addInitScript((seed) => {
|
||
window.localStorage.setItem("buzz-communities", JSON.stringify(seed));
|
||
window.localStorage.setItem("buzz-active-community-id", seed[0].id);
|
||
}, communities);
|
||
await page.goto("/");
|
||
|
||
const description = "Description that should survive switching";
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await page.getByTestId("profile-about").fill(description);
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await expect(page.getByTestId("profile-about-value")).toHaveText(description);
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
|
||
const communityA = page.getByTestId(
|
||
"community-rail-button-profile-community-a",
|
||
);
|
||
const communityB = page.getByTestId(
|
||
"community-rail-button-profile-community-b",
|
||
);
|
||
await communityB.click();
|
||
await expect(communityB).toHaveAttribute("aria-current", "true");
|
||
await communityA.click();
|
||
await expect(communityA).toHaveAttribute("aria-current", "true");
|
||
|
||
await openSettings(page, "profile");
|
||
await expect(page.getByTestId("profile-about-value")).toHaveText(description);
|
||
});
|
||
|
||
test("updates the relay-backed profile from settings", async ({ page }) => {
|
||
const stamp = Date.now();
|
||
const displayName = `Tyler QA ${stamp}`;
|
||
const avatarUrl = `https://example.com/avatar-${stamp}.png`;
|
||
const about = `Coordinating relay profile setup ${stamp}`;
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await expect(
|
||
page.getByTestId("settings-profile").getByRole("heading", {
|
||
exact: true,
|
||
name: "Profile",
|
||
}),
|
||
).toBeVisible();
|
||
|
||
await expect(page.getByTestId("profile-identity-details")).toBeHidden();
|
||
await expandIdentity(page);
|
||
await expect(page.getByTestId("profile-pubkey")).toContainText("deadbeef");
|
||
await expect(page.getByTestId("profile-nip05")).toContainText("Not set");
|
||
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await expect(page.getByTestId("profile-metadata-edit")).toHaveText("Done");
|
||
await expect(page.getByTestId("profile-about")).toBeVisible();
|
||
await page.getByTestId("profile-display-name").fill(displayName);
|
||
await page.getByTestId("profile-about").fill(about);
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
|
||
await expect(page.getByTestId("profile-display-name-value")).toHaveText(
|
||
displayName,
|
||
);
|
||
await expect(page.getByTestId("profile-about-value")).toHaveText(about);
|
||
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await page.getByTestId("profile-avatar-url").fill(avatarUrl);
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await waitForAvatarEditorToClose(page);
|
||
|
||
await expect(page.getByTestId("profile-display-name-value")).toHaveText(
|
||
displayName,
|
||
);
|
||
await expect(page.getByTestId("profile-nip05")).toContainText("Not set");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await expandIdentity(page);
|
||
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
await expectHomeView(page);
|
||
await expect(page.getByTestId("open-settings")).toBeVisible();
|
||
|
||
await openSettings(page, "profile");
|
||
await expect(page.getByTestId("profile-display-name-value")).toHaveText(
|
||
displayName,
|
||
);
|
||
await expandIdentity(page);
|
||
await expect(page.getByTestId("profile-nip05")).toContainText("Not set");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
await expect(page.getByTestId("profile-about-value")).toHaveText(about);
|
||
});
|
||
|
||
test("saves profile metadata from the block Done button", async ({ page }) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await expect(page.getByTestId("profile-display-name-value")).toHaveText(
|
||
"npub1mock...",
|
||
);
|
||
await expect(page.getByTestId("profile-save")).toHaveCount(0);
|
||
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await expect(page.getByTestId("profile-metadata-edit")).toHaveText("Done");
|
||
await expect(page.getByTestId("profile-about")).toBeVisible();
|
||
await page.getByTestId("profile-display-name").fill("Save Button QA");
|
||
await page.getByTestId("profile-about").fill("Temporary profile note");
|
||
await expect(page.getByTestId("profile-save")).toHaveCount(0);
|
||
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await waitForReactEffects(page);
|
||
await expect(page.getByTestId("profile-display-name")).toHaveCount(0);
|
||
await expect(page.getByTestId("profile-display-name-value")).toHaveText(
|
||
"Save Button QA",
|
||
);
|
||
await expect(page.getByTestId("profile-about-value")).toHaveText(
|
||
"Temporary profile note",
|
||
);
|
||
await expect(page.getByTestId("profile-metadata-edit")).toHaveText("Edit");
|
||
await expect(page.getByTestId("profile-save")).toHaveCount(0);
|
||
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await page.getByTestId("profile-about").fill("");
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await waitForReactEffects(page);
|
||
await expect(page.getByTestId("profile-about-value")).toHaveText("Not set");
|
||
await expect(page.getByTestId("profile-save")).toHaveCount(0);
|
||
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await page.getByTestId("profile-display-name").fill("");
|
||
await expect(
|
||
page.getByText("Clearing existing profile fields is not supported yet."),
|
||
).toBeVisible();
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await waitForReactEffects(page);
|
||
await expect(page.getByTestId("profile-display-name")).toHaveCount(0);
|
||
await expect(page.getByTestId("profile-display-name-value")).toHaveText(
|
||
"Save Button QA",
|
||
);
|
||
await expect(page.getByTestId("profile-metadata-edit")).toHaveText("Edit");
|
||
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await page.getByTestId("profile-display-name").fill("npub1mock...");
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await expect(page.getByTestId("profile-save")).toHaveCount(0);
|
||
});
|
||
|
||
test("shows profile save feedback as a toast", async ({ page }) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
await page.getByTestId("profile-display-name").fill("Toast QA");
|
||
await page.getByTestId("profile-metadata-edit").click();
|
||
|
||
await expect(
|
||
page.locator("[data-sonner-toast]").filter({ hasText: "Profile saved" }),
|
||
).toBeVisible();
|
||
await expect(page.getByText("Profile saved.", { exact: true })).toHaveCount(
|
||
0,
|
||
);
|
||
});
|
||
|
||
test("nests the avatar edit button in a clipped notch", async ({ page }) => {
|
||
// Under the Buzz default theme the settings nav overrides `--sidebar-active`
|
||
// (white pill on the gradient) while the avatar edit button deliberately
|
||
// keeps the root accent-driven token, so the shared-token comparison below
|
||
// only holds outside the Buzz theme.
|
||
await page.addInitScript(() => {
|
||
window.localStorage.setItem("buzz-theme", "github-light");
|
||
});
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
|
||
await expect(page.getByTestId("profile-avatar-preview-clip")).toHaveCSS(
|
||
"clip-path",
|
||
/polygon/,
|
||
);
|
||
const editShell = page.getByTestId("profile-avatar-edit-shell");
|
||
await expect(editShell).toHaveCSS("height", "54px");
|
||
await expect(editShell).toHaveCSS("width", "54px");
|
||
|
||
const editButton = page.getByTestId("profile-avatar-edit");
|
||
await expect(editButton).toHaveCSS("opacity", "1");
|
||
|
||
await expect(editButton).toHaveCSS(
|
||
"background-color",
|
||
await page
|
||
.getByTestId("settings-nav-profile")
|
||
.evaluate((element) => getComputedStyle(element).backgroundColor),
|
||
);
|
||
const transitionProperty = await editButton.evaluate(
|
||
(element) => getComputedStyle(element).transitionProperty,
|
||
);
|
||
expect(transitionProperty).toContain("opacity");
|
||
expect(transitionProperty).toContain("scale");
|
||
});
|
||
|
||
test("swaps the avatar preview and mode tabs while editing", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
|
||
const previewFrame = page.getByTestId("profile-avatar-clip-frame");
|
||
const closedPreviewBox = await previewFrame.boundingBox();
|
||
if (!closedPreviewBox) {
|
||
throw new Error("Profile avatar preview did not render bounds.");
|
||
}
|
||
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
const tabList = page.getByRole("tablist", { name: "Avatar type" });
|
||
await expect(tabList).toBeVisible();
|
||
await expect(page.getByTestId("profile-avatar-mode-tabs-slot")).toBeVisible();
|
||
await page.waitForTimeout(350);
|
||
|
||
const openPreviewBox = await previewFrame.boundingBox();
|
||
const tabListBox = await tabList.boundingBox();
|
||
if (!openPreviewBox || !tabListBox) {
|
||
throw new Error("Profile avatar edit layout did not render bounds.");
|
||
}
|
||
|
||
const closedPreviewCenterY = closedPreviewBox.y + closedPreviewBox.height / 2;
|
||
const tabListCenterY = tabListBox.y + tabListBox.height / 2;
|
||
expect(Math.abs(tabListCenterY - closedPreviewCenterY)).toBeLessThan(16);
|
||
const tabListBottomY = tabListBox.y + tabListBox.height;
|
||
const segmentToPreviewGap = openPreviewBox.y - tabListBottomY;
|
||
expect(segmentToPreviewGap).toBeGreaterThan(48);
|
||
expect(segmentToPreviewGap).toBeLessThan(72);
|
||
expect(openPreviewBox.y).toBeGreaterThan(closedPreviewCenterY + 72);
|
||
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await waitForAvatarEditorToClose(page);
|
||
await expect(tabList).toHaveCount(0);
|
||
|
||
const restoredPreviewBox = await previewFrame.boundingBox();
|
||
if (!restoredPreviewBox) {
|
||
throw new Error("Profile avatar preview did not restore bounds.");
|
||
}
|
||
expect(Math.abs(restoredPreviewBox.y - closedPreviewBox.y)).toBeLessThan(8);
|
||
});
|
||
|
||
test("highlights the avatar drop target while dragging an image", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
|
||
const uploadTarget = page.getByTestId("profile-avatar-upload");
|
||
await uploadTarget.evaluate((element) => {
|
||
const dataTransfer = new DataTransfer();
|
||
dataTransfer.items.add(
|
||
new File(["avatar"], "avatar.png", { type: "image/png" }),
|
||
);
|
||
|
||
element.dispatchEvent(
|
||
new DragEvent("dragenter", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
dataTransfer,
|
||
}),
|
||
);
|
||
});
|
||
|
||
await expect(uploadTarget).toHaveAttribute("data-dragging", "true");
|
||
await expect(uploadTarget).toContainText("Drop image here");
|
||
|
||
await uploadTarget.evaluate((element) => {
|
||
const dataTransfer = new DataTransfer();
|
||
dataTransfer.items.add(
|
||
new File(["avatar"], "avatar.png", { type: "image/png" }),
|
||
);
|
||
|
||
element.dispatchEvent(
|
||
new DragEvent("dragleave", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
dataTransfer,
|
||
}),
|
||
);
|
||
});
|
||
|
||
await expect(uploadTarget).not.toHaveAttribute("data-dragging", "true");
|
||
|
||
await uploadTarget.evaluate((element) => {
|
||
const dataTransfer = new DataTransfer();
|
||
dataTransfer.items.add(
|
||
new File(["avatar"], "avatar.png", { type: "image/png" }),
|
||
);
|
||
|
||
element.dispatchEvent(
|
||
new DragEvent("dragenter", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
dataTransfer,
|
||
}),
|
||
);
|
||
});
|
||
|
||
await expect(uploadTarget).toHaveAttribute("data-dragging", "true");
|
||
|
||
await page.evaluate(() => {
|
||
window.dispatchEvent(
|
||
new DragEvent("dragleave", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
clientX: -1,
|
||
clientY: 40,
|
||
}),
|
||
);
|
||
});
|
||
|
||
await expect(uploadTarget).not.toHaveAttribute("data-dragging", "true");
|
||
});
|
||
|
||
test("uploads local profile avatar files before saving", async ({ page }) => {
|
||
const uploadedAvatarUrl = "https://mock.relay/media/avatar-profile.png";
|
||
await installMockBridge(page, {
|
||
uploadDescriptors: [
|
||
{
|
||
filename: "avatar-profile.png",
|
||
sha256: "b".repeat(64),
|
||
size: 553432,
|
||
type: "image/png",
|
||
uploaded: 1_779_900_000,
|
||
url: uploadedAvatarUrl,
|
||
},
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await page.getByTestId("profile-avatar-input").setInputFiles({
|
||
buffer: Buffer.from("large-avatar-bytes"),
|
||
mimeType: "image/png",
|
||
name: "avatar-profile.png",
|
||
});
|
||
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await waitForAvatarEditorToClose(page);
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
|
||
const pastedAvatarUrl = await page.evaluate(
|
||
() => new URL("/buzz.svg", window.location.href).href,
|
||
);
|
||
await page.getByTestId("profile-avatar-url").click();
|
||
await page.keyboard.insertText(pastedAvatarUrl);
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue(
|
||
pastedAvatarUrl,
|
||
);
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await waitForAvatarEditorToClose(page);
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
await page.getByTestId("profile-avatar-url").fill("");
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await expect(
|
||
page.getByTestId("profile-avatar-preview").locator("img"),
|
||
).toHaveCount(1);
|
||
await waitForAvatarEditorToClose(page);
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(
|
||
() =>
|
||
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
|
||
.__BUZZ_E2E_COMMANDS__ ?? [],
|
||
),
|
||
)
|
||
.toEqual(expect.arrayContaining(["upload_media_bytes", "update_profile"]));
|
||
});
|
||
|
||
test("renders emoji avatars with a static background layer", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await page.getByRole("tab", { name: "Emoji" }).click();
|
||
await selectFirstEmojiFromPicker(page);
|
||
await page.getByRole("button", { name: "Use #FFE75C background" }).click();
|
||
|
||
const avatarPreview = page.getByTestId("profile-avatar-preview");
|
||
await expect(avatarPreview).toHaveCSS(
|
||
"background-color",
|
||
"rgb(255, 231, 92)",
|
||
);
|
||
await expect(avatarPreview).not.toHaveClass(/buzz-avatar-squish/);
|
||
await expect(page.getByTestId("profile-avatar-preview-emoji")).toHaveText(
|
||
"😀",
|
||
);
|
||
await expect(page.getByTestId("profile-avatar-preview-emoji")).toHaveCSS(
|
||
"font-size",
|
||
"96px",
|
||
);
|
||
});
|
||
|
||
test("offers emoji search and skin-tone controls for profile avatars", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await page.getByRole("tab", { name: "Emoji" }).click();
|
||
|
||
const picker = page.locator("em-emoji-picker");
|
||
const searchInput = picker.locator("input[type='search']");
|
||
await expect(searchInput).toBeVisible();
|
||
await expectEmojiMartStylesInstalled(picker);
|
||
await expect(page.getByTestId("profile-avatar-emoji-picker")).toHaveCSS(
|
||
"height",
|
||
"384px",
|
||
);
|
||
|
||
const hasSkinToneControl = await picker.evaluate((element) => {
|
||
const controls = element.shadowRoot?.querySelectorAll("button") ?? [];
|
||
return Array.from(controls).some((button) =>
|
||
/skin tone/i.test(button.getAttribute("aria-label") ?? ""),
|
||
);
|
||
});
|
||
expect(hasSkinToneControl).toBe(true);
|
||
|
||
const controlColors = await picker.evaluate((element) => {
|
||
const root = element.shadowRoot?.querySelector<HTMLElement>("#root");
|
||
const input = element.shadowRoot?.querySelector<HTMLInputElement>(
|
||
'input[type="search"]',
|
||
);
|
||
const toneControl =
|
||
element.shadowRoot?.querySelector<HTMLElement>(".search + .flex");
|
||
if (!root || !input || !toneControl) {
|
||
throw new Error("Profile emoji picker controls did not render.");
|
||
}
|
||
return {
|
||
input: getComputedStyle(input).backgroundColor,
|
||
picker: getComputedStyle(root).backgroundColor,
|
||
tone: getComputedStyle(toneControl).backgroundColor,
|
||
};
|
||
});
|
||
expect(controlColors.input).not.toBe(controlColors.picker);
|
||
expect(controlColors.tone).not.toBe(controlColors.picker);
|
||
|
||
const controlHeights = await picker.evaluate((element) => {
|
||
const input = element.shadowRoot?.querySelector<HTMLInputElement>(
|
||
'input[type="search"]',
|
||
);
|
||
const toneControl =
|
||
element.shadowRoot?.querySelector<HTMLElement>(".search + .flex");
|
||
const toneButton =
|
||
element.shadowRoot?.querySelector<HTMLElement>(".skin-tone-button");
|
||
if (!input || !toneControl || !toneButton) {
|
||
throw new Error("Profile emoji picker controls did not render.");
|
||
}
|
||
input.focus();
|
||
return {
|
||
inputHeight: input.getBoundingClientRect().height,
|
||
inputShadow: getComputedStyle(input).boxShadow,
|
||
toneButtonBorder: getComputedStyle(toneButton).borderTopWidth,
|
||
toneButtonShadow: getComputedStyle(toneButton).boxShadow,
|
||
toneHeight: toneControl.getBoundingClientRect().height,
|
||
};
|
||
});
|
||
expect(controlHeights.inputHeight).toBe(48);
|
||
expect(controlHeights.toneHeight).toBe(48);
|
||
expect(controlHeights.inputShadow).toMatch(/inset$/);
|
||
expect(controlHeights.toneButtonBorder).toBe("0px");
|
||
expect(controlHeights.toneButtonShadow).toBe("none");
|
||
});
|
||
|
||
test("reveals emoji background colors only after choosing an emoji", async ({
|
||
page,
|
||
}) => {
|
||
const imageAvatarUrl = `https://example.com/avatar-color-controls-${Date.now()}.png`;
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await page.getByTestId("profile-avatar-url").fill(imageAvatarUrl);
|
||
await page.getByTestId("profile-avatar-done").click();
|
||
await waitForAvatarEditorToClose(page);
|
||
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await expect(page.getByTestId("profile-avatar-url")).toHaveValue("");
|
||
await page.getByRole("tab", { name: "Emoji" }).click();
|
||
|
||
const colorGridShell = page.getByTestId("profile-avatar-color-grid-shell");
|
||
const doneButton = page.getByTestId("profile-avatar-done");
|
||
await expect(colorGridShell).toHaveAttribute("aria-hidden", "true");
|
||
|
||
const doneBeforeEmoji = await doneButton.boundingBox();
|
||
if (!doneBeforeEmoji) {
|
||
throw new Error("Avatar Done button did not render bounds.");
|
||
}
|
||
|
||
await selectFirstEmojiFromPicker(page);
|
||
|
||
await expect(colorGridShell).toHaveAttribute("aria-hidden", "false");
|
||
await expect(page.getByTestId("profile-avatar-color-grid")).toBeVisible();
|
||
await colorGridShell.evaluate((element) =>
|
||
Promise.all(
|
||
element
|
||
.getAnimations()
|
||
.map((animation) => animation.finished.catch(() => undefined)),
|
||
),
|
||
);
|
||
|
||
const doneAfterEmoji = await doneButton.boundingBox();
|
||
if (!doneAfterEmoji) {
|
||
throw new Error("Avatar Done button did not render bounds.");
|
||
}
|
||
expect(doneAfterEmoji.y).toBeGreaterThan(doneBeforeEmoji.y + 8);
|
||
});
|
||
|
||
test("snaps custom avatar colors to the dot grid", async ({ page }) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "profile");
|
||
await page.getByTestId("profile-avatar-edit").click();
|
||
await page.getByRole("tab", { name: "Emoji" }).click();
|
||
await selectFirstEmojiFromPicker(page);
|
||
|
||
const customColorSwatch = page.getByTestId("profile-avatar-custom-color");
|
||
await customColorSwatch.click();
|
||
|
||
const spectrum = page.getByTestId("profile-avatar-custom-color-spectrum");
|
||
await expect(spectrum).toBeVisible();
|
||
await expect(page.getByTestId("profile-avatar-done")).toHaveCount(0);
|
||
await expect(
|
||
page.getByTestId("profile-avatar-custom-color-done"),
|
||
).toBeVisible();
|
||
|
||
const hueSlider = page.getByTestId("profile-avatar-custom-color-hue");
|
||
await hueSlider.press("Home");
|
||
await expect(hueSlider).toHaveAttribute("aria-valuenow", "0");
|
||
|
||
const spectrumBox = await spectrum.boundingBox();
|
||
if (!spectrumBox) {
|
||
throw new Error("Custom color spectrum did not render bounds.");
|
||
}
|
||
|
||
await spectrum.click({
|
||
position: {
|
||
x: 24 + (spectrumBox.width - 48) * 0.33,
|
||
y: 24 + (spectrumBox.height - 48) * 0.44,
|
||
},
|
||
});
|
||
await expect(page.getByTestId("profile-avatar-preview")).toHaveCSS(
|
||
"background-color",
|
||
"rgb(145, 93, 93)",
|
||
);
|
||
await page.getByTestId("profile-avatar-custom-color-done").click();
|
||
|
||
await expect(customColorSwatch).toHaveAttribute("aria-pressed", "true");
|
||
await expect(customColorSwatch).toHaveCSS(
|
||
"background-color",
|
||
"rgb(145, 93, 93)",
|
||
);
|
||
await expect(page.getByTestId("profile-avatar-done")).toBeVisible();
|
||
});
|
||
|
||
test("opens Send feedback from the profile menu", async ({ page }) => {
|
||
await page.goto("/");
|
||
await openProfileMenu(page);
|
||
await page.getByTestId("profile-popover-send-feedback").click();
|
||
await expect(page.getByTestId("send-feedback-dialog")).toBeVisible();
|
||
await expect(page.getByTestId("feedback-privacy-disclosure")).toContainText(
|
||
"not posted to a channel",
|
||
);
|
||
});
|
||
|
||
test("keeps Send disabled when a stale attachment attempt finishes", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
uploadDelayMs: 1_200,
|
||
uploadDescriptors: [
|
||
{
|
||
url: `https://mock.relay/media/${"b".repeat(64)}.png`,
|
||
sha256: "b".repeat(64),
|
||
size: 42,
|
||
type: "image/png",
|
||
uploaded: 42,
|
||
},
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await openProfileMenu(page);
|
||
await page.getByTestId("profile-popover-send-feedback").click();
|
||
await page.getByTestId("feedback-message").fill("Attachment race");
|
||
await page.getByTestId("feedback-attach-image").click();
|
||
await expect(page.getByTestId("feedback-attach-image")).toContainText(
|
||
"Attaching…",
|
||
);
|
||
|
||
await page.waitForTimeout(450);
|
||
await page.getByRole("button", { name: "Cancel" }).click();
|
||
await openProfileMenu(page);
|
||
await page.getByTestId("profile-popover-send-feedback").click();
|
||
await page.getByTestId("feedback-message").fill("Second attachment");
|
||
await page.getByTestId("feedback-attach-image").click();
|
||
|
||
const submit = page.getByTestId("feedback-submit");
|
||
await expect(submit).toBeDisabled();
|
||
await page.waitForTimeout(900);
|
||
await expect(page.getByTestId("feedback-attach-image")).toContainText(
|
||
"Attaching…",
|
||
);
|
||
await expect(submit).toBeDisabled();
|
||
|
||
await expect(page.getByTestId("feedback-attachment-thumb")).toBeVisible();
|
||
await expect(submit).toBeEnabled();
|
||
});
|
||
|
||
test("proxies feedback attachment previews", async ({ page }) => {
|
||
const sha256 = "c".repeat(64);
|
||
const proxyUrl = `http://127.0.0.1:54321/media/${sha256}.png`;
|
||
await installMockBridge(page, {
|
||
uploadDescriptors: [
|
||
{
|
||
url: `http://localhost:3000/media/${sha256}.png`,
|
||
sha256,
|
||
size: 42,
|
||
type: "image/png",
|
||
uploaded: 42,
|
||
},
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await openProfileMenu(page);
|
||
await page.getByTestId("profile-popover-send-feedback").click();
|
||
await page.getByTestId("feedback-attach-image").click();
|
||
|
||
const thumbnail = page.getByTestId("feedback-attachment-thumb");
|
||
await expect(thumbnail.locator("img")).toHaveAttribute("src", proxyUrl);
|
||
await thumbnail.click();
|
||
|
||
const preview = page.getByTestId("feedback-attachment-preview");
|
||
await expect(preview).toBeVisible();
|
||
await expect(preview.locator("img")).toHaveAttribute("src", proxyUrl);
|
||
});
|
||
|
||
test("updates presence from the profile menu", async ({ page }) => {
|
||
await page.goto("/");
|
||
|
||
await openProfileMenu(page);
|
||
await expect(
|
||
page.getByTestId("profile-popover-presence-trigger"),
|
||
).toContainText("Online");
|
||
|
||
await page.getByTestId("profile-popover-presence-trigger").click();
|
||
await page.getByTestId("profile-popover-status-away").click();
|
||
await openProfileMenu(page);
|
||
await expect(
|
||
page.getByTestId("profile-popover-presence-trigger"),
|
||
).toContainText("Away");
|
||
|
||
await page.getByTestId("profile-popover-presence-trigger").click();
|
||
await page.getByTestId("profile-popover-status-offline").click();
|
||
await openProfileMenu(page);
|
||
await expect(
|
||
page.getByTestId("profile-popover-presence-trigger"),
|
||
).toContainText("Offline");
|
||
});
|
||
|
||
test("renders agent profile ingress subviews from the Playwright mock bridge", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
agentMemory: createMockAgentMemoryListing(),
|
||
// The viewer is the agent's verified NIP-OA owner, so archiveActions
|
||
// grants the Archive row (canArchive gate; the relay re-verifies on submit).
|
||
oaOwnerIsMe: true,
|
||
});
|
||
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
||
await page.goto("/");
|
||
|
||
const longAgentInstruction = [
|
||
"Watch the channel and help when asked.",
|
||
"Summarize active decisions, call out risks plainly, and keep the tone concise.",
|
||
"Prefer concrete next steps over broad commentary, and cite the relevant thread context when responding.",
|
||
"Avoid catchphrases, theatrical roleplay, and unsupported guesses.",
|
||
"When uncertainty remains, say exactly what evidence would resolve it.",
|
||
].join("\n\n");
|
||
const agentPubkey = await addGenericAgent(
|
||
page,
|
||
"general",
|
||
"Memory Bot",
|
||
longAgentInstruction,
|
||
);
|
||
// A running process is not presence. Supply this scenario's snapshot and
|
||
// authored kind-20001 updates through the mock relay, not the query cache.
|
||
const emitAgentPresence = (status: "online" | "offline") =>
|
||
page.evaluate(
|
||
({ pubkey, status }) => {
|
||
const emit = (
|
||
window as Window & {
|
||
__BUZZ_E2E_EMIT_MOCK_PRESENCE__?: (input: {
|
||
pubkey: string;
|
||
status: "online" | "offline";
|
||
}) => void;
|
||
}
|
||
).__BUZZ_E2E_EMIT_MOCK_PRESENCE__;
|
||
if (!emit) throw new Error("Mock presence emitter is unavailable.");
|
||
emit({ pubkey, status });
|
||
},
|
||
{ pubkey: agentPubkey, status },
|
||
);
|
||
await emitAgentPresence("online");
|
||
|
||
await page.getByTestId("channel-general").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||
await waitForMockLiveSubscription(page, "general");
|
||
|
||
await page.evaluate(
|
||
({ pubkey }) => {
|
||
const emit = (
|
||
window as Window & {
|
||
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
||
channelName: string;
|
||
content: string;
|
||
pubkey: string;
|
||
}) => unknown;
|
||
}
|
||
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
|
||
if (!emit) {
|
||
throw new Error("Mock message emitter is unavailable.");
|
||
}
|
||
emit({
|
||
channelName: "general",
|
||
content: "Memory bot check-in",
|
||
pubkey,
|
||
});
|
||
},
|
||
{ pubkey: agentPubkey },
|
||
);
|
||
|
||
const messageRow = page
|
||
.getByTestId("message-row")
|
||
.filter({ hasText: "Memory bot check-in" });
|
||
await expect(messageRow).toBeVisible();
|
||
await messageRow.locator("button").first().click();
|
||
|
||
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
|
||
await expect(page.getByTestId("user-profile-message")).toBeVisible();
|
||
await expect(page.getByTestId("user-profile-huddle")).toHaveCount(0);
|
||
await expect(page.getByTestId("user-profile-wave")).toHaveCount(0);
|
||
await expectHashSearchParam(page, "profile", agentPubkey);
|
||
const agentPresenceBadge = page.getByTestId("user-profile-presence-badge");
|
||
await expect(agentPresenceBadge).toBeVisible();
|
||
await expect(agentPresenceBadge).toHaveAttribute("aria-label", "Online");
|
||
const headerEditAgent = page.getByTestId("user-profile-header-edit-agent");
|
||
await expect(headerEditAgent).toHaveText("Edit");
|
||
await expect(headerEditAgent.locator("svg")).toHaveCount(0);
|
||
await expect(
|
||
page.getByTestId("user-profile-settings-menu-trigger"),
|
||
).toHaveCount(0);
|
||
const closePanelButton = page.getByTestId("auxiliary-panel-close");
|
||
const headerActionOrder = await headerEditAgent
|
||
.locator("xpath=..")
|
||
.locator("button")
|
||
.evaluateAll((buttons) =>
|
||
buttons.map((button) => button.getAttribute("data-testid")),
|
||
);
|
||
expect(headerActionOrder.at(-2)).toBe("user-profile-header-edit-agent");
|
||
expect(headerActionOrder.at(-1)).toBe("auxiliary-panel-close");
|
||
await headerEditAgent.click();
|
||
await expect(page.getByTestId("persona-dialog")).toBeVisible();
|
||
await page
|
||
.getByTestId("persona-dialog")
|
||
.getByRole("button", { name: "Cancel" })
|
||
.click();
|
||
await expect(closePanelButton).toBeVisible();
|
||
const agentEdit = page.getByTestId("user-profile-edit-agent");
|
||
const agentEditIcon = page.getByTestId("user-profile-edit-agent-icon");
|
||
await expect(agentEdit).toBeVisible();
|
||
await expect(agentEditIcon).toHaveCSS("opacity", "0");
|
||
await agentEdit.hover();
|
||
await expect(agentEditIcon).toHaveCSS("opacity", "1");
|
||
await expect(
|
||
page.getByTestId("user-profile-agent-primary-action"),
|
||
).toHaveAttribute("aria-label", "Stop");
|
||
await expect(page.getByTestId("user-profile-agent-restart")).toBeVisible();
|
||
const runningAgentActionOrder = await page
|
||
.getByTestId("user-profile-primary-actions")
|
||
.locator("button[data-testid]")
|
||
.evaluateAll((buttons) =>
|
||
buttons.map((button) => button.getAttribute("data-testid")),
|
||
);
|
||
expect(runningAgentActionOrder).toEqual([
|
||
"user-profile-agent-primary-action",
|
||
"user-profile-agent-restart",
|
||
"user-profile-message",
|
||
]);
|
||
const runningAgentActionWidths = await page
|
||
.getByTestId("user-profile-primary-actions")
|
||
.locator("button[data-testid]")
|
||
.evaluateAll((buttons) =>
|
||
buttons.map((button) => button.getBoundingClientRect().width),
|
||
);
|
||
expect(
|
||
runningAgentActionWidths.every(
|
||
(width) => Math.abs(width - runningAgentActionWidths[0]) <= 1,
|
||
),
|
||
).toBe(true);
|
||
const agentPrimaryAction = page.getByTestId(
|
||
"user-profile-agent-primary-action",
|
||
);
|
||
await expect(agentPrimaryAction).toHaveClass(/bg-foreground/);
|
||
await expect(agentPrimaryAction).toHaveClass(/text-background/);
|
||
await waitForMockLiveSubscription(page, "general", 20001);
|
||
await agentPrimaryAction.click();
|
||
await expect(agentPrimaryAction).toHaveAttribute("aria-label", "Start agent");
|
||
await expect(agentPresenceBadge).toHaveAttribute("aria-label", "Online");
|
||
await emitAgentPresence("offline");
|
||
await expect(agentPresenceBadge).toHaveAttribute("aria-label", "Offline");
|
||
await expectHashSearchParam(page, "profile", agentPubkey);
|
||
await expect(agentPrimaryAction).toHaveClass(/bg-foreground/);
|
||
await expect(agentPrimaryAction).toHaveClass(/text-background/);
|
||
await expect(page.getByTestId("user-profile-agent-restart")).toHaveCount(0);
|
||
const stoppedAgentActionOrder = await page
|
||
.getByTestId("user-profile-primary-actions")
|
||
.locator("button[data-testid]")
|
||
.evaluateAll((buttons) =>
|
||
buttons.map((button) => button.getAttribute("data-testid")),
|
||
);
|
||
expect(stoppedAgentActionOrder).toEqual([
|
||
"user-profile-agent-primary-action",
|
||
"user-profile-message",
|
||
]);
|
||
await expect(agentPrimaryAction).toBeEnabled();
|
||
await agentPrimaryAction.click();
|
||
await expect(agentPrimaryAction).toHaveAttribute("aria-label", "Stop");
|
||
await waitForMockLiveSubscription(page, "general", 20001);
|
||
await expect(agentPresenceBadge).toHaveAttribute("aria-label", "Offline");
|
||
await emitAgentPresence("online");
|
||
await expect(agentPresenceBadge).toHaveAttribute("aria-label", "Online");
|
||
await expect(page.getByTestId("user-profile-agent-restart")).toBeVisible();
|
||
await expectHashSearchParam(page, "profileTab", null);
|
||
|
||
await expect(page.getByTestId("user-profile-tab-info")).toBeVisible();
|
||
const profileScrollBody = page.getByTestId("user-profile-scroll-body");
|
||
const profileHeader = page.getByTestId("user-profile-panel-header");
|
||
const primaryActions = page.getByTestId("user-profile-primary-actions");
|
||
const stickyHero = page.getByTestId("user-profile-sticky-hero");
|
||
const stickyTabs = page.getByTestId("user-profile-sticky-tabs");
|
||
const stickyChromeSurface = page.getByTestId(
|
||
"user-profile-sticky-chrome-surface",
|
||
);
|
||
await expect(stickyHero).toHaveCSS("position", "sticky");
|
||
await expect(stickyTabs).toHaveCSS("position", "sticky");
|
||
await expect(stickyTabs).toHaveCSS("padding-top", "8px");
|
||
await expect(profileHeader).toHaveCSS("backdrop-filter", "none");
|
||
await expect(stickyHero).toHaveCSS("backdrop-filter", "none");
|
||
await expect(stickyTabs).toHaveCSS("backdrop-filter", "none");
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = 0;
|
||
});
|
||
await expect(stickyChromeSurface).toHaveAttribute("data-active", "false");
|
||
await expect(stickyChromeSurface).toHaveCSS("opacity", "0");
|
||
await expect(primaryActions).toHaveCSS("opacity", "1");
|
||
await expect(primaryActions).toHaveCSS("transform", "none");
|
||
for (const removedFadeTestId of [
|
||
"user-profile-header-blur-fade",
|
||
"user-profile-hero-blur-fade",
|
||
"user-profile-sticky-blur-fade",
|
||
]) {
|
||
await expect(page.getByTestId(removedFadeTestId)).toHaveCount(0);
|
||
}
|
||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = 24;
|
||
});
|
||
await expect(primaryActions).not.toHaveAttribute("aria-hidden", "true");
|
||
await expect(primaryActions).toHaveCSS("opacity", "0.75");
|
||
await expect(primaryActions).toHaveCSS("transform", "none");
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = 48;
|
||
});
|
||
await expect(primaryActions).not.toHaveAttribute("aria-hidden", "true");
|
||
await expect(primaryActions).toHaveCSS("opacity", "0.5");
|
||
await expect(primaryActions).toHaveCSS("transform", "none");
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = 96;
|
||
});
|
||
await expect(primaryActions).toHaveAttribute("aria-hidden", "true");
|
||
await expect(primaryActions).toHaveCSS("opacity", "0");
|
||
await expect(primaryActions).toHaveCSS("transform", "none");
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = 0;
|
||
});
|
||
await expect(primaryActions).not.toHaveAttribute("aria-hidden", "true");
|
||
await expect(primaryActions).toHaveCSS("opacity", "1");
|
||
await expect(primaryActions).toHaveCSS("transform", "none");
|
||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||
const stickyOffsets = await page
|
||
.getByTestId("user-profile-summary-scroll-layout")
|
||
.evaluate((layout) => {
|
||
const tabs = layout.querySelector<HTMLElement>(
|
||
'[data-testid="user-profile-sticky-tabs"]',
|
||
);
|
||
return {
|
||
heroHeight: Number.parseFloat(
|
||
getComputedStyle(layout).getPropertyValue(
|
||
"--buzz-profile-sticky-hero-height",
|
||
),
|
||
),
|
||
tabsTop: tabs ? Number.parseFloat(getComputedStyle(tabs).top) : 0,
|
||
};
|
||
});
|
||
expect(stickyOffsets.heroHeight).toBeGreaterThan(0);
|
||
expect(stickyOffsets.tabsTop).toBeCloseTo(stickyOffsets.heroHeight, 0);
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = element.scrollHeight;
|
||
});
|
||
await expect(
|
||
page.getByTestId("user-profile-primary-actions"),
|
||
).not.toBeInViewport();
|
||
await expect(primaryActions).toHaveCSS("opacity", "0");
|
||
await expect(stickyChromeSurface).toHaveAttribute("data-active", "true");
|
||
await expect(stickyChromeSurface).toHaveCSS("opacity", "1");
|
||
await expect(stickyChromeSurface).toHaveCSS("pointer-events", "none");
|
||
await expect(stickyChromeSurface).not.toHaveCSS("backdrop-filter", "none");
|
||
await expect(stickyHero).toBeInViewport();
|
||
await expect(stickyTabs).toBeInViewport();
|
||
const stickyChromeRects = await Promise.all(
|
||
[profileHeader, stickyHero, stickyTabs].map((element) =>
|
||
element.evaluate((node) => node.getBoundingClientRect().toJSON()),
|
||
),
|
||
);
|
||
expect(stickyChromeRects[1]?.top).toBeCloseTo(
|
||
stickyChromeRects[0]?.bottom ?? 0,
|
||
0,
|
||
);
|
||
expect(
|
||
Math.abs(
|
||
(stickyChromeRects[2]?.top ?? 0) - (stickyChromeRects[1]?.bottom ?? 0),
|
||
),
|
||
).toBeLessThanOrEqual(1);
|
||
const stickySurfaceRect = await stickyChromeSurface.evaluate((node) =>
|
||
node.getBoundingClientRect().toJSON(),
|
||
);
|
||
expect(stickySurfaceRect.top).toBeCloseTo(stickyChromeRects[0]?.top ?? 0, 0);
|
||
expect(stickySurfaceRect.bottom).toBeCloseTo(
|
||
stickyChromeRects[2]?.bottom ?? 0,
|
||
0,
|
||
);
|
||
await profileScrollBody.evaluate((element) => {
|
||
element.scrollTop = 0;
|
||
});
|
||
await expect(stickyChromeSurface).toHaveAttribute("data-active", "false");
|
||
await expect(stickyChromeSurface).toHaveCSS("opacity", "0");
|
||
await expect(
|
||
page.getByTestId("user-profile-panel").getByRole("heading", {
|
||
exact: true,
|
||
level: 2,
|
||
name: "Info",
|
||
}),
|
||
).toBeVisible();
|
||
await expect(page.getByTestId("user-profile-info-sections")).toHaveClass(
|
||
/space-y-4/,
|
||
);
|
||
await expect(
|
||
page.getByTestId("user-profile-tab-content-transition"),
|
||
).toHaveClass(/pt-2/);
|
||
const infoSection = page.getByTestId("user-profile-info-section");
|
||
await expect(
|
||
infoSection.getByRole("heading", { exact: true, name: "Info" }),
|
||
).toHaveClass(/text-xs/);
|
||
await expect(
|
||
infoSection.getByRole("heading", { exact: true, name: "Info" }),
|
||
).toHaveClass(/text-muted-foreground\/70/);
|
||
await expect(
|
||
infoSection.locator('[data-slot="panel-section-header"]'),
|
||
).toHaveClass(/px-4/);
|
||
await expect(
|
||
infoSection
|
||
.locator('[data-slot="panel-section-card"]')
|
||
.getByRole("heading", { exact: true, name: "Info" }),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-public-key")
|
||
.locator('[data-slot="profile-field-icon"]'),
|
||
).toHaveCount(1);
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-managed-by")
|
||
.locator('[data-slot="profile-field-icon"]'),
|
||
).toHaveCount(1);
|
||
const managedByRow = page.getByTestId("user-profile-managed-by");
|
||
const managedByActionIndicator = page.getByTestId(
|
||
"user-profile-managed-by-action-indicator",
|
||
);
|
||
await expect(managedByActionIndicator).toHaveCSS("opacity", "0");
|
||
await managedByRow.hover();
|
||
await expect(managedByActionIndicator).toHaveCSS("opacity", "1");
|
||
await expect(page.getByTestId("user-profile-owner-avatar")).toHaveCount(0);
|
||
await expect(
|
||
page.getByTestId("user-profile-model-settings-section"),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-agent-management-section")
|
||
.locator('[data-slot="panel-section-header"]'),
|
||
).toHaveCount(0);
|
||
const instructionRow = page.getByTestId("user-profile-agent-instruction-row");
|
||
await expect(instructionRow).toContainText("Agent instructions");
|
||
await expect(instructionRow).not.toContainText("View");
|
||
await expect(
|
||
instructionRow.locator('[data-slot="profile-ingress-icon"]'),
|
||
).toHaveCount(1);
|
||
for (const rowTestId of [
|
||
"user-profile-agent-instruction-row",
|
||
"user-profile-public-key",
|
||
"user-profile-managed-by",
|
||
]) {
|
||
await expect(
|
||
page.getByTestId(rowTestId).locator(":scope > span.rounded-full"),
|
||
).toHaveCount(0);
|
||
}
|
||
const publicKeyRow = page.getByTestId("user-profile-public-key");
|
||
const publicKeyCopy = page.getByTestId("user-profile-public-key-copy-status");
|
||
await expect(publicKeyCopy).toHaveCSS("opacity", "0");
|
||
await publicKeyRow.hover();
|
||
await expect(publicKeyCopy).toHaveCSS("opacity", "1");
|
||
await publicKeyRow.click();
|
||
await expect(publicKeyCopy).toHaveAttribute("data-copied", "true");
|
||
await expect
|
||
.poll(() => page.evaluate(() => navigator.clipboard.readText()))
|
||
.toBe(agentPubkey);
|
||
await expect(page.getByTestId("user-profile-agent-instruction")).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(page.getByTestId("user-profile-edit-agent-row")).toHaveCount(0);
|
||
await instructionRow.click();
|
||
await expect(page.getByTestId("persona-dialog")).toBeVisible();
|
||
await page
|
||
.getByTestId("persona-dialog")
|
||
.getByRole("button", {
|
||
name: "Cancel",
|
||
})
|
||
.click();
|
||
const managementRowOrder = await page
|
||
.getByTestId("user-profile-agent-management-section")
|
||
.locator("button[data-testid]")
|
||
.evaluateAll((buttons) =>
|
||
buttons.map((button) => button.getAttribute("data-testid")),
|
||
);
|
||
expect(managementRowOrder).toEqual([
|
||
"user-profile-duplicate-agent-row",
|
||
"user-profile-export-agent-row",
|
||
"user-profile-create-card-row",
|
||
"user-profile-archive-agent-row",
|
||
"user-profile-delete-agent-row",
|
||
]);
|
||
for (const rowTestId of managementRowOrder) {
|
||
const actionRow = page.getByTestId(rowTestId ?? "");
|
||
await expect(
|
||
actionRow.locator(":scope > svg[data-slot='profile-action-icon']"),
|
||
).toHaveCount(1);
|
||
await expect(actionRow.locator(":scope > span.rounded-full")).toHaveCount(
|
||
0,
|
||
);
|
||
}
|
||
await page.getByTestId("user-profile-duplicate-agent-row").click();
|
||
const duplicateDialog = page.getByTestId("persona-dialog");
|
||
await expect(duplicateDialog).toBeVisible();
|
||
await duplicateDialog.getByRole("button", { name: "Cancel" }).click();
|
||
await page.getByTestId("user-profile-export-agent-row").click();
|
||
const exportDialog = page.getByTestId("agent-snapshot-export-dialog");
|
||
await expect(exportDialog).toBeVisible();
|
||
await exportDialog.getByRole("button", { name: "Cancel" }).click();
|
||
await page.getByTestId("user-profile-create-card-row").click();
|
||
const cardMintDialog = page.getByTestId("agent-card-mint-dialog");
|
||
await expect(cardMintDialog).toBeVisible();
|
||
await page.keyboard.press("Escape");
|
||
await expect(cardMintDialog).toHaveCount(0);
|
||
const archiveAgentRow = page.getByTestId("user-profile-archive-agent-row");
|
||
await expect(archiveAgentRow).toHaveText(/Archive agent/);
|
||
await archiveAgentRow.click();
|
||
await expect(page.getByTestId("archive-confirm-dialog")).toBeVisible();
|
||
await page.getByRole("button", { name: "Cancel" }).click();
|
||
const deleteAgentRow = page.getByTestId("user-profile-delete-agent-row");
|
||
await expect(deleteAgentRow).toHaveText("Delete agent");
|
||
await deleteAgentRow.click();
|
||
await expect(page.getByTestId("agent-delete-confirm-dialog")).toBeVisible();
|
||
await page.getByRole("button", { name: "Cancel" }).click();
|
||
await expect(page.getByTestId("user-profile-tab-memories")).toHaveText(
|
||
"Memories",
|
||
);
|
||
await expect(page.getByTestId("user-profile-tab-runtime")).toHaveText(
|
||
"Runtime",
|
||
);
|
||
await expect(page.getByTestId("user-profile-tab-channels")).toHaveText(
|
||
"Channels",
|
||
);
|
||
await expect(page.getByTestId("user-profile-tab-list")).toHaveClass(
|
||
/bg-muted/,
|
||
);
|
||
const actionBackground = await page
|
||
.getByTestId("user-profile-agent-restart")
|
||
.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||
const segmentBackground = await page
|
||
.getByTestId("user-profile-tab-list")
|
||
.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||
expect(actionBackground).toBe(segmentBackground);
|
||
await expect(page.getByTestId("user-profile-tab-indicator")).toHaveCSS(
|
||
"transform",
|
||
"matrix(1, 0, 0, 1, 0, 0)",
|
||
);
|
||
await expect(page.getByTestId("user-profile-runtime-status")).toHaveCount(0);
|
||
await expect(page.getByTestId("user-profile-create-card")).toHaveCount(0);
|
||
await expect(
|
||
page.getByTestId("user-profile-model-settings-edit"),
|
||
).toHaveCount(0);
|
||
await expect(page.getByTestId("agent-config-model-copy-status")).toHaveCount(
|
||
0,
|
||
);
|
||
const tabContentTransition = page.getByTestId(
|
||
"user-profile-tab-content-transition",
|
||
);
|
||
await expect(tabContentTransition).toHaveClass(/pt-2/);
|
||
const tabMotionSamplesPromise = tabContentTransition.evaluate(
|
||
(container) =>
|
||
new Promise<Array<{ activeTab: string | undefined; x: number[] }>>(
|
||
(resolve) => {
|
||
const samples: Array<{
|
||
activeTab: string | undefined;
|
||
x: number[];
|
||
}> = [];
|
||
const startedAt = performance.now();
|
||
const sample = () => {
|
||
samples.push({
|
||
activeTab: container.dataset.activeTab,
|
||
x: Array.from(container.children).map((child) => {
|
||
const transform = getComputedStyle(child).transform;
|
||
return transform === "none"
|
||
? 0
|
||
: new DOMMatrixReadOnly(transform).m41;
|
||
}),
|
||
});
|
||
if (performance.now() - startedAt < 320) {
|
||
requestAnimationFrame(sample);
|
||
} else {
|
||
resolve(samples);
|
||
}
|
||
};
|
||
requestAnimationFrame(sample);
|
||
},
|
||
),
|
||
);
|
||
await page.getByTestId("user-profile-tab-runtime").click();
|
||
await expect(tabContentTransition).toHaveAttribute(
|
||
"data-transition-direction",
|
||
"forward",
|
||
);
|
||
const tabMotionSamples = await tabMotionSamplesPromise;
|
||
expect(
|
||
tabMotionSamples.some(
|
||
(sample) => sample.activeTab === "runtime" && sample.x.some((x) => x > 1),
|
||
),
|
||
).toBe(true);
|
||
expect(
|
||
tabMotionSamples.some(
|
||
(sample) =>
|
||
sample.activeTab === "runtime" && sample.x.some((x) => x < -1),
|
||
),
|
||
).toBe(true);
|
||
await expect
|
||
.poll(() =>
|
||
tabContentTransition.evaluate((container) =>
|
||
Array.from(container.children).every((child) => {
|
||
const transform = getComputedStyle(child).transform;
|
||
const x =
|
||
transform === "none" ? 0 : new DOMMatrixReadOnly(transform).m41;
|
||
return Math.abs(x) < 1;
|
||
}),
|
||
),
|
||
)
|
||
.toBe(true);
|
||
await expect(page.getByTestId("user-profile-tab-runtime")).toHaveAttribute(
|
||
"data-state",
|
||
"active",
|
||
);
|
||
await expect(
|
||
page.getByTestId("user-profile-panel").getByRole("heading", {
|
||
exact: true,
|
||
level: 2,
|
||
name: "Activity",
|
||
}),
|
||
).toBeVisible();
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-agent-status")
|
||
.locator('[data-slot="profile-field-icon"]'),
|
||
).toHaveCount(1);
|
||
await expect(
|
||
page.getByTestId("user-profile-model-settings-section"),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByTestId("user-profile-panel").getByRole("heading", {
|
||
exact: true,
|
||
level: 2,
|
||
name: "Agent configuration",
|
||
}),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByTestId("user-profile-agent-instruction-row"),
|
||
).toHaveCount(0);
|
||
const modelEditRow = page.getByRole("button", { name: "Edit Model" });
|
||
await expect(
|
||
modelEditRow.locator('[data-slot="agent-config-field-icon"]'),
|
||
).toHaveCount(1);
|
||
await expect(modelEditRow.locator(":scope > span.rounded-full")).toHaveCount(
|
||
0,
|
||
);
|
||
const modelEditIndicator = page.getByTestId(
|
||
"agent-config-model-edit-indicator",
|
||
);
|
||
await expect(modelEditIndicator).toHaveCSS("opacity", "0");
|
||
await modelEditRow.hover();
|
||
await expect(modelEditIndicator).toHaveCSS("opacity", "1");
|
||
await modelEditRow.click();
|
||
await expect(page.getByTestId("persona-dialog")).toBeVisible();
|
||
await page
|
||
.getByTestId("persona-dialog")
|
||
.getByRole("button", { name: "Cancel" })
|
||
.click();
|
||
const acpRow = page.getByTestId("user-profile-acp");
|
||
await expect(acpRow.locator('[data-slot="profile-field-icon"]')).toHaveCount(
|
||
1,
|
||
);
|
||
const acpCopy = page.getByTestId("user-profile-acp-copy-status");
|
||
await expect(acpCopy).toHaveCSS("opacity", "0");
|
||
await acpRow.hover();
|
||
await expect(acpCopy).toHaveCSS("opacity", "1");
|
||
const startOnLaunchRow = page.getByTestId("user-profile-start-on-launch");
|
||
await expect(
|
||
startOnLaunchRow.locator('[data-slot="profile-field-icon"]'),
|
||
).toHaveCount(1);
|
||
const startOnLaunchToggle = page.getByTestId(
|
||
"user-profile-start-on-launch-toggle",
|
||
);
|
||
const activitySection = page.getByTestId(
|
||
"user-profile-runtime-activity-section",
|
||
);
|
||
await expect(
|
||
activitySection.getByTestId("user-profile-agent-status"),
|
||
).toBeVisible();
|
||
await expect(
|
||
activitySection.getByTestId("user-profile-start-on-launch"),
|
||
).toBeVisible();
|
||
const activityRowOrder = await activitySection
|
||
.locator("[data-testid^='user-profile-']")
|
||
.evaluateAll((rows) =>
|
||
rows
|
||
.map((row) => row.getAttribute("data-testid"))
|
||
.filter((testId): testId is string => testId !== null),
|
||
);
|
||
expect(activityRowOrder.indexOf("user-profile-start-on-launch")).toBe(
|
||
activityRowOrder.indexOf("user-profile-agent-status") + 1,
|
||
);
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-agent-configuration-section")
|
||
.getByTestId("user-profile-start-on-launch"),
|
||
).toHaveCount(0);
|
||
await expect(startOnLaunchRow).not.toContainText("Yes");
|
||
await expect(startOnLaunchRow).toBeChecked();
|
||
await expect(startOnLaunchToggle).toHaveAttribute("data-state", "checked");
|
||
await startOnLaunchRow.click();
|
||
await expect(startOnLaunchRow).not.toBeChecked();
|
||
await expect(startOnLaunchToggle).toHaveAttribute("data-state", "unchecked");
|
||
await expectHashSearchParam(page, "profileTab", "runtime");
|
||
const instancesSection = page.getByTestId("user-profile-instances-section");
|
||
await expect(
|
||
instancesSection.getByRole("heading", {
|
||
exact: true,
|
||
level: 2,
|
||
name: "Instances",
|
||
}),
|
||
).toHaveClass(/text-xs/);
|
||
await expect(
|
||
instancesSection
|
||
.locator('[data-slot="panel-section-card"]')
|
||
.getByRole("heading", { exact: true, name: "Instances" }),
|
||
).toHaveCount(0);
|
||
const runtimeSectionHeaderOffsets = await Promise.all(
|
||
[
|
||
page.getByTestId("user-profile-runtime-activity-section"),
|
||
page.getByTestId("user-profile-agent-configuration-section"),
|
||
page.getByTestId("user-profile-model-settings-section"),
|
||
instancesSection,
|
||
].map((section) =>
|
||
section.evaluate((element) => {
|
||
const header = element.querySelector(
|
||
'[data-slot="panel-section-header"]',
|
||
);
|
||
const card = element.querySelector('[data-slot="panel-section-card"]');
|
||
if (
|
||
!(header instanceof HTMLElement) ||
|
||
!(card instanceof HTMLElement)
|
||
) {
|
||
throw new Error("Expected panel section header and card");
|
||
}
|
||
return (
|
||
card.getBoundingClientRect().top - header.getBoundingClientRect().top
|
||
);
|
||
}),
|
||
),
|
||
);
|
||
expect(
|
||
runtimeSectionHeaderOffsets.every(
|
||
(offset) => Math.abs(offset - runtimeSectionHeaderOffsets[0]) <= 1,
|
||
),
|
||
).toBe(true);
|
||
const instancesRow = page.getByTestId("user-profile-instances");
|
||
await expect(instancesRow).toContainText("1 instance");
|
||
await expect(instancesRow).toHaveClass(/min-h-16/);
|
||
await instancesRow.click();
|
||
await expect(
|
||
page.getByTestId(`user-profile-instance-${agentPubkey}`),
|
||
).toContainText("Current");
|
||
|
||
await expect(
|
||
page.getByTestId("user-profile-settings-menu-trigger"),
|
||
).toHaveCount(0);
|
||
|
||
const diagnosticsIngress = page.getByTestId(
|
||
"user-profile-diagnostics-ingress",
|
||
);
|
||
await expect(
|
||
diagnosticsIngress.locator('[data-slot="profile-ingress-icon"]'),
|
||
).toHaveCount(1);
|
||
await expect(diagnosticsIngress).not.toContainText("View");
|
||
await expect(
|
||
diagnosticsIngress.locator("svg.lucide-chevron-right"),
|
||
).toBeVisible();
|
||
await expect(diagnosticsIngress.locator("svg.lucide-chevron-up")).toHaveCount(
|
||
0,
|
||
);
|
||
await diagnosticsIngress.click();
|
||
await expectHashSearchParam(page, "profileView", "diagnostics");
|
||
await expect(
|
||
page.getByRole("heading", { level: 2, name: "Harness log" }),
|
||
).toBeVisible();
|
||
await expect(page.getByTestId("user-profile-agent-status")).toHaveCount(0);
|
||
await expect(page.getByTestId("managed-agent-log-content")).toBeVisible();
|
||
await page.getByTestId("user-profile-panel-back").click();
|
||
await expectHashSearchParam(page, "profileView", null);
|
||
await expectHashSearchParam(page, "profileTab", "runtime");
|
||
await expect(
|
||
page.getByRole("heading", { level: 2, name: "Profile" }),
|
||
).toBeVisible();
|
||
|
||
await page.getByTestId("user-profile-tab-info").click();
|
||
await expect(
|
||
page.getByTestId("user-profile-tab-content-transition"),
|
||
).toHaveAttribute("data-transition-direction", "backward");
|
||
await expectHashSearchParam(page, "profileTab", null);
|
||
await page.getByTestId(`user-profile-view-activity-${agentPubkey}`).click();
|
||
await expect(page.getByTestId("agent-session-thread-panel")).toBeVisible();
|
||
await page.getByTestId("agent-session-back").click();
|
||
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
|
||
await expect(
|
||
page.getByRole("heading", { level: 2, name: "Profile" }),
|
||
).toBeVisible();
|
||
|
||
await page.getByTestId("user-profile-tab-channels").click();
|
||
await expectHashSearchParam(page, "profileTab", "channels");
|
||
await expect(
|
||
page.getByTestId("user-profile-panel").getByRole("heading", {
|
||
exact: true,
|
||
level: 2,
|
||
name: "Channels",
|
||
}),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-channels-section")
|
||
.locator('[data-slot="panel-section-header"]'),
|
||
).toHaveCount(0);
|
||
await expect(page.getByTestId("user-profile-channels-list")).toContainText(
|
||
"#general",
|
||
);
|
||
|
||
await page.getByTestId("user-profile-tab-memories").click();
|
||
await expectHashSearchParam(page, "profileTab", "memories");
|
||
await expect(
|
||
page.getByTestId("user-profile-panel").getByRole("heading", {
|
||
exact: true,
|
||
level: 2,
|
||
name: "Memories",
|
||
}),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page
|
||
.getByTestId("user-profile-memories-section")
|
||
.locator('[data-slot="panel-section-header"]'),
|
||
).toHaveCount(0);
|
||
await expect(page.getByTestId("agent-memory-section")).toBeVisible();
|
||
await expect(page.getByTestId("agent-memory-list")).toContainText(
|
||
"ui-density",
|
||
);
|
||
await page.goBack();
|
||
await expectHashSearchParam(page, "profileTab", "channels");
|
||
await expect(page.getByTestId("user-profile-channels-list")).toContainText(
|
||
"#general",
|
||
);
|
||
await page.goForward();
|
||
await expectHashSearchParam(page, "profileTab", "memories");
|
||
await expect(page.getByTestId("agent-memory-section")).toBeVisible();
|
||
await expect(page.getByTestId("agent-memory-truncated")).toContainText(
|
||
"View all (9)",
|
||
);
|
||
await page.getByTestId("agent-memory-truncated").click();
|
||
await expect(page.getByTestId("agent-memory-list")).toContainText("orphan");
|
||
});
|
||
|
||
test("an older agent message stays exact while persona navigation selects the live instance", async ({
|
||
page,
|
||
}, testInfo) => {
|
||
const personaId = "profile-parity-agent";
|
||
const historicalPubkey = TEST_IDENTITIES.charlie.pubkey;
|
||
const currentPubkey = "d".repeat(64);
|
||
await installMockBridge(page, {
|
||
agentMemory: createMockAgentMemoryListing(),
|
||
managedAgents: [
|
||
{
|
||
channelNames: ["agents"],
|
||
name: "Earlier Parity Agent",
|
||
personaId,
|
||
pubkey: historicalPubkey,
|
||
status: "stopped",
|
||
},
|
||
{
|
||
channelNames: ["agents"],
|
||
name: "Current Parity Agent",
|
||
personaId,
|
||
pubkey: currentPubkey,
|
||
status: "running",
|
||
},
|
||
],
|
||
oaOwnerIsMe: true,
|
||
personas: [
|
||
{
|
||
displayName: "Parity Agent",
|
||
id: personaId,
|
||
isActive: true,
|
||
systemPrompt: "Keep every profile entry point in sync.",
|
||
},
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByTestId(`persona-agent-row-${personaId}`).click();
|
||
await expect(
|
||
page.getByTestId("user-profile-agent-primary-action"),
|
||
).toHaveAttribute("aria-label", "Stop");
|
||
|
||
await page.getByTestId("user-profile-tab-runtime").click();
|
||
await page.getByTestId("user-profile-instances").click();
|
||
await page.getByTestId(`user-profile-instance-${historicalPubkey}`).click();
|
||
await expectHashSearchParam(page, "profile", historicalPubkey);
|
||
await expectHashSearchParam(page, "profileTab", "runtime");
|
||
await expect(
|
||
page.getByTestId("user-profile-agent-primary-action"),
|
||
).toHaveAttribute("aria-label", "Start agent");
|
||
await expect(
|
||
page.getByTestId(`user-profile-instance-${historicalPubkey}`),
|
||
).toContainText("Current");
|
||
|
||
const exactInstanceContract = await readOwnedAgentProfileContract(page);
|
||
|
||
await page.getByTestId("auxiliary-panel-close").click();
|
||
await page.getByTestId("channel-agents").click();
|
||
const historicalMessage = page
|
||
.getByTestId("message-row")
|
||
.filter({ hasText: "Indexing the channel catalog now." });
|
||
await expect(historicalMessage).toBeVisible();
|
||
await historicalMessage.locator("button").first().click();
|
||
await expect(
|
||
page.getByTestId("user-profile-agent-primary-action"),
|
||
).toHaveAttribute("aria-label", "Start agent");
|
||
const messageContract = await readOwnedAgentProfileContract(page);
|
||
|
||
expect(messageContract).toEqual(exactInstanceContract);
|
||
await page.getByTestId("user-profile-tab-info").click();
|
||
await waitForAnimations(page);
|
||
await page.screenshot({
|
||
path: testInfo.outputPath("historical-exact-instance.png"),
|
||
});
|
||
});
|
||
|
||
test("restored Inbox deep link hides the back arrow", async ({ page }) => {
|
||
// Charlie is a `bot` member of #agents and authors a seeded message there;
|
||
// seeding a managed agent with the same pubkey makes that message's avatar
|
||
// open a managed-agent profile panel with the Inbox ingress. Unlike an
|
||
// agent created at runtime through the bridge, this seed survives
|
||
// `page.reload()` because init scripts re-run on navigation.
|
||
const agentPubkey = TEST_IDENTITIES.charlie.pubkey;
|
||
await installMockBridge(page, {
|
||
managedAgents: [
|
||
{
|
||
channelNames: ["agents"],
|
||
name: "Charlie",
|
||
pubkey: agentPubkey,
|
||
status: "running",
|
||
},
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await page.getByTestId("channel-agents").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
||
|
||
const messageRow = page
|
||
.getByTestId("message-row")
|
||
.filter({ hasText: "Indexing the channel catalog now." });
|
||
await expect(messageRow).toBeVisible();
|
||
await messageRow.locator("button").first().click();
|
||
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
|
||
|
||
// Opened from the profile panel: a return target was captured, so the
|
||
// header shows the back arrow.
|
||
await page.getByTestId(`user-profile-view-activity-${agentPubkey}`).click();
|
||
await expect(page.getByTestId("agent-session-thread-panel")).toBeVisible();
|
||
await expect(page.getByTestId("agent-session-back")).toBeVisible();
|
||
|
||
// A reload keeps the `agentSession` URL param but drops the in-memory
|
||
// return target, so the restored panel hides the back arrow and close is
|
||
// the only affordance — never a blind history pop.
|
||
await page.reload();
|
||
await expect(page.getByTestId("agent-session-thread-panel")).toBeVisible();
|
||
await expect(page.getByTestId("agent-session-back")).toHaveCount(0);
|
||
await expect(page.getByTestId("auxiliary-panel-close")).toBeVisible();
|
||
await page.getByTestId("auxiliary-panel-close").click();
|
||
await expect(page.getByTestId("agent-session-thread-panel")).toHaveCount(0);
|
||
});
|
||
|
||
test("declared owner sees runtime tab for a remote relay agent", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
relayAgents: [
|
||
{
|
||
pubkey:
|
||
"a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00",
|
||
name: "nadia",
|
||
agentType: "goose",
|
||
capabilities: ["search", "summaries"],
|
||
channelNames: ["agents"],
|
||
respondTo: "anyone",
|
||
},
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await page.getByTestId("channel-agents").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
||
|
||
const messageRow = page.getByTestId("message-row").filter({
|
||
has: page.getByText("Indexing remotely for my owner."),
|
||
});
|
||
await expect(messageRow.first()).toBeVisible({ timeout: 5_000 });
|
||
await messageRow.first().getByRole("button").first().click();
|
||
|
||
const panel = page.getByTestId("user-profile-panel");
|
||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||
await expect(panel.getByRole("tab", { name: "Runtime" })).toBeVisible();
|
||
await panel.getByRole("tab", { name: "Runtime" }).click();
|
||
|
||
await expect(panel.getByTestId("user-profile-runtime")).toContainText(
|
||
"Runtime",
|
||
);
|
||
await expect(panel.getByTestId("user-profile-runtime")).toContainText(
|
||
"Goose",
|
||
);
|
||
await expect(panel.getByTestId("user-profile-respond-to")).toContainText(
|
||
"Anyone",
|
||
);
|
||
|
||
// Declared ownership grants read visibility only; local-management write UI
|
||
// stays hidden because this relay agent is not in the managed-agents list.
|
||
await expect(panel.getByText("Model")).toHaveCount(0);
|
||
await expect(
|
||
panel.getByRole("button", { name: /Start|Stop|Deploy/ }),
|
||
).toHaveCount(0);
|
||
});
|
||
|
||
test("declared owner sees runtime tab without a relay-agent record", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await page.getByTestId("channel-agents").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
||
|
||
const messageRow = page.getByTestId("message-row").filter({
|
||
has: page.getByText("Indexing remotely for my owner."),
|
||
});
|
||
await expect(messageRow.first()).toBeVisible({ timeout: 5_000 });
|
||
await messageRow.first().getByRole("button").first().click();
|
||
|
||
const panel = page.getByTestId("user-profile-panel");
|
||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||
await expect(panel.getByTestId("user-profile-agent-type")).toHaveCount(0);
|
||
await expect(panel.getByTestId("user-profile-capabilities")).toHaveCount(0);
|
||
await expect(panel.getByRole("tab", { name: "Runtime" })).toBeVisible();
|
||
await panel.getByRole("tab", { name: "Runtime" }).click();
|
||
|
||
await expect(panel.getByTestId("user-profile-agent-profile")).toContainText(
|
||
"Agent profile",
|
||
);
|
||
await expect(panel.getByTestId("user-profile-agent-profile")).toContainText(
|
||
"Declared owner verified",
|
||
);
|
||
await expect(panel.getByTestId("user-profile-runtime")).toHaveCount(0);
|
||
await expect(panel.getByTestId("user-profile-respond-to")).toHaveCount(0);
|
||
await expect(panel.getByTestId("user-profile-runtime-preview")).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(
|
||
panel.getByTestId("user-profile-runtime-preview-notice"),
|
||
).toHaveCount(0);
|
||
await expect(panel.getByText("Harness log", { exact: true })).toHaveCount(0);
|
||
|
||
// No relay/managed runtime record means no write or management affordance —
|
||
// only the truthful NIP-OA profile signal is rendered in Runtime.
|
||
await expect(panel.getByText("Model")).toHaveCount(0);
|
||
await expect(
|
||
panel.getByRole("button", { name: /Start|Stop|Deploy/ }),
|
||
).toHaveCount(0);
|
||
});
|
||
|
||
test("non-owner agent profile shows only reported public agent data", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await page.getByTestId("channel-agents").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
||
|
||
const messageRow = page
|
||
.getByTestId("message-row")
|
||
.filter({ hasText: "Indexing the channel catalog now." });
|
||
await expect(messageRow).toBeVisible();
|
||
await messageRow.locator("button").first().click();
|
||
|
||
const panel = page.getByTestId("user-profile-panel");
|
||
await expect(panel).toBeVisible();
|
||
await expect(panel.getByTestId("user-profile-agent-type")).toContainText(
|
||
"codex",
|
||
);
|
||
await expect(panel.getByTestId("user-profile-capabilities")).toContainText(
|
||
"code, reviews",
|
||
);
|
||
await expect(panel.getByRole("tab", { name: "Runtime" })).toHaveCount(0);
|
||
await expect(panel.getByTestId("user-profile-runtime-preview")).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(
|
||
panel.getByTestId("user-profile-runtime-preview-notice"),
|
||
).toHaveCount(0);
|
||
});
|
||
|
||
test("owned agent absent from relay/managed lists still renders agent framing", async ({
|
||
page,
|
||
}) => {
|
||
// Regression: bot-detection used to rely solely on the relay-agents registry
|
||
// + the local managed-agents list. An owned agent deployed elsewhere can miss
|
||
// BOTH lists, so the panel rendered it as a human (wrong archive framing).
|
||
// The fix ORs in the kind:0 NIP-OA agent flag (same signal the archive gate
|
||
// trusts), surfaced via the users-batch summary's `isAgent`.
|
||
const ednaPubkey =
|
||
"16aaadcf39011edbd887e4abefe5837170621db277e234f3f6c220d38ba75ecf";
|
||
await installMockBridge(page, {
|
||
// Seeded as an agent (kind:0 NIP-OA owner) but NOT as a managed agent and
|
||
// NOT in the relay-agents registry — exactly the bug scenario.
|
||
searchProfiles: [
|
||
{ pubkey: ednaPubkey, displayName: "Edna", isAgent: true },
|
||
],
|
||
});
|
||
await page.goto("/");
|
||
|
||
await page.getByTestId("channel-general").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||
await waitForMockLiveSubscription(page, "general");
|
||
|
||
await page.evaluate(
|
||
({ pubkey }) => {
|
||
const emit = (
|
||
window as Window & {
|
||
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
||
channelName: string;
|
||
content: string;
|
||
pubkey: string;
|
||
}) => unknown;
|
||
}
|
||
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
|
||
if (!emit) {
|
||
throw new Error("Mock message emitter is unavailable.");
|
||
}
|
||
emit({ channelName: "general", content: "Edna check-in", pubkey });
|
||
},
|
||
{ pubkey: ednaPubkey },
|
||
);
|
||
|
||
const messageRow = page
|
||
.getByTestId("message-row")
|
||
.filter({ hasText: "Edna check-in" });
|
||
await expect(messageRow).toBeVisible();
|
||
await messageRow.locator("button").first().click();
|
||
|
||
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
|
||
// The bot indicator only renders when isBot resolves true — the assertion
|
||
// that the OA-owner signal now drives agent framing.
|
||
await expect(page.getByTestId("profile-bot-indicator")).toBeVisible();
|
||
});
|
||
|
||
test("renders settings in the app shell with a back button", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
const inboxNavButton = page
|
||
.getByTestId("app-sidebar")
|
||
.getByRole("button", { name: "Inbox" });
|
||
await expect(inboxNavButton).toBeVisible();
|
||
|
||
await openSettings(page);
|
||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||
await expect(page.getByTestId("settings-back-to-app")).toBeVisible();
|
||
await expect(page.getByPlaceholder("Search everything")).toHaveCount(0);
|
||
await expect(page.getByText("Personal", { exact: true })).toBeVisible();
|
||
const personalGroup = page
|
||
.getByTestId("settings-nav-channel-templates")
|
||
.locator("xpath=ancestor::*[@data-sidebar='group']");
|
||
await expect(personalGroup).toContainText("Personal");
|
||
await expect(
|
||
page.getByTestId("settings-nav-channel-templates"),
|
||
).toContainText("Channel templates");
|
||
await expect(page.getByTestId("settings-nav-profile")).toHaveAttribute(
|
||
"aria-pressed",
|
||
"true",
|
||
);
|
||
await expect(page.getByText("Communities", { exact: true })).toBeVisible();
|
||
await expect(
|
||
page.getByTestId("settings-nav-channel-templates"),
|
||
).toBeVisible();
|
||
await expect(page.getByText("App", { exact: true })).toBeVisible();
|
||
await expect(page.getByTestId("settings-nav-agents")).toBeVisible();
|
||
await expect(
|
||
page.getByTestId("settings-profile").getByRole("heading", {
|
||
exact: true,
|
||
name: "Profile",
|
||
}),
|
||
).toBeVisible();
|
||
await page.getByTestId("settings-nav-appearance").click();
|
||
await expect(
|
||
page.getByTestId("settings-theme").getByRole("heading", {
|
||
name: "Appearance",
|
||
}),
|
||
).toBeVisible();
|
||
await expect(inboxNavButton).toHaveCount(0);
|
||
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
await expectHomeView(page);
|
||
await expect(inboxNavButton).toBeVisible();
|
||
});
|
||
|
||
test("notification settings drive the Inbox badge and desktop alerts", async ({
|
||
page,
|
||
}) => {
|
||
async function getAppBadgeCount() {
|
||
return page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_APP_BADGE_COUNT__?: number;
|
||
};
|
||
|
||
return win.__BUZZ_E2E_APP_BADGE_COUNT__ ?? 0;
|
||
});
|
||
}
|
||
|
||
await page.goto("/");
|
||
await expect(page.getByTestId("sidebar-home-count")).toHaveCount(0);
|
||
|
||
await openSettings(page, "notifications");
|
||
await expect(page.getByTestId("settings-notifications")).toBeVisible();
|
||
await expect(page.getByTestId("notifications-desktop-state")).toContainText(
|
||
"On",
|
||
);
|
||
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
await page.getByTestId("channel-general").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||
|
||
// The dock badge sums unreadChannelIds.size + homeBadgeCount. Seeded test
|
||
// channels may start with unreads, so capture the baseline after navigating
|
||
// to general (which marks it read) but before injecting the mock mention.
|
||
const baseline = await getAppBadgeCount();
|
||
|
||
await page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
|
||
category: "mention" | "needs_action" | "activity" | "agent_activity";
|
||
channel_id: string | null;
|
||
channel_name: string;
|
||
content: string;
|
||
created_at: number;
|
||
id: string;
|
||
kind: number;
|
||
pubkey: string;
|
||
tags: string[][];
|
||
}) => unknown;
|
||
};
|
||
|
||
win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?.({
|
||
category: "mention",
|
||
channel_id: "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9",
|
||
channel_name: "engineering",
|
||
content: "Please review the rollout checklist.",
|
||
created_at: Math.floor(Date.now() / 1000) + 5,
|
||
id: `mock-feed-notification-${Date.now()}`,
|
||
kind: 9,
|
||
pubkey:
|
||
"bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260",
|
||
tags: [
|
||
["e", "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9"],
|
||
[
|
||
"p",
|
||
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||
],
|
||
],
|
||
});
|
||
});
|
||
|
||
await expect(page.getByTestId("sidebar-home-count")).toHaveText("1");
|
||
await expect.poll(getAppBadgeCount).toBe(baseline + 1);
|
||
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_NOTIFICATIONS__?: Array<{
|
||
body: string | null;
|
||
title: string;
|
||
}>;
|
||
};
|
||
|
||
return win.__BUZZ_E2E_NOTIFICATIONS__?.length ?? 0;
|
||
}),
|
||
)
|
||
.toBe(1);
|
||
|
||
const notifications = await page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_NOTIFICATIONS__?: Array<{
|
||
body: string | null;
|
||
title: string;
|
||
}>;
|
||
};
|
||
|
||
return win.__BUZZ_E2E_NOTIFICATIONS__ ?? [];
|
||
});
|
||
|
||
expect(notifications).toEqual([
|
||
{
|
||
body: "Please review the rollout checklist.",
|
||
title: "bob mentioned you in #engineering",
|
||
},
|
||
]);
|
||
|
||
const clickedNotification = await page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_CLICK_NOTIFICATION__?: (index: number) => boolean;
|
||
};
|
||
|
||
return win.__BUZZ_E2E_CLICK_NOTIFICATION__?.(0) ?? false;
|
||
});
|
||
expect(clickedNotification).toBe(true);
|
||
|
||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||
await expect(page.getByTestId("message-timeline")).toContainText(
|
||
"Please review the rollout checklist.",
|
||
);
|
||
|
||
await openSettings(page, "notifications");
|
||
await page.getByTestId("notifications-home-badge-toggle").click();
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||
await expect(page.getByTestId("sidebar-home-count")).toHaveCount(0);
|
||
await expect.poll(getAppBadgeCount).toBe(baseline);
|
||
|
||
await openSettings(page, "notifications");
|
||
await page.getByTestId("notifications-home-badge-toggle").click();
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
await expect(page.getByTestId("sidebar-home-count")).toHaveText("1");
|
||
await expect.poll(getAppBadgeCount).toBe(baseline + 1);
|
||
|
||
await page
|
||
.getByTestId("app-sidebar")
|
||
.getByRole("button", { name: "Inbox" })
|
||
.click();
|
||
await expectHomeView(page);
|
||
await expect(page.getByTestId("sidebar-home-count")).toHaveCount(0);
|
||
await expect.poll(getAppBadgeCount).toBe(baseline);
|
||
});
|
||
|
||
test("desktop notification clicks open the matching forum thread", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "notifications");
|
||
await expect(page.getByTestId("notifications-desktop-state")).toContainText(
|
||
"On",
|
||
);
|
||
await page.getByTestId("settings-back-to-app").click();
|
||
await expectHomeView(page);
|
||
|
||
await page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
|
||
category: "mention" | "needs_action" | "activity" | "agent_activity";
|
||
channel_id: string | null;
|
||
channel_name: string;
|
||
content: string;
|
||
created_at: number;
|
||
id: string;
|
||
kind: number;
|
||
pubkey: string;
|
||
tags: string[][];
|
||
}) => unknown;
|
||
};
|
||
|
||
win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?.({
|
||
category: "mention",
|
||
channel_id: "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11",
|
||
channel_name: "watercooler",
|
||
content: "Release checklist: async feedback thread.",
|
||
created_at: Math.floor(Date.now() / 1000) + 5,
|
||
id: "mock-forum-release-thread",
|
||
kind: 45001,
|
||
pubkey:
|
||
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f",
|
||
tags: [["h", "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11"]],
|
||
});
|
||
});
|
||
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_NOTIFICATIONS__?: Array<{
|
||
body: string | null;
|
||
title: string;
|
||
}>;
|
||
};
|
||
|
||
return win.__BUZZ_E2E_NOTIFICATIONS__?.length ?? 0;
|
||
}),
|
||
)
|
||
.toBe(1);
|
||
|
||
const clickedNotification = await page.evaluate(() => {
|
||
const win = window as Window & {
|
||
__BUZZ_E2E_CLICK_NOTIFICATION__?: (index: number) => boolean;
|
||
};
|
||
|
||
return win.__BUZZ_E2E_CLICK_NOTIFICATION__?.(0) ?? false;
|
||
});
|
||
expect(clickedNotification).toBe(true);
|
||
|
||
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
||
await expect(
|
||
page.getByRole("button", { name: "Back to posts" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByText("Release checklist: async feedback thread."),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("opens settings with the keyboard shortcut and updates theme", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
await expectHomeView(page);
|
||
|
||
await page.keyboard.press(
|
||
process.platform === "darwin" ? "Meta+," : "Control+,",
|
||
);
|
||
|
||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||
await expect(page.getByTestId("settings-nav-profile")).toHaveAttribute(
|
||
"aria-pressed",
|
||
"true",
|
||
);
|
||
await expect(
|
||
page.getByTestId("settings-profile").getByRole("heading", {
|
||
exact: true,
|
||
name: "Profile",
|
||
}),
|
||
).toBeVisible();
|
||
await page.getByTestId("settings-nav-appearance").click();
|
||
|
||
// Default is Buzz in System mode; Playwright's default color scheme is
|
||
// light, so the app boots with the light Buzz theme.
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() => document.documentElement.classList.contains("light")),
|
||
)
|
||
.toBe(true);
|
||
|
||
// Switch to Light mode tab to reveal light themes. Target the testid — in
|
||
// the default System mode the "Light" paired-theme tile shares the same
|
||
// accessible name as the mode button.
|
||
await page.getByTestId("appearance-mode-light").click();
|
||
|
||
// Switch to a light theme — verifies dark→light transition
|
||
await page.getByTestId("theme-style-trigger").click();
|
||
await page.getByTestId("theme-option-github-light").click();
|
||
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() => document.documentElement.classList.contains("light")),
|
||
)
|
||
.toBe(true);
|
||
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() => document.documentElement.classList.contains("dark")),
|
||
)
|
||
.toBe(false);
|
||
|
||
// CSS variables are set on the root element (the real theming mechanism)
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() =>
|
||
document.documentElement.style.getPropertyValue("--background").trim(),
|
||
),
|
||
)
|
||
.toBeTruthy();
|
||
|
||
// Theme name persists in localStorage
|
||
await expect
|
||
.poll(() => page.evaluate(() => localStorage.getItem("buzz-theme")))
|
||
.toBe("github-light");
|
||
|
||
// Switch to Dark mode tab to reveal dark themes
|
||
await page.getByTestId("appearance-mode-dark").click();
|
||
|
||
// Switch back to a dark theme — verifies light→dark transition
|
||
await page.getByTestId("theme-option-dracula").click();
|
||
|
||
await expect
|
||
.poll(() =>
|
||
page.evaluate(() => document.documentElement.classList.contains("dark")),
|
||
)
|
||
.toBe(true);
|
||
|
||
await expect
|
||
.poll(() => page.evaluate(() => localStorage.getItem("buzz-theme")))
|
||
.toBe("dracula");
|
||
|
||
// Close settings with keyboard shortcut
|
||
await page.keyboard.press(
|
||
process.platform === "darwin" ? "Meta+," : "Control+,",
|
||
);
|
||
await expect(page.getByTestId("settings-view")).toHaveCount(0);
|
||
await expectHomeView(page);
|
||
});
|
||
|
||
test("supports webview zoom keyboard shortcuts", async ({ page }) => {
|
||
await page.goto("/");
|
||
await expectHomeView(page);
|
||
|
||
const getTextScaleState = () =>
|
||
page.evaluate(() => ({
|
||
rootFontSize: getComputedStyle(document.documentElement).fontSize,
|
||
storedScale: localStorage.getItem("buzz:text-scale"),
|
||
webviewZoom: (window as Window & { __BUZZ_E2E_WEBVIEW_ZOOM__?: number })
|
||
.__BUZZ_E2E_WEBVIEW_ZOOM__,
|
||
}));
|
||
const dispatchPrimaryShortcut = (
|
||
key: string,
|
||
code: string,
|
||
shiftKey = false,
|
||
) =>
|
||
page.evaluate(
|
||
({ code, key, shiftKey }) => {
|
||
const isMac = /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
||
window.dispatchEvent(
|
||
new KeyboardEvent("keydown", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
code,
|
||
ctrlKey: !isMac,
|
||
key,
|
||
metaKey: isMac,
|
||
shiftKey,
|
||
}),
|
||
);
|
||
},
|
||
{ code, key, shiftKey },
|
||
);
|
||
|
||
await dispatchPrimaryShortcut("+", "Equal", true);
|
||
|
||
await expect.poll(getTextScaleState).toEqual({
|
||
rootFontSize: "17.6px",
|
||
storedScale: "1.1",
|
||
webviewZoom: 1,
|
||
});
|
||
|
||
await dispatchPrimaryShortcut("-", "Minus");
|
||
|
||
await expect.poll(getTextScaleState).toEqual({
|
||
rootFontSize: "16px",
|
||
storedScale: null,
|
||
webviewZoom: 1,
|
||
});
|
||
|
||
await dispatchPrimaryShortcut("+", "Equal", true);
|
||
await dispatchPrimaryShortcut("+", "Equal", true);
|
||
|
||
await expect.poll(getTextScaleState).toEqual({
|
||
rootFontSize: "19.2px",
|
||
storedScale: "1.2",
|
||
webviewZoom: 1,
|
||
});
|
||
|
||
await dispatchPrimaryShortcut("0", "Digit0");
|
||
|
||
await expect.poll(getTextScaleState).toEqual({
|
||
rootFontSize: "16px",
|
||
storedScale: null,
|
||
webviewZoom: 1,
|
||
});
|
||
});
|
||
|
||
test("storage clear resets composed font size and keyboard zoom across windows", async ({
|
||
context,
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
await openSettings(page, "appearance");
|
||
await page.getByTestId("font-size-larger").click();
|
||
|
||
const dispatchZoomIn = () =>
|
||
page.evaluate(() => {
|
||
const isMac = /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
||
window.dispatchEvent(
|
||
new KeyboardEvent("keydown", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
code: "Equal",
|
||
ctrlKey: !isMac,
|
||
key: "+",
|
||
metaKey: isMac,
|
||
shiftKey: true,
|
||
}),
|
||
);
|
||
});
|
||
|
||
for (let step = 0; step < 5; step += 1) {
|
||
await dispatchZoomIn();
|
||
}
|
||
|
||
// Zoom scales the real root; the Font size preference layers a text-only
|
||
// multiplier on top. Resolve the composed type rem through a rendered probe
|
||
// so the CSS calc is actually evaluated (root px × 15/14 for "larger").
|
||
const readTypographyState = () =>
|
||
page.evaluate(() => {
|
||
const probe = document.createElement("span");
|
||
probe.style.fontSize = "var(--buzz-type-rem)";
|
||
document.documentElement.appendChild(probe);
|
||
const typeRemPx =
|
||
Math.round(Number.parseFloat(getComputedStyle(probe).fontSize) * 100) /
|
||
100;
|
||
probe.remove();
|
||
return {
|
||
fontSize: document.documentElement.dataset.fontSize,
|
||
rootFontSize: getComputedStyle(document.documentElement).fontSize,
|
||
typeRemPx,
|
||
textScale: localStorage.getItem("buzz:text-scale"),
|
||
};
|
||
});
|
||
|
||
await expect.poll(readTypographyState).toEqual({
|
||
fontSize: "larger",
|
||
rootFontSize: "24px",
|
||
typeRemPx: 25.71,
|
||
textScale: "1.5",
|
||
});
|
||
|
||
const peerPage = await context.newPage();
|
||
await installMockBridge(peerPage);
|
||
await peerPage.goto("/");
|
||
await peerPage.evaluate(() => localStorage.clear());
|
||
|
||
await expect.poll(readTypographyState).toEqual({
|
||
fontSize: "default",
|
||
rootFontSize: "16px",
|
||
typeRemPx: 16,
|
||
textScale: null,
|
||
});
|
||
|
||
await page.keyboard.press(
|
||
process.platform === "darwin" ? "Meta+-" : "Control+-",
|
||
);
|
||
await expect.poll(readTypographyState).toEqual({
|
||
fontSize: "default",
|
||
rootFontSize: "14.4px",
|
||
typeRemPx: 14.4,
|
||
textScale: "0.9",
|
||
});
|
||
|
||
await peerPage.close();
|
||
});
|
||
|
||
test("shows agent runtimes in agent settings", async ({ page }) => {
|
||
await page.goto("/");
|
||
|
||
await openSettings(page, "agents");
|
||
|
||
const agentsPage = page.getByTestId("settings-agents");
|
||
await expect(
|
||
agentsPage.getByRole("heading", { name: "Agents", exact: true }),
|
||
).toBeVisible();
|
||
|
||
for (const testId of [
|
||
"agents-preferences-card",
|
||
"settings-harnesses",
|
||
"settings-global-agent-config",
|
||
]) {
|
||
const section = agentsPage
|
||
.getByTestId(testId)
|
||
.locator('[data-slot="settings-section-card"]');
|
||
await expect(section).toBeVisible();
|
||
await expect(section).toHaveCSS("border-radius", "12px");
|
||
await expect(section).toHaveCSS("border-top-width", "1px");
|
||
}
|
||
|
||
await expect(
|
||
agentsPage.getByRole("heading", {
|
||
name: "Agent runtimes",
|
||
exact: true,
|
||
}),
|
||
).toBeVisible();
|
||
await expect(
|
||
agentsPage.getByRole("heading", {
|
||
name: "Agent defaults",
|
||
exact: true,
|
||
}),
|
||
).toBeVisible();
|
||
const runtimeRow = page.getByTestId("doctor-runtime-goose");
|
||
await expect(runtimeRow).toContainText("Goose");
|
||
await expect(runtimeRow).toHaveCSS("border-radius", "0px");
|
||
await expect(runtimeRow).toHaveCSS("border-top-width", "0px");
|
||
|
||
const agentsSecondaryColor = await agentsPage
|
||
.getByTestId("settings-automatic-agent-mentions")
|
||
.locator("[data-settings-subcopy]")
|
||
.evaluate((element) => getComputedStyle(element).color);
|
||
await page.getByTestId("settings-nav-appearance").click();
|
||
const appearanceSecondaryColor = await page
|
||
.getByTestId("link-preview-style-group")
|
||
.locator("[data-settings-subcopy]")
|
||
.evaluate((element) => getComputedStyle(element).color);
|
||
expect(agentsSecondaryColor).toBe(appearanceSecondaryColor);
|
||
});
|
||
|
||
test("settings subtitles share the Appearance secondary color", async ({
|
||
page,
|
||
}) => {
|
||
await page.goto("/");
|
||
await openSettings(page, "appearance");
|
||
|
||
const appearancePanel = page.getByTestId("settings-panel-appearance");
|
||
const secondaryColor = await appearancePanel
|
||
.locator("[data-settings-subcopy]")
|
||
.first()
|
||
.evaluate((element) => getComputedStyle(element).color);
|
||
|
||
for (const section of [
|
||
"profile",
|
||
"appearance",
|
||
"notifications",
|
||
"voice",
|
||
"shortcuts",
|
||
"custom-emoji",
|
||
"local-archive",
|
||
"channel-templates",
|
||
"hosted-communities",
|
||
"agents",
|
||
"compute",
|
||
"experimental",
|
||
"mobile",
|
||
"updates",
|
||
]) {
|
||
await page.getByTestId(`settings-nav-${section}`).click();
|
||
const subtitles = page
|
||
.getByTestId(`settings-panel-${section}`)
|
||
.locator("[data-settings-subcopy]");
|
||
await expect(subtitles.first()).toBeVisible();
|
||
const colors = await subtitles.evaluateAll((elements) =>
|
||
elements.map((element) => getComputedStyle(element).color),
|
||
);
|
||
expect(new Set(colors), `${section} subtitle colors`).toEqual(
|
||
new Set([secondaryColor]),
|
||
);
|
||
}
|
||
});
|