diff --git a/.env.production b/.env.production index ea67cf152..5208c5697 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,3 @@ -REACT_APP_OPERATIONSGATEWAY_BUILD_DIRECTORY=/ -REACT_APP_E2E_TESTING=false \ No newline at end of file +VITE_APP_OPERATIONS_GATEWAY_BUILD_DIRECTORY=/ +VITE_APP_INCLUDE_MSW=false +VITE_APP_BUILD_STANDALONE=false \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a53f99e90..45ca724da 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: yarn --immutable - name: Run linting - run: yarn lint:js + run: yarn lint - name: Run unit tests run: yarn test - name: Upload unit test coverage @@ -117,7 +117,7 @@ jobs: with: mongodb-version: '5.0' - # Configure correct paths in config files + # Configure correct paths in config files - name: Configure private key path run: yq -i ".auth.private_key_path = \"$GITHUB_WORKSPACE/id_rsa\"" .github/ci_config.yml working-directory: ./operationsgateway-api @@ -225,4 +225,4 @@ jobs: with: name: playwright-report-real-tests path: playwright-report/ - retention-days: 10 \ No newline at end of file + retention-days: 10 diff --git a/.gitignore b/.gitignore index 818d16938..e713687ba 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ screenshots/ **/cypress/fixtures/example.json # production -/build +dist # misc .DS_Store diff --git a/README.md b/README.md index 4075f96fa..01cbc82b7 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,48 @@ # operationsgateway + OperationsGateway - user interface for operational data -# Getting Started with Create React App +## Getting Started with Vite -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +This project uses [Vite](https://vitejs.dev/). -## Available Scripts +### Available Scripts In the project directory, you can run: -### `yarn start` +### `yarn install` -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +This will install all the project dependencies. Running `yarn install` at the top +level initialises all the packages, and you will be ready to start development in any of them! -The page will reload if you make edits.\ -You will also see any lint errors in the console. +### `yarn dev` -You will need to create a settings file - you can use `./public/operationsgateway-settings.example.json` -as a base. Setting `apiUrl` to `""` enables request mocking via `msw`. +Runs the `dev` script, which runs the app in development mode. +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. ### `yarn test` -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `yarn build` +Runs unit tests for all packages -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +### `yarn e2e` -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +Runs e2e tests for all packages -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +### `yarn lint` -### `yarn eject` +Lints all packages -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** +### `yarn build` -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +Builds the app for production to the `dist` folder.\ -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +The build is minified and the filenames include the hashes. +Your app is ready to be deployed! -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +See the section about [building for production](https://vitejs.dev/guide/build.html) for more information. -## Learn More +### `yarn preview` -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +Deploys a static version of the build from the `dist` directory to port 5001. Use `yarn preview:build` to build and preview it in SciGateway. -To learn React, check out the [React documentation](https://reactjs.org/). +For development purposes, use `yarn preview:build:dev` to build in watch mode so that changes are built automatically. diff --git a/craco.config.js b/craco.config.js deleted file mode 100644 index 1fff57582..000000000 --- a/craco.config.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = { - webpack: { - configure: (webpackConfig, { env, paths }) => { - webpackConfig.externals = { - react: 'React', // Case matters here - 'react-dom': 'ReactDOM', // Case matters here - }; - - if ( - env === 'production' && - process.env.REACT_APP_E2E_TESTING !== 'true' - ) { - webpackConfig.output.library = 'operationsgateway'; - webpackConfig.output.libraryTarget = 'window'; - - webpackConfig.output.filename = '[name].js'; - webpackConfig.output.chunkFilename = '[name].chunk.js'; - - delete webpackConfig.optimization.splitChunks; - webpackConfig.optimization.runtimeChunk = false; - - webpackConfig.output.clean = { - keep(asset) { - // exclude mockServiceWorker.js from build - return !asset.includes('mockServiceWorker.js'); - }, - }; - } - - return webpackConfig; - }, - }, -}; diff --git a/cypress.config.js b/cypress.config.ts similarity index 76% rename from cypress.config.js rename to cypress.config.ts index ec61e5b08..2351ec06a 100644 --- a/cypress.config.js +++ b/cypress.config.ts @@ -1,8 +1,7 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const { defineConfig } = require('cypress'); -const { removeDirectory } = require('cypress-delete-downloads-folder'); +import { defineConfig } from 'cypress'; +import { removeDirectory } from 'cypress-delete-downloads-folder'; -module.exports = defineConfig({ +export default defineConfig({ chromeWebSecurity: false, video: false, retries: { @@ -12,9 +11,6 @@ module.exports = defineConfig({ e2e: { setupNodeEvents(on) { on('task', { removeDirectory }); - // https://github.com/bahmutov/cypress-failed-log - require('cypress-failed-log/on')(on); - on('before:browser:launch', (browser, launchOptions) => { if (browser.family === 'chromium' && browser.name !== 'electron') { // Set pointer type to fine so that date inputs work properly diff --git a/cypress/e2e/search.cy.ts b/cypress/e2e/search.cy.ts index 0be98c577..1f80a3e0c 100644 --- a/cypress/e2e/search.cy.ts +++ b/cypress/e2e/search.cy.ts @@ -1,3 +1,4 @@ +import { HttpResponse } from 'msw'; import { formatDateTimeForApi } from '../support/util'; function getParamsFromUrl(url: string) { @@ -1191,12 +1192,14 @@ describe('Search', () => { // check that if a search drops below the limit the tooltip no longer displays cy.window().then((window) => { // Reference global instances set in "src/mocks/browser.js". - const { worker, rest } = window.msw; + const { worker, http } = window.msw; worker.use( - rest.get('/records/count', (req, res, ctx) => { - return res.once(ctx.status(200), ctx.json(8)); //arbitrary number less than 10 - }) + http.get( + '/records/count', + () => HttpResponse.json(8, { status: 200 }), //arbitrary number less than 10 + { once: true } + ) ); }); diff --git a/cypress/e2e/sessions.cy.ts b/cypress/e2e/sessions.cy.ts index d8bbcdd20..e9c2e8ccc 100644 --- a/cypress/e2e/sessions.cy.ts +++ b/cypress/e2e/sessions.cy.ts @@ -1,3 +1,5 @@ +import { HttpResponse } from 'msw'; + function getParamsFromUrl(url: string) { const paramsString = url.split('?')[1]; const paramMap = new Map(); @@ -27,13 +29,13 @@ describe('Sessions', () => { cy.findByTestId('AddCircleIcon').should('exist'); cy.window().then((window) => { // Reference global instances set in "src/mocks/browser.js". - const { worker, rest } = window.msw; + const { worker, http } = window.msw; worker.use( - rest.post('/sessions', async (req, res, ctx) => { + http.post('/sessions', async () => { // return a session without popups const sessionID = '2'; - return res(ctx.status(200), ctx.json(sessionID)); + return HttpResponse.json(sessionID, { status: 200 }); }) ); }); @@ -47,10 +49,10 @@ describe('Sessions', () => { cy.findByRole('button', { name: 'Save' }).click(); cy.findBrowserMockedRequests({ method: 'POST', url: '/sessions' }).should( - (patchRequests) => { + async (patchRequests) => { expect(patchRequests.length).equal(1); const request = patchRequests[0]; - expect(JSON.stringify(request.body)).equal( + expect(JSON.stringify(await request.json())).equal( '{"table":{"columnStates":{},"selectedColumnIds":["timestamp"],"page":0,"resultsPerPage":25,"sort":{}},"search":{"searchParams":{"dateRange":{"toDate":"2024-08-02T14:00:59","fromDate":"2024-08-01T14:00:00"},"shotnumRange":{},"maxShots":50,"experimentID":null}},"plots":{},"filter":{"appliedFilters":[[]]},"windows":{},"selection":{"selectedRows":[]}}' ); @@ -198,10 +200,10 @@ describe('Sessions', () => { cy.findBrowserMockedRequests({ method: 'PATCH', url: '/sessions/:id', - }).should((patchRequests) => { + }).should(async (patchRequests) => { expect(patchRequests.length).equal(1); const request = patchRequests[0]; - expect(JSON.stringify(request.body)).equal( + expect(JSON.stringify(await request.json())).equal( '{"table":{"columnStates":{},"selectedColumnIds":["timestamp","CHANNEL_EFGHI","CHANNEL_FGHIJ","shotnum"],"page":0,"resultsPerPage":25,"sort":{}},"search":{"searchParams":{"dateRange":{"fromDate":"2022-01-06T13:00:00","toDate":"2022-01-09T12:00:59"},"shotnumRange":{"min":7,"max":9},"maxShots":50,"experimentID":{"_id":"19210012-1","end_date":"2022-01-09T12:00:00","experiment_id":"19210012","part":1,"start_date":"2022-01-06T13:00:00"}}},"plots":{},"filter":{"appliedFilters":[[{"type":"channel","value":"shotnum","label":"Shot Number"},{"type":"compop","value":">","label":">"},{"type":"number","value":"7","label":"7"}]]},"windows":{},"selection":{"selectedRows":[]}}' ); expect(request.url.toString()).to.contain('2'); diff --git a/cypress/e2e/table.cy.ts b/cypress/e2e/table.cy.ts index 146f35bbe..0a46d82c9 100644 --- a/cypress/e2e/table.cy.ts +++ b/cypress/e2e/table.cy.ts @@ -1,4 +1,5 @@ -import { getHandleSelector, addInitialSystemChannels } from '../support/util'; +import { HttpResponse } from 'msw'; +import { addInitialSystemChannels, getHandleSelector } from '../support/util'; const verifyColumnOrder = (columns: string[]): void => { // check if the first column contains the checkbox @@ -262,12 +263,12 @@ describe('Table Component', () => { cy.window().then((window) => { // Reference global instances set in "src/mocks/browser.js". - const { worker, rest } = window.msw; + const { worker, http } = window.msw; worker.use( - rest.get('/records/count', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(50)); - }) + http.get('/records/count', () => + HttpResponse.json(50, { status: 200 }) + ) ); }); @@ -279,12 +280,12 @@ describe('Table Component', () => { cy.window().then(async (window) => { // Reference global instances set in "src/mocks/browser.js". - const { worker, rest } = window.msw; + const { worker, http } = window.msw; worker.use( - rest.get('/records/count', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(1000)); - }) + http.get('/records/count', () => + HttpResponse.json(1000, { status: 200 }) + ) ); }); @@ -298,12 +299,12 @@ describe('Table Component', () => { cy.window().then((window) => { // Reference global instances set in "src/mocks/browser.js". - const { worker, rest } = window.msw; + const { worker, http } = window.msw; worker.use( - rest.get('/records/count', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(2500)); //arbirary number greater than 1000 - }) + http.get('/records/count', () => + HttpResponse.json(2500, { status: 200 }) + ) ); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 6397b0ff2..555076da3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -91,8 +91,13 @@ Cypress.Commands.add('startSnoopingBrowserMockedRequest', () => { cy.window().then((window) => { const worker = window?.msw?.worker; - worker.events.on('request:match', (req) => { - mockedRequests.push(req); + // Use start here instead of match as needs to be done before the request is read to + // avoid errors as an MDN Request's contents can only be read once. We then clone it + // here to ensure the MSW handlers can call .json() on it, and also any Cypress tests + // which would otherwise have failed for the same reason as json() can only be called + // once on the original request. + worker.events.on('request:start', ({ request }) => { + mockedRequests.push(request.clone()); }); }); }); @@ -103,27 +108,30 @@ Cypress.Commands.add('startSnoopingBrowserMockedRequest', () => { */ Cypress.Commands.add('findBrowserMockedRequests', ({ method, url }) => { return cy.window().then((window) => { - const { matchRequestUrl } = window?.msw; + const msw = window?.msw; + if (msw) { + const { matchRequestUrl } = msw; - return new Cypress.Promise((resolve, reject) => { - if ( - !method || - !url || - typeof method !== 'string' || - typeof url !== 'string' - ) { - return reject( - `Invalid parameters passed. Method: ${method} Url: ${url}` + return new Cypress.Promise((resolve, reject) => { + if ( + !method || + !url || + typeof method !== 'string' || + typeof url !== 'string' + ) { + return reject( + `Invalid parameters passed. Method: ${method} Url: ${url}` + ); + } + resolve( + mockedRequests.filter((req) => { + const matchesMethod = + req.method && req.method.toLowerCase() === method.toLowerCase(); + const matchesUrl = matchRequestUrl(new URL(req.url), url).matches; + return matchesMethod && matchesUrl; + }) ); - } - resolve( - mockedRequests.filter((req) => { - const matchesMethod = - req.method && req.method.toLowerCase() === method.toLowerCase(); - const matchesUrl = matchRequestUrl(req.url, url).matches; - return matchesMethod && matchesUrl; - }) - ); - }); + }); + } }); }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 96dcc102e..642fe2949 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -20,5 +20,3 @@ import './util'; // Alternatively you can use CommonJS syntax: // require('./commands') - -require('cypress-failed-log'); diff --git a/e2e/mocked/images.spec.ts b/e2e/mocked/images.spec.ts index 91af3dbb6..5180d18db 100644 --- a/e2e/mocked/images.spec.ts +++ b/e2e/mocked/images.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -321,56 +321,58 @@ test('user can change image via clicking on a thumbnail', async ({ page }) => { const url = window.URL.createObjectURL(responseBlob); msw.worker.use( - msw.rest.get('/images/:recordId/:channelName', async (req, res, ctx) => { - const canvas = window.document.createElement('canvas'); - const context = canvas.getContext('2d'); - - const result = await new Promise((resolve, reject) => { - const img = new Image(); - img.onload = function () { - canvas.width = img.width; - canvas.height = img.height; - - if (context) { - // draw image - context.drawImage(img, 0, 0, canvas.width, canvas.height); - - // set composite mode - context.globalCompositeOperation = 'color'; - - // draw color - context.fillStyle = '#f00'; - context.fillRect(0, 0, canvas.width, canvas.height); - - canvas.toBlob(async (blob) => { - if (blob) { - const arrayBuffer = await blob.arrayBuffer(); - - resolve( - res.once( - ctx.status(200), - ctx.set( - 'Content-Length', - arrayBuffer.byteLength.toString() - ), - ctx.set('Content-Type', 'image/png'), - ctx.body(arrayBuffer) - ) - ); - } else { - reject(); - } - }); - } else { - reject(); - } - }; - img.onerror = reject; - img.src = url; - }); - - return result; - }) + msw.http.get( + '/images/:recordId/:channelName', + async () => { + const canvas = window.document.createElement('canvas'); + const context = canvas.getContext('2d'); + + const result = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = function () { + canvas.width = img.width; + canvas.height = img.height; + + if (context) { + // draw image + context.drawImage(img, 0, 0, canvas.width, canvas.height); + + // set composite mode + context.globalCompositeOperation = 'color'; + + // draw color + context.fillStyle = '#f00'; + context.fillRect(0, 0, canvas.width, canvas.height); + + canvas.toBlob(async (blob) => { + if (blob) { + const arrayBuffer = await blob.arrayBuffer(); + + resolve( + new msw.HttpResponse(arrayBuffer, { + headers: { + 'Content-Length': arrayBuffer.byteLength.toString(), + 'Content-Type': 'image/png', + }, + status: 200, + }) + ); + } else { + reject(); + } + }); + } else { + reject(); + } + }; + img.onerror = reject; + img.src = url; + }); + + return result; + }, + { once: true } + ) ); }); diff --git a/e2e/mocked/plotting.spec.ts b/e2e/mocked/plotting.spec.ts index a971d87de..4ac4312b7 100644 --- a/e2e/mocked/plotting.spec.ts +++ b/e2e/mocked/plotting.spec.ts @@ -554,6 +554,9 @@ test('user can change the line width of plotted channels', async ({ page }) => { test('user can plot channels on the right y axis', async ({ page }) => { await page.goto('/'); + // MSW wont start immediately here, so wait for page to load first + await expect(page.locator('text=Plots')).toBeVisible(); + await page.evaluate(async () => { const { msw } = window; @@ -570,9 +573,9 @@ test('user can plot channels on the right y axis', async ({ page }) => { }); msw.worker.use( - msw.rest.get('/records', async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(modifiedRecordsJson)); - }) + msw.http.get('/records', async () => + msw.HttpResponse.json(modifiedRecordsJson, { status: 200 }) + ) ); }); diff --git a/e2e/mocked/traces.spec.ts b/e2e/mocked/traces.spec.ts index 4b1a2e1be..402e85f93 100644 --- a/e2e/mocked/traces.spec.ts +++ b/e2e/mocked/traces.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -136,18 +136,18 @@ test('user can change trace via clicking on a thumbnail', async ({ page }) => { const { msw } = window; msw.worker.use( - msw.rest.get( + msw.http.get( '/waveforms/:recordId/:channelName', - async (req, res, ctx) => { - return res.once( - ctx.status(200), - ctx.json({ + async () => + msw.HttpResponse.json( + { _id: '2', x: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], y: [8, 1, 10, 9, 4, 3, 5, 6, 2, 7], - }) - ); - } + }, + { status: 200 } + ), + { once: true } ) ); }); diff --git a/e2e/real/search.spec.ts b/e2e/real/search.spec.ts index 030989e6d..93f70ae67 100644 --- a/e2e/real/search.spec.ts +++ b/e2e/real/search.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test('should be able to search via shot number', async ({ page }) => { await page.goto('/'); diff --git a/globalSetup.js b/globalSetup.js new file mode 100644 index 000000000..6e1fbf41d --- /dev/null +++ b/globalSetup.js @@ -0,0 +1,3 @@ +module.exports = async () => { + process.env.TZ = 'UTC'; +}; diff --git a/index.html b/index.html new file mode 100644 index 000000000..92d239b44 --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + OperationsGateway + + + + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 32f339453..e2a019798 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "operationsgateway", "version": "0.1.0", + "type": "module", "private": true, "dependencies": { "@date-io/date-fns": "3.0.0", @@ -17,22 +18,24 @@ "@tanstack/react-query": "5.51.1", "@tanstack/react-query-devtools": "5.51.1", "@tanstack/react-table": "8.19.3", - "@types/jest": "29.5.2", "@types/node": "20.14.10", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "@vitejs/plugin-react": "^4.3.1", "axios": "1.7.2", + "browserslist-to-esbuild": "^2.1.1", "cypress-delete-downloads-folder": "^0.0.5", "date-fns": "3.6.0", "hacktimer": "1.1.3", "loglevel": "1.9.1", + "msw": "2.3.5", "react": "18.3.1", "react-colorful": "5.6.1", "react-dom": "18.3.1", "react-redux": "9.1.2", - "react-scripts": "5.0.1", "single-spa-react": "5.1.4", "typescript": "5.5.3", + "vite": "^5.3.5", "web-vitals": "3.5.1" }, "resolutions": { @@ -42,18 +45,19 @@ "@mui/x-date-pickers@^6.19.0": "patch:@mui/x-date-pickers@npm%3A6.19.0#./.yarn/patches/@mui-x-date-pickers-npm-6.19.0-d448743c52.patch" }, "scripts": { - "lint:js": "eslint --max-warnings=0 --ext=tsx --ext=ts --ext=js --ext=jsx --fix ./src", - "start": "craco start", - "build": "craco build", - "serve:build": "yarn build && serve -l 5001 build", - "test": "craco test --env=jsdom --coverage --watchAll=false", - "test:watch": "craco test --env=jsdom --watch", + "dev": "vite --open", + "build": "tsc --project tsconfig.build.json && vite build", + "preview": "vite preview", + "preview:build": "yarn build && yarn preview", + "preview:build:dev": "yarn build --watch & yarn preview", + "test": "vitest --coverage", + "lint": "eslint --max-warnings=0 --fix ./src ./cypress && tsc --noEmit -p tsconfig.build.json", + "build:e2e": "cross-env VITE_APP_BUILD_STANDALONE=true VITE_APP_INCLUDE_MSW=true GENERATE_SOURCEMAP=false yarn build", + "build:e2e:api": "cross-env VITE_APP_BUILD_STANDALONE=true VITE_APP_INCLUDE_MSW=false GENERATE_SOURCEMAP=false yarn build", + "e2e:serve": "yarn build:e2e && node ./server/e2e-test-server.js", + "e2e:serve:api": "yarn build:e2e:api && node ./server/e2e-test-server.js", "playwright:test:mocked": "docker run -v $PWD:/test -w=/test $(node -e \"if(process.env.CI !== 'true'){if(process.platform === 'win32'){console.log('-it -u pwuser')}else{console.log('-it -u $(id -u):$(id -g)')}}else{console.log('-e CI')}\") --rm --ipc=host -p 9323:9323 --add-host host.docker.internal:host-gateway mcr.microsoft.com/playwright:v$(yarn info @playwright/test --name-only --json | sed -n 's/^.*:\\(.*\\)\"$/\\1/p') yarn playwright test", "playwright:test:real": "docker run -v $PWD:/test -w=/test -e \"USE_REAL_API=true\" $(node -e \"if(process.env.CI !== 'true'){if(process.platform === 'win32'){console.log('-it -u pwuser')}else{console.log('-it -u $(id -u):$(id -g)')}}else{console.log('-e CI')}\") --rm --ipc=host -p 9323:9323 --add-host host.docker.internal:host-gateway mcr.microsoft.com/playwright:v$(yarn info @playwright/test --name-only --json | sed -n 's/^.*:\\(.*\\)\"$/\\1/p') yarn playwright test", - "eject": "react-scripts eject", - "build:e2e": "cross-env REACT_APP_E2E_TESTING=true GENERATE_SOURCEMAP=false craco build", - "e2e-test-server": "node ./server/e2e-test-server.js", - "e2e:serve": "yarn build:e2e && yarn e2e-test-server", "e2e:interactive": "start-server-and-test e2e:serve http://localhost:3000 cy:open", "e2e": "start-server-and-test e2e:serve http://localhost:3000 cy:run", "cy:open": "cypress open", @@ -73,42 +77,26 @@ "prettier --write" ] }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "jest": { - "resetMocks": false, - "transformIgnorePatterns": [ - "node_modules/(?!copy-anything|axios|is-what)" - ] - }, + "browserslist": [ + "defaults" + ], "packageManager": "yarn@4.3.1", "devDependencies": { "@babel/eslint-parser": "7.24.8", - "@craco/craco": "7.1.0", "@playwright/test": "1.45.2", "@testing-library/cypress": "10.0.1", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "16.0.0", - "@testing-library/user-event": "14.5.1", + "@testing-library/user-event": "14.5.2", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "@typescript-eslint/typescript-estree": "7.17.0", + "@vitest/coverage-v8": "^2.0.5", "chart.js": "4.4.1", "chartjs-plugin-zoom": "2.0.0", "cross-env": "7.0.3", "cypress": "13.13.0", - "cypress-failed-log": "2.10.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-config-react-app": "7.0.1", @@ -117,14 +105,15 @@ "eslint-plugin-prettier": "5.2.1", "express": "4.19.2", "husky": "9.1.1", - "jest-canvas-mock": "2.5.0", - "jest-fail-on-console": "3.3.0", + "jsdom": "^24.1.1", "lint-staged": "15.2.0", - "msw": "1.3.2", "prettier": "3.3.3", "serve": "14.2.0", "serve-static": "1.15.0", - "start-server-and-test": "2.0.0" + "start-server-and-test": "2.0.0", + "vitest": "^2.0.5", + "vitest-canvas-mock": "^0.3.3", + "vitest-fail-on-console": "^0.7.0" }, "msw": { "workerDirectory": "public" diff --git a/playwright.config.ts b/playwright.config.ts index 517826696..dfbb565ca 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -159,7 +159,7 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { - command: 'yarn e2e:serve', + command: process.env.USE_REAL_API ? 'yarn e2e:serve:api' : 'yarn e2e:serve', url: 'http://localhost:3000', timeout: 180 * 1000, stdout: 'pipe', diff --git a/public/index.html b/public/index.html deleted file mode 100644 index ea19965f1..000000000 --- a/public/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - OperationsGateway - - - - - -
- - - diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 51d85eeeb..15751fa19 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -2,13 +2,15 @@ /* tslint:disable */ /** - * Mock Service Worker (1.3.2). + * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const PACKAGE_VERSION = '2.3.5' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() self.addEventListener('install', function () { @@ -47,7 +49,10 @@ self.addEventListener('message', async function (event) { case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', - payload: INTEGRITY_CHECKSUM, + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, }) break } @@ -86,12 +91,6 @@ self.addEventListener('message', async function (event) { self.addEventListener('fetch', function (event) { const { request } = event - const accept = request.headers.get('accept') || '' - - // Bypass server-sent events. - if (accept.includes('text/event-stream')) { - return - } // Bypass navigation requests. if (request.mode === 'navigate') { @@ -112,29 +111,8 @@ self.addEventListener('fetch', function (event) { } // Generate unique request ID. - const requestId = Math.random().toString(16).slice(2) - - event.respondWith( - handleRequest(event, requestId).catch((error) => { - if (error.name === 'NetworkError') { - console.warn( - '[MSW] Successfully emulated a network error for the "%s %s" request.', - request.method, - request.url, - ) - return - } - - // At this point, any exception indicates an issue with the original request/response. - console.error( - `\ -[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, - request.method, - request.url, - `${error.name}: ${error.message}`, - ) - }), - ) + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) }) async function handleRequest(event, requestId) { @@ -146,21 +124,24 @@ async function handleRequest(event, requestId) { // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { ;(async function () { - const clonedResponse = response.clone() - sendToClient(client, { - type: 'RESPONSE', - payload: { - requestId, - type: clonedResponse.type, - ok: clonedResponse.ok, - status: clonedResponse.status, - statusText: clonedResponse.statusText, - body: - clonedResponse.body === null ? null : await clonedResponse.text(), - headers: Object.fromEntries(clonedResponse.headers.entries()), - redirected: clonedResponse.redirected, + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, }, - }) + [responseClone.body], + ) })() } @@ -196,20 +177,20 @@ async function resolveMainClient(event) { async function getResponse(event, client, requestId) { const { request } = event - const clonedRequest = request.clone() + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() function passthrough() { - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const headers = Object.fromEntries(clonedRequest.headers.entries()) + const headers = Object.fromEntries(requestClone.headers.entries()) - // Remove MSW-specific request headers so the bypassed requests - // comply with the server's CORS preflight check. - // Operate with the headers as an object because request "Headers" - // are immutable. - delete headers['x-msw-bypass'] + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] - return fetch(clonedRequest, { headers }) + return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. @@ -225,57 +206,46 @@ async function getResponse(event, client, requestId) { return passthrough() } - // Bypass requests with the explicit bypass header. - // Such requests can be issued by "ctx.fetch()". - if (request.headers.get('x-msw-bypass') === 'true') { - return passthrough() - } - // Notify the client that a request has been intercepted. - const clientMessage = await sendToClient(client, { - type: 'REQUEST', - payload: { - id: requestId, - url: request.url, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - mode: request.mode, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: await request.text(), - bodyUsed: request.bodyUsed, - keepalive: request.keepalive, + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, }, - }) + [requestBuffer], + ) switch (clientMessage.type) { case 'MOCK_RESPONSE': { return respondWithMock(clientMessage.data) } - case 'MOCK_NOT_FOUND': { + case 'PASSTHROUGH': { return passthrough() } - - case 'NETWORK_ERROR': { - const { name, message } = clientMessage.data - const networkError = new Error(message) - networkError.name = name - - // Rejecting a "respondWith" promise emulates a network error. - throw networkError - } } return passthrough() } -function sendToClient(client, message) { +function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() @@ -287,17 +257,28 @@ function sendToClient(client, message) { resolve(event.data) } - client.postMessage(message, [channel.port2]) + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) }) } -function sleep(timeMs) { - return new Promise((resolve) => { - setTimeout(resolve, timeMs) +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, }) -} -async function respondWithMock(response) { - await sleep(response.delay) - return new Response(response.body, response) + return mockedResponse } diff --git a/src/operationsgateway-logo-white.svg b/public/operationsgateway-logo-white.svg similarity index 100% rename from src/operationsgateway-logo-white.svg rename to public/operationsgateway-logo-white.svg diff --git a/src/operationsgateway-logo.svg b/public/operationsgateway-logo.svg similarity index 100% rename from src/operationsgateway-logo.svg rename to public/operationsgateway-logo.svg diff --git a/server/e2e-test-server.js b/server/e2e-test-server.js index 870ac7691..ab0343a7c 100644 --- a/server/e2e-test-server.js +++ b/server/e2e-test-server.js @@ -1,6 +1,6 @@ -var express = require('express'); -var path = require('path'); -var serveStatic = require('serve-static'); +import express from 'express'; +import path from 'path'; +import serveStatic from 'serve-static'; var app = express(); @@ -23,11 +23,11 @@ app.get('/operationsgateway-settings.json', function (req, res) { app.use( express.json(), - serveStatic(path.resolve('./build'), { index: ['index.html', 'index.htm'] }) + serveStatic(path.resolve('./dist'), { index: ['index.html', 'index.htm'] }) ); app.get('/*', function (req, res) { - res.sendFile(path.resolve('./build/index.html')); + res.sendFile(path.resolve('./dist/index.html')); }); var server = app.listen(3000, '0.0.0.0', function () { diff --git a/src/App.test.tsx b/src/App.test.tsx index 0f50c91d1..86dbcbb21 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,10 +1,10 @@ -import React from 'react'; import { act } from '@testing-library/react'; -import App from './App'; +import React from 'react'; import { createRoot } from 'react-dom/client'; -import { flushPromises } from './setupTests'; +import App from './App'; +import { flushPromises } from './testUtils'; -jest.mock('loglevel'); +vi.mock('loglevel'); describe('App', () => { it('renders without crashing', async () => { diff --git a/src/App.tsx b/src/App.tsx index f415b4633..99c4deeee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,21 @@ -import React from 'react'; -import ViewTabs from './views/viewTabs.component'; import { + QueryCache, QueryClient, QueryClientProvider, - QueryCache, } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { configureApp } from './state/slices/configSlice'; -import { requestPluginRerender } from './state/scigateway.actions'; +import React from 'react'; +import { connect, Provider } from 'react-redux'; +import './App.css'; import { MicroFrontendId } from './app.types'; import OGThemeProvider from './ogThemeProvider.component'; -import OpenWindows from './windows/openWindows.component'; -import { store, RootState } from './state/store'; -import { connect, Provider } from 'react-redux'; import Preloader from './preloader/preloader.component'; -import './App.css'; import SettingsMenuItems from './settingsMenuItems.component'; +import { requestPluginRerender } from './state/scigateway.actions'; +import { configureApp } from './state/slices/configSlice'; +import { RootState, store } from './state/store'; +import ViewTabs from './views/viewTabs.component'; +import OpenWindows from './windows/openWindows.component'; import { WindowContextProvider } from './windows/windowContext'; const queryClient = new QueryClient({ diff --git a/src/__snapshots__/settingsMenuItems.component.test.tsx.snap b/src/__snapshots__/settingsMenuItems.component.test.tsx.snap index 9a4bcebed..88ad3cb95 100644 --- a/src/__snapshots__/settingsMenuItems.component.test.tsx.snap +++ b/src/__snapshots__/settingsMenuItems.component.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Settings Menu Items component renders dropdown only when menu is visible 1`] = ` +exports[`Settings Menu Items component > renders dropdown only when menu is visible 1`] = `
useRecordsPaginated > sends request to fetch records, returns successful response and uses a select function to format the results 1`] = ` +[ + { "CHANNEL_ABCDE": 1, "_id": "1", "activeArea": undefined, @@ -10,7 +10,7 @@ Array [ "shotnum": 1, "timestamp": "2022-01-01 00:00:00", }, - Object { + { "CHANNEL_ABCDE": 2, "_id": "2", "activeArea": undefined, @@ -18,7 +18,7 @@ Array [ "shotnum": 2, "timestamp": "2022-01-02 00:00:00", }, - Object { + { "CHANNEL_ABCDE": 3, "_id": "3", "activeArea": undefined, @@ -26,7 +26,7 @@ Array [ "shotnum": 3, "timestamp": "2022-01-03 00:00:00", }, - Object { + { "CHANNEL_BCDEF": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABkAGQMBIgACEQEDEQH/xAAbAAABBAMAAAAAAAAAAAAAAAAFAAYICQECB//EACYQAAICAgICAQQDAQAAAAAAAAECAwQFBhESABMHISIxQRQVFkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArB1DXTt+24bBLfpYpsnchpi9kpvTWr+xwvslf/lF55J/QB8fGX+GasFvP2MbscN3XtetNUy+SlgCPXYSMkZSNXYTLKVIjKN9SD39a/cR2iYbGZHDM9ajjc3szW2iGOy981Ilg6p0eIB4vbIzl16+wkAD7G7crYLgdP0nIfEuxZLNQXa23x2n9mLpasktC4/EX9lJJIa7FllYS9GEqnotbpzLyGCtXb8BFrWbNOvae7XetWtxTyQiJ2SaCOZeyBmCkCQAgMRyD9fA3kg/mqvrNbHbjHl9bxeK2d7dSXXr2Nyzu7UQAEhlpB5BFzXeJgz+vj1qqqeWIj54BrSs3DrO54HMWI3lr4/IV7ciRcdmWORWIHP054H78f2U+QdO1nHf5vUKuYyeq5anWfYTm/VXuWraDk+hoy6xRxP2aPkMSWIk7gADlR/J8x4B/dM1Qy9rHR47+TJWo0o6gs3EVJp+pYhmVWYDqGCAdj9sa/j8BvebeLwP/9k=", "_id": "4", "activeArea": undefined, @@ -34,7 +34,7 @@ Array [ "shotnum": 4, "timestamp": "2022-01-04 00:00:00", }, - Object { + { "CHANNEL_BCDEF": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABkAGQMBIgACEQEDEQH/xAAbAAABBAMAAAAAAAAAAAAAAAAFAAYICQECB//EACYQAAICAgICAQQDAQAAAAAAAAECAwQFBhESABMHISIxQRQVFkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArB1DXTt+24bBLfpYpsnchpi9kpvTWr+xwvslf/lF55J/QB8fGX+GasFvP2MbscN3XtetNUy+SlgCPXYSMkZSNXYTLKVIjKN9SD39a/cR2iYbGZHDM9ajjc3szW2iGOy981Ilg6p0eIB4vbIzl16+wkAD7G7crYLgdP0nIfEuxZLNQXa23x2n9mLpasktC4/EX9lJJIa7FllYS9GEqnotbpzLyGCtXb8BFrWbNOvae7XetWtxTyQiJ2SaCOZeyBmCkCQAgMRyD9fA3kg/mqvrNbHbjHl9bxeK2d7dSXXr2Nyzu7UQAEhlpB5BFzXeJgz+vj1qqqeWIj54BrSs3DrO54HMWI3lr4/IV7ciRcdmWORWIHP054H78f2U+QdO1nHf5vUKuYyeq5anWfYTm/VXuWraDk+hoy6xRxP2aPkMSWIk7gADlR/J8x4B/dM1Qy9rHR47+TJWo0o6gs3EVJp+pYhmVWYDqGCAdj9sa/j8BvebeLwP/9k=", "_id": "5", "activeArea": undefined, @@ -42,7 +42,7 @@ Array [ "shotnum": 5, "timestamp": "2022-01-05 00:00:00", }, - Object { + { "CHANNEL_BCDEF": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABkAGQMBIgACEQEDEQH/xAAbAAABBAMAAAAAAAAAAAAAAAAFAAYICQECB//EACYQAAICAgICAQQDAQAAAAAAAAECAwQFBhESABMHISIxQRQVFkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArB1DXTt+24bBLfpYpsnchpi9kpvTWr+xwvslf/lF55J/QB8fGX+GasFvP2MbscN3XtetNUy+SlgCPXYSMkZSNXYTLKVIjKN9SD39a/cR2iYbGZHDM9ajjc3szW2iGOy981Ilg6p0eIB4vbIzl16+wkAD7G7crYLgdP0nIfEuxZLNQXa23x2n9mLpasktC4/EX9lJJIa7FllYS9GEqnotbpzLyGCtXb8BFrWbNOvae7XetWtxTyQiJ2SaCOZeyBmCkCQAgMRyD9fA3kg/mqvrNbHbjHl9bxeK2d7dSXXr2Nyzu7UQAEhlpB5BFzXeJgz+vj1qqqeWIj54BrSs3DrO54HMWI3lr4/IV7ciRcdmWORWIHP054H78f2U+QdO1nHf5vUKuYyeq5anWfYTm/VXuWraDk+hoy6xRxP2aPkMSWIk7gADlR/J8x4B/dM1Qy9rHR47+TJWo0o6gs3EVJp+pYhmVWYDqGCAdj9sa/j8BvebeLwP/9k=", "_id": "6", "activeArea": undefined, @@ -50,7 +50,7 @@ Array [ "shotnum": 6, "timestamp": "2022-01-06 00:00:00", }, - Object { + { "CHANNEL_CDEFG": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAB4AOAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAACAAMEBQYBCf/EADQQAAIBAgQEAggFBQAAAAAAAAECAwQRAAUSIRMxQVEGkRQiIzJScZKhFlNhgdIVQmKi0f/EABcBAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAcEQEAAgMBAQEAAAAAAAAAAAABAAIRIUExYRL/2gAMAwEAAhEDEQA/APSHK1pIa6CndaZovRonMJWElBw2JNgNf9t7nbtjQU1BRysyyZdTxOAG08NTseXTnsdsUoEqzZVfiPBJFCNK62AAR9V1C23G3O+/LF5NXRpU076Zt9UZ9g/Ii9+XdR54mqPyUAT7DbK6BNRajpgqi5JiXb7YZpqGjmYrJl1PE2kOoMam6nvtse46bb47WV0bpwtM3tSqW4D7i/rdPhBwU9dGk9PJpm5lCTA+wI+XcLgrnXIAxvsMZVQaSTR0wAvvwl/5iNHTUTRuzZbTodHFQFF9ZfLY8rje1xh2rro2pjHpm9q3DtwHvYne23a5/bAZhVI6oypNc6oyeC42YWty+LThZR1yKhjfYGaZVQ/0irPoVODwHO0S/Cf0wsVuZ5Rl8lHVzcTM1kaNmGqrqlS9ibWLabXPLlyFrWwsUk4OV75vQkryp4QCVHLhP11k/wCo+ZxpKxC9M+kamWzqo6kG4H2xi8oz7L4liqyatpXhjGnhQ2Sy22NtXU8yeeJtd4xX0ZjRmc1A91Z1QIf0JG4xiZMTRw5mjDrUVsZU6kSPiXHc3Cny1Ydq42kp3CC7j1lHdgbj7gYxWV+Lp4Kw+kU6R0pux4LamLdNiBt+/QYuvxvQ/lVH0r/LHJXSPZ022JyWySLVTwFDqRVMt/ncL5gt5YeqozLTuq+/a636MNx97Yz8XjDLYNXDp511HUbKu5+rDn43ofyqj6V/lgV0/rsNtmOSVmkq1WWuU90UzzX7XQgA/O58sLFPX+LstTLa1Y6edS8T3OldzY/5YWFRPZlkfJ//2Q==", "_id": "7", "activeArea": undefined, @@ -58,7 +58,7 @@ Array [ "shotnum": 7, "timestamp": "2022-01-07 00:00:00", }, - Object { + { "CHANNEL_CDEFG": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAB4AOAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAACAAMEBQYBCf/EADQQAAIBAgQEAggFBQAAAAAAAAECAwQRAAUSIRMxQVEGkRQiIzJScZKhFlNhgdIVQmKi0f/EABcBAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAcEQEAAgMBAQEAAAAAAAAAAAABAAIRIUExYRL/2gAMAwEAAhEDEQA/APSHK1pIa6CndaZovRonMJWElBw2JNgNf9t7nbtjQU1BRysyyZdTxOAG08NTseXTnsdsUoEqzZVfiPBJFCNK62AAR9V1C23G3O+/LF5NXRpU076Zt9UZ9g/Ii9+XdR54mqPyUAT7DbK6BNRajpgqi5JiXb7YZpqGjmYrJl1PE2kOoMam6nvtse46bb47WV0bpwtM3tSqW4D7i/rdPhBwU9dGk9PJpm5lCTA+wI+XcLgrnXIAxvsMZVQaSTR0wAvvwl/5iNHTUTRuzZbTodHFQFF9ZfLY8rje1xh2rro2pjHpm9q3DtwHvYne23a5/bAZhVI6oypNc6oyeC42YWty+LThZR1yKhjfYGaZVQ/0irPoVODwHO0S/Cf0wsVuZ5Rl8lHVzcTM1kaNmGqrqlS9ibWLabXPLlyFrWwsUk4OV75vQkryp4QCVHLhP11k/wCo+ZxpKxC9M+kamWzqo6kG4H2xi8oz7L4liqyatpXhjGnhQ2Sy22NtXU8yeeJtd4xX0ZjRmc1A91Z1QIf0JG4xiZMTRw5mjDrUVsZU6kSPiXHc3Cny1Ydq42kp3CC7j1lHdgbj7gYxWV+Lp4Kw+kU6R0pux4LamLdNiBt+/QYuvxvQ/lVH0r/LHJXSPZ022JyWySLVTwFDqRVMt/ncL5gt5YeqozLTuq+/a636MNx97Yz8XjDLYNXDp511HUbKu5+rDn43ofyqj6V/lgV0/rsNtmOSVmkq1WWuU90UzzX7XQgA/O58sLFPX+LstTLa1Y6edS8T3OldzY/5YWFRPZlkfJ//2Q==", "_id": "8", "activeArea": undefined, @@ -66,7 +66,7 @@ Array [ "shotnum": 8, "timestamp": "2022-01-08 00:00:00", }, - Object { + { "CHANNEL_CDEFG": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAB4AOAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAACAAMEBQYBCf/EADQQAAIBAgQEAggFBQAAAAAAAAECAwQRAAUSIRMxQVEGkRQiIzJScZKhFlNhgdIVQmKi0f/EABcBAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAcEQEAAgMBAQEAAAAAAAAAAAABAAIRIUExYRL/2gAMAwEAAhEDEQA/APSHK1pIa6CndaZovRonMJWElBw2JNgNf9t7nbtjQU1BRysyyZdTxOAG08NTseXTnsdsUoEqzZVfiPBJFCNK62AAR9V1C23G3O+/LF5NXRpU076Zt9UZ9g/Ii9+XdR54mqPyUAT7DbK6BNRajpgqi5JiXb7YZpqGjmYrJl1PE2kOoMam6nvtse46bb47WV0bpwtM3tSqW4D7i/rdPhBwU9dGk9PJpm5lCTA+wI+XcLgrnXIAxvsMZVQaSTR0wAvvwl/5iNHTUTRuzZbTodHFQFF9ZfLY8rje1xh2rro2pjHpm9q3DtwHvYne23a5/bAZhVI6oypNc6oyeC42YWty+LThZR1yKhjfYGaZVQ/0irPoVODwHO0S/Cf0wsVuZ5Rl8lHVzcTM1kaNmGqrqlS9ibWLabXPLlyFrWwsUk4OV75vQkryp4QCVHLhP11k/wCo+ZxpKxC9M+kamWzqo6kG4H2xi8oz7L4liqyatpXhjGnhQ2Sy22NtXU8yeeJtd4xX0ZjRmc1A91Z1QIf0JG4xiZMTRw5mjDrUVsZU6kSPiXHc3Cny1Ydq42kp3CC7j1lHdgbj7gYxWV+Lp4Kw+kU6R0pux4LamLdNiBt+/QYuvxvQ/lVH0r/LHJXSPZ022JyWySLVTwFDqRVMt/ncL5gt5YeqozLTuq+/a636MNx97Yz8XjDLYNXDp511HUbKu5+rDn43ofyqj6V/lgV0/rsNtmOSVmkq1WWuU90UzzX7XQgA/O58sLFPX+LstTLa1Y6edS8T3OldzY/5YWFRPZlkfJ//2Q==", "_id": "9", "activeArea": undefined, @@ -74,7 +74,7 @@ Array [ "shotnum": 9, "timestamp": "2022-01-09 00:00:00", }, - Object { + { "CHANNEL_DEFGH": 10, "_id": "10", "activeArea": undefined, @@ -82,7 +82,7 @@ Array [ "shotnum": 10, "timestamp": "2022-01-10 00:00:00", }, - Object { + { "CHANNEL_DEFGH": 11, "_id": "11", "activeArea": undefined, @@ -90,7 +90,7 @@ Array [ "shotnum": 9, "timestamp": "2022-01-11 00:00:00", }, - Object { + { "CHANNEL_DEFGH": 12, "_id": "12", "activeArea": undefined, @@ -98,7 +98,7 @@ Array [ "shotnum": 12, "timestamp": "2022-01-12 00:00:00", }, - Object { + { "CHANNEL_EFGHI": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABkAGQMBIgACEQEDEQH/xAAbAAABBAMAAAAAAAAAAAAAAAAFAAYICQECB//EACYQAAICAgICAQQDAQAAAAAAAAECAwQFBhESABMHISIxQRQVFkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArB1DXTt+24bBLfpYpsnchpi9kpvTWr+xwvslf/lF55J/QB8fGX+GasFvP2MbscN3XtetNUy+SlgCPXYSMkZSNXYTLKVIjKN9SD39a/cR2iYbGZHDM9ajjc3szW2iGOy981Ilg6p0eIB4vbIzl16+wkAD7G7crYLgdP0nIfEuxZLNQXa23x2n9mLpasktC4/EX9lJJIa7FllYS9GEqnotbpzLyGCtXb8BFrWbNOvae7XetWtxTyQiJ2SaCOZeyBmCkCQAgMRyD9fA3kg/mqvrNbHbjHl9bxeK2d7dSXXr2Nyzu7UQAEhlpB5BFzXeJgz+vj1qqqeWIj54BrSs3DrO54HMWI3lr4/IV7ciRcdmWORWIHP054H78f2U+QdO1nHf5vUKuYyeq5anWfYTm/VXuWraDk+hoy6xRxP2aPkMSWIk7gADlR/J8x4B/dM1Qy9rHR47+TJWo0o6gs3EVJp+pYhmVWYDqGCAdj9sa/j8BvebeLwP/9k=", "_id": "13", "activeArea": undefined, @@ -106,7 +106,7 @@ Array [ "shotnum": 13, "timestamp": "2022-01-13 00:00:00", }, - Object { + { "CHANNEL_EFGHI": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABkAGQMBIgACEQEDEQH/xAAbAAABBAMAAAAAAAAAAAAAAAAFAAYICQECB//EACYQAAICAgICAQQDAQAAAAAAAAECAwQFBhESABMHISIxQRQVFkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArB1DXTt+24bBLfpYpsnchpi9kpvTWr+xwvslf/lF55J/QB8fGX+GasFvP2MbscN3XtetNUy+SlgCPXYSMkZSNXYTLKVIjKN9SD39a/cR2iYbGZHDM9ajjc3szW2iGOy981Ilg6p0eIB4vbIzl16+wkAD7G7crYLgdP0nIfEuxZLNQXa23x2n9mLpasktC4/EX9lJJIa7FllYS9GEqnotbpzLyGCtXb8BFrWbNOvae7XetWtxTyQiJ2SaCOZeyBmCkCQAgMRyD9fA3kg/mqvrNbHbjHl9bxeK2d7dSXXr2Nyzu7UQAEhlpB5BFzXeJgz+vj1qqqeWIj54BrSs3DrO54HMWI3lr4/IV7ciRcdmWORWIHP054H78f2U+QdO1nHf5vUKuYyeq5anWfYTm/VXuWraDk+hoy6xRxP2aPkMSWIk7gADlR/J8x4B/dM1Qy9rHR47+TJWo0o6gs3EVJp+pYhmVWYDqGCAdj9sa/j8BvebeLwP/9k=", "_id": "14", "activeArea": undefined, @@ -114,7 +114,7 @@ Array [ "shotnum": 14, "timestamp": "2022-01-14 00:00:00", }, - Object { + { "CHANNEL_EFGHI": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABkAGQMBIgACEQEDEQH/xAAbAAABBAMAAAAAAAAAAAAAAAAFAAYICQECB//EACYQAAICAgICAQQDAQAAAAAAAAECAwQFBhESABMHISIxQRQVFkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8ArB1DXTt+24bBLfpYpsnchpi9kpvTWr+xwvslf/lF55J/QB8fGX+GasFvP2MbscN3XtetNUy+SlgCPXYSMkZSNXYTLKVIjKN9SD39a/cR2iYbGZHDM9ajjc3szW2iGOy981Ilg6p0eIB4vbIzl16+wkAD7G7crYLgdP0nIfEuxZLNQXa23x2n9mLpasktC4/EX9lJJIa7FllYS9GEqnotbpzLyGCtXb8BFrWbNOvae7XetWtxTyQiJ2SaCOZeyBmCkCQAgMRyD9fA3kg/mqvrNbHbjHl9bxeK2d7dSXXr2Nyzu7UQAEhlpB5BFzXeJgz+vj1qqqeWIj54BrSs3DrO54HMWI3lr4/IV7ciRcdmWORWIHP054H78f2U+QdO1nHf5vUKuYyeq5anWfYTm/VXuWraDk+hoy6xRxP2aPkMSWIk7gADlR/J8x4B/dM1Qy9rHR47+TJWo0o6gs3EVJp+pYhmVWYDqGCAdj9sa/j8BvebeLwP/9k=", "_id": "15", "activeArea": undefined, @@ -122,7 +122,7 @@ Array [ "shotnum": 15, "timestamp": "2022-01-15 00:00:00", }, - Object { + { "CHANNEL_FGHIJ": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAB4AOAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAACAAMEBQYBCf/EADQQAAIBAgQEAggFBQAAAAAAAAECAwQRAAUSIRMxQVEGkRQiIzJScZKhFlNhgdIVQmKi0f/EABcBAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAcEQEAAgMBAQEAAAAAAAAAAAABAAIRIUExYRL/2gAMAwEAAhEDEQA/APSHK1pIa6CndaZovRonMJWElBw2JNgNf9t7nbtjQU1BRysyyZdTxOAG08NTseXTnsdsUoEqzZVfiPBJFCNK62AAR9V1C23G3O+/LF5NXRpU076Zt9UZ9g/Ii9+XdR54mqPyUAT7DbK6BNRajpgqi5JiXb7YZpqGjmYrJl1PE2kOoMam6nvtse46bb47WV0bpwtM3tSqW4D7i/rdPhBwU9dGk9PJpm5lCTA+wI+XcLgrnXIAxvsMZVQaSTR0wAvvwl/5iNHTUTRuzZbTodHFQFF9ZfLY8rje1xh2rro2pjHpm9q3DtwHvYne23a5/bAZhVI6oypNc6oyeC42YWty+LThZR1yKhjfYGaZVQ/0irPoVODwHO0S/Cf0wsVuZ5Rl8lHVzcTM1kaNmGqrqlS9ibWLabXPLlyFrWwsUk4OV75vQkryp4QCVHLhP11k/wCo+ZxpKxC9M+kamWzqo6kG4H2xi8oz7L4liqyatpXhjGnhQ2Sy22NtXU8yeeJtd4xX0ZjRmc1A91Z1QIf0JG4xiZMTRw5mjDrUVsZU6kSPiXHc3Cny1Ydq42kp3CC7j1lHdgbj7gYxWV+Lp4Kw+kU6R0pux4LamLdNiBt+/QYuvxvQ/lVH0r/LHJXSPZ022JyWySLVTwFDqRVMt/ncL5gt5YeqozLTuq+/a636MNx97Yz8XjDLYNXDp511HUbKu5+rDn43ofyqj6V/lgV0/rsNtmOSVmkq1WWuU90UzzX7XQgA/O58sLFPX+LstTLa1Y6edS8T3OldzY/5YWFRPZlkfJ//2Q==", "CHANNEL_GHIJK": 18, "_id": "16", @@ -131,7 +131,7 @@ Array [ "shotnum": 16, "timestamp": "2022-01-16 00:00:00", }, - Object { + { "CHANNEL_FGHIJ": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAB4AOAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAACAAMEBQYBCf/EADQQAAIBAgQEAggFBQAAAAAAAAECAwQRAAUSIRMxQVEGkRQiIzJScZKhFlNhgdIVQmKi0f/EABcBAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAcEQEAAgMBAQEAAAAAAAAAAAABAAIRIUExYRL/2gAMAwEAAhEDEQA/APSHK1pIa6CndaZovRonMJWElBw2JNgNf9t7nbtjQU1BRysyyZdTxOAG08NTseXTnsdsUoEqzZVfiPBJFCNK62AAR9V1C23G3O+/LF5NXRpU076Zt9UZ9g/Ii9+XdR54mqPyUAT7DbK6BNRajpgqi5JiXb7YZpqGjmYrJl1PE2kOoMam6nvtse46bb47WV0bpwtM3tSqW4D7i/rdPhBwU9dGk9PJpm5lCTA+wI+XcLgrnXIAxvsMZVQaSTR0wAvvwl/5iNHTUTRuzZbTodHFQFF9ZfLY8rje1xh2rro2pjHpm9q3DtwHvYne23a5/bAZhVI6oypNc6oyeC42YWty+LThZR1yKhjfYGaZVQ/0irPoVODwHO0S/Cf0wsVuZ5Rl8lHVzcTM1kaNmGqrqlS9ibWLabXPLlyFrWwsUk4OV75vQkryp4QCVHLhP11k/wCo+ZxpKxC9M+kamWzqo6kG4H2xi8oz7L4liqyatpXhjGnhQ2Sy22NtXU8yeeJtd4xX0ZjRmc1A91Z1QIf0JG4xiZMTRw5mjDrUVsZU6kSPiXHc3Cny1Ydq42kp3CC7j1lHdgbj7gYxWV+Lp4Kw+kU6R0pux4LamLdNiBt+/QYuvxvQ/lVH0r/LHJXSPZ022JyWySLVTwFDqRVMt/ncL5gt5YeqozLTuq+/a636MNx97Yz8XjDLYNXDp511HUbKu5+rDn43ofyqj6V/lgV0/rsNtmOSVmkq1WWuU90UzzX7XQgA/O58sLFPX+LstTLa1Y6edS8T3OldzY/5YWFRPZlkfJ//2Q==", "CHANNEL_GHIJK": 17, "_id": "17", @@ -140,7 +140,7 @@ Array [ "shotnum": 17, "timestamp": "2022-01-17 00:00:00", }, - Object { + { "CHANNEL_FGHIJ": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAB4AOAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAACAAMEBQYBCf/EADQQAAIBAgQEAggFBQAAAAAAAAECAwQRAAUSIRMxQVEGkRQiIzJScZKhFlNhgdIVQmKi0f/EABcBAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAcEQEAAgMBAQEAAAAAAAAAAAABAAIRIUExYRL/2gAMAwEAAhEDEQA/APSHK1pIa6CndaZovRonMJWElBw2JNgNf9t7nbtjQU1BRysyyZdTxOAG08NTseXTnsdsUoEqzZVfiPBJFCNK62AAR9V1C23G3O+/LF5NXRpU076Zt9UZ9g/Ii9+XdR54mqPyUAT7DbK6BNRajpgqi5JiXb7YZpqGjmYrJl1PE2kOoMam6nvtse46bb47WV0bpwtM3tSqW4D7i/rdPhBwU9dGk9PJpm5lCTA+wI+XcLgrnXIAxvsMZVQaSTR0wAvvwl/5iNHTUTRuzZbTodHFQFF9ZfLY8rje1xh2rro2pjHpm9q3DtwHvYne23a5/bAZhVI6oypNc6oyeC42YWty+LThZR1yKhjfYGaZVQ/0irPoVODwHO0S/Cf0wsVuZ5Rl8lHVzcTM1kaNmGqrqlS9ibWLabXPLlyFrWwsUk4OV75vQkryp4QCVHLhP11k/wCo+ZxpKxC9M+kamWzqo6kG4H2xi8oz7L4liqyatpXhjGnhQ2Sy22NtXU8yeeJtd4xX0ZjRmc1A91Z1QIf0JG4xiZMTRw5mjDrUVsZU6kSPiXHc3Cny1Ydq42kp3CC7j1lHdgbj7gYxWV+Lp4Kw+kU6R0pux4LamLdNiBt+/QYuvxvQ/lVH0r/LHJXSPZ022JyWySLVTwFDqRVMt/ncL5gt5YeqozLTuq+/a636MNx97Yz8XjDLYNXDp511HUbKu5+rDn43ofyqj6V/lgV0/rsNtmOSVmkq1WWuU90UzzX7XQgA/O58sLFPX+LstTLa1Y6edS8T3OldzY/5YWFRPZlkfJ//2Q==", "CHANNEL_GHIJK": 18, "_id": "18", diff --git a/src/api/channels.test.tsx b/src/api/channels.test.tsx index 9f5b75ea3..e7a1176ab 100644 --- a/src/api/channels.test.tsx +++ b/src/api/channels.test.tsx @@ -1,26 +1,26 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { renderHook, waitFor } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; +import { FullChannelMetadata, timeChannelName } from '../app.types'; +import { server } from '../mocks/server'; +import { RootState } from '../state/store'; import { - useChannels, - useAvailableColumns, + getInitialState, + hooksWrapperWithProviders, + testChannels, +} from '../testUtils'; +import { + ChannelSummary, getScalarChannels, staticChannels, + useAvailableColumns, + useChannels, useChannelSummary, - ChannelSummary, } from './channels'; -import { FullChannelMetadata, timeChannelName } from '../app.types'; -import { - hooksWrapperWithProviders, - getInitialState, - testChannels, -} from '../setupTests'; -import { RootState } from '../state/store'; -import { server } from '../mocks/server'; -import { rest } from 'msw'; describe('channels api functions', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('useAvailableColumns', () => { @@ -77,8 +77,8 @@ describe('channels api functions', () => { it('returns no columns if no data was present in the request response', async () => { server.use( - rest.get('/channels', (req, res, ctx) => { - return res(ctx.status(200), ctx.json({ channels: {} })); + http.get('/channels', () => { + return HttpResponse.json({ channels: {} }, { status: 200 }); }) ); @@ -168,8 +168,8 @@ describe('channels api functions', () => { it('returns no channels if no data was present in the request response', async () => { server.use( - rest.get('/channels', (req, res, ctx) => { - return res(ctx.status(200), ctx.json({ channels: {} })); + http.get('/channels', () => { + return HttpResponse.json({ channels: {} }, { status: 200 }); }) ); diff --git a/src/api/channels.tsx b/src/api/channels.tsx index 809c937e2..a4e1fcb73 100644 --- a/src/api/channels.tsx +++ b/src/api/channels.tsx @@ -1,5 +1,11 @@ -import React from 'react'; +import { + useQuery, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; +import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; import axios, { AxiosError } from 'axios'; +import React from 'react'; import { FullChannelMetadata, FullScalarChannelMetadata, @@ -9,21 +15,15 @@ import { RecordRow, timeChannelName, } from '../app.types'; -import { - useQuery, - UseQueryResult, - UseQueryOptions, -} from '@tanstack/react-query'; -import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; +import { readSciGatewayToken } from '../parseTokens'; +import { useAppDispatch, useAppSelector } from '../state/hooks'; +import { selectUrls } from '../state/slices/configSlice'; +import { openImageWindow, openTraceWindow } from '../state/slices/windowSlice'; +import { AppDispatch } from '../state/store'; import { roundNumber, TraceOrImageThumbnail, } from '../table/cellRenderers/cellContentRenderers'; -import { selectUrls } from '../state/slices/configSlice'; -import { useAppDispatch, useAppSelector } from '../state/hooks'; -import { readSciGatewayToken } from '../parseTokens'; -import { AppDispatch } from '../state/store'; -import { openImageWindow, openTraceWindow } from '../state/slices/windowSlice'; interface ChannelsEndpoint { channels: { @@ -114,7 +114,7 @@ export const useChannels = ( return useQuery({ queryKey: ['channels'], - queryFn: (params) => { + queryFn: () => { return fetchChannels(apiUrl); }, @@ -134,7 +134,7 @@ export const useChannelSummary = ( return useQuery({ queryKey: ['channelSummary', dataChannel], - queryFn: (params) => { + queryFn: () => { return fetchChannelSummary(apiUrl, dataChannel); }, diff --git a/src/api/experiment.test.tsx b/src/api/experiment.test.tsx index 4fd97df67..6f89bcb30 100644 --- a/src/api/experiment.test.tsx +++ b/src/api/experiment.test.tsx @@ -1,12 +1,12 @@ 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'; +import { hooksWrapperWithProviders } from '../testUtils'; +import { useExperiment } from './experiment'; describe('channels api functions', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('useExperiment', () => { diff --git a/src/api/experiment.tsx b/src/api/experiment.tsx index a832f8d32..76e617e4a 100644 --- a/src/api/experiment.tsx +++ b/src/api/experiment.tsx @@ -1,9 +1,9 @@ -import axios, { AxiosError } from 'axios'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import axios, { AxiosError } from 'axios'; import { ExperimentParams } from '../app.types'; +import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; import { selectUrls } from '../state/slices/configSlice'; -import { readSciGatewayToken } from '../parseTokens'; const fetchExperiment = (apiUrl: string): Promise => { return axios @@ -26,8 +26,6 @@ export const useExperiment = (): UseQueryResult< return useQuery({ queryKey: ['experiments'], - queryFn: (params) => { - return fetchExperiment(apiUrl); - }, + queryFn: () => fetchExperiment(apiUrl), }); }; diff --git a/src/api/export.test.tsx b/src/api/export.test.tsx index 9a148a276..93ea72cda 100644 --- a/src/api/export.test.tsx +++ b/src/api/export.test.tsx @@ -1,13 +1,13 @@ -import axios from 'axios'; -import { useExportData } from './export'; import { renderHook, waitFor } from '@testing-library/react'; -import { hooksWrapperWithProviders, getInitialState } from '../setupTests'; +import axios from 'axios'; import { RootState } from '../state/store'; +import { getInitialState, hooksWrapperWithProviders } from '../testUtils'; +import { useExportData } from './export'; describe('useExportData', () => { let state: RootState; - const mockLinkClick = jest.fn(); - const mockLinkRemove = jest.fn(); + const mockLinkClick = vi.fn(); + const mockLinkRemove = vi.fn(); let mockLink: HTMLAnchorElement = {}; beforeEach(() => { @@ -50,19 +50,19 @@ describe('useExportData', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); document.createElement = document.originalCreateElement; document.body.appendChild = document.body.originalAppendChild; }); it('sends axios request to export selected rows and returns successful response', async () => { - const getSpy = jest.spyOn(axios, 'get'); + const getSpy = vi.spyOn(axios, 'get'); - document.createElement = jest.fn().mockImplementation((tag) => { + document.createElement = vi.fn().mockImplementation((tag) => { if (tag === 'a') return mockLink; else return document.originalCreateElement(tag); }); - document.body.appendChild = jest.fn().mockImplementation((node) => { + document.body.appendChild = vi.fn().mockImplementation((node) => { if (!(node instanceof Node)) return mockLink; else return document.body.originalAppendChild(node); }); @@ -133,13 +133,13 @@ describe('useExportData', () => { }); it('sends axios request to export all rows and returns successful response', async () => { - const getSpy = jest.spyOn(axios, 'get'); + const getSpy = vi.spyOn(axios, 'get'); - document.createElement = jest.fn().mockImplementation((tag) => { + document.createElement = vi.fn().mockImplementation((tag) => { if (tag === 'a') return mockLink; else return document.originalCreateElement(tag); }); - document.body.appendChild = jest.fn().mockImplementation((node) => { + document.body.appendChild = vi.fn().mockImplementation((node) => { if (!(node instanceof Node)) return mockLink; else return document.body.originalAppendChild(node); }); @@ -210,13 +210,13 @@ describe('useExportData', () => { }); it('sends axios request to export visible rows and returns successful response', async () => { - const getSpy = jest.spyOn(axios, 'get'); + const getSpy = vi.spyOn(axios, 'get'); - document.createElement = jest.fn().mockImplementation((tag) => { + document.createElement = vi.fn().mockImplementation((tag) => { if (tag === 'a') return mockLink; else return document.originalCreateElement(tag); }); - document.body.appendChild = jest.fn().mockImplementation((node) => { + document.body.appendChild = vi.fn().mockImplementation((node) => { if (!(node instanceof Node)) return mockLink; else return document.body.originalAppendChild(node); }); @@ -288,13 +288,13 @@ describe('useExportData', () => { it('sends axios request without skip and limit when maxShots is unlimited and exporting all rows', async () => { state.search.searchParams.maxShots = Infinity; - const getSpy = jest.spyOn(axios, 'get'); + const getSpy = vi.spyOn(axios, 'get'); - document.createElement = jest.fn().mockImplementation((tag) => { + document.createElement = vi.fn().mockImplementation((tag) => { if (tag === 'a') return mockLink; else return document.originalCreateElement(tag); }); - document.body.appendChild = jest.fn().mockImplementation((node) => { + document.body.appendChild = vi.fn().mockImplementation((node) => { if (!(node instanceof Node)) return mockLink; else return document.body.originalAppendChild(node); }); diff --git a/src/api/images.test.tsx b/src/api/images.test.tsx index 01ef04443..af0ed9dbb 100644 --- a/src/api/images.test.tsx +++ b/src/api/images.test.tsx @@ -1,12 +1,11 @@ -import { waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react'; -import { hooksWrapperWithProviders, waitForRequest } from '../setupTests'; -import { useColourBar, useColourMaps, useImage } from './images'; +import { renderHook, waitFor } from '@testing-library/react'; import colourMapsJson from '../mocks/colourMaps.json'; +import { hooksWrapperWithProviders, waitForRequest } from '../testUtils'; +import { useColourBar, useColourMaps, useImage } from './images'; describe('images api functions', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('useImage', () => { @@ -35,7 +34,7 @@ describe('images api functions', () => { params.set('original_image', 'true'); expect(result.current.data).toEqual('testObjectUrl'); - expect(request.url.searchParams).toEqual(params); + expect(new URL(request.url).searchParams).toEqual(params); }); it('sends request to fetch original image with empty false colour params and returns successful response', async () => { @@ -57,7 +56,7 @@ describe('images api functions', () => { params.set('original_image', 'true'); expect(result.current.data).toEqual('testObjectUrl'); - expect(request.url.searchParams).toEqual(params); + expect(new URL(request.url).searchParams).toEqual(params); }); it('sends request to fetch false colour image and returns successful response', async () => { @@ -89,7 +88,7 @@ describe('images api functions', () => { params.set('upper_level', '200'); expect(result.current.data).toEqual('testObjectUrl'); - expect(request.url.searchParams).toEqual(params); + expect(new URL(request.url).searchParams).toEqual(params); }); it.todo( @@ -130,7 +129,7 @@ describe('images api functions', () => { params.set('upper_level', '200'); expect(result.current.data).toEqual('testObjectUrl'); - expect(request.url.searchParams).toEqual(params); + expect(new URL(request.url).searchParams).toEqual(params); }); it.todo( diff --git a/src/api/images.tsx b/src/api/images.tsx index a17e23dd3..7bc3292ea 100644 --- a/src/api/images.tsx +++ b/src/api/images.tsx @@ -1,7 +1,7 @@ import { UseQueryResult, - useQuery, keepPreviousData, + useQuery, } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { readSciGatewayToken } from '../parseTokens'; @@ -101,7 +101,7 @@ export const useImage = ( return useQuery({ queryKey: ['images', recordId, channelName, falseColourParams], - queryFn: (params) => { + queryFn: () => { return fetchImage(apiUrl, recordId, channelName, falseColourParams); }, @@ -118,7 +118,7 @@ export const useColourBar = ( return useQuery({ queryKey: ['colourbar', falseColourParams], - queryFn: (params) => { + queryFn: () => { return fetchColourBar(apiUrl, falseColourParams); }, @@ -136,7 +136,7 @@ export const useColourMaps = (): UseQueryResult< return useQuery({ queryKey: ['colourmaps'], - queryFn: (params) => { + queryFn: () => { return fetchColourMaps(apiUrl); }, }); diff --git a/src/api/records.test.tsx b/src/api/records.test.tsx index 4d14f6090..0fe3cfb0e 100644 --- a/src/api/records.test.tsx +++ b/src/api/records.test.tsx @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { renderHook, waitFor } from '@testing-library/react'; +import { parseISO } from 'date-fns'; import { PlotDataset, Record, @@ -7,28 +9,26 @@ import { SelectedPlotChannel, timeChannelName, } from '../app.types'; +import { operators, parseFilter, Token } from '../filtering/filterParser'; +import recordsJson from '../mocks/records.json'; +import { MAX_SHOTS_VALUES } from '../search/components/maxShots.component'; +import { RootState } from '../state/store'; import { - hooksWrapperWithProviders, - getInitialState, createTestQueryClient, + getInitialState, + hooksWrapperWithProviders, waitForRequest, -} from '../setupTests'; -import { renderHook, waitFor } from '@testing-library/react'; +} from '../testUtils'; import { getFormattedAxisData, + useDateToShotnumConverter, + useIncomingRecordCount, usePlotRecords, useRecordCount, - useIncomingRecordCount, useRecordsPaginated, - useThumbnails, useShotnumToDateConverter, - useDateToShotnumConverter, + useThumbnails, } from './records'; -import { RootState } from '../state/store'; -import { parseISO } from 'date-fns'; -import { operators, parseFilter, Token } from '../filtering/filterParser'; -import { MAX_SHOTS_VALUES } from '../search/components/maxShots.component'; -import recordsJson from '../mocks/records.json'; describe('records api functions', () => { let state: RootState; @@ -38,8 +38,8 @@ describe('records api functions', () => { }); afterEach(() => { - jest.clearAllMocks(); - jest.useRealTimers(); + vi.clearAllMocks(); + vi.useRealTimers(); }); describe('useRecordCount', () => { @@ -104,7 +104,9 @@ describe('records api functions', () => { '{"$and":[{"metadata.timestamp":{"$gte":"2022-01-01 00:00:00","$lte":"2022-01-02 00:00:00"}},{"metadata.shotnum":{"$gt":300}}]}' ); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); expect(result.current.data).toEqual(recordsJson.length); }); @@ -143,7 +145,9 @@ describe('records api functions', () => { const request = await pendingRequest; // We should have made one call to /records/count - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); expect(incomingRecordCountResult.current.data).toEqual( recordsJson.length ); @@ -317,7 +321,9 @@ describe('records api functions', () => { '{"$and":[{"metadata.timestamp":{"$gte":"2022-01-01 00:00:00","$lte":"2022-01-02 00:00:00"}},{"metadata.shotnum":{"$gt":300}}]}' ); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); expect(result.current.data).toEqual(recordsJson.length); }); @@ -364,7 +370,9 @@ describe('records api functions', () => { '{"$and":[{"metadata.timestamp":{"$gte":"2022-01-01 00:00:00","$lte":"2022-01-02 00:00:00"}},{"metadata.shotnum":{"$gt":300}}]}' ); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); expect(result.current.data).toEqual(recordsJson.length); }); @@ -381,7 +389,7 @@ describe('records api functions', () => { }); it('sends request to fetch records, returns successful response and uses a select function to format the results', async () => { - jest.useFakeTimers().setSystemTime(new Date('2024-07-02 12:00:00')); + vi.useFakeTimers().setSystemTime(new Date('2024-07-02 12:00:00')); const pendingRequest = waitForRequest('GET', '/records'); @@ -406,7 +414,9 @@ describe('records api functions', () => { params.append('skip', '0'); params.append('limit', '25'); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); expect(result.current.data).toMatchSnapshot(); }); @@ -465,7 +475,9 @@ describe('records api functions', () => { params.append('skip', '0'); params.append('limit', '25'); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); }); }); @@ -489,11 +501,11 @@ describe('records api functions', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('uses a select function to format the results', async () => { - jest.useFakeTimers().setSystemTime(new Date('2024-07-02 12:00:00')); + vi.useFakeTimers().setSystemTime(new Date('2024-07-02 12:00:00')); const pendingRequest = waitForRequest('GET', '/records'); @@ -529,7 +541,9 @@ describe('records api functions', () => { params.append('skip', '0'); params.append('limit', '50'); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); const expectedData: PlotDataset[] = [ { @@ -614,7 +628,9 @@ describe('records api functions', () => { params.append('skip', '0'); params.append('limit', '1000'); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); const expectedData: PlotDataset[] = [ { @@ -682,7 +698,9 @@ describe('records api functions', () => { '{"$or":' + JSON.stringify(existsConditions) + '}' ); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); }); }); @@ -694,11 +712,11 @@ describe('records api functions', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('sends request to fetch records with a projection and returns successful response', async () => { - jest.useFakeTimers().setSystemTime(new Date('2024-07-02 12:00:00')); + vi.useFakeTimers().setSystemTime(new Date('2024-07-02 12:00:00')); const pendingRequest = waitForRequest('GET', '/records'); @@ -724,7 +742,9 @@ describe('records api functions', () => { params.append('skip', '25'); params.append('limit', '25'); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); expect(result.current.data).toEqual(recordsJson); }); @@ -782,7 +802,9 @@ describe('records api functions', () => { params.append('skip', '0'); params.append('limit', '25'); - expect(request.url.searchParams.toString()).toEqual(params.toString()); + expect(new URL(request.url).searchParams.toString()).toEqual( + params.toString() + ); }); }); diff --git a/src/api/records.tsx b/src/api/records.tsx index adbd6b4e3..0f833fbac 100644 --- a/src/api/records.tsx +++ b/src/api/records.tsx @@ -1,30 +1,30 @@ -import axios, { AxiosError } from 'axios'; import { useQuery, - UseQueryResult, useQueryClient, + UseQueryResult, } from '@tanstack/react-query'; +import axios, { AxiosError } from 'axios'; +import { parseISO } from 'date-fns'; import { + DateRangetoShotnumConverter, + isChannelImage, isChannelScalar, + isChannelWaveform, PlotDataset, Record, RecordRow, - SortType, - SelectedPlotChannel, SearchParams, + SelectedPlotChannel, + SortType, timeChannelName, - isChannelImage, - isChannelWaveform, - DateRangetoShotnumConverter, } from '../app.types'; +import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; -import { selectQueryParams } from '../state/slices/searchSlice'; -import { parseISO } from 'date-fns'; import { selectUrls } from '../state/slices/configSlice'; -import { readSciGatewayToken } from '../parseTokens'; +import { selectQueryParams } from '../state/slices/searchSlice'; +import { selectSelectedIdsIgnoreOrder } from '../state/slices/tableSlice'; import { renderTimestamp } from '../table/cellRenderers/cellContentRenderers'; import { staticChannels } from './channels'; -import { selectSelectedIdsIgnoreOrder } from '../state/slices/tableSlice'; const fetchRecords = async ( apiUrl: string, @@ -245,7 +245,7 @@ export const useDateToShotnumConverter = ( return useQuery({ queryKey: ['dateToShotnumConverter', { fromDate, toDate }], - queryFn: (params) => { + queryFn: () => { return fetchRangeRecordConverterQuery( apiUrl, fromDate, @@ -268,17 +268,14 @@ export const useShotnumToDateConverter = ( return useQuery({ queryKey: ['shotnumToDateConverter', { shotnumMin, shotnumMax }], - - queryFn: (params) => { - return fetchRangeRecordConverterQuery( + queryFn: () => + fetchRangeRecordConverterQuery( apiUrl, undefined, undefined, shotnumMin, shotnumMax - ); - }, - + ), enabled, }); }; diff --git a/src/api/sessions.test.tsx b/src/api/sessions.test.tsx index 749116693..6725ab2c0 100644 --- a/src/api/sessions.test.tsx +++ b/src/api/sessions.test.tsx @@ -1,4 +1,7 @@ import { renderHook, waitFor } from '@testing-library/react'; +import { Session, SessionListItem } from '../app.types'; +import sessionsListJSON from '../mocks/sessionsList.json'; +import { hooksWrapperWithProviders } from '../testUtils'; import { useDeleteSession, useEditSession, @@ -6,9 +9,6 @@ import { useSession, useSessionList, } from './sessions'; -import { Session, SessionListItem } from '../app.types'; -import { hooksWrapperWithProviders } from '../setupTests'; -import sessionsListJSON from '../mocks/sessionsList.json'; describe('session api functions', () => { let mockData: Session; @@ -22,7 +22,7 @@ describe('session api functions', () => { }; }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('useSaveSession', () => { diff --git a/src/api/sessions.tsx b/src/api/sessions.tsx index e74b4267f..48827d927 100644 --- a/src/api/sessions.tsx +++ b/src/api/sessions.tsx @@ -1,15 +1,15 @@ -import axios, { AxiosError } from 'axios'; import { useMutation, UseMutationResult, useQuery, - UseQueryResult, useQueryClient, + UseQueryResult, } from '@tanstack/react-query'; +import axios, { AxiosError } from 'axios'; import { Session, SessionListItem, SessionResponse } from '../app.types'; +import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; import { selectUrls } from '../state/slices/configSlice'; -import { readSciGatewayToken } from '../parseTokens'; const saveSession = (apiUrl: string, session: Session): Promise => { const queryParams = new URLSearchParams(); @@ -136,7 +136,7 @@ export const useSessionList = (): UseQueryResult< return useQuery({ queryKey: ['sessionList'], - queryFn: (params) => { + queryFn: () => { return fetchSessionList(apiUrl); }, }); @@ -165,7 +165,7 @@ export const useSession = ( return useQuery({ queryKey: ['session', session_id], - queryFn: (params) => { + queryFn: () => { return fetchSession(apiUrl, session_id); }, diff --git a/src/api/userPreferences.test.tsx b/src/api/userPreferences.test.tsx index 13a9e8b0f..d8aaaaa41 100644 --- a/src/api/userPreferences.test.tsx +++ b/src/api/userPreferences.test.tsx @@ -1,16 +1,16 @@ import { renderHook, waitFor } from '@testing-library/react'; -import { hooksWrapperWithProviders } from '../setupTests'; -import { useUpdateUserPreference, useUserPreference } from './userPreferences'; -import { PREFERRED_COLOUR_MAP_PREFERENCE_NAME } from '../settingsMenuItems.component'; import axios from 'axios'; import { setMockedPreferredColourMap } from '../mocks/handlers'; +import { PREFERRED_COLOUR_MAP_PREFERENCE_NAME } from '../settingsMenuItems.component'; +import { hooksWrapperWithProviders } from '../testUtils'; +import { useUpdateUserPreference, useUserPreference } from './userPreferences'; describe('user preferences api functions', () => { - const axiosPost = jest.spyOn(axios, 'post'); - const axiosDelete = jest.spyOn(axios, 'delete'); + const axiosPost = vi.spyOn(axios, 'post'); + const axiosDelete = vi.spyOn(axios, 'delete'); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('useUserPreference', () => { diff --git a/src/api/userPreferences.tsx b/src/api/userPreferences.tsx index dfacd80d4..66d80ac99 100644 --- a/src/api/userPreferences.tsx +++ b/src/api/userPreferences.tsx @@ -43,7 +43,7 @@ export const useUserPreference = ( return useQuery({ queryKey: ['userPreference', name], - queryFn: (params) => { + queryFn: () => { return fetchUserPreference(apiUrl, name); }, }); @@ -101,7 +101,7 @@ export const useUpdateUserPreference = ( onError: (error) => { console.log('Got error ' + error.message); }, - onSuccess: (data, vars) => { + onSuccess: (_data, vars) => { queryClient.setQueryData(['userPreference', name], vars.value); }, }); diff --git a/src/api/waveforms.test.tsx b/src/api/waveforms.test.tsx index 153545e19..60c6266ed 100644 --- a/src/api/waveforms.test.tsx +++ b/src/api/waveforms.test.tsx @@ -1,11 +1,10 @@ -import { waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react'; -import { hooksWrapperWithProviders } from '../setupTests'; +import { renderHook, waitFor } from '@testing-library/react'; +import { hooksWrapperWithProviders } from '../testUtils'; import { useWaveform } from './waveforms'; describe('waveform api functions', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('useWaveform', () => { diff --git a/src/api/waveforms.tsx b/src/api/waveforms.tsx index 5e3598455..37ab657ed 100644 --- a/src/api/waveforms.tsx +++ b/src/api/waveforms.tsx @@ -30,7 +30,7 @@ export const useWaveform = ( return useQuery({ queryKey: ['waveforms', recordId, channelName], - queryFn: (params) => { + queryFn: () => { return fetchWaveform(apiUrl, recordId, channelName); }, }); diff --git a/src/channels/__snapshots__/channelBreadcrumbs.component.test.tsx.snap b/src/channels/__snapshots__/channelBreadcrumbs.component.test.tsx.snap index 077a3bcb7..04f789ef2 100644 --- a/src/channels/__snapshots__/channelBreadcrumbs.component.test.tsx.snap +++ b/src/channels/__snapshots__/channelBreadcrumbs.component.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Channel Breadcrumbs should render correctly for path 1`] = ` +exports[`Channel Breadcrumbs > should render correctly for path 1`] = `