Clarify API platform onboarding flow

This commit is contained in:
rcourtman 2026-05-08 02:24:08 +01:00
parent d0940db33b
commit 73d549f248
14 changed files with 97 additions and 205 deletions

View file

@ -1452,7 +1452,7 @@ action, and the page does not fork back into a second monitored-systems ledger
below.
Adding infrastructure therefore happens in two governed steps. The
`?add=pick` modal owns grouped source-type selection and may offer
`Detect from address` as a secondary utility. The `?add=detect` modal owns
`Detect API platform` as a secondary utility. The `?add=detect` modal owns
probe-driven handoff into `ConnectionEditor.tsx`, while typed add routes jump
straight into the matching credential or install body. The shared Settings
sidebar still owns only the top-level `Infrastructure` destination; movement

View file

@ -325,6 +325,10 @@ prompt explain the same operator-facing priority.
session presentation policy says the operator cannot manage setup, `/settings`
and sidebar navigation must land on the canonical reporting/control surface
instead of setup-oriented install routes.
Route normalization must wait until that presentation policy has resolved
before stripping infrastructure onboarding queries such as `?add=pick`,
`?add=agent`, or `?add=detect`, so first-session and explicit add-flow
handoffs do not lose their modal target during session bootstrap.
That same settings-shell boundary also owns explicit organization-route
stability. Deep links such as `/settings/organization`,
`/settings/organization/access`, and adjacent organization shells must keep

View file

@ -30,7 +30,7 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
<form class="space-y-4" onSubmit={handleSubmit}>
<div class={formField}>
<label class={formLabel} for="connection-address">
Address
API endpoint
</label>
<input
id="connection-address"
@ -44,8 +44,8 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
disabled={props.state.phase() === 'probing'}
/>
<p class={formHelpText}>
Paste a hostname, IP, or URL to identify a supported platform. Pulse validates the match
and asks for credentials next.
Paste a hostname, IP, or URL for a supported API-backed platform. Pulse validates the
match and asks for credentials next.
</p>
</div>
@ -55,7 +55,7 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-60"
disabled={props.state.phase() === 'probing' || props.state.address().trim().length === 0}
>
{props.state.phase() === 'probing' ? 'Probing…' : 'Probe address'}
{props.state.phase() === 'probing' ? 'Probing…' : 'Probe API endpoint'}
</button>
</div>

View file

@ -101,7 +101,7 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
<section class="space-y-4 rounded-xl border border-border bg-surface p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="space-y-1">
<div class="text-sm font-semibold text-base-content">Address probe</div>
<div class="text-sm font-semibold text-base-content">API platform probe</div>
<p class="text-xs text-muted">
Pulse can auto-detect these platforms from an address when their management API
is reachable.
@ -162,7 +162,7 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
onClick={reopenProbe}
class="inline-flex items-center rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
>
← Back to detect
← Back to API probe
</button>
</Show>
</div>

View file

