diff --git a/cypress/e2e/linked-data/LD Edit/work-instance-edit.cy.js b/cypress/e2e/linked-data/LD Edit/work-instance-edit.cy.js index 1505514d2a..01a4536f9b 100644 --- a/cypress/e2e/linked-data/LD Edit/work-instance-edit.cy.js +++ b/cypress/e2e/linked-data/LD Edit/work-instance-edit.cy.js @@ -127,14 +127,20 @@ describe('Citation: core work and instance editor features', () => { UnsavedChangesModal.checkButtonsEnabled(); UnsavedChangesModal.clickDismiss(); EditResource.waitLoading(EDIT_RESOURCE_HEADINGS.NEW_INSTANCE); - EditResource.checkTextValueOnField(testData.uniqueInstanceTitleFirst, fieldData.instanceTitle); + EditResource.checkTextValueOnField( + testData.uniqueInstanceTitleFirst, + fieldData.instanceTitle, + ); // Unsaved changes modal, dismissed indirectly EditResource.clickEditWork(); UnsavedChangesModal.waitLoading(); UnsavedChangesModal.clickOverlayToDismiss(); EditResource.waitLoading(EDIT_RESOURCE_HEADINGS.NEW_INSTANCE); - EditResource.checkTextValueOnField(testData.uniqueInstanceTitleFirst, fieldData.instanceTitle); + EditResource.checkTextValueOnField( + testData.uniqueInstanceTitleFirst, + fieldData.instanceTitle, + ); // Unsaved changes modal, continue without saving EditResource.clickEditWork(); @@ -155,7 +161,11 @@ describe('Citation: core work and instance editor features', () => { UnsavedChangesModal.clickSaveAndContinue(); EditResource.waitLoading(EDIT_RESOURCE_HEADINGS.EDIT_WORK); EditResource.checkPreviewOpen(); - EditResource.checkPreviewSectionContainsField(fieldData.titleSection, fieldData.instanceTitle, testData.uniqueInstanceTitleFirst); + EditResource.checkPreviewSectionContainsField( + fieldData.titleSection, + fieldData.instanceTitle, + testData.uniqueInstanceTitleFirst, + ); EditResource.checkSaveButtonsDisabled(); EditResource.checkCloseAndCancelEnabled(); @@ -180,7 +190,11 @@ describe('Citation: core work and instance editor features', () => { UnsavedChangesModal.waitLoading(); UnsavedChangesModal.clickContinueWithoutSaving(); EditResource.waitLoading(EDIT_RESOURCE_HEADINGS.EDIT_INSTANCE); - EditResource.checkPreviewSectionContainsField(fieldData.titleSection, fieldData.workTitle, testData.uniqueWorkTitleFirst); + EditResource.checkPreviewSectionContainsField( + fieldData.titleSection, + fieldData.workTitle, + testData.uniqueWorkTitleFirst, + ); // Unsaved work changes saved through modal EditResource.clickEditWork(); @@ -190,7 +204,11 @@ describe('Citation: core work and instance editor features', () => { UnsavedChangesModal.clickSaveAndContinue(); EditResource.waitLoading(EDIT_RESOURCE_HEADINGS.EDIT_INSTANCE); EditResource.checkPreviewOpen(); - EditResource.checkPreviewSectionContainsField(fieldData.titleSection, fieldData.workTitle, testData.uniqueWorkTitleSecond); + EditResource.checkPreviewSectionContainsField( + fieldData.titleSection, + fieldData.workTitle, + testData.uniqueWorkTitleSecond, + ); // Back to work and then back to instance EditResource.clickEditWork(); @@ -227,7 +245,10 @@ describe('Citation: core work and instance editor features', () => { // See instance preview SearchAndFilter.openSearchResultPreviewByTitle(resourceData.uniqueInstanceTitle); SearchAndFilter.waitPreviewLoading(); - SearchAndFilter.checkPreviewContains(fieldData.instanceTitle, resourceData.uniqueInstanceTitle); + SearchAndFilter.checkPreviewContains( + fieldData.instanceTitle, + resourceData.uniqueInstanceTitle, + ); }, ); }); diff --git a/cypress/e2e/receiving/remove-bound-piece-with-outstanding-transfer-request.cy.js b/cypress/e2e/receiving/remove-bound-piece-with-outstanding-transfer-request.cy.js new file mode 100644 index 0000000000..9e1858511d --- /dev/null +++ b/cypress/e2e/receiving/remove-bound-piece-with-outstanding-transfer-request.cy.js @@ -0,0 +1,429 @@ +import { v4 as uuid } from 'uuid'; + +import { Checkbox, KeyValue } from '../../../interactors'; +import { + BOUND_PIECES_DATA_LIST_COLUMNS, + FULFILMENT_PREFERENCES, + ITEM_STATUS_NAMES, + NO_VALUE, + ORDER_FORMAT_VALUES, + ORDER_STATUSES, + RECEIVING_PIECE_FORM_MODES, + POL_CREATE_INVENTORY_SETTINGS, + RECEIVING_PIECE_STATUSES, + RECEIVING_RECEIVED_PIECE_FILTER_LABELS, + RECEIVING_TITLE_SEARCH_INDEXES, + REQUEST_LEVELS, + REQUEST_TYPES, + RECEIVING_PIECE_FORM_FIELD_LABELS, +} from '../../support/constants'; +import Permissions from '../../support/dictionary/permissions'; +import { + InventoryHoldings, + InventoryInstance, + InventoryInstances, + InventoryItems, + ItemRecordView, +} from '../../support/fragments/inventory'; +import { + BasicOrderLine, + NewOrder, + NewPiece, + OrderLines, + Orders, + Pieces, +} from '../../support/fragments/orders'; +import { NewOrganization, Organizations } from '../../support/fragments/organizations'; +import { PieceForm, ReceivingDetails, Receivings } from '../../support/fragments/receiving'; +import Requests from '../../support/fragments/requests/requests'; +import ServicePoints from '../../support/fragments/settings/tenant/servicePoints/servicePoints'; +import TopMenu from '../../support/fragments/topMenu'; +import Users from '../../support/fragments/users/users'; +import { ExecutionFlowManager } from '../../support/utils'; +import getRandomPostfix from '../../support/utils/stringTools'; + +const R = { + ACQUISITION_METHOD: 'acquisitionMethod', + ADMIN: 'admin', + BOUND_ITEM: 'boundItem', + LOAN_TYPE: 'loanType', + LOCALE: 'locale', + LOCATIONS: 'locations', + MATERIAL_TYPE: 'materialType', + ORDER: 'order', + ORDER_LINE: 'orderLine', + ORGANIZATION: 'organization', + PIECES: 'pieces', + PICKUP_SERVICE_POINT: 'pickupServicePoint', + REQUEST: 'request', + REQUEST_ITEM: 'requestItem', + RUN_USER: 'runUser', + TITLE: 'title', +}; + +const PIECES_TOTAL = 2; + +const getVisitOptions = (flow) => ({ + url: TopMenu.receivingPath, + qs: { + qindex: RECEIVING_TITLE_SEARCH_INDEXES.POL_NUMBER, + query: flow.get(R.ORDER_LINE).poLineNumber, + }, +}); + +const createPiecesCleanup = (pieces) => () => { + for (const piece of pieces) { + InventoryItems.deleteItemViaApi(piece.itemId); + Pieces.deleteOrderPieceViaApi(piece.id); + InventoryHoldings.deleteHoldingRecordViaApi(piece.holdingId); + } +}; + +const createRequestCleanup = (requestId) => () => Requests.deleteRequestViaApi(requestId); + +const createItemLevelRequestBody = (flow) => ({ + fulfillmentPreference: FULFILMENT_PREFERENCES.HOLD_SHELF, + holdingsRecordId: flow.get(R.ORDER_LINE).locations[0].holdingId, + instanceId: flow.get(R.ORDER_LINE).instanceId, + itemId: flow.get(R.REQUEST_ITEM).id, + pickupServicePointId: flow.get(R.PICKUP_SERVICE_POINT).id, + requestDate: new Date().toISOString(), + requestLevel: REQUEST_LEVELS.ITEM, + requestType: REQUEST_TYPES.HOLD, + requesterId: flow.get(R.ADMIN).id, +}); + +const buildListConfig = (pieces) => { + return [ + pieces.map((piece) => [ + { column: BOUND_PIECES_DATA_LIST_COLUMNS.BARCODE, value: piece.barcode }, + { column: BOUND_PIECES_DATA_LIST_COLUMNS.DISPLAY_SUMMARY, value: piece.displaySummary }, + { column: BOUND_PIECES_DATA_LIST_COLUMNS.CHRONOLOGY, value: piece.chronology }, + { column: BOUND_PIECES_DATA_LIST_COLUMNS.COPY_NUMBER, value: piece.copyNumber }, + { column: BOUND_PIECES_DATA_LIST_COLUMNS.ENUMERATION, value: piece.enumeration }, + { + column: BOUND_PIECES_DATA_LIST_COLUMNS.EXPECTED_RECEIPT_DATE, + value: piece.expectedReceiptDate, + }, + ]), + ]; +}; + +describe('Receiving', () => { + const flow = new ExecutionFlowManager(); + + before('Create C502959 preconditions', () => { + cy.clearLocalStorage(); + cy.getAdminToken(); + cy.getAdminUserDetails().then((admin) => flow.set(R.ADMIN, admin)); + cy.getTenantLocaleApi().then((locale) => flow.set(R.LOCALE, locale)); + + const steps = getPreconditionSteps(); // eslint-disable-line no-use-before-define + + flow + .step(steps.createOrganization) + .step(steps.fetchLocation) + .step(steps.fetchMaterialType) + .step(steps.fetchLoanType) + .step(steps.fetchAcquisitionMethod) + .step(steps.fetchPickupServicePoint) + .step(steps.createOrder) + .step(steps.createOrderLine) + .step(steps.openOrder) + .step(steps.setTitleFromOrderLine) + .step(steps.createPieces) + .step(steps.createOutstandingRequestForPiece2) + .step(steps.receivePiecesViaApi) + .step(steps.bindPiecesViaApi) + .step(steps.createAuthorizedUser) + .step(steps.loginAsAuthorizedUser); + }); + + after('Delete C502959 data (what can be deleted)', () => { + cy.getAdminToken(); + flow.cleanup(); + }); + + it( + 'C502959 Remove bound piece from ongoing order with transfer request', + { tags: ['extendedPath', 'thunderjet', 'C502959'] }, + () => { + const { + boundItem, + pieces: { all: pieces }, + requestItem, + title, + } = flow.ctx(); + + const piece1 = pieces.find((piece) => piece.barcode !== flow.get(R.PIECES).piece2Barcode); + const piece2 = pieces.find((piece) => piece.barcode === flow.get(R.PIECES).piece2Barcode); + + cy.log('< --- STEP 1 --- >'); + Receivings.selectFromResultsList(title.title); + ReceivingDetails.checkTitlePaneIsDisplayed(title.title); + ReceivingDetails.assertBoundItemsListCount(1); + ReceivingDetails.assertBoundItemsListColumns(); + + cy.log('< --- STEP 2 --- >'); + ReceivingDetails.filterReceivedPiecesByOptions([ + RECEIVING_RECEIVED_PIECE_FILTER_LABELS.BOUND, + ]); + ReceivingDetails.verifyReceivedRecordsCount(PIECES_TOTAL); + + cy.log('< --- STEP 3 --- >'); + ReceivingDetails.clickBoundItemBarcodeLink(boundItem.barcode); + ItemRecordView.waitLoading(); + ItemRecordView.verifyItemBarcode(boundItem.barcode); + ItemRecordView.verifyItemStatus(ITEM_STATUS_NAMES.IN_PROCESS); + + cy.log('< --- STEP 4 --- >'); + ItemRecordView.verifyRequestsCount(1); + + cy.log('< --- STEP 5 --- >'); + ItemRecordView.assertBoundPiecesDataContent(buildListConfig([piece1, piece2])); + ItemRecordView.assertBoundPiecesResultsCount(PIECES_TOTAL); + + cy.log('< --- STEP 6 --- >'); + ItemRecordView.removePieceFromBoundItem(piece2.barcode); + + cy.log('< --- STEP 7 --- >'); + ItemRecordView.assertBoundPiecesDataContent(buildListConfig([piece1])); + ItemRecordView.assertBoundPiecesResultsCount(1); + + cy.log('< --- STEP 8 --- >'); + cy.visit(getVisitOptions(flow)); + Receivings.waitLoading(); + Receivings.selectFromResultsList(title.title); + ReceivingDetails.checkTitlePaneIsDisplayed(title.title); + ReceivingDetails.verifyReceivedRecordsCount(1); + ReceivingDetails.checkReceivedTableContent([piece2]); + + cy.log('< --- STEP 9 --- >'); + ReceivingDetails.openEditPieceModal({ section: 'received' }); + PieceForm.waitLoading(RECEIVING_PIECE_FORM_MODES.EDIT); + cy.expect(Checkbox(RECEIVING_PIECE_FORM_FIELD_LABELS.BOUND).has({ checked: false })); + cy.expect( + KeyValue(RECEIVING_PIECE_FORM_FIELD_LABELS.REQUEST).has({ + value: NO_VALUE, + }), + ); + + cy.log('< --- STEP 10 --- >'); + PieceForm.clickConnectedItemLink(); + ItemRecordView.waitLoading(); + ItemRecordView.verifyItemBarcode(requestItem.barcode); + + cy.log('< --- STEP 11 --- >'); + cy.visit(getVisitOptions(flow)); + Receivings.waitLoading(); + Receivings.selectFromResultsList(title.title); + ReceivingDetails.clickBoundItemBarcodeLink(boundItem.barcode); + ItemRecordView.waitLoading(); + ItemRecordView.verifyItemBarcode(boundItem.barcode); + ItemRecordView.verifyItemStatus(ITEM_STATUS_NAMES.IN_PROCESS); + + cy.log('< --- STEP 12 --- >'); + ItemRecordView.verifyRequestsCount(1); + + cy.log('< --- STEP 13 --- >'); + ItemRecordView.assertBoundPiecesDataContent(buildListConfig([piece1])); + ItemRecordView.assertBoundPiecesResultsCount(1); + ItemRecordView.clickBarcodeLinkInBoundPiecesDataAccordion(); + ItemRecordView.waitLoading(); + ItemRecordView.verifyItemStatus(ITEM_STATUS_NAMES.UNAVAILABLE); + }, + ); +}); + +function getPreconditionSteps() { + const createOrganization = (flow) => { + const organization = { ...NewOrganization.getDefaultOrganization() }; + + return Organizations.createOrganizationViaApi(organization).then((organizationId) => flow.set(R.ORGANIZATION, { ...organization, id: organizationId }, () => Organizations.deleteOrganizationViaApi(organizationId))); + }; + + const fetchLocation = (flow) => { + return InventoryInstances.getLocations({ limit: 2 }).then((locations) => flow.set(R.LOCATIONS, locations)); + }; + + const fetchMaterialType = (flow) => { + return cy.getBookMaterialType().then((materialType) => flow.set(R.MATERIAL_TYPE, materialType)); + }; + + const fetchLoanType = (flow) => { + return cy.getLoanTypes({ limit: 1 }).then((loanTypes) => flow.set(R.LOAN_TYPE, loanTypes[0])); + }; + + const fetchAcquisitionMethod = (flow) => { + return cy + .getAcquisitionMethodsApi() + .then(({ body }) => flow.set(R.ACQUISITION_METHOD, body.acquisitionMethods[0])); + }; + + const fetchPickupServicePoint = (flow) => { + return ServicePoints.getViaApi({ limit: 1, query: 'pickupLocation==true' }).then( + (servicePoints) => flow.set(R.PICKUP_SERVICE_POINT, servicePoints[0]), + ); + }; + + const createOrder = (flow) => { + return Orders.createOrderViaApi({ + ...NewOrder.getDefaultOngoingOrder({ vendorId: flow.get(R.ORGANIZATION).id }), + approved: true, + }).then((entity) => flow.set(R.ORDER, entity, () => Orders.deleteOrderViaApi(entity.id, false))); + }; + + const createOrderLine = (flow) => { + return OrderLines.createOrderLineViaApi({ + ...BasicOrderLine.defaultOrderLine, + id: uuid(), + purchaseOrderId: flow.get(R.ORDER).id, + checkinItems: true, + acquisitionMethod: flow.get(R.ACQUISITION_METHOD).id, + orderFormat: ORDER_FORMAT_VALUES.PHYSICAL_RESOURCE, + locations: [ + { + locationId: flow.get(R.LOCATIONS)[0].id, + quantity: PIECES_TOTAL, + quantityPhysical: PIECES_TOTAL, + }, + ], + details: { + ...BasicOrderLine.defaultOrderLine.details, + isBinderyActive: true, + }, + cost: { + ...BasicOrderLine.defaultOrderLine.cost, + quantityPhysical: PIECES_TOTAL, + currency: flow.get(R.LOCALE).currency, + }, + physical: { + createInventory: POL_CREATE_INVENTORY_SETTINGS.INSTANCE_HOLDING_ITEM, + materialType: flow.get(R.MATERIAL_TYPE).id, + materialSupplier: flow.get(R.ORGANIZATION).id, + volumes: [], + }, + }).then((entity) => flow.set(R.ORDER_LINE, entity)); + }; + + const openOrder = (flow) => { + Orders.updateOrderViaApi({ + ...flow.get(R.ORDER), + workflowStatus: ORDER_STATUSES.OPEN, + }); + + const cleanup = (orderLine) => { + OrderLines.deleteOrderLineViaApi(orderLine.id); + InventoryHoldings.deleteHoldingRecordViaApi(orderLine.locations[0].holdingId); + InventoryInstance.deleteInstanceViaApi(orderLine.instanceId); + }; + + return OrderLines.getOrderLineByIdViaApi(flow.get(R.ORDER_LINE).id).then((orderLine) => flow.set(R.ORDER_LINE, orderLine, cleanup.bind(null, orderLine))); + }; + + const setTitleFromOrderLine = (flow) => { + return Receivings.getTitleByPoLineIdViaApi(flow.get(R.ORDER_LINE).id).then((title) => flow.set(R.TITLE, title)); + }; + + const createPieces = (flow) => { + const piece2Barcode = `AT_piece2_item_barcode-${getRandomPostfix()}`; + + return Pieces.upsertOrderPiecesBatchViaApi( + [ + { + ...NewPiece.defaultPiece, + id: uuid(), + poLineId: flow.get(R.ORDER_LINE).id, + titleId: flow.get(R.TITLE).id, + locationId: flow.get(R.LOCATIONS)[1].id, + comment: 'Piece #1', + }, + { + ...NewPiece.defaultPiece, + id: uuid(), + poLineId: flow.get(R.ORDER_LINE).id, + titleId: flow.get(R.TITLE).id, + locationId: flow.get(R.LOCATIONS)[1].id, + comment: 'Piece #2', + barcode: piece2Barcode, + }, + ], + { createItem: true }, + ).then((data) => { + const cleanup = createPiecesCleanup(data.pieces); + + flow.set(R.PIECES, { all: data.pieces, piece2Barcode }, cleanup); + + InventoryItems.getItemByIdViaApi(data.pieces[1].itemId).then((item) => flow.set(R.REQUEST_ITEM, item)); + }); + }; + + const receivePiecesViaApi = (flow) => { + return Pieces.updateOrderPiecesStatusesBatchViaApi({ + pieceIds: flow.get(R.PIECES).all.map((piece) => piece.id), + receivingStatus: RECEIVING_PIECE_STATUSES.RECEIVED, + }); + }; + + const createOutstandingRequestForPiece2 = (flow) => { + return Requests.createNewRequestViaApi(createItemLevelRequestBody(flow)).then((request) => flow.set(R.REQUEST, request.body, createRequestCleanup(request.body.id))); + }; + + const bindPiecesViaApi = (flow) => { + const boundBarcode = `bound-${getRandomPostfix()}`; + + return Pieces.bindPiecesViaApi({ + bindItem: { + barcode: boundBarcode, + holdingId: flow.get(R.ORDER_LINE).locations[0].holdingId, + materialTypeId: flow.get(R.MATERIAL_TYPE).id, + permanentLoanTypeId: flow.get(R.LOAN_TYPE).id, + }, + bindPieceIds: flow.get(R.PIECES).all.map((piece) => piece.id), + instanceId: flow.get(R.ORDER_LINE).instanceId, + poLineId: flow.get(R.ORDER_LINE).id, + requestsAction: 'Transfer', + }) + .then(({ itemId }) => InventoryItems.getItemByIdViaApi(itemId)) + .then((boundItem) => flow.set(R.BOUND_ITEM, boundItem, () => InventoryItems.deleteItemViaApi(boundItem.id))); + }; + + const createAuthorizedUser = (flow) => { + return cy + .createTempUser([ + Permissions.uiInventoryViewInstances.gui, + Permissions.uiOrdersView.gui, + Permissions.uiReceivingView.gui, + Permissions.uiReceivingViewEditCreate.gui, + ]) + .then((user) => flow.set(R.RUN_USER, user, () => Users.deleteViaApi(user.userId))); + }; + + const loginAsAuthorizedUser = (flow) => { + const user = flow.get(R.RUN_USER); + + return cy.login(user.username, user.password, { + path: getVisitOptions(flow), + waiter: Receivings.waitLoading, + }); + }; + + return { + createOrganization, + fetchLocation, + fetchMaterialType, + fetchLoanType, + fetchAcquisitionMethod, + fetchPickupServicePoint, + createOrder, + createOrderLine, + openOrder, + setTitleFromOrderLine, + createPieces, + receivePiecesViaApi, + createOutstandingRequestForPiece2, + bindPiecesViaApi, + createAuthorizedUser, + loginAsAuthorizedUser, + }; +} diff --git a/cypress/support/constants/constants.js b/cypress/support/constants/constants.js index bc8f427ebd..6564ad84cf 100644 --- a/cypress/support/constants/constants.js +++ b/cypress/support/constants/constants.js @@ -76,27 +76,6 @@ export const MATERIAL_TYPE_NAMES = { VIDEO_RECORDING: 'video recording', }; -export const ITEM_STATUS_NAMES = { - ON_ORDER: 'On order', - IN_PROCESS: 'In process', - AVAILABLE: 'Available', - MISSING: 'Missing', - LONG_MISSING: 'Long missing', - IN_TRANSIT: 'In transit', - PAGED: 'Paged', - AWAITING_PICKUP: 'Awaiting pickup', - CHECKED_OUT: 'Checked out', - CLAIMED_RETURNED: 'Claimed returned', - DECLARED_LOST: 'Declared lost', - MARKED_AS_MISSING: 'Marked as missing', - AWAITING_DELIVERY: 'Awaiting delivery', - FOUND_BY_LIBRARY: 'Checked in (found by library)', - AGED_TO_LOST: 'Aged to lost', - LOST_AND_PAID: 'Lost and paid', - WITHDRAWN: 'Withdrawn', - ORDER_CLOSED: 'Order closed', -}; - export const CY_ENV = { CIRCULATION_RULES: 'circulationRules', DIKU_LOGIN: 'diku_login', @@ -1797,11 +1776,13 @@ export const SORT_DIRECTIONS = { }; export const COMMON_BUTTON_LABELS = { + ACTIONS: 'Actions', APPLY: 'Apply', CANCEL: 'Cancel', CONFIRM: 'Confirm', NEXT: 'Next', PREVIOUS: 'Previous', + REMOVE: 'Remove', RESET_ALL: 'Reset all', YES: 'Yes', NO: 'No', diff --git a/cypress/support/constants/index.js b/cypress/support/constants/index.js index bdb01e078e..30f16c1a7c 100644 --- a/cypress/support/constants/index.js +++ b/cypress/support/constants/index.js @@ -2,6 +2,7 @@ export * from './consortia'; export * from './constants'; export * from './finance'; +export * from './inventory'; export * from './invoices'; export * from './orders'; export * from './organizations'; diff --git a/cypress/support/constants/inventory/index.js b/cypress/support/constants/inventory/index.js new file mode 100644 index 0000000000..23e7266a1d --- /dev/null +++ b/cypress/support/constants/inventory/index.js @@ -0,0 +1 @@ +export * from './item'; diff --git a/cypress/support/constants/inventory/item.js b/cypress/support/constants/inventory/item.js new file mode 100644 index 0000000000..c3c3636995 --- /dev/null +++ b/cypress/support/constants/inventory/item.js @@ -0,0 +1,33 @@ +export const ITEM_STATUS_NAMES = { + AGED_TO_LOST: 'Aged to lost', + AVAILABLE: 'Available', + AWAITING_DELIVERY: 'Awaiting delivery', + AWAITING_PICKUP: 'Awaiting pickup', + CHECKED_OUT: 'Checked out', + CLAIMED_RETURNED: 'Claimed returned', + DECLARED_LOST: 'Declared lost', + FOUND_BY_LIBRARY: 'Checked in (found by library)', + IN_PROCESS: 'In process', + IN_PROCESS_NON_REQUESTABLE: 'In process (non-requestable)', + IN_TRANSIT: 'In transit', + LONG_MISSING: 'Long missing', + LOST_AND_PAID: 'Lost and paid', + MARKED_AS_MISSING: 'Marked as missing', + MISSING: 'Missing', + ON_ORDER: 'On order', + ORDER_CLOSED: 'Order closed', + PAGED: 'Paged', + RESTRICTED: 'Restricted', + UNAVAILABLE: 'Unavailable', + UNKNOWN: 'Unknown', + WITHDRAWN: 'Withdrawn', +}; + +export const BOUND_PIECES_DATA_LIST_COLUMNS = { + BARCODE: 'Barcode', + DISPLAY_SUMMARY: 'Display summary', + CHRONOLOGY: 'Chronology', + COPY_NUMBER: 'Copy number', + ENUMERATION: 'Enumeration', + EXPECTED_RECEIPT_DATE: 'Expected receipt date', +}; diff --git a/cypress/support/constants/receiving/piece.js b/cypress/support/constants/receiving/piece.js index 6e25bb6da8..ae1805e55d 100644 --- a/cypress/support/constants/receiving/piece.js +++ b/cypress/support/constants/receiving/piece.js @@ -1,7 +1,7 @@ export const RECEIVING_PIECE_FORMATS = { - PHYSICAL: 'Physical', ELECTRONIC: 'Electronic', OTHER: 'Other', + PHYSICAL: 'Physical', }; export const RECEIVING_PIECE_STATUSES = { @@ -12,3 +12,58 @@ export const RECEIVING_PIECE_STATUSES = { RECEIVED: 'Received', UNRECEIVABLE: 'Unreceivable', }; + +export const RECEIVING_RECEIVED_PIECE_FILTER_LABELS = { + BOUND: 'Bound', + NON_SUPPLEMENTS: 'Non supplements', + NOT_BOUND: 'Not bound', + SUPPLEMENTS: 'Supplements', +}; + +export const RECEIVING_PIECE_FORM_MODES = { + CREATE: 'create', + EDIT: 'edit', +}; + +export const RECEIVING_PIECE_FORM_ACCORDIONS_LABELS = { + ITEM_DETAILS: 'Item details', + PIECE_DETAILS: 'Piece details', + STATUS_LOG: 'Status log', +}; + +export const RECEIVING_PIECE_FORM_FIELD_LABELS = { + ACCESSION_NUMBER: 'Accession number', + BARCODE: 'Barcode', + BOUND: 'Bound', + CALL_NUMBER: 'Call number', + CHRONOLOGY: 'Chronology', + COMMENTS: 'Comments', + COPY_NUMBER: 'Copy number', + CREATE_ITEM: 'Create item', + DISPLAY_ON_HOLDING: 'Display on holding', + DISPLAY_SUMMARY: 'Display summary', + ENUMERATION: 'Enumeration', + EXPECTED_RECEIPT_DATE: 'Expected receipt date', + EXTERNAL_NOTE: 'External note', + INTERNAL_NOTE: 'Internal note', + ITEM_STATUS: 'Item status', + ORDER_LINE_LOCATIONS: 'Order line locations', + PIECE_FORMAT: 'Piece format', + REQUEST: 'Request', + SELECT_HOLDINGS: 'Select holdings', + SEQUENCE: 'Sequence', + SUPPLEMENT: 'Supplement', +}; + +export const RECEIVING_PIECE_FORM_ACTIONS_LABELS = { + CANCEL: 'Cancel', + DELAY_CLAIM: 'Delay claim', + DELETE: 'Delete', + MARK_LATE: 'Mark late', + QUICK_RECEIVE: 'Quick receive', + SAVE_AND_CLOSE: 'Save & close', + SAVE_AND_CREATE: 'Save & create another', + SEND_CLAIM: 'Send claim', + UNRECEIVE: 'Unreceive', + UNRECEIVABLE: 'Unreceivable', +}; diff --git a/cypress/support/fragments/inventory/index.js b/cypress/support/fragments/inventory/index.js index ee789f3908..b4a9a002b7 100644 --- a/cypress/support/fragments/inventory/index.js +++ b/cypress/support/fragments/inventory/index.js @@ -3,6 +3,7 @@ export { default as InventoryInstance } from './inventoryInstance'; export { default as InventoryInstances } from './inventoryInstances'; export { default as InstanceRecordEdit } from './instanceRecordEdit'; export { default as InstanceRecordView } from './instanceRecordView'; +export { default as InventoryItems } from './item/inventoryItems'; export { default as ItemRecordEdit } from './item/itemRecordEdit'; export { default as ItemRecordView } from './item/itemRecordView'; export { default as InventorySearchAndFilter } from './inventorySearchAndFilter'; diff --git a/cypress/support/fragments/inventory/item/inventoryItems.js b/cypress/support/fragments/inventory/item/inventoryItems.js index 9f63697634..61a963e54e 100644 --- a/cypress/support/fragments/inventory/item/inventoryItems.js +++ b/cypress/support/fragments/inventory/item/inventoryItems.js @@ -122,6 +122,13 @@ export default { }) .then(({ body }) => body.items); }, + getItemByIdViaApi(itemId) { + return cy + .okapiRequest({ + path: `inventory/items/${itemId}`, + }) + .then(({ body }) => body); + }, createItemViaApi(item) { return cy .okapiRequest({ diff --git a/cypress/support/fragments/inventory/item/itemRecordView.js b/cypress/support/fragments/inventory/item/itemRecordView.js index 43d6722ef2..f4cb6d5bfc 100644 --- a/cypress/support/fragments/inventory/item/itemRecordView.js +++ b/cypress/support/fragments/inventory/item/itemRecordView.js @@ -3,10 +3,12 @@ import { Accordion, Button, Callout, + ConfirmationModal, KeyValue, Link, MultiColumnList, MultiColumnListCell, + MultiColumnListRow, MultiSelect, Pane, PaneHeader, @@ -14,8 +16,9 @@ import { TextField, ValueChipRoot, } from '../../../../../interactors'; -import { ITEM_STATUS_NAMES } from '../../../constants'; +import { COMMON_BUTTON_LABELS, ITEM_STATUS_NAMES } from '../../../constants'; import dateTools from '../../../utils/dateTools'; +import MCLHelper from '../../multiColumnList'; import ConfirmDeleteItemModal from '../modals/confirmDeleteItemModal'; import UpdateOwnershipModal from '../modals/updateOwnershipModal'; import ItemRecordEdit from './itemRecordEdit'; @@ -30,10 +33,14 @@ const circulationHistoryAccordion = Accordion('Circulation history'); const saveAndCloseBtn = Button('Save & close'); const electronicAccessAccordion = Accordion('Electronic access'); const tagsAccordion = Accordion('Tags'); +const boundPiecesDataAccordion = Accordion('Bound pieces data'); const hridKeyValue = KeyValue('Item HRID'); const textFieldTagInput = MultiSelect({ label: 'Tag text area' }); const closeIcon = Button({ icon: 'times' }); const versionHistoryButton = Button({ icon: 'clock' }); +const confirmationModal = ConfirmationModal('Are you sure?'); + +const NO_BARCODE = 'No barcode'; const verifyItemBarcode = (value) => { cy.expect(KeyValue('Item barcode').has({ value })); @@ -713,4 +720,39 @@ export default { clickVersionHistoryButton() { cy.do(versionHistoryButton.click()); }, + + assertBoundPiecesResultsCount(expectedCount) { + cy.expect(boundPiecesDataAccordion.find(MultiColumnList()).has({ rowCount: expectedCount })); + }, + + assertBoundPiecesDataContent(rowsConfig = []) { + MCLHelper.assertRowsCellsContent(boundPiecesDataAccordion.find(MultiColumnList(), rowsConfig)); + }, + + clickBarcodeLinkInBoundPiecesDataAccordion(barcode) { + cy.do( + boundPiecesDataAccordion + .find(MultiColumnListRow({ content: including(barcode || NO_BARCODE), isContainer: false })) + .find(Link()) + .perform((element) => { + if (element.hasAttribute('target') && element.getAttribute('target') === '_blank') { + element.removeAttribute('target'); + } + element.click(); + }), + ); + }, + + removePieceFromBoundItem(content) { + const removeBtn = boundPiecesDataAccordion + .find(MultiColumnListRow({ content: including(content), isContainer: false })) + .find(Button({ button: true })); + + cy.expect(removeBtn.exists()); + cy.do(removeBtn.click()); + cy.expect(confirmationModal.exists()); + cy.expect(confirmationModal.has({ message: 'Remove this piece from the bound item?' })); + cy.do(confirmationModal.confirm(COMMON_BUTTON_LABELS.REMOVE)); + cy.expect(confirmationModal.absent()); + }, }; diff --git a/cypress/support/fragments/linked-data/editResource.js b/cypress/support/fragments/linked-data/editResource.js index b26150d324..b085f9167c 100644 --- a/cypress/support/fragments/linked-data/editResource.js +++ b/cypress/support/fragments/linked-data/editResource.js @@ -101,21 +101,24 @@ export default { }, checkSuccessStatusDisplayed() { - cy.xpath('//span[@class="status-message-text" and text()="Resource updated. Expect a short delay before changes are visible in FOLIO."]') - .should('be.visible'); + cy.xpath( + '//span[@class="status-message-text" and text()="Resource updated. Expect a short delay before changes are visible in FOLIO."]', + ).should('be.visible'); }, clearStatusMessages() { // Toasts are stacked in an overlapping way likely obscuring close button; close them all anwyays. // eslint-disable-next-line cypress/no-force - cy.xpath('//section[@data-testid="common-status"]//button[contains(@class, "status-message-close")]') - .click({ multiple: true, force: true }); + cy.xpath( + '//section[@data-testid="common-status"]//button[contains(@class, "status-message-close")]', + ).click({ multiple: true, force: true }); cy.wait(1000); }, toggleSectionMarcTooltip(section) { - cy.xpath(`//div[text()="${section}"]/following-sibling::div/div[contains(@class, "marc-tooltip-wrapper")]/button`) - .click(); + cy.xpath( + `//div[text()="${section}"]/following-sibling::div/div[contains(@class, "marc-tooltip-wrapper")]/button`, + ).click(); cy.wait(500); }, @@ -127,7 +130,9 @@ export default { }, checkMarcTooltipContains(field, mapping) { - cy.xpath(`//dialog[contains(@class, "marc-tooltip-content")]/div[span[@class="marc-tooltip-field" and normalize-space()="${field}:"] and span[@class="marc-tooltip-mapping" and text()="${mapping}"]]`) + cy.xpath( + `//dialog[contains(@class, "marc-tooltip-content")]/div[span[@class="marc-tooltip-field" and normalize-space()="${field}:"] and span[@class="marc-tooltip-mapping" and text()="${mapping}"]]`, + ) .scrollIntoView() .should('be.visible'); }, @@ -144,8 +149,9 @@ export default { setValueForSectionFieldDropdown(value, field, section, repeatPosition = 1) { cy.wait(1000); - cy.xpath(`(//div[text()="${section}"]/../../div/following-sibling::div/div[@class="label" and text()="${field}"])[${repeatPosition}]/following-sibling::div/select`) - .select(value); + cy.xpath( + `(//div[text()="${section}"]/../../div/following-sibling::div/div[@class="label" and text()="${field}"])[${repeatPosition}]/following-sibling::div/select`, + ).select(value); cy.wait(1000); }, @@ -185,11 +191,13 @@ export default { setValueForSectionSimpleField(value, field, repeatPosition = 1) { cy.wait(1000); - cy.xpath(`(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div/following-sibling::div//div[contains(@class, "simple-lookup__control")]`) - .click(); + cy.xpath( + `(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div/following-sibling::div//div[contains(@class, "simple-lookup__control")]`, + ).click(); cy.wait(500); - cy.xpath(`(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div/following-sibling::div//div[contains(@class, "simple-lookup__menu")]/div/div[text()="${value}"]`) - .click(); + cy.xpath( + `(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div/following-sibling::div//div[contains(@class, "simple-lookup__menu")]/div/div[text()="${value}"]`, + ).click(); cy.wait(1000); }, @@ -336,23 +344,25 @@ export default { checkSectionDropdownContainsOptions(section, field, optionLabels, repeatPosition = 1) { cy.xpath( - `(//div[text()='${section}']/../../div/following-sibling::div/div[@class="label" and text()="${field}"])[${repeatPosition}]/following-sibling::div/select/option` + `(//div[text()='${section}']/../../div/following-sibling::div/div[@class="label" and text()="${field}"])[${repeatPosition}]/following-sibling::div/select/option`, ).then(($options) => { - const labels = [...$options].map(opt => opt.text); + const labels = [...$options].map((opt) => opt.text); expect(labels).to.include.members(optionLabels); }); }, checkSimpleFieldDropdownContainsOptions(field, optionLabels, repeatPosition = 1) { cy.wait(500); - cy.xpath(`(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div//div[contains(@class, "simple-lookup__control")]`) - .click(); + cy.xpath( + `(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div//div[contains(@class, "simple-lookup__control")]`, + ).click(); cy.wait(1000); - cy.xpath(`(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div//div[contains(@class, "simple-lookup__menu")]/div/div`) - .then(($options) => { - const labels = [...$options].map(opt => opt.textContent); - expect(labels).to.include.members(optionLabels); - }); + cy.xpath( + `(//div[@class="label" and text()="${field}"])[${repeatPosition}]/../../div//div[contains(@class, "simple-lookup__menu")]/div/div`, + ).then(($options) => { + const labels = [...$options].map((opt) => opt.textContent); + expect(labels).to.include.members(optionLabels); + }); cy.do(Keyboard.escape()); cy.wait(500); }, @@ -373,7 +383,9 @@ export default { }, checkPreviewSectionContainsLink(section, field, text, link) { - cy.xpath(`//div[@class="preview-block" and strong[@class="sub-heading" and text()="${section}"]]`) + cy.xpath( + `//div[@class="preview-block" and strong[@class="sub-heading" and text()="${section}"]]`, + ) .should('exist') .filter((_secIdx, sectionBlock) => { const $sectionBlock = Cypress.$(sectionBlock); @@ -387,9 +399,11 @@ export default { const $linkEl = $fieldBlock.next().find('.preview-value-link'); - return $linkEl.text() === text - && $linkEl.attr('target') === 'blank' - && $linkEl.attr('href') === link; + return ( + $linkEl.text() === text && + $linkEl.attr('target') === 'blank' && + $linkEl.attr('href') === link + ); }) .should('have.length.at.least', 1); }, @@ -486,7 +500,9 @@ export default { }, checkDropdownTextValue(textValue, field) { - cy.xpath(`//div[text()="${field}"]/following-sibling::div//select[@data-testid="dropdown-field"]`) + cy.xpath( + `//div[text()="${field}"]/following-sibling::div//select[@data-testid="dropdown-field"]`, + ) .filter((_selectIdx, selectBlock) => { const opt = selectBlock.options[selectBlock.selectedIndex]; return opt && opt.text === textValue; diff --git a/cypress/support/fragments/linked-data/searchAndFilter.js b/cypress/support/fragments/linked-data/searchAndFilter.js index ad98194f2a..5beafa7662 100644 --- a/cypress/support/fragments/linked-data/searchAndFilter.js +++ b/cypress/support/fragments/linked-data/searchAndFilter.js @@ -108,15 +108,12 @@ export default { }, openSearchResultPreviewByTitle(title) { - cy.xpath(`//button[contains(text(),"${title}")]`) - .scrollIntoView() - .should('be.visible') - .click(); + cy.xpath(`//button[contains(text(),"${title}")]`).scrollIntoView().should('be.visible').click(); }, checkPreviewContains(field, value) { cy.xpath( - `//strong[@class="value-heading" and text()="${field}"]/following-sibling::div[normalize-space()="${value}"]` + `//strong[@class="value-heading" and text()="${field}"]/following-sibling::div[normalize-space()="${value}"]`, ).should('exist'); }, @@ -125,7 +122,9 @@ export default { }, checkTitleNoOverlap() { - cy.xpath('//div[@class="search-result-entry-container"][1]//button[contains(@class, "title")]').then(($title) => { + cy.xpath( + '//div[@class="search-result-entry-container"][1]//button[contains(@class, "title")]', + ).then(($title) => { cy.xpath('//div[@class="search-result-entry-container"][1]//table').then(($instances) => { const titleBottom = $title[0].getBoundingClientRect().bottom; const instancesTop = $instances[0].getBoundingClientRect().top; diff --git a/cypress/support/fragments/linked-data/unsavedChangesModal.js b/cypress/support/fragments/linked-data/unsavedChangesModal.js index 434d2229a2..77661d6496 100644 --- a/cypress/support/fragments/linked-data/unsavedChangesModal.js +++ b/cypress/support/fragments/linked-data/unsavedChangesModal.js @@ -1,6 +1,7 @@ import { Button } from '../../../../interactors'; -const unsavedChangesModal = '//dialog[@data-testid="modal" and contains(@class, "modal-switch-to-new-record")]'; +const unsavedChangesModal = + '//dialog[@data-testid="modal" and contains(@class, "modal-switch-to-new-record")]'; const modalOverlay = '//div[@data-testid="modal-overlay"]'; const dismissButton = Button({ ariaLabel: 'Close Basic modal' }); const continueWithoutSaveButton = Button({ dataTestID: 'modal-button-cancel' }); @@ -13,7 +14,10 @@ export default { checkButtonsEnabled() { cy.expect([dismissButton.exists(), dismissButton.is({ disabled: false })]); - cy.expect([continueWithoutSaveButton.exists(), continueWithoutSaveButton.is({ disabled: false })]); + cy.expect([ + continueWithoutSaveButton.exists(), + continueWithoutSaveButton.is({ disabled: false }), + ]); cy.expect([saveButton.exists(), saveButton.is({ disabled: false })]); }, @@ -34,9 +38,7 @@ export default { clickOverlayToDismiss() { // eslint-disable-next-line cypress/no-force - cy.xpath(modalOverlay) - .should('be.visible') - .click('topLeft', { force: true }); + cy.xpath(modalOverlay).should('be.visible').click('topLeft', { force: true }); cy.wait(1000); }, }; diff --git a/cypress/support/fragments/orders/pieces/pieces.js b/cypress/support/fragments/orders/pieces/pieces.js index 6cb81dff78..ad2d1c4ef0 100644 --- a/cypress/support/fragments/orders/pieces/pieces.js +++ b/cypress/support/fragments/orders/pieces/pieces.js @@ -23,11 +23,12 @@ export default { }, // https://s3.amazonaws.com/foliodocs/api/mod-orders/r/pieces-batch.html#orders_pieces_batch_post - upsertOrderPiecesBatchViaApi(pieces) { + upsertOrderPiecesBatchViaApi(pieces, { createItem } = {}) { return cy .okapiRequest({ method: 'POST', path: 'orders/pieces-batch', + searchParams: { createItem }, body: { pieces, totalRecords: pieces.length, diff --git a/cypress/support/fragments/receiving/index.js b/cypress/support/fragments/receiving/index.js index 84b018bdf0..a20a43dfb1 100644 --- a/cypress/support/fragments/receiving/index.js +++ b/cypress/support/fragments/receiving/index.js @@ -1,3 +1,4 @@ +export { default as PieceForm } from './pieceForm'; export { default as Receivings } from './receiving'; export { default as ReceivingDetails } from './receivingDetails'; export { default as ReceivingEditForm } from './receivingEditForm'; diff --git a/cypress/support/fragments/receiving/pieceForm.js b/cypress/support/fragments/receiving/pieceForm.js new file mode 100644 index 0000000000..fb2985e833 --- /dev/null +++ b/cypress/support/fragments/receiving/pieceForm.js @@ -0,0 +1,25 @@ +import { Accordion, Link, Pane } from '../../../../interactors'; +import { + RECEIVING_PIECE_FORM_ACCORDIONS_LABELS, + RECEIVING_PIECE_FORM_MODES, +} from '../../constants'; + +const pieceFormPane = Pane({ id: 'pane-title-form' }); +const connectedItemLink = Link('Connected'); +const pieceDetailsAccordion = Accordion(RECEIVING_PIECE_FORM_ACCORDIONS_LABELS.PIECE_DETAILS); + +export default { + waitLoading(mode = RECEIVING_PIECE_FORM_MODES.CREATE) { + cy.expect(pieceFormPane.exists()); + cy.expect( + pieceFormPane.has({ + title: mode === RECEIVING_PIECE_FORM_MODES.CREATE ? 'Add piece' : 'Edit piece', + }), + ); + cy.get('[class^="spinner"]').should('not.exist'); + }, + + clickConnectedItemLink() { + cy.do(pieceDetailsAccordion.find(connectedItemLink).click()); + }, +}; diff --git a/cypress/support/fragments/receiving/receivingDetails.js b/cypress/support/fragments/receiving/receivingDetails.js index d54f006d3d..52733ccd90 100644 --- a/cypress/support/fragments/receiving/receivingDetails.js +++ b/cypress/support/fragments/receiving/receivingDetails.js @@ -9,12 +9,15 @@ import { Link, PaneHeader, MultiColumnList, + Checkbox, + DropdownMenu, } from '../../../../interactors'; import { COMMON_BUTTON_LABELS, DEFAULT_WAIT_TIME, RECEIVING_BOUND_ITEMS_COLUMN_LABELS, } from '../../constants'; +import { ItemRecordView } from '../inventory'; import InventoryInstance from '../inventory/inventoryInstance'; import MultiColumnListHelper from '../multiColumnList'; import OrderLineDetails from '../orders/orderLineDetails'; @@ -313,6 +316,19 @@ export default { MultiColumnListHelper.assertPaginationControlsDisabled(boundItemsAccordion, { previous, next }); }, + filterReceivedPiecesByOptions(optionLabels = []) { + const actionsBtn = receivedSection.find(Button(COMMON_BUTTON_LABELS.ACTIONS)); + + cy.do(actionsBtn.click()); + optionLabels.forEach((label) => { + const checkbox = DropdownMenu().find(Checkbox(label)); + + cy.do(checkbox.click()); + cy.expect(checkbox.has({ checked: true, disabled: false })); + }); + cy.do(actionsBtn.click()); + }, + clickNextPageButtonInBoundItemsAccordion() { cy.do(boundItemsAccordion.find(Button(COMMON_BUTTON_LABELS.NEXT)).click()); }, @@ -320,4 +336,12 @@ export default { clickPreviousPageButtonInBoundItemsAccordion() { cy.do(boundItemsAccordion.find(Button(COMMON_BUTTON_LABELS.PREVIOUS)).click()); }, + + clickBoundItemBarcodeLink(barcode) { + cy.contains('#bound-items-list a', barcode) + .invoke('removeAttr', 'target') // to open the link in the same tab + .click(); + + ItemRecordView.waitLoading(); + }, };