mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
* feat(server): run internal plugins in-process through the plugin runtime
* fix(server): harden internal plugin startup and boundary checks
* feat(usage): add plugin usage sources and Codex discovery
* fix(usage): share SDK helpers and bound source cache
* feat(usage): migrate usage fetchers to plugin sources
Serve the legacy usage RPC from discovered source reports so older apps retain the same host usage list while new clients use the source registry. Keep credential reads and normalization inside each source.
* refactor(usage): shape sources as fetch functions
Keep the plugin usage API limited to stable report and presentation helpers. Source-local parsers and timeout handling preserve credential and response parity while the cases move alongside each source.
* feat(usage): resolve reports from active session accounts
Resolve source inputs at fetch time so model and credential changes select the account each session spends. Keep the legacy discovered-account RPC for existing clients. Sync OpenCode model events outside foreground turns so usage follows model switches.
* fix usage source discovery and session report parity
* test adapter usage references through daemon RPC
* name usage reference contract across server
* refactor(usage): own usage RPCs and validate session auth
* feat(omp): resolve session usage from active OAuth credential
* fix(omp): match usage source input and valid credential fallback
* test(omp): use upstream credential database schema
* test(omp): verify live session usage references
* fix(usage): declare YAML parser and consolidate credential validation
* feat(app): show usage sources in the composer and a Usage screen
Replace the provider-matched usage in the context ring tooltip with a
composer pill driven by agent.get_usage_report, add a sidebar Usage
screen over usage.list_reports grouped by host, and move the host
settings Usage page to usage.list_reports. Hosts without
features.usageSources show an update message. Source icons come from
the report's catalog SVG. The app no longer calls provider.usage.list.
* fix(usage): declare the Paseo requirement on internal usage-source plugins
A manifest without requirements.paseo targets Paseo before 0.8, so the daemon
refused to start eight of the nine usage sources.
* fix(app): keep host connection statuses live under the React Compiler
The hook read a store version only as a memo trigger (void version); the
compiler dropped it, so pages that mounted before a host came online kept
showing it as connecting.
* fix(app): place the context ring slot before the usage pill
The ring reserves its slot before it has data. With the pill first, the empty
slot sat between the pill and the mic; now it falls in the flexible space.
* refactor(app): extract the settings page layout into PageLayout
Desktop puts the title in the page above a centered column; compact moves it
into a back header. Settings renders the same pixels through it.
* feat(app): lay out the Usage screen like a settings page
The title moves from the header bar into the page on desktop; compact gets the
settings back header, which returns home when the screen was opened by URL.
* refactor(app): drop the unused actions slot from PageLayout
* test(plugin): classify the usage source SDK entry
* fix(usage): preserve platform test contracts
* fix(settings): preserve page title test contract
* feat(usage): identify and cache reports by account ID
Keep report identity stable across input routes and token rotation, and make selective refresh operate on known account reports.
* feat(app): read usage reports through report IDs
Share report data between agent pills and host views while preserving the existing popover interaction for the next UI phase.
* fix(usage): keep account identity aligned with credentials
Read labels from local token claims and use one credential lookup for usage fetches and account identification. Bound cached profile identities across token rotation.
* fix(usage): isolate failure IDs and prune expired reports
Failure reports live outside the account-key namespace, and cache writes discard entries past the five-minute TTL.
* refactor(app): share the compact time-ago prose formatter
Move the tab tooltip's "3m" -> "3m ago" wording into utils/time so the
usage card can reuse it.
* feat(usage): show when each report was fetched
The usage card reads fetchedAt as "Updated 3m ago": a tooltip on the
card header where hover exists (web, desktop), printed in the card
footer on native and compact. Shared by the pill popover and the Usage
screen.
* feat(usage): open the pill popover from cache and refresh on demand
Opening the popover no longer forces a vendor fetch; it shows the cached
report. A Refresh control force-refreshes only this report
(usage.list_reports with its single ID and forceRefresh), shows
Refreshing... while it runs, keeps the previous report visible, and says
"Unable to refresh usage" if the request fails.
* feat(app): let ToolbarButton show a tooltip other than its label
An icon action can now say a status in its tooltip ("Updated 3m ago")
while keeping its label for accessibility.
* feat(usage): refresh a report from its card header
Refresh is an icon button at the right edge of every usage card header,
in the pill popover and on the Usage screen. It force-refreshes only that
report, writes the result to the report's cache and its host list, shows
a spinner and disables itself while pending, and prints "Unable to
refresh usage" in the card on failure. Its tooltip carries the
freshness; without hover the freshness stays in the card footer. The
status indicator sits just left of the button, and the popover's
separate refresh row is gone.
* fix(server): include internal plugin assets in built output
* feat(server): built-in plugins in /plugins (#5544)
* feat(server): load compiled built-in Codex plugin
* test(server): guard built-in plugin loading and installs
* test(server): verify built-in plugins from dist in CI
* refactor(server): migrate remaining usage sources to built-in plugins
* test(server): guard all packaged usage source registrations
* docs: describe usage validation across plugin hosts
* test(ci): include built-in plugins in routing contract
* test(server): import moved Codex usage source
* fix(server): retain ACP updates received with session creation
456 lines
18 KiB
TypeScript
456 lines
18 KiB
TypeScript
import { expect, type Page } from "@playwright/test";
|
|
import { buildCreateAgentPreferences, buildSeededHost, TEST_HOST_LABEL } from "./daemon-registry";
|
|
import { getServerId } from "./server-id";
|
|
import { expectAppRoute } from "./route-assertions";
|
|
import {
|
|
buildSettingsHostSectionRoute,
|
|
buildSettingsRoute,
|
|
buildSettingsSectionRoute,
|
|
} from "@/utils/host-routes";
|
|
|
|
const DISABLE_DEFAULT_SEED_ONCE_KEY = "@paseo:e2e-disable-default-seed-once";
|
|
const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce";
|
|
const REGISTRY_KEY = "@paseo:daemon-registry";
|
|
|
|
interface SavedSettingsHostInput {
|
|
serverId: string;
|
|
label: string;
|
|
endpoint: string;
|
|
}
|
|
|
|
const SECTION_LABELS = {
|
|
general: "General",
|
|
chat: "Chat",
|
|
appearance: "Appearance",
|
|
sidebar: "Sidebar",
|
|
editor: "Editor",
|
|
terminal: "Terminal",
|
|
browser: "Browser",
|
|
shortcuts: "Shortcuts",
|
|
integrations: "Integrations",
|
|
permissions: "Permissions",
|
|
diagnostics: "Diagnostics",
|
|
about: "About",
|
|
} as const;
|
|
|
|
export type SettingsSection = keyof typeof SECTION_LABELS;
|
|
|
|
type HostSection =
|
|
| "projects"
|
|
| "connections"
|
|
| "pair-device"
|
|
| "agents"
|
|
| "metadata"
|
|
| "workspaces"
|
|
| "providers"
|
|
| "usage"
|
|
| "terminals"
|
|
| "plugins"
|
|
| "host";
|
|
|
|
export async function openSettingsSection(page: Page, section: SettingsSection): Promise<void> {
|
|
const sidebar = page.getByTestId("settings-sidebar");
|
|
await expect(sidebar).toBeVisible();
|
|
|
|
await sidebar.getByRole("button", { name: SECTION_LABELS[section], exact: true }).click();
|
|
await expectAppRoute(page, buildSettingsSectionRoute(section));
|
|
}
|
|
|
|
export async function openSettingsHost(page: Page, serverId: string): Promise<void> {
|
|
// Host sections are now flat top-level rows under the Host group. Navigate by
|
|
// clicking the Connections section row; the picker only matters when >1 host.
|
|
await page.getByTestId("settings-host-section-connections").click();
|
|
await expectHostSettingsUrl(page, serverId);
|
|
await expect(page.getByTestId("host-page-connections-card")).toBeVisible();
|
|
}
|
|
|
|
export async function openSettingsHostSection(
|
|
page: Page,
|
|
serverId: string,
|
|
section: HostSection,
|
|
): Promise<void> {
|
|
await page.getByTestId(`settings-host-section-${section}`).click();
|
|
await expectAppRoute(page, buildSettingsHostSectionRoute(serverId, section));
|
|
}
|
|
|
|
export async function expectSettingsHeader(page: Page, title: string): Promise<void> {
|
|
await expect(page.getByTestId("page-title")).toHaveText(title);
|
|
}
|
|
|
|
export async function openAddHostFlow(page: Page): Promise<void> {
|
|
// "Add host" is now an item inside the host picker (a Combobox); open the
|
|
// picker first, then pick it. The picker renders whenever a host exists.
|
|
await page.getByTestId("settings-host-picker").click();
|
|
await page.getByTestId("settings-add-host").click();
|
|
await expect(page.getByText("Add connection", { exact: true })).toBeVisible();
|
|
}
|
|
|
|
export async function selectHostConnectionType(
|
|
page: Page,
|
|
type: "direct" | "relay",
|
|
): Promise<void> {
|
|
const label = type === "direct" ? "Direct connection" : "Paste pairing link";
|
|
await page.getByRole("button", { name: label }).click();
|
|
}
|
|
|
|
export async function addDirectHostFromSettings(
|
|
page: Page,
|
|
input: { host: string; port: number; password?: string },
|
|
): Promise<void> {
|
|
await openAddHostFlow(page);
|
|
await selectHostConnectionType(page, "direct");
|
|
await page.getByTestId("direct-host-input").fill(input.host);
|
|
await page.getByTestId("direct-port-input").fill(String(input.port));
|
|
if (input.password) {
|
|
await page.getByTestId("direct-password-input").fill(input.password);
|
|
}
|
|
await page.getByTestId("direct-host-submit").click();
|
|
await expect(page.getByTestId("add-host-modal")).toHaveCount(0, { timeout: 30_000 });
|
|
}
|
|
|
|
export async function toggleHostAdvanced(page: Page): Promise<void> {
|
|
await page.getByTestId("direct-host-advanced-toggle").click();
|
|
}
|
|
|
|
export async function openCompactSettings(page: Page, expectedStartRoute: string): Promise<void> {
|
|
await expectAppRoute(page, expectedStartRoute, { timeout: 15_000 });
|
|
await page.getByRole("button", { name: "Open menu", exact: true }).first().click();
|
|
const settingsButton = page.locator('[data-testid="sidebar-settings"]:visible').first();
|
|
await expect(settingsButton).toBeVisible();
|
|
await settingsButton.click();
|
|
await expectAppRoute(page, buildSettingsRoute());
|
|
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
|
}
|
|
|
|
export async function seedSavedSettingsHosts(
|
|
page: Page,
|
|
hosts: SavedSettingsHostInput[],
|
|
): Promise<void> {
|
|
await page.goto("/");
|
|
const nowIso = new Date().toISOString();
|
|
const registry = hosts.map((host) =>
|
|
buildSeededHost({
|
|
serverId: host.serverId,
|
|
label: host.label,
|
|
endpoint: host.endpoint,
|
|
nowIso,
|
|
}),
|
|
);
|
|
const firstHost = registry[0];
|
|
if (!firstHost) {
|
|
throw new Error("Expected at least one settings host fixture.");
|
|
}
|
|
const preferences = buildCreateAgentPreferences();
|
|
|
|
await page.evaluate(
|
|
({ keys, storedRegistry, storedPreferences }) => {
|
|
const nonce = localStorage.getItem(keys.seedNonce);
|
|
if (!nonce) {
|
|
throw new Error("Expected e2e seed nonce before overriding settings host registry.");
|
|
}
|
|
|
|
localStorage.setItem(keys.registry, JSON.stringify(storedRegistry));
|
|
localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(storedPreferences));
|
|
localStorage.setItem(keys.disableDefaultSeedOnce, nonce);
|
|
},
|
|
{
|
|
keys: {
|
|
disableDefaultSeedOnce: DISABLE_DEFAULT_SEED_ONCE_KEY,
|
|
registry: REGISTRY_KEY,
|
|
seedNonce: SEED_NONCE_KEY,
|
|
},
|
|
storedRegistry: registry,
|
|
storedPreferences: preferences,
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function selectSettingsHost(page: Page, serverId: string): Promise<void> {
|
|
await page.locator('[data-testid="settings-host-picker"]:visible').click();
|
|
await page.locator(`[data-testid="settings-host-picker-item-${serverId}"]:visible`).click();
|
|
}
|
|
|
|
export async function expectSettingsHostPickerLabel(page: Page, label: string): Promise<void> {
|
|
await expect(
|
|
page.getByTestId("settings-host-picker").getByText(label, { exact: true }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
export async function expectCompactSettingsList(page: Page): Promise<void> {
|
|
await expectAppRoute(page, buildSettingsRoute());
|
|
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
|
await expect(page.getByText("Theme", { exact: true })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Play test" })).toHaveCount(0);
|
|
await expect(page.getByTestId("host-page-connections-card")).toHaveCount(0);
|
|
}
|
|
|
|
export async function expectSettingsSidebarVisible(page: Page): Promise<void> {
|
|
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
|
}
|
|
|
|
export async function expectSettingsSidebarHidden(page: Page): Promise<void> {
|
|
await expect(page.locator('[data-testid="settings-sidebar"]:visible')).toHaveCount(0);
|
|
}
|
|
|
|
export async function expectSettingsSidebarSections(
|
|
page: Page,
|
|
sections: SettingsSection[],
|
|
): Promise<void> {
|
|
const sidebar = page.getByTestId("settings-sidebar");
|
|
for (const section of sections) {
|
|
await expect(
|
|
sidebar.getByRole("button", { name: SECTION_LABELS[section], exact: true }),
|
|
).toBeVisible();
|
|
}
|
|
}
|
|
|
|
export async function goBackInSettings(page: Page): Promise<void> {
|
|
await page.getByRole("button", { name: "Back", exact: true }).click();
|
|
}
|
|
|
|
export async function closeCompactSettings(page: Page): Promise<void> {
|
|
await goBackInSettings(page);
|
|
await expect(page).not.toHaveURL(/\/settings(\/|$)/);
|
|
}
|
|
|
|
export async function removeCurrentHostFromSettings(page: Page): Promise<void> {
|
|
await page.getByTestId("host-page-remove-host-button").click();
|
|
await expect(page.getByTestId("remove-host-confirm-modal")).toBeVisible();
|
|
await page.getByTestId("remove-host-confirm").click();
|
|
await expectAppRoute(page, buildSettingsRoute());
|
|
}
|
|
|
|
export async function expectSettingsBackButton(page: Page): Promise<void> {
|
|
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
|
}
|
|
|
|
export async function clickSettingsBackToWorkspace(page: Page): Promise<void> {
|
|
await page.getByTestId("settings-back-to-workspace").click();
|
|
}
|
|
|
|
export async function expectHostSettingsUrl(page: Page, serverId: string): Promise<void> {
|
|
await expectAppRoute(page, buildSettingsHostSectionRoute(serverId, "connections"));
|
|
}
|
|
|
|
export async function verifyLegacyHostSettingsRedirect(page: Page): Promise<void> {
|
|
const serverId = getServerId();
|
|
await page.goto(`/h/${encodeURIComponent(serverId)}/settings`);
|
|
await expectHostSettingsUrl(page, serverId);
|
|
}
|
|
|
|
export async function openCompactSettingsHost(page: Page): Promise<void> {
|
|
const serverId = getServerId();
|
|
await openSettingsHost(page, serverId);
|
|
await expectHostSettingsUrl(page, serverId);
|
|
}
|
|
|
|
export async function expectAddHostMethodOptions(page: Page): Promise<void> {
|
|
await expect(page.getByRole("button", { name: "Direct connection" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Paste pairing link" })).toBeVisible();
|
|
}
|
|
|
|
export async function fillDirectHostUri(page: Page, uri: string): Promise<void> {
|
|
await page.getByTestId("direct-host-uri-input").fill(uri);
|
|
}
|
|
|
|
export async function expectDirectHostFormValues(
|
|
page: Page,
|
|
fields: { host: string; port: string; password: string },
|
|
): Promise<void> {
|
|
await expect(page.getByTestId("direct-host-input")).toHaveValue(fields.host);
|
|
await expect(page.getByTestId("direct-port-input")).toHaveValue(fields.port);
|
|
await expect(page.getByTestId("direct-password-input")).toHaveValue(fields.password);
|
|
}
|
|
|
|
export async function expectDirectHostSslEnabled(page: Page): Promise<void> {
|
|
await expect(page.getByTestId("direct-ssl-toggle-checked")).toBeVisible();
|
|
}
|
|
|
|
export async function expectDirectHostUriValue(page: Page, uri: string): Promise<void> {
|
|
await expect(page.getByTestId("direct-host-uri-input")).toHaveValue(uri);
|
|
}
|
|
|
|
export async function expectDirectHostUriHidden(page: Page): Promise<void> {
|
|
await expect(page.getByTestId("direct-host-uri-input")).toHaveCount(0);
|
|
}
|
|
|
|
export async function expectDiagnosticsContent(page: Page): Promise<void> {
|
|
await expect(page.getByRole("button", { name: "Run" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Play test" })).toBeVisible();
|
|
}
|
|
|
|
export async function expectAboutContent(page: Page): Promise<void> {
|
|
await expect(page.getByText("App version", { exact: true }).first()).toBeVisible();
|
|
}
|
|
|
|
export async function expectGeneralContent(page: Page): Promise<void> {
|
|
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
|
|
}
|
|
|
|
export async function expectAppearanceContent(page: Page): Promise<void> {
|
|
await expect(page.getByText("Highlight theme", { exact: true }).first()).toBeVisible();
|
|
}
|
|
|
|
export async function expectHostLabelDisplayed(page: Page): Promise<void> {
|
|
await expect(page.getByTestId("host-page-label-edit-button")).toBeVisible();
|
|
await expect(page.getByTestId("host-page-rename-modal-input")).toHaveCount(0);
|
|
}
|
|
|
|
export async function clickEditHostLabel(page: Page): Promise<void> {
|
|
await page.getByTestId("host-page-label-edit-button").click();
|
|
}
|
|
|
|
export async function expectHostLabelEditMode(page: Page, expectedLabel: string): Promise<void> {
|
|
await expect(page.getByTestId("host-page-rename-modal-input")).toBeVisible();
|
|
await expect(page.getByTestId("host-page-rename-modal-input")).toHaveValue(expectedLabel);
|
|
await expect(page.getByTestId("host-page-rename-modal-submit")).toBeVisible();
|
|
}
|
|
|
|
export async function expectHostConnectionsCard(page: Page, port: string): Promise<void> {
|
|
const card = page.getByTestId("host-page-connections-card");
|
|
await expect(card).toBeVisible();
|
|
// "Connections" appears three times on this page: the sidebar section row, the
|
|
// detail header title, and the SettingsSection heading above the card. Match
|
|
// the first to keep the heading assertion without tripping Playwright strict
|
|
// mode.
|
|
await expect(page.getByText("Connections", { exact: true }).first()).toBeVisible();
|
|
await expect(
|
|
card.getByText(new RegExp(`TCP \\((localhost|127\\.0\\.0\\.1):${port}\\)`)),
|
|
).toBeVisible();
|
|
}
|
|
|
|
export async function expectHostInjectMcpCard(page: Page): Promise<void> {
|
|
const card = page.getByTestId("host-page-inject-mcp-card");
|
|
await expect(card).toBeVisible();
|
|
await expect(card.getByRole("switch", { name: "Inject Paseo tools" })).toBeVisible();
|
|
}
|
|
|
|
export async function openHostSection(
|
|
page: Page,
|
|
serverId: string,
|
|
section: HostSection,
|
|
): Promise<void> {
|
|
await openSettingsHostSection(page, serverId, section);
|
|
}
|
|
|
|
export async function expectHostActionCards(page: Page, serverId: string): Promise<void> {
|
|
// Restart + remove cards live on the Host section; providers moved to its
|
|
// own Providers section (asserted via expectHostProvidersCard).
|
|
await openSettingsHostSection(page, serverId, "host");
|
|
await expect(page.getByTestId("host-page-restart-card")).toBeVisible();
|
|
await expect(page.getByTestId("host-page-restart-button")).toBeVisible();
|
|
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
|
|
await expect(page.getByTestId("host-page-remove-host-button")).toBeVisible();
|
|
}
|
|
|
|
export async function expectHostProvidersCard(page: Page, serverId: string): Promise<void> {
|
|
await openSettingsHostSection(page, serverId, "providers");
|
|
await expect(page.getByTestId("host-page-providers-card")).toBeVisible();
|
|
}
|
|
|
|
export async function serveJson(page: Page, url: string, body: unknown): Promise<void> {
|
|
await page.route(url, async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function openAddProviderArea(page: Page): Promise<void> {
|
|
await page.getByTestId("host-page-add-provider-card").scrollIntoViewIfNeeded();
|
|
await expect(page.getByRole("textbox", { name: "Search providers" })).toBeVisible();
|
|
}
|
|
|
|
export async function findAcpCatalogProvider(page: Page, providerName: string): Promise<void> {
|
|
await page.getByRole("textbox", { name: "Search providers" }).fill(providerName);
|
|
await expect(page.getByText(providerName, { exact: true })).toBeVisible();
|
|
}
|
|
|
|
export async function installAcpCatalogProvider(page: Page, providerName: string): Promise<void> {
|
|
await findAcpCatalogProvider(page, providerName);
|
|
await page.getByRole("button", { name: "Add", exact: true }).click();
|
|
}
|
|
|
|
export async function expectProviderInstalledInSettings(
|
|
page: Page,
|
|
providerName: string,
|
|
): Promise<void> {
|
|
await expect(
|
|
page.getByRole("button", { name: `${providerName} provider details`, exact: true }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
export async function expectHostNoDaemonLifecycleRow(page: Page): Promise<void> {
|
|
await expect(page.getByTestId("host-page-daemon-lifecycle-card")).toHaveCount(0);
|
|
}
|
|
|
|
export async function expectRetiredSidebarSectionsAbsent(page: Page): Promise<void> {
|
|
const sidebar = page.getByTestId("settings-sidebar");
|
|
await expect(sidebar).toBeVisible();
|
|
|
|
// App group rows remain top-level.
|
|
await expect(sidebar.getByRole("button", { name: "General", exact: true })).toBeVisible();
|
|
await expect(sidebar.getByRole("button", { name: "Diagnostics", exact: true })).toBeVisible();
|
|
await expect(sidebar.getByRole("button", { name: "About", exact: true })).toBeVisible();
|
|
await expect(sidebar.getByRole("button", { name: "Daemon", exact: true })).toHaveCount(0);
|
|
|
|
// Host group rows are now flat top-level sections (no drill-in).
|
|
await expect(sidebar.getByTestId("settings-host-section-connections")).toBeVisible();
|
|
await expect(sidebar.getByTestId("settings-host-section-projects")).toBeVisible();
|
|
await expect(sidebar.getByTestId("settings-host-section-agents")).toBeVisible();
|
|
await expect(sidebar.getByTestId("settings-host-section-workspaces")).toBeVisible();
|
|
await expect(sidebar.getByTestId("settings-host-section-providers")).toBeVisible();
|
|
await expect(sidebar.getByTestId("settings-host-section-usage")).toBeVisible();
|
|
await expect(sidebar.getByTestId("settings-host-section-host")).toBeVisible();
|
|
|
|
// The old per-host entry rows are replaced by the host picker.
|
|
await expect(sidebar.locator('[data-testid^="settings-host-entry-"]')).toHaveCount(0);
|
|
}
|
|
|
|
export async function expectHostPageVisible(page: Page, _serverId: string): Promise<void> {
|
|
await expect(page.getByTestId("host-page-connections-card")).toBeVisible();
|
|
}
|
|
|
|
export async function expectLocalHostEntryFirst(page: Page, _serverId: string): Promise<void> {
|
|
const sidebar = page.getByTestId("settings-sidebar");
|
|
await expect(sidebar).toBeVisible({ timeout: 15_000 });
|
|
|
|
// Single-host fixture: the picker is a non-interactive chip (no dropdown to
|
|
// open) that surfaces the local host by its label. The per-row connection
|
|
// endpoint only appears on dropdown rows in the multi-host case, which this
|
|
// fixture does not exercise.
|
|
const picker = sidebar.getByTestId("settings-host-picker");
|
|
await expect(picker).toBeVisible();
|
|
await expect(picker.getByText(TEST_HOST_LABEL, { exact: true })).toBeVisible();
|
|
}
|
|
|
|
export async function expectHostRejectedWithReAddGuidance(
|
|
page: Page,
|
|
reason: string,
|
|
): Promise<void> {
|
|
const error = page.getByTestId("host-connection-error");
|
|
await expect(error).toContainText(reason);
|
|
await expect(error).toContainText(
|
|
"Remove this host and add it again with the password this daemon asks for.",
|
|
);
|
|
}
|
|
|
|
export async function expectNoHostPasswordControls(page: Page): Promise<void> {
|
|
await expect(page.getByText("Password", { exact: true })).toHaveCount(0);
|
|
await expect(page.locator('input[type="password"]')).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: /password/i })).toHaveCount(0);
|
|
}
|
|
|
|
export async function removeHostFromHostPage(page: Page, serverId: string): Promise<void> {
|
|
await page.getByTestId("host-page-remove-host-button").click();
|
|
await page.getByTestId("remove-host-confirm").click();
|
|
await expect(page).not.toHaveURL(new RegExp(`/settings/hosts/${serverId}`));
|
|
}
|
|
|
|
export async function expectHostOnlineWithoutError(page: Page): Promise<void> {
|
|
await expect(page.getByTestId("host-page-identity")).toContainText("Online");
|
|
await expect(page.getByTestId("host-connection-error")).toHaveCount(0);
|
|
}
|