test(e2e-ui): verify team create, invite and delete against the API

Three team mutations stopped at a toast, and one of those toasts is matched as
loosely as /success/i -- almost any notification satisfied it.

  - create: the team is readable from /team/list and kept the models chosen in
    the modal, rather than trusting the UI's own "Team created"
  - invite: the invited address really appears in members_with_roles, which is
    the point of the flow
  - delete: the team is gone from /team/list. The existing assertion was that
    the row vanished, which is the client dropping it from local state and
    happens whether or not the delete reached the database.

Shapes read off a live proxy: /team/list is a bare array; /team/info nests the
record under team_info. All 6 tests pass locally.
This commit is contained in:
Yuneng Jiang 2026-08-08 22:14:36 -07:00
parent 464da52e03
commit 172e250fe1

View file

@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect, type Page as PlaywrightPage } from "@playwright/test";
import {
ADMIN_STORAGE_PATH,
E2E_TEAM_CRUD_ID,
@ -8,6 +8,22 @@ import {
} from "../../constants";
import { Page } from "../../fixtures/pages";
import { navigateToPage, dismissFeedbackPopup, clickTeamId } from "../../helpers/navigation";
import { readBack } from "../../helpers/roundTrip";
/** GET /team/list returns a bare array of teams, each carrying team_alias/team_id. */
async function findTeamByAlias(page: PlaywrightPage, alias: string): Promise<Record<string, any> | undefined> {
const teams = await readBack<Record<string, any>[]>(page, "/team/list");
return teams.find((team) => team.team_alias === alias);
}
/** GET /team/info nests the record under `team_info`; membership lives in members_with_roles. */
async function teamMemberEmails(page: PlaywrightPage, teamId: string): Promise<string[]> {
const info = await readBack<{ team_info: { members_with_roles?: { user_email?: string }[] } }>(
page,
`/team/info?team_id=${encodeURIComponent(teamId)}`,
);
return (info.team_info.members_with_roles ?? []).map((member) => member.user_email ?? "").filter(Boolean);
}
test.describe("Proxy Admin - Teams", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
@ -42,6 +58,13 @@ test.describe("Proxy Admin - Teams", () => {
// Verify success notification
await expect(page.getByText("Team created").first()).toBeVisible({ timeout: 10_000 });
// "Team created" is the UI reporting on its own request. Confirm the team
// is really there, and that the models picked in the modal came with it --
// a create that drops its model selection still toasts success.
const created = await findTeamByAlias(page, uniqueAlias);
expect(created, `team ${uniqueAlias} readable from /team/list`).toBeTruthy();
expect(created?.models, "created team kept its model selection").toBeTruthy();
});
test("Invite a user to a team", async ({ page }) => {
@ -71,6 +94,16 @@ test.describe("Proxy Admin - Teams", () => {
await modal.getByRole("button", { name: /Add Member/i }).click();
await expect(page.getByText(/member.*added|success/i).first()).toBeVisible({ timeout: 10_000 });
// Membership is the point of the flow, and the toast is matched loosely
// enough (/success/i) that almost any notification satisfies it. Confirm
// the user is actually on the team.
await expect
.poll(async () => await teamMemberEmails(page, E2E_TEAM_CRUD_ID), {
message: "invited user never appeared in the team's members",
timeout: 15_000,
})
.toContain("invitable@test.local");
});
test("Edit team member for team proxy admin does not belong to", async ({ page }) => {
@ -106,6 +139,15 @@ test.describe("Proxy Admin - Teams", () => {
await modal.getByRole("button", { name: /Force Delete|Delete/i }).click();
await expect(teamRow).not.toBeVisible({ timeout: 10_000 });
// A row vanishing from the table is the client dropping it from local
// state; it happens whether or not the delete reached the database.
await expect
.poll(async () => await findTeamByAlias(page, E2E_TEAM_DELETE_ALIAS), {
message: `team ${E2E_TEAM_DELETE_ALIAS} still readable from /team/list after delete`,
timeout: 15_000,
})
.toBeUndefined();
});
test("Team in org - edit team member", async ({ page }) => {