@ -37,7 +37,7 @@ describe('ConnectionEditor', () => {
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
fireEvent.input(input, { target: { value: 'pve.lab' } });
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
await waitFor(() => expect(mockedProbe).toHaveBeenCalledWith('pve.lab'));
@ -61,7 +61,7 @@ describe('ConnectionEditor', () => {
/>
));
expect(screen.getByText('Address probe')).toBeInTheDocument();
expect(screen.getByText('API platform probe')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Back to source types/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument();
@ -83,7 +83,7 @@ describe('ConnectionEditor', () => {
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
fireEvent.input(input, { target: { value: '192.168.1.50' } });
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
await waitFor(() =>
expect(screen.getByText(/No supported API-backed platform detected/i)).toBeInTheDocument(),
@ -107,7 +107,7 @@ describe('ConnectionEditor', () => {
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
fireEvent.input(input, { target: { value: 'baremetal.lan' } });
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
await waitFor(() => expect(mockedProbe).toHaveBeenCalled());
const agentButton = await screen.findByRole('button', {
@ -146,17 +146,17 @@ describe('ConnectionEditor', () => {
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
fireEvent.input(input, { target: { value: 'truenas.lab' } });
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
fireEvent.click((await screen.findByText('https://truenas.lab')).closest('button')!);
await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:truenas'));
fireEvent.click(screen.getByRole('button', { name: /Back to detect/i }));
fireEvent.click(screen.getByRole('button', { name: /Back to API probe/i }));
const resetInput = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
expect(resetInput.value).toBe('');
expect(screen.queryByTestId('slot')).toBeNull();
expect(screen.getByText('Address probe')).toBeInTheDocument();
expect(screen.getByText('API platform probe')).toBeInTheDocument();
});
it('opens direct type routes without the detect setup', async () => {
@ -171,7 +171,7 @@ describe('ConnectionEditor', () => {
));
await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:truenas'));
expect(screen.queryByRole('button', { name: /probe address/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /probe api endpoint/i })).not.toBeInTheDocument();
});
it('skips probe setup when an initialType is supplied in edit mode', () => {

View file

@ -74,7 +74,7 @@ export function createConnectionEditorState(): ConnectionEditorState {
const runProbe = async () => {
const value = address().trim();
if (!isSubmittableAddress(value)) {
setErrorMessage('Enter an address to probe.');
setErrorMessage('Enter an API endpoint to probe.');
setPhase('error');
return 'error';
}

View file

@ -12,7 +12,7 @@ import {
interface InfrastructureSourcePickerProps {
onSelectStep: (step: InfrastructureSourcePickerRouteStep) => void;
onDetectFromAddress?: () => void;
onDetectApiPlatform?: () => void;
}
const detectButtonClass =
@ -82,10 +82,10 @@ export const InfrastructureSourcePicker: Component<InfrastructureSourcePickerPro
placeholder="Search platforms, hosts, services..."
/>
</label>
<Show when={props.onDetectFromAddress}>
<Show when={props.onDetectApiPlatform}>
<button
type="button"
onClick={props.onDetectFromAddress}
onClick={props.onDetectApiPlatform}
class={`${detectButtonClass} sm:w-auto`}
>
<Search class="mr-2 h-4 w-4" />

View file

@ -960,7 +960,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
<Match when={routeStep() === 'pick'}>
<InfrastructureSourcePicker
onSelectStep={(step) => openAddFlow(step as ManagedAddTypeStep)}
onDetectFromAddress={() => openAddFlow('detect')}
onDetectApiPlatform={() => openAddFlow('detect')}
/>
</Match>

View file

@ -677,7 +677,7 @@ describe('InfrastructureWorkspace', () => {
within(screen.getByRole('dialog')).getByRole('button', { name: /Back to source types/i }),
).toBeInTheDocument();
expect(
within(screen.getByRole('dialog')).getByRole('button', { name: /Probe address/i }),
within(screen.getByRole('dialog')).getByRole('button', { name: /Probe API endpoint/i }),
).toBeInTheDocument();
});

View file

@ -456,7 +456,7 @@ describe('settings architecture guardrails', () => {
expect(infrastructureWorkspaceSource).toContain('flex h-full min-h-0 flex-col');
expect(infrastructureWorkspaceSource).toContain('showSlotHeader={false}');
expect(infrastructureWorkspaceSource).toContain(
"onDetectFromAddress={() => openAddFlow('detect')}",
"onDetectApiPlatform={() => openAddFlow('detect')}",
);
expect(infrastructureWorkspaceSource).toContain("onBackToCatalog={() => openAddFlow('pick')}");
expect(infrastructureWorkspaceSource).not.toContain('trackInitialCatalogSelection');
@ -532,10 +532,10 @@ describe('settings architecture guardrails', () => {
"from '@/utils/infrastructureOnboardingPresentation';",
);
expect(connectionEditorSource).toContain('<AddressProbeStep');
expect(connectionEditorSource).toContain('Address probe');
expect(connectionEditorSource).toContain('API platform probe');
expect(connectionEditorSource).toContain('flex h-full min-h-0 flex-col');
expect(connectionEditorSource).toContain('Back to source types');
expect(connectionEditorSource).toContain('Back to detect');
expect(connectionEditorSource).toContain('Back to API probe');
expect(connectionEditorSource).toContain('Install Pulse Agent');
expect(connectionEditorSource).not.toContain('buildConnectionEditorCatalogEntries');
expect(connectionEditorSource).not.toContain('selectedFamilyId');
@ -545,7 +545,7 @@ describe('settings architecture guardrails', () => {
expect(connectionEditorSource).not.toContain('Back to platforms');
expect(connectionEditorSource).not.toContain('NodeModal');
expect(addressProbeStepSource).toContain('Probe address');
expect(addressProbeStepSource).toContain('Probe API endpoint');
expect(addressProbeStepSource).toContain('install Pulse Agent instead');
expect(addressProbeStepSource).toContain('Choose a source type instead');
expect(addressProbeStepSource).toContain('getInfrastructureAgentHostProfileSupportText');

View file

@ -3,10 +3,12 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-li
import { useSettingsNavigation } from '../useSettingsNavigation';
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
const sessionPresentationPolicyResolvedMock = vi.hoisted(() => vi.fn(() => true));
const navigateSpy = vi.hoisted(() => vi.fn());
vi.mock('@/stores/sessionPresentationPolicy', () => ({
presentationPolicyIsReadOnly: () => presentationPolicyIsReadOnlyMock(),
sessionPresentationPolicyResolved: () => sessionPresentationPolicyResolvedMock(),
}));
function renderHarness(pathname = '/settings', search = '', hash = '') {
@ -36,7 +38,9 @@ describe('useSettingsNavigation', () => {
cleanup();
navigateSpy.mockReset();
presentationPolicyIsReadOnlyMock.mockReset();
sessionPresentationPolicyResolvedMock.mockReset();
presentationPolicyIsReadOnlyMock.mockReturnValue(false);
sessionPresentationPolicyResolvedMock.mockReturnValue(true);
});
it('lands /settings on reporting inventory when the session is read-only', async () => {
@ -62,6 +66,20 @@ describe('useSettingsNavigation', () => {
});
});
it('does not strip infrastructure onboarding queries before presentation policy resolves', async () => {
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
sessionPresentationPolicyResolvedMock.mockReturnValue(false);
renderHarness('/settings/infrastructure', '?add=pick');
await waitFor(() => {
expect(navigateSpy).not.toHaveBeenCalledWith('/settings/infrastructure', {
replace: true,
scroll: false,
});
});
});
it('syncs the selected proxmox agent from canonical deep links on initial load', async () => {
renderHarness('/settings/infrastructure/platforms/proxmox/pbs');

View file

@ -1,5 +1,8 @@
import { createEffect, createSignal, on } from 'solid-js';
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
import {
presentationPolicyIsReadOnly,
sessionPresentationPolicyResolved,
} from '@/stores/sessionPresentationPolicy';
import { resolveCanonicalSelfHostedBillingHref } from '@/utils/pricingHandoff';
import {
buildInfrastructureOnboardingPath,
@ -91,6 +94,7 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
}
if (
sessionPresentationPolicyResolved() &&
presentationPolicyIsReadOnly() &&
(path.startsWith('/settings/infrastructure') ||
path === '/settings/workloads' ||

View file

@ -39,14 +39,16 @@ const test = base.extend<{}, WorkerFixtures>({
test.describe("runtime-home onboarding contract", () => {
test.setTimeout(180_000);
test("normalizes the agent install handoff onto the shared infrastructure workspace", async ({
test("opens the agent install handoff on the shared infrastructure workspace", async ({
page,
}) => {
await page.goto("/settings/infrastructure?add=agent", {
waitUntil: "domcontentloaded",
});
await page.waitForURL(/\/settings\/infrastructure$/, { timeout: 15_000 });
await page.waitForURL(/\/settings\/infrastructure\?add=agent$/, {
timeout: 15_000,
});
await expect(
page.getByRole("heading", {
level: 1,
@ -54,24 +56,35 @@ test.describe("runtime-home onboarding contract", () => {
}),
).toBeVisible();
await expect(
page.getByRole("heading", { level: 2, name: "Install on a host" }),
page.getByRole("dialog").getByRole("heading", {
name: "Add Pulse Agent",
exact: true,
}),
).toBeVisible();
await expect(
page.getByRole("dialog").getByRole("heading", {
level: 2,
name: "Install on a host",
}),
).toBeVisible();
await expect(
page.getByRole("button", { name: /Generate token/i }),
).toBeVisible();
await expect(
page.getByRole("button", { name: /Probe address/i }),
page.getByRole("button", { name: /Probe API endpoint/i }),
).toHaveCount(0);
});
test("normalizes the platform-pick handoff onto the shared infrastructure workspace", async ({
test("opens the platform-pick handoff on the shared infrastructure workspace", async ({
page,
}) => {
await page.goto("/settings/infrastructure?add=pick", {
waitUntil: "domcontentloaded",
});
await page.waitForURL(/\/settings\/infrastructure$/, { timeout: 15_000 });
await page.waitForURL(/\/settings\/infrastructure\?add=pick$/, {
timeout: 15_000,
});
await expect(
page.getByRole("heading", {
level: 1,
@ -79,7 +92,9 @@ test.describe("runtime-home onboarding contract", () => {
}),
).toBeVisible();
await expect(
page.getByRole("button", { name: /Probe address/i }),
page.getByRole("dialog").getByRole("button", {
name: /Detect API platform/i,
}),
).toBeVisible();
await expect(
page.getByRole("button", { name: /Generate token/i }),
@ -101,10 +116,10 @@ test.describe("runtime-home onboarding contract", () => {
}),
).toBeVisible();
await expect(
page.getByRole("heading", { level: 3, name: "Monitored systems" }),
page.getByText("Infrastructure systems", { exact: true }),
).toBeVisible();
await expect(
page.getByRole("button", { name: /Probe address/i }),
page.getByRole("button", { name: /Probe API endpoint/i }),
).toHaveCount(0);
await expect(
page.getByRole("heading", { level: 2, name: "Install on a host" }),

View file

@ -10,13 +10,6 @@ type WorkerFixtures = {
authStorageStatePath: string;
};
type UpgradeMetricEventPayload = {
type: string;
surface: string;
capability?: string;
idempotency_key?: string;
};
type OverflowAudit = {
viewportWidth: number;
pageWidth: number;
@ -69,49 +62,10 @@ async function stubConnectionsList(page: Page): Promise<void> {
});
}
async function recordUpgradeMetricEvents(
page: Page,
): Promise<UpgradeMetricEventPayload[]> {
const events: UpgradeMetricEventPayload[] = [];
await page.route("**/api/upgrade-metrics/events", async (route) => {
const payload = route.request().postDataJSON() as UpgradeMetricEventPayload;
events.push(payload);
await route.fulfill({
status: 202,
contentType: "application/json",
body: "{}",
});
});
return events;
}
async function prepareOnboardingPage(page: Page): Promise<void> {
await stubConnectionsList(page);
}
function countUpgradeEvents(
events: readonly UpgradeMetricEventPayload[],
type: string,
capability?: string,
): number {
const matchingKeys = new Set<string>();
for (const event of events) {
if (event.surface !== "settings_infrastructure_add") continue;
if (event.type !== type) continue;
if (capability !== undefined && event.capability !== capability) continue;
matchingKeys.add(
event.idempotency_key ??
`${event.type}:${event.surface}:${event.capability ?? ""}:${matchingKeys.size}`,
);
}
return matchingKeys.size;
}
async function scrollToBottom(page: Page): Promise<void> {
const viewportHeight = await page.evaluate(() => window.innerHeight || 800);
const step = Math.max(240, Math.floor(viewportHeight * 0.75));
@ -188,7 +142,6 @@ test.describe("Infrastructure onboarding", () => {
"Desktop-only infrastructure manager coverage",
);
const metricEvents = await recordUpgradeMetricEvents(page);
await prepareOnboardingPage(page);
await page.route("**/api/discover", async (route) => {
const requestUrl = new URL(route.request().url());
@ -253,7 +206,7 @@ test.describe("Infrastructure onboarding", () => {
page.getByRole("button", { name: /Add TrueNAS SCALE/i }),
).toHaveCount(0);
await expect(
page.getByRole("button", { name: /Detect from address/i }),
page.getByRole("button", { name: /Detect API platform/i }),
).toHaveCount(0);
await expect(
page.getByText("Monitored systems", { exact: true }),
@ -261,12 +214,6 @@ test.describe("Infrastructure onboarding", () => {
await expect(
page.getByText("Connection types", { exact: true }),
).toHaveCount(0);
await expect
.poll(() =>
countUpgradeEvents(metricEvents, "infrastructure_onboarding_opened"),
)
.toBe(0);
});
test("desktop discovery settings open as an infrastructure-owned dialog", async ({
@ -302,7 +249,7 @@ test.describe("Infrastructure onboarding", () => {
await expect(page).toHaveURL(/\/settings\/infrastructure(?:\?.*)?$/);
});
test("desktop picker add opens the matching modal and records type onboarding", async ({
test("desktop picker add opens the matching modal", async ({
page,
}, testInfo) => {
test.skip(
@ -310,7 +257,6 @@ test.describe("Infrastructure onboarding", () => {
"Desktop-only manager add-path instrumentation coverage",
);
const metricEvents = await recordUpgradeMetricEvents(page);
await prepareOnboardingPage(page);
await page.goto("/settings/infrastructure", {
@ -320,7 +266,10 @@ test.describe("Infrastructure onboarding", () => {
timeout: 15_000,
});
await page.getByRole("button", { name: /Add infrastructure/i }).click();
await page
.getByRole("button", { name: "Add infrastructure", exact: true })
.first()
.click();
await page.waitForURL(/\/settings\/infrastructure\?add=pick$/, {
timeout: 15_000,
});
@ -347,39 +296,6 @@ test.describe("Infrastructure onboarding", () => {
exact: true,
}),
).toBeVisible();
await expect
.poll(() =>
countUpgradeEvents(metricEvents, "infrastructure_onboarding_opened"),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_path_selected",
"api",
),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_catalog_selected",
"truenas",
),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_credentials_opened",
"truenas",
),
)
.toBe(1);
});
test("desktop explicit discovery scan surfaces a candidate row and opens a prefilled review dialog", async ({
@ -390,7 +306,6 @@ test.describe("Infrastructure onboarding", () => {
"Desktop-only discovery-candidate review coverage",
);
const metricEvents = await recordUpgradeMetricEvents(page);
await prepareOnboardingPage(page);
await page.route("**/api/discover", async (route) => {
@ -469,42 +384,9 @@ test.describe("Infrastructure onboarding", () => {
await expect(
page.getByPlaceholder("https://proxmox.example.com:8006"),
).toHaveValue("https://discovered-pve.lab:8006");
await expect
.poll(() =>
countUpgradeEvents(metricEvents, "infrastructure_onboarding_opened"),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_path_selected",
"api",
),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_credentials_opened",
"pve",
),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_catalog_selected",
"pve",
),
)
.toBe(0);
});
test("desktop detect utility records no-match agent fallback from the add-infrastructure picker", async ({
test("desktop detect utility offers no-match agent fallback from the add-infrastructure picker", async ({
page,
}, testInfo) => {
test.skip(
@ -512,7 +394,6 @@ test.describe("Infrastructure onboarding", () => {
"Desktop-only onboarding probe instrumentation coverage",
);
const metricEvents = await recordUpgradeMetricEvents(page);
await prepareOnboardingPage(page);
await page.route("**/api/connections/probe", async (route) => {
@ -541,18 +422,21 @@ test.describe("Infrastructure onboarding", () => {
await page.waitForURL(/\/settings\/infrastructure(?:\?.*)?$/, {
timeout: 15_000,
});
await page.getByRole("button", { name: /Add infrastructure/i }).click();
await page
.getByRole("button", { name: "Add infrastructure", exact: true })
.first()
.click();
await page.waitForURL(/\/settings\/infrastructure\?add=pick$/, {
timeout: 15_000,
});
await page.getByRole("button", { name: /Detect from address/i }).click();
await page.getByRole("button", { name: /Detect API platform/i }).click();
await page.waitForURL(/\/settings\/infrastructure\?add=detect$/, {
timeout: 15_000,
});
await page.getByLabel("Address").fill("baremetal.lab");
await page.getByLabel("API endpoint").fill("baremetal.lab");
await page
.getByRole("button", { name: "Probe address", exact: true })
.getByRole("button", { name: "Probe API endpoint", exact: true })
.click();
await expect(
@ -561,51 +445,18 @@ test.describe("Infrastructure onboarding", () => {
{ exact: true },
),
).toBeVisible();
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_path_selected",
"api",
),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_probe_result",
"no-match",
),
)
.toBe(1);
await page
.getByRole("button", { name: "install Pulse Agent instead", exact: true })
.click();
await page.waitForURL(/\/settings\/infrastructure\?add=agent$/, {
timeout: 15_000,
});
await expect(
page.getByRole("button", { name: /Back to detect/i }),
page.getByRole("heading", { level: 2, name: "Install on a host" }),
).toBeVisible();
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_path_selected",
"agent",
),
)
.toBe(1);
await expect
.poll(() =>
countUpgradeEvents(
metricEvents,
"infrastructure_onboarding_credentials_opened",
"agent",
),
)
.toBe(1);
await expect(
page.getByRole("button", { name: /Probe API endpoint/i }),
).toHaveCount(0);
});
test("mobile landing and grouped platform tables stay inside the viewport", async ({