From 0ab25536d34971943526be555db2ef9cb0e4aa3f Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:03:20 +0000 Subject: [PATCH 01/25] Update environment configuration files to include DESTROYED_LOCATION_BARCODE variable --- .env.development | 5 +++-- .env.production | 3 ++- .env.uat | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.env.development b/.env.development index 56b88095a..80dd7dde4 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,8 @@ VITE_TRACTION_BASE_URL=http://traction VITE_TRACTION_GRAPHQL_URL=http://traction/graphql VITE_PRINTMYBARCODE_BASE_URL=http://printmybarcode VITE_SAMPLEEXTRACTION_BASE_URL=http://sampleextraction -VITE_LABWHERE_BASE_URL=http://labwhere +VITE_LABWHERE_BASE_URL=http://localhost:3003 VITE_SEQUENCESCAPE_API_KEY=development VITE_LOG=true -VITE_ENVIRONMENT=development \ No newline at end of file +VITE_ENVIRONMENT=development +VITE_DESTROYED_LOCATION_BARCODE=lw-destroyed-217 diff --git a/.env.production b/.env.production index 214eea82b..78b4b10cc 100644 --- a/.env.production +++ b/.env.production @@ -5,4 +5,5 @@ VITE_SAMPLEEXTRACTION_BASE_URL=REPLACE_VITE_SAMPLEEXTRACTION_BASE_URL VITE_SEQUENCESCAPE_BASE_URL=REPLACE_VITE_SEQUENCESCAPE_BASE_URL VITE_LABWHERE_BASE_URL=REPLACE_VITE_LABWHERE_BASE_URL VITE_SEQUENCESCAPE_API_KEY=REPLACE_VITE_SEQUENCESCAPE_API_KEY -VITE_ENVIRONMENT=REPLACE_VITE_ENVIRONMENT \ No newline at end of file +VITE_ENVIRONMENT=REPLACE_VITE_ENVIRONMENT +VITE_DESTROYED_LOCATION_BARCODE=REPLACE_VITE_LABWHERE_DESTROY_LOCATION_BARCODE diff --git a/.env.uat b/.env.uat index 311e83754..78b4b10cc 100644 --- a/.env.uat +++ b/.env.uat @@ -6,3 +6,4 @@ VITE_SEQUENCESCAPE_BASE_URL=REPLACE_VITE_SEQUENCESCAPE_BASE_URL VITE_LABWHERE_BASE_URL=REPLACE_VITE_LABWHERE_BASE_URL VITE_SEQUENCESCAPE_API_KEY=REPLACE_VITE_SEQUENCESCAPE_API_KEY VITE_ENVIRONMENT=REPLACE_VITE_ENVIRONMENT +VITE_DESTROYED_LOCATION_BARCODE=REPLACE_VITE_LABWHERE_DESTROY_LOCATION_BARCODE From ecae3df150ee3f02502367eda9d4c97ae23407a8 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:03:48 +0000 Subject: [PATCH 02/25] update(client.js): Add exhaustSamplesIfDestroyed function to handle destroyed location barcodes --- src/services/labwhere/client.js | 45 +++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/services/labwhere/client.js b/src/services/labwhere/client.js index 4d42c2697..b5f87502c 100644 --- a/src/services/labwhere/client.js +++ b/src/services/labwhere/client.js @@ -1,7 +1,13 @@ import { extractLocationsForLabwares } from './helpers.js' import { FetchWrapper } from '@/api/FetchWrapper.js' +import { + fetchLibraries, + exhaustLibrayVolume, + formatAndTransformLibraries, +} from '@/stores/utilities/pacbioLibraries.js' const labwhereFetch = FetchWrapper(import.meta.env['VITE_LABWHERE_BASE_URL'], 'LabWhere') +const destroyLocation = import.meta.env['VITE_DESTROYED_LOCATION_BARCODE'] /** * Fetches the locations of labwares from LabWhere based on provided barcodes. * @@ -46,7 +52,7 @@ const getLabwhereLocations = async (labwhereBarcodes) => { * * @param {string} userCode - The user code or swipecard. * @param {string} locationBarcode - The barcode of the location where labware will be stored. - * @param {string} labwareBarcodes - The barcodes of the labware to be stored, separated by newlines. + * @param {string} labwareBarcodes - The barcodes of the labware (library barcode or the plate / tube barcode for samples) to be stored, separated by newlines. * @param {number|null} [startPosition=null] - The starting position for storing the labware (optional). * @returns {Promise<{success: boolean, errors: string[]}>} - A promise that resolves to an object containing the success status, any errors, and the data. * @@ -90,4 +96,39 @@ const scanBarcodesInLabwhereLocation = async ( return { success: response.success, errors: response.errors, message: response.data.message } } -export { getLabwhereLocations, scanBarcodesInLabwhereLocation } +const exhaustSamplesIfDestroyed = async (locationBarcode, labwareBarcodes) => { + if (locationBarcode !== destroyLocation) return { success: false } + let librariesToDestroy = [] + + const fetchAndMergeLibraries = async (barcodes, filterKey) => { + const filterOptions = { filter: { [filterKey]: barcodes.join(',') } } + const { success, libraries, tubes, tags, requests } = await fetchLibraries(filterOptions) + if (success) { + librariesToDestroy = [ + ...librariesToDestroy, + ...formatAndTransformLibraries(libraries, tubes, tags, requests), + ] + } + } + await fetchAndMergeLibraries(labwareBarcodes, 'source_identifier') + const barcodesNotFetchedAsSourceIdentifer = labwareBarcodes.filter( + (barcode) => !librariesToDestroy.some((library) => library.source_identifier === barcode), + ) + // If not all libraries are found by source_identifier, try fetching by barcode + if (barcodesNotFetchedAsSourceIdentifer.length > 0) { + //Fetch libraries which are not found by barcode + await fetchAndMergeLibraries(barcodesNotFetchedAsSourceIdentifer, 'barcode') + } + if(!librariesToDestroy.length) return { success: false } + const exhaustedLibraries = [] + await Promise.all( + librariesToDestroy.map(async (library) => { + const { success } = await exhaustLibrayVolume(library) + if (success) { + exhaustedLibraries.push(library) + } + }), + ) + return { success: exhaustedLibraries.length>0, exhaustedLibraries } +} +export { getLabwhereLocations, scanBarcodesInLabwhereLocation, exhaustSamplesIfDestroyed } From 5d9c54cf640b722d074b7f4bc7fccdd87e7dd4fa Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:04:07 +0000 Subject: [PATCH 03/25] refactor(pacbioLibraries): Simplify library fetching and transformation logic --- src/stores/pacbioLibraries.js | 90 ++++++++--------------------------- 1 file changed, 19 insertions(+), 71 deletions(-) diff --git a/src/stores/pacbioLibraries.js b/src/stores/pacbioLibraries.js index d8d8f3fb2..6a0beca39 100644 --- a/src/stores/pacbioLibraries.js +++ b/src/stores/pacbioLibraries.js @@ -1,27 +1,14 @@ import { defineStore } from 'pinia' import useRootStore from '@/stores' import { handleResponse } from '@/api/v2/ResponseHelper.js' -import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js' +import { groupIncludedByResource } from '@/api/JsonApi.js' import { usePacbioRootStore } from '@/stores/pacbioRoot.js' -import { libraryPayload } from '@/stores//utilities/pacbioLibraries.js' - -/** - * @function validateFields - * @param {Object} library - The library object to validate. - * @returns {boolean} Returns true if all required fields are present and truthy in the library object, false otherwise. - * @description Validates that the required fields are present in the given library object. - * The required fields are 'id', 'template_prep_kit_box_barcode', 'volume' and 'concentration'. - * The 'tag' and 'insert_size' fields are optional. - */ -const validateFields = (library) => { - const requiredAttributes = ['id', 'template_prep_kit_box_barcode', 'volume', 'concentration'] - const errors = requiredAttributes.filter((field) => !library[field]) - - return { - success: errors.length === 0, - errors: 'Missing required field(s)', - } -} +import { + libraryPayload, + fetchLibraries, + updateLibrary, + formatAndTransformLibraries +} from '@/stores//utilities/pacbioLibraries.js' /** * Importing `defineStore` function from 'pinia' library. @@ -60,13 +47,7 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { */ librariesArray: (state) => { const pacbioRootStore = usePacbioRootStore() - return Object.values(state.libraries) - .filter((library) => library.tube) - .map((library) => { - const { id, request, tag_id, tag, tube, ...attributes } = library - const tagId = tag_id ?? tag - - /*Get the tag group ID from the library's tag ID or from the tag in pacbioRoot store(where all pacbio tags are kept). Why is this required? + /*Get the tag group ID from the library's tag ID or from the tag in pacbioRoot store(where all pacbio tags are kept). Why is this required? The librariesArray is called in multiple places (in create and edit context) to get the libraries. Therefore, librariesArray needs to search for the tag first in tags. If not found, it should then look for it in 'pacbioRoot' store tags. @@ -74,23 +55,9 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { which may not happen in all the places where it's called. Hence, a search in both places is required to ensure that librariesArray returns the correct tag associated with all libraries."*/ - - const tagGroupId = state.tags[tagId] - ? state.tags[tagId].group_id - : pacbioRootStore.tags[tagId] - ? pacbioRootStore.tags[tagId].group_id - : '' - - return { - id, - tag_id: String(tagId), - tube, - ...attributes, - tag_group_id: tagGroupId ?? '', - sample_name: state.requests[request].sample_name, - barcode: state.tubes[tube].barcode, - } - }) + const tags = { ...state.tags, ...pacbioRootStore.tags } + return formatAndTransformLibraries(state.libraries, state.tubes, tags, state.requests) + }, }, actions: { @@ -155,24 +122,15 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { * @param {number} page - The page number to fetch from the server. * @returns {Promise<{success: boolean, errors: Array}>} - A promise that resolves to an object containing a success boolean and an array of errors. */ - async fetchLibraries(filter = {}, page = {}) { - const rootStore = useRootStore() - const pacbioLibraries = rootStore.api.v2.traction.pacbio.libraries - const promise = pacbioLibraries.get({ - page, - filter, - include: 'request,tag,tube', + async fetchLibraries(filterOptions) { + const { success, data, meta, errors, libraries, tubes, tags, requests } = await fetchLibraries({ + ...filterOptions, }) - const response = await handleResponse(promise) - - const { success, body: { data, included = [], meta = {} } = {}, errors = [] } = response - if (success && data.length > 0) { - const { tubes, tags, requests } = groupIncludedByResource(included) - this.libraries = dataToObjectById({ data, includeRelationships: true }) - this.tubes = dataToObjectById({ data: tubes }) - this.tags = dataToObjectById({ data: tags }) - this.requests = dataToObjectById({ data: requests }) + this.libraries = libraries + this.tubes = tubes + this.tags = tags + this.requests = requests } return { success, errors, meta } }, @@ -185,17 +143,7 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { * If 'success' is false, the object also has an 'errors' property with a message describing the error. */ async updateLibrary(libraryFields) { - //Validate the libraryFields to ensure that all required fields are present - const valid = validateFields(libraryFields) - if (!valid.success) { - return { success: false, errors: valid.errors } - } - - const rootStore = useRootStore() - const request = rootStore.api.v2.traction.pacbio.libraries - const payload = libraryPayload(libraryFields) - const promise = request.update(payload) - const { success, errors } = await handleResponse(promise) + const { success, errors } = await updateLibrary(libraryFields) if (success) { //Update all fields of the library in the store with matching ID with the given values. this.libraries[libraryFields.id] = { From 2f76ac326935d0ef6ed361f735d8974d7127fcea Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:04:28 +0000 Subject: [PATCH 04/25] update(pacbioLibraries): Add library validation and fetching functions --- src/stores/utilities/pacbioLibraries.js | 116 +++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/src/stores/utilities/pacbioLibraries.js b/src/stores/utilities/pacbioLibraries.js index 514799fab..c8893fbb0 100644 --- a/src/stores/utilities/pacbioLibraries.js +++ b/src/stores/utilities/pacbioLibraries.js @@ -1,3 +1,7 @@ +import useRootStore from '@/stores' +import { handleResponse } from '@/api/v2/ResponseHelper.js' +import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js' + /** * * @param {Integer | String} id - id of the library @@ -45,4 +49,114 @@ const libraryPayload = ({ return payload } -export { libraryPayload } +/** + * @function validateFields + * @param {Object} library - The library object to validate. + * @returns {boolean} Returns true if all required fields are present and truthy in the library object, false otherwise. + * @description Validates that the required fields are present in the given library object. + * The required fields are 'id', 'template_prep_kit_box_barcode', 'volume' and 'concentration'. + * The 'tag' and 'insert_size' fields are optional. + */ +const validateLibraryFields = (library) => { + const requiredAttributes = [ + 'id', + 'template_prep_kit_box_barcode', + 'volume', + 'concentration', + 'insert_size', + ] + const errors = requiredAttributes.filter( + (field) => library[field] === null || library[field] === '', + ) + + return { + success: errors.length === 0, + errors: errors.length ? 'Missing required field(s)' : '', + } +} + +/** + * Fetches all libraries. + * + * @param {Object} fetchOptions - The options to fetch libraries with. + * The options include page, filter, and include. + * e.g { page: { "size": "24", "number": "1"}, filter: { source_identifier: 'sample1' }, include: 'request,tag,tube' } + */ +async function fetchLibraries(fetchOptions = {}) { + const includes = new Set(fetchOptions.include ? fetchOptions.include.split(',') : []) + const requiredIncludes = ['request', 'tag', 'tube'] + requiredIncludes.forEach((item) => includes.add(item)) + + const fetchOptionsDefaultInclude = { + ...fetchOptions, + include: Array.from(includes).join(','), + } + const rootStore = useRootStore() + const pacbioLibraries = rootStore.api.v2.traction.pacbio.libraries + const promise = pacbioLibraries.get(fetchOptionsDefaultInclude) + const response = await handleResponse(promise) + + const { success, body: { data, included = [], meta = {} } = {}, errors = [] } = response + let libraries = {}, + tubes = {}, + tags = {}, + requests = {} + if (success && data && data.length > 0) { + const { + tubes: included_tubes, + tags: included_tags, + requests: included_requests, + } = groupIncludedByResource(included) + libraries = dataToObjectById({ data, includeRelationships: true }) + tubes = dataToObjectById({ data: included_tubes }) + tags = dataToObjectById({ data: included_tags }) + requests = dataToObjectById({ data: included_requests }) + } + return { success, data, errors, meta, libraries, tubes, tags, requests } +} + +const formatAndTransformLibraries = (libraries, tubes, tags, requests) => + Object.values(libraries) + .filter((library) => library.tube) + .map((library) => { + const { id, request, tag_id, tag, tube, ...attributes } = library + const tagId = tag_id ?? tag + const tagGroupId = tags[tagId] ? (tags[tagId].group_id ?? '') : '' + return { + id, + tag_id: String(tagId), + tube, + ...attributes, + tag_group_id: tagGroupId, + sample_name: requests[request]?.sample_name, + barcode: tubes[tube]?.barcode, + } + }) + +async function exhaustLibrayVolume(library) { + library.volume = library.used_volume + const { success, errors } = await updateLibrary(library) + return { success, errors } +} + +async function updateLibrary(libraryFields) { + //Validate the libraryFields to ensure that all required fields are present + const valid = validateLibraryFields(libraryFields) + if (!valid.success) { + return valid + } + const rootStore = useRootStore() + const request = rootStore.api.v2.traction.pacbio.libraries + const payload = libraryPayload(libraryFields) + const promise = request.update(payload) + const { success, errors } = await handleResponse(promise) + return { success, errors } +} + +export { + libraryPayload, + fetchLibraries, + updateLibrary, + exhaustLibrayVolume, + formatAndTransformLibraries, +} From 6aaa5a2beef3ceeb24d8a806409d143b50a91a01 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:05:22 +0000 Subject: [PATCH 05/25] update(LabwhereReception): Integrate exhaustSamplesIfDestroyed for exhausting samples if destroyed location is scanned --- src/views/LabwhereReception.vue | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/views/LabwhereReception.vue b/src/views/LabwhereReception.vue index c0c18eacf..5ad5b4717 100644 --- a/src/views/LabwhereReception.vue +++ b/src/views/LabwhereReception.vue @@ -97,7 +97,10 @@ * It displays a success message if the barcodes are stored successfully, or an error message if the submission fails. */ import { ref, reactive, computed } from 'vue' -import { scanBarcodesInLabwhereLocation } from '@/services/labwhere/client.js' +import { + scanBarcodesInLabwhereLocation, + exhaustSamplesIfDestroyed, +} from '@/services/labwhere/client.js' import useAlert from '@/composables/useAlert.js' const user_code = ref('') // User code or swipecard @@ -170,7 +173,15 @@ const scanBarcodesToLabwhere = async () => { start_position.value, ) if (response.success) { - showAlert(response.message, 'success') + let message = response.message + const {success,exhaustedLibraries} = await exhaustSamplesIfDestroyed( + location_barcode.value, + uniqueBarcodesArray.value, + ) + if (success && exhaustedLibraries.length > 0) { + message += ` and sample volumes have been exhausted for ${exhaustedLibraries.length} libraries }` + } + showAlert(message, 'success') } else { showAlert(response.errors.join('\n'), 'danger') } From 0092a8a597f908620c1ce847047cd85dcda6edc7 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:06:02 +0000 Subject: [PATCH 06/25] Add unit tests --- tests/factories/PacbioLibraryFactory.js | 20 +- tests/unit/services/labwhere/client.spec.js | 319 ++++++++++++------ tests/unit/stores/pacbioLibraries.spec.js | 34 +- .../stores/utilities/pacbioLibraries.spec.js | 221 ++++++++++++ 4 files changed, 439 insertions(+), 155 deletions(-) create mode 100644 tests/unit/stores/utilities/pacbioLibraries.spec.js diff --git a/tests/factories/PacbioLibraryFactory.js b/tests/factories/PacbioLibraryFactory.js index e00023dc0..0690be629 100644 --- a/tests/factories/PacbioLibraryFactory.js +++ b/tests/factories/PacbioLibraryFactory.js @@ -1,5 +1,6 @@ import BaseFactory from './BaseFactory.js' import { groupIncludedByResource, dataToObjectById } from '../../src/api/JsonApi.js' +import {formatAndTransformLibraries} from '../../src/stores/utilities/pacbioLibraries.js' /** * @function createStoreData @@ -24,26 +25,9 @@ const createStoreData = ({ data, included }) => { * @param {Object} storeData - libraries, tubes, tags, requests * @returns {Array} An array of libraries with sample_name, barcode, and group_id. * @description A function that creates an array of libraries with sample_name, barcode, and group_id. - * This is verbatim repeating what is in the store, but it is useful to have a function that - * does this for testing purposes. It can be used for refactoring and eventually removing the function */ const createLibrariesArray = ({ libraries, tubes, tags, requests }) => { - return Object.values(libraries) - .filter((library) => library.tube) - .map((library) => { - const { request, tag, ...attributes } = library - const { sample_name } = requests[request] - const { barcode } = tubes[library.tube] - const { group_id } = tags[tag] - - return { - ...attributes, - sample_name, - barcode, - tag_group_id: group_id, - tag_id: tag, - } - }) + return formatAndTransformLibraries(libraries, tubes, tags, requests) } /* diff --git a/tests/unit/services/labwhere/client.spec.js b/tests/unit/services/labwhere/client.spec.js index 94090ceaa..87068eec0 100644 --- a/tests/unit/services/labwhere/client.spec.js +++ b/tests/unit/services/labwhere/client.spec.js @@ -1,130 +1,241 @@ -import { getLabwhereLocations, scanBarcodesInLabwhereLocation } from '@/services/labwhere/client.js' +import { + getLabwhereLocations, + scanBarcodesInLabwhereLocation, + exhaustSamplesIfDestroyed, +} from '@/services/labwhere/client.js' import LabwhereLocationsFactory from '@tests/factories/LabwhereLocationsFactory.js' +import * as pacbioLibraryUtilities from '@/stores/utilities/pacbioLibraries.js' +import { createPinia, setActivePinia } from '@support/testHelper.js' +import { beforeEach, describe, it } from 'vitest' +const mockFetch = vi.fn() +const labwhereLocationsFactory = LabwhereLocationsFactory() -describe('client', () => { - const mockFetch = vi.fn() - const labwhereLocationsFactory = LabwhereLocationsFactory() +beforeEach(() => { + global.fetch = mockFetch +}) - beforeEach(() => { - global.fetch = mockFetch +afterEach(() => { + vi.clearAllMocks() +}) + +describe('getLabwhereLocations', () => { + it('should return an error if no barcodes are provided', async () => { + const result = await getLabwhereLocations([]) + expect(result).toEqual({ success: false, errors: ['No barcodes provided'], data: {} }) }) - afterEach(() => { - vi.clearAllMocks() + it('should return an error if fetch fails', async () => { + mockFetch.mockRejectedValue(new Error('Network error')) + const result = await getLabwhereLocations(['barcode1']) + expect(result).toEqual({ + success: false, + errors: ['Failed to access LabWhere: Network error'], + data: {}, + }) }) - describe('getLabwhereLocations', () => { - it('should return an error if no barcodes are provided', async () => { - const result = await getLabwhereLocations([]) - expect(result).toEqual({ success: false, errors: ['No barcodes provided'], data: {} }) + it('should return an error if response is not ok', async () => { + mockFetch.mockResolvedValue({ + ok: false, + json: async () => ({ errors: ['Some error'] }), }) + const result = await getLabwhereLocations(['barcode1']) + expect(result).toEqual({ success: false, errors: ['Some error'], data: {} }) + }) - it('should return an error if fetch fails', async () => { - mockFetch.mockRejectedValue(new Error('Network error')) - const result = await getLabwhereLocations(['barcode1']) - expect(result).toEqual({ - success: false, - errors: ['Failed to access LabWhere: Network error'], - data: {}, - }) + it('should return locations if fetch is successful', async () => { + const mockResponse = [ + { + barcode: 'barcode1', + location: labwhereLocationsFactory.content[0], + }, + { + barcode: 'barcode2', + location: labwhereLocationsFactory.content[1], + }, + ] + const expectedData = { + barcode1: mockResponse[0].location, + barcode2: mockResponse[1].location, + barcode3: {}, + } + mockFetch.mockResolvedValue({ + ok: true, + json: async () => mockResponse, }) - it('should return an error if response is not ok', async () => { - mockFetch.mockResolvedValue({ - ok: false, - json: async () => ({ errors: ['Some error'] }), - }) - const result = await getLabwhereLocations(['barcode1']) - expect(result).toEqual({ success: false, errors: ['Some error'], data: {} }) - }) - - it('should return locations if fetch is successful', async () => { - const mockResponse = [ - { - barcode: 'barcode1', - location: labwhereLocationsFactory.content[0], - }, - { - barcode: 'barcode2', - location: labwhereLocationsFactory.content[1], - }, - ] - const expectedData = { - barcode1: mockResponse[0].location, - barcode2: mockResponse[1].location, - barcode3: {}, - } - mockFetch.mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }) + const result = await getLabwhereLocations(['barcode1', 'barcode2', 'barcode3']) + expect(result).toEqual({ success: true, errors: [], data: expectedData }) + }) +}) +describe('scanBarcodesInLabwhereLocation', () => { + it('should return an error if required parameters are missing', async () => { + const result = await scanBarcodesInLabwhereLocation('', '', '', null) + expect(result).toEqual({ + success: false, + errors: ['Required parameters are missing for the Scan In operation'], + }) + }) - const result = await getLabwhereLocations(['barcode1', 'barcode2', 'barcode3']) - expect(result).toEqual({ success: true, errors: [], data: expectedData }) + it('should return an error if fetch fails', async () => { + mockFetch.mockRejectedValue(new Error('Network error')) + const result = await scanBarcodesInLabwhereLocation('user123', 'location123', 'barcode1', null) + expect(result).toEqual({ + success: false, + errors: ['Failed to access LabWhere: Network error'], }) }) - describe('scanBarcodesInLabwhereLocation', () => { - it('should return an error if required parameters are missing', async () => { - const result = await scanBarcodesInLabwhereLocation('', '', '', null) - expect(result).toEqual({ - success: false, - errors: ['Required parameters are missing for the Scan In operation'], - }) + + it('should return an error if response is not ok', async () => { + mockFetch.mockResolvedValue({ + ok: false, + json: async () => ({ errors: ['Some error'] }), }) + const result = await scanBarcodesInLabwhereLocation('user123', 'location123', 'barcode1', null) + expect(result).toEqual({ success: false, errors: ['Some error'] }) + }) - it('should return an error if fetch fails', async () => { - mockFetch.mockRejectedValue(new Error('Network error')) - const result = await scanBarcodesInLabwhereLocation( - 'user123', - 'location123', - 'barcode1', - null, - ) - expect(result).toEqual({ - success: false, - errors: ['Failed to access LabWhere: Network error'], - }) + it('should return success if fetch is successful', async () => { + const mockResponse = { success: true, errors: [], message: 'Labware stored to location 1' } + mockFetch.mockResolvedValue({ + ok: true, + json: async () => mockResponse, }) - it('should return an error if response is not ok', async () => { - mockFetch.mockResolvedValue({ - ok: false, - json: async () => ({ errors: ['Some error'] }), - }) - const result = await scanBarcodesInLabwhereLocation( - 'user123', - 'location123', - 'barcode1', - null, - ) - expect(result).toEqual({ success: false, errors: ['Some error'] }) - }) - - it('should return success if fetch is successful', async () => { - const mockResponse = { success: true, errors: [], message: 'Labware stored to location 1' } - mockFetch.mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }) + const result = await scanBarcodesInLabwhereLocation('user123', 'location123', 'barcode1', null) + expect(result).toEqual(mockResponse) + }) - const result = await scanBarcodesInLabwhereLocation( - 'user123', - 'location123', - 'barcode1', - null, - ) - expect(result).toEqual(mockResponse) + it('should include start position if provided', async () => { + const mockResponse = { success: true, errors: [] } + mockFetch.mockResolvedValue({ + ok: true, + json: async () => mockResponse, }) - it('should include start position if provided', async () => { - const mockResponse = { success: true, errors: [] } - mockFetch.mockResolvedValue({ - ok: true, - json: async () => mockResponse, + const result = await scanBarcodesInLabwhereLocation('user123', 'location123', 'barcode1', 1) + expect(result).toEqual(mockResponse) + }) +}) + +describe('exhaustSamplesIfDestroyed', () => { + beforeEach(() => { + const pinia = createPinia() + setActivePinia(pinia) + }) + const destroyLocation = import.meta.env['VITE_DESTROYED_LOCATION_BARCODE'] + const labwareBarcodes = ['barcode1', 'barcode2', 'barcode3'] + let mockFetchLibraries, mockFormatAndTransformLibraries, mockExhaustLibrayVolume + const fetchLibraryResponses = [ + { + success: true, + libraries: [{ id: 1, source_identifier: 'barcode1', tube: 1 }], + tubes: {}, + tags: {}, + requests: {}, + }, + { + success: true, + libraries: [ + { id: 2, barcode: 'barcode2', tube: 2 }, + { id: 3, barcode: 'barcode3', tube: 3 }, + ], + tubes: {}, + tags: {}, + requests: {}, + }, + ] + const formattedLibraries = [ + { id: 1, source_identifier: 'barcode1', barcode: 'TRAC-1' }, + { id: 2, source_identifier: 'barcode2', barcode: 'TRAC-2' }, + ] + + beforeEach(() => { + vi.clearAllMocks() + // Mock the environment variable + import.meta.env = { VITE_DESTROYED_LOCATION_BARCODE: destroyLocation } + mockFetchLibraries = vi.fn() + mockFormatAndTransformLibraries = vi.fn() + mockExhaustLibrayVolume = vi.fn() + mockFetchLibraries = vi.spyOn(pacbioLibraryUtilities, 'fetchLibraries') + mockFormatAndTransformLibraries = vi.spyOn( + pacbioLibraryUtilities, + 'formatAndTransformLibraries', + ) + mockExhaustLibrayVolume = vi.spyOn(pacbioLibraryUtilities, 'exhaustLibrayVolume') + }) + + it('should return false if locationBarcode does not match destroyLocation', async () => { + const result = await exhaustSamplesIfDestroyed('wrong-location', labwareBarcodes) + expect(mockFetchLibraries).not.toHaveBeenCalled() + expect(result).toEqual({ success: false }) + }) + + describe('when succesfully fetching libraries', () => { + beforeEach(() => { + mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[0]) + mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[1]) + mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[0]]) + mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[1]]) + mockExhaustLibrayVolume.mockResolvedValue({ success: true }) + }) + it('should fetch libraries by source_identifier and barcode', async () => { + await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(mockFetchLibraries).toHaveBeenCalledTimes(2) + expect(mockFetchLibraries).toHaveBeenCalledWith({ + filter: { source_identifier: 'barcode1,barcode2,barcode3' }, }) + expect(mockFetchLibraries).toHaveBeenCalledWith({ filter: { barcode: 'barcode2,barcode3' } }) + }) + it('should return exhaused libraries', async () => { + const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(result).toEqual({success: true, exhaustedLibraries:formattedLibraries}) + }) - const result = await scanBarcodesInLabwhereLocation('user123', 'location123', 'barcode1', 1) - expect(result).toEqual(mockResponse) + it('should exhaust library volumes', async () => { + await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(mockExhaustLibrayVolume).toHaveBeenCalledTimes(2) + expect(mockExhaustLibrayVolume).toHaveBeenCalledWith(formattedLibraries[0]) + expect(mockExhaustLibrayVolume).toHaveBeenCalledWith(formattedLibraries[1]) + }) + }) + describe('when fetching libraries fails', () => { + beforeEach(() => { + mockFetchLibraries.mockResolvedValueOnce({ success: false }) + }) + it('should return an empty array', async () => { + const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(result).toEqual({ success: false }) + expect(mockExhaustLibrayVolume).not.toHaveBeenCalled() + }) + }) + describe('when formatAndTransformLibraries returns an empty array', () => { + beforeEach(() => { + mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[0]) + mockFormatAndTransformLibraries.mockReturnValueOnce([]) + }) + it('should return an empty array', async () => { + const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(result).toEqual({success: false}) + }) + }) + describe('when exhaustLibrayVolume fails', () => { + it('should return an empty array', async () => { + mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[0]) + mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[0]]) + mockExhaustLibrayVolume.mockResolvedValue({ success: false }) + const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(result).toEqual({success:false, exhaustedLibraries: []}) + }) + it('should not return libraries for which the exhaustLibrayVolume fails ', async () => { + mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[0]) + mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[1]) + mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[0]]) + mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[1]]) + mockExhaustLibrayVolume.mockResolvedValueOnce({ success: true }) + mockExhaustLibrayVolume.mockResolvedValueOnce({ success: false }) + const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + expect(result).toEqual({success:true, exhaustedLibraries: [formattedLibraries[0]]}) }) }) }) diff --git a/tests/unit/stores/pacbioLibraries.spec.js b/tests/unit/stores/pacbioLibraries.spec.js index b3519b447..7d8a1ede1 100644 --- a/tests/unit/stores/pacbioLibraries.spec.js +++ b/tests/unit/stores/pacbioLibraries.spec.js @@ -164,6 +164,7 @@ describe('usePacbioLibrariesStore', () => { get.mockResolvedValue(pacbioLibraryFactory.responses.fetch) const { success, errors } = await store.fetchLibraries() + const expectedLibrary = Object.values(pacbioLibraryFactory.storeData.libraries)[0] expect(store.libraries[expectedLibrary.id]).toEqual(expectedLibrary) @@ -193,13 +194,6 @@ describe('usePacbioLibrariesStore', () => { expect(errors).toEqual([]) }) - it('unsuccessfully', async () => { - const failureResponse = failedResponse() - get.mockResolvedValue(failureResponse) - const { success, errors } = await store.fetchLibraries() - expect(success).toEqual(false) - expect(errors).toEqual(failureResponse.errorSummary) - }) }) describe('#updateLibrary', () => { @@ -242,32 +236,6 @@ describe('usePacbioLibrariesStore', () => { expect(storeLibrary.template_prep_kit_box_barcode).toEqual('LK12348') expect(storeLibrary.volume).toEqual(4.0) }) - - it('should return error if required attributes are empty', async () => { - await store.fetchLibraries() - expect(store.libraries[libraryBeforeUpdate.id]).toEqual(libraryBeforeUpdate) - library.volume = '' - const { success, errors } = await store.updateLibrary(library) - expect(success).toBeFalsy() - expect(errors).toEqual('Missing required field(s)') - }) - - it('should not return error if optional attributes are empty', async () => { - const mockResponse = successfulResponse() - update.mockResolvedValue(mockResponse) - await store.fetchLibraries() - const newLibrary = { ...library, tag_id: null } - const { success } = await store.updateLibrary(newLibrary) - expect(success).toBeTruthy() - }) - - it('unsuccessfully', async () => { - const mockResponse = failedResponse() - update.mockResolvedValue(mockResponse) - const { success, errors } = await store.updateLibrary(library) - expect(success).toBeFalsy() - expect(errors).toEqual(mockResponse.errorSummary) - }) }) }) }) diff --git a/tests/unit/stores/utilities/pacbioLibraries.spec.js b/tests/unit/stores/utilities/pacbioLibraries.spec.js new file mode 100644 index 000000000..97e19e504 --- /dev/null +++ b/tests/unit/stores/utilities/pacbioLibraries.spec.js @@ -0,0 +1,221 @@ +import useRootStore from '@/stores' +import { + createPinia, + setActivePinia, + failedResponse, + successfulResponse, +} from '@support/testHelper.js' +import { + fetchLibraries, + formatAndTransformLibraries, + updateLibrary, + exhaustLibrayVolume, +} from '@/stores/utilities/pacbioLibraries.js' +import PacbioLibraryFactory from '@tests/factories/PacbioLibraryFactory.js' +import { beforeEach, describe, expect, it } from 'vitest' +import { libraryPayload } from '../../../../src/stores/utilities/pacbioLibraries' + +const pacbioLibraryFactory = PacbioLibraryFactory() + +describe('pacbioLibraries', () => { + let rootStore + beforeEach(() => { + /*Creates a fresh pinia instance and make it active so it's automatically picked + up by any useStore() call without having to pass it to it for e.g `useStore(pinia)`*/ + const pinia = createPinia() + setActivePinia(pinia) + }) + describe('fecthLibraries', () => { + let get + + beforeEach(() => { + rootStore = useRootStore() + get = vi.fn() + rootStore.api.v2.traction.pacbio.libraries.get = get + }) + + it('calls the api to fetch libraries with default includea', async () => { + const fetchOptions = { filter: { source_identifier: 'sample1' } } + await fetchLibraries(fetchOptions) + expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'request,tag,tube' }) + }) + + it('calls the api to fetch libraries with custom includes along with default includes', async () => { + const fetchOptions = { include: 'test' } + await fetchLibraries(fetchOptions) + expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'test,request,tag,tube' }) + }) + it('calls api to fetch libraries with joined includes if custom includes includes default values', async () => { + const fetchOptions = { include: 'request,tag,tube,test' } + await fetchLibraries(fetchOptions) + expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'request,tag,tube,test' }) + }) + + it('calls api successfully', async () => { + get.mockResolvedValue(pacbioLibraryFactory.responses.fetch) + const { success, errors, libraries, tubes, requests } = await fetchLibraries() + expect(success).toEqual(true) + expect(errors).toEqual([]) + expect(libraries).toEqual(pacbioLibraryFactory.storeData.libraries) + expect(tubes).toEqual(pacbioLibraryFactory.storeData.tubes) + expect(requests).toEqual(pacbioLibraryFactory.storeData.requests) + }) + + it('unsuccessfully', async () => { + const failureResponse = failedResponse() + get.mockResolvedValue(failureResponse) + const { success, errors } = await fetchLibraries() + expect(success).toEqual(false) + expect(errors).toEqual(failureResponse.errorSummary) + }) + }) + describe('formatAndTransformLibraries', () => { + it('formats and transforms libraries', () => { + const libraries = { + 1: { + id: 1, + request: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + }, + } + const tubes = { + 1: { + id: 1, + barcode: 'tube1', + }, + } + const tags = { + 1: { + id: 1, + name: 'tag1', + group_id: 'group1', + }, + } + const requests = { + 1: { + id: 1, + sample_name: 'request1', + }, + } + const formattedLibraries = formatAndTransformLibraries(libraries, tubes, tags, requests) + expect(formattedLibraries).toEqual([ + { + id: 1, + tag_id: '1', + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + tag_group_id: 'group1', + sample_name: 'request1', + barcode: 'tube1', + }, + ]) + }) + }) + describe('updateLibrary', () => { + let update + beforeEach(() => { + rootStore = useRootStore() + update = vi.fn() + rootStore.api.v2.traction.pacbio.libraries.update = update + }) + it('doesnt call update if required library fields are empty', async () => { + const libraryFields = { + id: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 1, + insert_size: '', + } + const { success, errors } = await updateLibrary(libraryFields) + expect(success).toBeFalsy() + expect(errors).toEqual('Missing required field(s)') + expect(update).not.toHaveBeenCalled() + }) + it('validates successfully if any of the value is zero', async () => { + const libraryFields = { + id: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 0, + insert_size: 1, + } + await updateLibrary(libraryFields) + expect(update).toBeCalled() + }) + + it('calls the api to update the library for library with valid field values', async () => { + const libraryFields = { + id: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + } + const mockResponse = successfulResponse() + update.mockResolvedValue(mockResponse) + const { success } = await updateLibrary(libraryFields) + expect(update).toHaveBeenCalledWith(libraryPayload(libraryFields)) + expect(success).toBeTruthy() + }) + it('should not return error if optional attributes are empty', async () => { + const libraryFields = { + id: 1, + tag_id: null, + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + } + const mockResponse = successfulResponse() + update.mockResolvedValue(mockResponse) + const { success } = await updateLibrary(libraryFields) + expect(success).toBeTruthy() + }) + it('unsuccessfully', async () => { + const libraryFields = { + id: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + } + const mockResponse = failedResponse() + update.mockResolvedValue(mockResponse) + const { success, errors } = await updateLibrary(libraryFields) + expect(success).toBeFalsy() + expect(errors).toEqual(mockResponse.errorSummary) + }) + }) + + describe('exhaustLibraryVolume', () => { + let update + beforeEach(() => { + rootStore = useRootStore() + update = vi.fn() + rootStore.api.v2.traction.pacbio.libraries.update = update + }) + it('exhausts library volume', async () => { + const library = { + id: 1, + volume: 15, + used_volume: 10, + } + const mockResponse = successfulResponse() + update.mockResolvedValue(mockResponse) + const { success } = await exhaustLibrayVolume(library) + expect(library.volume).toEqual(library.used_volume) + expect(update).toHaveBeenCalled() + expect(success).toBeTruthy() + }) + }) +}) From 47737a09d06d8d66cfe718a960fea3ce0a1b65f2 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:48:35 +0000 Subject: [PATCH 07/25] rename: Update function name to exhaustLibraryVolumeIfDestroyed for clarity --- src/services/labwhere/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/labwhere/client.js b/src/services/labwhere/client.js index b5f87502c..ec4e99829 100644 --- a/src/services/labwhere/client.js +++ b/src/services/labwhere/client.js @@ -96,7 +96,7 @@ const scanBarcodesInLabwhereLocation = async ( return { success: response.success, errors: response.errors, message: response.data.message } } -const exhaustSamplesIfDestroyed = async (locationBarcode, labwareBarcodes) => { +const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) => { if (locationBarcode !== destroyLocation) return { success: false } let librariesToDestroy = [] @@ -131,4 +131,4 @@ const exhaustSamplesIfDestroyed = async (locationBarcode, labwareBarcodes) => { ) return { success: exhaustedLibraries.length>0, exhaustedLibraries } } -export { getLabwhereLocations, scanBarcodesInLabwhereLocation, exhaustSamplesIfDestroyed } +export { getLabwhereLocations, scanBarcodesInLabwhereLocation, exhaustLibraryVolumeIfDestroyed } From 2f7e594d7f2eda602f21a6562565ff81f926a5ee Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:48:44 +0000 Subject: [PATCH 08/25] Update pacbioLibraries.js --- src/stores/pacbioLibraries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/pacbioLibraries.js b/src/stores/pacbioLibraries.js index 6a0beca39..bfbdc83dc 100644 --- a/src/stores/pacbioLibraries.js +++ b/src/stores/pacbioLibraries.js @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import useRootStore from '@/stores' +import useRootStore from '@/stores/index.js' import { handleResponse } from '@/api/v2/ResponseHelper.js' import { groupIncludedByResource } from '@/api/JsonApi.js' import { usePacbioRootStore } from '@/stores/pacbioRoot.js' @@ -8,7 +8,7 @@ import { fetchLibraries, updateLibrary, formatAndTransformLibraries -} from '@/stores//utilities/pacbioLibraries.js' +} from '@/stores/utilities/pacbioLibraries.js' /** * Importing `defineStore` function from 'pinia' library. From 1cd5b7cb0b497343b9083d8747da4961fa5e96f1 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:48:51 +0000 Subject: [PATCH 09/25] Update LabwhereReception.vue --- src/views/LabwhereReception.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/views/LabwhereReception.vue b/src/views/LabwhereReception.vue index 5ad5b4717..f45e7aae3 100644 --- a/src/views/LabwhereReception.vue +++ b/src/views/LabwhereReception.vue @@ -99,7 +99,7 @@ import { ref, reactive, computed } from 'vue' import { scanBarcodesInLabwhereLocation, - exhaustSamplesIfDestroyed, + exhaustLibraryVolumeIfDestroyed, } from '@/services/labwhere/client.js' import useAlert from '@/composables/useAlert.js' @@ -174,12 +174,13 @@ const scanBarcodesToLabwhere = async () => { ) if (response.success) { let message = response.message - const {success,exhaustedLibraries} = await exhaustSamplesIfDestroyed( + const {success,exhaustedLibraries} = await exhaustLibraryVolumeIfDestroyed( location_barcode.value, uniqueBarcodesArray.value, ) if (success && exhaustedLibraries.length > 0) { - message += ` and sample volumes have been exhausted for ${exhaustedLibraries.length} libraries }` + const length = exhaustedLibraries.length + message += ` and sample volumes have been exhausted for ${length} ${length === 1 ? 'library' : 'libraries'}` } showAlert(message, 'success') } else { From 6ffb245bc03cc2ce5438bb7c82caf9c0b45d5570 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:49:07 +0000 Subject: [PATCH 10/25] tests --- tests/unit/services/labwhere/client.spec.js | 23 +++++++------- tests/unit/views/LabWhereReception.spec.js | 35 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/tests/unit/services/labwhere/client.spec.js b/tests/unit/services/labwhere/client.spec.js index 87068eec0..47f7048d3 100644 --- a/tests/unit/services/labwhere/client.spec.js +++ b/tests/unit/services/labwhere/client.spec.js @@ -1,7 +1,7 @@ import { getLabwhereLocations, scanBarcodesInLabwhereLocation, - exhaustSamplesIfDestroyed, + exhaustLibraryVolumeIfDestroyed, } from '@/services/labwhere/client.js' import LabwhereLocationsFactory from '@tests/factories/LabwhereLocationsFactory.js' import * as pacbioLibraryUtilities from '@/stores/utilities/pacbioLibraries.js' @@ -10,6 +10,7 @@ import { beforeEach, describe, it } from 'vitest' const mockFetch = vi.fn() const labwhereLocationsFactory = LabwhereLocationsFactory() + beforeEach(() => { global.fetch = mockFetch }) @@ -118,7 +119,7 @@ describe('scanBarcodesInLabwhereLocation', () => { }) }) -describe('exhaustSamplesIfDestroyed', () => { +describe('exhaustLibraryVolumeIfDestroyed', () => { beforeEach(() => { const pinia = createPinia() setActivePinia(pinia) @@ -152,8 +153,6 @@ describe('exhaustSamplesIfDestroyed', () => { beforeEach(() => { vi.clearAllMocks() - // Mock the environment variable - import.meta.env = { VITE_DESTROYED_LOCATION_BARCODE: destroyLocation } mockFetchLibraries = vi.fn() mockFormatAndTransformLibraries = vi.fn() mockExhaustLibrayVolume = vi.fn() @@ -166,7 +165,7 @@ describe('exhaustSamplesIfDestroyed', () => { }) it('should return false if locationBarcode does not match destroyLocation', async () => { - const result = await exhaustSamplesIfDestroyed('wrong-location', labwareBarcodes) + const result = await exhaustLibraryVolumeIfDestroyed('wrong-location', labwareBarcodes) expect(mockFetchLibraries).not.toHaveBeenCalled() expect(result).toEqual({ success: false }) }) @@ -180,7 +179,7 @@ describe('exhaustSamplesIfDestroyed', () => { mockExhaustLibrayVolume.mockResolvedValue({ success: true }) }) it('should fetch libraries by source_identifier and barcode', async () => { - await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(mockFetchLibraries).toHaveBeenCalledTimes(2) expect(mockFetchLibraries).toHaveBeenCalledWith({ filter: { source_identifier: 'barcode1,barcode2,barcode3' }, @@ -188,12 +187,12 @@ describe('exhaustSamplesIfDestroyed', () => { expect(mockFetchLibraries).toHaveBeenCalledWith({ filter: { barcode: 'barcode2,barcode3' } }) }) it('should return exhaused libraries', async () => { - const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(result).toEqual({success: true, exhaustedLibraries:formattedLibraries}) }) it('should exhaust library volumes', async () => { - await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(mockExhaustLibrayVolume).toHaveBeenCalledTimes(2) expect(mockExhaustLibrayVolume).toHaveBeenCalledWith(formattedLibraries[0]) expect(mockExhaustLibrayVolume).toHaveBeenCalledWith(formattedLibraries[1]) @@ -204,7 +203,7 @@ describe('exhaustSamplesIfDestroyed', () => { mockFetchLibraries.mockResolvedValueOnce({ success: false }) }) it('should return an empty array', async () => { - const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(result).toEqual({ success: false }) expect(mockExhaustLibrayVolume).not.toHaveBeenCalled() }) @@ -215,7 +214,7 @@ describe('exhaustSamplesIfDestroyed', () => { mockFormatAndTransformLibraries.mockReturnValueOnce([]) }) it('should return an empty array', async () => { - const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(result).toEqual({success: false}) }) }) @@ -224,7 +223,7 @@ describe('exhaustSamplesIfDestroyed', () => { mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[0]) mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[0]]) mockExhaustLibrayVolume.mockResolvedValue({ success: false }) - const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(result).toEqual({success:false, exhaustedLibraries: []}) }) it('should not return libraries for which the exhaustLibrayVolume fails ', async () => { @@ -234,7 +233,7 @@ describe('exhaustSamplesIfDestroyed', () => { mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[1]]) mockExhaustLibrayVolume.mockResolvedValueOnce({ success: true }) mockExhaustLibrayVolume.mockResolvedValueOnce({ success: false }) - const result = await exhaustSamplesIfDestroyed(destroyLocation, labwareBarcodes) + const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) expect(result).toEqual({success:true, exhaustedLibraries: [formattedLibraries[0]]}) }) }) diff --git a/tests/unit/views/LabWhereReception.spec.js b/tests/unit/views/LabWhereReception.spec.js index feb8e3bf7..33eec7020 100644 --- a/tests/unit/views/LabWhereReception.spec.js +++ b/tests/unit/views/LabWhereReception.spec.js @@ -1,6 +1,8 @@ import { mount } from '@support/testHelper' import LabwhereReception from '@/views/LabwhereReception.vue' import { scanBarcodesInLabwhereLocation } from '@/services/labwhere/client.js' +import * as labwhereClient from '@/services/labwhere/client.js' +import { beforeEach } from 'vitest' vi.mock('@/services/labwhere/client.js') @@ -12,10 +14,16 @@ vi.mock('@/composables/useAlert', () => ({ })) describe('LabWhereReception', () => { + let mockExhaustSamples; const buildWrapper = () => { return mount(LabwhereReception) } + beforeEach(() => { + mockExhaustSamples = vi.spyOn(labwhereClient, 'exhaustLibraryVolumeIfDestroyed') + mockExhaustSamples.mockResolvedValue({ success: false }) + }) + it('has a user code input field', () => { const wrapper = buildWrapper() expect(wrapper.find('#userCode').exists()).toBe(true) @@ -55,7 +63,6 @@ describe('LabWhereReception', () => { success: true, message: 'barcode1 successfully stored in location123', }) - await wrapper.find('#submit-button').trigger('submit') expect(scanBarcodesInLabwhereLocation).toBeCalledWith('user1', 'location1', 'barcode1', null) }) @@ -90,6 +97,7 @@ describe('LabWhereReception', () => { success: true, message: 'barcode1, barcode2 successfully stored in location123', }) + await wrapper.find('#submit-button').trigger('submit') expect(scanBarcodesInLabwhereLocation).toHaveBeenCalledWith( @@ -102,6 +110,31 @@ describe('LabWhereReception', () => { 'barcode1, barcode2 successfully stored in location123', 'success', ) + expect(mockExhaustSamples).toBeCalledWith('location123', ['barcode1', 'barcode2']) + }) + + it('validates form and exhaust libraries successfully', async () => { + const wrapper = buildWrapper() + wrapper.vm.user_code = 'user123' + wrapper.vm.location_barcode = 'location123' + wrapper.vm.labware_barcodes = 'barcode1\nbarcode2' + scanBarcodesInLabwhereLocation.mockResolvedValue({ + success: true, + message: 'barcode1, barcode2 successfully stored in location123', + }) + mockExhaustSamples.mockResolvedValue({ success: true, exhaustedLibraries: [{barcode:'barcode1'}] }) + + await wrapper.find('#submit-button').trigger('submit') + expect(scanBarcodesInLabwhereLocation).toHaveBeenCalledWith( + 'user123', + 'location123', + 'barcode1\nbarcode2', + null, + ) + expect(mockShowAlert).toBeCalledWith( + 'barcode1, barcode2 successfully stored in location123 and sample volumes have been exhausted for 1 library', + 'success', + ) }) it('displays preview message when user enters values in the form', async () => { From f77f9e882205f4728d69dbe0f203ce964b4f73c7 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:51:32 +0000 Subject: [PATCH 11/25] refactor: remove createLibrariesArray --- tests/factories/PacbioLibraryFactory.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/factories/PacbioLibraryFactory.js b/tests/factories/PacbioLibraryFactory.js index 0690be629..b97b6a6c7 100644 --- a/tests/factories/PacbioLibraryFactory.js +++ b/tests/factories/PacbioLibraryFactory.js @@ -1,6 +1,5 @@ import BaseFactory from './BaseFactory.js' import { groupIncludedByResource, dataToObjectById } from '../../src/api/JsonApi.js' -import {formatAndTransformLibraries} from '../../src/stores/utilities/pacbioLibraries.js' /** * @function createStoreData @@ -20,15 +19,6 @@ const createStoreData = ({ data, included }) => { } } -/** - * @function createLibrariesArray - * @param {Object} storeData - libraries, tubes, tags, requests - * @returns {Array} An array of libraries with sample_name, barcode, and group_id. - * @description A function that creates an array of libraries with sample_name, barcode, and group_id. - */ -const createLibrariesArray = ({ libraries, tubes, tags, requests }) => { - return formatAndTransformLibraries(libraries, tubes, tags, requests) -} /* * Factory for creating a pacbio library @@ -735,8 +725,7 @@ const PacbioLibraryFactory = ({ relationships = true } = {}) => { // certain tests require data with no relationships if (relationships) { const storeData = createStoreData(data) - const librariesArray = createLibrariesArray(storeData) - return { ...BaseFactory(data), storeData, librariesArray } + return { ...BaseFactory(data), storeData } } else { // take all the relationships out of the data const dataWithoutRelationships = data.data.map(({ id, type, attributes }) => ({ From c96544e2ff6f41b7399293bfd7a13f8b7c959ec0 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:51:36 +0000 Subject: [PATCH 12/25] Update pacbioLibraries.spec.js --- tests/unit/stores/pacbioLibraries.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/stores/pacbioLibraries.spec.js b/tests/unit/stores/pacbioLibraries.spec.js index 7d8a1ede1..0a8cd32b1 100644 --- a/tests/unit/stores/pacbioLibraries.spec.js +++ b/tests/unit/stores/pacbioLibraries.spec.js @@ -8,7 +8,7 @@ import { import { usePacbioLibrariesStore } from '@/stores/pacbioLibraries.js' import { beforeEach, describe, expect } from 'vitest' import PacbioLibraryFactory from '@tests/factories/PacbioLibraryFactory.js' -import { libraryPayload } from '@/stores/utilities/pacbioLibraries.js' +import { libraryPayload , formatAndTransformLibraries} from '@/stores/utilities/pacbioLibraries.js' const pacbioLibraryFactory = PacbioLibraryFactory() const pacbioLibraryWithoutRelationships = PacbioLibraryFactory({ relationships: false }) @@ -75,8 +75,9 @@ describe('usePacbioLibrariesStore', () => { const store = usePacbioLibrariesStore() store.$state = { ...pacbioLibraryFactory.storeData } + const { libraries, tubes, tags, requests } = pacbioLibraryFactory.storeData - expect(store.librariesArray).toEqual(pacbioLibraryFactory.librariesArray) + expect(store.librariesArray).toEqual(formatAndTransformLibraries(libraries, tubes, tags, requests)) }) }) describe('actions', () => { From 9546b611223da0325744ab3fac2b1eaa263a4b90 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:51:39 +0000 Subject: [PATCH 13/25] Update visit_labwhere_reception_page.cy.js --- .../specs/visit_labwhere_reception_page.cy.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/e2e/specs/visit_labwhere_reception_page.cy.js b/tests/e2e/specs/visit_labwhere_reception_page.cy.js index da0e6afd8..fb991b950 100644 --- a/tests/e2e/specs/visit_labwhere_reception_page.cy.js +++ b/tests/e2e/specs/visit_labwhere_reception_page.cy.js @@ -1,3 +1,6 @@ + +import PacbioLibraryFactory from '../../factories/PacbioLibraryFactory.js' + describe('Labware Reception page', () => { beforeEach(() => { cy.visit('#/labwhere-reception') @@ -34,6 +37,39 @@ describe('Labware Reception page', () => { cy.contains('barcode1, barcode2 successfully stored in location123') }) + it('exhausts samples when scanning to a destroyed location ', () => { + cy.get('#userCode').type('user123') + cy.get('#locationBarcode').type('lw-destroyed-217') + cy.get('#labware_barcodes').type('barcode1\nbarcode2') + const pacbioLibraryFactory = PacbioLibraryFactory() + + cy.wrap(pacbioLibraryFactory).as('pacbioLibraryFactory') + cy.get('@pacbioLibraryFactory').then((pacbioLibraryFactory) => { + cy.intercept('/v1/pacbio/libraries?filter[barcode]=barcode1,barcode2&include=request,tag,tube', { + statusCode: 200, + body: pacbioLibraryFactory.content, + }) + cy.intercept('PATCH', '/v1/pacbio/libraries/722', { + statusCode: 200, + body: { + data: {}, + }, + }) + }) + cy.intercept('POST', '/api/scans', { + statusCode: 201, + body: { + message: 'barcode1, barcode2 successfully stored in location123', + }, + }).as('storeBarcodes') + + cy.get('#submit-button').click() + + cy.wait('@storeBarcodes').its('response.statusCode').should('eq', 201) + cy.contains('barcode1, barcode2 successfully stored in location123 and sample volumes have been exhausted for 1 library') + }) + + it('displays error when POST is unsuccessful', () => { cy.get('#userCode').type('user123') cy.get('#locationBarcode').type('location123') From 54d03a4238592d899bb317002f1ba3a017d28bfd Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:58:25 +0000 Subject: [PATCH 14/25] added documentation and prettier updates --- src/services/labwhere/client.js | 18 +++++++++++++--- src/stores/pacbioLibraries.js | 19 ++++++++--------- src/stores/utilities/pacbioLibraries.js | 21 +++++++++++++++++++ src/views/LabwhereReception.vue | 3 ++- src/views/pacbio/PacbioLibraryIndex.vue | 2 +- .../specs/visit_labwhere_reception_page.cy.js | 21 +++++++++++-------- tests/factories/PacbioLibraryFactory.js | 1 - tests/unit/services/labwhere/client.spec.js | 9 ++++---- tests/unit/stores/pacbioLibraries.spec.js | 8 +++---- tests/unit/views/LabWhereReception.spec.js | 10 +++++---- 10 files changed, 74 insertions(+), 38 deletions(-) diff --git a/src/services/labwhere/client.js b/src/services/labwhere/client.js index ec4e99829..427d3db96 100644 --- a/src/services/labwhere/client.js +++ b/src/services/labwhere/client.js @@ -95,11 +95,18 @@ const scanBarcodesInLabwhereLocation = async ( ) return { success: response.success, errors: response.errors, message: response.data.message } } - +/** + * Exhausts the volume of libraries if the location barcode matches the destroy location. + * + * @param {string} locationBarcode - The barcode of the location. + * @param {Array} labwareBarcodes - The barcodes of the labware. + * @returns {Promise} - An object containing the success status and the exhausted libraries. + */ const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) => { if (locationBarcode !== destroyLocation) return { success: false } let librariesToDestroy = [] + //Fetch libraries by filter key const fetchAndMergeLibraries = async (barcodes, filterKey) => { const filterOptions = { filter: { [filterKey]: barcodes.join(',') } } const { success, libraries, tubes, tags, requests } = await fetchLibraries(filterOptions) @@ -110,6 +117,8 @@ const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) ] } } + + //Fetch libraries by source_identifier await fetchAndMergeLibraries(labwareBarcodes, 'source_identifier') const barcodesNotFetchedAsSourceIdentifer = labwareBarcodes.filter( (barcode) => !librariesToDestroy.some((library) => library.source_identifier === barcode), @@ -119,8 +128,11 @@ const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) //Fetch libraries which are not found by barcode await fetchAndMergeLibraries(barcodesNotFetchedAsSourceIdentifer, 'barcode') } - if(!librariesToDestroy.length) return { success: false } + // If no libraries are found, return a failed response + if (!librariesToDestroy.length) return { success: false } const exhaustedLibraries = [] + + // Exhaust the volume of libraries await Promise.all( librariesToDestroy.map(async (library) => { const { success } = await exhaustLibrayVolume(library) @@ -129,6 +141,6 @@ const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) } }), ) - return { success: exhaustedLibraries.length>0, exhaustedLibraries } + return { success: exhaustedLibraries.length > 0, exhaustedLibraries } } export { getLabwhereLocations, scanBarcodesInLabwhereLocation, exhaustLibraryVolumeIfDestroyed } diff --git a/src/stores/pacbioLibraries.js b/src/stores/pacbioLibraries.js index bfbdc83dc..0d9e37e03 100644 --- a/src/stores/pacbioLibraries.js +++ b/src/stores/pacbioLibraries.js @@ -7,7 +7,7 @@ import { libraryPayload, fetchLibraries, updateLibrary, - formatAndTransformLibraries + formatAndTransformLibraries, } from '@/stores/utilities/pacbioLibraries.js' /** @@ -57,7 +57,6 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { associated with all libraries."*/ const tags = { ...state.tags, ...pacbioRootStore.tags } return formatAndTransformLibraries(state.libraries, state.tubes, tags, state.requests) - }, }, actions: { @@ -123,9 +122,10 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { * @returns {Promise<{success: boolean, errors: Array}>} - A promise that resolves to an object containing a success boolean and an array of errors. */ async fetchLibraries(filterOptions) { - const { success, data, meta, errors, libraries, tubes, tags, requests } = await fetchLibraries({ - ...filterOptions, - }) + const { success, data, meta, errors, libraries, tubes, tags, requests } = + await fetchLibraries({ + ...filterOptions, + }) if (success && data.length > 0) { this.libraries = libraries this.tubes = tubes @@ -136,11 +136,10 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { }, /** - * Updates the library with matchingid with all given field values. - * @param {*} libraryFields - The fields to update the library with. - * * @returns {Promise} A promise that resolves to an object. - * The object has a 'success' property that is true if the library was updated successfully and false otherwise. - * If 'success' is false, the object also has an 'errors' property with a message describing the error. + * Updates a library with the given fields and updates the store if successful. + * + * @param {Object} libraryFields - The fields of the library to update. + * @returns {Promise} - An object containing the success status and any errors. */ async updateLibrary(libraryFields) { const { success, errors } = await updateLibrary(libraryFields) diff --git a/src/stores/utilities/pacbioLibraries.js b/src/stores/utilities/pacbioLibraries.js index c8893fbb0..b9aa959f7 100644 --- a/src/stores/utilities/pacbioLibraries.js +++ b/src/stores/utilities/pacbioLibraries.js @@ -115,6 +115,15 @@ async function fetchLibraries(fetchOptions = {}) { return { success, data, errors, meta, libraries, tubes, tags, requests } } +/** + * Formats and transforms libraries. + * + * @param {Object} libraries - The libraries to format and transform. + * @param {Object} tubes - The tubes associated with the libraries. + * @param {Object} tags - The tags associated with the libraries. + * @param {Object} requests - The requests associated with the libraries. + * @returns {Array} - The formatted and transformed libraries. + */ const formatAndTransformLibraries = (libraries, tubes, tags, requests) => Object.values(libraries) .filter((library) => library.tube) @@ -133,12 +142,24 @@ const formatAndTransformLibraries = (libraries, tubes, tags, requests) => } }) +/** + * Exhausts the volume of a library. + * + * @param {Object} library - The library to exhaust the volume of. + * @returns {Promise} - An object containing the success status and any errors. + */ async function exhaustLibrayVolume(library) { library.volume = library.used_volume const { success, errors } = await updateLibrary(library) return { success, errors } } +/** + * Updates a library with the given fields and updates the store if successful. + * + * @param {Object} libraryFields - The fields of the library to update. + * @returns {Promise} - An object containing the success status and any errors. + */ async function updateLibrary(libraryFields) { //Validate the libraryFields to ensure that all required fields are present const valid = validateLibraryFields(libraryFields) diff --git a/src/views/LabwhereReception.vue b/src/views/LabwhereReception.vue index f45e7aae3..2a18d57c9 100644 --- a/src/views/LabwhereReception.vue +++ b/src/views/LabwhereReception.vue @@ -174,7 +174,8 @@ const scanBarcodesToLabwhere = async () => { ) if (response.success) { let message = response.message - const {success,exhaustedLibraries} = await exhaustLibraryVolumeIfDestroyed( + // Check if the library volume need to be exhausted + const { success, exhaustedLibraries } = await exhaustLibraryVolumeIfDestroyed( location_barcode.value, uniqueBarcodesArray.value, ) diff --git a/src/views/pacbio/PacbioLibraryIndex.vue b/src/views/pacbio/PacbioLibraryIndex.vue index ffe9c05a6..ba5b7f55c 100644 --- a/src/views/pacbio/PacbioLibraryIndex.vue +++ b/src/views/pacbio/PacbioLibraryIndex.vue @@ -28,7 +28,7 @@
diff --git a/tests/e2e/specs/visit_labwhere_reception_page.cy.js b/tests/e2e/specs/visit_labwhere_reception_page.cy.js index fb991b950..577d1cbff 100644 --- a/tests/e2e/specs/visit_labwhere_reception_page.cy.js +++ b/tests/e2e/specs/visit_labwhere_reception_page.cy.js @@ -1,4 +1,3 @@ - import PacbioLibraryFactory from '../../factories/PacbioLibraryFactory.js' describe('Labware Reception page', () => { @@ -42,13 +41,16 @@ describe('Labware Reception page', () => { cy.get('#locationBarcode').type('lw-destroyed-217') cy.get('#labware_barcodes').type('barcode1\nbarcode2') const pacbioLibraryFactory = PacbioLibraryFactory() - + cy.wrap(pacbioLibraryFactory).as('pacbioLibraryFactory') - cy.get('@pacbioLibraryFactory').then((pacbioLibraryFactory) => { - cy.intercept('/v1/pacbio/libraries?filter[barcode]=barcode1,barcode2&include=request,tag,tube', { - statusCode: 200, - body: pacbioLibraryFactory.content, - }) + cy.get('@pacbioLibraryFactory').then((pacbioLibraryFactory) => { + cy.intercept( + '/v1/pacbio/libraries?filter[barcode]=barcode1,barcode2&include=request,tag,tube', + { + statusCode: 200, + body: pacbioLibraryFactory.content, + }, + ) cy.intercept('PATCH', '/v1/pacbio/libraries/722', { statusCode: 200, body: { @@ -66,10 +68,11 @@ describe('Labware Reception page', () => { cy.get('#submit-button').click() cy.wait('@storeBarcodes').its('response.statusCode').should('eq', 201) - cy.contains('barcode1, barcode2 successfully stored in location123 and sample volumes have been exhausted for 1 library') + cy.contains( + 'barcode1, barcode2 successfully stored in location123 and sample volumes have been exhausted for 1 library', + ) }) - it('displays error when POST is unsuccessful', () => { cy.get('#userCode').type('user123') cy.get('#locationBarcode').type('location123') diff --git a/tests/factories/PacbioLibraryFactory.js b/tests/factories/PacbioLibraryFactory.js index b97b6a6c7..e4469efa6 100644 --- a/tests/factories/PacbioLibraryFactory.js +++ b/tests/factories/PacbioLibraryFactory.js @@ -19,7 +19,6 @@ const createStoreData = ({ data, included }) => { } } - /* * Factory for creating a pacbio library * @returns a base factory object with the libraries data diff --git a/tests/unit/services/labwhere/client.spec.js b/tests/unit/services/labwhere/client.spec.js index 47f7048d3..d7a132d6d 100644 --- a/tests/unit/services/labwhere/client.spec.js +++ b/tests/unit/services/labwhere/client.spec.js @@ -10,7 +10,6 @@ import { beforeEach, describe, it } from 'vitest' const mockFetch = vi.fn() const labwhereLocationsFactory = LabwhereLocationsFactory() - beforeEach(() => { global.fetch = mockFetch }) @@ -188,7 +187,7 @@ describe('exhaustLibraryVolumeIfDestroyed', () => { }) it('should return exhaused libraries', async () => { const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) - expect(result).toEqual({success: true, exhaustedLibraries:formattedLibraries}) + expect(result).toEqual({ success: true, exhaustedLibraries: formattedLibraries }) }) it('should exhaust library volumes', async () => { @@ -215,7 +214,7 @@ describe('exhaustLibraryVolumeIfDestroyed', () => { }) it('should return an empty array', async () => { const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) - expect(result).toEqual({success: false}) + expect(result).toEqual({ success: false }) }) }) describe('when exhaustLibrayVolume fails', () => { @@ -224,7 +223,7 @@ describe('exhaustLibraryVolumeIfDestroyed', () => { mockFormatAndTransformLibraries.mockReturnValueOnce([formattedLibraries[0]]) mockExhaustLibrayVolume.mockResolvedValue({ success: false }) const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) - expect(result).toEqual({success:false, exhaustedLibraries: []}) + expect(result).toEqual({ success: false, exhaustedLibraries: [] }) }) it('should not return libraries for which the exhaustLibrayVolume fails ', async () => { mockFetchLibraries.mockResolvedValueOnce(fetchLibraryResponses[0]) @@ -234,7 +233,7 @@ describe('exhaustLibraryVolumeIfDestroyed', () => { mockExhaustLibrayVolume.mockResolvedValueOnce({ success: true }) mockExhaustLibrayVolume.mockResolvedValueOnce({ success: false }) const result = await exhaustLibraryVolumeIfDestroyed(destroyLocation, labwareBarcodes) - expect(result).toEqual({success:true, exhaustedLibraries: [formattedLibraries[0]]}) + expect(result).toEqual({ success: true, exhaustedLibraries: [formattedLibraries[0]] }) }) }) }) diff --git a/tests/unit/stores/pacbioLibraries.spec.js b/tests/unit/stores/pacbioLibraries.spec.js index 0a8cd32b1..6fbaf747b 100644 --- a/tests/unit/stores/pacbioLibraries.spec.js +++ b/tests/unit/stores/pacbioLibraries.spec.js @@ -8,7 +8,7 @@ import { import { usePacbioLibrariesStore } from '@/stores/pacbioLibraries.js' import { beforeEach, describe, expect } from 'vitest' import PacbioLibraryFactory from '@tests/factories/PacbioLibraryFactory.js' -import { libraryPayload , formatAndTransformLibraries} from '@/stores/utilities/pacbioLibraries.js' +import { libraryPayload, formatAndTransformLibraries } from '@/stores/utilities/pacbioLibraries.js' const pacbioLibraryFactory = PacbioLibraryFactory() const pacbioLibraryWithoutRelationships = PacbioLibraryFactory({ relationships: false }) @@ -77,7 +77,9 @@ describe('usePacbioLibrariesStore', () => { store.$state = { ...pacbioLibraryFactory.storeData } const { libraries, tubes, tags, requests } = pacbioLibraryFactory.storeData - expect(store.librariesArray).toEqual(formatAndTransformLibraries(libraries, tubes, tags, requests)) + expect(store.librariesArray).toEqual( + formatAndTransformLibraries(libraries, tubes, tags, requests), + ) }) }) describe('actions', () => { @@ -165,7 +167,6 @@ describe('usePacbioLibrariesStore', () => { get.mockResolvedValue(pacbioLibraryFactory.responses.fetch) const { success, errors } = await store.fetchLibraries() - const expectedLibrary = Object.values(pacbioLibraryFactory.storeData.libraries)[0] expect(store.libraries[expectedLibrary.id]).toEqual(expectedLibrary) @@ -194,7 +195,6 @@ describe('usePacbioLibrariesStore', () => { expect(success).toEqual(true) expect(errors).toEqual([]) }) - }) describe('#updateLibrary', () => { diff --git a/tests/unit/views/LabWhereReception.spec.js b/tests/unit/views/LabWhereReception.spec.js index 33eec7020..df9a8c5c2 100644 --- a/tests/unit/views/LabWhereReception.spec.js +++ b/tests/unit/views/LabWhereReception.spec.js @@ -14,13 +14,13 @@ vi.mock('@/composables/useAlert', () => ({ })) describe('LabWhereReception', () => { - let mockExhaustSamples; + let mockExhaustSamples const buildWrapper = () => { return mount(LabwhereReception) } beforeEach(() => { - mockExhaustSamples = vi.spyOn(labwhereClient, 'exhaustLibraryVolumeIfDestroyed') + mockExhaustSamples = vi.spyOn(labwhereClient, 'exhaustLibraryVolumeIfDestroyed') mockExhaustSamples.mockResolvedValue({ success: false }) }) @@ -97,7 +97,6 @@ describe('LabWhereReception', () => { success: true, message: 'barcode1, barcode2 successfully stored in location123', }) - await wrapper.find('#submit-button').trigger('submit') expect(scanBarcodesInLabwhereLocation).toHaveBeenCalledWith( @@ -122,7 +121,10 @@ describe('LabWhereReception', () => { success: true, message: 'barcode1, barcode2 successfully stored in location123', }) - mockExhaustSamples.mockResolvedValue({ success: true, exhaustedLibraries: [{barcode:'barcode1'}] }) + mockExhaustSamples.mockResolvedValue({ + success: true, + exhaustedLibraries: [{ barcode: 'barcode1' }], + }) await wrapper.find('#submit-button').trigger('submit') expect(scanBarcodesInLabwhereLocation).toHaveBeenCalledWith( From c2074309e3f05bc286efcfe15c7a781d14d78ee7 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:05:41 +0000 Subject: [PATCH 15/25] update: removed v2 namespace --- src/stores/pacbioLibraries.js | 2 +- src/stores/utilities/pacbioLibraries.js | 6 +++--- tests/unit/stores/utilities/pacbioLibraries.spec.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/stores/pacbioLibraries.js b/src/stores/pacbioLibraries.js index e5dfa2ac8..22ffddd01 100644 --- a/src/stores/pacbioLibraries.js +++ b/src/stores/pacbioLibraries.js @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import useRootStore from '@/stores/index.js' -import { handleResponse } from '@/api/v2/ResponseHelper.js' +import { handleResponse } from '@/api/ResponseHelper.js' import { groupIncludedByResource } from '@/api/JsonApi.js' import { usePacbioRootStore } from '@/stores/pacbioRoot.js' import { diff --git a/src/stores/utilities/pacbioLibraries.js b/src/stores/utilities/pacbioLibraries.js index b9aa959f7..5d7105ccd 100644 --- a/src/stores/utilities/pacbioLibraries.js +++ b/src/stores/utilities/pacbioLibraries.js @@ -1,5 +1,5 @@ import useRootStore from '@/stores' -import { handleResponse } from '@/api/v2/ResponseHelper.js' +import { handleResponse } from '@/api/ResponseHelper.js' import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js' /** @@ -92,7 +92,7 @@ async function fetchLibraries(fetchOptions = {}) { include: Array.from(includes).join(','), } const rootStore = useRootStore() - const pacbioLibraries = rootStore.api.v2.traction.pacbio.libraries + const pacbioLibraries = rootStore.api.traction.pacbio.libraries const promise = pacbioLibraries.get(fetchOptionsDefaultInclude) const response = await handleResponse(promise) @@ -167,7 +167,7 @@ async function updateLibrary(libraryFields) { return valid } const rootStore = useRootStore() - const request = rootStore.api.v2.traction.pacbio.libraries + const request = rootStore.api.traction.pacbio.libraries const payload = libraryPayload(libraryFields) const promise = request.update(payload) const { success, errors } = await handleResponse(promise) diff --git a/tests/unit/stores/utilities/pacbioLibraries.spec.js b/tests/unit/stores/utilities/pacbioLibraries.spec.js index 97e19e504..c7b813892 100644 --- a/tests/unit/stores/utilities/pacbioLibraries.spec.js +++ b/tests/unit/stores/utilities/pacbioLibraries.spec.js @@ -31,7 +31,7 @@ describe('pacbioLibraries', () => { beforeEach(() => { rootStore = useRootStore() get = vi.fn() - rootStore.api.v2.traction.pacbio.libraries.get = get + rootStore.api.traction.pacbio.libraries.get = get }) it('calls the api to fetch libraries with default includea', async () => { @@ -122,7 +122,7 @@ describe('pacbioLibraries', () => { beforeEach(() => { rootStore = useRootStore() update = vi.fn() - rootStore.api.v2.traction.pacbio.libraries.update = update + rootStore.api.traction.pacbio.libraries.update = update }) it('doesnt call update if required library fields are empty', async () => { const libraryFields = { @@ -202,7 +202,7 @@ describe('pacbioLibraries', () => { beforeEach(() => { rootStore = useRootStore() update = vi.fn() - rootStore.api.v2.traction.pacbio.libraries.update = update + rootStore.api.traction.pacbio.libraries.update = update }) it('exhausts library volume', async () => { const library = { From f9bcd24481753b51c9681f352ea39fdfe416ed47 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:35:34 +0000 Subject: [PATCH 16/25] feat: add PacbioLibrary service for fetching and updating library resources --- src/services/traction/PacbioLibrary.js | 103 +++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/services/traction/PacbioLibrary.js diff --git a/src/services/traction/PacbioLibrary.js b/src/services/traction/PacbioLibrary.js new file mode 100644 index 000000000..8f9e75da3 --- /dev/null +++ b/src/services/traction/PacbioLibrary.js @@ -0,0 +1,103 @@ +import { handleResponse } from '@/api/ResponseHelper.js' +import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js' +import useRootStore from '@/stores/index.js' + +/** + * Fetches all libraries. + * + * @param {Object} getPacbioLibraryResources - The options to fetch libraries with. + * The options include page, filter, and include. + * e.g { page: { "size": "24", "number": "1"}, filter: { source_identifier: 'sample1' }, include: 'request,tag,tube' } + */ +async function getPacbioLibraryResources(fetchOptions = {}) { + const includes = new Set(fetchOptions.include ? fetchOptions.include.split(',') : []) + const requiredIncludes = ['request', 'tag', 'tube'] + requiredIncludes.forEach((item) => includes.add(item)) + + const fetchOptionsDefaultInclude = { + ...fetchOptions, + include: Array.from(includes).join(','), + } + const request = useRootStore().api.traction.pacbio.libraries + const response = await handleResponse(request.get(fetchOptionsDefaultInclude)) + + const { success, body: { data, included = [], meta = {} } = {}, errors = [] } = response + let libraries = {}, + tubes = {}, + tags = {}, + requests = {} + if (success && data && data.length > 0) { + const { + tubes: included_tubes, + tags: included_tags, + requests: included_requests, + } = groupIncludedByResource(included) + libraries = dataToObjectById({ data, includeRelationships: true }) + tubes = dataToObjectById({ data: included_tubes }) + tags = dataToObjectById({ data: included_tags }) + requests = dataToObjectById({ data: included_requests }) + } + return { success, data, errors, meta, libraries, tubes, tags, requests } +} + +/** + * + * @param {Integer | String} id - id of the library + * @param {Integer | String} pacbio_request_id - id of the pacbio request + * @param {String} template_prep_kit_box_barcode - barcode of the template prep kit box + * @param {Integer | String} tag_id - id of the tag + * @param {Float} concentration - concentration of the library + * @param {Float} volume - volume of the library + * @param {Float} insert_size - insert size of the library + * @returns {Object} - payload for creating a library + * if it is an update id is added otherwise pacbio_request_id is added + * + */ +const buildLibraryResourcePayload = ({ + id, + pacbio_request_id, + template_prep_kit_box_barcode, + tag_id, + concentration, + volume, + insert_size, +}) => { + const requiredAttributes = { + template_prep_kit_box_barcode, + tag_id, + concentration, + volume, + insert_size, + } + + const payload = { + data: { + type: 'libraries', + attributes: { + ...requiredAttributes, + primary_aliquot_attributes: { + ...requiredAttributes, + }, + }, + }, + } + + id ? (payload.data.id = id) : (payload.data.attributes.pacbio_request_id = pacbio_request_id) + + return payload +} + +/** + * Updates a library with the given fields and updates the store if successful. + * + * @param {Object} libraryFields - The fields of the library to update. + * @returns {Promise} - An object containing the success status and any errors. + */ +async function updatePacbioLibraryResource(libraryFields) { + const request = useRootStore().api.traction.pacbio.libraries + const promise = request.update(buildLibraryResourcePayload(libraryFields)) + const { success, errors } = await handleResponse(promise) + return { success, errors } +} + +export { getPacbioLibraryResources, updatePacbioLibraryResource, buildLibraryResourcePayload } From c99b7cb00d9ef1be7c81596c6f774cc748efc771 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:35:54 +0000 Subject: [PATCH 17/25] refactor: replace fetchLibraries with getPacbioLibraryResources in labwhere client --- src/services/labwhere/client.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/labwhere/client.js b/src/services/labwhere/client.js index 427d3db96..6cec2f3ec 100644 --- a/src/services/labwhere/client.js +++ b/src/services/labwhere/client.js @@ -1,10 +1,10 @@ import { extractLocationsForLabwares } from './helpers.js' import { FetchWrapper } from '@/api/FetchWrapper.js' import { - fetchLibraries, exhaustLibrayVolume, formatAndTransformLibraries, } from '@/stores/utilities/pacbioLibraries.js' +import { getPacbioLibraryResources } from '@/services/traction/PacbioLibrary.js' const labwhereFetch = FetchWrapper(import.meta.env['VITE_LABWHERE_BASE_URL'], 'LabWhere') const destroyLocation = import.meta.env['VITE_DESTROYED_LOCATION_BARCODE'] @@ -109,7 +109,9 @@ const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) //Fetch libraries by filter key const fetchAndMergeLibraries = async (barcodes, filterKey) => { const filterOptions = { filter: { [filterKey]: barcodes.join(',') } } - const { success, libraries, tubes, tags, requests } = await fetchLibraries(filterOptions) + + const { success, libraries, tubes, tags, requests } = + await getPacbioLibraryResources(filterOptions) if (success) { librariesToDestroy = [ ...librariesToDestroy, From 8a46b564b018fc005ab76d8f87cf72f6a209fe2c Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:36:12 +0000 Subject: [PATCH 18/25] refactor: update library resource handling in pacbioLibraries store --- src/stores/pacbioLibraries.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/stores/pacbioLibraries.js b/src/stores/pacbioLibraries.js index 22ffddd01..686d2cac9 100644 --- a/src/stores/pacbioLibraries.js +++ b/src/stores/pacbioLibraries.js @@ -4,9 +4,11 @@ import { handleResponse } from '@/api/ResponseHelper.js' import { groupIncludedByResource } from '@/api/JsonApi.js' import { usePacbioRootStore } from '@/stores/pacbioRoot.js' import { - libraryPayload, - fetchLibraries, - updateLibrary, + getPacbioLibraryResources, + buildLibraryResourcePayload, +} from '@/services/traction/PacbioLibrary.js' +import { + validateAndUpdateLibrary, formatAndTransformLibraries, } from '@/stores/utilities/pacbioLibraries.js' @@ -79,7 +81,7 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { }) { const rootState = useRootStore() const request = rootState.api.traction.pacbio.libraries - const payload = libraryPayload({ + const payload = buildLibraryResourcePayload({ pacbio_request_id, template_prep_kit_box_barcode, tag_id, @@ -123,7 +125,7 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { */ async fetchLibraries(filterOptions) { const { success, data, meta, errors, libraries, tubes, tags, requests } = - await fetchLibraries({ + await getPacbioLibraryResources({ ...filterOptions, }) if (success && data.length > 0) { @@ -142,7 +144,7 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', { * @returns {Promise} - An object containing the success status and any errors. */ async updateLibrary(libraryFields) { - const { success, errors } = await updateLibrary(libraryFields) + const { success, errors } = await validateAndUpdateLibrary(libraryFields) if (success) { //Update all fields of the library in the store with matching ID with the given values. this.libraries[libraryFields.id] = { From 995d5cc62487a3c00bffbc4aec22724b5bd54afb Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:36:33 +0000 Subject: [PATCH 19/25] refactor: simplify library update logic and remove unused fetchLibraries function --- src/stores/utilities/pacbioLibraries.js | 109 ++---------------------- 1 file changed, 5 insertions(+), 104 deletions(-) diff --git a/src/stores/utilities/pacbioLibraries.js b/src/stores/utilities/pacbioLibraries.js index 5d7105ccd..d3a88d942 100644 --- a/src/stores/utilities/pacbioLibraries.js +++ b/src/stores/utilities/pacbioLibraries.js @@ -1,53 +1,4 @@ -import useRootStore from '@/stores' -import { handleResponse } from '@/api/ResponseHelper.js' -import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js' - -/** - * - * @param {Integer | String} id - id of the library - * @param {Integer | String} pacbio_request_id - id of the pacbio request - * @param {String} template_prep_kit_box_barcode - barcode of the template prep kit box - * @param {Integer | String} tag_id - id of the tag - * @param {Float} concentration - concentration of the library - * @param {Float} volume - volume of the library - * @param {Float} insert_size - insert size of the library - * @returns {Object} - payload for creating a library - * if it is an update id is added otherwise pacbio_request_id is added - * - */ -const libraryPayload = ({ - id, - pacbio_request_id, - template_prep_kit_box_barcode, - tag_id, - concentration, - volume, - insert_size, -}) => { - const requiredAttributes = { - template_prep_kit_box_barcode, - tag_id, - concentration, - volume, - insert_size, - } - - const payload = { - data: { - type: 'libraries', - attributes: { - ...requiredAttributes, - primary_aliquot_attributes: { - ...requiredAttributes, - }, - }, - }, - } - - id ? (payload.data.id = id) : (payload.data.attributes.pacbio_request_id = pacbio_request_id) - - return payload -} +import { updatePacbioLibraryResource } from '@/services/traction/PacbioLibrary.js' /** * @function validateFields @@ -75,46 +26,6 @@ const validateLibraryFields = (library) => { } } -/** - * Fetches all libraries. - * - * @param {Object} fetchOptions - The options to fetch libraries with. - * The options include page, filter, and include. - * e.g { page: { "size": "24", "number": "1"}, filter: { source_identifier: 'sample1' }, include: 'request,tag,tube' } - */ -async function fetchLibraries(fetchOptions = {}) { - const includes = new Set(fetchOptions.include ? fetchOptions.include.split(',') : []) - const requiredIncludes = ['request', 'tag', 'tube'] - requiredIncludes.forEach((item) => includes.add(item)) - - const fetchOptionsDefaultInclude = { - ...fetchOptions, - include: Array.from(includes).join(','), - } - const rootStore = useRootStore() - const pacbioLibraries = rootStore.api.traction.pacbio.libraries - const promise = pacbioLibraries.get(fetchOptionsDefaultInclude) - const response = await handleResponse(promise) - - const { success, body: { data, included = [], meta = {} } = {}, errors = [] } = response - let libraries = {}, - tubes = {}, - tags = {}, - requests = {} - if (success && data && data.length > 0) { - const { - tubes: included_tubes, - tags: included_tags, - requests: included_requests, - } = groupIncludedByResource(included) - libraries = dataToObjectById({ data, includeRelationships: true }) - tubes = dataToObjectById({ data: included_tubes }) - tags = dataToObjectById({ data: included_tags }) - requests = dataToObjectById({ data: included_requests }) - } - return { success, data, errors, meta, libraries, tubes, tags, requests } -} - /** * Formats and transforms libraries. * @@ -150,7 +61,7 @@ const formatAndTransformLibraries = (libraries, tubes, tags, requests) => */ async function exhaustLibrayVolume(library) { library.volume = library.used_volume - const { success, errors } = await updateLibrary(library) + const { success, errors } = await validateAndUpdateLibrary(library) return { success, errors } } @@ -160,24 +71,14 @@ async function exhaustLibrayVolume(library) { * @param {Object} libraryFields - The fields of the library to update. * @returns {Promise} - An object containing the success status and any errors. */ -async function updateLibrary(libraryFields) { +async function validateAndUpdateLibrary(libraryFields) { //Validate the libraryFields to ensure that all required fields are present const valid = validateLibraryFields(libraryFields) if (!valid.success) { return valid } - const rootStore = useRootStore() - const request = rootStore.api.traction.pacbio.libraries - const payload = libraryPayload(libraryFields) - const promise = request.update(payload) - const { success, errors } = await handleResponse(promise) + const { success, errors } = await updatePacbioLibraryResource(libraryFields) return { success, errors } } -export { - libraryPayload, - fetchLibraries, - updateLibrary, - exhaustLibrayVolume, - formatAndTransformLibraries, -} +export { validateAndUpdateLibrary, exhaustLibrayVolume, formatAndTransformLibraries } From c550d0c3aed3d58db8d1c0af22e27c47cd06ddbe Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:36:47 +0000 Subject: [PATCH 20/25] test updates --- tests/unit/services/labwhere/client.spec.js | 3 +- .../services/traction/PacbioLibrary.spec.js | 110 ++++++++++++++++++ tests/unit/stores/pacbioLibraries.spec.js | 9 +- .../stores/utilities/pacbioLibraries.spec.js | 84 ++----------- 4 files changed, 124 insertions(+), 82 deletions(-) create mode 100644 tests/unit/services/traction/PacbioLibrary.spec.js diff --git a/tests/unit/services/labwhere/client.spec.js b/tests/unit/services/labwhere/client.spec.js index d7a132d6d..5aee9fdc4 100644 --- a/tests/unit/services/labwhere/client.spec.js +++ b/tests/unit/services/labwhere/client.spec.js @@ -5,6 +5,7 @@ import { } from '@/services/labwhere/client.js' import LabwhereLocationsFactory from '@tests/factories/LabwhereLocationsFactory.js' import * as pacbioLibraryUtilities from '@/stores/utilities/pacbioLibraries.js' +import * as pacbioLibraryService from '@/services/traction/PacbioLibrary.js' import { createPinia, setActivePinia } from '@support/testHelper.js' import { beforeEach, describe, it } from 'vitest' const mockFetch = vi.fn() @@ -155,7 +156,7 @@ describe('exhaustLibraryVolumeIfDestroyed', () => { mockFetchLibraries = vi.fn() mockFormatAndTransformLibraries = vi.fn() mockExhaustLibrayVolume = vi.fn() - mockFetchLibraries = vi.spyOn(pacbioLibraryUtilities, 'fetchLibraries') + mockFetchLibraries = vi.spyOn(pacbioLibraryService, 'getPacbioLibraryResources') mockFormatAndTransformLibraries = vi.spyOn( pacbioLibraryUtilities, 'formatAndTransformLibraries', diff --git a/tests/unit/services/traction/PacbioLibrary.spec.js b/tests/unit/services/traction/PacbioLibrary.spec.js new file mode 100644 index 000000000..577aa1adf --- /dev/null +++ b/tests/unit/services/traction/PacbioLibrary.spec.js @@ -0,0 +1,110 @@ +import useRootStore from '@/stores' +import PacbioLibraryFactory from '@tests/factories/PacbioLibraryFactory.js' +import { + getPacbioLibraryResources, + updatePacbioLibraryResource, + buildLibraryResourcePayload, +} from '@/services/traction/PacbioLibrary.js' +import { + createPinia, + setActivePinia, + failedResponse, + successfulResponse, +} from '@support/testHelper.js' + +const pacbioLibraryFactory = PacbioLibraryFactory() + +describe('PacbioLibrary', () => { + let rootStore + + beforeEach(() => { + /*Creates a fresh pinia instance and make it active so it's automatically picked + up by any useStore() call without having to pass it to it for e.g `useStore(pinia)`*/ + const pinia = createPinia() + setActivePinia(pinia) + }) + + describe('getPacbioLibraryResources', () => { + let get + + beforeEach(() => { + rootStore = useRootStore() + get = vi.fn() + rootStore.api.traction.pacbio.libraries.get = get + }) + + it('calls the api to fetch libraries with default includea', async () => { + const fetchOptions = { filter: { source_identifier: 'sample1' } } + await getPacbioLibraryResources(fetchOptions) + expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'request,tag,tube' }) + }) + + it('calls the api to fetch libraries with custom includes along with default includes', async () => { + const fetchOptions = { include: 'test' } + await getPacbioLibraryResources(fetchOptions) + expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'test,request,tag,tube' }) + }) + it('calls api to fetch libraries with joined includes if custom includes includes default values', async () => { + const fetchOptions = { include: 'request,tag,tube,test' } + await getPacbioLibraryResources(fetchOptions) + expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'request,tag,tube,test' }) + }) + + it('calls api successfully', async () => { + get.mockResolvedValue(pacbioLibraryFactory.responses.fetch) + const { success, errors, libraries, tubes, requests } = await getPacbioLibraryResources() + expect(success).toEqual(true) + expect(errors).toEqual([]) + expect(libraries).toEqual(pacbioLibraryFactory.storeData.libraries) + expect(tubes).toEqual(pacbioLibraryFactory.storeData.tubes) + expect(requests).toEqual(pacbioLibraryFactory.storeData.requests) + }) + + it('unsuccessfully', async () => { + const failureResponse = failedResponse() + get.mockResolvedValue(failureResponse) + const { success, errors } = await getPacbioLibraryResources() + expect(success).toEqual(false) + expect(errors).toEqual(failureResponse.errorSummary) + }) + }) + + describe('updatePacbioLibraryResource', () => { + let update + beforeEach(() => { + rootStore = useRootStore() + update = vi.fn() + rootStore.api.traction.pacbio.libraries.update = update + }) + it('calls the api successfully', async () => { + const libraryFields = { + id: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + } + const mockResponse = successfulResponse() + update.mockResolvedValue(mockResponse) + const { success } = await updatePacbioLibraryResource(libraryFields) + expect(update).toHaveBeenCalledWith(buildLibraryResourcePayload(libraryFields)) + expect(success).toBeTruthy() + }) + it('unsuccessfully', async () => { + const libraryFields = { + id: 1, + tag_id: 1, + tube: 1, + concentration: 1, + volume: 1, + insert_size: 1, + } + const mockResponse = failedResponse() + update.mockResolvedValue(mockResponse) + const { success, errors } = await updatePacbioLibraryResource(libraryFields) + expect(success).toBeFalsy() + expect(errors).toEqual(mockResponse.errorSummary) + }) + }) +}) diff --git a/tests/unit/stores/pacbioLibraries.spec.js b/tests/unit/stores/pacbioLibraries.spec.js index c85c0d92f..7116d06b1 100644 --- a/tests/unit/stores/pacbioLibraries.spec.js +++ b/tests/unit/stores/pacbioLibraries.spec.js @@ -8,7 +8,8 @@ import { import { usePacbioLibrariesStore } from '@/stores/pacbioLibraries.js' import { beforeEach, describe, expect } from 'vitest' import PacbioLibraryFactory from '@tests/factories/PacbioLibraryFactory.js' -import { libraryPayload, formatAndTransformLibraries } from '@/stores/utilities/pacbioLibraries.js' +import { formatAndTransformLibraries } from '@/stores/utilities/pacbioLibraries.js' +import { buildLibraryResourcePayload } from '@/services/traction/PacbioLibrary.js' const pacbioLibraryFactory = PacbioLibraryFactory() const pacbioLibraryWithoutRelationships = PacbioLibraryFactory({ relationships: false }) @@ -36,7 +37,7 @@ describe('usePacbioLibrariesStore', () => { describe('#libraryPayload', () => { it('for a create action', () => { - const payload = libraryPayload({ ...requiredAttributes, pacbio_request_id: 1 }) + const payload = buildLibraryResourcePayload({ ...requiredAttributes, pacbio_request_id: 1 }) expect(Object.keys(payload.data).includes('id')).toBeFalsy() expect(payload).toEqual({ data: { @@ -53,7 +54,7 @@ describe('usePacbioLibrariesStore', () => { }) it('for an update action', () => { - const payload = libraryPayload({ ...requiredAttributes, id: 1 }) + const payload = buildLibraryResourcePayload({ ...requiredAttributes, id: 1 }) expect(Object.keys(payload.data.attributes).includes('pacbio_request_id')).toBeFalsy() expect(payload).toEqual({ data: { @@ -113,7 +114,7 @@ describe('usePacbioLibrariesStore', () => { create.mockResolvedValue(mockResponse) const { success, barcode } = await store.createLibrary(formLibrary) expect(create).toBeCalledWith({ - data: libraryPayload({ ...requiredAttributes, pacbio_request_id: 1 }), + data: buildLibraryResourcePayload({ ...requiredAttributes, pacbio_request_id: 1 }), include: 'tube,primary_aliquot', }) expect(success).toBeTruthy() diff --git a/tests/unit/stores/utilities/pacbioLibraries.spec.js b/tests/unit/stores/utilities/pacbioLibraries.spec.js index c7b813892..d3619d5d9 100644 --- a/tests/unit/stores/utilities/pacbioLibraries.spec.js +++ b/tests/unit/stores/utilities/pacbioLibraries.spec.js @@ -1,21 +1,11 @@ import useRootStore from '@/stores' +import { createPinia, setActivePinia, successfulResponse } from '@support/testHelper.js' import { - createPinia, - setActivePinia, - failedResponse, - successfulResponse, -} from '@support/testHelper.js' -import { - fetchLibraries, formatAndTransformLibraries, - updateLibrary, + validateAndUpdateLibrary, exhaustLibrayVolume, } from '@/stores/utilities/pacbioLibraries.js' -import PacbioLibraryFactory from '@tests/factories/PacbioLibraryFactory.js' import { beforeEach, describe, expect, it } from 'vitest' -import { libraryPayload } from '../../../../src/stores/utilities/pacbioLibraries' - -const pacbioLibraryFactory = PacbioLibraryFactory() describe('pacbioLibraries', () => { let rootStore @@ -25,50 +15,6 @@ describe('pacbioLibraries', () => { const pinia = createPinia() setActivePinia(pinia) }) - describe('fecthLibraries', () => { - let get - - beforeEach(() => { - rootStore = useRootStore() - get = vi.fn() - rootStore.api.traction.pacbio.libraries.get = get - }) - - it('calls the api to fetch libraries with default includea', async () => { - const fetchOptions = { filter: { source_identifier: 'sample1' } } - await fetchLibraries(fetchOptions) - expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'request,tag,tube' }) - }) - - it('calls the api to fetch libraries with custom includes along with default includes', async () => { - const fetchOptions = { include: 'test' } - await fetchLibraries(fetchOptions) - expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'test,request,tag,tube' }) - }) - it('calls api to fetch libraries with joined includes if custom includes includes default values', async () => { - const fetchOptions = { include: 'request,tag,tube,test' } - await fetchLibraries(fetchOptions) - expect(get).toHaveBeenCalledWith({ ...fetchOptions, include: 'request,tag,tube,test' }) - }) - - it('calls api successfully', async () => { - get.mockResolvedValue(pacbioLibraryFactory.responses.fetch) - const { success, errors, libraries, tubes, requests } = await fetchLibraries() - expect(success).toEqual(true) - expect(errors).toEqual([]) - expect(libraries).toEqual(pacbioLibraryFactory.storeData.libraries) - expect(tubes).toEqual(pacbioLibraryFactory.storeData.tubes) - expect(requests).toEqual(pacbioLibraryFactory.storeData.requests) - }) - - it('unsuccessfully', async () => { - const failureResponse = failedResponse() - get.mockResolvedValue(failureResponse) - const { success, errors } = await fetchLibraries() - expect(success).toEqual(false) - expect(errors).toEqual(failureResponse.errorSummary) - }) - }) describe('formatAndTransformLibraries', () => { it('formats and transforms libraries', () => { const libraries = { @@ -117,7 +63,7 @@ describe('pacbioLibraries', () => { ]) }) }) - describe('updateLibrary', () => { + describe('validateAndUpdateLibrary', () => { let update beforeEach(() => { rootStore = useRootStore() @@ -133,7 +79,7 @@ describe('pacbioLibraries', () => { volume: 1, insert_size: '', } - const { success, errors } = await updateLibrary(libraryFields) + const { success, errors } = await validateAndUpdateLibrary(libraryFields) expect(success).toBeFalsy() expect(errors).toEqual('Missing required field(s)') expect(update).not.toHaveBeenCalled() @@ -147,7 +93,7 @@ describe('pacbioLibraries', () => { volume: 0, insert_size: 1, } - await updateLibrary(libraryFields) + await validateAndUpdateLibrary(libraryFields) expect(update).toBeCalled() }) @@ -162,8 +108,7 @@ describe('pacbioLibraries', () => { } const mockResponse = successfulResponse() update.mockResolvedValue(mockResponse) - const { success } = await updateLibrary(libraryFields) - expect(update).toHaveBeenCalledWith(libraryPayload(libraryFields)) + const { success } = await validateAndUpdateLibrary(libraryFields) expect(success).toBeTruthy() }) it('should not return error if optional attributes are empty', async () => { @@ -177,24 +122,9 @@ describe('pacbioLibraries', () => { } const mockResponse = successfulResponse() update.mockResolvedValue(mockResponse) - const { success } = await updateLibrary(libraryFields) + const { success } = await validateAndUpdateLibrary(libraryFields) expect(success).toBeTruthy() }) - it('unsuccessfully', async () => { - const libraryFields = { - id: 1, - tag_id: 1, - tube: 1, - concentration: 1, - volume: 1, - insert_size: 1, - } - const mockResponse = failedResponse() - update.mockResolvedValue(mockResponse) - const { success, errors } = await updateLibrary(libraryFields) - expect(success).toBeFalsy() - expect(errors).toEqual(mockResponse.errorSummary) - }) }) describe('exhaustLibraryVolume', () => { From df19bef765ee07dd01ccdffe943c82b8507dedb5 Mon Sep 17 00:00:00 2001 From: Seena Nair <55585488+seenanair@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:15:16 +0000 Subject: [PATCH 21/25] feat: add exhausted badge to volume cell in PacbioLibraryIndex component --- src/stores/utilities/pacbioLibraries.js | 17 ++++++++++++++++- src/views/pacbio/PacbioLibraryIndex.vue | 24 +++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/stores/utilities/pacbioLibraries.js b/src/stores/utilities/pacbioLibraries.js index d3a88d942..61a2891b1 100644 --- a/src/stores/utilities/pacbioLibraries.js +++ b/src/stores/utilities/pacbioLibraries.js @@ -65,6 +65,16 @@ async function exhaustLibrayVolume(library) { return { success, errors } } +/** + * Checks if the library is exhausted. + * + * @param {Object} library - The library object to check. + * @param {number} library.volume - The current volume of the library. + * @param {number} library.used_volume - The used volume of the library. + * @returns {boolean} - Returns true if the library is exhausted, otherwise false. + */ +const isLibraryExhausted = (library) => library.volume === library.used_volume + /** * Updates a library with the given fields and updates the store if successful. * @@ -81,4 +91,9 @@ async function validateAndUpdateLibrary(libraryFields) { return { success, errors } } -export { validateAndUpdateLibrary, exhaustLibrayVolume, formatAndTransformLibraries } +export { + validateAndUpdateLibrary, + exhaustLibrayVolume, + formatAndTransformLibraries, + isLibraryExhausted, +} diff --git a/src/views/pacbio/PacbioLibraryIndex.vue b/src/views/pacbio/PacbioLibraryIndex.vue index ba5b7f55c..716963788 100644 --- a/src/views/pacbio/PacbioLibraryIndex.vue +++ b/src/views/pacbio/PacbioLibraryIndex.vue @@ -53,6 +53,22 @@ select-mode="multi" @row-selected="(items) => (state.selected = items)" > +