From ebd01eb5d90884ed3e3a34bb3aa8ba72778fb98a Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Thu, 17 Oct 2024 16:06:58 +0100
Subject: [PATCH 18/33] Update test.rb
---
config/environments/test.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/config/environments/test.rb b/config/environments/test.rb
index ed6751e91..1eecb6eb1 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -73,4 +73,6 @@
config.disable_animations = true
config.sprint_uri = 'http://example_sprint.com/graphql'
+ config.traction_ui_uri = 'http://localhost:5173/#'
+ config.traction_service_uri = 'http://localhost:3100/v1'
end
From 9cd5ebd9267e30064d2d990dc701cdfab093cafa Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Thu, 17 Oct 2024 16:07:49 +0100
Subject: [PATCH 19/33] tests (PoolXPTubeSubmitPanel): update units tests
---
.../components/PoolXPTubeSubmitPanel.spec.js | 434 ++++++++++++++++++
1 file changed, 434 insertions(+)
create mode 100644 app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
new file mode 100644
index 000000000..a0caccdab
--- /dev/null
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
@@ -0,0 +1,434 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import flushPromises from 'flush-promises'
+import PoolXPTubeSubmitPanel from './PoolXPTubeSubmitPanel.vue'
+import BootstrapVue from 'bootstrap-vue'
+import { beforeEach, expect, it, vi } from 'vitest'
+import ReadyIcon from '../../icons/ReadyIcon.vue'
+import TubeSearchIcon from '../../icons/TubeSearchIcon.vue'
+import SuccessIcon from '../../icons/SuccessIcon.vue'
+import ErrorIcon from '../../icons/ErrorIcon.vue'
+import TubeIcon from '../../icons/TubeIcon.vue'
+
+const localVue = createLocalVue()
+localVue.use(BootstrapVue)
+
+describe('PoolXPTubeSubmitPanel', () => {
+ let wrapper
+
+ const maxPollAttempts = 10
+ const pollInterval = 1000
+
+ const defaultProps = {
+ barcode: '12345',
+ userId: 'user123',
+ sequencescapeApiUrl: 'http://example.com/api',
+ tractionServiceUrl: 'http://traction.example.com',
+ tractionUIUrl: 'http://traction-ui.example.com',
+ }
+
+ const createWrapper = (state = 'initial', props = { ...defaultProps }) => {
+ return mount(PoolXPTubeSubmitPanel, {
+ localVue,
+ propsData: {
+ ...props,
+ },
+ data() {
+ return {
+ state,
+ }
+ },
+ })
+ }
+
+ const spyMethod = (method) => vi.spyOn(PoolXPTubeSubmitPanel.methods, method)
+
+ beforeEach(() => {
+ vi.useFakeTimers()
+ })
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ const verifyComponentState = (wrapper, state) => {
+ const exportButton = wrapper.find('#pool_xp_tube_export_button')
+ const statusLabel = wrapper.find('#pool_xp_tube_export_status')
+ const spinner = wrapper.find('#progress_spinner')
+ const statusIcon = wrapper.find('#status_icon')
+ switch (state) {
+ case 'initial': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-success')
+ expect(statusLabel.text()).toBe('Ready to export')
+ expect(exportButton.text()).toBe('Export')
+ const iconComponent = statusIcon.findComponent(ReadyIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toBe('green')
+ break
+ }
+
+ case 'fetching': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(true)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBe('disabled')
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-black')
+ expect(statusLabel.text()).toBe('Checking tube is in Traction')
+ expect(exportButton.text()).toBe('Please wait')
+ const iconComponent = statusIcon.findComponent(TubeSearchIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('black')
+ break
+ }
+
+ case 'tube_exists': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-success')
+ expect(statusLabel.text()).toBe('Tube already exported to Traction')
+ expect(exportButton.text()).toBe('Open Traction')
+ const iconComponent = statusIcon.findComponent(SuccessIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('green')
+ break
+ }
+
+ case 'polling': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(true)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBe('disabled')
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-black')
+ expect(statusLabel.text()).toBe('Tube is being exported to Traction')
+ expect(exportButton.text()).toBe('Please wait')
+ const iconComponent = statusIcon.findComponent(TubeIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('black')
+ break
+ }
+
+ case 'tube_export_success': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-success')
+ expect(statusLabel.text()).toBe('Tube has been exported to Traction')
+ expect(exportButton.text()).toBe('Open Traction')
+ const iconComponent = statusIcon.findComponent(SuccessIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('green')
+ break
+ }
+
+ case 'failure_export_tube': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-warning')
+ expect(statusLabel.classes()).toContain('text-warning')
+ expect(statusLabel.text()).toBe('Unable to send tube to Traction. Try again?')
+ expect(exportButton.text()).toBe('Retry')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('orange')
+ break
+ }
+
+ case 'failure_poll_tube': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-warning')
+ expect(statusLabel.classes()).toContain('text-warning')
+ expect(statusLabel.text()).toBe('Unable to check whether tube is in Traction. Try again?')
+ expect(exportButton.text()).toBe('Refresh')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('orange')
+ break
+ }
+
+ case 'failure_export_tube_after_recheck': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-primary')
+ expect(statusLabel.classes()).toContain('text-danger')
+ expect(statusLabel.text()).toBe('Unable to send tube to Traction')
+ expect(exportButton.text()).toBe('Export')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('red')
+ break
+ }
+
+ case 'invalid_props': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBe('disabled')
+ expect(exportButton.classes()).toContain('btn-danger')
+ expect(statusLabel.classes()).toContain('text-danger')
+ expect(statusLabel.text()).toBe('Required props are missing')
+ expect(exportButton.text()).toBe('Export')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('red')
+ break
+ }
+ }
+ }
+
+ it('renders the component', () => {
+ wrapper = createWrapper()
+ expect(wrapper.exists()).toBe(true)
+ })
+
+ it.each([
+ 'initial',
+ 'fetching',
+ 'tube_exists',
+ 'polling',
+ 'tube_export_success',
+ 'failure_export_tube',
+ 'failure_poll_tube',
+ 'failure_export_tube_after_recheck',
+ 'invalid_props',
+ ])('displays the correct status based on %s state', (stateValue) => {
+ const wrapper = createWrapper(stateValue)
+ verifyComponentState(wrapper, stateValue)
+ })
+
+ describe('on Mount', () => {
+ it('calls isTubeInTraction on mount', async () => {
+ vi.spyOn(PoolXPTubeSubmitPanel.methods, 'isTubeInTraction')
+ wrapper = createWrapper()
+ expect(PoolXPTubeSubmitPanel.methods.isTubeInTraction).toHaveBeenCalled()
+ })
+
+ it('handles invalid props correctly on mount', async () => {
+ wrapper = createWrapper('initial', {
+ barcode: '',
+ userId: '',
+ sequencescapeApiUrl: '',
+ tractionServiceUrl: '',
+ tractionUIUrl: 'http://traction-ui.example.com',
+ })
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('invalid_props')
+ verifyComponentState(wrapper, 'invalid_props')
+ })
+
+ it('handles fetching tube success from Traction correctly on mount', async () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [{ id: '1' }] }),
+ })
+ )
+
+ wrapper = createWrapper()
+ await flushPromises()
+ expect(global.fetch).toBeCalledTimes(1)
+ expect(global.fetch).toHaveBeenCalledWith(wrapper.vm.tractionTubeCheckUrl)
+
+ expect(wrapper.vm.state).toBe('tube_exists')
+ verifyComponentState(wrapper, 'tube_exists')
+ })
+
+ it('handles fetching tube failure from Traction correctly on mount', async () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: false,
+ json: () => Promise.resolve({ error: 'API call failed' }),
+ })
+ )
+ wrapper = createWrapper()
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('initial')
+ verifyComponentState(wrapper, 'initial')
+ })
+ })
+
+ describe('Export action', () => {
+ it('handleSubmit on button click', async () => {
+ const spyHandleSubmit = spyMethod('handleSubmit')
+ wrapper = createWrapper()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ expect(spyHandleSubmit).toHaveBeenCalled()
+ })
+ it('calls exportTubeToTraction when handleSubmit is invoked in the normal state', async () => {
+ const spyExportTubeToTraction = spyMethod('exportTubeToTraction')
+ wrapper = createWrapper()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ expect(spyExportTubeToTraction).toHaveBeenCalled()
+ })
+ it('calls isTubeInTraction when handleSubmit is invoked in the failure_poll_tube state', async () => {
+ const spyExportTubeToTraction = spyMethod('exportTubeToTraction')
+ const spyIsTubeInTraction = spyMethod('isTubeInTraction')
+ wrapper = createWrapper()
+ wrapper.setData({ state: 'failure_poll_tube' })
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ expect(spyIsTubeInTraction).toHaveBeenCalled()
+ expect(spyExportTubeToTraction).not.toHaveBeenCalled()
+ })
+
+ it.each(['tube_export_success', 'tube_exists'])(
+ 'opens Traction in a new tab when handleSubmit is invoked in the %s state',
+ async (stateValue) => {
+ const spyWindowOpen = vi.spyOn(window, 'open').mockImplementation(() => {})
+ const wrapper = createWrapper()
+
+ // Set the state and trigger the click
+ wrapper.setData({ state: stateValue })
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ expect(spyWindowOpen).toHaveBeenCalledWith(wrapper.vm.tractionTubeOpenUrl, '_blank')
+ }
+ )
+ it('handles export tube success correctly', async () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [] }),
+ })
+ )
+
+ const spyPollTractionForTube = spyMethod('pollTractionForTube')
+ wrapper = createWrapper()
+ await flushPromises()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(global.fetch).toHaveBeenCalledWith(wrapper.vm.sequencescapeApiExportUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(wrapper.vm.submitPayload),
+ })
+ expect(spyPollTractionForTube).toHaveBeenCalled()
+ expect(wrapper.vm.state).toBe('polling')
+ verifyComponentState(wrapper, 'polling')
+ })
+ it('handles export tube failure correctly', async () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: false,
+ json: () => Promise.resolve({ error: 'API call failed' }),
+ })
+ )
+ const spyPollTractionForTube = spyMethod('pollTractionForTube')
+ wrapper = createWrapper()
+ await flushPromises()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_export_tube')
+ expect(spyPollTractionForTube).not.toHaveBeenCalled()
+ verifyComponentState(wrapper, 'failure_export_tube')
+ })
+
+ it('handles export tube and pollTractionForTube successes correctly', async () => {
+ vi.spyOn(global, 'fetch').mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [{ id: '1' }] }),
+ })
+ )
+ wrapper = createWrapper()
+ await flushPromises()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ vi.advanceTimersByTime(pollInterval)
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_exists')
+ verifyComponentState(wrapper, 'tube_exists')
+ })
+
+ it('handles export tube success and pollTractionForTube failure correctly', async () => {
+ vi.spyOn(global, 'fetch')
+ .mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [] }),
+ })
+ )
+ .mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [] }),
+ })
+ )
+ .mockImplementation(() =>
+ Promise.resolve({
+ ok: false,
+ json: () => Promise.resolve({ error: 'API call failed' }),
+ })
+ )
+ wrapper = createWrapper()
+ await flushPromises()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ // Fast-forward time to simulate the polling intervals
+ for (let i = 0; i <= maxPollAttempts; i++) {
+ vi.advanceTimersByTime(pollInterval)
+ await flushPromises()
+ }
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_poll_tube')
+ verifyComponentState(wrapper, 'failure_poll_tube')
+ })
+ })
+ it('handles export tube success and pollTractionForTube success correctly', async () => {
+ vi.spyOn(global, 'fetch')
+ .mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [] }),
+ })
+ )
+ .mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [] }),
+ })
+ )
+ .mockImplementation(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: [{ id: '1' }] }),
+ })
+ )
+ wrapper = createWrapper()
+ await flushPromises()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ // Fast-forward time to simulate the polling intervals
+ for (let i = 0; i <= maxPollAttempts; i++) {
+ vi.advanceTimersByTime(pollInterval)
+ await flushPromises()
+ }
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_export_success')
+ verifyComponentState(wrapper, 'tube_export_success')
+ })
+})
From 4ab3c7bd0eca7170f30886955189dc7d62eca668 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Thu, 17 Oct 2024 16:07:58 +0100
Subject: [PATCH 20/33] Create pcr_pool_xp_presenter_spec.rb
---
.../presenters/pcr_pool_xp_presenter_spec.rb | 35 +++++++++++++++++++
1 file changed, 35 insertions(+)
create mode 100644 spec/models/presenters/pcr_pool_xp_presenter_spec.rb
diff --git a/spec/models/presenters/pcr_pool_xp_presenter_spec.rb b/spec/models/presenters/pcr_pool_xp_presenter_spec.rb
new file mode 100644
index 000000000..f0aeab625
--- /dev/null
+++ b/spec/models/presenters/pcr_pool_xp_presenter_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'presenters/tube_presenter'
+require_relative 'shared_labware_presenter_examples'
+
+RSpec.describe Presenters::PcrPoolXpPresenter do
+ let(:labware) do
+ build :v2_tube, purpose_name: purpose_name, state: state, barcode_number: 6, created_at: '2016-10-19 12:00:00 +0100'
+ end
+
+ before { create(:stock_plate_config, uuid: 'stock-plate-purpose-uuid') }
+
+ let(:purpose_name) { 'Limber example purpose' }
+ let(:title) { purpose_name }
+ let(:state) { 'pending' }
+ let(:summary_tab) do
+ [
+ ['Barcode', 'NT6T
3980000006844 '],
+ ['Tube type', purpose_name],
+ ['Current tube state', state],
+ ['Input plate barcode', labware.stock_plate.human_barcode],
+ ['Created on', '2016-10-19']
+ ]
+ end
+ let(:sidebar_partial) { 'default' }
+
+ subject { Presenters::PcrPoolXpPresenter.new(labware:) }
+
+ it_behaves_like 'a labware presenter'
+
+ it 'has export_to_traction option' do
+ expect(subject.export_to_traction).to be_truthy
+ end
+end
From 0a0bb82fac5d7c5383b2f845352ee6ee4a3c05a2 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Thu, 17 Oct 2024 16:13:22 +0100
Subject: [PATCH 21/33] format: prettier updates
---
app/frontend/javascript/icons/ReadyIcon.vue | 41 ++++++++-----------
app/frontend/javascript/icons/TubeIcon.vue | 25 +++++------
.../components/PoolXPTubeSubmitPanel.spec.js | 24 +++++------
.../javascript/pool-xp-tube-panel/index.js | 4 +-
.../presenters/pcr_pool_xp_presenter.rb | 4 +-
5 files changed, 45 insertions(+), 53 deletions(-)
diff --git a/app/frontend/javascript/icons/ReadyIcon.vue b/app/frontend/javascript/icons/ReadyIcon.vue
index b886f1351..e69bc747c 100644
--- a/app/frontend/javascript/icons/ReadyIcon.vue
+++ b/app/frontend/javascript/icons/ReadyIcon.vue
@@ -1,26 +1,19 @@
-
-
-
-
-
-
+ },
+}
+
diff --git a/app/frontend/javascript/icons/TubeIcon.vue b/app/frontend/javascript/icons/TubeIcon.vue
index c624d67df..afd116364 100644
--- a/app/frontend/javascript/icons/TubeIcon.vue
+++ b/app/frontend/javascript/icons/TubeIcon.vue
@@ -1,18 +1,19 @@
-
+
-
+
-
+
-
\ No newline at end of file
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
index a0caccdab..348f1f280 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
@@ -246,7 +246,7 @@ describe('PoolXPTubeSubmitPanel', () => {
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [{ id: '1' }] }),
- })
+ }),
)
wrapper = createWrapper()
@@ -263,7 +263,7 @@ describe('PoolXPTubeSubmitPanel', () => {
Promise.resolve({
ok: false,
json: () => Promise.resolve({ error: 'API call failed' }),
- })
+ }),
)
wrapper = createWrapper()
await flushPromises()
@@ -305,14 +305,14 @@ describe('PoolXPTubeSubmitPanel', () => {
wrapper.setData({ state: stateValue })
await wrapper.find('#pool_xp_tube_export_button').trigger('click')
expect(spyWindowOpen).toHaveBeenCalledWith(wrapper.vm.tractionTubeOpenUrl, '_blank')
- }
+ },
)
it('handles export tube success correctly', async () => {
vi.spyOn(global, 'fetch').mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [] }),
- })
+ }),
)
const spyPollTractionForTube = spyMethod('pollTractionForTube')
@@ -336,7 +336,7 @@ describe('PoolXPTubeSubmitPanel', () => {
Promise.resolve({
ok: false,
json: () => Promise.resolve({ error: 'API call failed' }),
- })
+ }),
)
const spyPollTractionForTube = spyMethod('pollTractionForTube')
wrapper = createWrapper()
@@ -353,7 +353,7 @@ describe('PoolXPTubeSubmitPanel', () => {
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [{ id: '1' }] }),
- })
+ }),
)
wrapper = createWrapper()
await flushPromises()
@@ -370,19 +370,19 @@ describe('PoolXPTubeSubmitPanel', () => {
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [] }),
- })
+ }),
)
.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [] }),
- })
+ }),
)
.mockImplementation(() =>
Promise.resolve({
ok: false,
json: () => Promise.resolve({ error: 'API call failed' }),
- })
+ }),
)
wrapper = createWrapper()
await flushPromises()
@@ -404,19 +404,19 @@ describe('PoolXPTubeSubmitPanel', () => {
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [] }),
- })
+ }),
)
.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [] }),
- })
+ }),
)
.mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: [{ id: '1' }] }),
- })
+ }),
)
wrapper = createWrapper()
await flushPromises()
diff --git a/app/frontend/javascript/pool-xp-tube-panel/index.js b/app/frontend/javascript/pool-xp-tube-panel/index.js
index be31801a8..091fb2921 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/index.js
+++ b/app/frontend/javascript/pool-xp-tube-panel/index.js
@@ -45,16 +45,14 @@ document.addEventListener('DOMContentLoaded', async () => {
// a very basic vue component (essentially just an error message)
// if userId is missing
-
if (userId) {
new Vue({
el: '#pool-xp-tube-submit-panel',
render(h) {
-
let barcode = this.$el.dataset.barcode
return h(PoolXPTubeSubmitPanel, {
- props: { barcode, userId, sequencescapeApiUrl,tractionServiceUrl,tractionUIUrl },
+ props: { barcode, userId, sequencescapeApiUrl, tractionServiceUrl, tractionUIUrl },
})
},
})
diff --git a/app/models/presenters/pcr_pool_xp_presenter.rb b/app/models/presenters/pcr_pool_xp_presenter.rb
index 7d405d47d..e7bb8d165 100644
--- a/app/models/presenters/pcr_pool_xp_presenter.rb
+++ b/app/models/presenters/pcr_pool_xp_presenter.rb
@@ -1,10 +1,10 @@
+# frozen_string_literal: true
+
module Presenters
# FinalTubePresenter
class PcrPoolXpPresenter < FinalTubePresenter
-
def export_to_traction
true
end
end
-
end
From 02ef105f72b3a7188af27db683f69773426c9933 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:05:13 +0100
Subject: [PATCH 22/33] refactor: changes to state transitions
---
.../components/PoolXPTubeSubmitPanel.vue | 297 ++++++++++++------
1 file changed, 193 insertions(+), 104 deletions(-)
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
index 380f6f122..a90a4807d 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
@@ -34,42 +34,71 @@
* @property {String} tractionUIUrl - The URL of the Traction UI
*
* This component exports a tube to Traction and polls Traction to check if the tube is exported successfully.
- * It displays the status of the export and provides a button to export the tube.
+ * This is performed in two steps:
+ * - Export the tube to Traction using the Sequencescape API (export_pool_xp_to_traction) endpoint
+ * - Poll Traction to check if the tube is exported successfully using the Traction API (GET /pacbio/tubes?filter[barcode]=)
*
* The component has the following states:
*
- * 1. INITIAL
- * - When the component is first loaded, it is in the INITIAL state.
- * - On Mount, the component checks if the tube is already exported to Traction.
- * - If the tube is found, the component transitions to the TUBE_ALREADY_EXPORTED state.
- * - If the tube is not found, the component remains in the INITIAL state.
+ * State 1: CHECKING_TUBE_STATUS
+ * When the component is first loaded, it is in the CHECKING_TUBE_STATUS state.
+ * The components displays a spinner and a message indicating that it is checking if the tube is in Traction. The button is disabled.
+ * Transition:
+ * - If the tube is not found, the component remains in the State 2 (READY_TO_EXPORT) state.
+ * - If the tube is found, the component transitions to the State 6 (TUBE_ALREADY_EXPORTED).
+ * - If the service is unavailable or returns error, the component transitions to the State 4 (FAILURE_TUBE_CHECK) state.
*
- * 2. TUBE_ALREADY_EXPORTED
- * - This state occurs if the tube is found in Traction during the initial check.
- * - The component provides a button to open the tube in Traction.
+ * State 2: READY_TO_EXPORT
+ * This state occurs if the tube is found in Traction during the State 1.
+ * The component provides a button to export tube to Traction. Clicking the button transitions the component as follows
+ * Transition:
+ * - If the export using Sequencescape API is successful, the component transitions to the State 3 (EXPORTING_TUBE) state.
+ * - If the export using Sequencescape API fails, the component transitions to the State 8 (FAILURE_EXPORT_TUBE) state.
+ * - If the tube is found in Traction after the export, the component transitions to the State 5 (TUBE_EXPORT_SUCCESS) state.
+ * - If the tube is not found in Traction after the export, the component transitions to the State 7 (FAILURE_TUBE_CHECK_AFTER_EXPORT) state.
*
- * 3. FAILURE_EXPORT_TUBE
- * - This state occurs if the request to export the tube to Traction fails.
- * - The component provides a button to retry exporting the tube.
+ * State 3: EXPORTING_TUBE
+ * This state occurs when the user clicks the export button and the request to export Sequencescape SS API is successful.
+ * At this state, the component polls Traction using Traction API to check if the tube is exported successfully.
+ * The component displays a spinner and a message indicating that the tube is being exported to Traction. The button is disabled.
+ * Transition:
+ * - If the polling is successful, the component transitions to the State 5 (TUBE_EXPORT_SUCCESS) state.
+ * - If the polling fails, the component transitions to the State 7 (FAILURE_TUBE_CHECK_AFTER_EXPORT) state.
*
- * 4. POLLING_TUBE
- * - This state occurs when the user clicks the export button and the request to export the tube to Traction is successful.
- * - The component polls Traction to check if the tube is exported successfully.
- * - If the polling is successful, the component transitions to the TUBE_EXPORT_SUCCESS state.
- * - If the polling fails, the component transitions to the FAILURE_POLL_TUBE state.
+ * State 4: FAILURE_TUBE_CHECK
+ * This state occurs if the initial check to see if the tube is in Traction fails.
+ * The component provides a button to retry checking if the tube is in Traction and a message indicating that the export cannot be verified.
+ * On clicking the button, the component perfors same operation as in State 1.
+ * Transition:
+ * - The component transitions to the State 1 (CHECKING_TUBE_STATUS) state.
*
- * 5. TUBE_EXPORT_SUCCESS
- * - This state occurs if the polling is successful.
- * - The export button will be disabled.
+ * State 5: TUBE_EXPORT_SUCCESS
+ * This state occurs if the exporting the tube using Sequencescape API is successful and the polling using Traction API is successful.
+ * The component displays a message indicating that the tube has been exported to Traction and an open Traction button to open the tube in Traction is displayed.
+ * Transition:
+ * - If the user clicks the open Traction button, the component opens the tube in Traction.
*
- * 6. FAILURE_POLL_TUBE
- * - This state occurs if the polling fails.
- * - The component provides a button to retry polling.
- * - If the retry fails, the component transitions to the FAILURE_EXPORT_TUBE_AFTER_RECHECK state.
+ * State 6: TUBE_ALREADY_EXPORTED
+ * This state occurs if the initial check tube to see if the tube is in Traction returns a tube.
+ * The component displays a message indicating that the tube is already exported to Traction and an open Traction button to open the tube in Traction is displayed.
+ * Transition:
+ * - If the user clicks the open Traction button, the component opens the tube in Traction.
*
- * 7. FAILURE_EXPORT_TUBE_AFTER_RECHECK
- * - This state occurs if the retry after polling failure fails.
- * - The component allows the user to try exporting the tube again.
+ * State 7: FAILURE_TUBE_CHECK_AFTER_EXPORT
+ * This state occurs if the exporting the tube using Sequencescape API is successful but the polling using Traction API fails.
+ * The component provides a button to retry polling and a message indicating that the export cannot be verified.
+ * Transition:
+ * - When retry button is clicked, the component transitions to the State 3 (EXPORTING_TUBE) state.
+ *
+ * State 8: FAILURE_EXPORT_TUBE
+ * This state occurs if the exporting the tube using Sequencescape API fails.
+ * The component provides a button to retry exporting the tube and a message indicating that the tube export to Traction failed.
+ * Transition:
+ * - When retry button is clicked, the component transitions to the State 3 (EXPORTING_TUBE) state.
+ *
+ * State 9: INVALID_PROPS
+ * This state occurs if the required props are missing when the component is mounted.
+ * The component provides a message indicating that the required props are missing and the button is disabled.
*/
import ReadyIcon from '../../icons/ReadyIcon.vue'
@@ -78,21 +107,21 @@ import ErrorIcon from '../../icons/ErrorIcon.vue'
import TubeSearchIcon from '../../icons/TubeSearchIcon.vue'
import TubeIcon from '../../icons/TubeIcon.vue'
-const maxPollttempts = 10
-const pollInterval = 1000
+const DEFAULT_MAX_TUBE_CHECK_RETRIES = 3
+const DEFAULT_MAX_TUBE_CHECK_RETRY_DELAY = 1000
/**
* Enum for the possible states of the component
*/
const StateEnum = {
- INITIAL: 'initial', // The default state or the initial state when component is first loaded
- CHECKING_TUBE_EXISTS: 'fetching', // The state when the component is checking if the tube exists in Traction on mount
- TUBE_ALREADY_EXPORTED: 'tube_exists', // The state when the component finds the tube in Traction on mount
- POLLING_TUBE: 'polling', // The state when the component is polling Traction to check if the tube is exported
+ READY_TO_EXPORT: 'ready_to_export', // The state when the component is ready to export the tube
+ CHECKING_TUBE_STATUS: 'checking_tube_status', // The state when the component is checking if the tube is in Traction and this is the initial state as well
+ TUBE_ALREADY_EXPORTED: 'tube_exists', // The state when the tube is already exported to Traction
+ EXPORTING_TUBE: 'exporting', // The state when the component is exporting the tube to Traction
TUBE_EXPORT_SUCESS: 'tube_export_success', // The state when the tube is successfully exported to Traction
- FAILURE_POLL_TUBE: 'failure_poll_tube', // The state when the component fails to poll Traction for the tube
- FAILURE_EXPORT_TUBE: 'failure_export_tube', // The state when the component fails to export the tube to Traction
- FAILURE_EXPORT_TUBE_AFTER_RECHECK: 'failure_export_tube_after_recheck', // The state when the component fails to export the tube to Traction after rechecking
+ FAILURE_TUBE_CHECK: 'failure_tube_check', // The state when the component fails to check if the tube is in Traction using the Traction API
+ FAILURE_TUBE_CHECK_AFTER_EXPORT: 'failure_tube_check_export', // The state when the component fails to check if the tube is in Traction after exporting using the Traction API
+ FAILURE_EXPORT_TUBE: 'failure_export_tube', // The state when the component fails to export the tube when the export (SS API) fails
INVALID_PROPS: 'invalid_props', // The state when the component receives invalid props
}
@@ -100,25 +129,26 @@ const StateEnum = {
* Data for the different states of the component
*/
const StateData = {
- [StateEnum.INITIAL]: {
- statusText: 'Ready to export',
- buttonText: 'Export',
- styles: { button: 'success', text: 'text-success', icon: 'green' },
- icon: ReadyIcon,
- },
- [StateEnum.CHECKING_TUBE_EXISTS]: {
+ [StateEnum.CHECKING_TUBE_STATUS]: {
statusText: 'Checking tube is in Traction',
buttonText: 'Please wait',
styles: { button: 'success', text: 'text-black', icon: 'black' },
icon: TubeSearchIcon,
},
+ [StateEnum.READY_TO_EXPORT]: {
+ statusText: 'Ready to export',
+ buttonText: 'Export',
+ styles: { button: 'success', text: 'text-success', icon: 'green' },
+ icon: ReadyIcon,
+ },
[StateEnum.TUBE_ALREADY_EXPORTED]: {
statusText: 'Tube already exported to Traction',
buttonText: 'Open Traction',
- styles: { button: 'success', text: 'text-success', icon: 'green' },
+ styles: { button: 'primary', text: 'text-success', icon: 'green' },
icon: SuccessIcon,
},
- [StateEnum.POLLING_TUBE]: {
+
+ [StateEnum.EXPORTING_TUBE]: {
statusText: 'Tube is being exported to Traction',
buttonText: 'Please wait',
styles: { button: 'success', text: 'text-black', icon: 'black' },
@@ -127,25 +157,25 @@ const StateData = {
[StateEnum.TUBE_EXPORT_SUCESS]: {
statusText: 'Tube has been exported to Traction',
buttonText: 'Open Traction',
- styles: { button: 'success', text: 'text-success', icon: 'green' },
+ styles: { button: 'primary', text: 'text-success', icon: 'green' },
icon: SuccessIcon,
},
- [StateEnum.FAILURE_POLL_TUBE]: {
- statusText: 'Unable to check whether tube is in Traction. Try again?',
+ [StateEnum.FAILURE_TUBE_CHECK]: {
+ statusText: 'The export cannot be verified. Refresh to try again',
buttonText: 'Refresh',
- styles: { button: 'warning', text: 'text-warning', icon: 'orange' },
+ styles: { button: 'danger', text: 'text-danger', icon: 'red' },
icon: ErrorIcon,
},
- [StateEnum.FAILURE_EXPORT_TUBE]: {
- statusText: 'Unable to send tube to Traction. Try again?',
- buttonText: 'Retry',
- styles: { button: 'warning', text: 'text-warning', icon: 'orange' },
+ [StateEnum.FAILURE_TUBE_CHECK_AFTER_EXPORT]: {
+ statusText: 'The export cannot be verified. Try again',
+ buttonText: 'Try again',
+ styles: { button: 'danger', text: 'text-danger', icon: 'red' },
icon: ErrorIcon,
},
- [StateEnum.FAILURE_EXPORT_TUBE_AFTER_RECHECK]: {
- statusText: 'Unable to send tube to Traction',
- buttonText: 'Export',
- styles: { button: 'primary', text: 'text-danger', icon: 'red' },
+ [StateEnum.FAILURE_EXPORT_TUBE]: {
+ statusText: 'The tube export to Traction failed. Try again',
+ buttonText: 'Try again',
+ styles: { button: 'danger', text: 'text-danger', icon: 'red' },
icon: ErrorIcon,
},
[StateEnum.INVALID_PROPS]: {
@@ -156,6 +186,15 @@ const StateData = {
},
}
+/**
+ * Enum for the possible results of the tube search using the Traction API
+ */
+const TubeSearchResult = {
+ FOUND: 'found',
+ NOT_FOUND: 'not_found',
+ SERVICE_ERROR: 'service_error',
+}
+
export default {
name: 'PoolXPTubeSubmitPanel',
components: {
@@ -185,7 +224,7 @@ export default {
},
data: function () {
return {
- state: StateEnum.INITIAL,
+ state: StateEnum.CHECKING_TUBE_STATUS,
}
},
computed: {
@@ -196,7 +235,7 @@ export default {
return StateData[this.state].buttonText
},
stateStyles() {
- return StateData[this.state].styles || { button: 'danger', text: 'text-danger' }
+ return StateData[this.state]?.styles || { button: 'danger', text: 'text-danger', icon: 'red' }
},
statusIcon() {
return StateData[this.state].icon
@@ -204,13 +243,13 @@ export default {
isButtonDisabled() {
return (
- this.state === StateEnum.CHECKING_TUBE_EXISTS ||
- this.state === StateEnum.POLLING_TUBE ||
+ this.state === StateEnum.CHECKING_TUBE_STATUS ||
+ this.state === StateEnum.EXPORTING_TUBE ||
this.state === StateEnum.INVALID_PROPS
)
},
displaySpinner() {
- return this.state === StateEnum.POLLING_TUBE || this.state === StateEnum.CHECKING_TUBE_EXISTS
+ return this.state === StateEnum.EXPORTING_TUBE || this.state === StateEnum.CHECKING_TUBE_STATUS
},
sequencescapeApiExportUrl() {
return `${this.sequencescapeApiUrl}/bioscan/export_pool_xp_to_traction`
@@ -236,13 +275,15 @@ export default {
/**
* On mount, check if the tube is already exported to Traction
+ * If the tube is found, transition to the TUBE_ALREADY_EXPORTED state
+ * If the tube is not found, transition to the INITIAL state
+ * If the service is unavailable or returns error, transition to the FAILURE_TUBE_CHECK state
*/
async mounted() {
+ // Validate the props
this.validateProps()
if (this.state === StateEnum.INVALID_PROPS) return
- this.state = StateEnum.CHECKING_TUBE_EXISTS
- const isTubeFound = await this.isTubeInTraction()
- this.state = isTubeFound ? StateEnum.TUBE_ALREADY_EXPORTED : StateEnum.INITIAL
+ this.initialiseStartState()
},
methods: {
/**
@@ -254,56 +295,92 @@ export default {
return
}
},
+
/**
* Check if the tube is in Traction
+ * @returns {TubeSearchResult} - The result of the tube search
+ * - FOUND: If the tube is found in Traction
+ * - NOT_FOUND: If the tube is not found in Traction
+ * - SERVICE_ERROR: If the service is unavailable or returns error
*/
- async isTubeInTraction() {
- let isTubeFound = false
+ async checkTubeInTraction() {
try {
const response = await fetch(this.tractionTubeCheckUrl)
-
if (response.ok) {
const data = await response.json()
if (data.data && data.data.length > 0) {
- isTubeFound = true
+ return TubeSearchResult.FOUND
}
+ return TubeSearchResult.NOT_FOUND
+ } else {
+ console.log('Fetch response not ok:', response.statusText)
+ return TubeSearchResult.SERVICE_ERROR
}
- return isTubeFound
} catch (error) {
- return false
+ console.log('Error during fetch:', error)
+ return TubeSearchResult.SERVICE_ERROR
+ }
+ },
+
+ /**
+ * Initialise the start state of the component
+ * - Check if the tube is already exported to Traction and transition to the appropriate state based on the result
+ * - If the tube is found in Traction, transition to the TUBE_ALREADY_EXPORTED state
+ * - If the service is unavailable or returns error, transition to the FAILURE_TUBE_CHECK state
+ * - If the tube is not found in Traction, transition to the INITIAL state which allows the user to export the tube
+ */
+ async initialiseStartState() {
+ this.state = StateEnum.CHECKING_TUBE_STATUS
+ const result = await this.checkTubeStatusWithRetries()
+
+ // If the tube is found in Traction, transition to the TUBE_ALREADY_EXPORTED state
+ if (result === TubeSearchResult.FOUND) {
+ this.state = StateEnum.TUBE_ALREADY_EXPORTED
+ return
+ }
+ // If the service is unavailable or returns error, transition to the FAILURE_TUBE_CHECK state
+ if (result === TubeSearchResult.SERVICE_ERROR) {
+ this.state = StateEnum.FAILURE_TUBE_CHECK
+ return
}
+ // If the tube is not found in Traction, transition to the INITIAL state which allows the user to export the tube
+ this.state = StateEnum.READY_TO_EXPORT
},
/**
* Handle the submit button click
* The action taken depends on the current state of the component
*
- * - If the submit button is clicked in the FAILURE_POLL_TUBE state (after polling fails post-submission), the component will check if the tube exists in Traction.
- * - If the submit button is clicked in the TUBE_EXPORT_SUCCESS state (after successful export), the component will open the tube in Traction.
- * - In all other states, clicking the submit button will submit the tube to Traction.
- */
+ * - If the state is FAILURE_TUBE_CHECK, it means the initial check failed, therefore repeat the initial check
+ * - If the state is TUBE_EXPORT_SUCESS or TUBE_ALREADY_EXPORTED, open the tube in Traction
+ * - Otherwise, export the tube to Traction
+ * */
async handleSubmit() {
switch (this.state) {
- case StateEnum.FAILURE_POLL_TUBE: {
- this.state = StateEnum.CHECKING_TUBE_EXISTS
- const isFound = await this.isTubeInTraction()
- this.state = isFound ? StateEnum.TUBE_EXPORT_SUCESS : StateEnum.FAILURE_EXPORT_TUBE_AFTER_RECHECK
- return
+ case StateEnum.FAILURE_TUBE_CHECK: {
+ this.initialiseStartState()
+ break
}
case StateEnum.TUBE_EXPORT_SUCESS:
case StateEnum.TUBE_ALREADY_EXPORTED: {
// Open Traction in a new tab
window.open(this.tractionTubeOpenUrl, '_blank')
- return
+ break
}
+
default: {
await this.exportTubeToTraction()
- return
+ break
}
}
},
/**
* Export the tube to Traction and poll Traction for the tube status if the export is successful
+ *
+ * - If the export api is successful, transition to the EXPORTING_TUBE state and poll Traction for the tube status
+ * - If the export api fails, transition to the FAILURE_EXPORT_TUBE state which allows the user to retry exporting the tube
+ * - If the tube is found in Traction after the export, transition to the TUBE_EXPORT_SUCCESS state which allows the user to open the tube in Traction
+ * - If the tube is not found in Traction after the export, transition to the FAILURE_TUBE_CHECK_AFTER_EXPORT state which allows the user to retry exporting the tube
*/
async exportTubeToTraction() {
try {
@@ -314,42 +391,54 @@ export default {
},
body: JSON.stringify(this.submitPayload),
})
+
if (response.ok) {
- this.isSubmitted = true
- this.state = StateEnum.POLLING_TUBE
- await this.pollTractionForTube()
+ this.state = StateEnum.EXPORTING_TUBE
+ const retStatus = await this.checkTubeStatusWithRetries(DEFAULT_MAX_TUBE_CHECK_RETRIES + 2)
+ this.state =
+ retStatus === TubeSearchResult.FOUND
+ ? StateEnum.TUBE_EXPORT_SUCESS
+ : StateEnum.FAILURE_TUBE_CHECK_AFTER_EXPORT
+ return
} else {
this.state = StateEnum.FAILURE_EXPORT_TUBE
}
} catch (error) {
- console.error('Error submitting tube to Traction:', error)
+ console.log('Error exporting tube to Traction:', error)
this.state = StateEnum.FAILURE_EXPORT_TUBE
}
},
/**
- * Poll Traction for the tube status
- * If the tube is found, the component transitions to the TUBE_EXPORT_SUCCESS state
- * If the tube is not found, the component retries polling after a delay until the maximum number of attempts is reached
+ * Check the tube status with retries
+ * @param {number} retries - Number of retries
+ * @param {number} delay - Delay between retries in milliseconds
*/
- async pollTractionForTube() {
- let attempts = 0
-
- const poll = async () => {
- if (attempts > +maxPollttempts) {
- this.state = StateEnum.FAILURE_POLL_TUBE
- return
+ async checkTubeStatusWithRetries(
+ retries = DEFAULT_MAX_TUBE_CHECK_RETRIES,
+ delay = DEFAULT_MAX_TUBE_CHECK_RETRY_DELAY,
+ ) {
+ let result = TubeSearchResult.NOT_FOUND
+ for (let i = 0; i < retries; i++) {
+ result = await this.checkTubeInTraction()
+ if (result === TubeSearchResult.FOUND) {
+ return result
}
- const isTubeFound = await this.isTubeInTraction()
- if (isTubeFound) {
- this.state = StateEnum.TUBE_EXPORT_SUCESS
- return
- } else {
- attempts++
- setTimeout(poll, pollInterval)
+ if (i < retries - 1) {
+ await this.sleep(delay)
}
}
- poll()
+
+ return result
+ },
+
+ /**
+ * Sleep for a specified duration
+ * @param {number} ms - Duration in milliseconds
+ * @returns {Promise}
+ */
+ sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
},
},
}
From 87352d451b3851ff3e8f7cb3d5c9e614c7622d6f Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:05:21 +0100
Subject: [PATCH 23/33] tests: updates
---
.../components/PoolXPTubeSubmitPanel.spec.js | 928 +++++++++++-------
1 file changed, 584 insertions(+), 344 deletions(-)
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
index 348f1f280..fc77b27ab 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.spec.js
@@ -1,8 +1,8 @@
import { mount, createLocalVue } from '@vue/test-utils'
-import flushPromises from 'flush-promises'
import PoolXPTubeSubmitPanel from './PoolXPTubeSubmitPanel.vue'
import BootstrapVue from 'bootstrap-vue'
-import { beforeEach, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import flushPromises from 'flush-promises'
import ReadyIcon from '../../icons/ReadyIcon.vue'
import TubeSearchIcon from '../../icons/TubeSearchIcon.vue'
import SuccessIcon from '../../icons/SuccessIcon.vue'
@@ -12,194 +12,236 @@ import TubeIcon from '../../icons/TubeIcon.vue'
const localVue = createLocalVue()
localVue.use(BootstrapVue)
-describe('PoolXPTubeSubmitPanel', () => {
- let wrapper
+// Default props
+const defaultProps = {
+ barcode: '12345',
+ userId: 'user123',
+ sequencescapeApiUrl: 'http://example.com/api',
+ tractionServiceUrl: 'http://traction.example.com',
+ tractionUIUrl: 'http://traction-ui.example.com',
+}
- const maxPollAttempts = 10
- const pollInterval = 1000
+// Helper function to create the wrapper with the given state and props
+const createWrapper = (state = 'checking_tube_status', props = { ...defaultProps }) => {
+ return mount(PoolXPTubeSubmitPanel, {
+ localVue,
+ propsData: {
+ ...props,
+ },
+ data() {
+ return {
+ state,
+ }
+ },
+ })
+}
- const defaultProps = {
- barcode: '12345',
- userId: 'user123',
- sequencescapeApiUrl: 'http://example.com/api',
- tractionServiceUrl: 'http://traction.example.com',
- tractionUIUrl: 'http://traction-ui.example.com',
- }
+// Helper function to mock fetch responses sequentially
+const mockFetch = (responses) => {
+ const fetchMock = vi.spyOn(global, 'fetch')
+ responses.forEach((response) => {
+ fetchMock.mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: response.ok,
+ json: () => Promise.resolve(response.data),
+ }),
+ )
+ })
+ return fetchMock
+}
- const createWrapper = (state = 'initial', props = { ...defaultProps }) => {
- return mount(PoolXPTubeSubmitPanel, {
- localVue,
- propsData: {
- ...props,
- },
- data() {
- return {
- state,
- }
- },
- })
- }
+//Response objects
+const emptyResponse = { ok: true, data: { data: [] } } // Initial successful response
+const failedResponse = { ok: false, data: { error: 'API call failed' } } // Subsequent failure response
+const successResponse = { ok: true, data: { data: [{ id: '1' }] } } // Subsequent success response
- const spyMethod = (method) => vi.spyOn(PoolXPTubeSubmitPanel.methods, method)
+const mockTubeFoundResponse = () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ ...successResponse.data }),
+ }),
+ )
+}
+const mockTubeCheckFailureResponse = () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: false,
+ json: () => Promise.resolve({ ...failedResponse.data }),
+ }),
+ )
+}
- beforeEach(() => {
- vi.useFakeTimers()
- })
- afterEach(() => {
- vi.useRealTimers()
- })
+const mockNoTubeFoundResponse = () => {
+ vi.spyOn(global, 'fetch').mockImplementation(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ ...emptyResponse.data }),
+ }),
+ )
+}
- const verifyComponentState = (wrapper, state) => {
- const exportButton = wrapper.find('#pool_xp_tube_export_button')
- const statusLabel = wrapper.find('#pool_xp_tube_export_status')
- const spinner = wrapper.find('#progress_spinner')
- const statusIcon = wrapper.find('#status_icon')
- switch (state) {
- case 'initial': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBeUndefined()
- expect(exportButton.classes()).toContain('btn-success')
- expect(statusLabel.classes()).toContain('text-success')
- expect(statusLabel.text()).toBe('Ready to export')
- expect(exportButton.text()).toBe('Export')
- const iconComponent = statusIcon.findComponent(ReadyIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toBe('green')
- break
- }
+// Helper function to check if the component displays the correct state as per the given state value
+const verifyComponentState = (wrapper, state) => {
+ const exportButton = wrapper.find('#pool_xp_tube_export_button')
+ const statusLabel = wrapper.find('#pool_xp_tube_export_status')
+ const spinner = wrapper.find('#progress_spinner')
+ const statusIcon = wrapper.find('#status_icon')
+ switch (state) {
+ case 'ready_to_export': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-success')
+ expect(statusLabel.text()).toBe('Ready to export')
+ expect(exportButton.text()).toBe('Export')
+ const iconComponent = statusIcon.findComponent(ReadyIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toBe('green')
+ break
+ }
- case 'fetching': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(true)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBe('disabled')
- expect(exportButton.classes()).toContain('btn-success')
- expect(statusLabel.classes()).toContain('text-black')
- expect(statusLabel.text()).toBe('Checking tube is in Traction')
- expect(exportButton.text()).toBe('Please wait')
- const iconComponent = statusIcon.findComponent(TubeSearchIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('black')
- break
- }
+ case 'checking_tube_status': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(true)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBe('disabled')
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-black')
+ expect(statusLabel.text()).toBe('Checking tube is in Traction')
+ expect(exportButton.text()).toBe('Please wait')
+ const iconComponent = statusIcon.findComponent(TubeSearchIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('black')
+ break
+ }
- case 'tube_exists': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBeUndefined()
- expect(exportButton.classes()).toContain('btn-success')
- expect(statusLabel.classes()).toContain('text-success')
- expect(statusLabel.text()).toBe('Tube already exported to Traction')
- expect(exportButton.text()).toBe('Open Traction')
- const iconComponent = statusIcon.findComponent(SuccessIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('green')
- break
- }
+ case 'tube_exists': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-primary')
+ expect(statusLabel.classes()).toContain('text-success')
+ expect(statusLabel.text()).toBe('Tube already exported to Traction')
+ expect(exportButton.text()).toBe('Open Traction')
+ const iconComponent = statusIcon.findComponent(SuccessIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('green')
+ break
+ }
- case 'polling': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(true)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBe('disabled')
- expect(exportButton.classes()).toContain('btn-success')
- expect(statusLabel.classes()).toContain('text-black')
- expect(statusLabel.text()).toBe('Tube is being exported to Traction')
- expect(exportButton.text()).toBe('Please wait')
- const iconComponent = statusIcon.findComponent(TubeIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('black')
- break
- }
+ case 'exporting': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(true)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBe('disabled')
+ expect(exportButton.classes()).toContain('btn-success')
+ expect(statusLabel.classes()).toContain('text-black')
+ expect(statusLabel.text()).toBe('Tube is being exported to Traction')
+ expect(exportButton.text()).toBe('Please wait')
+ const iconComponent = statusIcon.findComponent(TubeIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('black')
+ break
+ }
- case 'tube_export_success': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBeUndefined()
- expect(exportButton.classes()).toContain('btn-success')
- expect(statusLabel.classes()).toContain('text-success')
- expect(statusLabel.text()).toBe('Tube has been exported to Traction')
- expect(exportButton.text()).toBe('Open Traction')
- const iconComponent = statusIcon.findComponent(SuccessIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('green')
- break
- }
+ case 'tube_export_success': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-primary')
+ expect(statusLabel.classes()).toContain('text-success')
+ expect(statusLabel.text()).toBe('Tube has been exported to Traction')
+ expect(exportButton.text()).toBe('Open Traction')
+ const iconComponent = statusIcon.findComponent(SuccessIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('green')
+ break
+ }
- case 'failure_export_tube': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBeUndefined()
- expect(exportButton.classes()).toContain('btn-warning')
- expect(statusLabel.classes()).toContain('text-warning')
- expect(statusLabel.text()).toBe('Unable to send tube to Traction. Try again?')
- expect(exportButton.text()).toBe('Retry')
- const iconComponent = statusIcon.findComponent(ErrorIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('orange')
- break
- }
+ case 'failure_export_tube': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-danger')
+ expect(statusLabel.classes()).toContain('text-danger')
+ expect(statusLabel.text()).toBe('The tube export to Traction failed. Try again')
+ expect(exportButton.text()).toBe('Try again')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('red')
+ break
+ }
- case 'failure_poll_tube': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBeUndefined()
- expect(exportButton.classes()).toContain('btn-warning')
- expect(statusLabel.classes()).toContain('text-warning')
- expect(statusLabel.text()).toBe('Unable to check whether tube is in Traction. Try again?')
- expect(exportButton.text()).toBe('Refresh')
- const iconComponent = statusIcon.findComponent(ErrorIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('orange')
- break
- }
+ case 'failure_tube_check': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-danger')
+ expect(statusLabel.classes()).toContain('text-danger')
+ expect(statusLabel.text()).toBe('The export cannot be verified. Refresh to try again')
+ expect(exportButton.text()).toBe('Refresh')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('red')
+ break
+ }
- case 'failure_export_tube_after_recheck': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBeUndefined()
- expect(exportButton.classes()).toContain('btn-primary')
- expect(statusLabel.classes()).toContain('text-danger')
- expect(statusLabel.text()).toBe('Unable to send tube to Traction')
- expect(exportButton.text()).toBe('Export')
- const iconComponent = statusIcon.findComponent(ErrorIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('red')
- break
- }
+ case 'failure_tube_check_export': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBeUndefined()
+ expect(exportButton.classes()).toContain('btn-danger')
+ expect(statusLabel.classes()).toContain('text-danger')
+ expect(statusLabel.text()).toBe('The export cannot be verified. Try again')
+ expect(exportButton.text()).toBe('Try again')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('red')
+ break
+ }
- case 'invalid_props': {
- expect(exportButton.exists()).toBe(true)
- expect(statusLabel.exists()).toBe(true)
- expect(spinner.isVisible()).toBe(false)
- expect(statusIcon.isVisible()).toBe(true)
- expect(exportButton.attributes('disabled')).toBe('disabled')
- expect(exportButton.classes()).toContain('btn-danger')
- expect(statusLabel.classes()).toContain('text-danger')
- expect(statusLabel.text()).toBe('Required props are missing')
- expect(exportButton.text()).toBe('Export')
- const iconComponent = statusIcon.findComponent(ErrorIcon)
- expect(iconComponent.exists()).toBe(true)
- expect(iconComponent.props().color).toContain('red')
- break
- }
+ case 'invalid_props': {
+ expect(exportButton.exists()).toBe(true)
+ expect(statusLabel.exists()).toBe(true)
+ expect(spinner.isVisible()).toBe(false)
+ expect(statusIcon.isVisible()).toBe(true)
+ expect(exportButton.attributes('disabled')).toBe('disabled')
+ expect(exportButton.classes()).toContain('btn-danger')
+ expect(statusLabel.classes()).toContain('text-danger')
+ expect(statusLabel.text()).toBe('Required props are missing')
+ expect(exportButton.text()).toBe('Export')
+ const iconComponent = statusIcon.findComponent(ErrorIcon)
+ expect(iconComponent.exists()).toBe(true)
+ expect(iconComponent.props().color).toContain('red')
+ break
}
}
+}
+
+describe('PoolXPTubeSubmitPanel', () => {
+ let wrapper
+
+ beforeEach(() => {
+ mockNoTubeFoundResponse()
+ vi.spyOn(console, 'log').mockImplementation(() => {})
+ vi.spyOn(window, 'open').mockImplementation(() => {})
+ })
it('renders the component', () => {
wrapper = createWrapper()
@@ -207,29 +249,162 @@ describe('PoolXPTubeSubmitPanel', () => {
})
it.each([
- 'initial',
- 'fetching',
+ 'ready_to_export',
+ 'checking_tube_status',
'tube_exists',
- 'polling',
+ 'exporting',
'tube_export_success',
'failure_export_tube',
- 'failure_poll_tube',
- 'failure_export_tube_after_recheck',
+ 'failure_tube_check',
+ 'failure_tube_check_export',
'invalid_props',
])('displays the correct status based on %s state', (stateValue) => {
const wrapper = createWrapper(stateValue)
verifyComponentState(wrapper, stateValue)
})
+ describe('methods', () => {
+ describe('initialiseStartState', () => {
+ let wrapper
+ beforeEach(async () => {
+ wrapper = createWrapper()
+ // Mock the sleep function to resolve immediately
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ })
+ it('transitions to TUBE_ALREADY_EXPORTED state when tube is found', async () => {
+ // Mock the fetch response to indicate a tube is found
+ mockTubeFoundResponse()
+
+ // Call initialiseStartState
+ await wrapper.vm.initialiseStartState()
+ await flushPromises()
+
+ // Check the state after initialiseStartState
+ expect(wrapper.vm.state).toBe('tube_exists')
+ })
+
+ it('transitions to FAILURE_TUBE_CHECK state when service error occurs', async () => {
+ // Mock the fetch response to indicate a service error
+ mockTubeCheckFailureResponse()
+
+ // Call initialiseStartState
+ await wrapper.vm.initialiseStartState()
+ await flushPromises()
+
+ // Check the state after initialiseStartState
+ expect(wrapper.vm.state).toBe('failure_tube_check')
+ })
+
+ it('transitions to READY_TO_EXPORT state when no tube is found', async () => {
+ // Mock the fetch response to indicate no tube is found
+ mockNoTubeFoundResponse()
+
+ // Call initialiseStartState
+ await wrapper.vm.initialiseStartState()
+ await flushPromises()
+
+ // Check the state after initialiseStartState
+ expect(wrapper.vm.state).toBe('ready_to_export')
+ })
+ })
+
+ describe('checkTubeInTraction', () => {
+ let wrapper
+ beforeEach(async () => {
+ wrapper = createWrapper()
+ // Mock the sleep function to resolve immediately
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ })
+
+ it('returns "found" when tube is found', async () => {
+ // Mock the fetch response to indicate the tube is found
+ mockTubeFoundResponse()
+ const result = await wrapper.vm.checkTubeInTraction()
+ expect(result).toBe('found')
+ })
+
+ it('returns "not_found" when no tube is found', async () => {
+ // Mock the fetch response to indicate no tube is found
+ mockNoTubeFoundResponse()
+ const result = await wrapper.vm.checkTubeInTraction()
+ expect(result).toBe('not_found')
+ })
+
+ it('returns "service_error" when response is not ok', async () => {
+ // Mock the fetch response to indicate a service error
+ mockTubeCheckFailureResponse()
+ const result = await wrapper.vm.checkTubeInTraction()
+ expect(result).toBe('service_error')
+ })
+
+ it('returns "service_error" when an exception is thrown', async () => {
+ // Mock the fetch to throw an error
+ vi.spyOn(global, 'fetch').mockImplementationOnce(() => Promise.reject(new Error('Network error')))
+
+ const result = await wrapper.vm.checkTubeInTraction()
+ expect(result).toBe('service_error')
+ })
+ })
+
+ describe('checkTubeStatusWithRetries', () => {
+ let wrapper
+ beforeEach(async () => {
+ wrapper = createWrapper()
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ })
+ it('calls checkTubeStatusWithRetries once when tube is found on first try', async () => {
+ // Mock the fetch response to indicate the tube is found on the first try
+ vi.spyOn(wrapper.vm, 'checkTubeInTraction').mockImplementationOnce(() => Promise.resolve('found'))
+
+ const result = await wrapper.vm.checkTubeStatusWithRetries()
+ expect(result).toBe('found')
+ expect(wrapper.vm.checkTubeInTraction).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls checkTubeStatusWithRetries multiple times when tube is found after retries', async () => {
+ // Mock the fetch response to indicate the tube is found after some retries
+ vi.spyOn(wrapper.vm, 'checkTubeInTraction')
+ .mockImplementationOnce(() => Promise.resolve('not_found'))
+ .mockImplementationOnce(() => Promise.resolve('not_found'))
+ .mockImplementationOnce(() => Promise.resolve('found'))
+
+ const result = await wrapper.vm.checkTubeStatusWithRetries(5, 1000)
+ expect(result).toBe('found')
+ expect(wrapper.vm.checkTubeInTraction).toHaveBeenCalledTimes(3)
+ })
+
+ it('calls checkTubeStatusWithRetries the maximum number of retries when tube is not found', async () => {
+ // Mock the fetch response to indicate no tube is found after all retries
+ vi.spyOn(wrapper.vm, 'checkTubeInTraction').mockImplementation(() => Promise.resolve('not_found'))
+
+ const result = await wrapper.vm.checkTubeStatusWithRetries(3, 1000)
+ expect(result).toBe('not_found')
+ expect(wrapper.vm.checkTubeInTraction).toHaveBeenCalledTimes(3)
+ })
+
+ it('calls checkTubeStatusWithRetries once when service error occurs', async () => {
+ // Mock the fetch response to indicate a service error
+ vi.spyOn(wrapper.vm, 'checkTubeInTraction').mockImplementation(() => Promise.resolve('service_error'))
+
+ const result = await wrapper.vm.checkTubeStatusWithRetries(3, 1000)
+ expect(result).toBe('service_error')
+ expect(wrapper.vm.checkTubeInTraction).toHaveBeenCalledTimes(3)
+ })
+ })
+ })
+
describe('on Mount', () => {
- it('calls isTubeInTraction on mount', async () => {
- vi.spyOn(PoolXPTubeSubmitPanel.methods, 'isTubeInTraction')
+ it('calls checkTubeInTraction on mount', async () => {
+ vi.spyOn(PoolXPTubeSubmitPanel.methods, 'checkTubeInTraction')
wrapper = createWrapper()
- expect(PoolXPTubeSubmitPanel.methods.isTubeInTraction).toHaveBeenCalled()
+ expect(PoolXPTubeSubmitPanel.methods.checkTubeInTraction).toHaveBeenCalled()
})
it('handles invalid props correctly on mount', async () => {
- wrapper = createWrapper('initial', {
+ wrapper = createWrapper('checking_tube_status', {
barcode: '',
userId: '',
sequencescapeApiUrl: '',
@@ -242,13 +417,7 @@ describe('PoolXPTubeSubmitPanel', () => {
})
it('handles fetching tube success from Traction correctly on mount', async () => {
- vi.spyOn(global, 'fetch').mockImplementation(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [{ id: '1' }] }),
- }),
- )
-
+ mockTubeFoundResponse()
wrapper = createWrapper()
await flushPromises()
expect(global.fetch).toBeCalledTimes(1)
@@ -257,178 +426,249 @@ describe('PoolXPTubeSubmitPanel', () => {
expect(wrapper.vm.state).toBe('tube_exists')
verifyComponentState(wrapper, 'tube_exists')
})
+ it('handles fetching no tubes from Traction correctly on mount', async () => {
+ mockNoTubeFoundResponse()
+ wrapper = createWrapper()
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('ready_to_export')
+ verifyComponentState(wrapper, 'ready_to_export')
+ })
it('handles fetching tube failure from Traction correctly on mount', async () => {
- vi.spyOn(global, 'fetch').mockImplementation(() =>
- Promise.resolve({
- ok: false,
- json: () => Promise.resolve({ error: 'API call failed' }),
- }),
- )
+ mockTubeCheckFailureResponse()
wrapper = createWrapper()
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
await flushPromises()
- expect(wrapper.vm.state).toBe('initial')
- verifyComponentState(wrapper, 'initial')
+ expect(wrapper.vm.state).toBe('failure_tube_check')
+ verifyComponentState(wrapper, 'failure_tube_check')
})
})
describe('Export action', () => {
- it('handleSubmit on button click', async () => {
- const spyHandleSubmit = spyMethod('handleSubmit')
- wrapper = createWrapper()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- expect(spyHandleSubmit).toHaveBeenCalled()
- })
- it('calls exportTubeToTraction when handleSubmit is invoked in the normal state', async () => {
- const spyExportTubeToTraction = spyMethod('exportTubeToTraction')
- wrapper = createWrapper()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- expect(spyExportTubeToTraction).toHaveBeenCalled()
+ describe('When tube is not already exported and component is in ready_to_export state', () => {
+ let originalSleep, wrapper
+ beforeEach(async () => {
+ mockNoTubeFoundResponse()
+ wrapper = createWrapper()
+ originalSleep = wrapper.vm.sleep
+ // Mock the sleep function to resolve immediately
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ })
+ it('dispalys the correct state', () => {
+ expect(wrapper.vm.state).toBe('ready_to_export')
+ verifyComponentState(wrapper, 'ready_to_export')
+ })
+ it('immediately transitions to exporting state when export button is clicked', async () => {
+ wrapper.vm.sleep = originalSleep
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('exporting')
+ })
+ it('transitions state correctly when export is successful', async () => {
+ mockTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_export_success')
+ verifyComponentState(wrapper, 'tube_export_success')
+
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(window.open).toHaveBeenCalledWith(wrapper.vm.tractionTubeOpenUrl, '_blank')
+ })
+ it('transitions state correctly when no tube exists and export fails', async () => {
+ mockTubeCheckFailureResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_export_tube')
+ verifyComponentState(wrapper, 'failure_export_tube')
+ })
+ it('transitions states correctly when no tube exists and tube checking using traction api fails', async () => {
+ mockFetch([
+ emptyResponse, // Initial empty response
+ failedResponse, // Subsequent failure response
+ ])
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_tube_check_export')
+ verifyComponentState(wrapper, 'failure_tube_check_export')
+
+ mockNoTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_tube_check_export')
+ verifyComponentState(wrapper, 'failure_tube_check_export')
+ })
+ it('transitions states correctly when no tube exists and tube checking using traction api succeeds', async () => {
+ mockFetch([
+ emptyResponse, // Initial empty response
+ failedResponse, // Subsequent success response
+ ])
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_tube_check_export')
+ verifyComponentState(wrapper, 'failure_tube_check_export')
+
+ mockTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_export_success')
+ verifyComponentState(wrapper, 'tube_export_success')
+ })
})
- it('calls isTubeInTraction when handleSubmit is invoked in the failure_poll_tube state', async () => {
- const spyExportTubeToTraction = spyMethod('exportTubeToTraction')
- const spyIsTubeInTraction = spyMethod('isTubeInTraction')
- wrapper = createWrapper()
- wrapper.setData({ state: 'failure_poll_tube' })
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- expect(spyIsTubeInTraction).toHaveBeenCalled()
- expect(spyExportTubeToTraction).not.toHaveBeenCalled()
+ describe('When tube is already exported and component is in tube_exists state', () => {
+ let wrapper
+ beforeEach(async () => {
+ mockTubeFoundResponse()
+ wrapper = createWrapper()
+ await flushPromises()
+ })
+ it('dispalys the correct state', () => {
+ expect(wrapper.vm.state).toBe('tube_exists')
+ verifyComponentState(wrapper, 'tube_exists')
+ })
+ it('opens Traction in a new tab when open traction button is clicked', async () => {
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(window.open).toHaveBeenCalledWith(wrapper.vm.tractionTubeOpenUrl, '_blank')
+ })
})
- it.each(['tube_export_success', 'tube_exists'])(
- 'opens Traction in a new tab when handleSubmit is invoked in the %s state',
- async (stateValue) => {
- const spyWindowOpen = vi.spyOn(window, 'open').mockImplementation(() => {})
- const wrapper = createWrapper()
+ describe('when traction api to check tube fails on mount and component is in failure_tube_check state', () => {
+ let wrapper
+ beforeEach(async () => {
+ mockTubeCheckFailureResponse()
+ wrapper = createWrapper()
+ // Mock the sleep function to resolve immediately
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ })
+ it('displays the correct state', () => {
+ expect(wrapper.vm.state).toBe('failure_tube_check')
+ verifyComponentState(wrapper, 'failure_tube_check')
+ })
+ it('remains in failure_tube_check state if response is again failure when refresh button is clicked', async () => {
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_tube_check')
+ verifyComponentState(wrapper, 'failure_tube_check')
+ })
- // Set the state and trigger the click
- wrapper.setData({ state: stateValue })
+ it('transitions to ready_to_export state when no existing tubes are found and the refresh button is clicked', async () => {
+ mockNoTubeFoundResponse()
await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- expect(spyWindowOpen).toHaveBeenCalledWith(wrapper.vm.tractionTubeOpenUrl, '_blank')
- },
- )
- it('handles export tube success correctly', async () => {
- vi.spyOn(global, 'fetch').mockImplementation(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [] }),
- }),
- )
-
- const spyPollTractionForTube = spyMethod('pollTractionForTube')
- wrapper = createWrapper()
- await flushPromises()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- await flushPromises()
- expect(global.fetch).toHaveBeenCalledWith(wrapper.vm.sequencescapeApiExportUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(wrapper.vm.submitPayload),
- })
- expect(spyPollTractionForTube).toHaveBeenCalled()
- expect(wrapper.vm.state).toBe('polling')
- verifyComponentState(wrapper, 'polling')
- })
- it('handles export tube failure correctly', async () => {
- vi.spyOn(global, 'fetch').mockImplementation(() =>
- Promise.resolve({
- ok: false,
- json: () => Promise.resolve({ error: 'API call failed' }),
- }),
- )
- const spyPollTractionForTube = spyMethod('pollTractionForTube')
- wrapper = createWrapper()
- await flushPromises()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- await flushPromises()
- expect(wrapper.vm.state).toBe('failure_export_tube')
- expect(spyPollTractionForTube).not.toHaveBeenCalled()
- verifyComponentState(wrapper, 'failure_export_tube')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('ready_to_export')
+ verifyComponentState(wrapper, 'ready_to_export')
+ })
+ it('transitions to tube_exists state when an existing tube is found when refresh button is clicked', async () => {
+ mockTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_exists')
+ verifyComponentState(wrapper, 'tube_exists')
+ })
})
- it('handles export tube and pollTractionForTube successes correctly', async () => {
- vi.spyOn(global, 'fetch').mockImplementationOnce(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [{ id: '1' }] }),
- }),
- )
- wrapper = createWrapper()
- await flushPromises()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- vi.advanceTimersByTime(pollInterval)
- await flushPromises()
- expect(wrapper.vm.state).toBe('tube_exists')
- verifyComponentState(wrapper, 'tube_exists')
+ describe('when export api fails and the component is in failure_export_tube state', () => {
+ let wrapper, originalSleep
+ beforeEach(async () => {
+ mockNoTubeFoundResponse()
+ wrapper = createWrapper()
+ originalSleep = wrapper.vm.sleep
+ // Mock the sleep function to resolve immediately
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
+ await flushPromises()
+ mockTubeCheckFailureResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ })
+ it('displays the correct state', () => {
+ expect(wrapper.vm.state).toBe('failure_export_tube')
+ verifyComponentState(wrapper, 'failure_export_tube')
+ })
+ it('immediately transitions to exporting state when export button is clicked', async () => {
+ wrapper.vm.sleep = originalSleep
+ mockNoTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('exporting')
+ verifyComponentState(wrapper, 'exporting')
+ })
+ it('remains in failure_export_tube state if response is failure when retry button is clicked', async () => {
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_export_tube')
+ verifyComponentState(wrapper, 'failure_export_tube')
+ })
+ it('transitions to tube_export_success state if response is success when retry button is clicked', async () => {
+ mockTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_export_success')
+ verifyComponentState(wrapper, 'tube_export_success')
+ })
+ it('transitions to failure_tube_check state if response is tube check api fails when retry button is clicked', async () => {
+ mockFetch([
+ emptyResponse, // Initial successful response
+ failedResponse, // Subsequent failure response
+ ])
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_tube_check_export')
+ verifyComponentState(wrapper, 'failure_tube_check_export')
+ })
})
- it('handles export tube success and pollTractionForTube failure correctly', async () => {
- vi.spyOn(global, 'fetch')
- .mockImplementationOnce(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [] }),
- }),
- )
- .mockImplementationOnce(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [] }),
- }),
- )
- .mockImplementation(() =>
- Promise.resolve({
- ok: false,
- json: () => Promise.resolve({ error: 'API call failed' }),
- }),
- )
- wrapper = createWrapper()
- await flushPromises()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- await flushPromises()
- // Fast-forward time to simulate the polling intervals
- for (let i = 0; i <= maxPollAttempts; i++) {
- vi.advanceTimersByTime(pollInterval)
+ describe('when the traction api to check tube fails after export and the component is in failure_tube_check_export state', () => {
+ let wrapper, originalSleep
+ beforeEach(async () => {
+ mockNoTubeFoundResponse()
+ wrapper = createWrapper()
+ originalSleep = wrapper.vm.sleep
+ // Mock the sleep function to resolve immediately
+ wrapper.vm.sleep = vi.fn().mockImplementation(() => Promise.resolve())
await flushPromises()
- }
- await flushPromises()
- expect(wrapper.vm.state).toBe('failure_poll_tube')
- verifyComponentState(wrapper, 'failure_poll_tube')
+ mockTubeCheckFailureResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ mockFetch([
+ emptyResponse, // Initial successful response
+ failedResponse, // Subsequent failure response
+ ])
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ })
+ it('displays the correct state', () => {
+ expect(wrapper.vm.state).toBe('failure_tube_check_export')
+ verifyComponentState(wrapper, 'failure_tube_check_export')
+ })
+ it('immediately transitions to exporting state when retry button is clicked', async () => {
+ wrapper.vm.sleep = originalSleep
+ mockNoTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('exporting')
+ verifyComponentState(wrapper, 'exporting')
+ })
+ it('transitions to tube_export_success state if response is success when retry button is clicked', async () => {
+ mockTubeFoundResponse()
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('tube_export_success')
+ verifyComponentState(wrapper, 'tube_export_success')
+ })
+ it('remains in failure_tube_check_export state if response is failure when retry button is clicked', async () => {
+ mockFetch([
+ emptyResponse, // Initial successful response
+ failedResponse, // Subsequent failure response
+ ])
+ await wrapper.find('#pool_xp_tube_export_button').trigger('click')
+ await flushPromises()
+ expect(wrapper.vm.state).toBe('failure_tube_check_export')
+ verifyComponentState(wrapper, 'failure_tube_check_export')
+ })
})
})
- it('handles export tube success and pollTractionForTube success correctly', async () => {
- vi.spyOn(global, 'fetch')
- .mockImplementationOnce(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [] }),
- }),
- )
- .mockImplementationOnce(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [] }),
- }),
- )
- .mockImplementation(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve({ data: [{ id: '1' }] }),
- }),
- )
- wrapper = createWrapper()
- await flushPromises()
- await wrapper.find('#pool_xp_tube_export_button').trigger('click')
- await flushPromises()
- // Fast-forward time to simulate the polling intervals
- for (let i = 0; i <= maxPollAttempts; i++) {
- vi.advanceTimersByTime(pollInterval)
- await flushPromises()
- }
- await flushPromises()
- expect(wrapper.vm.state).toBe('tube_export_success')
- verifyComponentState(wrapper, 'tube_export_success')
- })
})
From ce86d3c06b43fbfa5ae33efd8b2581f59c8d1288 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Mon, 28 Oct 2024 10:29:12 +0000
Subject: [PATCH 24/33] update(PcrPoolXpPresenter): documentation update
---
app/models/presenters/pcr_pool_xp_presenter.rb | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/models/presenters/pcr_pool_xp_presenter.rb b/app/models/presenters/pcr_pool_xp_presenter.rb
index e7bb8d165..82b9a0c16 100644
--- a/app/models/presenters/pcr_pool_xp_presenter.rb
+++ b/app/models/presenters/pcr_pool_xp_presenter.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
module Presenters
- # FinalTubePresenter
+ # PcrPoolXpPresenter
+ # This class is responsible for presenting PCR Pool XP tube data.
+ # It inherits from FinalTubePresenter and provides methods
+ # to export data to Traction.
class PcrPoolXpPresenter < FinalTubePresenter
+ # Enables the export of the PCR Pool XP tube to Traction.
def export_to_traction
true
end
From d0d84412ebab26b421a09fe17fedfe15b45462c5 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Mon, 28 Oct 2024 10:35:52 +0000
Subject: [PATCH 25/33] update(PoolXPTubeSubmitPanel): adds traction-ui url to
prop validation method
---
.../pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
index a90a4807d..475e91d1e 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
@@ -290,7 +290,7 @@ export default {
* Validate the props
*/
validateProps() {
- if (!(this.barcode && this.userId && this.sequencescapeApiUrl && this.tractionServiceUrl)) {
+ if (!(this.barcode && this.userId && this.sequencescapeApiUrl && this.tractionServiceUrl && this.tractionUIUrl)) {
this.state = StateEnum.INVALID_PROPS
return
}
From c02e35f88e6bc04479971e42627953288e730f6b Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:05:14 +0000
Subject: [PATCH 26/33] refactor(PcrPoolXpPresenter): Enables the export of the
PCR Pool XP tube to Traction if tube is in passed state
---
app/models/presenters/pcr_pool_xp_presenter.rb | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/app/models/presenters/pcr_pool_xp_presenter.rb b/app/models/presenters/pcr_pool_xp_presenter.rb
index 82b9a0c16..14c4075d8 100644
--- a/app/models/presenters/pcr_pool_xp_presenter.rb
+++ b/app/models/presenters/pcr_pool_xp_presenter.rb
@@ -6,9 +6,14 @@ module Presenters
# It inherits from FinalTubePresenter and provides methods
# to export data to Traction.
class PcrPoolXpPresenter < FinalTubePresenter
- # Enables the export of the PCR Pool XP tube to Traction.
+ # Enables the export of the PCR Pool XP tube to Traction if tube is in passed state.
def export_to_traction
- true
+ if self.state == 'passed'
+ # Add the logic for exporting to Traction here
+ true
+ else
+ false
+ end
end
end
end
From f0855744f7102fb45888cf64b4f5bab82b9cb322 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:06:01 +0000
Subject: [PATCH 27/33] tests: Update tests to check the export of the PCR Pool
XP tube to Traction is enabled if tube is in passed state
---
spec/models/presenters/pcr_pool_xp_presenter_spec.rb | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/spec/models/presenters/pcr_pool_xp_presenter_spec.rb b/spec/models/presenters/pcr_pool_xp_presenter_spec.rb
index f0aeab625..653a7272c 100644
--- a/spec/models/presenters/pcr_pool_xp_presenter_spec.rb
+++ b/spec/models/presenters/pcr_pool_xp_presenter_spec.rb
@@ -13,7 +13,7 @@
let(:purpose_name) { 'Limber example purpose' }
let(:title) { purpose_name }
- let(:state) { 'pending' }
+ let(:state) { 'passed' }
let(:summary_tab) do
[
['Barcode', 'NT6T 3980000006844 '],
@@ -32,4 +32,9 @@
it 'has export_to_traction option' do
expect(subject.export_to_traction).to be_truthy
end
+
+ it 'has no export_to_traction option' do
+ labware.state = 'pending'
+ expect(subject.export_to_traction).to be_falsey
+ end
end
From b33188e70db7956cb24376fbb2385b46f4e93115 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:14:18 +0000
Subject: [PATCH 28/33] update(PcrPoolXpPresenter): simplified check for
'passed' state
---
app/models/presenters/pcr_pool_xp_presenter.rb | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/app/models/presenters/pcr_pool_xp_presenter.rb b/app/models/presenters/pcr_pool_xp_presenter.rb
index 14c4075d8..cce6fa01c 100644
--- a/app/models/presenters/pcr_pool_xp_presenter.rb
+++ b/app/models/presenters/pcr_pool_xp_presenter.rb
@@ -8,12 +8,7 @@ module Presenters
class PcrPoolXpPresenter < FinalTubePresenter
# Enables the export of the PCR Pool XP tube to Traction if tube is in passed state.
def export_to_traction
- if self.state == 'passed'
- # Add the logic for exporting to Traction here
- true
- else
- false
- end
+ state == 'passed'
end
end
end
From 6d78fd5024b9224588f3f335148b57a4c5f36905 Mon Sep 17 00:00:00 2001
From: Ben Topping
Date: Mon, 28 Oct 2024 15:20:00 +0000
Subject: [PATCH 29/33] feat(rvi-bcl): adds limber_reisc as acceptable request
type
---
config/pipelines/high_throughput_rvi_bcl.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/config/pipelines/high_throughput_rvi_bcl.yml b/config/pipelines/high_throughput_rvi_bcl.yml
index 299ad6395..0d21644fc 100644
--- a/config/pipelines/high_throughput_rvi_bcl.yml
+++ b/config/pipelines/high_throughput_rvi_bcl.yml
@@ -5,6 +5,7 @@ RVI Bait Capture Library: # Top of the pipeline (Library Prep)
filters:
request_type_key:
- limber_bait_capture_library_prep
+ - limber_reisc # Typically this should occur on RVI Lib PCR XP
library_type:
- RVI-BCL
library_pass:
From 4ad139be8c902e2a23b5de47417be463fca8fc08 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:33:05 +0000
Subject: [PATCH 30/33] Update index.js
---
.../javascript/pool-xp-tube-panel/index.js | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/app/frontend/javascript/pool-xp-tube-panel/index.js b/app/frontend/javascript/pool-xp-tube-panel/index.js
index 091fb2921..e464821cb 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/index.js
+++ b/app/frontend/javascript/pool-xp-tube-panel/index.js
@@ -10,22 +10,6 @@ import PoolXPTubeSubmitPanel from './components/PoolXPTubeSubmitPanel.vue'
Vue.use(BootstrapVue)
Vue.use(BootstrapVueIcons)
document.addEventListener('DOMContentLoaded', async () => {
- /*
- * As we add more components to this page we should
- * consider switching to proper components and custom tags.
- * Ran into a problems as I tried to do this at this stage:
- * 1 - Vue needs to compile the template (ie. our HTML) on the fly
- # which means we import a different version of vue above.
- # import Vue from 'vue/dist/vue.esm'
- # This is slower, and generally recommended against.
- # 2 - Things didn't appear to be as straight forward as I
- # had hoped. I *think* this was because I began wrestling
- # vue's expectations with regards to single page applications
- # 3 - Vue does NOT like our existing templates. The script tags
- # seem to upset it.
- # In general it looks like this is something we should consider
- # once the majority of our components are vue based.
- */
const assetElem = document.getElementById('pool-xp-tube-submit-panel')
const missingUserIdError = `
Unfortunately Limber can't find your user id, which is required to add custom metadata.
From 958fde464a512498f4d9452f3887fe428054a1eb Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Wed, 30 Oct 2024 11:45:53 +0000
Subject: [PATCH 31/33] refactor(PoolXPTubeSubmitPanel): changed traction-ui
url query params
In traction a new barcode is generated for the given tube and the given barcode is used as a source_identifier field
---
.../pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
index 475e91d1e..48681ef97 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
@@ -260,7 +260,7 @@ export default {
},
tractionTubeOpenUrl() {
if (!this.barcode || !this.tractionUIUrl) return ''
- return `${this.tractionUIUrl}/pacbio/libraries?filter_value=barcode&filter_input=${this.barcode}`
+ return `${this.tractionUIUrl}/pacbio/libraries?filter_value=source_identifier&filter_input=${this.barcode}`
},
submitPayload() {
return {
From 662c207e5933fb69537d970e4853ca6c95ee2d38 Mon Sep 17 00:00:00 2001
From: Seena Nair <55585488+seenanair@users.noreply.github.com>
Date: Wed, 30 Oct 2024 12:15:02 +0000
Subject: [PATCH 32/33] Update PoolXPTubeSubmitPanel.vue
---
.../pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
index 48681ef97..ab1a657bf 100644
--- a/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
+++ b/app/frontend/javascript/pool-xp-tube-panel/components/PoolXPTubeSubmitPanel.vue
@@ -260,7 +260,7 @@ export default {
},
tractionTubeOpenUrl() {
if (!this.barcode || !this.tractionUIUrl) return ''
- return `${this.tractionUIUrl}/pacbio/libraries?filter_value=source_identifier&filter_input=${this.barcode}`
+ return `${this.tractionUIUrl}/#/pacbio/libraries?filter_value=source_identifier&filter_input=${this.barcode}`
},
submitPayload() {
return {
From 3d356d9180587f16852a3c918405732a1f12639b Mon Sep 17 00:00:00 2001
From: Ben Topping
Date: Thu, 31 Oct 2024 14:59:37 +0000
Subject: [PATCH 33/33] fix(bait-layout-preview): update to use well_layout to
fix baits not being displayed
---
app/models/labware_creators/baited_plate.rb | 5 ++++-
spec/factories/bait_library_layout_factories.rb | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/app/models/labware_creators/baited_plate.rb b/app/models/labware_creators/baited_plate.rb
index ebddd05c7..89104674b 100644
--- a/app/models/labware_creators/baited_plate.rb
+++ b/app/models/labware_creators/baited_plate.rb
@@ -26,7 +26,10 @@ def plate
def bait_library_layout_preview
@bait_library_layout_preview ||=
- Sequencescape::Api::V2::BaitLibraryLayout.preview(plate_uuid: parent_uuid, user_uuid: user_uuid).first.layout
+ Sequencescape::Api::V2::BaitLibraryLayout
+ .preview(plate_uuid: parent_uuid, user_uuid: user_uuid)
+ .first
+ .well_layout
end
def create_labware!
diff --git a/spec/factories/bait_library_layout_factories.rb b/spec/factories/bait_library_layout_factories.rb
index f5fc1bc58..a11ebc99e 100644
--- a/spec/factories/bait_library_layout_factories.rb
+++ b/spec/factories/bait_library_layout_factories.rb
@@ -21,7 +21,7 @@
# For example:
# { 'A1' => 'Human all exon 50MB', 'B1' => 'Human all exon 50MB',
# 'C1' => 'Mouse all exon', 'D1' => 'Mouse all exon'}
- layout do
+ well_layout do
wells = WellHelpers.column_order.dup
pools.each_with_object({}) { |pool, hash| wells.shift(pool[:size]).each { |well| hash[well] = pool[:bait] } }
end