diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsDragAndDrop.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsDragAndDrop.spec.ts index 50aa8c9f0146..d3f017ad6f64 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsDragAndDrop.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsDragAndDrop.spec.ts @@ -12,8 +12,8 @@ */ import { expect, test } from '@playwright/test'; import { PLAYWRIGHT_BASIC_TEST_TAG_OBJ } from '../../constant/config'; -import { GlobalSettingOptions } from '../../constant/settings'; import { + createNewPage, redirectToHomePage, toastNotification, uuid, @@ -23,9 +23,11 @@ import { dragAndDropElement, openDragDropDropdown, } from '../../utils/dragDrop'; -import { waitForAllLoadersToDisappear } from '../../utils/entity'; -import { settingClick } from '../../utils/sidebar'; -import { addTeamHierarchy } from '../../utils/team'; +import { + addTeamHierarchy, + hardDeleteTeamsByName, + visitTeamsPage, +} from '../../utils/team'; // use the admin user to login test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -91,26 +93,26 @@ test.describe( () => { test.beforeEach(async ({ page }) => { await redirectToHomePage(page); + await visitTeamsPage(page); + }); - const getOrganizationResponse = page.waitForResponse( - (response) => - response.url().includes('/api/v1/teams/name/') && - response.status() === 200 - ); - const permissionResponse = page.waitForResponse( - (response) => - response.url().includes('/api/v1/permissions/team/name/') && - response.status() === 200 - ); + test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); - await settingClick(page, GlobalSettingOptions.TEAMS); - await permissionResponse; - await getOrganizationResponse; - await waitForAllLoadersToDisappear(page); + try { + await hardDeleteTeamsByName(apiContext, [ + teamNameBusiness, + teamNameDivision, + teamNameDepartment, + teamNameGroup, + ]); + } finally { + await afterAction(); + } }); test('Add teams in hierarchy', async ({ page }) => { - test.slow(); + test.slow(true); for (const teamDetails of DRAG_AND_DROP_TEAM_DETAILS) { await addTeamHierarchy(page, teamDetails); @@ -158,7 +160,7 @@ test.describe( test(`Should drag and drop on ${TEAM_TYPE_BY_NAME[droppableTeamName]} team type`, async ({ page, }) => { - test.slow(); + test.slow(true); // Nested team will be shown once anything is moved under it if (index !== 0) { await openDragDropDropdown(page, teams[index - 1]); @@ -182,7 +184,7 @@ test.describe( } test(`Should drag and drop team on table level`, async ({ page }) => { - test.slow(); + test.slow(true); // Open department team dropdown as it is moved under it from last test await openDragDropDropdown(page, teamNameDepartment); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsHierarchy.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsHierarchy.spec.ts index 397ad57048c9..615520804692 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsHierarchy.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TeamsHierarchy.spec.ts @@ -19,6 +19,7 @@ import { addTeamHierarchy, getNewTeamDetails, searchTeam, + visitTeamsPage, } from '../../utils/team'; // use the admin user to login @@ -45,17 +46,7 @@ test.describe( test.beforeEach(async ({ page }) => { await redirectToHomePage(page); - - const getOrganizationResponse = page.waitForResponse( - '/api/v1/teams/name/*' - ); - const permissionResponse = page.waitForResponse( - '/api/v1/permissions/team/name/*' - ); - - await settingClick(page, GlobalSettingOptions.TEAMS); - await permissionResponse; - await getOrganizationResponse; + await visitTeamsPage(page); }); test('Add teams in hierarchy', async ({ page }) => { @@ -110,8 +101,6 @@ test.describe( }); test('Delete Parent Team', async ({ page }) => { - await settingClick(page, GlobalSettingOptions.TEAMS); - await page.getByRole('link', { name: businessTeamName }).click(); await page.click('[data-testid="manage-button"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts index fa61cd70715c..e166c0322b41 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts @@ -74,6 +74,121 @@ export const openAddTeamModal = async ( return addTeamModal; }; +/** + * Land on Settings > Teams with the hierarchy table settled. + * + * The table spins on its own child-teams fetch plus the per-team asset-count + * aggregation, so navigation alone is not enough — a caller that acts right + * after the click drags rows that are still being repainted. Wait on the two + * calls that gate the first paint, then on the table itself. + */ +export const visitTeamsPage = async (page: Page) => { + const organizationResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/teams/name/') && response.ok() + ); + const permissionResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/permissions/team/name/') && response.ok() + ); + + await settingClick(page, GlobalSettingOptions.TEAMS); + await Promise.all([permissionResponse, organizationResponse]); + + await expect(page.getByTestId('team-hierarchy-table')).toBeVisible(); + await waitForAllLoadersToDisappear(page); +}; + +interface TeamCleanupFailure { + teamName: string; + reason: string; +} + +/** + * Hard-delete one team created through the UI, children included. + * + * Reports a failure rather than throwing so a caller cleaning up several teams + * still attempts the rest — a throw here would leave the remaining teams behind + * and recreate the accumulation this cleanup exists to prevent. + * + * Specs that build teams through the UI have no entity handle to call + * `TeamClass.delete` on, so the id is resolved by name first. Delete-by-name is + * not an option: `TeamResource` pins that route to `recursive=false`, and these + * teams are nested by the time cleanup runs. + * + * 404 on the lookup is the one tolerated outcome — the spec may have deleted + * the team as part of what it asserts, and a recursive delete of its parent + * takes its children with it. + */ +const hardDeleteTeamByName = async ( + apiContext: APIRequestContext, + teamName: string +): Promise => { + let failure: TeamCleanupFailure | undefined; + + try { + const teamResponse = await apiContext.get( + `/api/v1/teams/name/${encodeURIComponent(teamName)}` + ); + + if (!teamResponse.ok()) { + if (teamResponse.status() !== 404) { + failure = { + teamName, + reason: `lookup returned ${teamResponse.status()} ${await teamResponse.text()}`, + }; + } + } else { + const { id } = await teamResponse.json(); + const deleteResponse = await apiContext.delete( + `/api/v1/teams/${id}?hardDelete=true&recursive=true` + ); + + if (!deleteResponse.ok()) { + failure = { + teamName, + reason: `delete returned ${deleteResponse.status()} ${await deleteResponse.text()}`, + }; + } + } + } catch (error) { + failure = { teamName, reason: (error as Error).message }; + } + + return failure; +}; + +/** + * Hard-delete teams created through the UI, children included. + * + * Deletes are sequential: a recursive delete takes a team's children with it, + * so issuing them in parallel would race the ones already removed. Every name + * is attempted before anything is asserted, and the assertion then names every + * team that survived — cleanup that fails quietly is what lets teams pile up on + * a long-lived deployment in the first place. + */ +export const hardDeleteTeamsByName = async ( + apiContext: APIRequestContext, + teamNames: string[] +) => { + const failures: TeamCleanupFailure[] = []; + + for (const teamName of teamNames) { + const failure = await hardDeleteTeamByName(apiContext, teamName); + + if (failure) { + failures.push(failure); + } + } + + expect( + failures, + `Failed to clean up teams: ${failures + .map(({ teamName, reason }) => `"${teamName}" (${reason})`) + .join(', ')}` + ).toEqual([]); +}; + interface SearchTeamOptions { expectEmptyResults?: boolean; expectNotFound?: boolean;