From 3ddfe50a09c77084bb34c26edbf489cf6632d55b Mon Sep 17 00:00:00 2001 From: Bhargav Date: Tue, 27 Oct 2026 11:33:56 +0000 Subject: [PATCH] fix(iam): cancelling the Edit User dialog no longer reopens it (#15879) --- .../ci-matrix/ci_matrix_regression.json | 1 + .../pages/iamPages/iamFormValidationPage.js | 6 ++ ...iam-users-15546-edit-dialog-cancel.spec.js | 48 ++++++++++++++++ web/src/components/iam/users/User.spec.ts | 56 +++++++++++++++++++ web/src/components/iam/users/User.vue | 15 ++++- 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 tests/ui-testing/playwright-tests/RegressionSet/General/iam-users-15546-edit-dialog-cancel.spec.js diff --git a/tests/ui-testing/ci-matrix/ci_matrix_regression.json b/tests/ui-testing/ci-matrix/ci_matrix_regression.json index 634671f70d..0393e0f395 100644 --- a/tests/ui-testing/ci-matrix/ci_matrix_regression.json +++ b/tests/ui-testing/ci-matrix/ci_matrix_regression.json @@ -128,6 +128,7 @@ "landing-regression.spec.js", "internal-route-guards.spec.js", "datetime-paste-invalid-values-14648.spec.js", + "iam-users-15546-edit-dialog-cancel.spec.js", "query-management-sql-tooltip.spec.js" ], "disabled": [] diff --git a/tests/ui-testing/pages/iamPages/iamFormValidationPage.js b/tests/ui-testing/pages/iamPages/iamFormValidationPage.js index 15d37774eb..d1567ac062 100644 --- a/tests/ui-testing/pages/iamPages/iamFormValidationPage.js +++ b/tests/ui-testing/pages/iamPages/iamFormValidationPage.js @@ -355,6 +355,12 @@ export class IamFormValidationPage { return this.page.locator(this.editBasicUserFirstBtn).count(); } + async clickFirstEditUserButton() { + await this.page.locator(this.editBasicUserFirstBtn).first().click(); + } + + getUrlWithUserActionPattern() { return /[?&]action=(update|add)\b|[?&]email=/; } + async navigateToUsersTab() { await this.navigateToIam(); const tab = this.page.locator(this.iamUsersTab); diff --git a/tests/ui-testing/playwright-tests/RegressionSet/General/iam-users-15546-edit-dialog-cancel.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/General/iam-users-15546-edit-dialog-cancel.spec.js new file mode 100644 index 0000000000..0f59bda2b9 --- /dev/null +++ b/tests/ui-testing/playwright-tests/RegressionSet/General/iam-users-15546-edit-dialog-cancel.spec.js @@ -0,0 +1,48 @@ +const { test, expect, navigateToBase } = require('../../utils/enhanced-baseFixtures.js'); +const testLogger = require('../../utils/test-logger.js'); +const PageManager = require('../../../pages/page-manager.js'); + +test.describe("IAM Users edit dialog cancel Regression", () => { + let pm; + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + await navigateToBase(page); + pm = new PageManager(page); + await pm.iamFormValidation.navigateToUsersTab(); + }); + + test("cancelled edit dialog stays closed and leaves no action in the URL @bug-15546 @P3 @regression @iamUsers", async ({ page }) => { + const iam = pm.iamFormValidation; + + await iam.openUpdateRoleDialogForFirstUser(); + await expect(page).toHaveURL(/action=update/); + + await iam.getUpdateRoleCancelBtnLocator().click(); + await expect(iam.getUpdateRoleDialogLocator()).toBeHidden(); + await expect(page).not.toHaveURL(iam.getUrlWithUserActionPattern()); + + // Outlast the dialog's 100ms open timer so a late reopen is caught. + await page.waitForTimeout(2000); + await expect(iam.getUpdateRoleDialogLocator()).toBeHidden(); + testLogger.info('Edit dialog stayed closed and URL is clean after Cancel'); + }); + + test("edit dialog cancelled during a slow roles load does not reopen @bug-15546 @P3 @regression @iamUsers", async ({ page }) => { + const iam = pm.iamFormValidation; + await page.route(/\/api\/[^/]+\/users\/roles(\?.*)?$/, async (route) => { + await new Promise((r) => setTimeout(r, 3000)); + await route.continue(); + }); + + await page.reload(); + await iam.clickFirstEditUserButton(); + await expect(iam.getUpdateRoleDialogLocator()).toBeVisible(); + await iam.getUpdateRoleCancelBtnLocator().click(); + await expect(iam.getUpdateRoleDialogLocator()).toBeHidden(); + + await page.waitForTimeout(4500); + await expect(iam.getUpdateRoleDialogLocator()).toBeHidden(); + await expect(page).not.toHaveURL(iam.getUrlWithUserActionPattern()); + }); +}); diff --git a/web/src/components/iam/users/User.spec.ts b/web/src/components/iam/users/User.spec.ts index 1159cdc78f..bc1e9afeb2 100644 --- a/web/src/components/iam/users/User.spec.ts +++ b/web/src/components/iam/users/User.spec.ts @@ -1051,6 +1051,62 @@ describe("User Component", () => { }); }); + describe("AddUser dialog dismissal (#15546)", () => { + it("clears action/email from the URL when the dialog is cancelled", async () => { + const replaceSpy = vi.spyOn(router, "replace").mockImplementation(() => Promise.resolve()); + wrapper.vm.showAddUserDialog = true; + await flushPromises(); + + wrapper.findComponent(AddUserStub).vm.$emit("update:open", false); + await flushPromises(); + + expect(wrapper.vm.showAddUserDialog).toBe(false); + expect(replaceSpy).toHaveBeenCalledWith({ + name: "users", + query: { org_identifier: store.state.selectedOrganization.identifier }, + }); + replaceSpy.mockRestore(); + }); + + it("drops a pending open timer when the form is hidden", async () => { + const replaceSpy = vi.spyOn(router, "replace").mockImplementation(() => Promise.resolve()); + wrapper.vm.addUser({ row: { email: "test@example.com" } }, true); + wrapper.vm.hideForm(); + await new Promise((r) => setTimeout(r, 150)); + + expect(wrapper.vm.showAddUserDialog).toBe(false); + replaceSpy.mockRestore(); + }); + + it("does not reopen the edit dialog when it was cancelled while the page was still loading", async () => { + sharedUsersState.users = []; + queryClient.clear(); + let resolveRoles: (v: any) => void = () => {}; + mockUsersService.getRoles.mockReturnValue(new Promise((r) => (resolveRoles = r)) as any); + await router.push({ + name: "users", + query: { + action: "update", + email: "test@example.com", + org_identifier: store.state.selectedOrganization.identifier, + }, + }); + const late = mountUser(); + await flushPromises(); + + late.findComponent(AddUserStub).vm.$emit("update:open", false); + await flushPromises(); + expect(router.currentRoute.value.query.action).toBeUndefined(); + + resolveRoles({ data: [{ label: "admin", value: "admin" }] }); + await flushPromises(); + await new Promise((r) => setTimeout(r, 150)); + + expect((late.vm as any).showAddUserDialog).toBe(false); + late.unmount(); + }); + }); + describe("hideForm", () => { it("should hide add user dialog and navigate", () => { const replaceSpy = vi.spyOn(router, "replace").mockImplementation(() => Promise.resolve()); diff --git a/web/src/components/iam/users/User.vue b/web/src/components/iam/users/User.vue index e1fd207579..d5487ff897 100644 --- a/web/src/components/iam/users/User.vue +++ b/web/src/components/iam/users/User.vue @@ -283,7 +283,8 @@ along with this program. If not, see . /> | undefined; + const addUser = (props: any, is_updated: boolean) => { isUpdated.value = is_updated; selectedUser.value.organization = store.state.selectedOrganization.identifier; @@ -1133,7 +1136,8 @@ export default defineComponent({ } else { selectedUser.value = {}; } - setTimeout(() => { + clearTimeout(openAddUserTimer); + openAddUserTimer = setTimeout(() => { showAddUserDialog.value = true; }, 100); }; @@ -1227,6 +1231,7 @@ export default defineComponent({ }; const hideForm = () => { + clearTimeout(openAddUserTimer); showAddUserDialog.value = false; router.replace({ name: "users", @@ -1236,6 +1241,11 @@ export default defineComponent({ }); }; + const onAddUserOpenChange = (open: boolean) => { + if (open) showAddUserDialog.value = true; + else hideForm(); + }; + const addMember = async (res: any, data: any, operationType: string) => { showAddUserDialog.value = false; if (res.code == 200) { @@ -1576,6 +1586,7 @@ export default defineComponent({ addRoutePush, addMember, hideForm, + onAddUserOpenChange, isUpdated, showAddUserDialog, selectedUser,