diff --git a/.env b/.env deleted file mode 100644 index 4a10ac9..0000000 --- a/.env +++ /dev/null @@ -1,8 +0,0 @@ -# reference https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser -# to learn about the NEXT_PUBLIC prefix - -NEXT_PUBLIC_PROVIDER_NAME=phenovista -NEXT_PUBLIC_PROVIDER_ID=12527 -NEXT_PUBLIC_SCIENTIST_API_VERSION=v2 -NEXT_PUBLIC_WEBHOOK_URL=http://ss-mailer/webstore -NEXT_PUBLIC_APP_BASE_URL=https://www.phenovista.vercel.app diff --git a/README.md b/README.md index 7890bf5..5bca77e 100644 --- a/README.md +++ b/README.md @@ -174,14 +174,16 @@ There are 2 types of Cypress tests, e2e & component. If you are creating an e2e test, it will live in the `cypress/e2e` directory. Component tests will need to be created in a directory called `cypress/component ` #### Cypress ENV Variables -- the Cypress suite requires an environment variable that should be stored in your `.env.development` and not committed to git. +- the Cypress suite requires an environment variable that should be stored in your `.env` and not committed to git. - TEST_SESSION_COOKIE= - - to get the value for this variable, open your browser to your running app at `localhost:3000`. + - to get the value for this variable, open your browser to your running app at `localhost:3000` + - sign in - inspect the page - - click the "Application" tab + - Chrome: click the "Application" tab + - Firefox: click the "Storage" tab - click "Cookies" - find the value for `next-auth.session-token` - - copy that value and paste it in the `TEST_SESSION_COOKIE` variable in your .env.development + - copy that value and paste it in the `TEST_SESSION_COOKIE` variable in your `.env` - do not ever commit this value - this value will need to be updated whenever the cookie expires, approximately once per month diff --git a/cypress.config.js b/cypress.config.js index 76c23b9..59795e0 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -11,6 +11,7 @@ module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:3000', chromeWebSecurity: false, + defaultCommandTimeout: 10000, setupNodeEvents(on, config) { config = dotenvFlowPlugin(config) config.env = { @@ -21,14 +22,19 @@ module.exports = defineConfig({ }, }, env: { - TEST_SCIENTIST_USER: 'test@test.com', + CYPRESS_SEARCH_QUERY: 'test', + NEXT_PUBLIC_PROVIDER_ID: process.env.NEXT_PUBLIC_PROVIDER_ID, + NEXT_PUBLIC_PROVIDER_NAME: process.env.NEXT_PUBLIC_PROVIDER_NAME, + NEXT_PUBLIC_TOKEN: process.env.NEXT_PUBLIC_TOKEN, TEST_SCIENTIST_PW: '!test1234', NEXT_PUBLIC_PROVIDER_NAME: 'phenovista', - NEXT_PUBLIC_PROVIDER_ID: '12527' + NEXT_PUBLIC_PROVIDER_ID: '12527', + TEST_SCIENTIST_USER: 'test@test.com', + TEST_SESSION_COOKIE: process.env.TEST_SESSION_COOKIE, }, reporter: 'junit', reporterOptions: { mochaFile: 'cypress/results/results-[hash].xml', toConsole: true, }, -}); +}) diff --git a/cypress/e2e/browse.cy.js b/cypress/e2e/browse.cy.js index dd0e64f..aee5320 100644 --- a/cypress/e2e/browse.cy.js +++ b/cypress/e2e/browse.cy.js @@ -19,10 +19,10 @@ describe('Browsing', () => { emptyFixture: 'services/no-wares.json', }, ] - + beforeEach(() => { // Intercept the responses from the endpoint to view all requests. - // Even though this is to the same endpoint, the call happens on each page twice, + // Even though this is to the same endpoint, the call happens on each page twice, // once when the page loads with all the wares, and again after any search is performed. // this makes it necessary to create an intercept for each time the call is made. intercepts.forEach((intercept) => { @@ -103,39 +103,4 @@ describe('Browsing', () => { }) }) }) - - describe('from the home page', () => { - beforeEach(() => { - wares = true - // Intercept the api call being made on the homepage - cy.customApiIntercept({ - action: 'GET', - alias: 'useAllWares', - requestURL: `/providers/${Cypress.env('NEXT_PUBLIC_PROVIDER_ID')}/wares.json`, - data: wares, - defaultFixture: 'services/wares.json', - loading, - error - }) - cy.visit('/') - }) - - context('a search is completed successfully and', () => { - it('navigates to "/browse" with a blank query', () => { - cy.get('button.search-button').click() - cy.url().should('include', '/browse') - cy.url().should('not.include', '?') - cy.get('input.search-bar').should('have.value', '') - cy.get(".card[data-cy='item-card']").should('be.visible') - }) - - it('navigates to "/browse" with a query term', () => { - cy.get('input.search-bar').type('test') - cy.get('button.search-button').click() - cy.url().should('include', '/browse?q=test') - cy.get('input.search-bar').should('have.value', 'test') - cy.get(".card[data-cy='item-card']").should('be.visible') - }) - }) - }) -}) \ No newline at end of file +}) diff --git a/cypress/e2e/home.cy.js b/cypress/e2e/home.cy.js index cff47a0..1d967bf 100644 --- a/cypress/e2e/home.cy.js +++ b/cypress/e2e/home.cy.js @@ -1,67 +1,120 @@ -describe('Viewing Home page', () => { +describe('Navigating to the home page', () => { // declare variables that can be used to change how the response is intercepted. - let loading + let data = 'services/wares.json' let error - let featuredServices beforeEach(() => { - // Intercept the response from the endpoint to view all requests cy.customApiIntercept({ - action: 'GET', alias: 'useAllWares', - requestURL: `/providers/${Cypress.env('NEXT_PUBLIC_PROVIDER_ID')}/wares.json`, - data: featuredServices, - defaultFixture: 'services/wares.json', - emptyFixture: 'services/no-wares.json', - loading, - error + data, + error, + requestURL: '/wares.json?per_page=2000', }) + cy.visit('/') }) - - context('featured services list is loading', () => { - before(() => { - loading = true + describe('renders a search bar', () => { + it('with no query', () => { + cy.get("form[data-cy='search-bar']").should('exist').then(() => { + cy.log('Search bar renders successfully.') + }) }) - it('should show 3 placeholder cards loading', () => { - cy.get('p.placeholder-glow').should('be.visible').then(() => { - cy.log('Loading text displays correctly.') + + context('able to navigate to "/browse"', () => { + const testSetup = ({ data, defaultFixture, requestURL }) => { + cy.customApiIntercept({ + alias: 'useFilteredWares', + data, + error, + requestURL, + }) + } + + it('with a blank query', () => { + testSetup({ + data: 'services/wares.json', + requestURL: '/wares.json?per_page=2000&q=', + }) + + cy.get('button.search-button').click() + cy.url().should('include', '/browse') + cy.url().should('not.include', '?') + cy.get('input.search-bar').should('have.value', '') + cy.get(".card[data-cy='item-card']").should('be.visible') + }) + + it('with a valid query term', () => { + testSetup({ + data: 'services/filtered-wares.json', + requestURL: `/wares.json?per_page=2000&q=${Cypress.env('CYPRESS_SEARCH_QUERY')}`, + }) + + cy.get('input.search-bar').type(Cypress.env('CYPRESS_SEARCH_QUERY')) + cy.get('button.search-button').click() + cy.url().should('include', `/browse?q=${Cypress.env('CYPRESS_SEARCH_QUERY')}`) + cy.get('input.search-bar').should('have.value', Cypress.env('CYPRESS_SEARCH_QUERY')) + cy.get(".card[data-cy='item-card']").should('be.visible') + }) + + it('with an invalid query term', () => { + const invalidQuery = 'asdfghjk' + testSetup({ + data: 'services/no-wares.json', + requestURL: `/wares.json?per_page=2000&q=${invalidQuery}`, + }) + + cy.get('input.search-bar').type(invalidQuery) + cy.get('button.search-button').click() + cy.url().should('include', `/browse?q=${invalidQuery}`) + cy.get('input.search-bar').should('have.value', invalidQuery) + cy.get("p[data-cy='no-results']").should('contain', `Your search for ${invalidQuery} returned no results`) }) }) }) - context('error while making a request to the api', () => { - before(() => { - loading = false - error = true - }) - it('should show an error message.', () => { - cy.get("div[role='alert']").should('be.visible').then(() => { - cy.log('Successfully hits an error.') + describe('renders a text box', () => { + it('showing the about text.', () => { + cy.get("section[data-cy='about-us-section']").should('exist').then(() => { + cy.log('Abouttext renders successfully.') }) }) }) - context('home page components are loading successfully, &', () => { - before(() => { - featuredServices = true - error = false - }) - it('should show the search bar.', () => { - cy.get("form[data-cy='search-bar']").should('exist').then(() => { - cy.log('Search bar renders successfully.') + describe('makes a call to the api', () => { + context('which when given an invalid access token', () => { + before(() => { + data = undefined + error = { + body: { + message: 'No access token provided.', + }, + statusCode: 403, + } + }) + + it('shows an error message', () => { + cy.get("div[role='alert']").should('be.visible').then(() => { + cy.log('Successfully hits an error.') + }) + cy.get("div[role='alert']").contains('No access token provided.') }) }) - it('should show the about text.', () => { - cy.get("section[data-cy='about-us-section']").should('exist').then(() => { - cy.log('Abouttext renders successfully.') + + context('which when returns no error or data', () => { + it('shows 3 placeholder cards loading', () => { + cy.get('p.placeholder-glow').should('have.length', 3).then(() => { + cy.log('Loading text displays correctly.') + }) }) }) - it('should show the featured services cards.', () => { - cy.get("div[data-cy='item-group']").should('exist').then(() => { - cy.log('Status bar renders successfully.') + + context('which when returns data', () => { + it('shows the featured services cards', () => { + cy.get("div[data-cy='item-group']").should('exist').then(() => { + cy.log('Status bar renders successfully.') + }) }) }) }) -}) \ No newline at end of file +}) diff --git a/cypress/e2e/request.cy.js b/cypress/e2e/request.cy.js index aa83b90..a03b39f 100644 --- a/cypress/e2e/request.cy.js +++ b/cypress/e2e/request.cy.js @@ -1,12 +1,9 @@ -import useOneRequestResponseBody from '../fixtures/one-request/request.json' - -describe.skip('Viewing one request', () => { - // TODO: currently this uses a real request uuid, which would allow it to visit a route that actually existed. - // since the routes are generated dynamically, we will need to mock the next router in order to generate a route for a fake request w/ mock uuid within the test - // this test should remain skipped until the above is done since it runs as a regular e2e vs e2e with mocked data - // Existing ticket to complete this test: https://github.com/scientist-softserv/webstore/issues/218 - let uuid = useOneRequestResponseBody.uuid +import { + requestUuid as uuid, + requestPageApiCalls, +} from '../support/e2e' +describe('Viewing one request', () => { describe('as a logged out user', () => { it('should show an error message.', () => { cy.visit(`/requests/${uuid}`) @@ -17,99 +14,128 @@ describe.skip('Viewing one request', () => { }) describe('as a logged in user', () => { - // declare variables that can be used to change how the response is intercepted. - let request - let proposals - let messages - let files - let loading - let error + let apiCalls = Object.assign({}, requestPageApiCalls) beforeEach(() => { - // Call the custom cypress command to log in cy.login(Cypress.env('TEST_SCIENTIST_USER'), Cypress.env('TEST_SCIENTIST_PW')) - - // Intercept the response from the endpoint to view one request - cy.customApiIntercept({ - action: 'GET', - alias: 'useOneRequest', - requestURL: `/quote_groups/${uuid}.json`, - data: request, - dataFixture: 'one-request/request.json', - emptyDataFixture: 'empty.json', - loading, - error - }) - cy.customApiIntercept({ - action: 'GET', - alias: 'useAllSOWs', - requestURL: `/quote_groups/${uuid}/proposals.json`, - data: proposals, - dataFixture: 'one-request/proposals.json', - emptyDataFixture: 'empty.json', - loading, - error - }) - - cy.customApiIntercept({ - action: 'GET', - alias: 'useAllMessages', - requestURL: `/quote_groups/${uuid}/messages.json`, - data: messages, - dataFixture: 'one-request/messages.json', - emptyDataFixture: 'empty.json', - loading, - error - }) - - cy.customApiIntercept({ - action: 'GET', - alias: 'useAllFiles', - requestURL: `/quote_groups/${uuid}/notes.json`, - data: files, - dataFixture: 'one-request/notes.json', - emptyDataFixture: 'empty.json', - loading, - error + Object.entries(apiCalls).forEach((item) => { + cy.customApiIntercept(item[1]) }) cy.visit(`/requests/${uuid}`) }) - context('request is loading', () => { - before(() => { - loading = true - }) - it('should show a loading spinner.', () => { - cy.get("[aria-label='tail-spin-loading']").should('be.visible').then(() => { - cy.log('Loading spinner displays correctly.') + afterEach(() => { + // in order for the tests to not be order dependent, we need to reset the apiCalls object to the original state + apiCalls = Object.assign({}, requestPageApiCalls) + }) + + describe('makes a call to the api', () => { + context('which when given an invalid uuid', () => { + before(() => { + apiCalls['useOneRequest'] = { + ...apiCalls['useOneRequest'], + data: undefined, + error: { + body: { + message: 'Quote Group Not Found', + }, + statusCode: 404, + }, + } + }) + + it('returns an error message', () => { + cy.get("div[role='alert']").should('be.visible').then(() => { + cy.log('Successfully hits an error.') + }) + cy.get("div[role='alert']").contains('Quote Group Not Found') }) }) - }) - describe('request page components are loading successfully, &', () => { - context('the request page', () => { + context('which when returns undefined error and data values', () => { before(() => { - loading = - request = true - proposals = true - messages = true - files = true + Object.entries(apiCalls).forEach(([key, value]) => { + apiCalls[key] = { + ...value, + data: undefined, + error: undefined, + } + }) + }) + + it('shows a loading spinner.', () => { + cy.get("[aria-label='tail-spin-loading']").should('be.visible').then(() => { + cy.log('Loading spinner displays correctly.') + }) }) + }) - it("should show the request stats section.", () => { - cy.get('div.request-stats-card').should('exist').then(() => { + describe('which when returns request data', () => { + it('shows the request stats section', () => { + cy.get('div.request-stats.card').should('exist').then(() => { cy.log('Request stats section renders successfully.') }) }) - it("should show the status bar.", () => { + it('shows the status bar', () => { cy.get("div[data-cy='status-bar']").should('exist').then(() => { cy.log('Status bar renders successfully.') }) }) - // TODO: add tests to confirm that messages, files, additional info, document sections all show correctly. + + context('with messages', () => { + before(() => { + apiCalls['useMessages'] = { + ...apiCalls['useMessages'], + data: 'one-request/messages/index.json', + } + }) + + it('displays the messages', () => { + cy.get('div.card-body p.card-text') + .contains('this is a message from the customer') + .should('be.visible') + }) + }) + + context('with documents', () => { + before(() => { + apiCalls['useAllSOWs'] = { + ...apiCalls['useAllSOWs'], + data: 'one-request/sows/index.json', + } + apiCalls['getAllPOs'] = { + ...apiCalls['getAllPOs'], + data: 'one-request/pos/index.json', + } + }) + + it('displays the documents', () => { + cy.get('div.document').should('have.length', 2) + cy.get('div.badge').contains('SOW').should('be.visible') + cy.get('div.badge').contains('PO').should('be.visible') + }) + }) + + context('with files', () => { + before(() => { + apiCalls['useFiles'] = { + ...apiCalls['useFiles'], + data: 'one-request/files/index.json', + } + }) + + it('displays the files', () => { + cy.get('div.actions-group') + .contains('View Files') + .click() + cy.get('div#document-tabs-tabpane-files') + .contains('downtown.jpg') + .should('be.visible') + }) + }) }) }) }) -}) \ No newline at end of file +}) diff --git a/cypress/e2e/requests.cy.js b/cypress/e2e/requests.cy.js index 9217f4d..4b32557 100644 --- a/cypress/e2e/requests.cy.js +++ b/cypress/e2e/requests.cy.js @@ -1,108 +1,103 @@ -import { scientistApiBaseURL } from '../support/e2e' - describe('Viewing all requests', () => { describe('as a logged out user', () => { - it('should show an error message.', () => { - // Visit a protected route in order to allow cypress to set the cookie and mock the login + it('shows an error message.', () => { cy.visit('/requests') cy.get('div.alert-heading').contains('Unauthorized').then(() => { cy.log('A logged out user is not able to view requests.') }) }) }) - + describe('as a logged in user', () => { // declare variables that can be used to change how the response is intercepted. - let requestList - let loading + let data let error beforeEach(() => { - // Call the custom cypress command to log in cy.login(Cypress.env('TEST_SCIENTIST_USER'), Cypress.env('TEST_SCIENTIST_PW')) - // Intercept the response from the endpoint to view all requests - cy.customApiIntercept({ - action: 'GET', - alias: 'useAllRequests', - requestURL: `/quote_groups/mine.json`, - data: requestList, - defaultFixture: 'all-requests/requests.json', - emptyFixture: 'all-requests/no-requests.json', - loading, - error - }) - // Intercept the response from the endpoint that gets the default ware ID - cy.customApiIntercept({ - action: 'GET', - alias: 'useDefaultWare', - requestURL: `/wares.json?q=make-a-request`, - defaultFixture: 'all-requests/make-a-request.json', - error - }) - cy.visit('/requests') }) + describe('makes a call to the api', () => { + beforeEach(() => { + cy.customApiIntercept({ + alias: 'useAllRequests', + data, + error, + requestURL: `/quote_groups/mine.json`, + }) - context('request list is loading', () => { - before(() => { - loading = true + cy.visit('/requests') }) - it('should show a loading spinner.', () => { - cy.get("[aria-label='tail-spin-loading']").should('be.visible').then(() => { - cy.log('Loading spinner displays correctly.') + + context('which when given an invalid access token', () => { + before(() => { + error = { + body: { + message: 'No access token provided.', + }, + statusCode: 403, + } }) - }) - }) - context('error while making a request to the api', () => { - before(() => { - requestList = undefined - loading = false - error = true - }) - it('should show an error message.', () => { - cy.get("div[role='alert']").should('be.visible').then(() => { - cy.log('Successfully hits an error.') + it('shows an error message.', () => { + cy.get("div[role='alert']").should('be.visible').then(() => { + cy.log('Successfully hits an error.') + }) + cy.get("div[role='alert']").contains('No access token provided.') }) }) - }) - describe('request components are loading successfully, &', () => { - context('the user has requests', () => { - before(() => { - requestList = true - error = false - }) - it("should show the user's request list.", () => { - cy.get('article.request-item').should('exist').then(() => { - cy.log('Successfully viewing request list.') + context('which when returns undefined error and data values', () => { + it('shows a loading spinner.', () => { + cy.get("[aria-label='tail-spin-loading']").should('be.visible').then(() => { + cy.log('Loading spinner displays correctly.') }) }) }) - context('the user has 0 requests', () => { + describe('which when returns a data object', () => { before(() => { - requestList = false + data = 'all-requests/requests.json' + cy.customApiIntercept({ + alias: 'useDefaultWare', + data: 'all-requests/make-a-request.json', + error, + requestURL: '/wares.json', + }) + }) + + it('renders the "New Request" button for the default service', () => { + cy.get("a[data-cy='linked-button']") + .should('have.attr', 'href', `/requests/new/make-a-request?id=123`) + .and('have.text', 'Initiate a New Request') + .then(() => { + cy.log('The component displays correctly') + }) }) - it("should show a message notifying the user they don't have any requests.", () => { - cy.get('p.no-requests').contains('You do not have any requests yet.').then(() => { - cy.log('Successfully viewing request page with no requests.') + + context('with values', () => { + it("shows the user's request list.", () => { + cy.get('article.request-item') + .should('exist') + .and('have.length', 3) + .then(() => { + cy.log('Successfully viewing request list.') + }) }) }) - }) - context('the user can see the component', () => { - [true, false].forEach((value) => { + context('with no values', () => { before(() => { - requestList = value + data = 'all-requests/no-requests.json' }) - it(`should show a button that links to the initialize request page for the default ware ${value ? 'with a request list' : 'with 0 requests'}.`, () => { - cy.get("a[data-cy='linked-button']").should('have.attr', 'href', `/requests/new/make-a-request?id=123`).then(() => { - cy.log('The component displays correctly') + + it("shows a message notifying the user they don't have any requests.", () => { + cy.get('p.no-requests').contains('You do not have any requests yet.').then(() => { + cy.log('Successfully viewing request page with no requests.') }) }) }) }) }) }) -}) \ No newline at end of file +}) diff --git a/cypress/fixtures/all-requests/make-a-request.json b/cypress/fixtures/all-requests/make-a-request.json index 6c36641..3a3c38e 100644 --- a/cypress/fixtures/all-requests/make-a-request.json +++ b/cypress/fixtures/all-requests/make-a-request.json @@ -1,7 +1,8 @@ { "ware_refs": [ { - "id": 123 + "id": 123, + "slug": "make-a-request" } ] -} \ No newline at end of file +} diff --git a/cypress/fixtures/one-request/files.json b/cypress/fixtures/one-request/files.json deleted file mode 100644 index 5c8fd4b..0000000 --- a/cypress/fixtures/one-request/files.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "notes": [ - { - "id": 674804, - "title": null, - "status": null, - "body": "hi", - "created_by": "Summer Cook", - "created_at": "2023-03-01T22:16:46.167Z", - "updated_at": "2023-03-01T22:16:46.167Z", - "attachments": [], - "user_ref": { - "first_name": "Summer", - "last_name": "Cook", - "organization_name": "Acme", - "email": "summer@scientist.com", - "title": "Frontend Dev", - "company": "", - "image": "https://avatars.scientist.com/avatars/0d93b3808f701fc3dbde5002a80c2475/S C/xs?time=1677774999" - } - } - ] -} diff --git a/cypress/fixtures/one-request/files/default.json b/cypress/fixtures/one-request/files/default.json new file mode 100644 index 0000000..8de43c7 --- /dev/null +++ b/cypress/fixtures/one-request/files/default.json @@ -0,0 +1,3 @@ +{ + "notes": [] +} diff --git a/cypress/fixtures/one-request/files/index.json b/cypress/fixtures/one-request/files/index.json new file mode 100644 index 0000000..15a6143 --- /dev/null +++ b/cypress/fixtures/one-request/files/index.json @@ -0,0 +1,32 @@ +{ + "notes": [ + { + "id": 5019371, + "title": "New Attachment", + "status": "Other File", + "body": null, + "created_by": "Alisha Evans", + "created_at": "2024-02-14T19:40:47.759Z", + "updated_at": "2024-02-14T19:40:47.759Z", + "attachments": [ + { + "uuid": "fa70a15b-3230-41d5-9913-93ee0bcf2e3e", + "filename": "downtown.jpg", + "content_type": "image/jpeg", + "content_length": 326048, + "created_at": "2024-02-14T19:40:47.769Z", + "download": "/quote_groups/596127b7-2356-45aa-aec4-a4f8608ae755/attachments/downtown.jpg" + } + ], + "user_ref": { + "first_name": "Alisha", + "last_name": "Evans", + "organization_name": "Acme", + "email": "user@example.com", + "title": "software engineer", + "company": "Triage Pharmaceuticals [DEMO], Scientist.com", + "image": "https://avatars.scientist.com/avatars/alpha/user/xs?time=1707939658" + } + } + ] +} diff --git a/cypress/fixtures/one-request/messages/default.json b/cypress/fixtures/one-request/messages/default.json new file mode 100644 index 0000000..ab71998 --- /dev/null +++ b/cypress/fixtures/one-request/messages/default.json @@ -0,0 +1,3 @@ +{ + "messages": [] +} diff --git a/cypress/fixtures/one-request/messages.json b/cypress/fixtures/one-request/messages/index.json similarity index 97% rename from cypress/fixtures/one-request/messages.json rename to cypress/fixtures/one-request/messages/index.json index 222addb..0adbe19 100644 --- a/cypress/fixtures/one-request/messages.json +++ b/cypress/fixtures/one-request/messages/index.json @@ -54,7 +54,7 @@ "id": 674804, "title": null, "status": null, - "body": "hi", + "body": "this is a message from the customer", "created_by": "Summer Cook", "created_at": "2023-03-01T22:16:46.167Z", "updated_at": "2023-03-01T22:16:46.167Z", diff --git a/cypress/fixtures/one-request/pos/default.json b/cypress/fixtures/one-request/pos/default.json new file mode 100644 index 0000000..d4adc6a --- /dev/null +++ b/cypress/fixtures/one-request/pos/default.json @@ -0,0 +1,4 @@ +{ + "total": 0, + "purchase_orders": [] +} diff --git a/cypress/fixtures/one-request/pos/index.json b/cypress/fixtures/one-request/pos/index.json new file mode 100644 index 0000000..c7efdfa --- /dev/null +++ b/cypress/fixtures/one-request/pos/index.json @@ -0,0 +1,37 @@ +{ + "total": 1, + "purchase_orders": [ + { + "id": 168795, + "po_number": "PO595363", + "retail_total_price": "1625.0", + "retail_subtotal_price": "1425.0", + "wholesale_total_price": "1553.75", + "wholesale_subtotal_price": "1353.75", + "tax_cost": "0.0", + "shipping_cost": "200.0", + "retail_total_price_currency": "$1,625.00", + "retail_subtotal_price_currency": "$1,425.00", + "wholesale_total_price_currency": "$1,553.75", + "wholesale_subtotal_price_currency": "$1,353.75", + "tax_cost_currency": "$0.00", + "shipping_cost_currency": "$200.00", + "currency": "USD", + "currency_unit": "$", + "provider_name": "Beachside Biotechnology Services [Demo]", + "status": "Work in Progress", + "status_description": "(SOW 595363)", + "created_at": "2023-12-15T19:45:33.745Z", + "updated_at": "2023-12-15T19:45:35.408Z", + "type": "PO", + "identifier": null, + "turn_around_time": { + "id": 890642, + "min": 604800, + "max": 1814400, + "display_units": "weeks", + "human": "1 - 3 weeks" + } + } + ] +} diff --git a/cypress/fixtures/one-request/pos/one.json b/cypress/fixtures/one-request/pos/one.json new file mode 100644 index 0000000..023bb5a --- /dev/null +++ b/cypress/fixtures/one-request/pos/one.json @@ -0,0 +1,199 @@ +{ + "purchase_order": { + "id": 168795, + "po_number": "PO595363", + "retail_total_price": "1625.0", + "retail_subtotal_price": "1425.0", + "wholesale_total_price": "1553.75", + "wholesale_subtotal_price": "1353.75", + "tax_cost": "0.0", + "shipping_cost": "200.0", + "retail_total_price_currency": "$1,625.00", + "retail_subtotal_price_currency": "$1,425.00", + "wholesale_total_price_currency": "$1,553.75", + "wholesale_subtotal_price_currency": "$1,353.75", + "tax_cost_currency": "$0.00", + "shipping_cost_currency": "$200.00", + "currency": "USD", + "currency_unit": "$", + "provider_name": "Beachside Biotechnology Services [Demo]", + "status": "Work in Progress", + "status_description": "(SOW 595363)", + "created_at": "2023-12-15T19:45:33.745Z", + "updated_at": "2023-12-15T19:45:35.408Z", + "type": "PO", + "identifier": null, + "turn_around_time": { + "id": 890642, + "min": 604800, + "max": 1814400, + "display_units": "weeks", + "human": "1 - 3 weeks" + }, + "scientist_identifier": null, + "quote_group_name": "65659A: New Request", + "quote_group_identifier": "65659A", + "quoted_ware_uuids": [ + "6a96b904-1032-42cb-8083-4afac941f939" + ], + "quoted_ware_ids": [ + 168795 + ], + "quoted_ware_uuid": "6a96b904-1032-42cb-8083-4afac941f939", + "quoted_ware_id": 168795, + "study_objective": "This is a test proposal for the service requested", + "payment_terms": "NET 60", + "ship_to": { + "id": 3512146, + "organization_name": "beachsidebiotech", + "street": "asdf", + "street2": "", + "city": "asdf", + "state": "asdf", + "zipcode": "asdf", + "country": "Kazakhstan", + "latitude": null, + "longitude": null, + "name": null, + "attention": null, + "person_name": "Alisha Evans", + "care_of": null, + "text": "asdf\nasdf, asdf, asdf" + }, + "ship_from": { + "id": 3512148, + "organization_name": "Beachside Biotechnology Services", + "street": "10", + "street2": "", + "city": "San Diego", + "state": "CA", + "zipcode": "91910", + "country": "US", + "latitude": "32.6385134", + "longitude": "-117.0617553", + "name": null, + "attention": null, + "person_name": null, + "care_of": null, + "text": "10\nSan Diego, CA, 91910\nUnited States" + }, + "line_items": [ + { + "id": 1511411, + "retail_unit_price_currency": "$300.00", + "retail_unit_price": "300.0", + "retail_subtotal_price_currency": "$300.00", + "retail_subtotal_price": "300.0", + "quantity": "1.0", + "currency": "USD", + "currency_unit": "$", + "name": "Study Design", + "subtotal": "$300.00", + "unit_price": "$300.00" + }, + { + "id": 1511412, + "retail_unit_price_currency": "$150.00", + "retail_unit_price": "150.0", + "retail_subtotal_price_currency": "$600.00", + "retail_subtotal_price": "600.0", + "quantity": "4.0", + "currency": "USD", + "currency_unit": "$", + "name": "Analysis (3 samples plus control)", + "subtotal": "$600.00", + "unit_price": "$150.00" + }, + { + "id": 1511413, + "retail_unit_price_currency": "$500.00", + "retail_unit_price": "500.0", + "retail_subtotal_price_currency": "$500.00", + "retail_subtotal_price": "500.0", + "quantity": "1.0", + "currency": "USD", + "currency_unit": "$", + "name": "Report", + "subtotal": "$500.00", + "unit_price": "$500.00" + }, + { + "id": 1511414, + "retail_unit_price_currency": "$25.00", + "retail_unit_price": "25.0", + "retail_subtotal_price_currency": "$25.00", + "retail_subtotal_price": "25.0", + "quantity": "1.0", + "currency": "USD", + "currency_unit": "$", + "name": "Disposal", + "subtotal": "$25.00", + "unit_price": "$25.00" + } + ], + "proposal_refs": [ + { + "id": 4594, + "type": "SOW", + "identifier": "092985", + "retail_total_price": "1625.0", + "retail_subtotal_price": "1425.0", + "wholesale_total_price": "1553.75", + "wholesale_subtotal_price": "1353.75", + "tax_cost": "0.0", + "shipping_cost": "200.0", + "retail_total_price_currency": "$1,625.00", + "retail_subtotal_price_currency": "$1,425.00", + "wholesale_total_price_currency": "$1,553.75", + "wholesale_subtotal_price_currency": "$1,353.75", + "tax_cost_currency": "$0.00", + "shipping_cost_currency": "$200.00", + "currency": "USD", + "currency_unit": "$", + "provider_name": "Beachside Biotechnology Services [Demo]", + "status": "Selected for Work", + "status_description": "This Proposal has been selected for work", + "created_at": "2023-04-14T16:09:35.286Z", + "updated_at": "2023-12-15T19:45:33.256Z", + "turn_around_time": { + "id": 827275, + "min": 604800, + "max": 1814400, + "display_units": "weeks", + "human": "1 - 3 weeks" + } + } + ], + "proposal_ref": { + "id": 158535, + "type": "SOW", + "identifier": "595363", + "retail_total_price": "1625.0", + "retail_subtotal_price": "1425.0", + "wholesale_total_price": "1553.75", + "wholesale_subtotal_price": "1353.75", + "tax_cost": "0.0", + "shipping_cost": "200.0", + "retail_total_price_currency": "$1,625.00", + "retail_subtotal_price_currency": "$1,425.00", + "wholesale_total_price_currency": "$1,553.75", + "wholesale_subtotal_price_currency": "$1,353.75", + "tax_cost_currency": "$0.00", + "shipping_cost_currency": "$200.00", + "currency": "USD", + "currency_unit": "$", + "provider_name": "Beachside Biotechnology Services [Demo]", + "status": "Selected for Work", + "status_description": "This Proposal has been selected for work", + "created_at": "2023-04-14T16:09:35.286Z", + "updated_at": "2023-12-15T19:45:33.256Z", + "turn_around_time": { + "id": 827275, + "min": 604800, + "max": 1814400, + "display_units": "weeks", + "human": "1 - 3 weeks" + } + } + } +} diff --git a/cypress/fixtures/one-request/sows/default.js b/cypress/fixtures/one-request/sows/default.js new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/cypress/fixtures/one-request/sows/default.js @@ -0,0 +1 @@ +[] diff --git a/cypress/fixtures/one-request/proposals.json b/cypress/fixtures/one-request/sows/index.json similarity index 100% rename from cypress/fixtures/one-request/proposals.json rename to cypress/fixtures/one-request/sows/index.json diff --git a/cypress/fixtures/services/filtered-wares.json b/cypress/fixtures/services/filtered-wares.json new file mode 100644 index 0000000..4ac4391 --- /dev/null +++ b/cypress/fixtures/services/filtered-wares.json @@ -0,0 +1,22 @@ +{ + "ware_refs": [ + { + "id": 3456, + "slug": "test-ware", + "name": "Test Ware", + "snippet": "Here is a test ware snippet.", + "urls": { + "promo_image": "https://y.yarn.co/193fa4ae-a245-4f7a-ac9d-64bbebb18c8d_screenshot.jpg" + } + }, + { + "id": 4567, + "slug": "another-test-ware", + "name": "Another Test Ware", + "snippet": "Another test snippet.", + "urls": { + "promo_image": "https://cdn.drawception.com/images/panels/2017/7-2/jtqKRKSpyj-6.png" + } + } + ] +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index dcf2c9a..c51cae9 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,69 +1,40 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) import { scientistApiBaseURL } from './e2e' // add a command to login that uses a session, so the user will remain logged in throughout the test file vs. needing to log in before each example. // source: https://github.com/nextauthjs/next-auth/discussions/2053#discussioncomment-1191016 Cypress.Commands.add('login', (username, password) => { cy.session([username, password], () => { - cy.intercept("/api/auth/session", { fixture: "session.json" }).as("session"); + cy.intercept('/api/auth/session', { fixture: 'session.json' }).as('session') - // Set the cookie for cypress. - // It has to be a valid cookie so next-auth can decrypt it and confirm its validity. - // This cookie also may need to be refreshed intermittently if it expires - cy.setCookie("next-auth.session-token", Cypress.env('TEST_SESSION_COOKIE')); + // Set the cookie for cypress. + // It has to be a valid cookie so next-auth can decrypt it and confirm its validity. + // This cookie also may need to be refreshed intermittently if it expires + // TODO(alishaevn): https://github.com/scientist-softserv/webstore/issues/375 + cy.setCookie('next-auth.session-token', Cypress.env('TEST_SESSION_COOKIE')) }) }) -// intercepts requests and creates potential cases for loading, error, data, and empty data -// required params are action, defaultFixture, requestURL -// optional params such as data, loading, and error can be passed depending on the creation of test cases that are related to that specific api call +/** + * This command intercepts requests and returns the given stubbed response + * + * @param {string} alias - the alias to give the intercept (convention is to + * use the function name) + * @param {string} data - the fixture to return as the response data + * @param {object} error - the error object to return as the response error + * @param {string} requestURL - the URL to intercept + * + * @returns {object} - the stubbed response + */ Cypress.Commands.add('customApiIntercept', ({ - action, alias, data, defaultFixture, emptyFixture, error, errorCaseStatusCode, loading, requestURL + alias, data, error, requestURL }) => { - cy.intercept(action, scientistApiBaseURL + requestURL, (req) => { - switch (true) { - // reply with an empty response: both data and error will be undefined. - case loading: req.reply() - break - - // error will be defined - case error: req.reply({ statusCode: errorCaseStatusCode || 500 }) - break - - // reply with a request body- default status code is 200 - case data: req.reply({ fixture: defaultFixture }) - break - - // reply with the empty fixture is there is one, and the default as a backup. Allows us to isolate one api call at a time that may potentially respond with empty data. - case !data: req.reply({ fixture: emptyFixture || defaultFixture }) - break - - default: req.reply({ fixture: defaultFixture }) - break + cy.intercept(`${scientistApiBaseURL}${requestURL}`, (req) => { + const response = { + data: data && { fixture: data }, + error, } + + // falling back to an empty object mimics the loading state + return req.reply(response.data || response.error || {}) }).as(alias || 'customIntercept') -}) \ No newline at end of file +}) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index b6a7ca5..cc9ffe2 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -16,4 +16,47 @@ // Import commands.js using ES2015 syntax: import './commands' -export const scientistApiBaseURL = `https://${Cypress.env('NEXT_PUBLIC_PROVIDER_NAME')}.scientist.com/api/v2` \ No newline at end of file +export const scientistApiBaseURL = `https://${Cypress.env('NEXT_PUBLIC_PROVIDER_NAME')}.scientist.com/api/v2` + +let error +const quotedWareId = 728152 +export const requestUuid = '596127b7-2356-45aa-aec4-a4f8608ae755' +export const requestPageApiCalls = { + 'useOneRequest': { + alias: 'useOneRequest', + data: 'one-request/request.json', + error, + requestURL: `/quote_groups/${requestUuid}.json`, + }, + 'useAllSOWs': { + alias: 'useAllSOWs', + data: 'one-request/sows/default.js', + error, + requestURL: `/quote_groups/${requestUuid}/proposals.json` + }, + 'useMessages': { + alias: 'useMessages', + data: 'one-request/messages/default.json', + error, + requestURL: `/quote_groups/${requestUuid}/messages.json` + }, + 'useFiles': { + alias: 'useFiles', + data: 'one-request/files/default.json', + error, + requestURL: `/quote_groups/${requestUuid}/notes.json`, + }, + 'getAllPOs': { + alias: 'getAllPOs', + data: 'one-request/pos/default.json', + error, + requestURL: `/quote_groups/${requestUuid}/quoted_wares/${quotedWareId}/purchase_orders.json`, + }, + 'getOnePO': { + alias: 'getOnePO', + data: 'one-request/pos/one.json', + error: undefined, + requestURL: `/quote_groups/${requestUuid}/quoted_wares/${quotedWareId}/purchase_orders/168795.json`, + } +} + diff --git a/package.json b/package.json index d41a000..74e80a4 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "cypress:e2e": "start-server-and-test dev http://localhost:3000 \"cypress open --e2e --browser electron\"", "cypress:headless:e2e": "start-server-and-test dev http://localhost:3000 \"cypress run --e2e --browser electron\"", "dev": "next dev", - "lint": "next lint --dir pages --dir utils", - "lint:fix": "next lint --dir pages --dir utils --fix", + "lint": "next lint --dir pages --dir utils --dir cypress/e2e", + "lint:fix": "next lint --dir pages --dir utils --dir cypress/e2e --fix", "jest": "jest", "jest-watch": "jest --watch", "release": "release-it", diff --git a/pages/requests/[uuid].js b/pages/requests/[uuid].js index addbb78..003785e 100644 --- a/pages/requests/[uuid].js +++ b/pages/requests/[uuid].js @@ -196,7 +196,7 @@ const Request = ({ session }) => { {documents.length ? documents.map((document, index) => ( <Document - addClass='mt-3' + addClass='mt-3 document' acceptSOW={() => handleAcceptingSOWandCreatingPO(document)} document={document} key={`${request.id}-${index}`} @@ -209,7 +209,7 @@ const Request = ({ session }) => { text='No documents have been submitted.' /> )} - <Messages addClass='mt-4' messages={messages} /> + <Messages addClass='mt-4 messages' messages={messages} /> </div> </div> </div> diff --git a/pages/requests/new/[ware].js b/pages/requests/new/[ware].js index 32b8313..5b47fb3 100644 --- a/pages/requests/new/[ware].js +++ b/pages/requests/new/[ware].js @@ -3,6 +3,7 @@ import { default as BsForm } from 'react-bootstrap/Form' import Form from '@rjsf/core' import validator from '@rjsf/validator-ajv8' import { useRouter } from 'next/router' +import { signIn } from 'next-auth/react' import { AdditionalInfo, BlankRequestForm, @@ -266,7 +267,8 @@ const SignInRequired = () => ( <Notice addClass='mt-5' alert={{ - body: ['To proceed with making a request, please log in to your account.'], + body: [<p key='signin-link'>To proceed with making a request, <a href='#' onClick={() => signIn + (process.env.NEXT_PUBLIC_PROVIDER_NAME)}>please log in</a> to your account.</p>], title: 'Sign in required', variant: 'info' }} diff --git a/utils/api/base.js b/utils/api/base.js index b16c0ab..1ae2a44 100644 --- a/utils/api/base.js +++ b/utils/api/base.js @@ -11,6 +11,7 @@ export const fetcher = (url, token) => { .then(res => res.data) .catch(error => { Sentry.captureException(error) + throw error }) } diff --git a/utils/api/requests.js b/utils/api/requests.js index 057d8bc..3e5355f 100644 --- a/utils/api/requests.js +++ b/utils/api/requests.js @@ -42,8 +42,8 @@ export const useOneRequest = (uuid, accessToken) => { } } -export const useAllSOWs = (id, requestIdentifier, accessToken) => { - const { data, error } = useSWR(accessToken ? [`/quote_groups/${id}/proposals.json`, accessToken] : null) +export const useAllSOWs = (uuid, requestIdentifier, accessToken) => { + const { data, error } = useSWR(accessToken ? [`/quote_groups/${uuid}/proposals.json`, accessToken] : null) let allSOWs if (data) { allSOWs = configureSOWs(data, requestIdentifier) @@ -84,8 +84,8 @@ export const getAllPOs = async (quotedWareId, uuid, requestIdentifier, accessTok } } -export const useMessages = (requestUuid, accessToken) => { - const { data, error, mutate } = useSWR(accessToken ? [`/quote_groups/${requestUuid}/messages.json`, accessToken] : null) +export const useMessages = (uuid, accessToken) => { + const { data, error, mutate } = useSWR(accessToken ? [`/quote_groups/${uuid}/messages.json`, accessToken] : null) let messages if (data) { messages = configureMessages(data.messages) @@ -102,11 +102,11 @@ export const useMessages = (requestUuid, accessToken) => { } } -export const useFiles = (id, accessToken) => { - const { data, error, mutate } = useSWR(accessToken ? [`/quote_groups/${id}/notes.json`, accessToken] : null) +export const useFiles = (uuid, accessToken) => { + const { data, error, mutate } = useSWR(accessToken ? [`/quote_groups/${uuid}/notes.json`, accessToken] : null) let files if (data) { - files = configureFiles(data.notes) + files = configureFiles(data.notes) } return {