diff --git a/e2e/playwright/copy-move-actions/src/tests/copy-move-repository.e2e.ts b/e2e/playwright/copy-move-actions/src/tests/copy-move-repository.e2e.ts new file mode 100644 index 000000000..b42d87523 --- /dev/null +++ b/e2e/playwright/copy-move-actions/src/tests/copy-move-repository.e2e.ts @@ -0,0 +1,177 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { + ApiClientFactory, + NodesApi, + PersonalFilesPage, + RepositoryTestData, + RecentFilesPage, + test, + timeouts, + TrashcanApi, + users, + Utils, + cleanupRepositoryTestData, + seedRepositoryTestData +} from '@alfresco/aca-playwright-shared'; +import { expect } from '@playwright/test'; + +test.describe('Copy / Move — Repository destination', () => { + const admin = users.admin; + const REPOSITORY_LOCATION_LABEL = 'Repository'; + + let userNodesApi: NodesApi; + let adminNodesApi: NodesApi; + let trashcanApi: TrashcanApi; + let testData!: RepositoryTestData; + + test.beforeAll(async () => { + try { + const apiClientFactory = new ApiClientFactory(); + await apiClientFactory.setUpAcaBackend('admin'); + userNodesApi = await NodesApi.initialize('admin'); + adminNodesApi = userNodesApi; + trashcanApi = await TrashcanApi.initialize('admin'); + } catch (error) { + console.error(`beforeAll failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage, personalFiles }) => { + testData = await seedRepositoryTestData({ userNodesApi, adminNodesApi }); + + await Utils.tryLoginUser(loginPage, admin.username, admin.password, 'beforeEach failed'); + await personalFiles.navigate(); + }); + + test.afterEach(async () => { + await cleanupRepositoryTestData(testData, { adminNodesApi }); + }); + + test.afterAll(async () => { + await Utils.deleteNodesSitesEmptyTrashcan(adminNodesApi, trashcanApi, 'afterAll failed'); + }); + + const openDialogOnRepository = async (personalFiles: PersonalFilesPage, fileName: string, operation: 'Copy' | 'Move') => { + await Utils.reloadPageIfRowNotVisible(personalFiles, fileName); + await personalFiles.dataTable.selectItems(fileName); + await personalFiles.clickMoreActionsButton(operation); + await personalFiles.contentNodeSelector.selectLocation(REPOSITORY_LOCATION_LABEL); + await personalFiles.contentNodeSelector.spinnerWaitForReload(); + }; + + test('[XAT-19598] Copy a Personal file into a Repository folder (via search)', async ({ personalFiles }) => { + await openDialogOnRepository(personalFiles, testData.personalFile.name, 'Copy'); + await personalFiles.contentNodeSelector.searchAndSelectDestination(testData.repoFolder.name); + await personalFiles.contentNodeSelector.actionButton.click(); + + const msg = await personalFiles.snackBar.message.innerText(); + expect.soft(msg, 'Success snackbar did not confirm copy').toContain('Copied 1 item'); + + expect.soft(await personalFiles.dataTable.isItemPresent(testData.personalFile.name), 'Original personal file disappeared after Copy').toBe(true); + + await personalFiles.navigate({ remoteUrl: `#/repository/${testData.repoFolder.id}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + expect(await personalFiles.dataTable.isItemPresent(testData.personalFile.name), 'Copy is missing in Repository folder').toBe(true); + }); + + test('[XAT-19599] Move a Personal file into a Repository folder (via search)', async ({ personalFiles }) => { + await openDialogOnRepository(personalFiles, testData.personalFile.name, 'Move'); + await personalFiles.contentNodeSelector.searchAndSelectDestination(testData.repoFolder.name); + await personalFiles.contentNodeSelector.actionButton.click(); + + const msg = await personalFiles.snackBar.message.innerText(); + expect.soft(msg, 'Success snackbar did not confirm move').toContain('Moved 1 item'); + await personalFiles.snackBar.closeIcon.click(); + await personalFiles.dataTable.spinnerWaitForReload(); + + expect + .soft(await personalFiles.dataTable.isItemPresent(testData.personalFile.name), 'Original personal file still visible after Move') + .toBe(false); + + await personalFiles.navigate({ remoteUrl: `#/repository/${testData.repoFolder.id}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + expect(await personalFiles.dataTable.isItemPresent(testData.personalFile.name), 'Moved file is missing in Repository folder').toBe(true); + }); + + test.describe('from Recent Files', () => { + let recentFileName: string; + let recentFileId: string; + let recentDestFolderId: string; + let recentDestFolderName: string; + + test.beforeAll(async () => { + recentFileName = `recent-repo-file-${Utils.random()}.txt`; + recentDestFolderName = `recent-repo-dest-${Utils.random()}`; + recentFileId = (await adminNodesApi.createFile(recentFileName, '-root-')).entry.id; + recentDestFolderId = (await adminNodesApi.createFolder(recentDestFolderName, '-root-')).entry.id; + }); + + test.beforeEach(async ({ recentFilesPage }) => { + await recentFilesPage.navigate(); + await recentFilesPage.dataTable.spinnerWaitForReload(); + await waitForRowByReloading(recentFilesPage, recentFileName); + }); + + test.afterAll(async () => { + try { + await adminNodesApi.deleteNodes([recentFileId, recentDestFolderId], true); + } catch (error) { + console.error(`TS-16 afterAll cleanup failed: ${error}`); + } + }); + + test('[XAT-19600] Copy a Repository file from the Recent Files view into a Repository folder', async ({ recentFilesPage }) => { + await recentFilesPage.dataTable.selectItems(recentFileName); + await recentFilesPage.acaHeader.clickMoreActions(); + await recentFilesPage.matMenu.clickMenuItem('Copy'); + + await recentFilesPage.contentNodeSelector.selectLocation(REPOSITORY_LOCATION_LABEL); + await recentFilesPage.contentNodeSelector.spinnerWaitForReload(); + await recentFilesPage.contentNodeSelector.searchAndSelectDestination(recentDestFolderName); + await recentFilesPage.contentNodeSelector.actionButton.click(); + + const msg = await recentFilesPage.snackBar.message.innerText(); + expect.soft(msg, 'Success snackbar did not confirm copy').toContain('Copied 1 item'); + + await recentFilesPage.page.goto(`${recentFilesPage.page.url().split('#')[0]}#/repository/${recentDestFolderId}`); + await recentFilesPage.dataTable.spinnerWaitForReload(); + expect(await recentFilesPage.dataTable.isItemPresent(recentFileName), 'Copied Repository file is missing in destination folder').toBe(true); + }); + }); +}); + +async function waitForRowByReloading(page: RecentFilesPage, name: string, maxAttempts = 12): Promise { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + if (await page.dataTable.getRowByName(name).isVisible()) { + return; + } + await page.page.waitForTimeout(timeouts.tiny); + await page.page.reload({ waitUntil: 'load' }); + await page.dataTable.spinnerWaitForReload(); + } + throw new Error(`Row "${name}" did not appear in Recent Files after ${maxAttempts} attempts`); +} diff --git a/e2e/playwright/delete-actions/src/tests/restore-repository.e2e.ts b/e2e/playwright/delete-actions/src/tests/restore-repository.e2e.ts new file mode 100644 index 000000000..64c9163a3 --- /dev/null +++ b/e2e/playwright/delete-actions/src/tests/restore-repository.e2e.ts @@ -0,0 +1,93 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ApiClientFactory, NodesApi, test, TrashcanApi, users, Utils } from '@alfresco/aca-playwright-shared'; +import { expect } from '@playwright/test'; + +test.describe('Repository — Trash restore', () => { + const admin = users.admin; + + let adminNodesApi: NodesApi; + let trashcanApi: TrashcanApi; + + let repoFolderName: string; + let repoFolderId: string; + let repoFileName: string; + let repoFileId: string; + + test.beforeAll(async () => { + try { + const apiClientFactory = new ApiClientFactory(); + await apiClientFactory.setUpAcaBackend('admin'); + adminNodesApi = await NodesApi.initialize('admin'); + trashcanApi = await TrashcanApi.initialize('admin'); + + repoFolderName = `repo-folder-restore-${Utils.random()}`; + repoFileName = `repo-file-restore-${Utils.random()}.txt`; + repoFolderId = (await adminNodesApi.createFolder(repoFolderName, '-root-')).entry.id; + repoFileId = (await adminNodesApi.createFile(repoFileName, repoFolderId)).entry.id; + + await adminNodesApi.deleteNodes([repoFileId], false); + } catch (error) { + console.error(`beforeAll failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, admin.username, admin.password, 'beforeEach failed'); + }); + + test.afterAll(async () => { + try { + await adminNodesApi.deleteNodes([repoFolderId], true); + } catch (error) { + console.error(`Repository folder cleanup failed: ${error}`); + } + await Utils.deleteNodesSitesEmptyTrashcan(adminNodesApi, trashcanApi, 'afterAll failed'); + }); + + test('[XAT-19601] Restoring a Repository file from Trash lands it back in the Repository folder', async ({ trashPage, personalFiles }) => { + await trashPage.navigate(); + await trashPage.dataTable.spinnerWaitForReload(); + await expect(trashPage.dataTable.getRowByName(repoFileName)).toBeVisible(); + + await trashPage.dataTable.selectItems(repoFileName); + await trashPage.acaHeader.restoreButton.click(); + await trashPage.snackBar.verifySnackBarActionText(`${repoFileName} restored`); + + await expect(trashPage.dataTable.getRowByName(repoFileName), `${repoFileName} should be gone from Trash after restore`).toBeHidden(); + + await personalFiles.navigate({ remoteUrl: `#/repository/${repoFolderId}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + expect( + await personalFiles.dataTable.isItemPresent(repoFileName), + `Restored file should reappear inside the original Repository folder (${repoFolderName})` + ).toBe(true); + + const url = personalFiles.page.url(); + expect(url, 'Repository view must stay on /repository').toContain(`/repository/${repoFolderId}`); + expect(url, 'Restored Repository file must not force a /personal-files navigation').not.toContain('/personal-files/'); + }); +}); diff --git a/e2e/playwright/info-drawer/src/tests/info-drawer-repository.e2e.ts b/e2e/playwright/info-drawer/src/tests/info-drawer-repository.e2e.ts new file mode 100644 index 000000000..d3679d97b --- /dev/null +++ b/e2e/playwright/info-drawer/src/tests/info-drawer-repository.e2e.ts @@ -0,0 +1,166 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { + ApiClientFactory, + cleanupRepositoryTestData, + FavoritesPageApi, + NodesApi, + RepositoryTestData, + seedRepositoryTestData, + test, + TrashcanApi, + users, + Utils +} from '@alfresco/aca-playwright-shared'; +import { expect, Locator } from '@playwright/test'; + +test.describe('Repository — Info drawer expand routing', () => { + const admin = users.admin; + + interface ListViewLike { + navigate: (opts?: { remoteUrl?: string }) => Promise; + dataTable: { + spinnerWaitForReload: () => Promise; + getRowByName: (name: string) => Locator; + selectItems: (...names: string[]) => Promise; + }; + acaHeader: { viewDetails: Locator }; + infoDrawer: { expandDetailsButton: Locator }; + } + + const openAndExpandInfoDrawer = async (page: ListViewLike, folderUrl: string, fileName: string) => { + await page.navigate({ remoteUrl: folderUrl }); + await page.dataTable.spinnerWaitForReload(); + + await expect(page.dataTable.getRowByName(fileName)).toBeVisible(); + await page.dataTable.selectItems(fileName); + await page.acaHeader.viewDetails.click(); + + await expect(page.infoDrawer.expandDetailsButton).toBeVisible(); + await page.infoDrawer.expandDetailsButton.click(); + }; + + test.describe('signed in as admin', () => { + let userNodesApi: NodesApi; + let trashcanApi: TrashcanApi; + let favoritesApi: FavoritesPageApi; + let testData!: RepositoryTestData; + + test.beforeAll(async () => { + try { + userNodesApi = await NodesApi.initialize('admin'); + trashcanApi = await TrashcanApi.initialize('admin'); + favoritesApi = await FavoritesPageApi.initialize('admin'); + testData = await seedRepositoryTestData({ userNodesApi, adminNodesApi: userNodesApi }); + + await favoritesApi.addFavoriteById('file', testData.repoFile.id); + } catch (error) { + console.error(`beforeAll (admin) failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, admin.username, admin.password, 'beforeEach (admin) failed'); + }); + + test.afterAll(async () => { + await cleanupRepositoryTestData(testData, { adminNodesApi: userNodesApi }); + await Utils.deleteNodesSitesEmptyTrashcan(userNodesApi, trashcanApi, 'afterAll (admin) failed'); + }); + + test('[XAT-19602] Expand info drawer on a Repository file preserves /repository route', async ({ personalFiles }) => { + await openAndExpandInfoDrawer(personalFiles, `#/repository/${testData.repoFolder.id}`, testData.repoFile.name); + + await personalFiles.page.waitForURL(`**/repository/details/${testData.repoFile.id}**`); + const url = personalFiles.page.url(); + + expect(url, 'URL must be in the /repository details area').toContain(`/repository/details/${testData.repoFile.id}`); + expect(url, 'URL must not redirect to /personal-files').not.toContain('/personal-files/details/'); + await expect(personalFiles.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + + test('[XAT-19603] Expand info drawer from Favorites keeps a Repository file on /repository', async ({ favoritePage }) => { + await openAndExpandInfoDrawer(favoritePage, '#/favorites', testData.repoFile.name); + + await favoritePage.page.waitForURL(`**/repository/details/${testData.repoFile.id}**`); + const url = favoritePage.page.url(); + + expect(url, 'Repository file opened from Favorites must stay on /repository').toContain(`/repository/details/${testData.repoFile.id}`); + expect(url, 'Repository file must not leak into /personal-files').not.toContain('/personal-files/details/'); + await expect(favoritePage.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + }); + + test.describe('signed in as a personal user', () => { + const personalUser = `info-personal-${Utils.random()}`; + + let personalUserNodesApi: NodesApi; + let personalUserTrashcanApi: TrashcanApi; + let personalFile: { id: string; name: string }; + + test.beforeAll(async () => { + try { + const apiClientFactory = new ApiClientFactory(); + await apiClientFactory.setUpAcaBackend('admin'); + try { + await apiClientFactory.createUser({ username: personalUser }); + } catch (error) { + if (!String(error).includes('409')) { + throw error; + } + } + + personalUserNodesApi = await NodesApi.initialize(personalUser, personalUser); + personalUserTrashcanApi = await TrashcanApi.initialize(personalUser, personalUser); + const personalFileName = `personal-file-${Utils.random()}.txt`; + const created = (await personalUserNodesApi.createFile(personalFileName)).entry; + personalFile = { id: created.id, name: personalFileName }; + } catch (error) { + console.error(`beforeAll (personal user) failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, personalUser, personalUser, 'beforeEach (personal user) failed'); + }); + + test.afterAll(async () => { + await Utils.deleteNodesSitesEmptyTrashcan(personalUserNodesApi, personalUserTrashcanApi, 'afterAll (personal user) failed'); + }); + + test('[XAT-19604] Expand info drawer on a Personal file still routes to /personal-files (regression)', async ({ personalFiles }) => { + await openAndExpandInfoDrawer(personalFiles, '#/personal-files', personalFile.name); + + await personalFiles.page.waitForURL(`**/personal-files/details/${personalFile.id}**`); + const url = personalFiles.page.url(); + + expect(url, 'URL must be in the /personal-files details area').toContain(`/personal-files/details/${personalFile.id}`); + expect(url, 'Personal file must not leak into /repository').not.toContain('/repository/details/'); + await expect(personalFiles.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + }); +}); diff --git a/e2e/playwright/navigation/src/tests/repository-breadcrumb.e2e.ts b/e2e/playwright/navigation/src/tests/repository-breadcrumb.e2e.ts new file mode 100644 index 000000000..f9ccde854 --- /dev/null +++ b/e2e/playwright/navigation/src/tests/repository-breadcrumb.e2e.ts @@ -0,0 +1,100 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ApiClientFactory, NodesApi, test, TrashcanApi, users, Utils } from '@alfresco/aca-playwright-shared'; +import { expect } from '@playwright/test'; + +test.describe('Repository — Breadcrumb', () => { + const admin = users.admin; + + let adminNodesApi: NodesApi; + let trashcanApi: TrashcanApi; + + let repoRootName: string; + let repoRootId: string; + let subFolderName: string; + let subFolderId: string; + let leafFolderName: string; + let leafFolderId: string; + + test.beforeAll(async () => { + try { + const apiClientFactory = new ApiClientFactory(); + await apiClientFactory.setUpAcaBackend('admin'); + adminNodesApi = await NodesApi.initialize('admin'); + trashcanApi = await TrashcanApi.initialize('admin'); + + repoRootName = `repo-root-${Utils.random()}`; + subFolderName = `sub-folder-${Utils.random()}`; + leafFolderName = `leaf-folder-${Utils.random()}`; + + repoRootId = (await adminNodesApi.createFolder(repoRootName, '-root-')).entry.id; + subFolderId = (await adminNodesApi.createFolder(subFolderName, repoRootId)).entry.id; + leafFolderId = (await adminNodesApi.createFolder(leafFolderName, subFolderId)).entry.id; + } catch (error) { + console.error(`beforeAll failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, admin.username, admin.password, 'beforeEach failed'); + }); + + test.afterAll(async () => { + try { + await adminNodesApi.deleteNodes([repoRootId], true); + } catch (error) { + console.error(`Repository tree cleanup failed: ${error}`); + } + await Utils.deleteNodesSitesEmptyTrashcan(adminNodesApi, trashcanApi, 'afterAll failed'); + }); + + test('[XAT-19605] Breadcrumb for a nested Repository folder shows every ancestor and stays on /repository', async ({ personalFiles }) => { + await personalFiles.navigate({ remoteUrl: `#/repository/${leafFolderId}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + + const items = await personalFiles.breadcrumb.getAllItems(); + expect(items, 'Breadcrumb must contain every ancestor plus the current folder').toEqual([ + 'Repository', + repoRootName, + subFolderName, + leafFolderName + ]); + + await expect(personalFiles.breadcrumb.currentItem).toHaveText(leafFolderName); + + await personalFiles.breadcrumb.clickItem(subFolderName); + await personalFiles.dataTable.spinnerWaitForReload(); + + await personalFiles.page.waitForURL(`**/repository/${subFolderId}**`); + const url = personalFiles.page.url(); + expect(url, 'Breadcrumb navigation must stay on /repository').toContain(`/repository/${subFolderId}`); + expect(url, 'Breadcrumb navigation must NOT redirect to /personal-files').not.toContain('/personal-files/'); + + const trimmed = await personalFiles.breadcrumb.getAllItems(); + expect(trimmed).toEqual(['Repository', repoRootName, subFolderName]); + await expect(personalFiles.breadcrumb.currentItem).toHaveText(subFolderName); + }); +}); diff --git a/e2e/playwright/smoke-test/src/tests/repository/repository-smoke.e2e.ts b/e2e/playwright/smoke-test/src/tests/repository/repository-smoke.e2e.ts new file mode 100644 index 000000000..5d179c2d1 --- /dev/null +++ b/e2e/playwright/smoke-test/src/tests/repository/repository-smoke.e2e.ts @@ -0,0 +1,93 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { + ApiClientFactory, + cleanupRepositoryTestData, + NodesApi, + RepositoryTestData, + seedRepositoryTestData, + test, + TrashcanApi, + users, + Utils +} from '@alfresco/aca-playwright-shared'; +import { expect } from '@playwright/test'; + +test.describe('Repository — smoke test', () => { + const admin = users.admin; + + let userNodesApi: NodesApi; + let trashcanApi: TrashcanApi; + let testData!: RepositoryTestData; + let smokeFileName: string; + let smokeFileId: string; + + test.beforeAll(async () => { + try { + const apiClientFactory = new ApiClientFactory(); + await apiClientFactory.setUpAcaBackend('admin'); + userNodesApi = await NodesApi.initialize('admin'); + trashcanApi = await TrashcanApi.initialize('admin'); + testData = await seedRepositoryTestData({ userNodesApi, adminNodesApi: userNodesApi }); + + smokeFileName = `smoke-repo-${Utils.random()}.txt`; + smokeFileId = (await userNodesApi.createFile(smokeFileName, testData.repoFolder.id)).entry.id; + } catch (error) { + console.error(`beforeAll failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, admin.username, admin.password, 'beforeEach failed'); + }); + + test.afterAll(async () => { + await cleanupRepositoryTestData(testData, { adminNodesApi: userNodesApi }); + await Utils.deleteNodesSitesEmptyTrashcan(userNodesApi, trashcanApi, 'afterAll failed'); + }); + + test('[XAT-19607] Sequential Repository actions never leak to /personal-files', async ({ personalFiles }) => { + await personalFiles.navigate({ remoteUrl: `#/repository/${testData.repoFolder.id}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + await expect(personalFiles.dataTable.getRowByName(smokeFileName)).toBeVisible(); + + await personalFiles.dataTable.selectItems(smokeFileName); + await personalFiles.acaHeader.viewDetails.click(); + await personalFiles.infoDrawer.expandDetailsButton.click(); + await personalFiles.page.waitForURL(`**/repository/details/${smokeFileId}**`); + expect(personalFiles.page.url()).toContain(`/repository/details/${smokeFileId}`); + + await personalFiles.navigate({ remoteUrl: `#/repository/${testData.repoFolder.id}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + expect(personalFiles.page.url(), 'Should be back on /repository, not /personal-files').toContain(`/repository/${testData.repoFolder.id}`); + + await personalFiles.dataTable.getRowByName(smokeFileName).click({ button: 'right' }); + await personalFiles.pagination.clickMenuItem('Permissions'); + await personalFiles.page.waitForURL(`**/repository/details/${smokeFileId}/permissions`); + expect(personalFiles.page.url()).toContain(`/repository/details/${smokeFileId}/permissions`); + expect(personalFiles.page.url()).not.toContain('/personal-files/details/'); + }); +}); diff --git a/e2e/playwright/special-permissions-actions-available/src/tests/repository-permissions.e2e.ts b/e2e/playwright/special-permissions-actions-available/src/tests/repository-permissions.e2e.ts new file mode 100644 index 000000000..942417437 --- /dev/null +++ b/e2e/playwright/special-permissions-actions-available/src/tests/repository-permissions.e2e.ts @@ -0,0 +1,170 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { + ApiClientFactory, + cleanupRepositoryTestData, + NodesApi, + PersonalFilesPage, + RepositoryTestData, + seedRepositoryTestData, + SharedLinksApi, + test, + timeouts, + TrashcanApi, + users, + Utils +} from '@alfresco/aca-playwright-shared'; +import { expect } from '@playwright/test'; + +test.describe('Repository — Permissions routing', () => { + const admin = users.admin; + + const openPermissionsFromContextMenu = async (page: PersonalFilesPage, fileName: string) => { + await expect(page.dataTable.getRowByName(fileName)).toBeVisible({ timeout: timeouts.large }); + await page.dataTable.getRowByName(fileName).click({ button: 'right' }); + await page.pagination.clickMenuItem('Permissions'); + }; + + test.describe('signed in as admin', () => { + let userNodesApi: NodesApi; + let trashcanApi: TrashcanApi; + let sharedLinksApi: SharedLinksApi; + let testData!: RepositoryTestData; + + test.beforeAll(async () => { + try { + userNodesApi = await NodesApi.initialize('admin'); + trashcanApi = await TrashcanApi.initialize('admin'); + sharedLinksApi = await SharedLinksApi.initialize('admin'); + testData = await seedRepositoryTestData({ userNodesApi, adminNodesApi: userNodesApi }); + await sharedLinksApi.shareFileById(testData.repoFile.id); + } catch (error) { + console.error(`beforeAll (admin) failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, admin.username, admin.password, 'beforeEach (admin) failed'); + }); + + test.afterAll(async () => { + await cleanupRepositoryTestData(testData, { adminNodesApi: userNodesApi }); + await Utils.deleteNodesSitesEmptyTrashcan(userNodesApi, trashcanApi, 'afterAll (admin) failed'); + }); + + test('[XAT-19608] Permissions from context menu on Repository preserves the /repository route', async ({ personalFiles }) => { + await personalFiles.navigate({ remoteUrl: `#/repository/${testData.repoFolder.id}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + + await openPermissionsFromContextMenu(personalFiles, testData.repoFile.name); + + await personalFiles.page.waitForURL(`**/repository/details/${testData.repoFile.id}/permissions`); + expect(personalFiles.page.url()).toContain(`/repository/details/${testData.repoFile.id}/permissions`); + expect(personalFiles.page.url(), 'URL must not redirect to /personal-files').not.toContain('/personal-files/details/'); + await expect(personalFiles.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + + test('[XAT-19609] Permissions from toolbar on Repository preserves the /repository route', async ({ personalFiles }) => { + await personalFiles.navigate({ remoteUrl: `#/repository/${testData.repoFolder.id}` }); + await personalFiles.dataTable.spinnerWaitForReload(); + + await expect(personalFiles.dataTable.getRowByName(testData.repoFile.name)).toBeVisible({ timeout: timeouts.large }); + await personalFiles.dataTable.selectItems(testData.repoFile.name); + await personalFiles.clickMoreActionsButton('Permissions'); + + await personalFiles.page.waitForURL(`**/repository/details/${testData.repoFile.id}/permissions`); + expect(personalFiles.page.url()).toContain(`/repository/details/${testData.repoFile.id}/permissions`); + expect(personalFiles.page.url(), 'URL must not redirect to /personal-files').not.toContain('/personal-files/details/'); + await expect(personalFiles.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + + test('[XAT-19610] Permissions from Shared keeps a Repository file on /repository', async ({ sharedPage }) => { + await sharedPage.navigate(); + await sharedPage.dataTable.spinnerWaitForReload(); + + await expect(sharedPage.dataTable.getRowByName(testData.repoFile.name)).toBeVisible({ timeout: timeouts.large }); + await sharedPage.dataTable.getRowByName(testData.repoFile.name).click({ button: 'right' }); + await sharedPage.matMenu.clickMenuItem('Permissions'); + + await sharedPage.page.waitForURL(/\/repository\/details\/[^/]+\/permissions/); + const url = sharedPage.page.url(); + expect(url, 'Repository file opened from Shared must stay on /repository').toMatch(/\/repository\/details\/[^/]+\/permissions/); + expect(url, 'Repository file must not leak into /personal-files').not.toContain('/personal-files/details/'); + await expect(sharedPage.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + }); + + test.describe('signed in as a personal user', () => { + const personalUser = `perm-personal-${Utils.random()}`; + + let personalUserNodesApi: NodesApi; + let personalUserTrashcanApi: TrashcanApi; + let personalFile: { id: string; name: string }; + + test.beforeAll(async () => { + try { + const apiClientFactory = new ApiClientFactory(); + await apiClientFactory.setUpAcaBackend('admin'); + try { + await apiClientFactory.createUser({ username: personalUser }); + } catch (error) { + if (!String(error).includes('409')) { + throw error; + } + } + + personalUserNodesApi = await NodesApi.initialize(personalUser, personalUser); + personalUserTrashcanApi = await TrashcanApi.initialize(personalUser, personalUser); + const personalFileName = `personal-file-${Utils.random()}.txt`; + const created = (await personalUserNodesApi.createFile(personalFileName)).entry; + personalFile = { id: created.id, name: personalFileName }; + } catch (error) { + console.error(`beforeAll (personal user) failed: ${error}`); + throw error; + } + }); + + test.beforeEach(async ({ loginPage }) => { + await Utils.tryLoginUser(loginPage, personalUser, personalUser, 'beforeEach (personal user) failed'); + }); + + test.afterAll(async () => { + await Utils.deleteNodesSitesEmptyTrashcan(personalUserNodesApi, personalUserTrashcanApi, 'afterAll (personal user) failed'); + }); + + test('[XAT-19611] Permissions for a Personal File still routes to /personal-files (regression)', async ({ personalFiles }) => { + await personalFiles.navigate(); + await personalFiles.dataTable.spinnerWaitForReload(); + + await openPermissionsFromContextMenu(personalFiles, personalFile.name); + + await personalFiles.page.waitForURL(`**/personal-files/details/${personalFile.id}/permissions`); + expect(personalFiles.page.url()).toContain(`/personal-files/details/${personalFile.id}/permissions`); + expect(personalFiles.page.url(), 'Personal file must not leak into /repository').not.toContain('/repository/details/'); + await expect(personalFiles.infoDrawer.expandedDetailsPermissionsTab).toBeVisible(); + }); + }); +}); diff --git a/projects/aca-playwright-shared/src/page-objects/components/dialogs/content-node-selector-dialog.ts b/projects/aca-playwright-shared/src/page-objects/components/dialogs/content-node-selector-dialog.ts index af4002090..1aa2a7ad2 100755 --- a/projects/aca-playwright-shared/src/page-objects/components/dialogs/content-node-selector-dialog.ts +++ b/projects/aca-playwright-shared/src/page-objects/components/dialogs/content-node-selector-dialog.ts @@ -33,6 +33,7 @@ export class ContentNodeSelectorDialog extends BaseComponent { public cancelButton = this.getChild('[data-automation-id="content-node-selector-actions-cancel"]'); public actionButton = this.getChild('[data-automation-id="content-node-selector-actions-choose"]'); public locationDropDown = this.getChild('[id="site-dropdown-container"] .adf-sites-dropdown-form-field'); + public searchInput = this.getChild('[data-automation-id="content-node-selector-search-input"]'); getOptionLocator = (optionName: string): Locator => this.page.locator('[role=listbox] [role=option]', { hasText: optionName }).first(); getDialogTitle = (text: string) => this.getChild('[data-automation-id="content-node-selector-title"]', { hasText: text }); @@ -74,4 +75,11 @@ export class ContentNodeSelectorDialog extends BaseComponent { timeout: 20_000 }); } + + async searchAndSelectDestination(folderName: string): Promise { + await expect(this.searchInput).toBeVisible(); + await this.searchInput.fill(folderName); + await this.spinnerWaitForReload(); + await this.selectDestination(folderName); + } } diff --git a/projects/aca-playwright-shared/src/utils/index.ts b/projects/aca-playwright-shared/src/utils/index.ts index 89588a447..8b7d09bf0 100644 --- a/projects/aca-playwright-shared/src/utils/index.ts +++ b/projects/aca-playwright-shared/src/utils/index.ts @@ -32,3 +32,4 @@ export * from './config'; export * from './error-strings'; export * from './api'; export * from './logger'; +export * from './repository-test-data'; diff --git a/projects/aca-playwright-shared/src/utils/repository-test-data.ts b/projects/aca-playwright-shared/src/utils/repository-test-data.ts new file mode 100644 index 000000000..0084b899d --- /dev/null +++ b/projects/aca-playwright-shared/src/utils/repository-test-data.ts @@ -0,0 +1,73 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { NodesApi } from '../api'; +import { Utils } from './utils'; + +export interface RepositoryTestData { + repoFolder: { id: string; name: string }; + repoFile: { id: string; name: string }; + personalFile: { id: string; name: string }; +} + +export interface RepositoryTestDataApis { + userNodesApi: NodesApi; + adminNodesApi: NodesApi; +} + +export async function seedRepositoryTestData(apis: RepositoryTestDataApis): Promise { + const { userNodesApi, adminNodesApi } = apis; + + const repoFolderName = `repo-folder-${Utils.random()}`; + const repoFileName = `repo-file-${Utils.random()}.txt`; + const personalFileName = `personal-file-${Utils.random()}.txt`; + + const repoFolder = (await adminNodesApi.createFolder(repoFolderName, '-root-')).entry; + const repoFile = (await adminNodesApi.createFile(repoFileName, repoFolder.id)).entry; + const personalFile = (await userNodesApi.createFile(personalFileName)).entry; + + return { + repoFolder: { id: repoFolder.id, name: repoFolderName }, + repoFile: { id: repoFile.id, name: repoFileName }, + personalFile: { id: personalFile.id, name: personalFileName } + }; +} + +export async function cleanupRepositoryTestData( + testData: RepositoryTestData | undefined, + apis: Pick +): Promise { + if (!testData) { + return; + } + const { adminNodesApi } = apis; + + if (testData.personalFile?.id) { + await adminNodesApi.deleteNodes([testData.personalFile.id], true); + } + + if (testData.repoFolder?.id) { + await adminNodesApi.deleteNodes([testData.repoFolder.id], true); + } +}