Files
kandev/apps/web/scripts/demo-team-access-shots.mjs
T
João Salavisa 481359ddb2 feat(auth): organization units, tenancy, roles and scopes for team access (#2919)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:40:43 +00:00

120 lines
4.9 KiB
JavaScript

// Capture screenshots of the team-access surfaces from the seeded demo
// instance. Run after scripts/demo-team-access.sh up:
//
// cd apps/web && node scripts/demo-team-access-shots.mjs
//
// It lives under apps/web because Node resolves imports relative to the
// script file, and Playwright is installed in this workspace.
import { chromium } from "@playwright/test";
import { mkdirSync } from "node:fs";
const BASE = process.env.KANDEV_DEMO_URL ?? "http://127.0.0.1:8231";
const OUT = process.env.SHOT_DIR ?? "/tmp/kandev-team-access-demo/shots";
const PASSWORD = "kandev-demo-pw-1";
mkdirSync(OUT, { recursive: true });
async function signIn(context, email) {
const page = await context.newPage();
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
await page.getByLabel(/email/i).fill(email);
await page.getByLabel(/password/i).fill(PASSWORD);
await Promise.all([
page.waitForResponse(
(r) => r.url().includes("/api/v1/auth/login") && r.request().method() === "POST",
),
page.getByRole("button", { name: /sign in|log in/i }).click(),
]);
await page.waitForLoadState("networkidle");
await dismissOnboarding(page);
return page;
}
// Each account hits the first-run wizard on its first sign-in, which would
// otherwise sit on top of every board screenshot.
async function dismissOnboarding(page) {
for (let step = 0; step < 6; step += 1) {
const skip = page.getByRole("button", { name: /^skip$/i });
if (!(await skip.count())) return;
await skip
.first()
.click()
.catch(() => {});
await page.waitForTimeout(500);
}
}
async function shot(page, name, focus) {
await page.waitForTimeout(1200); // let the SPA settle before capture
if (focus) {
const target = page.locator(focus).first();
if (await target.count()) {
await target.scrollIntoViewIfNeeded();
await page.waitForTimeout(400);
}
}
await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: false });
console.log(`captured ${name}.png`);
}
// The Team access card sits below the workspace form, so scroll it into frame.
// Scroll the *bottom* of the card into frame: the heading alone is already
// on screen, so targeting it scrolls nothing and the members list stays cut off.
const TEAM_CARD = '#add-member-user, [data-testid="workspace-member-row"]:last-of-type';
const browser = await chromium.launch();
try {
// 1. Owner: the Team access card on the shared workspace.
const anaCtx = await browser.newContext({ viewport: { width: 1440, height: 1250 } });
const ana = await signIn(anaCtx, "ana@example.com");
const listResponse = await ana.request.get(`${BASE}/api/v1/workspaces`);
const { workspaces } = await listResponse.json();
const team = workspaces.find((w) => w.name === "Platform Team");
const priv = workspaces.find((w) => w.name.startsWith("Ana - security"));
await ana.goto(`${BASE}/settings/workspace/${team.id}`, { waitUntil: "networkidle" });
await shot(ana, "01-owner-team-access-shared", TEAM_CARD);
await ana.goto(`${BASE}/settings/workspace/${priv.id}`, { waitUntil: "networkidle" });
await shot(ana, "02-owner-team-access-private", TEAM_CARD);
await ana.goto(`${BASE}/`, { waitUntil: "networkidle" });
await shot(ana, "03-owner-board");
// 2. Member with no invitation: the shared board is simply there.
const brunoCtx = await browser.newContext({ viewport: { width: 1440, height: 1250 } });
const bruno = await signIn(brunoCtx, "bruno@example.com");
await bruno.goto(`${BASE}/`, { waitUntil: "networkidle" });
await shot(bruno, "04-member-sees-shared-board");
await bruno.goto(`${BASE}/settings/workspace/${team.id}`, { waitUntil: "networkidle" });
await shot(bruno, "05-member-team-access-readonly", TEAM_CARD);
// 3. Viewer: reads the same workspace, holds one scope.
const carlaCtx = await browser.newContext({ viewport: { width: 1440, height: 1250 } });
const carla = await signIn(carlaCtx, "carla@example.com");
await carla.goto(`${BASE}/settings/workspace/${team.id}`, { waitUntil: "networkidle" });
await shot(carla, "06-viewer-team-access-readonly", TEAM_CARD);
// 4. Guest: the shared workspace does not exist for her.
const danaCtx = await browser.newContext({ viewport: { width: 1440, height: 1250 } });
const dana = await signIn(danaCtx, "dana@example.com");
await dana.goto(`${BASE}/`, { waitUntil: "networkidle" });
await shot(dana, "07-guest-sees-only-invited-workspace");
// 5. Mobile composition of the members surface.
const mobileCtx = await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
deviceScaleFactor: 2,
});
const mobile = await signIn(mobileCtx, "ana@example.com");
await mobile.goto(`${BASE}/settings/workspace/${team.id}`, { waitUntil: "networkidle" });
await shot(mobile, "08-mobile-team-access", TEAM_CARD);
console.log(`\nscreenshots written to ${OUT}`);
} finally {
await browser.close();
}