diff --git a/cypress/integration/table/search.spec.ts b/cypress/integration/table/search.spec.ts index 8369fb4eb..56d284c74 100644 --- a/cypress/integration/table/search.spec.ts +++ b/cypress/integration/table/search.spec.ts @@ -50,12 +50,8 @@ describe('Search', () => { }); it('searches by date-time', () => { - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-01 00:00' - ); - cy.get('input[aria-label="to, date-time input"]').type( - '2022-01-02 00:00' - ); + cy.findByLabelText('from, date-time input').type('2022-01-01 00:00'); + cy.findByLabelText('to, date-time input').type('2022-01-02 00:00'); cy.startSnoopingBrowserMockedRequest(); @@ -75,14 +71,21 @@ describe('Search', () => { request.url.toString() ); const conditionsMap = getConditionsFromParams(paramMap); - expect(conditionsMap.length).equal(1); + expect(conditionsMap.length).equal(2); - const condition = conditionsMap[0]; - const timestampRange = condition['metadata.timestamp']; - const gte: string = timestampRange['$gte']; - const lte: string = timestampRange['$lte']; - expect(gte).equal('2022-01-01T00:00:00'); - expect(lte).equal('2022-01-02T00:00:59'); + const timestampCondition = conditionsMap[0]; + const timestampRange = timestampCondition['metadata.timestamp']; + const timestampGte: string = timestampRange['$gte']; + const timestampLte: string = timestampRange['$lte']; + expect(timestampGte).equal('2022-01-01T00:00:00'); + expect(timestampLte).equal('2022-01-02T00:00:59'); + + const shotnumCondition = conditionsMap[1]; + const shotnumRange = shotnumCondition['metadata.shotnum']; + const shotnumGte: string = shotnumRange['$gte']; + const shotnumLte: string = shotnumRange['$lte']; + expect(shotnumGte).equal(1); + expect(shotnumLte).equal(2); } ); @@ -98,14 +101,21 @@ describe('Search', () => { request.url.toString() ); const conditionsMap = getConditionsFromParams(paramMap); - expect(conditionsMap.length).equal(1); + expect(conditionsMap.length).equal(2); - const condition = conditionsMap[0]; - const timestampRange = condition['metadata.timestamp']; - const gte: string = timestampRange['$gte']; - const lte: string = timestampRange['$lte']; - expect(gte).equal('2022-01-01T00:00:00'); - expect(lte).equal('2022-01-02T00:00:59'); + const timestampCondition = conditionsMap[0]; + const timestampRange = timestampCondition['metadata.timestamp']; + const timestampGte: string = timestampRange['$gte']; + const timestampLte: string = timestampRange['$lte']; + expect(timestampGte).equal('2022-01-01T00:00:00'); + expect(timestampLte).equal('2022-01-02T00:00:59'); + + const shotnumCondition = conditionsMap[1]; + const shotnumRange = shotnumCondition['metadata.shotnum']; + const shotnumGte: string = shotnumRange['$gte']; + const shotnumLte: string = shotnumRange['$lte']; + expect(shotnumGte).equal(1); + expect(shotnumLte).equal(2); }); }); @@ -115,7 +125,7 @@ describe('Search', () => { }); it('last 10 minutes', () => { - cy.get('div[aria-label="open timeframe search box"]').click(); + cy.findByLabelText('open timeframe search box').click(); cy.contains('Last 10 mins').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -178,7 +188,7 @@ describe('Search', () => { }); it('last 24 hours', () => { - cy.get('div[aria-label="open timeframe search box"]').click(); + cy.findByLabelText('open timeframe search box').click(); cy.contains('Last 24 hours').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -241,7 +251,7 @@ describe('Search', () => { }); it('last 7 days', () => { - cy.get('div[aria-label="open timeframe search box"]').click(); + cy.findByLabelText('open timeframe search box').click(); cy.contains('Last 7 days').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -305,7 +315,7 @@ describe('Search', () => { it('refreshes datetime stamps and launches search if timeframe is set and refresh button clicked', () => { // Set a relative timestamp and verify the initial seach is correct - cy.get('div[aria-label="open timeframe search box"]').click(); + cy.findByLabelText('open timeframe search box').click(); cy.contains('Last 10 mins').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -348,7 +358,7 @@ describe('Search', () => { // Advance time forward a minute cy.tick(60000); - cy.get('button[aria-label="Refresh data"]').click(); + cy.findByLabelText('Refresh data').click(); // wait for search to initiate and finish cy.findByRole('progressbar').should('be.visible'); @@ -391,8 +401,8 @@ describe('Search', () => { }); it('last 5 minutes', () => { - cy.get('div[aria-label="open timeframe search box"]').click(); - cy.get('input[name="timeframe"]').type('5'); + cy.findByLabelText('open timeframe search box').click(); + cy.findByRole('spinbutton', { name: 'Timeframe' }).type('5'); cy.contains('Mins').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -455,8 +465,8 @@ describe('Search', () => { }); it('last 5 hours', () => { - cy.get('div[aria-label="open timeframe search box"]').click(); - cy.get('input[name="timeframe"]').type('5'); + cy.findByLabelText('open timeframe search box').click(); + cy.findByRole('spinbutton', { name: 'Timeframe' }).type('5'); cy.contains('Hours').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -519,8 +529,8 @@ describe('Search', () => { }); it('last 5 days', () => { - cy.get('div[aria-label="open timeframe search box"]').click(); - cy.get('input[name="timeframe"]').type('5'); + cy.findByLabelText('open timeframe search box').click(); + cy.findByRole('spinbutton', { name: 'Timeframe' }).type('5'); cy.contains('Days').click(); const expectedToDate = new Date('1970-01-08 01:00:59'); @@ -584,9 +594,9 @@ describe('Search', () => { }); it('searches by shot number range', () => { - cy.get('div[aria-label="open shot number search box"]').click(); - cy.get('input[name="shot number min"]').type('1'); - cy.get('input[name="shot number max"]').type('9'); + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('spinbutton', { name: 'Min' }).type('1'); + cy.findByRole('spinbutton', { name: 'Max' }).type('9'); cy.startSnoopingBrowserMockedRequest(); @@ -606,14 +616,21 @@ describe('Search', () => { request.url.toString() ); const conditionsMap = getConditionsFromParams(paramMap); - expect(conditionsMap.length).equal(1); + expect(conditionsMap.length).equal(2); - const condition = conditionsMap[0]; - const shotnumRange = condition['metadata.shotnum']; - const gte: string = shotnumRange['$gte']; - const lte: string = shotnumRange['$lte']; - expect(gte).equal(1); - expect(lte).equal(9); + const timestampCondition = conditionsMap[0]; + const timestampRange = timestampCondition['metadata.timestamp']; + const timestampGte: string = timestampRange['$gte']; + const timestampLte: string = timestampRange['$lte']; + expect(timestampGte).equal('2022-01-01T00:00:00'); + expect(timestampLte).equal('2022-01-09T00:00:59'); + + const shotnumCondition = conditionsMap[1]; + const shotnumRange = shotnumCondition['metadata.shotnum']; + const shotnumGte: string = shotnumRange['$gte']; + const shotnumLte: string = shotnumRange['$lte']; + expect(shotnumGte).equal(1); + expect(shotnumLte).equal(9); } ); @@ -629,30 +646,100 @@ describe('Search', () => { request.url.toString() ); const conditionsMap = getConditionsFromParams(paramMap); - expect(conditionsMap.length).equal(1); + expect(conditionsMap.length).equal(2); - const condition = conditionsMap[0]; - const shotnumRange = condition['metadata.shotnum']; - const gte: string = shotnumRange['$gte']; - const lte: string = shotnumRange['$lte']; - expect(gte).equal(1); - expect(lte).equal(9); + const timestampCondition = conditionsMap[0]; + const timestampRange = timestampCondition['metadata.timestamp']; + const timestampGte: string = timestampRange['$gte']; + const timestampLte: string = timestampRange['$lte']; + expect(timestampGte).equal('2022-01-01T00:00:00'); + expect(timestampLte).equal('2022-01-09T00:00:59'); + + const shotnumCondition = conditionsMap[1]; + const shotnumRange = shotnumCondition['metadata.shotnum']; + const shotnumGte: string = shotnumRange['$gte']; + const shotnumLte: string = shotnumRange['$lte']; + expect(shotnumGte).equal(1); + expect(shotnumLte).equal(9); }); }); - it('searches by multiple parameters', () => { - // Date-time fields - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-01 00:00' + it('should highlight boxes red if error in search params', () => { + // Date-time box + + cy.findByLabelText('from, date-time input').type('2022-01-01 00:00'); + cy.findByLabelText('to, date-time input').type('2021-01-01 00:00'); + + cy.findByLabelText('date-time search box').should( + 'have.css', + 'border-color', + 'rgb(214, 65, 65)' // shade of red ); - cy.get('input[aria-label="to, date-time input"]').type( - '2022-01-02 00:00' + + // Shot number box + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('spinbutton', { name: 'Min' }).type('2'); + cy.findByRole('spinbutton', { name: 'Max' }).type('1'); + cy.findByLabelText('close shot number search box').click(); + cy.findByLabelText('open shot number search box').should( + 'have.css', + 'border-color', + 'rgb(214, 65, 65)' // shade of red ); + }); + + it('select a experiment Id and it appears in the experiment box', () => { + const expectedExperiment = { + _id: '22110007-1', + end_date: '2022-01-15T12:00:59', + experiment_id: '22110007', + part: 1, + start_date: '2022-01-12T13:00:00', + }; // Shot number fields - cy.get('div[aria-label="open shot number search box"]').click(); - cy.get('input[name="shot number min"]').type('1'); - cy.get('input[name="shot number max"]').type('9'); + + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('spinbutton', { name: 'Min' }).type('1'); + cy.findByRole('spinbutton', { name: 'Max' }).type('9'); + cy.findByLabelText('close shot number search box').click(); + cy.findByLabelText('open shot number search box') + .contains('1 to 9') + .should('exist'); + + // timeframe + + cy.findByLabelText('open timeframe search box').click(); + cy.findByRole('spinbutton', { name: 'Timeframe' }).type('5'); + cy.contains('Days').click(); + cy.findByLabelText('close timeframe search box').click(); + + cy.findByLabelText('open timeframe search box') + .contains('5 days') + .should('exist'); + + // experiment box + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('not.exist'); + + // Checks that when a experiment id is selected it updates + // the shot number, timeframe and experiment id + + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('open shot number search box') + .contains('1 to 9') + .should('not.exist'); + + cy.findByLabelText('open timeframe search box') + .contains('5 days') + .should('not.exist'); cy.startSnoopingBrowserMockedRequest(); @@ -675,18 +762,19 @@ describe('Search', () => { expect(conditionsMap.length).equal(2); const timestampCondition = conditionsMap[0]; + const timestampRange = timestampCondition['metadata.timestamp']; const timestampGte: string = timestampRange['$gte']; const timestampLte: string = timestampRange['$lte']; - expect(timestampGte).equal('2022-01-01T00:00:00'); - expect(timestampLte).equal('2022-01-02T00:00:59'); + expect(timestampGte).equal(expectedExperiment.start_date); + expect(timestampLte).equal(expectedExperiment.end_date); const shotnumCondition = conditionsMap[1]; const shotnumRange = shotnumCondition['metadata.shotnum']; const shotnumGte: string = shotnumRange['$gte']; const shotnumLte: string = shotnumRange['$lte']; - expect(shotnumGte).equal(1); - expect(shotnumLte).equal(9); + expect(shotnumGte).equal(13); + expect(shotnumLte).equal(15); } ); @@ -705,79 +793,31 @@ describe('Search', () => { expect(conditionsMap.length).equal(2); const timestampCondition = conditionsMap[0]; + const timestampRange = timestampCondition['metadata.timestamp']; const timestampGte: string = timestampRange['$gte']; const timestampLte: string = timestampRange['$lte']; - expect(timestampGte).equal('2022-01-01T00:00:00'); - expect(timestampLte).equal('2022-01-02T00:00:59'); + expect(timestampGte).equal(expectedExperiment.start_date); + expect(timestampLte).equal(expectedExperiment.end_date); const shotnumCondition = conditionsMap[1]; const shotnumRange = shotnumCondition['metadata.shotnum']; const shotnumGte: string = shotnumRange['$gte']; const shotnumLte: string = shotnumRange['$lte']; - expect(shotnumGte).equal(1); - expect(shotnumLte).equal(9); + expect(shotnumGte).equal(13); + expect(shotnumLte).equal(15); }); }); - it('should highlight boxes red if error in search params', () => { - // Date-time box - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-01 00:00' - ); - cy.get('input[aria-label="to, date-time input"]').type( - '2021-01-01 00:00' - ); - cy.get('div[aria-label="date-time search box"]').should( - 'have.css', - 'border-color', - 'rgb(214, 65, 65)' // shade of red - ); - - // Shot Number box - cy.get('div[aria-label="open shot number search box"]').click(); - cy.get('input[name="shot number min"]').type('2'); - cy.get('input[name="shot number max"]').type('1'); - cy.get('div[aria-label="close shot number search box"]').click(); - cy.get('div[aria-label="open shot number search box"]').should( - 'have.css', - 'border-color', - 'rgb(214, 65, 65)' // shade of red - ); - }); - - it('select a experiment Id and it appears in the experiment box', () => { - // experiment box - cy.findByLabelText('open experiment search box') - .contains('ID 19510000') - .should('not.exist'); - - cy.findByLabelText('open experiment search box').click(); - cy.findByRole('combobox').type('195').type('{downArrow}{enter}'); - cy.findByLabelText('close experiment search box').click(); - cy.findByLabelText('open experiment search box') - .contains('ID 19510000') - .should('exist'); - }); - it('changes to and from dateTimes to use 0 seconds and 59 seconds respectively', () => { - // Date-time fields - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-01 00:00' - ); - cy.get('input[aria-label="to, date-time input"]').type( - '2022-01-02 00:00' - ); + cy.findByLabelText('from, date-time input').type('2022-01-01 00:00'); + cy.findByLabelText('to, date-time input').type('2022-01-02 00:00'); + const expectedToDate = new Date('2022-01-02 00:00:59'); const expectedFromDate = new Date('2022-01-01 00:00:00'); const expectedToDateString = formatDateTimeForApi(expectedToDate); const expectedFromDateString = formatDateTimeForApi(expectedFromDate); - // Shot number fields - cy.get('div[aria-label="open shot number search box"]').click(); - cy.get('input[name="shot number min"]').type('1'); - cy.get('input[name="shot number max"]').type('9'); - cy.startSnoopingBrowserMockedRequest(); cy.contains('Search').click(); @@ -803,6 +843,13 @@ describe('Search', () => { const lte: string = timestampRange['$lte']; expect(gte).equal(expectedFromDateString); expect(lte).equal(expectedToDateString); + + const shotnumCondition = conditionsMap[1]; + const shotnumRange = shotnumCondition['metadata.shotnum']; + const shotnumGte: string = shotnumRange['$gte']; + const shotnumLte: string = shotnumRange['$lte']; + expect(shotnumGte).equal(1); + expect(shotnumLte).equal(2); } ); @@ -826,9 +873,166 @@ describe('Search', () => { const lte: string = timestampRange['$lte']; expect(gte).equal(expectedFromDateString); expect(lte).equal(expectedToDateString); + + const shotnumCondition = conditionsMap[1]; + const shotnumRange = shotnumCondition['metadata.shotnum']; + const shotnumGte: string = shotnumRange['$gte']; + const shotnumLte: string = shotnumRange['$lte']; + expect(shotnumGte).equal(1); + expect(shotnumLte).equal(2); }); }); + it('searches within an experiment timeframe without the experiment id clearing', () => { + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('from, date-time picker').click(); + cy.findByRole('dialog').contains(13).click(); + cy.findByLabelText('from, date-time picker').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('to, date-time picker').click(); + cy.findByRole('dialog').contains(14).click(); + cy.findByLabelText('to, date-time picker').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + }); + + it('clears experiment id when it searches outside the given experiment id experiment timeframe', () => { + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('from, date-time picker').click(); + cy.findByRole('dialog').contains(11).click(); + cy.findByLabelText('from, date-time picker').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('not.exist'); + + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('to, date-time picker').click(); + cy.findByRole('dialog').contains(16).click(); + cy.findByLabelText('to, date-time picker').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('not.exist'); + }); + + it('searches within an experiment shot number range without the experiment id clearing', () => { + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Min', + }) + .clear(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Min', + }) + .type(14); + cy.findByLabelText('close shot number search box').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Max', + }) + .clear(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Max', + }) + .type(14); + cy.findByLabelText('close shot number search box').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + }); + + it('clears experiment id when it searches outside the given experiment id shot number range', () => { + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Min', + }) + .clear(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Min', + }) + .type(12); + cy.findByLabelText('close shot number search box').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('not.exist'); + + cy.findByLabelText('open experiment search box').click(); + cy.findByRole('combobox').type('221').type('{downArrow}{enter}'); + cy.findByLabelText('close experiment search box').click(); + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('exist'); + + cy.findByLabelText('open shot number search box').click(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Max', + }) + .clear(); + cy.findByRole('dialog') + .findByRole('spinbutton', { + name: 'Max', + }) + .type(16); + cy.findByLabelText('close shot number search box').click(); + + cy.findByLabelText('open experiment search box') + .contains('ID 22110007') + .should('not.exist'); + }); + it('can be hidden and shown', () => { cy.contains(/^Search$/).should('be.visible'); @@ -863,9 +1067,7 @@ describe('Search', () => { }); it('displays appropriate tooltips', () => { - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-01 00:00' - ); + cy.findByLabelText('from, date-time input').type('2022-01-01 00:00'); cy.startSnoopingBrowserMockedRequest(); @@ -936,10 +1138,8 @@ describe('Search', () => { ); }); - cy.get('input[aria-label="from, date-time input"]').clear(); - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-11 00:00' - ); + cy.findByLabelText('from, date-time input').clear(); + cy.findByLabelText('from, date-time input').type('2022-01-11 00:00'); cy.contains('Search').click(); // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -950,10 +1150,8 @@ describe('Search', () => { cy.clearMocks(); - cy.get('input[aria-label="from, date-time input"]').clear(); - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-02 00:00' - ); + cy.findByLabelText('from, date-time input').clear(); + cy.findByLabelText('from, date-time input').type('2022-01-02 00:00'); cy.contains('Search').click(); // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -1006,10 +1204,8 @@ describe('Search', () => { expect(gte).equal('2022-01-02T00:00:00'); }); - cy.get('input[aria-label="from, date-time input"]').clear(); - cy.get('input[aria-label="from, date-time input"]').type( - '2022-01-01 00:00' - ); + cy.findByLabelText('from, date-time input').clear(); + cy.findByLabelText('from, date-time input').type('2022-01-01 00:00'); cy.contains('Search').click(); // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/src/api/experiment.test.tsx b/src/api/experiment.test.tsx index 73686d340..4fd97df67 100644 --- a/src/api/experiment.test.tsx +++ b/src/api/experiment.test.tsx @@ -2,6 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { useExperiment } from './experiment'; import { ExperimentParams } from '../app.types'; import { hooksWrapperWithProviders } from '../setupTests'; +import experimentsJson from '../mocks/experiments.json'; describe('channels api functions', () => { afterEach(() => { @@ -18,43 +19,7 @@ describe('channels api functions', () => { expect(result.current.isSuccess).toBeTruthy(); }); - const expected: ExperimentParams[] = [ - { - _id: '18325019-4', - end_date: '2020-01-06T18:00:00', - experiment_id: '18325019', - part: 4, - start_date: '2020-01-03T10:00:00', - }, - { - _id: '18325019-5', - end_date: '2019-06-12T17:00:00', - experiment_id: '18325019', - part: 5, - start_date: '2019-06-12T09:00:00', - }, - { - _id: '18325024-1', - end_date: '2019-03-13T18:00:00', - experiment_id: '18325024', - part: 1, - start_date: '2019-03-13T10:00:00', - }, - { - _id: '18325025-1', - end_date: '2019-12-01T18:00:00', - experiment_id: '18325025', - part: 1, - start_date: '2019-12-01T10:00:00', - }, - { - _id: '19510000-1', - end_date: '2019-10-01T17:00:00', - experiment_id: '19510000', - part: 1, - start_date: '2019-10-01T09:00:00', - }, - ]; + const expected: ExperimentParams[] = experimentsJson; expect(result.current.data).toEqual(expected); }); diff --git a/src/api/records.test.tsx b/src/api/records.test.tsx index a54e58458..2f9d0cf58 100644 --- a/src/api/records.test.tsx +++ b/src/api/records.test.tsx @@ -20,6 +20,8 @@ import { useIncomingRecordCount, useRecordsPaginated, useThumbnails, + useShotnumToDateConverter, + useDateToShotnumConverter, } from './records'; import { PreloadedState } from '@reduxjs/toolkit'; import { RootState } from '../state/store'; @@ -169,6 +171,60 @@ describe('records api functions', () => { ); }); + describe('useShotnumToDateConverter', () => { + it('send a request to fetch date using ShotnumToDateConverter and returns a succesful response', async () => { + const expectedReponse = { + from: '2022-01-04T00:00:00', + to: '2022-01-18T00:00:00', + min: 4, + max: 19, + }; + + const { result } = renderHook( + () => + useShotnumToDateConverter(expectedReponse.min, expectedReponse.max), + { + wrapper: hooksWrapperWithProviders(state), + } + ); + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + expect(result.current.data).toEqual(expectedReponse); + }); + it.todo( + 'sends axios request to fetch records and throws an appropriate error on failure' + ); + }); + + describe('useDateToShotnumConverter', () => { + it('send a request to fetch date usingDateToShotnumConverter and returns a succesful response', async () => { + const expectedReponse = { + from: '2021-12-01T00:00:00', + to: '2022-01-19T00:00:00', + min: 1, + max: 18, + }; + + const { result } = renderHook( + () => + useDateToShotnumConverter(expectedReponse.from, expectedReponse.to), + { + wrapper: hooksWrapperWithProviders(state), + } + ); + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + expect(result.current.data).toEqual(expectedReponse); + }); + it.todo( + 'sends axios request to fetch records and throws an appropriate error on failure' + ); + }); + describe('useIncomingRecordCount', () => { let params: URLSearchParams; diff --git a/src/api/records.tsx b/src/api/records.tsx index 50a7cf1ea..44502c18c 100644 --- a/src/api/records.tsx +++ b/src/api/records.tsx @@ -15,6 +15,7 @@ import { timeChannelName, isChannelImage, isChannelWaveform, + DateRangetoShotnumConverter, } from '../app.types'; import { useAppSelector } from '../state/hooks'; import { selectQueryParams } from '../state/slices/searchSlice'; @@ -161,6 +162,118 @@ const fetchRecordCountQuery = ( .then((response) => response.data); }; +export const fetchRangeRecordConverterQuery = ( + apiUrl: string, + fromDate: string | undefined, + toDate: string | undefined, + shotnumMin: number | undefined, + shotnumMax: number | undefined +): Promise => { + const queryParams = new URLSearchParams(); + let timestampObj = {}; + if (fromDate || toDate) { + timestampObj = { + from: fromDate, + to: toDate, + }; + } + + if (fromDate || toDate) { + queryParams.append('date_range', JSON.stringify(timestampObj)); + } + + let shotnumObj = {}; + if (shotnumMin || shotnumMax) { + shotnumObj = { + min: shotnumMin, + max: shotnumMax, + }; + } + + if (shotnumMin || shotnumMax) { + queryParams.append('shotnum_range', JSON.stringify(shotnumObj)); + } + + return axios + .get(`${apiUrl}/records/range_converter`, { + params: queryParams, + headers: { + Authorization: `Bearer ${readSciGatewayToken()}`, + }, + }) + .then((response) => { + if (response.data) { + let inputRange; + if (fromDate || toDate) { + inputRange = { from: fromDate, to: toDate }; + } + if (shotnumMin || shotnumMax) { + inputRange = { min: shotnumMin, max: shotnumMax }; + } + return { ...inputRange, ...response.data }; + } + }); +}; + +export const useDateToShotnumConverter = ( + fromDate: string | undefined, + toDate: string | undefined +): UseQueryResult => { + const { apiUrl } = useAppSelector(selectUrls); + + return useQuery< + DateRangetoShotnumConverter, + AxiosError, + DateRangetoShotnumConverter, + [string, { fromDate: string | undefined; toDate: string | undefined }] + >( + ['dateToShotnumConverter', { fromDate, toDate }], + (params) => { + return fetchRangeRecordConverterQuery( + apiUrl, + fromDate, + toDate, + undefined, + undefined + ); + }, + { + onError: (error) => { + console.log('Got error ' + error.message); + }, + } + ); +}; + +export const useShotnumToDateConverter = ( + shotnumMin: number | undefined, + shotnumMax: number | undefined +): UseQueryResult => { + const { apiUrl } = useAppSelector(selectUrls); + + return useQuery< + DateRangetoShotnumConverter, + AxiosError, + DateRangetoShotnumConverter, + [string, { shotnumMin: number | undefined; shotnumMax: number | undefined }] + >( + ['shotnumToDateConverter', { shotnumMin, shotnumMax }], + (params) => { + return fetchRangeRecordConverterQuery( + apiUrl, + undefined, + undefined, + shotnumMin, + shotnumMax + ); + }, + { + onError: (error) => { + console.log('Got error ' + error.message); + }, + } + ); +}; export const useRecordsPaginated = (): UseQueryResult< RecordRow[], AxiosError diff --git a/src/app.types.tsx b/src/app.types.tsx index 3cfabe09c..4f4884fa5 100644 --- a/src/app.types.tsx +++ b/src/app.types.tsx @@ -141,6 +141,13 @@ export interface DateRange { toDate?: string; } +export interface DateRangetoShotnumConverter { + from?: string; + to?: string; + min?: number; + max?: number; +} + export interface ShotnumRange { min?: number; max?: number; diff --git a/src/mocks/experiments.json b/src/mocks/experiments.json index c41962805..414515360 100644 --- a/src/mocks/experiments.json +++ b/src/mocks/experiments.json @@ -1,37 +1,44 @@ [ { - "_id": "18325019-4", - "end_date": "2020-01-06T18:00:00", - "experiment_id": "18325019", - "part": 4, - "start_date": "2020-01-03T10:00:00" + "_id": "19210001-1", + "end_date": "2022-01-03T12:00:00", + "experiment_id": "19210001", + "part": 1, + "start_date": "2021-12-31T13:00:00" }, { - "_id": "18325019-5", - "end_date": "2019-06-12T17:00:00", - "experiment_id": "18325019", - "part": 5, - "start_date": "2019-06-12T09:00:00" + "_id": "19210001-2", + "end_date": "2022-01-06T12:00:00", + "experiment_id": "19210001", + "part": 2, + "start_date": "2022-01-03T13:00:00" }, { - "_id": "18325024-1", - "end_date": "2019-03-13T18:00:00", - "experiment_id": "18325024", + "_id": "19210012-1", + "end_date": "2022-01-09T12:00:00", + "experiment_id": "19210012", "part": 1, - "start_date": "2019-03-13T10:00:00" + "start_date": "2022-01-06T13:00:00" }, { - "_id": "18325025-1", - "end_date": "2019-12-01T18:00:00", - "experiment_id": "18325025", - "part": 1, - "start_date": "2019-12-01T10:00:00" + "_id": "19210012-2", + "end_date": "2022-01-12T12:00:00", + "experiment_id": "19210012", + "part": 2, + "start_date": "2022-01-09T13:00:00" }, { - "_id": "19510000-1", - "end_date": "2019-10-01T17:00:00", - "experiment_id": "19510000", + "_id": "22110007-1", + "end_date": "2022-01-15T12:00:00", + "experiment_id": "22110007", "part": 1, - "start_date": "2019-10-01T09:00:00" + "start_date": "2022-01-12T13:00:00" + }, + { + "_id": "22110007-2", + "end_date": "2022-01-18T12:00:00", + "experiment_id": "22110007", + "part": 2, + "start_date": "2022-01-15T13:00:00" } ] diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 94c807287..f9fe0395e 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -33,6 +33,92 @@ export const handlers = [ rest.get('/records/count', (req, res, ctx) => { return res(ctx.status(200), ctx.json(recordsJson.length)); }), + rest.get('/records/range_converter', (req, res, ctx) => { + const searchParams = new URLSearchParams(req.url.search); + const shotnumRange = searchParams.get('shotnum_range'); + const dateRange = searchParams.get('date_range'); + + if (shotnumRange) { + const { min, max } = JSON.parse(decodeURIComponent(shotnumRange)); + const shotnumMin = Number(min); + const shotnumMax = Number(max); + + const shotnumRangeRecord = recordsJson.filter((record) => { + return ( + record.metadata.shotnum >= shotnumMin && + record.metadata.shotnum <= shotnumMax + ); + }); + + const { shotnumMaxRecord, shotnumMinRecord } = shotnumRangeRecord.reduce( + (acc, record) => { + if (record.metadata.shotnum > acc.shotnumMaxRecord.metadata.shotnum) { + acc.shotnumMaxRecord = record; + } + + if (record.metadata.shotnum < acc.shotnumMinRecord.metadata.shotnum) { + acc.shotnumMinRecord = record; + } + + return acc; + }, + { + shotnumMaxRecord: shotnumRangeRecord[0], + shotnumMinRecord: shotnumRangeRecord[0], + } + ); + + const reponseData = { + from: shotnumMinRecord.metadata.timestamp, + to: shotnumMaxRecord.metadata.timestamp, + }; + + return res(ctx.status(200), ctx.json(reponseData)); + } else if (dateRange) { + const { from: fromDate, to: toDate } = JSON.parse( + decodeURIComponent(dateRange) + ); + + const dateRangeRecord = recordsJson.filter((record) => { + return ( + new Date(record.metadata.timestamp) >= new Date(fromDate) && + new Date(record.metadata.timestamp) <= new Date(toDate) + ); + }); + + const { fromDateRecord, toDateRecord } = dateRangeRecord.reduce( + (acc, record) => { + if ( + new Date(record.metadata.timestamp) > + new Date(acc.fromDateRecord.metadata.timestamp) + ) { + acc.toDateRecord = record; + } + + if ( + new Date(record.metadata.timestamp) < + new Date(acc.toDateRecord.metadata.timestamp) + ) { + acc.fromDateRecord = record; + } + + return acc; + }, + { + fromDateRecord: dateRangeRecord[0], + toDateRecord: dateRangeRecord[0], + } + ); + + const reponseData = { + min: fromDateRecord.metadata.shotnum, + max: toDateRecord.metadata.shotnum, + }; + return res(ctx.status(200), ctx.json(reponseData)); + } else { + return res(ctx.status(500), ctx.json(undefined)); + } + }), rest.get('/channels/summary/:channelName', (req, res, ctx) => { const { channelName } = req.params; let channel; diff --git a/src/search/components/__snapshots__/dateTime.component.test.tsx.snap b/src/search/components/__snapshots__/dateTime.component.test.tsx.snap index 15acb10f7..35433ed82 100644 --- a/src/search/components/__snapshots__/dateTime.component.test.tsx.snap +++ b/src/search/components/__snapshots__/dateTime.component.test.tsx.snap @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`CustomPickersDay function renders the CustomPickerDay correctly 1`] = ` + +
+
+
+ +`; + exports[`DateTime tests renders correctly with input date-time ranges 1`] = `
`; + +exports[`renderExperimentPickerDay function renders a PickersDay component when selectedDate is not with an experiment 1`] = ` + +`; + +exports[`renderExperimentPickerDay function renders a PickersDay component when selectedDate is null 1`] = ` + +`; + +exports[`renderExperimentPickerDay function renders a PickersDay component when selectedDate is with an experiment 1`] = ` + +`; diff --git a/src/search/components/__snapshots__/experiment.component.test.tsx.snap b/src/search/components/__snapshots__/experiment.component.test.tsx.snap index 73e6b46b0..d57782dd5 100644 --- a/src/search/components/__snapshots__/experiment.component.test.tsx.snap +++ b/src/search/components/__snapshots__/experiment.component.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Experiment search renders correctly 1`] = ` >
  • - {option.experiment_id} + {`${option.experiment_id} (part ${option.part})`}
  • ); }; @@ -65,7 +76,13 @@ const ExperimentPopup = (props: ExperimentProps): React.ReactElement => { getOptionLabel={(option) => option.experiment_id} blurOnSelect onChange={(event: unknown, newValue: ExperimentParams | null) => { - if (newValue) onExperimentChange(newValue); + resetTimeframe(); + + if (newValue) { + resetShotnumber(); + changeExperimentTimeframe(newValue); + onExperimentChange(newValue); + } setValue(newValue); }} renderOption={renderOptions} @@ -100,6 +117,23 @@ const Experiment = (props: ExperimentProps): React.ReactElement => { const close = React.useCallback(() => toggle(false), []); // use parent node which is always mounted to get the document to attach event listeners to useClickOutside(popover, close, parent.current?.ownerDocument); + const [flashAnimationPlaying, setFlashAnimationPlaying] = + React.useState(false); + + // Stop the flash animation from playing after 1500ms + React.useEffect(() => { + if (props.experiment === null) { + setFlashAnimationPlaying(true); + setTimeout(() => { + setFlashAnimationPlaying(false); + }, FLASH_ANIMATION.length); + } + }, [props.experiment]); + + // Prevent the flash animation playing on mount + React.useEffect(() => { + setFlashAnimationPlaying(false); + }, []); return ( @@ -111,8 +145,12 @@ const Experiment = (props: ExperimentProps): React.ReactElement => { display: 'flex', flexDirection: 'row', paddingRight: 5, + paddingBottom: '4px', cursor: 'pointer', overflow: 'hidden', + ...(flashAnimationPlaying && { + animation: `${FLASH_ANIMATION.animation} ${FLASH_ANIMATION.length}ms`, + }), }} onClick={() => toggle(!isOpen)} > @@ -120,7 +158,9 @@ const Experiment = (props: ExperimentProps): React.ReactElement => {
    Experiment - {experiment ? `ID ${experiment.experiment_id}` : 'Select'} + {experiment + ? `ID ${experiment.experiment_id} (part ${experiment.part})` + : 'Select'}
    diff --git a/src/search/components/shotNumber.component.test.tsx b/src/search/components/shotNumber.component.test.tsx index 738f49d43..f17688d16 100644 --- a/src/search/components/shotNumber.component.test.tsx +++ b/src/search/components/shotNumber.component.test.tsx @@ -12,6 +12,8 @@ describe('shotNumber search', () => { let props: ShotNumberProps; const changeSearchParameterShotnumMin = jest.fn(); const changeSearchParameterShotnumMax = jest.fn(); + const resetDateRange = jest.fn(); + const resetExperimentTimeframe = jest.fn(); let user; const createView = (): RenderResult => { @@ -22,6 +24,9 @@ describe('shotNumber search', () => { props = { changeSearchParameterShotnumMin, changeSearchParameterShotnumMax, + resetDateRange, + resetExperimentTimeframe, + isDateToShotnum: false, }; user = userEvent.setup(); @@ -68,11 +73,12 @@ describe('shotNumber search', () => { await user.type(maxInput, '2'); expect(changeSearchParameterShotnumMin).toHaveBeenCalledWith(1); expect(changeSearchParameterShotnumMax).toHaveBeenCalledWith(2); + expect(resetDateRange).toHaveBeenCalled(); const helperTexts = within(shotnumPopup).queryAllByText('Invalid range'); expect(helperTexts.length).toEqual(0); }); - it('displays invalid range message when min > max', async () => { + it('displays invalid range message when min > max and calls resetExperimentTimeframe if cleared', async () => { props = { ...props, searchParameterShotnumMin: 1, @@ -95,6 +101,9 @@ describe('shotNumber search', () => { const helperTexts = within(shotnumPopup).getAllByText('Invalid range'); // One helper text below each input expect(helperTexts.length).toEqual(2); + await user.clear(minInput); + await user.clear(maxInput); + expect(resetExperimentTimeframe).toHaveBeenCalled(); }); describe('displays the currently selected shot number range', () => { diff --git a/src/search/components/shotNumber.component.tsx b/src/search/components/shotNumber.component.tsx index 86262ef34..baacc91de 100644 --- a/src/search/components/shotNumber.component.tsx +++ b/src/search/components/shotNumber.component.tsx @@ -2,12 +2,16 @@ import React from 'react'; import { Box, Typography, Divider, Grid, TextField } from '@mui/material'; import { Adjust } from '@mui/icons-material'; import { useClickOutside } from '../../hooks'; +import { FLASH_ANIMATION } from '../../animation'; export interface ShotNumberProps { searchParameterShotnumMin?: number; searchParameterShotnumMax?: number; changeSearchParameterShotnumMin: (min: number | undefined) => void; changeSearchParameterShotnumMax: (max: number | undefined) => void; + resetDateRange: () => void; + resetExperimentTimeframe: () => void; + isDateToShotnum: boolean; } const ShotNumberPopup = ( @@ -19,6 +23,8 @@ const ShotNumberPopup = ( changeSearchParameterShotnumMin: changeMin, changeSearchParameterShotnumMax: changeMax, invalidRange, + resetDateRange, + resetExperimentTimeframe, } = props; return ( @@ -49,11 +55,13 @@ const ShotNumberPopup = ( type="number" size="small" inputProps={{ min: 0 }} - onChange={(event) => + onChange={(event) => { changeMin( event.target.value ? Number(event.target.value) : undefined - ) - } + ); + resetDateRange(); + if (!event.target.value && !max) resetExperimentTimeframe(); + }} error={invalidRange} {...(invalidRange && { helperText: 'Invalid range' })} /> @@ -69,11 +77,13 @@ const ShotNumberPopup = ( type="number" size="small" inputProps={{ min: 0 }} - onChange={(event) => + onChange={(event) => { changeMax( event.target.value ? Number(event.target.value) : undefined - ) - } + ); + resetDateRange(); + if (!event.target.value && !min) resetExperimentTimeframe(); + }} error={invalidRange} {...(invalidRange && { helperText: 'Invalid range' })} /> @@ -84,8 +94,11 @@ const ShotNumberPopup = ( }; const ShotNumber = (props: ShotNumberProps): React.ReactElement => { - const { searchParameterShotnumMin: min, searchParameterShotnumMax: max } = - props; + const { + searchParameterShotnumMin: min, + searchParameterShotnumMax: max, + isDateToShotnum, + } = props; const popover = React.useRef(null); const parent = React.useRef(null); @@ -98,6 +111,32 @@ const ShotNumber = (props: ShotNumberProps): React.ReactElement => { const invalidRange = min !== undefined && max !== undefined ? min > max : false; + const [flashAnimationPlaying, setFlashAnimationPlaying] = + React.useState(false); + + // Stop the flash animation from playing after 1500ms + React.useEffect(() => { + if ( + (typeof props.searchParameterShotnumMax === undefined && + typeof props.searchParameterShotnumMin === undefined) || + isDateToShotnum + ) { + setFlashAnimationPlaying(true); + setTimeout(() => { + setFlashAnimationPlaying(false); + }, FLASH_ANIMATION.length); + } + }, [ + isDateToShotnum, + props.searchParameterShotnumMax, + props.searchParameterShotnumMin, + ]); + + // Prevent the flash animation playing on mount + React.useEffect(() => { + setFlashAnimationPlaying(false); + }, []); + return ( { display: 'flex', flexDirection: 'row', paddingRight: 5, + paddingBottom: '4px', cursor: 'pointer', overflow: 'hidden', + ...(flashAnimationPlaying && { + animation: `${FLASH_ANIMATION.animation} ${FLASH_ANIMATION.length}ms`, + }), }} onClick={() => toggle(!isOpen)} > diff --git a/src/search/components/timeframe.component.test.tsx b/src/search/components/timeframe.component.test.tsx index 9e524e85b..a7143db7f 100644 --- a/src/search/components/timeframe.component.test.tsx +++ b/src/search/components/timeframe.component.test.tsx @@ -12,6 +12,8 @@ describe('timeframe search', () => { let props: TimeframeProps; let user; const changeTimeframe = jest.fn(); + const resetExperimentTimeframe = jest.fn(); + const resetShotnumber = jest.fn(); const createView = (): RenderResult => { return render(); @@ -21,6 +23,8 @@ describe('timeframe search', () => { props = { timeframe: null, changeTimeframe, + resetExperimentTimeframe, + resetShotnumber, }; user = userEvent.setup(); }); @@ -80,6 +84,8 @@ describe('timeframe search', () => { value: 10, timescale: 'minutes', }); + expect(resetExperimentTimeframe).toHaveBeenCalledTimes(1); + expect(resetShotnumber).toHaveBeenCalledTimes(1); }); it('last 24 hours', async () => { @@ -95,6 +101,8 @@ describe('timeframe search', () => { value: 24, timescale: 'hours', }); + expect(resetExperimentTimeframe).toHaveBeenCalledTimes(1); + expect(resetShotnumber).toHaveBeenCalledTimes(1); }); it('last 7 days', async () => { @@ -110,6 +118,8 @@ describe('timeframe search', () => { value: 7, timescale: 'days', }); + expect(resetExperimentTimeframe).toHaveBeenCalledTimes(1); + expect(resetShotnumber).toHaveBeenCalledTimes(1); }); }); @@ -131,6 +141,8 @@ describe('timeframe search', () => { value: 5, timescale: 'minutes', }); + expect(resetExperimentTimeframe).toHaveBeenCalledTimes(1); + expect(resetShotnumber).toHaveBeenCalledTimes(1); }); it('last 18 hours', async () => { @@ -150,6 +162,8 @@ describe('timeframe search', () => { value: 18, timescale: 'hours', }); + expect(resetExperimentTimeframe).toHaveBeenCalledTimes(1); + expect(resetShotnumber).toHaveBeenCalledTimes(1); }); it('last 21 days', async () => { @@ -169,6 +183,8 @@ describe('timeframe search', () => { value: 21, timescale: 'days', }); + expect(resetExperimentTimeframe).toHaveBeenCalledTimes(1); + expect(resetShotnumber).toHaveBeenCalledTimes(1); }); it('buttons do not respond if working timeframe is zero', async () => { @@ -188,6 +204,8 @@ describe('timeframe search', () => { ); expect(changeTimeframe).not.toHaveBeenCalled(); + expect(resetExperimentTimeframe).not.toHaveBeenCalled(); + expect(resetShotnumber).not.toHaveBeenCalled(); }); }); }); diff --git a/src/search/components/timeframe.component.tsx b/src/search/components/timeframe.component.tsx index 278e1c174..bb7484a7f 100644 --- a/src/search/components/timeframe.component.tsx +++ b/src/search/components/timeframe.component.tsx @@ -19,10 +19,12 @@ export type TimeframeRange = { export interface TimeframeProps { timeframe: TimeframeRange | null; changeTimeframe: (value: TimeframeRange) => void; + resetExperimentTimeframe: () => void; + resetShotnumber: () => void; } const TimeframePopup = (props: TimeframeProps): React.ReactElement => { - const { changeTimeframe } = props; + const { changeTimeframe, resetExperimentTimeframe, resetShotnumber } = props; const [workingTimeframe, setWorkingTimeframe] = React.useState(0); @@ -45,7 +47,11 @@ const TimeframePopup = (props: TimeframeProps): React.ReactElement => { size="small" variant="outlined" sx={{ height: '100%' }} - onClick={() => changeTimeframe({ value: 10, timescale: 'minutes' })} + onClick={() => { + resetExperimentTimeframe(); + resetShotnumber(); + changeTimeframe({ value: 10, timescale: 'minutes' }); + }} > Last 10 mins @@ -55,7 +61,11 @@ const TimeframePopup = (props: TimeframeProps): React.ReactElement => { size="small" variant="outlined" sx={{ height: '100%' }} - onClick={() => changeTimeframe({ value: 24, timescale: 'hours' })} + onClick={() => { + resetExperimentTimeframe(); + resetShotnumber(); + changeTimeframe({ value: 24, timescale: 'hours' }); + }} > Last 24 hours @@ -65,7 +75,11 @@ const TimeframePopup = (props: TimeframeProps): React.ReactElement => { size="small" variant="outlined" sx={{ height: '100%' }} - onClick={() => changeTimeframe({ value: 7, timescale: 'days' })} + onClick={() => { + resetExperimentTimeframe(); + resetShotnumber(); + changeTimeframe({ value: 7, timescale: 'days' }); + }} > Last 7 days @@ -92,11 +106,14 @@ const TimeframePopup = (props: TimeframeProps): React.ReactElement => { variant="outlined" sx={{ height: '100%' }} onClick={() => { - if (workingTimeframe > 0) + if (workingTimeframe > 0) { + resetExperimentTimeframe(); + resetShotnumber(); changeTimeframe({ value: workingTimeframe, timescale: 'minutes', }); + } }} > Mins @@ -108,11 +125,14 @@ const TimeframePopup = (props: TimeframeProps): React.ReactElement => { variant="outlined" sx={{ height: '100%' }} onClick={() => { - if (workingTimeframe > 0) + if (workingTimeframe > 0) { + resetExperimentTimeframe(); + resetShotnumber(); changeTimeframe({ value: workingTimeframe, timescale: 'hours', }); + } }} > Hours @@ -124,11 +144,14 @@ const TimeframePopup = (props: TimeframeProps): React.ReactElement => { variant="outlined" sx={{ height: '100%' }} onClick={() => { - if (workingTimeframe > 0) + if (workingTimeframe > 0) { + resetExperimentTimeframe(); + resetShotnumber(); changeTimeframe({ value: workingTimeframe, timescale: 'days', }); + } }} > Days @@ -177,6 +200,7 @@ const Timeframe = (props: TimeframeProps): React.ReactElement => { display: 'flex', flexDirection: 'row', paddingRight: 5, + paddingBottom: '4px', cursor: 'pointer', overflow: 'hidden', ...(flashAnimationPlaying && { diff --git a/src/search/searchBar.component.test.tsx b/src/search/searchBar.component.test.tsx index df2e8d82e..b140ab288 100644 --- a/src/search/searchBar.component.test.tsx +++ b/src/search/searchBar.component.test.tsx @@ -44,17 +44,65 @@ describe('searchBar component', () => { jest.clearAllMocks(); }); - it('dispatches changeSearchParams on search button click', async () => { + it('dispatches changeSearchParams on search button click for a given date range', async () => { const state = getInitialState(); const { store } = createView(state); + // experiment field + + await user.click(screen.getByLabelText('open experiment search box')); + const experimentPopup = screen.getByLabelText('Select your experiment'); + + await user.type(experimentPopup, '221{arrowdown}{enter}'); + expect(experimentPopup).toHaveValue('22110007'); + // Date-time fields const dateFilterFromDate = screen.getByLabelText('from, date-time input'); const dateFilterToDate = screen.getByLabelText('to, date-time input'); + await user.clear(dateFilterFromDate); + await user.clear(dateFilterToDate); + await user.type(dateFilterFromDate, '2022-01-01 00:00'); await user.type(dateFilterToDate, '2022-01-02 00:00'); + // Max shots + + const maxShotsRadioGroup = screen.getByRole('radiogroup', { + name: 'select max shots', + }); + await user.click( + within(maxShotsRadioGroup).getByLabelText('Select 1000 max shots') + ); + + // Initiate search + + await user.click(screen.getByRole('button', { name: 'Search' })); + expect(store.getState().search.searchParams).toStrictEqual({ + dateRange: { + fromDate: '2022-01-01T00:00:00', + toDate: '2022-01-02T00:00:59', + }, + shotnumRange: { + min: 1, + max: 2, + }, + maxShots: 1000, + experimentID: null, + }); + }); + + it('dispatches changeSearchParams on search button click for a given shot number range', async () => { + const state = getInitialState(); + const { store } = createView(state); + + // experiment field + + await user.click(screen.getByLabelText('open experiment search box')); + const experimentPopup = screen.getByLabelText('Select your experiment'); + + await user.type(experimentPopup, '221{arrowdown}{enter}'); + expect(experimentPopup).toHaveValue('22110007'); // Shot number fields await user.click(screen.getByLabelText('open shot number search box')); @@ -65,9 +113,60 @@ describe('searchBar component', () => { const shotnumMax = within(shotnumPopup).getByRole('spinbutton', { name: 'Max', }); + await user.clear(shotnumMin); + await user.clear(shotnumMax); + await user.type(shotnumMin, '5'); + await user.type(shotnumMax, '10'); + await user.click(screen.getByLabelText('close shot number search box')); + + // Max shots + + const maxShotsRadioGroup = screen.getByRole('radiogroup', { + name: 'select max shots', + }); + await user.click( + within(maxShotsRadioGroup).getByLabelText('Select 1000 max shots') + ); + + // Initiate search + + await user.click(screen.getByRole('button', { name: 'Search' })); + expect(store.getState().search.searchParams).toStrictEqual({ + dateRange: { + fromDate: '2022-01-05T00:00:00', + toDate: '2022-01-10T00:00:59', + }, + shotnumRange: { + min: 5, + max: 10, + }, + maxShots: 1000, + experimentID: null, + }); + }); + + it('clears experiment field when shot number min is not within the experiment timeframe', async () => { + const state = getInitialState(); + const { store } = createView(state); + + await user.click(screen.getByLabelText('open experiment search box')); + const experimentPopup = screen.getByLabelText('Select your experiment'); + + await user.type(experimentPopup, '221{arrowdown}{enter}'); + expect(experimentPopup).toHaveValue('22110007'); + + // Shot number fields + + await user.click(screen.getByLabelText('open shot number search box')); + const shotnumPopup = screen.getByRole('dialog'); + const shotnumMin = within(shotnumPopup).getByRole('spinbutton', { + name: 'Min', + }); + + await user.clear(shotnumMin); + + await user.type(shotnumMin, '12'); - await user.type(shotnumMin, '1'); - await user.type(shotnumMax, '2'); await user.click(screen.getByLabelText('close shot number search box')); // Max shots @@ -84,33 +183,100 @@ describe('searchBar component', () => { await user.click(screen.getByRole('button', { name: 'Search' })); expect(store.getState().search.searchParams).toStrictEqual({ dateRange: { - fromDate: '2022-01-01T00:00:00', - toDate: '2022-01-02T00:00:59', + fromDate: '2022-01-12T00:00:00', + toDate: '2022-01-15T00:00:59', }, shotnumRange: { - min: 1, - max: 2, + min: 12, + max: 15, + }, + maxShots: 1000, + experimentID: null, + }); + }); + + it('clears experiment field when shot number max is not within the experiment timeframe', async () => { + const state = getInitialState(); + const { store } = createView(state); + + await user.click(screen.getByLabelText('open experiment search box')); + const experimentPopup = screen.getByLabelText('Select your experiment'); + + await user.type(experimentPopup, '221{arrowdown}{enter}'); + expect(experimentPopup).toHaveValue('22110007'); + + // Shot number fields + + await user.click(screen.getByLabelText('open shot number search box')); + const shotnumPopup = screen.getByRole('dialog'); + const shotnumMax = within(shotnumPopup).getByRole('spinbutton', { + name: 'Max', + }); + + await user.clear(shotnumMax); + + await user.type(shotnumMax, '16'); + + await user.click(screen.getByLabelText('close shot number search box')); + + // Max shots + + const maxShotsRadioGroup = screen.getByRole('radiogroup', { + name: 'select max shots', + }); + await user.click( + within(maxShotsRadioGroup).getByLabelText('Select 1000 max shots') + ); + + // Initiate search + + await user.click(screen.getByRole('button', { name: 'Search' })); + expect(store.getState().search.searchParams).toStrictEqual({ + dateRange: { + fromDate: '2022-01-13T00:00:00', + toDate: '2022-01-16T00:00:59', + }, + shotnumRange: { + min: 13, + max: 16, }, maxShots: 1000, experimentID: null, }); }); - it('selects an experiment and displays it in the experiment box', async () => { - createView(); + it('dispatches searchParams on search button click for a given experiment', async () => { + const state = getInitialState(); + const { store } = createView(state); + const expectedExperiment = { - _id: '18325019-4', - end_date: '2020-01-06T18:00:00', - experiment_id: '18325019', - part: 4, - start_date: '2020-01-03T10:00:00', + _id: '22110007-1', + end_date: '2022-01-15T12:00:00', + experiment_id: '22110007', + part: 1, + start_date: '2022-01-12T13:00:00', }; + const expectedEndDate = '2022-01-15T12:00:59'; + await user.click(screen.getByLabelText('open experiment search box')); const experimentPopup = screen.getByLabelText('Select your experiment'); - await user.type(experimentPopup, '183{arrowdown}{enter}'); - expect(experimentPopup).toHaveValue(expectedExperiment.experiment_id); + await user.type(experimentPopup, '221{arrowdown}{enter}'); + + await user.click(screen.getByRole('button', { name: 'Search' })); + expect(store.getState().search.searchParams).toStrictEqual({ + dateRange: { + fromDate: expectedExperiment.start_date, + toDate: expectedEndDate, + }, + shotnumRange: { + min: 13, + max: 15, + }, + maxShots: 50, + experimentID: expectedExperiment, + }); }); it('changes to and from dateTimes to use 0 seconds and 59 seconds respectively', async () => { @@ -133,8 +299,8 @@ describe('searchBar component', () => { toDate: '2022-01-02T00:00:59', }, shotnumRange: { - min: undefined, - max: undefined, + min: 1, + max: 2, }, maxShots: MAX_SHOTS_VALUES[0], experimentID: null, @@ -459,6 +625,56 @@ describe('searchBar component', () => { expect(actualToDate).toEqual(formatDateTimeForApi(expectedToDate)); }); + it('clears timeframe range when shot numbers are manually selected', async () => { + const state = getInitialState(); + const { store } = createView(state); + + await user.click(screen.getByLabelText('open timeframe search box')); + const timeframePopup = screen.getByRole('dialog'); + await user.click( + within(timeframePopup).getByRole('button', { name: 'Last 7 days' }) + ); + + // Shot number fields + + await user.click(screen.getByLabelText('open shot number search box')); + const shotnumPopup = screen.getByRole('dialog'); + const shotnumMax = within(shotnumPopup).getByRole('spinbutton', { + name: 'Max', + }); + + await user.clear(shotnumMax); + + await user.type(shotnumMax, '16'); + + await user.click(screen.getByLabelText('close shot number search box')); + + // Max shots + + const maxShotsRadioGroup = screen.getByRole('radiogroup', { + name: 'select max shots', + }); + await user.click( + within(maxShotsRadioGroup).getByLabelText('Select 1000 max shots') + ); + + // Initiate search + + await user.click(screen.getByRole('button', { name: 'Search' })); + expect(store.getState().search.searchParams).toStrictEqual({ + dateRange: { + fromDate: '2022-01-05T00:00:00', + toDate: '2022-01-16T00:00:59', + }, + shotnumRange: { + min: 5, + max: 16, + }, + maxShots: 1000, + experimentID: null, + }); + }); + it('refreshes datetime stamps and launches search if timeframe is set and refresh button clicked', async () => { const state = getInitialState(); const { store } = createView(state); diff --git a/src/search/searchBar.component.tsx b/src/search/searchBar.component.tsx index 3fda54e79..dcb11d217 100644 --- a/src/search/searchBar.component.tsx +++ b/src/search/searchBar.component.tsx @@ -31,7 +31,11 @@ import { formatDateTimeForApi, } from '../state/slices/searchSlice'; import { selectRecordLimitWarning } from '../state/slices/configSlice'; -import { useIncomingRecordCount } from '../api/records'; +import { + useDateToShotnumConverter, + useIncomingRecordCount, + useShotnumToDateConverter, +} from '../api/records'; import { useQueryClient } from '@tanstack/react-query'; import { selectQueryFilters } from '../state/slices/filterSlice'; import { useExperiment } from '../api/experiment'; @@ -84,6 +88,14 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { searchParameterToDate.setSeconds(59); } + const setDateRange = React.useCallback( + (fromDate: Date | null, toDate: Date | null) => { + setSearchParameterFromDate(fromDate); + setSearchParameterToDate(toDate); + }, + [] + ); + // ######################## // TIMEFRAME // ######################## @@ -129,6 +141,14 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { const [searchParameterShotnumMax, setSearchParameterShotnumMax] = React.useState(shotnumRange.max ?? undefined); + const setShotnumberRange = React.useCallback( + (shotnumMin: number | undefined, shotnumMax: number | undefined) => { + setSearchParameterShotnumMin(shotnumMin); + setSearchParameterShotnumMax(shotnumMax); + }, + [] + ); + const [maxShots, setMaxShots] = React.useState(maxShotsParam); @@ -140,6 +160,108 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { const [searchParameterExperiment, setSearchParameterExperiment] = React.useState(experimentID); + const calculateExperimentDateRange = ( + experiment: ExperimentParams + ): { from: Date; to: Date } => { + const to = new Date(experiment.end_date); + const from = new Date(experiment.start_date); + + return { from, to }; + }; + + const setExperimentTimeframe = React.useCallback( + (experiment: ExperimentParams | null) => { + if (experiment == null) { + setSearchParameterExperiment(null); + return; + } + const { from, to } = calculateExperimentDateRange(experiment); + setSearchParameterExperiment(experiment); + setSearchParameterFromDate(from); + setSearchParameterToDate(to); + }, + [] + ); + + const isDateTimeInExperiment = ( + dateTime: Date, + experiment: ExperimentParams + ): boolean => { + const startDate = new Date(experiment.start_date); + const endDate = new Date(experiment.end_date); + return dateTime >= startDate && dateTime <= endDate; + }; + // Date range to shot number range converter + const { data: dateToShotnum } = useDateToShotnumConverter( + searchParameterFromDate + ? formatDateTimeForApi(searchParameterFromDate) + : undefined, + searchParameterToDate + ? formatDateTimeForApi(searchParameterToDate) + : undefined + ); + + // Shot number range to date range converter + const { data: shotnumToDate } = useShotnumToDateConverter( + searchParameterShotnumMin, + searchParameterShotnumMax + ); + + // Checks for changes to shot number range and date range + // This is for the animation in date time box and shotnum box + + const [isShotnumToDate, setIsShotnumToDate] = React.useState(false); + const [isDateToShotnum, setIsDateToShotnum] = React.useState(false); + + // handles the date range to shot number conversion + React.useEffect(() => { + setIsShotnumToDate(!dateToShotnum && !!shotnumToDate); + setIsDateToShotnum(!!dateToShotnum && !shotnumToDate); + // Sets the date range when the shot number range is selected. + // Additionally if the new shot number range is not within + // the current experiment id time frame it clears the experiment id + // and if a time frame range exist it clears the time frame range + if (!dateToShotnum && !!shotnumToDate) { + if (shotnumToDate.from && shotnumToDate.to) { + const shotnumToDateFromDate = new Date(shotnumToDate.from); + const shotnumToDateToDate = new Date(shotnumToDate.to); + setSearchParameterFromDate(shotnumToDateFromDate); + setSearchParameterToDate(shotnumToDateToDate); + if (timeframeRange) { + setTimeframeRange(null); + } + if (searchParameterExperiment) { + if ( + !( + isDateTimeInExperiment( + shotnumToDateFromDate, + searchParameterExperiment + ) && + isDateTimeInExperiment( + shotnumToDateToDate, + searchParameterExperiment + ) + ) + ) { + setExperimentTimeframe(null); + } + } + } + // Sets the shot number range when the date Range is selected. + // the logic for the timeframes and experiment timeframe is done + // in the dateTime component + } else if (!!dateToShotnum && !shotnumToDate) { + setSearchParameterShotnumMin(dateToShotnum.min); + setSearchParameterShotnumMax(dateToShotnum.max); + } + }, [ + dateToShotnum, + searchParameterExperiment, + setExperimentTimeframe, + shotnumToDate, + timeframeRange, + ]); + React.useEffect(() => { setParamsUpdated(true); // reset warning message when search params change @@ -278,6 +400,7 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { const [refreshingData, setRefreshingData] = React.useState(false); const refreshData = () => { + setExperimentTimeframe(searchParameterExperiment); setRelativeTimeframe(timeframeRange); setRefreshingData(true); }; @@ -305,12 +428,24 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { changeSearchParameterToDate={setSearchParameterToDate} resetTimeframe={() => setRelativeTimeframe(null)} timeframeRange={timeframeRange} + resetExperimentTimeframe={() => setExperimentTimeframe(null)} + searchParameterExperiment={searchParameterExperiment} + experiments={experiments ?? []} + resetShotnumberRange={() => + setShotnumberRange(undefined, undefined) + } + isShotnumToDate={isShotnumToDate} + isDateTimeInExperiment={isDateTimeInExperiment} /> setExperimentTimeframe(null)} + resetShotnumber={() => + setShotnumberRange(undefined, undefined) + } /> @@ -318,6 +453,11 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { experiments={experiments ?? []} onExperimentChange={setSearchParameterExperiment} experiment={searchParameterExperiment} + resetTimeframe={() => setRelativeTimeframe(null)} + changeExperimentTimeframe={setExperimentTimeframe} + resetShotnumber={() => + setShotnumberRange(undefined, undefined) + } /> @@ -326,6 +466,9 @@ const SearchBar = (props: SearchBarProps): React.ReactElement => { searchParameterShotnumMax={searchParameterShotnumMax} changeSearchParameterShotnumMin={setSearchParameterShotnumMin} changeSearchParameterShotnumMax={setSearchParameterShotnumMax} + resetDateRange={() => setDateRange(null, null)} + resetExperimentTimeframe={() => setExperimentTimeframe(null)} + isDateToShotnum={isDateToShotnum} />