Files
multica/e2e/runtime-sharing.spec.ts
Bohan Jiangandmultica-agent ff674c4e1d MUL-8185: Support sharing runtimes with specific members (#9455)
* feat: share runtimes with selected workspace members

Co-authored-by: multica-agent <[email protected]>

* refactor(runtimes): one-click private/public and popover member picker

Private and Public apply immediately again (restrictions still go through
the reviewed revocation dialog). Specific members opens a draft popover
with search, Cancel and Save; selected recipients are summarized under
the scope control with an Edit entry point. The editor moves to its own
runtime-access-editor component.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-23 09:28:22 +08:00

61 lines
3.1 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { TestApiClient } from "./fixtures";
test("runtime owner can save specific members and revoke their access", async ({ page }, testInfo) => {
const api = new TestApiClient();
const member = new TestApiClient();
const run = `${Date.now()}-${process.pid}`;
await api.login(`runtime-owner-${run}@localhost`, "Runtime Owner");
const workspace = await api.ensureWorkspace("Runtime sharing", `runtime-sharing-${run}`);
await api.markUserOnboarded();
try {
await member.login(`runtime-member-${run}@localhost`, "Alice");
const memberId = await api.addMemberByEmail(`runtime-member-${run}@localhost`);
const runtimeId = await api.seedRuntime("Studio Mac");
await page.addInitScript((token) => {
localStorage.setItem("multica_token", token!);
localStorage.setItem("multica:chat:isOpen", "false");
}, api.getToken());
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(`/${workspace.slug}/runtimes/${runtimeId}`);
await page.getByRole("row", { name: /Studio Mac Built-in/ }).click();
const scope = (name: string) => page.getByRole("button", { name, exact: true });
const patched = () => page.waitForResponse((response) => response.url().endsWith(`/api/runtimes/${runtimeId}`) && response.request().method() === "PATCH");
await expect(scope("Private")).toHaveAttribute("aria-pressed", "true");
await page.screenshot({ path: testInfo.outputPath("runtime-private.png"), fullPage: true });
// Choosing members opens a draft picker; nothing persists until Save.
await scope("Members").click();
await expect(page.getByRole("button", { name: "Save", exact: true })).toBeDisabled();
await page.getByRole("checkbox", { name: "Alice" }).check();
await page.screenshot({ path: testInfo.outputPath("runtime-member-picker.png"), fullPage: true });
const saved = patched();
await page.getByRole("button", { name: "Save", exact: true }).click();
expect((await saved).status()).toBe(200);
await page.reload();
await expect(scope("Members")).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("button", { name: /Alice.*Edit/ })).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("runtime-shared.png"), fullPage: true });
// Private applies in one click.
const revoked = patched();
await scope("Private").click();
expect((await revoked).status()).toBe(200);
await page.reload();
await expect(scope("Private")).toHaveAttribute("aria-pressed", "true");
await scope("Members").click();
await page.getByRole("checkbox", { name: "Alice" }).check();
const sharedAgain = patched();
await page.getByRole("button", { name: "Save", exact: true }).click();
expect((await sharedAgain).status()).toBe(200);
await api.removeMember(memberId);
await expect(scope("Private")).toHaveAttribute("aria-pressed", "true", { timeout: 15000 });
await api.addMemberByEmail(`runtime-member-${run}@localhost`);
await page.reload();
await expect(scope("Private")).toHaveAttribute("aria-pressed", "true");
} finally {
await api.deleteWorkspace();
}
});