diff --git a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts index 17ff62f37cf..b1f6a0bdbfe 100644 --- a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts +++ b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts @@ -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 | undefined> { + const teams = await readBack[]>(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 { + 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 }) => {