Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
6 changes: 6 additions & 0 deletions apps/mission-control/e2e/connectors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ test.describe("mission-control connectors registry", () => {
await enableConnectors(page);
await clickAdvancedNav(page, "connectors");

// The Connectors room lands on connector management; Quick Setup lives
// on the Setup surface, so enter it deliberately.
await page
.locator(".mc-connectors-tab-bar")
.getByRole("button", { name: "Setup", exact: true })
.click();
await page
.locator(".mc-connectors-quick-card", { hasText: "Discord" })
.first()
Expand Down
288 changes: 288 additions & 0 deletions apps/mission-control/e2e/p5-connectors-slice.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { expect, test, type Page } from "./testHarness";
import { completeQuickstartLocalOnboarding } from "./onboardingFlow";

/**
* Both Basement rooms on the connectors route ride the optional Connectors
* page: their lamps only exist while it is enabled in Config.
*/
async function setConnectorsPage(page: Page, enabled: boolean): Promise<void> {
await page.locator('[data-tour-id="nav-config"]').click();
const checkbox = page.getByRole("checkbox", { name: "Connectors page" });
if (!(await checkbox.isVisible())) {
await page.getByText("2. Choose what pages show").click();
}
if (enabled) {
await checkbox.check();
} else {
await checkbox.uncheck();
}
await page.keyboard.press("Escape");
}

function activeConnectorsTab(page: Page) {
return page.locator(".mc-connectors-tab-bar button.active");
}

/**
* P5 Basement · Connectors room slice: the Connectors and Setup rooms share
* one route but keep distinct stable identities — Connectors lands on
* connector management (Registry), Setup lands on Setup, internal tab
* clicks never move the lamp, and only the Connectors room offers the
* registry-backed Pin to Office. The pinned shortcut walks back by stable
* id, refuses honestly while the Connectors page is disabled, and clears
* that refusal the moment it is restored. Verified at desktop and 390px
* with console-error and horizontal-overflow assertions.
*/

test("@core @p5-connectors connectors/setup room identity, pin-to-office, and the office shortcut hold at desktop and 390px", async ({
page,
}) => {
test.setTimeout(90_000);
const browserErrors: string[] = [];
page.on("pageerror", (error) => browserErrors.push(error.message));
page.on("console", (message) => {
if (
message.type() === "error" &&
!message.text().startsWith("Failed to load resource:")
) {
browserErrors.push(message.text());
}
});
page.on("response", (response) => {
if (response.status() >= 400) {
browserErrors.push(`${response.status()} ${response.url()}`);
}
});

await completeQuickstartLocalOnboarding(page);

// Neither room lamp exists until the Connectors page is enabled.
await expect(page.locator('button[title="BF · Connectors"]')).toHaveCount(0);
await expect(page.locator('button[title="BF · Setup"]')).toHaveCount(0);
await setConnectorsPage(page, true);
await expect(page.locator('button[title="BF · Connectors"]')).toHaveCount(1);
await expect(page.locator('button[title="BF · Setup"]')).toHaveCount(1);

// The Connectors room lights exactly one lamp and lands on management.
const activeRooms = page.locator(".mc-nav-item-active");
await page.locator('button[title="BF · Connectors"]').click();
await expect(activeRooms).toHaveCount(1);
await expect(activeRooms).toHaveAttribute("title", "BF · Connectors");
await expect(page.getByTestId("connectors-page")).toBeVisible();
await expect(activeConnectorsTab(page)).toHaveText(/^Registry/);
await expect(page.getByTestId("connectors-page")).toContainText(
"Installed registry",
);

// The Setup room is its own stable identity and lands on Setup.
await page.locator('button[title="BF · Setup"]').click();
await expect(activeRooms).toHaveCount(1);
await expect(activeRooms).toHaveAttribute("title", "BF · Setup");
await expect(activeConnectorsTab(page)).toHaveText("Setup");
await expect(page.getByTestId("connectors-page")).toContainText(
"Quick Setup",
);

// Internal tab clicks never move the lamp, and the Setup room offers no
// Connectors pin — one door must not ambiguously open both rooms.
await page.getByRole("button", { name: /^Catalog/ }).click();
await expect(activeConnectorsTab(page)).toHaveText(/^Catalog/);
await expect(activeRooms).toHaveAttribute("title", "BF · Setup");
await expect(
page.getByRole("button", { name: "Pin Connectors to Office" }),
).toHaveCount(0);

// Returning to the Connectors room is a real room change: it relands on
// Registry and offers the pin.
await page.locator('button[title="BF · Connectors"]').click();
await expect(activeRooms).toHaveAttribute("title", "BF · Connectors");
await expect(activeConnectorsTab(page)).toHaveText(/^Registry/);
const pin = page.getByRole("button", { name: "Pin Connectors to Office" });
await expect(pin).toBeVisible();

// A full default canvas refuses the Basement shortcut visibly and leaves
// the exact persisted config untouched.
const fullCanvasConfig = await page.evaluate(() => {
const key = "mc-glass-config-v1";
const config = JSON.parse(localStorage.getItem(key) ?? "{}");
config.layout = [
{ id: "needs-you", size: "l", visible: true },
{ id: "in-motion", size: "m", visible: true },
{ id: "done", size: "m", visible: true },
{ id: "next", size: "s", visible: false },
{ id: "boards", size: "s", visible: true },
{ id: "calendar", size: "s", visible: true },
{ id: "strategy", size: "s", visible: true },
{ id: "staff", size: "s", visible: true },
];
const serialized = JSON.stringify(config);
localStorage.setItem(key, serialized);
window.dispatchEvent(new Event("mc-glass-config-changed"));
return serialized;
});
await pin.click();
await expect(page.locator(".mc-pin-to-office-note.is-error")).toHaveText(
"Pinning this would exceed the six-column, four-row Office canvas.",
);
await page.screenshot({
path: "../../runtime/qa/p5-connectors-slice/connectors-full-canvas-refusal.png",
fullPage: true,
});
expect(
await page.evaluate(() => localStorage.getItem("mc-glass-config-v1")),
).toBe(fullCanvasConfig);
expect(
await page.evaluate(() => {
const config = JSON.parse(
localStorage.getItem("mc-glass-config-v1") ?? "{}",
);
return config.layout?.some(
(placement: { id?: string }) => placement.id === "connectors",
);
}),
).toBe(false);

// Free one medium default block. Pin now succeeds, and pinning again
// reports the truth instead of pretending a second change.
await page.evaluate(() => {
const key = "mc-glass-config-v1";
const config = JSON.parse(localStorage.getItem(key) ?? "{}");
config.layout = config.layout.map(
(placement: { id: string; visible: boolean }) =>
placement.id === "in-motion"
? { ...placement, visible: false }
: placement,
);
localStorage.setItem(key, JSON.stringify(config));
window.dispatchEvent(new Event("mc-glass-config-changed"));
});
await expect(page.locator(".mc-pin-to-office-note")).toHaveCount(0);
await pin.click();
await expect(page.locator(".mc-pin-to-office-note")).toHaveText(
"On the Office canvas.",
);
await pin.click();
await expect(page.locator(".mc-pin-to-office-note")).toHaveText(
"Already on the Office canvas.",
);
await page.screenshot({
path: "../../runtime/qa/p5-connectors-slice/connectors-pinned-desktop.png",
fullPage: true,
});

// The pinned shortcut appears on the Office canvas and names its floor.
await page.locator('[data-tour-id="nav-assistant"]').click();
const shortcut = page.getByTestId("office-block-connectors");
await expect(shortcut).toBeVisible();
await expect(shortcut).toContainText("The Basement");
await shortcut.scrollIntoViewIfNeeded();
await shortcut.screenshot({
path: "../../runtime/qa/p5-connectors-slice/office-shortcut-block.png",
});

// The mounted Office consumes config events live; no route remount may be
// required for a shortcut hide/show to be truthful.
await page.evaluate(() => {
const key = "mc-glass-config-v1";
const config = JSON.parse(localStorage.getItem(key) ?? "{}");
config.layout = config.layout.map(
(placement: { id: string; visible: boolean }) =>
placement.id === "connectors"
? { ...placement, visible: false }
: placement,
);
localStorage.setItem(key, JSON.stringify(config));
window.dispatchEvent(new Event("mc-glass-config-changed"));
});
await expect(page.getByTestId("office-block-connectors")).toHaveCount(0);
await page.evaluate(() => {
const key = "mc-glass-config-v1";
const config = JSON.parse(localStorage.getItem(key) ?? "{}");
config.layout = config.layout.map(
(placement: { id: string; visible: boolean }) =>
placement.id === "connectors"
? { ...placement, visible: true }
: placement,
);
localStorage.setItem(key, JSON.stringify(config));
window.dispatchEvent(new Event("mc-glass-config-changed"));
});
await expect(page.getByTestId("office-block-connectors")).toBeVisible();

// Opening the shortcut lands back in the Connectors room by stable id —
// lamp, landing surface, and pin included.
await shortcut.getByRole("button", { name: "Open Connectors" }).click();
await expect(page.getByTestId("connectors-page")).toBeVisible();
await expect(activeRooms).toHaveCount(1);
await expect(activeRooms).toHaveAttribute("title", "BF · Connectors");
await expect(activeConnectorsTab(page)).toHaveText(/^Registry/);

// The pin is config: it survives a full reload. Browser runs keep the
// gateway token in memory, so onboarding runs again after the reload.
await page.reload();
await completeQuickstartLocalOnboarding(page);
await page.locator('[data-tour-id="nav-assistant"]').click();
await expect(page.getByTestId("office-block-connectors")).toBeVisible();

// Turning the Connectors page off removes both room lamps; the persisted
// door stays visible but refuses honestly instead of dying silently.
await setConnectorsPage(page, false);
await page.locator('[data-tour-id="nav-assistant"]').click();
await expect(page.locator('button[title="BF · Connectors"]')).toHaveCount(0);
await expect(page.locator('button[title="BF · Setup"]')).toHaveCount(0);
await page
.getByTestId("office-block-connectors")
.getByRole("button", { name: "Open Connectors" })
.click();
await expect(
page.getByTestId("office-block-connectors").getByRole("status"),
).toHaveText("Unavailable — turn on in Config");
await page.screenshot({
path: "../../runtime/qa/p5-connectors-slice/connectors-disabled-door.png",
fullPage: true,
});

// Restoring the Connectors page clears the stale refusal without another
// click: an enabled door may not stay labeled unavailable.
await setConnectorsPage(page, true);
await page.locator('[data-tour-id="nav-assistant"]').click();
await expect(page.locator('button[title="BF · Connectors"]')).toHaveCount(1);
await expect(
page.getByTestId("office-block-connectors").getByRole("status"),
).toHaveCount(0);
await expect(
page
.getByTestId("office-block-connectors")
.getByRole("button", { name: "Open Connectors" }),
).toBeVisible();
await page.screenshot({
path: "../../runtime/qa/p5-connectors-slice/connectors-restored-door.png",
fullPage: true,
});

// Narrow width: both rooms keep readable marks, the pin stays reachable,
// and nothing overflows horizontally.
await page.setViewportSize({ width: 390, height: 844 });
await page.locator('[data-tour-id="nav-connectors"]').click();
const mobileConnectorsRoom = page.locator('button[title="BF · Connectors"]');
await expect(mobileConnectorsRoom).toHaveClass(/mc-nav-item-active/);
await expect(mobileConnectorsRoom.locator(".mc-nav-room-mark")).toHaveText(
"C",
);
await expect(
page.locator('button[title="BF · Setup"] .mc-nav-room-mark'),
).toHaveText("S");
await expect(
page.getByRole("button", { name: "Pin Connectors to Office" }),
).toBeVisible();
const overflows = await page.evaluate(
() => document.documentElement.scrollWidth > window.innerWidth,
);
expect(overflows).toBe(false);
await page.screenshot({
path: "../../runtime/qa/p5-connectors-slice/connectors-390.png",
fullPage: true,
});

expect(browserErrors).toEqual([]);
});
1 change: 1 addition & 0 deletions apps/mission-control/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,7 @@ export default function App() {
activeTab={activeTab}
onTabChange={setActiveTab}
onRoomSelect={selectAvailableRoom}
activeRoomId={resolvedActiveRoomId}
onOpenHelpDocs={openHelpDocs}
helpDocsTarget={helpDocsTarget}
onStartGuidedTour={openGuidedTour}
Expand Down
3 changes: 3 additions & 0 deletions apps/mission-control/src/app/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ interface AppContentProps {
onTabChange: (tab: MissionControlTab) => void;
/** Navigate by stable room id (pinned room shortcuts, elevator deep links). */
onRoomSelect: (roomId: string) => void;
/** Resolved stable room id owning the lamp; shared-route surfaces land by it. */
activeRoomId: string | null;
onOpenHelpDocs: (section?: string) => void;
helpDocsTarget: { section?: string; seq: number };
onStartGuidedTour: () => void;
Expand Down Expand Up @@ -944,6 +946,7 @@ export function AppContent(props: AppContentProps) {
<ConnectorsPage
controller={props.connectorsController}
onOpenSimpleIntegrationWizard={props.onOpenSimpleIntegrationWizard}
activeRoomId={props.activeRoomId}
/>
</TabBoundaryPane>

Expand Down
25 changes: 25 additions & 0 deletions apps/mission-control/src/app/useAppController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,31 @@ describe("useAppController room identity", () => {
expect(controller?.activeRoomId).toBe("staff");
});

test("the shared connectors route keeps Setup and Connectors identities distinct", async () => {
await render();
await act(async () => {
controller?.selectRoom("setup");
});
expect(controller?.activeTab).toBe("connectors");
expect(controller?.activeRoomId).toBe("setup");

await act(async () => {
controller?.selectRoom("connectors");
});
expect(controller?.activeTab).toBe("connectors");
expect(controller?.activeRoomId).toBe("connectors");

// Leaving and returning by tab only resolves to the first registry
// owner of the route, which is the Connectors room.
await act(async () => {
controller?.setActiveTab("boards");
});
await act(async () => {
controller?.setActiveTab("connectors");
});
expect(controller?.activeRoomId).toBe("connectors");
});

test("selectRoom fails closed on unknown room ids", async () => {
await render();
let selected: boolean | undefined;
Expand Down
Loading
Loading