Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3cb76f7
Capture netbird.ai signup source before authentication
mlsmaycon Jul 12, 2026
b62a184
Resolve Agent Network mode from account settings
mlsmaycon Jul 12, 2026
b0b9c38
Enable Agent Network focused view for netbird.ai signups
mlsmaycon Jul 12, 2026
d4217db
Add Agent Network focused view toggle to client settings
mlsmaycon Jul 12, 2026
7dd95af
Swap remaining agent network gating call sites
mlsmaycon Jul 12, 2026
fb409b7
Use signup_source query parameter for signup source capture
mlsmaycon Jul 12, 2026
b280848
Resolve Agent Network mode optimistically for pending netbird.ai signups
mlsmaycon Jul 12, 2026
d16672f
Respect explicit agent_network_only opt-out over deployment config
mlsmaycon Jul 12, 2026
55f2847
Merge remote-tracking branch 'origin/main' into feature/agent-network…
mlsmaycon Jul 12, 2026
cc7b811
Add e2e spec for Agent Network focused view menu and routes
mlsmaycon Jul 12, 2026
8920b2d
Anchor focused-view e2e assertions on always-present nav to avoid bac…
mlsmaycon Jul 12, 2026
38dc2aa
Deflake focused-view e2e: assert route reachability instead of sideba…
mlsmaycon Jul 12, 2026
2aa59ca
Fix onboarding form closing when Agent Network focused view is applied
mlsmaycon Jul 12, 2026
7ad5be9
Show the agent-network signup form on cloud for netbird.ai signups
mlsmaycon Jul 12, 2026
dd35a33
Defer onboarding until Agent Network mode resolves to avoid wrong-for…
mlsmaycon Jul 12, 2026
0fff98c
Decide onboarding form from signup_source so the regular form never f…
mlsmaycon Jul 12, 2026
6c0a29b
Add e2e coverage for onboarding form selection
mlsmaycon Jul 12, 2026
0ef296f
Let loginToApp expect an onboarding overlay instead of dismissing the…
mlsmaycon Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions e2e/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ const credentials: Record<TestUser, { username: string; password: string }> = {
/**
* Navigate to the app, authenticate via Zitadel, and wait for the app to load.
*/
export async function loginToApp(page: Page, user: TestUser = "owner") {
export async function loginToApp(
page: Page,
user: TestUser = "owner",
opts: { expectOnboarding?: boolean } = {},
) {
const { username, password } = credentials[user];

await page.goto("/");
Expand All @@ -39,7 +43,9 @@ export async function loginToApp(page: Page, user: TestUser = "owner") {
const which = await Promise.race([
appReady.waitFor({ timeout: 20_000 }).then(() => "app" as const),
setupModal.waitFor({ timeout: 20_000 }).then(() => "modal" as const),
approvalPending.waitFor({ timeout: 20_000 }).then(() => "approval" as const),
approvalPending
.waitFor({ timeout: 20_000 })
.then(() => "approval" as const),
onboarding.waitFor({ timeout: 20_000 }).then(() => "onboarding" as const),
selectAccount.waitFor({ timeout: 20_000 }).then(() => "select" as const),
loginInput.waitFor({ timeout: 20_000 }).then(() => "login" as const),
Expand All @@ -51,6 +57,9 @@ export async function loginToApp(page: Page, user: TestUser = "owner") {
}

if (which === "modal") {
// Onboarding tests render a full-screen overlay over the setup modal;
// clicking modal-close would be pointer-intercepted, so leave it be.
if (opts.expectOnboarding) return;
await setupModal.getByTestId("modal-close").click();
await expect(setupModal).not.toBeVisible();
return;
Expand Down Expand Up @@ -89,8 +98,12 @@ export async function loginToApp(page: Page, user: TestUser = "owner") {
onboarding.waitFor({ timeout: 15_000 }),
]);

// Dismiss setup modal if present
if (await setupModal.isVisible().catch(() => false)) {
// Dismiss setup modal if present, unless an onboarding overlay is expected
// on top of it (clicking through it would be pointer-intercepted).
if (
!opts.expectOnboarding &&
(await setupModal.isVisible().catch(() => false))
) {
await setupModal.getByTestId("modal-close").click();
await expect(setupModal).not.toBeVisible();
}
Expand Down
220 changes: 220 additions & 0 deletions e2e/tests/agent-network-focused-view.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* Agent Network focused-view spec.
*
* Exercises the account-driven focused view added for netbird.ai signups:
* the `agent_network_only` account setting hides the rest of the dashboard,
* an explicit `false` opts back out, and a pending netbird.ai signup applies
* the focused view optimistically and persists the setting via PUT /accounts.
*
* The test management backend does not know the setting, so GET /accounts is
* rewritten per-test to inject it (same interception approach as
* edition-gating.spec.ts). "only" mode is asserted through the Networks nav
* item, which toggles purely on the focused-view flag and does not depend on
* premium permission modules.
*/
import { test, expect, type Browser, type Page } from "@playwright/test";
import { loginToApp, navigateTo } from "../helpers/auth";

const SIGNUP_SOURCE_KEY = "netbird-signup-source";
const AGENT_NETWORK_SOURCE = "netbird.ai";

type AccountMock = {
// undefined leaves the setting absent (as an un-onboarded account would be).
agentNetworkOnly?: boolean;
signupFormPending?: boolean;
};

// mockAccounts rewrites GET /accounts to inject the focused-view setting and
// onboarding state onto the real account. A PUT to /accounts/{id} is captured
// and flips the injected setting to true, mirroring the server persisting the
// value so the optimistic view does not revert once the write settles.
function mockAccounts(
page: Page,
initial: AccountMock,
captured: { putBody?: any },
) {
let applied = initial.agentNetworkOnly;

page.route("**/api/accounts", async (route) => {
if (route.request().method() !== "GET") return route.continue();
const response = await route.fetch();
let body: any;
try {
body = await response.json();
} catch (e) {
return route.fulfill({ response });
}
if (Array.isArray(body) && body[0]) {
body[0].settings = { ...(body[0].settings ?? {}) };
if (applied === undefined) {
delete body[0].settings.agent_network_only;
} else {
body[0].settings.agent_network_only = applied;
}
body[0].onboarding = {
...(body[0].onboarding ?? {}),
signup_form_pending: !!initial.signupFormPending,
};
}
return route.fulfill({ response, json: body });
});

page.route("**/api/accounts/*", async (route) => {
if (route.request().method() !== "PUT") return route.continue();
try {
captured.putBody = route.request().postDataJSON();
} catch (e) {
captured.putBody = null;
}
applied = true;
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ settings: { agent_network_only: true } }),
});
});
}

async function openWithAccount(
browser: Browser,
account: AccountMock,
opts: { signupSource?: boolean } = {},
): Promise<{
page: Page;
captured: { putBody?: any };
close: () => Promise<void>;
}> {
const context = await browser.newContext({
storageState: "e2e/fixtures/auth/owner.json",
});
if (opts.signupSource) {
await context.addInitScript(
([key, value]) => {
try {
window.localStorage.setItem(key as string, value as string);
} catch (e) {}
},
[SIGNUP_SOURCE_KEY, AGENT_NETWORK_SOURCE],
);
}
const page = await context.newPage();
const captured: { putBody?: any } = {};
mockAccounts(page, account, captured);
await loginToApp(page, "owner");
return { page, captured, close: () => context.close() };
}

function navItem(page: Page, text: string) {
return page
.getByTestId("left-navigation-item")
.getByText(text, { exact: true });
}

// Regular dashboard sections that the focused view hides (gated on
// !agentNetworkOnly in Navigation.tsx).
const FOCUS_HIDDEN_NAV = ["Networks", "Reverse Proxy", "DNS", "Activity"];
// Agent Network view routes that make up the focused surface.
const AGENT_VIEW_PATHS = [
"/agent-network/providers",
"/agent-network/policies",
"/agent-network/usage",
"/agent-network/configuration",
];

test.describe.serial("Agent Network focused view @agent-network", () => {
test("focused menu shows only Agent Network views and hides the regular sections", async ({
browser,
}) => {
const { page, close } = await openWithAccount(browser, {
agentNetworkOnly: true,
});
try {
// Peers always renders for an owner and is not part of the focused
// split, so it confirms the sidebar rendered before asserting absences.
await expect(navItem(page, "Peers")).toBeVisible();

// The Agent Network section is present and the regular sections are gone.
await expect(navItem(page, "Agent Network")).toBeVisible();
for (const label of FOCUS_HIDDEN_NAV) {
await expect(navItem(page, label)).toHaveCount(0);
}
} finally {
await close();
}
});

test("focused view keeps every Agent Network route reachable", async ({
browser,
}) => {
const { page, close } = await openWithAccount(browser, {
agentNetworkOnly: true,
});
try {
// Each Agent Network view renders instead of the route guard redirecting
// away; the Agent Network section stays in the sidebar throughout.
for (const path of AGENT_VIEW_PATHS) {
await navigateTo(page, path);
await expect(page).toHaveURL(new RegExp(path.replace(/\//g, "\\/")));
await expect(navItem(page, "Agent Network")).toBeVisible();
}
} finally {
await close();
}
});

test("explicit opt-out restores the regular menu and removes Agent Network", async ({
browser,
}) => {
const { page, close } = await openWithAccount(browser, {
agentNetworkOnly: false,
});
try {
// The sidebar rendered (Peers is always present for an owner)...
await expect(navItem(page, "Peers")).toBeVisible();
// ...and the Agent Network section is gone: the explicit opt-out wins
// and, without a deployment config flag, the section is not enabled.
await expect(navItem(page, "Agent Network")).toHaveCount(0);
} finally {
await close();
}
});

test("applies the focused view optimistically for a pending netbird.ai signup and persists it", async ({
browser,
}) => {
const { page, captured, close } = await openWithAccount(
browser,
{ agentNetworkOnly: undefined, signupFormPending: true },
{ signupSource: true },
);
try {
// Focused view is applied immediately, before the setting is persisted.
await expect(navItem(page, "Agent Network")).toBeVisible();
await expect(navItem(page, "Networks")).toHaveCount(0);

// The signup source is persisted as the account setting.
await expect
.poll(() => captured.putBody?.settings?.agent_network_only)
.toBe(true);

// The focused view is retained after the write settles.
await expect(navItem(page, "Networks")).toHaveCount(0);
} finally {
await close();
}
});

test("exposes the focused-view toggle in client settings", async ({
browser,
}) => {
const { page, close } = await openWithAccount(browser, {
agentNetworkOnly: true,
});
try {
await navigateTo(page, "/settings?tab=clients");
await expect(page.getByTestId("agent-network-only")).toBeVisible();
} finally {
await close();
}
});
});
Loading
Loading