diff --git a/cypress/e2e/linked-data/manage-profile-settings/drag-drop.cy.js b/cypress/e2e/linked-data/manage-profile-settings/drag-drop.cy.js new file mode 100644 index 0000000000..2fc7880d64 --- /dev/null +++ b/cypress/e2e/linked-data/manage-profile-settings/drag-drop.cy.js @@ -0,0 +1,381 @@ +import TopMenu from '../../../support/fragments/topMenu'; +import Marigold from '../../../support/fragments/linked-data/marigold'; +import WorkProfileModal from '../../../support/fragments/linked-data/workProfileModal'; +import EditResource from '../../../support/fragments/linked-data/editResource'; +import { MARIGOLD_ROLES } from '../../../support/constants'; +import Users from '../../../support/fragments/users/users'; +import Permissions from '../../../support/dictionary/permissions'; +import ManageProfileSettings from '../../../support/fragments/linked-data/manageProfileSettings'; + +let user; +const roleNames = [MARIGOLD_ROLES.CATALOGER, MARIGOLD_ROLES.CATALOGER_MARIGOLD]; +const profileUris = [ + 'http://bibfra.me/vocab/lite/Hub', + 'http://bibfra.me/vocab/lite/Instance', + 'http://bibfra.me/vocab/lite/Work', +]; + +// Assumes a viewport resolution greater than 720; less than 720, +// responsiveness adds a new screen to move between profiles list +// and settings editor. + +describe('Manage profile settings', () => { + before('Create test data', () => { + const roleIds = []; + cy.getAdminToken(); + + roleNames.forEach((roleName) => { + cy.getUserRoleIdByNameApi(roleName).then((roleId) => { + if (roleId) { + roleIds.push(roleId); + } + }); + }); + + cy.createTempUser([ + Permissions.linkedDataDeletePreferredProfile.gui, + ]).then((userProperties) => { + user = userProperties; + }); + + cy.then(() => { + if (roleIds.length > 0) { + cy.updateRolesForUserApi(user.userId, roleIds); + } + }); + }); + + after('Delete test data', () => { + cy.getAdminToken(); + + Users.deleteViaApi(user.userId); + }); + + beforeEach(() => { + cy.login(user.username, user.password, { + path: TopMenu.linkedDataEditor, + waiter: Marigold.waitLoading, + authRefresh: true, + }); + }); + + afterEach(() => { + // Clear preferred profiles and reset profile settings to basic inactive state + profileUris.forEach((uri) => { + cy.getPreferredProfileForUser(uri).then((profiles) => { + if (profiles.length > 0) { + cy.deletePreferredProfileForUser(uri); + } + }); + cy.getProfileMetadataByResourceType(uri).then((profiles) => { + profiles.forEach((profile) => { + cy.setProfileSettingsForUser(profile.id, { + active: false, + children: [], + }); + }); + }); + }); + }); + + it( + 'settings are applied to Marigold editor', + { tags: ['citation'] }, + () => { + // Modify settings + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + ManageProfileSettings.selectProfile('Books'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:LanguageCode'); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:LanguageCode'); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:CreatorOfWork'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:CreatorOfWork', 2); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:CreatorOfWork'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:CreatorOfWork', 3); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:CreatorOfWork'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:CreatorOfWork', 4); + ManageProfileSettings.saveAndClose(); + ManageProfileSettings.modalUnusedSave(); + + // Check that settings apply, and also toggle profile default + Marigold.waitLoading(); + // TODO - bug, the menu bar is missing after closing profile settings editor; remove when merged + cy.xpath('//button[@data-testid="resources-actions-dropdown"]').focus(); + Marigold.openNewResourceForm(); + WorkProfileModal.waitLoading(); + WorkProfileModal.checkOptionSelected('Books'); + WorkProfileModal.toggleDefaultProfile(); + WorkProfileModal.selectDefaultOption(); + EditResource.waitLoading(); + EditResource.checkSectionIsNotVisible('Profile::0__Work::0___languages::0'); + EditResource.checkSectionInPosition('Profile::0__Work::0___creatorReference::0', 4); + EditResource.clickCloseResourceButton(); + + Marigold.waitLoading(); + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + ManageProfileSettings.selectProfile('Books'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.verifyPreferredProfile(true); + ManageProfileSettings.verifyCustomSettingsSelected(); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:LanguageCode'); + ManageProfileSettings.selectDefaultSettings(); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:IntendedAudience'); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:IntendedAudience'); + ManageProfileSettings.saveAndClose(); + ManageProfileSettings.modalUnusedSave(); + + // Check that modifications applied + Marigold.waitLoading(); + // TODO - bug, the menu bar is missing after closing profile settings editor; remove when merged + cy.xpath('//button[@data-testid="resources-actions-dropdown"]').focus(); + Marigold.openNewResourceForm(); + // No modal this time, default was chosen + EditResource.waitLoading(); + EditResource.checkSectionInPosition('Profile::0__Work::0___languages::0', 17); + EditResource.checkSectionIsNotVisible('Profile::0__Work::0__targetAudience::0'); + }, + ); + + it( + 'changes to settings persist', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Serials Work'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.togglePreferredProfile(); + + ManageProfileSettings.dragSelectedToUnusedContainer('Profile:Work:ContentsNote'); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:ContentsNote'); + ManageProfileSettings.dragSelectedToUnusedContainer('Profile:Work:SummaryNote'); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:SummaryNote'); + ManageProfileSettings.dragReorderSelectedComponent('Profile:Work:TitleInformation', 'Profile:Work:CreatorOfWork'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:TitleInformation', 1); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:Hubs'); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:Hubs'); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:Hubs'); + ManageProfileSettings.nudgeComponentDownButton('Profile:Work:Hubs'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:Hubs', 7); + ManageProfileSettings.nudgeComponentUpButton('Profile:Work:ClassificationNumbers'); + ManageProfileSettings.nudgeComponentUpButton('Profile:Work:ClassificationNumbers'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:ClassificationNumbers', 12); + + // Save settings, swap to a different profile, then return to see that settings persisted. + ManageProfileSettings.saveAndKeepEditing(); + ManageProfileSettings.modalUnusedSave(); + ManageProfileSettings.selectProfile('Books'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.verifyPreferredProfile(false); + + ManageProfileSettings.selectProfile('Serials Work'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.verifyPreferredProfile(true); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:ContentsNote'); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:SummaryNote'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:TitleInformation', 1); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:Hubs', 7); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:ClassificationNumbers', 12); + }, + ); + + it( + 'required components cannot be moved to unused list', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Serials'); + + // TitleInformation is a required field and cannot be hidden + ManageProfileSettings.keyboardDragSelectedToUnused('Profile:Instance:TitleInformation'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Instance:TitleInformation', 1); + + ManageProfileSettings.dragSelectedToUnusedContainer('Profile:Instance:TitleInformation'); + // Cypress may end up reordering, but the overall point is that it didn't move to unused. + ManageProfileSettings.verifySelectedComponent('Profile:Instance:TitleInformation'); + + ManageProfileSettings.moveComponentUnavailable('Profile:Instance:TitleInformation'); + ManageProfileSettings.verifySelectedComponent('Profile:Instance:TitleInformation'); + }, + ); + + it( + 'keyboard reordering and moving between lists', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Books'); + ManageProfileSettings.waitEditorLoading(); + + ManageProfileSettings.keyboardDragSelectedComponentAndCancel(3); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:Hubs', 3); + + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:DateOfWork'); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:LanguageCode'); + ManageProfileSettings.keyboardDragUnusedComponentAndCancel(2); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Work:LanguageCode', 2); + + ManageProfileSettings.selectDefaultSettings(); + ManageProfileSettings.keyboardDragReorderSelectedComponent('Profile:Work:ContentType', 'Profile:Work:SupplementaryContent'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:ContentType', 6); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Work:SupplementaryContent', 7); + + ManageProfileSettings.selectDefaultSettings(); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:DateOfWork'); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:LanguageCode'); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Work:ContentType'); + ManageProfileSettings.keyboardDragReorderUnusedComponent('Profile:Work:ContentType', 'Profile:Work:DateOfWork'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Work:ContentType', 1); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Work:LanguageCode', 3); + + ManageProfileSettings.keyboardDragSelectedToUnused('Profile:Work:Hubs'); + ManageProfileSettings.verifyUnusedComponent('Profile:Work:Hubs'); + + ManageProfileSettings.keyboardDragUnusedToSelected('Profile:Work:DateOfWork'); + ManageProfileSettings.verifySelectedComponent('Profile:Work:DateOfWork'); + }, + ); + + it( + 'mouse reordering and dragging between lists', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Hubs'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.dragReorderSelectedComponent('Profile:Hub:CreatorOfHub', 'Profile:Hub:LanguageCode'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:CreatorOfHub', 3); + + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Hub:LanguageCode'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:LanguageCode', 2); + ManageProfileSettings.dragReorderUnusedComponent('Profile:Hub:CreatorOfHub', 'Profile:Hub:LanguageCode'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 2); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:LanguageCode', 1); + + ManageProfileSettings.dragUnusedToSelected('Profile:Hub:LanguageCode', 'Profile:Hub:TitleInformation'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:LanguageCode', 1); + + ManageProfileSettings.dragSelectedToUnused('Profile:Hub:LanguageCode', 'Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:LanguageCode', 1); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:TitleInformation', 1); + + ManageProfileSettings.selectDefaultSettings(); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:TitleInformation', 2); + ManageProfileSettings.dragSelectedToUnusedContainer('Profile:Hub:LanguageCode'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:LanguageCode', 1); + ManageProfileSettings.dragSelectedToUnusedContainer('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + + ManageProfileSettings.dragUnusedToUndroppableRegion('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + + ManageProfileSettings.selectDefaultSettings(); + ManageProfileSettings.dragSelectedToUndroppableRegion('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:CreatorOfHub', 1); + }, + ); + + it( + 'move component to other list by button press', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Hubs'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.dragSelectedComponentAndCancel(1); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:CreatorOfHub', 1); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Hub:CreatorOfHub', 3); + + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Hub:CreatorOfHub'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + ManageProfileSettings.moveComponentToOppositeListButton('Profile:Hub:LanguageCode'); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:LanguageCode', 2); + ManageProfileSettings.dragUnusedComponentAndCancel(1); + ManageProfileSettings.verifyUnusedComponentPosition('Profile:Hub:CreatorOfHub', 1); + + ManageProfileSettings.saveAndKeepEditing(); + ManageProfileSettings.verifyModalUnusedOpen(); + ManageProfileSettings.modalUnusedClose(); + ManageProfileSettings.saveAndKeepEditing(); + ManageProfileSettings.verifyModalUnusedOpen(); + ManageProfileSettings.modalUnusedCancel(); + ManageProfileSettings.saveAndKeepEditing(); + ManageProfileSettings.verifyModalUnusedOpen(); + ManageProfileSettings.modalUnusedSave(); + }, + ); + + it( + 'nudge buttons in selected component list', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Rare Books'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Instance:StatementOfResponsibility', 2); + ManageProfileSettings.nudgeComponentUpButton('Profile:Instance:StatementOfResponsibility'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Instance:StatementOfResponsibility', 1); + ManageProfileSettings.nudgeComponentDownButton('Profile:Instance:StatementOfResponsibility'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Instance:StatementOfResponsibility', 2); + ManageProfileSettings.nudgeComponentDownButton('Profile:Instance:StatementOfResponsibility'); + ManageProfileSettings.verifySelectedComponentPosition('Profile:Instance:StatementOfResponsibility', 3); + }, + ); + + it( + 'verifies preferred profile setting persists across profile selection changes', + { tags: ['citation'] }, + () => { + Marigold.openManageProfileSettings(); + ManageProfileSettings.waitMainLoading(); + ManageProfileSettings.waitProfilesLoading(); + + ManageProfileSettings.selectProfile('Rare Books'); + ManageProfileSettings.waitEditorLoading(); + + ManageProfileSettings.verifyPreferredProfile(false); + ManageProfileSettings.togglePreferredProfile(); + ManageProfileSettings.verifyPreferredProfile(true); + + ManageProfileSettings.selectProfile('Hubs'); + ManageProfileSettings.verifyModalUnsavedOpen(); + ManageProfileSettings.modalUnsavedClose(); + ManageProfileSettings.selectProfile('Hubs'); + ManageProfileSettings.verifyModalUnsavedOpen(); + ManageProfileSettings.modalUnsavedContinueWithSaving(); + + ManageProfileSettings.selectProfile('Rare Books'); + ManageProfileSettings.waitEditorLoading(); + ManageProfileSettings.verifyPreferredProfile(true); + ManageProfileSettings.togglePreferredProfile(); + ManageProfileSettings.verifyPreferredProfile(false); + }, + ); +}); diff --git a/cypress/support/api/linkedDataEditor.js b/cypress/support/api/linkedDataEditor.js index 62e29b7dcf..a50492d88e 100644 --- a/cypress/support/api/linkedDataEditor.js +++ b/cypress/support/api/linkedDataEditor.js @@ -1,12 +1,64 @@ -Cypress.Commands.add('setPrefferedProfileForUser', () => { - // id is hardcoded as 3 on purpose - API will automatically identify the user ID from the folioAccessToken cookie. - const prefferedProfile = { - id: 3, - resourceType: 'http://bibfra.me/vocab/lite/Instance', +// For *ForUser commands, the API will automatically identify the +// user ID from the folioAccessToken cookie. + +Cypress.Commands.add('getProfileMetadataByResourceType', (resourceTypeURL) => { + return cy.okapiRequest({ + method: 'GET', + path: `linked-data/profile/metadata?resourceType=${resourceTypeURL}`, + isDefaultSearchParamsRequired: false, + }).then((response) => response.body); +}); + +Cypress.Commands.add('getAllPreferredProfilesForUser', () => { + return cy.okapiRequest({ + method: 'GET', + path: 'linked-data/profile/preferred', + isDefaultSearchParamsRequired: false, + }).then((response) => response.body); +}); + +Cypress.Commands.add('getPreferredProfileForUser', (resourceTypeURL) => { + return cy.okapiRequest({ + method: 'GET', + path: `linked-data/profile/preferred?resourceType=${resourceTypeURL}`, + isDefaultSearchParamsRequired: false, + }).then((response) => response.body); +}); + +Cypress.Commands.add('setPreferredProfileForUser', (id, resourceTypeURL) => { + const preferredProfile = { + id, + resourceType: resourceTypeURL, }; cy.okapiRequest({ method: 'POST', path: 'linked-data/profile/preferred', - body: prefferedProfile, + body: preferredProfile, + isDefaultSearchParamsRequired: false, + }); +}); + +Cypress.Commands.add('deletePreferredProfileForUser', (resourceTypeURL) => { + cy.okapiRequest({ + method: 'DELETE', + path: `linked-data/profile/preferred?resourceType=${resourceTypeURL}`, + isDefaultSearchParamsRequired: false, + }); +}); + +Cypress.Commands.add('getProfileSettingsForUser', (id) => { + return cy.okapiRequest({ + method: 'GET', + path: `linked-data/profile/settings/${id}`, + isDefaultSearchParamsRequired: false, + }).then((response) => response.body); +}); + +Cypress.Commands.add('setProfileSettingsForUser', (id, settings) => { + cy.okapiRequest({ + method: 'POST', + path: `linked-data/profile/settings/${id}`, + body: JSON.stringify(settings), + isDefaultSearchParamsRequired: false, }); }); diff --git a/cypress/support/dictionary/permissions.js b/cypress/support/dictionary/permissions.js index 3586d07b2f..0d4b03b503 100644 --- a/cypress/support/dictionary/permissions.js +++ b/cypress/support/dictionary/permissions.js @@ -1516,6 +1516,11 @@ export default { internal: 'ui-users.settings.departments.all', gui: 'Settings (Users): Can create, edit, view, and delete departments', }, + // Linked Data Editor + linkedDataDeletePreferredProfile: { + internal: 'linked-data.profiles.preferred.delete', + gui: 'Linked Data: Delete the preferred profile for a resource type for the current user', + }, ebsconetAll: { internal: 'ebsconet.all', diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index b0e3cb5c54..b2af54f5f7 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -18,6 +18,7 @@ import './inventory'; import './users'; import 'cypress-file-upload'; import 'cypress-recurse/commands'; +import 'cypress-real-events/support'; import './commands'; registerCypressGrep(); diff --git a/cypress/support/fragments/linked-data/assignAuthorityHubModal.js b/cypress/support/fragments/linked-data/assignAuthorityHubModal.js index a7918c56e6..0666d28a6a 100644 --- a/cypress/support/fragments/linked-data/assignAuthorityHubModal.js +++ b/cypress/support/fragments/linked-data/assignAuthorityHubModal.js @@ -1,6 +1,6 @@ import { Button, TextInput } from '../../../../interactors'; -const MARCAuthorityModal = "//div[@data-testid='modal']"; +const MARCAuthorityModal = "//dialog[@data-testid='modal']"; const searchOption = Button({ dataTestID: 'id-search-segment-button-authorities:search' }); const searchInput = TextInput({ id: 'id-search-textarea' }); const searchButton = Button({ dataTestID: 'id-search-button' }); diff --git a/cypress/support/fragments/linked-data/closeResourceModal.js b/cypress/support/fragments/linked-data/closeResourceModal.js index f4f165fcaa..4aebf2d5b1 100644 --- a/cypress/support/fragments/linked-data/closeResourceModal.js +++ b/cypress/support/fragments/linked-data/closeResourceModal.js @@ -1,6 +1,6 @@ const closeResourceModal = - "//div[@data-testid='modal']//h3[@class='title' and text()='Close resource']"; -const closeResourceModalByTestId = "//div[@data-testid='modal']"; + "//dialog[@data-testid='modal']//h3[@class='title' and text()='Close resource']"; +const closeResourceModalByTestId = "//dialog[@data-testid='modal']"; const modalContent = "//div[@data-testid='modal-close-record-content']"; const closeButton = "//button[@class='close-button']"; const yesButton = "//button[@data-testid='modal-button-cancel']"; diff --git a/cypress/support/fragments/linked-data/editResource.js b/cypress/support/fragments/linked-data/editResource.js index 27c2ab7c37..b4526bec93 100644 --- a/cypress/support/fragments/linked-data/editResource.js +++ b/cypress/support/fragments/linked-data/editResource.js @@ -253,6 +253,18 @@ export default { .should('be.visible'); }, + checkSectionIsNotVisible(section) { + cy.xpath(`//div[@id="${section}"]`).should('not.be.visible'); + }, + + // 1-indexed + checkSectionInPosition(section, position) { + cy.xpath('//div[@id="edit-section"]/div[contains(@class,"edit-section-group")]/div[1]/div[1]') + .eq(position - 1) + .invoke('attr', 'id') + .should('eq', section); + }, + clickCloseResourceButton() { cy.do(Button({ dataTestID: 'nav-close-button' }).click()); cy.wait(500); diff --git a/cypress/support/fragments/linked-data/instanceProfileModal.js b/cypress/support/fragments/linked-data/instanceProfileModal.js index 2ab9c598e7..88c61a828d 100644 --- a/cypress/support/fragments/linked-data/instanceProfileModal.js +++ b/cypress/support/fragments/linked-data/instanceProfileModal.js @@ -1,4 +1,4 @@ -const instanceProfileModal = 'div[class="modal modal-choose-profile"]'; +const instanceProfileModal = 'dialog[class="modal modal-choose-profile"]'; export default { waitLoading() { diff --git a/cypress/support/fragments/linked-data/manageProfileSettings.js b/cypress/support/fragments/linked-data/manageProfileSettings.js new file mode 100644 index 0000000000..83f157a1dd --- /dev/null +++ b/cypress/support/fragments/linked-data/manageProfileSettings.js @@ -0,0 +1,498 @@ +const manageProfileSettingsPage = "//main[@data-testid='manage-profile-settings']"; +const profilesSection = "//section[@data-testid='profiles-list']"; +const editorSection = "//div[@data-testid='profile-settings-editor']"; + +const saveAndCloseButton = "//button[@data-testid='save-and-close']"; +const saveAndKeepEditingButton = "//button[@data-testid='save-and-keep-editing']"; +const closeButton = "//button[@data-testid='nav-close-button']"; + +const preferredProfileCheckbox = "//input[@data-testid='type-default-setting']"; +const defaultProfileSettingsRadio = "//input[@data-testid='settings-active-default']"; +const customProfileSettingsRadio = "//input[@data-testid='settings-active-custom']"; + +const selectedList = "//section[@data-testid='selected-component-list']"; +const unusedList = "//section[@data-testid='unused-component-list']"; +const unusedContainer = "//div[@data-droppable-id='unused-container']"; +const profileComponent = 'div.component'; +const draggingComponent = 'div.drag-overlay'; +const componentActivateMenu = "//button[@data-testid='activate-menu']"; +const componentMoveAction = "//button[@data-testid='move-action']"; +const componentNudgeUp = "//button[@data-testid='nudge-up']"; +const componentNudgeDown = "//button[@data-testid='nudge-down']"; + +const modalUnsaved = "//dialog[@data-testid='modal-close-profile-settings']"; +const modalUnused = "//dialog[@data-testid='modal-save-unused-profile-components']"; +const modalCloseButton = ".//button[contains(@class, 'close-button')]"; +const modalCancelButton = ".//button[@data-testid='modal-button-cancel']"; +const modalSubmitButton = ".//button[@data-testid='modal-button-submit']"; +const loaderOverlay = 'div.loader-overlay'; + +const component = (id, selector, sub) => { + return `${sub ? '.' : ''}//div[@data-testid='component-${id}']${selector}`; +}; + +const center = (el) => { + const r = el.getBoundingClientRect(); + return { x: Math.floor(r.left + r.width / 2), y: Math.floor(r.top + r.height / 2) }; +}; + +const doDrag = (node, endCoords) => { + // The dnd-kit DragOverlay makes things work more smoothly, but in + // this context it adds an extra burden of replacing the original + // object with the drag overlay, which may have a markedly different + // position compared to the original object. We need to calculate + // the difference between them in order to move the overlay over the + // the target object. The mouse pointer does not actually move, so we + // have to acquire the position of the overlay to make the coordinate + // calculations. + cy.wrap(node) + .realMouseMove(0, 0, { position: 'center', steps: 6 }) + .realMouseDown({ button: 'left' }) + .realMouseMove(0, 1, { position: 'center', steps: 2 }) + .then(($el) => { + const doc = $el[0].ownerDocument; + const overlay = doc.querySelector('.drag-overlay'); + const ov = center(overlay); + + const dx = endCoords.x - ov.x; + const dy = endCoords.y - ov.y; + + return { dx, dy }; + }) + .then(({ dx, dy }) => { + cy.wrap(node) + .realMouseMove(dx, dy, { position: 'center', steps: 14 }) + .wait(500) + .realMouseUp({ button: 'left' }); + }); + cy.get(draggingComponent).should('not.exist') + .wait(1000); +}; + +const dragDropGeneral = (dragTarget, dragTargetList, dropTarget) => { + cy.xpath(dragTargetList) + .xpath(component(dragTarget, '', true)).then((drag) => { + cy.xpath(dropTarget).then((drop) => { + const end = center(drop[0]); + doDrag(drag, end); + }); + }); +}; + +const dragDropList = (dragTarget, dragTargetList, dropTarget, dropTargetList) => { + cy.xpath(dragTargetList) + .xpath(component(dragTarget, '', true)).then((drag) => { + cy.xpath(dropTargetList) + .xpath(component(dropTarget, '', true)).then((drop) => { + const end = center(drop[0]); + doDrag(drag, end); + }); + }); +}; + +const getComponentPosition = (id, list) => { + return cy.xpath(list) + .find(profileComponent) + .then((els) => { + const idx = els.toArray().findIndex((el) => { + const val = el.getAttribute('data-testid'); + return val === `component-${id}`; + }); + return idx; + }); +}; + +const getListLength = (list) => { + return cy + .xpath(list) + .find(profileComponent) + .its('length') + .then((count) => { + return count; + }); +}; + +const keyBackToStartingPosition = (list, listLength, initialPosition) => { + // Keyboard interactions suffer from the same recalibrating overlay + // position issue as mouse, and since the mouse isn't involved, it can't be + // rectified in the same way with a coordinate diff caluclation. But + // switching (non-required) fields between lists should return the horizontal + // positioning, then resetting with up arrows to the top of the list + // before finally using down arrows to reach the original position + // should help put it into the correct original starting position. + + if (list === 'selected') { + cy.realPress('ArrowLeft') + .realPress('ArrowRight'); + } else if (list === 'unused') { + cy.realPress('ArrowRight') + .realPress('ArrowLeft'); + } + + Cypress._.times(listLength, () => cy.realPress('ArrowUp', { pressDelay: 100 })); + Cypress._.times(initialPosition, () => cy.realPress('ArrowDown', { pressDelay: 100 })); +}; + +// All position methods are 1-indexed. +export default { + waitMainLoading: () => { + cy.xpath(manageProfileSettingsPage).should('be.visible'); + cy.xpath(saveAndCloseButton).should('be.disabled'); + cy.xpath(saveAndKeepEditingButton).should('be.disabled'); + }, + + waitProfilesLoading: () => { + cy.xpath(profilesSection).should('be.visible'); + }, + + waitEditorLoading: () => { + cy.xpath(editorSection).should('be.visible'); + cy.get(loaderOverlay).should('not.exist'); + }, + + clickCloseButton: () => { + cy.xpath(closeButton).should('be.visible'); + cy.xpath(closeButton).click(); + }, + + clickCloseButtonNoChanges: () => { + cy.xpath(closeButton).should('be.visible'); + cy.xpath(closeButton).click(); + cy.xpath(manageProfileSettingsPage).should('not.exist'); + }, + + saveAndClose: () => { + cy.xpath(saveAndCloseButton).should('be.visible').and('be.enabled'); + cy.xpath(saveAndCloseButton).click(); + }, + + saveAndKeepEditing: () => { + cy.xpath(saveAndKeepEditingButton).should('be.visible').and('be.enabled'); + cy.xpath(saveAndKeepEditingButton).click(); + }, + + togglePreferredProfile: () => { + cy.xpath(preferredProfileCheckbox).should('be.visible'); + cy.xpath(preferredProfileCheckbox).click(); + }, + + verifyPreferredProfile: (enabled) => { + if (enabled) { + cy.xpath(preferredProfileCheckbox).should('be.checked'); + } else { + cy.xpath(preferredProfileCheckbox).should('not.be.checked'); + } + }, + + selectDefaultSettings: () => { + cy.xpath(defaultProfileSettingsRadio).scrollIntoView(); + cy.xpath(defaultProfileSettingsRadio).should('be.visible'); + cy.xpath(defaultProfileSettingsRadio).click(); + }, + + selectCustomSettings: () => { + cy.xpath(customProfileSettingsRadio).should('be.visible'); + cy.xpath(customProfileSettingsRadio).click(); + }, + + verifyDefaultSettingsSelected: () => { + cy.xpath(defaultProfileSettingsRadio).should('be.checked'); + }, + + verifyCustomSettingsSelected: () => { + cy.xpath(customProfileSettingsRadio).should('be.checked'); + }, + + selectProfile: (name) => { + cy.contains('button', new RegExp(`^${name}$`)) + .scrollIntoView() + .should('be.visible') + .click(); + }, + + moveComponentToOppositeListButton: (id) => { + cy.xpath(component(id, componentActivateMenu)) + .scrollIntoView() + .should('be.visible') + .focus() + .wait(100) + .realClick(); + cy.xpath(component(id, componentMoveAction)) + .scrollIntoView() + .should('be.visible') + .focus() + .wait(100) + .realClick(); + }, + + moveComponentUnavailable: (id) => { + cy.xpath(component(id, componentActivateMenu)) + .should('be.visible') + .focus() + .wait(100) + .realClick(); + cy.xpath(component(id, componentMoveAction)) + .should('not.exist'); + }, + + nudgeComponentUpButton: (id) => { + cy.xpath(component(id, componentNudgeUp)) + .should('be.visible') + .focus() + .wait(100) + .click() + .wait(500); + }, + + nudgeComponentDownButton: (id) => { + cy.xpath(component(id, componentNudgeDown)) + .should('be.visible') + .focus() + .wait(100) + .click() + .wait(500); + }, + + verifySelectedComponent: (id) => { + cy.xpath(selectedList).xpath(component(id, '', true)).should('exist'); + }, + + verifySelectedComponentPosition: (id, position) => { + cy.xpath(selectedList).find(profileComponent).eq(position - 1) + .invoke('attr', 'data-testid') + .should('eq', `component-${id}`); + if (position === 1) { + cy.xpath(component(id, componentNudgeUp)) + .should('not.exist'); + } + cy.xpath(selectedList).find(profileComponent).its('length').then((length) => { + if (position === length) { + cy.xpath(component(id, componentNudgeDown)) + .should('not.exist'); + } + }); + }, + + verifyUnusedComponent: (id) => { + cy.xpath(unusedList).xpath(component(id, '', true)).should('exist'); + }, + + verifyUnusedComponentPosition: (id, position) => { + cy.xpath(unusedList).find(profileComponent).eq(position - 1) + .invoke('attr', 'data-testid') + .should('eq', `component-${id}`); + }, + + dragUnusedComponentAndCancel: (position) => { + cy.xpath(unusedList) + .find(profileComponent) + .eq(position - 1) + .realMouseDown({ button: 'left', position: 'center' }) + .realMouseMove(0, 10, { position: 'center' }) + .wait(200); + cy.realPress('Escape') + .wait(50); + }, + + dragSelectedComponentAndCancel: (position) => { + cy.xpath(selectedList) + .find(profileComponent) + .eq(position - 1) + .realMouseDown({ button: 'left', position: 'center' }) + .realMouseMove(0, 10, { position: 'center' }) + .wait(200); + cy.realPress('Escape') + .wait(50); + }, + + // Note that dragging downwards will drag past the target by one, because + // the remainder of the list shifts upward in response to 'removing' the + // selected component. Plan accordingly. + dragReorderSelectedComponent: (id, targetId) => { + dragDropList(id, selectedList, targetId, selectedList); + }, + + // Note that dragging downwards will drag past the target by one, because + // the remainder of the list shifts upward in response to 'removing' the + // selected component. Plan accordingly. + dragReorderUnusedComponent: (id, targetId) => { + dragDropList(id, unusedList, targetId, unusedList); + }, + + dragSelectedToUnused: (id, targetId) => { + dragDropList(id, selectedList, targetId, unusedList); + }, + + dragSelectedToUnusedContainer: (id) => { + dragDropGeneral(id, selectedList, unusedContainer); + }, + + dragUnusedToSelected: (id, targetId) => { + dragDropList(id, unusedList, targetId, selectedList); + }, + + dragSelectedToUndroppableRegion: (id) => { + dragDropGeneral(id, selectedList, profilesSection); + }, + + dragUnusedToUndroppableRegion: (id) => { + dragDropGeneral(id, unusedList, profilesSection); + }, + + keyboardDragUnusedComponentAndCancel: (position) => { + cy.xpath(unusedList) + .find(profileComponent) + .eq(position - 1) + .focus() + .wait(200); + cy.realPress(' '); + cy.realPress('ArrowDown'); + cy.realPress('ArrowDown'); + cy.realPress('Escape'); + }, + + keyboardDragSelectedComponentAndCancel: (position) => { + cy.xpath(selectedList) + .find(profileComponent) + .eq(position - 1) + .focus() + .wait(200); + cy.realPress(' '); + cy.realPress('ArrowDown'); + cy.realPress('ArrowDown'); + cy.realPress('Escape'); + }, + + keyboardDragReorderSelectedComponent: (id, targetId) => { + getListLength(selectedList).then((length) => { + getComponentPosition(id, selectedList).then((focusedPosition) => { + getComponentPosition(targetId, selectedList).then((targetPosition) => { + cy.xpath(selectedList) + .xpath(component(id, '', true)) + .focus(); + cy.realPress(' '); + keyBackToStartingPosition('selected', length, focusedPosition - 1); + if (focusedPosition < targetPosition) { + Cypress._.times(targetPosition - focusedPosition - 1, () => cy.realPress('ArrowDown', { pressDelay: 100 })); + } else if (focusedPosition > targetPosition) { + Cypress._.times(focusedPosition - targetPosition - 1, () => cy.realPress('ArrowUp', { pressDelay: 100 })); + } + cy.realPress('Enter'); + }); + }); + }); + cy.get(draggingComponent).should('not.exist') + .wait(1000); + }, + + keyboardDragReorderUnusedComponent: (id, targetId) => { + getListLength(unusedList).then((length) => { + getComponentPosition(id, unusedList).then((focusedPosition) => { + getComponentPosition(targetId, unusedList).then((targetPosition) => { + cy.xpath(unusedList) + .xpath(component(id, '', true)) + .focus(); + cy.realPress(' '); + keyBackToStartingPosition('unused', length, focusedPosition - 1); + if (focusedPosition < targetPosition) { + Cypress._.times(targetPosition - focusedPosition - 1, () => cy.realPress('ArrowDown', { pressDelay: 100 })); + } else if (focusedPosition > targetPosition) { + Cypress._.times(focusedPosition - targetPosition - 1, () => cy.realPress('ArrowUp', { pressDelay: 100 })); + } + cy.realPress('Enter'); + }); + }); + }); + cy.get(draggingComponent).should('not.exist') + .wait(1000); + }, + + keyboardDragSelectedToUnused: (id) => { + getListLength(selectedList).then((length) => { + getComponentPosition(id, selectedList).then((focusedPosition) => { + cy.xpath(selectedList) + .xpath(component(id, '', true)) + .focus(); + cy.realPress(' ', { pressDelay: 100 }); + keyBackToStartingPosition('selected', length, focusedPosition); + cy.realPress('ArrowLeft', { pressDelay: 100 }); + cy.realPress('Enter'); + }); + }); + cy.get(draggingComponent).should('not.exist') + .wait(1000); + }, + + keyboardDragUnusedToSelected: (id) => { + getListLength(unusedList).then((length) => { + getComponentPosition(id, unusedList).then((focusedPosition) => { + cy.xpath(unusedList) + .xpath(component(id, '', true)) + .focus(); + cy.realPress(' ', { pressDelay: 100 }); + keyBackToStartingPosition('unused', length, focusedPosition); + cy.realPress('ArrowRight', { pressDelay: 100 }); + cy.realPress('Enter'); + }); + }); + cy.get(draggingComponent).should('not.exist') + .wait(1000); + }, + + verifyModalUnsavedOpen: () => { + cy.xpath(modalUnsaved).should('be.visible'); + }, + + modalUnsavedClose: () => { + cy.xpath(modalUnsaved) + .xpath(modalCloseButton) + .should('be.enabled') + .click(); + cy.xpath(modalUnsaved).should('not.exist'); + }, + + modalUnsavedContinueWithoutSaving: () => { + cy.xpath(modalUnsaved) + .xpath(modalCancelButton) + .should('be.enabled') + .click(); + cy.xpath(modalUnsaved).should('not.exist'); + }, + + modalUnsavedContinueWithSaving: () => { + cy.xpath(modalUnsaved) + .xpath(modalSubmitButton) + .should('be.enabled') + .click(); + cy.xpath(modalUnsaved).should('not.exist'); + }, + + verifyModalUnusedOpen: () => { + cy.xpath(modalUnused).should('be.visible'); + }, + + modalUnusedClose: () => { + cy.xpath(modalUnused) + .xpath(modalCloseButton) + .should('be.enabled') + .click(); + cy.xpath(modalUnused).should('not.exist'); + }, + + modalUnusedCancel: () => { + cy.xpath(modalUnused) + .xpath(modalCancelButton) + .should('be.enabled') + .click(); + cy.xpath(modalUnused).should('not.exist'); + }, + + modalUnusedSave: () => { + cy.xpath(modalUnused) + .xpath(modalSubmitButton) + .should('be.enabled') + .click(); + cy.xpath(modalUnused).should('not.exist'); + cy.wait(2000); + }, +}; diff --git a/cypress/support/fragments/linked-data/marigold.js b/cypress/support/fragments/linked-data/marigold.js index 852acf0c37..ee7e3507e1 100644 --- a/cypress/support/fragments/linked-data/marigold.js +++ b/cypress/support/fragments/linked-data/marigold.js @@ -16,6 +16,9 @@ const newResourceButton = Button({ const compareSelectedButton = Button({ dataTestID: 'resources-actions-dropdown__option-ld.compareSelected', }); +const manageProfileSettingsButton = Button({ + dataTestID: 'resources-actions-dropdown__option-ld.manageProfileSettings', +}); const searchSelect = "//select[@id='id-search-select']"; const searchButton = Button({ dataTestID: 'id-search-button' }); const workPreviewPanel = "//div[@class='preview-panel']"; @@ -72,6 +75,11 @@ export default { cy.do(newHubButton.click()); }, + openManageProfileSettings: () => { + cy.do(actionsWorkButton.click()); + cy.do(manageProfileSettingsButton.click()); + }, + editWork: () => { cy.xpath("//div[@class='full-display-control-panel']//button[text()='Edit work']").click(); cy.wait(1000); diff --git a/cypress/support/fragments/linked-data/uncontrolledAuthModal.js b/cypress/support/fragments/linked-data/uncontrolledAuthModal.js index eecf348332..e8f6e49055 100644 --- a/cypress/support/fragments/linked-data/uncontrolledAuthModal.js +++ b/cypress/support/fragments/linked-data/uncontrolledAuthModal.js @@ -1,7 +1,7 @@ export default { closeIfDisplayed() { cy.get('body').then(($body) => { - if ($body.find('div[class="modal modal-uncontrolled-authorities-warning"]').length > 0) { + if ($body.find('dialog[class="modal modal-uncontrolled-authorities-warning"]').length > 0) { cy.xpath('//button[@data-testid="modal-button-submit"]').click(); cy.wait(1000); } diff --git a/cypress/support/fragments/linked-data/workProfileModal.js b/cypress/support/fragments/linked-data/workProfileModal.js index 0481ab7945..5440dda932 100644 --- a/cypress/support/fragments/linked-data/workProfileModal.js +++ b/cypress/support/fragments/linked-data/workProfileModal.js @@ -1,4 +1,4 @@ -const profileModalSelector = 'div[class="modal modal-choose-profile"]'; +const profileModalSelector = 'dialog[class="modal modal-choose-profile"]'; export default { waitLoading() { @@ -14,6 +14,10 @@ export default { } }); }, + toggleDefaultProfile() { + cy.xpath('//input[@type="checkbox"][@name="My default work profile"]') + .click(); + }, selectDefaultOption() { cy.get(profileModalSelector).then((modal) => { if (modal.is(':visible')) { diff --git a/package.json b/package.json index 130c8d7cbe..3a3d477310 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@interactors/with-cypress": "1.0.0", "@reportportal/agent-js-cypress": ">=5.3.1 <6.0.0", "@shelex/cypress-allure-plugin": "^2.40.0", + "ally.js": "^1.4.1", "axe-core": "4.3.3", "axios": "1.14.0", "babel-loader": "^9.2.1", @@ -60,6 +61,7 @@ "cypress-downloadfile": "^1.2.1", "cypress-file-upload": "^5.0.8", "cypress-plugin-tab": "^1.0.5", + "cypress-real-events": "^1.15.0", "cypress-recurse": "^1.13.1", "cypress-testrail-simple": "^3.1.0", "cypress-xpath": "^1.6.2",