From f25ec342e9815581ab5416a4ffc075bbeda530de Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:17:49 +0700 Subject: [PATCH 01/13] Added api helper for creating tiptap data type with media folder --- .../lib/helpers/DataTypeApiHelper.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeApiHelper.ts b/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeApiHelper.ts index 968fd1a48ecc..011898069492 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeApiHelper.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeApiHelper.ts @@ -1312,6 +1312,17 @@ export class DataTypeApiHelper { return await this.save(dataType); } + async createTiptapDataTypeWithMediaFolder(name: string, mediaFolderId: string) { + await this.ensureNameNotExists(name); + + const dataType = new TiptapDataTypeBuilder() + .withName(name) + .withMediaFolderParentId(mediaFolderId) + .build(); + + return await this.save(dataType); + } + async createTipTapDataTypeWithABlock(name: string, contentElementTypeKey: string) { await this.ensureNameNotExists(name); From dabf0794ddb51ad830c69d20adff77a118b59f4a Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:18:37 +0700 Subject: [PATCH 02/13] Added ui helper for remove image upload folder --- .../lib/helpers/DataTypeUiHelper.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeUiHelper.ts b/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeUiHelper.ts index fa3c6840b5cc..479f2ddc9939 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeUiHelper.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeUiHelper.ts @@ -441,7 +441,7 @@ export class DataTypeUiHelper extends UiBaseLocators { } async addMediaStartNode(mediaName: string) { - await this.click(this.mediaCardItems.filter({hasText: mediaName})); + await this.selectMediaWithName(mediaName); await this.clickChooseModalButton(); } @@ -733,6 +733,11 @@ export class DataTypeUiHelper extends UiBaseLocators { await this.clickChooseModalButton(); } + async removeImageUploadFolder(mediaFolderName: string) { + await this.click(this.page.locator(`uui-card-media[name="${mediaFolderName}"]`).locator('[label="Remove"]')); + await this.click(this.confirmToRemoveBtn); + } + async clickAddWithPlusButton() { await this.click(this.addWithPlusBtn); } From 5b2ead8c3a4b36151f90c7af457bb7d711339c6e Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:18:54 +0700 Subject: [PATCH 03/13] Updated ui helper for selecting media with name --- .../lib/helpers/UiBaseLocators.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UiBaseLocators.ts b/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UiBaseLocators.ts index c3867ced7eb1..36ebf91dbdb2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UiBaseLocators.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UiBaseLocators.ts @@ -1236,18 +1236,16 @@ export class UiBaseLocators extends BasePage { await this.click(this.mediaCardItems.filter({hasText: name})); } - async selectMediaWithName(mediaName: string, isForce: boolean = false) { + async selectMediaWithName(mediaName: string) { const mediaLocator = this.mediaCardItems.filter({hasText: mediaName}); await this.waitForVisible(mediaLocator); - // Using direct click with position option (not supported by BasePage.click) - await mediaLocator.click({position: {x: 0.5, y: 0.5}, force: isForce}); + await this.click(mediaLocator.locator('#select-checkbox'), {force: true}); } async selectMediaWithTestId(mediaKey: string) { - const locator = this.page.getByTestId('media:' + mediaKey); - await this.waitForVisible(locator); - // Using direct click with position option (not supported by BasePage.click) - await locator.click({position: {x: 0.5, y: 0.5}}); + const mediaLocator = this.page.getByTestId('media:' + mediaKey); + await this.waitForVisible(mediaLocator); + await this.click(mediaLocator.locator('#select-checkbox'), {force: true}); } async clickMediaPickerModalSubmitButton() { @@ -1620,4 +1618,9 @@ export class UiBaseLocators extends BasePage { await this.pressKey(this.searchTxt, 'Enter'); await this.page.waitForTimeout(ConstantHelper.wait.medium); } + + async isSelectCheckboxVisibleForMediaName(mediaName: string, isVisible: boolean = true) { + const selectCheckboxLocator = this.mediaCardItems.filter({hasText: mediaName}).locator('#select-checkbox'); + await this.isVisible(selectCheckboxLocator, isVisible); + } } \ No newline at end of file From b7ff21e7da4df78e7e6cda2ee8fc06d005f444c9 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:19:58 +0700 Subject: [PATCH 04/13] Added tests for selecting media link in multi url picker --- .../Content/ContentWithMultiURLPicker.spec.ts | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts index 0fca636d2d9d..dded7f0d6940 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts @@ -144,7 +144,7 @@ test('can create content with the media link', async ({umbracoApi, umbracoUi}) = await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); await umbracoUi.content.clickMediaLinkButton(); - await umbracoUi.content.selectMediaWithName(mediaFileName, true); + await umbracoUi.content.selectMediaWithName(mediaFileName); await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickLinkPickerAddButton(); await umbracoUi.content.clickSaveButtonAndWaitForContentToBeUpdated(); @@ -178,7 +178,7 @@ test('can add multiple links in the content', {tag: '@release'}, async ({umbraco // Add media link await umbracoUi.content.clickAddMultiURLPickerButton(); await umbracoUi.content.clickMediaLinkButton(); - await umbracoUi.content.selectMediaWithName(mediaFileName, true); + await umbracoUi.content.selectMediaWithName(mediaFileName); await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickLinkPickerAddButton(); await umbracoUi.waitForTimeout(500); // Wait for the media link to be added @@ -388,6 +388,62 @@ test('can create content with target toggle enabled to open link in new window', expect(contentData.values[0].value[0].target).toEqual('_blank'); }); +test('cannot select a media folder as a media link', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + const mediaFolderName = 'TestMediaFolder'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.clickMediaLinkButton(); + + // Assert + await umbracoUi.content.isSelectCheckboxVisibleForMediaName(mediaFolderName, false); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); +}); + +test('can select a media file inside a folder as a media link', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + const mediaFolderName = 'TestMediaFolder'; + const mediaFileName = 'TestMediaFileInFolder'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(mediaFileName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + const mediaFileId = await umbracoApi.media.createDefaultMediaWithImageAndParentId(mediaFileName, mediaFolderId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.clickMediaLinkButton(); + await umbracoUi.content.clickMediaWithName(mediaFolderName); + await umbracoUi.content.selectMediaWithName(mediaFileName); + await umbracoUi.content.clickChooseModalButton(); + await umbracoUi.content.clickLinkPickerAddButton(); + await umbracoUi.content.clickSaveButtonAndWaitForContentToBeUpdated(); + + // Assert + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('media'); + expect(contentData.values[0].value[0].unique).toEqual(mediaFileId); + expect(contentData.values[0].value[0].name).toEqual(mediaFileName); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(mediaFileName); +}); + test.describe('manual tab validation tests', () => { test('cannot create content with empty manual url and anchor', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { // Arrange From 20a5237b996bc3594da77120c5dd2061c6c6c23c Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:20:15 +0700 Subject: [PATCH 05/13] Added tests for media picker start node --- .../DataType/MediaPicker.spec.ts | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts index a2354e2a97d0..37336fc5f3cb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts @@ -127,10 +127,10 @@ test('can remove accepted types', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.dataType.doesDataTypeHaveValue(customDataTypeName, 'filter', mediaTypeData.id)).toBeFalsy(); }); -test('can add start node', async ({umbracoApi, umbracoUi}) => { +test('cannot add a media file as start node', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { // Arrange // Create media - const mediaName = 'TestStartNode'; + const mediaName = 'TestStartNodeFile'; const mediaId = await umbracoApi.media.createDefaultMediaWithArticle(mediaName); expect(await umbracoApi.media.doesNameExist(mediaName)).toBeTruthy(); await umbracoApi.dataType.createDefaultMediaPickerDataType(customDataTypeName); @@ -138,35 +138,74 @@ test('can add start node', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickChooseStartNodeButton(); - await umbracoUi.dataType.addMediaStartNode(mediaName); - await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); // Assert - expect(await umbracoApi.dataType.doesDataTypeHaveValue(customDataTypeName, 'startNodeId', mediaId)).toBeTruthy(); + await umbracoUi.dataType.isMediaCardItemWithNameDisabled(mediaName); // Clean await umbracoApi.media.ensureNameNotExists(mediaName); }); -test('can remove start node', async ({umbracoApi, umbracoUi}) => { +test('can add a media folder as start node', async ({umbracoApi, umbracoUi}) => { // Arrange - // Create media - const mediaName = 'TestStartNode'; - await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMediaWithArticle(mediaName); - expect(await umbracoApi.media.doesNameExist(mediaName)).toBeTruthy(); - await umbracoApi.dataType.createImageMediaPickerDataTypeWithStartNodeId(customDataTypeName, mediaId); + const mediaFolderName = 'TestStartNodeFolder'; + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoApi.dataType.createDefaultMediaPickerDataType(customDataTypeName); await umbracoUi.dataType.goToDataType(customDataTypeName); // Act - await umbracoUi.dataType.removeMediaStartNode(mediaName); + await umbracoUi.dataType.clickChooseStartNodeButton(); + await umbracoUi.dataType.selectMediaWithName(mediaFolderName); + await umbracoUi.dataType.clickChooseModalButton(); await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); // Assert - expect(await umbracoApi.dataType.doesDataTypeHaveValue(customDataTypeName, 'startNodeId', mediaId)).toBeFalsy(); + expect(await umbracoApi.dataType.doesDataTypeHaveValue(customDataTypeName, 'startNodeId', mediaFolderId)).toBeTruthy(); // Clean - await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); +}); + +test('can remove a media folder start node', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaFolderName = 'TestStartNodeFolder'; + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoApi.dataType.createMediaPickerDataTypeWithStartNodeId(customDataTypeName, mediaFolderId); + await umbracoUi.dataType.goToDataType(customDataTypeName); + + // Act + await umbracoUi.dataType.removeMediaStartNode(mediaFolderName); + await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); + + // Assert + expect(await umbracoApi.dataType.doesDataTypeHaveValue(customDataTypeName, 'startNodeId', mediaFolderId)).toBeFalsy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); +}); + +test('can add a nested media folder as start node', async ({umbracoApi, umbracoUi}) => { + // Arrange + const parentFolderName = 'ParentMediaFolder'; + const childFolderName = 'ChildMediaFolder'; + await umbracoApi.media.ensureNameNotExists(parentFolderName); + const parentFolderId = await umbracoApi.media.createDefaultMediaFolder(parentFolderName); + const childFolderId = await umbracoApi.media.createDefaultMediaFolderAndParentId(childFolderName, parentFolderId); + await umbracoApi.dataType.createDefaultMediaPickerDataType(customDataTypeName); + await umbracoUi.dataType.goToDataType(customDataTypeName); + + // Act + await umbracoUi.dataType.clickChooseStartNodeButton(); + await umbracoUi.dataType.clickMediaWithName(parentFolderName); + await umbracoUi.dataType.selectMediaWithName(childFolderName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); + + // Assert + expect(await umbracoApi.dataType.doesDataTypeHaveValue(customDataTypeName, 'startNodeId', childFolderId)).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(parentFolderName); }); for (const mediaPicker of mediaPickerTypes) { From 59c7b533c73c748449913e44aebaefc59217f679 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:20:38 +0700 Subject: [PATCH 06/13] Added tests for image upload folder in tiptap data type --- .../DefaultConfig/DataType/Tiptap.spec.ts | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts index 3af90161e8e9..e2fcc2409751 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -129,7 +129,7 @@ test('can add an available block', async ({umbracoApi, umbracoUi}) => { await umbracoApi.documentType.ensureNameNotExists(elementTypeName); }); -test('can add image upload folder', async ({umbracoApi, umbracoUi}) => { +test('can add image upload folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const mediaFolderName = 'TestMediaFolder'; const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); @@ -147,6 +147,88 @@ test('can add image upload folder', async ({umbracoApi, umbracoUi}) => { await umbracoApi.media.ensureNameNotExists(mediaFolderName); }); +test('cannot select a media file as image upload folder', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaFileName = 'TestMediaFile'; + const mediaFileId = await umbracoApi.media.createDefaultMediaFile(mediaFileName); + await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.clickChooseWithPlusButton(); + + // Assert + await umbracoUi.dataType.isMediaCardItemWithNameDisabled(mediaFileName); + await umbracoUi.dataType.isSelectCheckboxVisibleForMediaName(mediaFileName, false); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFileName); +}); + +test('can remove image upload folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaFolderName = 'TestMediaFolder'; + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoApi.dataType.createTiptapDataTypeWithMediaFolder(tipTapName, mediaFolderId); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.removeImageUploadFolder(mediaFolderName); + await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); + + // Assert + expect(await umbracoApi.dataType.doesDataTypeHaveValue(tipTapName, 'mediaParentId', mediaFolderId)).toBeFalsy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); +}); + +test('can update image upload folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const firstFolderName = 'FirstMediaFolder'; + const secondFolderName = 'SecondMediaFolder'; + const firstFolderId = await umbracoApi.media.createDefaultMediaFolder(firstFolderName); + const secondFolderId = await umbracoApi.media.createDefaultMediaFolder(secondFolderName); + await umbracoApi.dataType.createTiptapDataTypeWithMediaFolder(tipTapName, firstFolderId); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.removeImageUploadFolder(firstFolderName); + await umbracoUi.dataType.addImageUploadFolder(secondFolderName); + await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); + + // Assert + expect(await umbracoApi.dataType.doesDataTypeHaveValue(tipTapName, 'mediaParentId', secondFolderId)).toBeTruthy(); + expect(await umbracoApi.dataType.doesDataTypeHaveValue(tipTapName, 'mediaParentId', firstFolderId)).toBeFalsy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(firstFolderName); + await umbracoApi.media.ensureNameNotExists(secondFolderName); +}); + +test('can add a nested media folder as image upload folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const parentFolderName = 'ParentMediaFolder'; + const childFolderName = 'ChildMediaFolder'; + const parentFolderId = await umbracoApi.media.createDefaultMediaFolder(parentFolderName); + const childFolderId = await umbracoApi.media.createDefaultMediaFolderAndParentId(childFolderName, parentFolderId); + await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.clickChooseWithPlusButton(); + await umbracoUi.dataType.clickMediaWithName(parentFolderName); + await umbracoUi.dataType.selectMediaWithName(childFolderName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSaveButtonAndWaitForDataTypeToBeUpdated(); + + // Assert + expect(await umbracoApi.dataType.doesDataTypeHaveValue(tipTapName, 'mediaParentId', childFolderId)).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(parentFolderName); +}); + test('can enable ignore user start nodes', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName); From da0a0eeffc8c24d23b7400ee6a3b19710db56361 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:21:04 +0700 Subject: [PATCH 07/13] Updated tests for user media start nodes --- .../tests/DefaultConfig/Users/User.spec.ts | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index deaf80f5f4e8..b0fdd6f9216a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -212,83 +212,97 @@ test('can remove a content start node from a user', {tag: '@release'}, async ({u await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); -test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +test('cannot add a media file as a media start node to a user', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { // Arrange const mediaName = 'TestMediaFile'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); + await umbracoApi.media.createDefaultMediaFile(mediaName); await umbracoUi.user.goToUserWithName(nameOfTheUser); // Act await umbracoUi.user.clickChooseMediaStartNodeButton(); - await umbracoUi.user.selectMediaWithName(mediaName); - await umbracoUi.user.clickChooseModalButton(); - await umbracoUi.user.clickSaveButtonAndWaitForUserToBeUpdated(); // Assert - expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + await umbracoUi.user.isMediaCardItemWithNameDisabled(mediaName); + await umbracoUi.user.isSelectCheckboxVisibleForMediaName(mediaName, false); // Clean await umbracoApi.media.ensureNameNotExists(mediaName); }); -test('can add multiple media start nodes for a user', async ({umbracoApi, umbracoUi}) => { +test('can add a media folder as a media start node to a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange + const mediaFolderName = 'TestMediaFolder'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); - const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - const mediaName = 'TestMediaFile'; - const secondMediaName = 'SecondMediaFile'; - await umbracoApi.media.ensureNameNotExists(mediaName); - await umbracoApi.media.ensureNameNotExists(secondMediaName); - const firstMediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); - const secondMediaId = await umbracoApi.media.createDefaultMediaFile(secondMediaName); - // Adds the media start node to the user - const userData = await umbracoApi.user.getByName(nameOfTheUser); - userData.mediaStartNodeIds.push({id: firstMediaId}); - await umbracoApi.user.update(userId, userData); - expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId])).toBeTruthy(); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); await umbracoUi.user.goToUserWithName(nameOfTheUser); // Act await umbracoUi.user.clickChooseMediaStartNodeButton(); - await umbracoUi.user.selectMediaWithName(secondMediaName); + await umbracoUi.user.selectMediaWithName(mediaFolderName); await umbracoUi.user.clickChooseModalButton(); await umbracoUi.user.clickSaveButtonAndWaitForUserToBeUpdated(); // Assert - expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId, secondMediaId])).toBeTruthy(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaFolderId])).toBeTruthy(); // Clean - await umbracoApi.media.ensureNameNotExists(mediaName); - await umbracoApi.media.ensureNameNotExists(secondMediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); }); -test('can remove a media start node from a user', async ({umbracoApi, umbracoUi}) => { +test('can remove a media folder start node from a user', async ({umbracoApi, umbracoUi}) => { // Arrange + const mediaFolderName = 'TestMediaFolder'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - const mediaName = 'TestMediaFile'; - await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); // Adds the media start node to the user const userData = await umbracoApi.user.getByName(nameOfTheUser); - userData.mediaStartNodeIds.push({id: mediaId}); + userData.mediaStartNodeIds.push({id: mediaFolderId}); await umbracoApi.user.update(userId, userData); - expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaFolderId])).toBeTruthy(); await umbracoUi.user.goToUserWithName(nameOfTheUser); // Act - await umbracoUi.user.clickRemoveButtonForMediaNodeWithName(mediaName); + await umbracoUi.user.clickRemoveButtonForMediaNodeWithName(mediaFolderName); await umbracoUi.user.clickConfirmRemoveButton(); await umbracoUi.user.clickSaveButtonAndWaitForUserToBeUpdated(); // Assert - expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeFalsy(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaFolderId])).toBeFalsy(); // Clean - await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); +}); + +test('can add a nested media folder as a media start node to a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const parentFolderName = 'ParentMediaFolder'; + const childFolderName = 'ChildMediaFolder'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.media.ensureNameNotExists(parentFolderName); + const parentFolderId = await umbracoApi.media.createDefaultMediaFolder(parentFolderName); + const childFolderId = await umbracoApi.media.createDefaultMediaFolderAndParentId(childFolderName, parentFolderId); + await umbracoUi.user.goToUserWithName(nameOfTheUser); + + // Act + await umbracoUi.user.clickChooseMediaStartNodeButton(); + await umbracoUi.user.clickMediaWithName(parentFolderName); + await umbracoUi.user.selectMediaWithName(childFolderName); + await umbracoUi.user.clickChooseModalButton(); + await umbracoUi.user.clickSaveButtonAndWaitForUserToBeUpdated(); + + // Assert + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [childFolderId])).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(parentFolderName); }); test('can allow access to all documents for a user', async ({umbracoApi, umbracoUi}) => { @@ -354,23 +368,23 @@ test('can see if the user has the correct access based on media start nodes', as // Arrange const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); - const mediaName = 'TestMediaFile'; - await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); + const mediaFolderName = 'TestMediaFolder'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); // Adds the media start node to the user const userData = await umbracoApi.user.getByName(nameOfTheUser); - userData.mediaStartNodeIds.push({id: mediaId}); + userData.mediaStartNodeIds.push({id: mediaFolderId}); await umbracoApi.user.update(userId, userData); - expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaFolderId])).toBeTruthy(); // Act await umbracoUi.user.goToUserWithName(nameOfTheUser); // Assert - await umbracoUi.user.doesUserHaveAccessToMediaNode(mediaName); + await umbracoUi.user.doesUserHaveAccessToMediaNode(mediaFolderName); // Clean - await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); }); test('can change password for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { From 4bf898f1ea5c36df7a802e8152e27522491bfd72 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 11:21:17 +0700 Subject: [PATCH 08/13] Updated tests for user group media start nodes --- .../DefaultConfig/Users/UserGroups.spec.ts | 73 +++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts index 0972bc6410b3..3bfe7b68d794 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts @@ -300,48 +300,93 @@ test('can enable access to all content from a user group ', async ({umbracoApi, expect(await umbracoApi.userGroup.doesUserGroupContainDocumentRootAccess(userGroupName)).toBeTruthy(); }); -test('can add a media start node to a user group', async ({umbracoApi, umbracoUi}) => { +test('cannot add a media file as a media start node to a user group', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.userGroup.createEmptyUserGroup(userGroupName); const mediaName = 'TestMedia'; await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); + await umbracoApi.media.createDefaultMediaFile(mediaName); await umbracoUi.userGroup.clickUserGroupsButton(); await umbracoUi.userGroup.clickUserGroupWithName(userGroupName); // Act await umbracoUi.userGroup.clickChooseMediaStartNodeButton(); - await umbracoUi.userGroup.selectMediaWithName(mediaName); + + // Assert + await umbracoUi.userGroup.isMediaCardItemWithNameDisabled(mediaName); + await umbracoUi.userGroup.isSelectCheckboxVisibleForMediaName(mediaName, false); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can add a media folder as a media start node to a user group', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.userGroup.createEmptyUserGroup(userGroupName); + const mediaFolderName = 'TestMediaFolder'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoUi.userGroup.clickUserGroupsButton(); + await umbracoUi.userGroup.clickUserGroupWithName(userGroupName); + + // Act + await umbracoUi.userGroup.clickChooseMediaStartNodeButton(); + await umbracoUi.userGroup.selectMediaWithName(mediaFolderName); await umbracoUi.userGroup.clickChooseModalButton(); await umbracoUi.userGroup.clickSaveButtonAndWaitForUserGroupToBeUpdated(); // Assert - expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, mediaId)).toBeTruthy(); + expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, mediaFolderId)).toBeTruthy(); // Clean - await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); }); -test('can remove a media start node from a user group ', async ({umbracoApi, umbracoUi}) => { +test('can remove a media folder start node from a user group', async ({umbracoApi, umbracoUi}) => { // Arrange - const mediaName = 'TestMedia'; - await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); - await umbracoApi.userGroup.createUserGroupWithMediaStartNode(userGroupName, mediaId); - expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, mediaId)).toBeTruthy(); + const mediaFolderName = 'TestMediaFolder'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoApi.userGroup.createUserGroupWithMediaStartNode(userGroupName, mediaFolderId); + expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, mediaFolderId)).toBeTruthy(); await umbracoUi.userGroup.clickUserGroupsButton(); await umbracoUi.userGroup.clickUserGroupWithName(userGroupName); // Act - await umbracoUi.userGroup.clickRemoveMediaStartNodeFromUserGroup(mediaName); + await umbracoUi.userGroup.clickRemoveMediaStartNodeFromUserGroup(mediaFolderName); await umbracoUi.userGroup.clickConfirmRemoveButton(); await umbracoUi.userGroup.clickSaveButtonAndWaitForUserGroupToBeUpdated(); // Assert - expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, mediaId)).toBeFalsy(); + expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, mediaFolderId)).toBeFalsy(); // Clean - await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); +}); + +test('can add a nested media folder as a media start node to a user group', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.userGroup.createEmptyUserGroup(userGroupName); + const parentFolderName = 'ParentMediaFolder'; + const childFolderName = 'ChildMediaFolder'; + await umbracoApi.media.ensureNameNotExists(parentFolderName); + const parentFolderId = await umbracoApi.media.createDefaultMediaFolder(parentFolderName); + const childFolderId = await umbracoApi.media.createDefaultMediaFolderAndParentId(childFolderName, parentFolderId); + await umbracoUi.userGroup.clickUserGroupsButton(); + await umbracoUi.userGroup.clickUserGroupWithName(userGroupName); + + // Act + await umbracoUi.userGroup.clickChooseMediaStartNodeButton(); + await umbracoUi.userGroup.clickMediaWithName(parentFolderName); + await umbracoUi.userGroup.selectMediaWithName(childFolderName); + await umbracoUi.userGroup.clickChooseModalButton(); + await umbracoUi.userGroup.clickSaveButtonAndWaitForUserGroupToBeUpdated(); + + // Assert + expect(await umbracoApi.userGroup.doesUserGroupContainMediaStartNodeId(userGroupName, childFolderId)).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(parentFolderName); }); test('can enable access to all media in a user group ', async ({umbracoApi, umbracoUi}) => { From b6a1bd10888404e176765d2312f73d21a57032cb Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Tue, 3 Mar 2026 12:26:32 +0700 Subject: [PATCH 09/13] Make tests run in the pipeline --- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 3c4c27724684..e1adc16e0d3a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -23,7 +23,7 @@ "testSqlite": "npm run build && npx playwright test DefaultConfig --grep-invert \"Users\"", "all": "npm run build && npx playwright test", "createTest": "node createTest.js", - "smokeTest": "npm run build && npx playwright test DefaultConfig --grep \"@smoke\"", + "smokeTest": "npm run build && npx playwright test DefaultConfig/Content/ContentWithMultiURLPicker DefaultConfig/DataType/MediaPicker DefaultConfig/DataType/Tiptap DefaultConfig/Users/User DefaultConfig/Users/UserGroup", "smokeTestSqlite": "npm run build && npx playwright test DefaultConfig --grep \"@smoke\" --grep-invert \"Users\"", "releaseTest": "npm run build && npx playwright test DefaultConfig --grep \"@release\"", "testWindows": "npm run build && npx playwright test DefaultConfig --grep-invert \"RelationType\"" From b1f769f0df5a778eeec7e9be75ac660a6818bd87 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Fri, 6 Mar 2026 10:11:23 +0700 Subject: [PATCH 10/13] Fixed comment --- .../tests/DefaultConfig/DataType/MediaPicker.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts index 37336fc5f3cb..5f9a66713d32 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts @@ -131,7 +131,7 @@ test('cannot add a media file as start node', {tag: '@release'}, async ({umbraco // Arrange // Create media const mediaName = 'TestStartNodeFile'; - const mediaId = await umbracoApi.media.createDefaultMediaWithArticle(mediaName); + await umbracoApi.media.createDefaultMediaWithArticle(mediaName); expect(await umbracoApi.media.doesNameExist(mediaName)).toBeTruthy(); await umbracoApi.dataType.createDefaultMediaPickerDataType(customDataTypeName); await umbracoUi.dataType.goToDataType(customDataTypeName); From 9deb14e622f20d543a167327211cf169b5e3d926 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Fri, 6 Mar 2026 10:27:18 +0700 Subject: [PATCH 11/13] Cleaned code --- .../tests/DefaultConfig/DataType/Tiptap.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts index e2fcc2409751..6acfe6437779 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -150,7 +150,7 @@ test('can add image upload folder', {tag: '@smoke'}, async ({umbracoApi, umbraco test('cannot select a media file as image upload folder', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { // Arrange const mediaFileName = 'TestMediaFile'; - const mediaFileId = await umbracoApi.media.createDefaultMediaFile(mediaFileName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName); await umbracoUi.dataType.goToDataType(tipTapName); From f2b192730cfdc78fa9dbb55dbd02cd1be94e8d64 Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Fri, 6 Mar 2026 10:27:48 +0700 Subject: [PATCH 12/13] Added tests for add multiple media start nodes to a user --- .../tests/DefaultConfig/Users/User.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index b0fdd6f9216a..a0cc7816e0b7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -254,6 +254,36 @@ test('can add a media folder as a media start node to a user', {tag: '@smoke'}, await umbracoApi.media.ensureNameNotExists(mediaFolderName); }); +test('can add multiple media start nodes to a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaFolderName = 'TestMediaFolder'; + const secondMediaFolderName = 'SecondMediaFolder'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(secondMediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + // Adds the first media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: mediaFolderId}); + await umbracoApi.user.update(userId, userData); + const secondMediaFolderId = await umbracoApi.media.createDefaultMediaFolder(secondMediaFolderName); + await umbracoUi.user.goToUserWithName(nameOfTheUser); + + // Act + await umbracoUi.user.clickChooseMediaStartNodeButton(); + await umbracoUi.user.selectMediaWithName(secondMediaFolderName); + await umbracoUi.user.clickChooseModalButton(); + await umbracoUi.user.clickSaveButtonAndWaitForUserToBeUpdated(); + + // Assert + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaFolderId, secondMediaFolderId])).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(secondMediaFolderName); +}); + test('can remove a media folder start node from a user', async ({umbracoApi, umbracoUi}) => { // Arrange const mediaFolderName = 'TestMediaFolder'; From f95edacc37508fce10bb4254193f78f10faecc4d Mon Sep 17 00:00:00 2001 From: Nhu Dinh Date: Fri, 6 Mar 2026 16:52:56 +0700 Subject: [PATCH 13/13] Reverted npm command --- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index e1adc16e0d3a..3c4c27724684 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -23,7 +23,7 @@ "testSqlite": "npm run build && npx playwright test DefaultConfig --grep-invert \"Users\"", "all": "npm run build && npx playwright test", "createTest": "node createTest.js", - "smokeTest": "npm run build && npx playwright test DefaultConfig/Content/ContentWithMultiURLPicker DefaultConfig/DataType/MediaPicker DefaultConfig/DataType/Tiptap DefaultConfig/Users/User DefaultConfig/Users/UserGroup", + "smokeTest": "npm run build && npx playwright test DefaultConfig --grep \"@smoke\"", "smokeTestSqlite": "npm run build && npx playwright test DefaultConfig --grep \"@smoke\" --grep-invert \"Users\"", "releaseTest": "npm run build && npx playwright test DefaultConfig --grep \"@release\"", "testWindows": "npm run build && npx playwright test DefaultConfig --grep-invert \"RelationType\""