diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a80059637..27cb7534b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,8 +38,11 @@ jobs: run: cd front && npm run lint - name: Build Storybook run: cd front && npm run build-storybook --quiet - - name: Serve Storybook and run tests + - name: Serve Storybook and run storybook tests run: | cd front && npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ "npx http-server storybook-static --silent --port 6006" \ - "npm run coverage" \ No newline at end of file + "npm run coverage" + - name: run jest tests + run: | + cd front && npm run test \ No newline at end of file diff --git a/front/package.json b/front/package.json index 4f6d3e841..edc64559e 100644 --- a/front/package.json +++ b/front/package.json @@ -33,7 +33,7 @@ "storybook": "storybook dev -p 6006 -s ../public", "test-storybook": "test-storybook", "build-storybook": "storybook build -s public", - "coverage": "test-storybook --coverage && npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook --check-coverage --lines 50", + "coverage": "test-storybook --coverage && npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook --check-coverage", "graphql:generate": "REACT_APP_GRAPHQL_ADMIN_SECRET=$REACT_APP_GRAPHQL_ADMIN_SECRET graphql-codegen --config codegen.js" }, "eslintConfig": { @@ -122,5 +122,9 @@ }, "msw": { "workerDirectory": "public" + }, + "nyc": { + "lines": 60, + "statements": 60 } } diff --git a/front/src/components/table/table-header/SortAndFilterBar.tsx b/front/src/components/table/table-header/SortAndFilterBar.tsx index 6b0a62985..b5e778221 100644 --- a/front/src/components/table/table-header/SortAndFilterBar.tsx +++ b/front/src/components/table/table-header/SortAndFilterBar.tsx @@ -60,7 +60,7 @@ function SortAndFilterBar({ key={sort.key} labelValue={sort.label} id={sort.key} - icon={sort.order === 'asc' ? : } + icon={sort.order === 'desc' ? : } onRemove={() => onRemoveSort(sort.key)} /> ); diff --git a/front/src/interfaces/entities/user.interface.ts b/front/src/interfaces/entities/user.interface.ts index 363604dd8..e6e791f4e 100644 --- a/front/src/interfaces/entities/user.interface.ts +++ b/front/src/interfaces/entities/user.interface.ts @@ -24,7 +24,7 @@ export type GraphqlMutationUser = { id: string; email?: string; displayName?: string; - workspaceMember_id?: string; + workspaceMemberId?: string; __typename: string; }; @@ -42,6 +42,6 @@ export const mapToGqlUser = (user: User): GraphqlMutationUser => ({ id: user.id, email: user.email, displayName: user.displayName, - workspaceMember_id: user.workspaceMember?.id, + workspaceMemberId: user.workspaceMember?.id, __typename: 'users', }); diff --git a/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx b/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx new file mode 100644 index 000000000..37887fb0e --- /dev/null +++ b/front/src/pages/companies/__stories__/Companies.filterBy.stories.tsx @@ -0,0 +1,74 @@ +import { expect } from '@storybook/jest'; +import type { Meta } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import Companies from '../Companies'; +import { Story } from './Companies.stories'; +import { mocks, render } from './shared'; + +const meta: Meta = { + title: 'Pages/Companies', + component: Companies, +}; + +export default meta; + +export const FilterByName: Story = { + render, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const filterButton = canvas.getByText('Filter'); + await userEvent.click(filterButton); + + const nameFilterButton = canvas.getByText('Name', { selector: 'li' }); + await userEvent.click(nameFilterButton); + + const nameInput = canvas.getByPlaceholderText('Name'); + await userEvent.type(nameInput, 'Air', { + delay: 200, + }); + + expect(await canvas.findByText('Airbnb')).toBeInTheDocument(); + expect(await canvas.findByText('Aircall')).toBeInTheDocument(); + await expect(canvas.queryAllByText('Qonto')).toStrictEqual([]); + + expect(await canvas.findByText('Name:')).toBeInTheDocument(); + expect(await canvas.findByText('Contains Air')).toBeInTheDocument(); + }, + parameters: { + msw: mocks, + }, +}; + +export const FilterByAccountOwner: Story = { + render, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const filterButton = canvas.getByText('Filter'); + await userEvent.click(filterButton); + + const accountOwnerFilterButton = canvas.getByText('Account Owner', { + selector: 'li', + }); + await userEvent.click(accountOwnerFilterButton); + + const accountOwnerNameInput = canvas.getByPlaceholderText('Account Owner'); + await userEvent.type(accountOwnerNameInput, 'Char', { + delay: 200, + }); + + const charlesChip = canvas.getByText('Charles Test', { selector: 'li' }); + await userEvent.click(charlesChip); + + expect(await canvas.findByText('Airbnb')).toBeInTheDocument(); + await expect(canvas.queryAllByText('Qonto')).toStrictEqual([]); + + expect(await canvas.findByText('Account Owner:')).toBeInTheDocument(); + expect(await canvas.findByText('Is Charles Test')).toBeInTheDocument(); + }, + parameters: { + msw: mocks, + }, +}; diff --git a/front/src/pages/companies/__stories__/Companies.mdx b/front/src/pages/companies/__stories__/Companies.mdx new file mode 100644 index 000000000..1c85ce2ea --- /dev/null +++ b/front/src/pages/companies/__stories__/Companies.mdx @@ -0,0 +1,11 @@ +{ /* Companies.mdx */ } + +import { Canvas, Meta } from '@storybook/blocks'; + +import * as Companies from './Companies.stories'; + + + +# Companies View + + \ No newline at end of file diff --git a/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx b/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx new file mode 100644 index 000000000..f391ebd1d --- /dev/null +++ b/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx @@ -0,0 +1,45 @@ +import { expect } from '@storybook/jest'; +import type { Meta } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import Companies from '../Companies'; +import { Story } from './Companies.stories'; +import { mocks, render } from './shared'; + +const meta: Meta = { + title: 'Pages/Companies', + component: Companies, +}; + +export default meta; + +export const SortByName: Story = { + render, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const sortButton = canvas.getByText('Sort'); + await userEvent.click(sortButton); + + const nameSortButton = canvas.getByText('Name', { selector: 'li' }); + await userEvent.click(nameSortButton); + + expect(await canvas.getByTestId('remove-icon-name')).toBeInTheDocument(); + + expect(await canvas.findByText('Airbnb')).toBeInTheDocument(); + + expect( + (await canvas.findAllByRole('checkbox')).map((item) => { + return item.getAttribute('id'); + })[1], + ).toStrictEqual('company-selected-89bb825c-171e-4bcc-9cf7-43448d6fb278'); + + const cancelButton = canvas.getByText('Cancel'); + await userEvent.click(cancelButton); + + await expect(canvas.queryAllByTestId('remove-icon-name')).toStrictEqual([]); + }, + parameters: { + msw: mocks, + }, +}; diff --git a/front/src/pages/companies/__stories__/Companies.stories.tsx b/front/src/pages/companies/__stories__/Companies.stories.tsx new file mode 100644 index 000000000..9441c91b4 --- /dev/null +++ b/front/src/pages/companies/__stories__/Companies.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Companies from '../Companies'; + +import { render, mocks } from './shared'; + +const meta: Meta = { + title: 'Pages/Companies', + component: Companies, +}; + +export default meta; + +export type Story = StoryObj; + +export const Default: Story = { + render, + parameters: { + msw: mocks, + }, +}; diff --git a/front/src/pages/companies/__stories__/shared.tsx b/front/src/pages/companies/__stories__/shared.tsx new file mode 100644 index 000000000..6205f0efb --- /dev/null +++ b/front/src/pages/companies/__stories__/shared.tsx @@ -0,0 +1,61 @@ +import { graphql } from 'msw'; +import { RecoilRoot } from 'recoil'; +import { ThemeProvider } from '@emotion/react'; +import { MemoryRouter } from 'react-router-dom'; +import { ApolloProvider } from '@apollo/client'; + +import { filterAndSortData } from '../../../testing/mock-data'; +import { mockedCompaniesData } from '../../../testing/mock-data/companies'; +import { GraphqlQueryCompany } from '../../../interfaces/entities/company.interface'; + +import { lightTheme } from '../../../layout/styles/themes'; +import { FullHeightStorybookLayout } from '../../../testing/FullHeightStorybookLayout'; +import { mockedClient } from '../../../testing/mockedClient'; +import Companies from '../Companies'; +import { GraphqlQueryUser } from '../../../interfaces/entities/user.interface'; +import { mockedUsersData } from '../../../testing/mock-data/users'; + +export const mocks = [ + graphql.query('GetCompanies', (req, res, ctx) => { + const returnedMockedData = filterAndSortData( + mockedCompaniesData, + req.variables.where, + req.variables.orderBy, + req.variables.limit, + ); + return res( + ctx.data({ + companies: returnedMockedData, + }), + ); + }), + graphql.query('SearchUserQuery', (req, res, ctx) => { + const returnedMockedData = filterAndSortData( + mockedUsersData, + req.variables.where, + req.variables.orderBy, + req.variables.limit, + ); + return res( + ctx.data({ + searchResults: returnedMockedData, + }), + ); + }), +]; + +export function render() { + return ( + + + + + + + + + + + + ); +} diff --git a/front/src/pages/companies/companies-filters.tsx b/front/src/pages/companies/companies-filters.tsx index 4ed1bb3d3..e3d82eded 100644 --- a/front/src/pages/companies/companies-filters.tsx +++ b/front/src/pages/companies/companies-filters.tsx @@ -14,7 +14,7 @@ import { QueryMode } from '../../generated/graphql'; export const nameFilter = { key: 'name', - label: 'Company', + label: 'Name', icon: , type: 'text', operands: [ diff --git a/front/src/pages/people/__stories__/People.filterBy.stories.tsx b/front/src/pages/people/__stories__/People.filterBy.stories.tsx new file mode 100644 index 000000000..3de867c52 --- /dev/null +++ b/front/src/pages/people/__stories__/People.filterBy.stories.tsx @@ -0,0 +1,71 @@ +import { expect } from '@storybook/jest'; +import type { Meta } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import People from '../People'; +import { Story } from './People.stories'; +import { mocks, render } from './shared'; + +const meta: Meta = { + title: 'Pages/People', + component: People, +}; + +export default meta; + +export const FilterByEmail: Story = { + render, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const filterButton = canvas.getByText('Filter'); + await userEvent.click(filterButton); + + const emailFilterButton = canvas.getByText('Email', { selector: 'li' }); + await userEvent.click(emailFilterButton); + + const emailInput = canvas.getByPlaceholderText('Email'); + await userEvent.type(emailInput, 'al', { + delay: 200, + }); + + expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument(); + await expect(canvas.queryAllByText('John Doe')).toStrictEqual([]); + + expect(await canvas.findByText('Email:')).toBeInTheDocument(); + expect(await canvas.findByText('Contains al')).toBeInTheDocument(); + }, + parameters: { + msw: mocks, + }, +}; + +export const FilterByCompanyName: Story = { + render, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const filterButton = canvas.getByText('Filter'); + await userEvent.click(filterButton); + + const companyFilterButton = canvas.getByText('Company', { selector: 'li' }); + await userEvent.click(companyFilterButton); + + const companyNameInput = canvas.getByPlaceholderText('Company'); + await userEvent.type(companyNameInput, 'Qon', { + delay: 200, + }); + + const qontoChip = canvas.getByText('Qonto', { selector: 'li' }); + await userEvent.click(qontoChip); + + expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument(); + await expect(canvas.queryAllByText('John Doe')).toStrictEqual([]); + + expect(await canvas.findByText('Company:')).toBeInTheDocument(); + expect(await canvas.findByText('Is Qonto')).toBeInTheDocument(); + }, + parameters: { + msw: mocks, + }, +}; diff --git a/front/src/pages/people/__stories__/People.sortBy.stories.tsx b/front/src/pages/people/__stories__/People.sortBy.stories.tsx new file mode 100644 index 000000000..ecfef9e56 --- /dev/null +++ b/front/src/pages/people/__stories__/People.sortBy.stories.tsx @@ -0,0 +1,47 @@ +import { expect } from '@storybook/jest'; +import type { Meta } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import People from '../People'; +import { Story } from './People.stories'; +import { mocks, render } from './shared'; + +const meta: Meta = { + title: 'Pages/People', + component: People, +}; + +export default meta; + +export const SortByEmail: Story = { + render, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const sortButton = canvas.getByText('Sort'); + await userEvent.click(sortButton); + + const emailSortButton = canvas.getByText('Email', { selector: 'li' }); + await userEvent.click(emailSortButton); + + expect(await canvas.getByTestId('remove-icon-email')).toBeInTheDocument(); + + expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument(); + + expect( + (await canvas.findAllByRole('checkbox')).map((item) => { + return item.getAttribute('id'); + })[1], + ).toStrictEqual('person-selected-7dfbc3f7-6e5e-4128-957e-8d86808cdf6b'); + + const cancelButton = canvas.getByText('Cancel'); + await userEvent.click(cancelButton); + + await expect(canvas.queryAllByTestId('remove-icon-email')).toStrictEqual( + [], + ); + }, + parameters: { + msw: mocks, + }, +}; diff --git a/front/src/pages/people/__stories__/People.stories.tsx b/front/src/pages/people/__stories__/People.stories.tsx index 082ef86d6..89bf61da3 100644 --- a/front/src/pages/people/__stories__/People.stories.tsx +++ b/front/src/pages/people/__stories__/People.stories.tsx @@ -1,113 +1,21 @@ -import { expect } from '@storybook/jest'; import type { Meta, StoryObj } from '@storybook/react'; -import { RecoilRoot } from 'recoil'; -import { ThemeProvider } from '@emotion/react'; -import { MemoryRouter } from 'react-router-dom'; -import { graphql } from 'msw'; -import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; -import { userEvent, within } from '@storybook/testing-library'; import People from '../People'; -import { lightTheme } from '../../../layout/styles/themes'; -import { FullHeightStorybookLayout } from '../../../testing/FullHeightStorybookLayout'; -import { filterAndSortData } from '../../../testing/mock-data'; -import { GraphqlQueryPerson } from '../../../interfaces/entities/person.interface'; -import { mockedPeopleData } from '../../../testing/mock-data/people'; -import { GraphqlQueryCompany } from '../../../interfaces/entities/company.interface'; -import { mockCompaniesData } from '../../../testing/mock-data/companies'; + +import { render, mocks } from './shared'; const meta: Meta = { title: 'Pages/People', component: People, }; -const mockedClient = new ApolloClient({ - uri: process.env.REACT_APP_API_URL, - cache: new InMemoryCache(), - defaultOptions: { - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'all', - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all', - }, - }, -}); - export default meta; -type Story = StoryObj; -const render = () => ( - - - - - - - - - - - -); - -const defaultMocks = [ - graphql.query('GetPeople', (req, res, ctx) => { - const returnedMockedData = filterAndSortData( - mockedPeopleData, - req.variables.where, - req.variables.orderBy, - req.variables.limit, - ); - return res( - ctx.data({ - people: returnedMockedData, - }), - ); - }), - graphql.query('SearchQuery', (req, res, ctx) => { - const returnedMockedData = filterAndSortData( - mockCompaniesData, - req.variables.where, - req.variables.orderBy, - req.variables.limit, - ); - return res( - ctx.data({ - searchResults: returnedMockedData, - }), - ); - }), -]; +export type Story = StoryObj; export const Default: Story = { render, parameters: { - msw: defaultMocks, - }, -}; - -export const FilterByEmail: Story = { - render, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const filterButton = canvas.getByText('Filter'); - await userEvent.click(filterButton); - - const emailFilterButton = canvas.getByText('Email', { selector: 'li' }); - await userEvent.click(emailFilterButton); - - const emailInput = canvas.getByPlaceholderText('Email'); - await userEvent.type(emailInput, 'al', { - delay: 200, - }); - - await expect(canvas.queryAllByText('John')).toStrictEqual([]); - }, - parameters: { - msw: defaultMocks, + msw: mocks, }, }; diff --git a/front/src/pages/people/__stories__/shared.tsx b/front/src/pages/people/__stories__/shared.tsx new file mode 100644 index 000000000..71cb1f462 --- /dev/null +++ b/front/src/pages/people/__stories__/shared.tsx @@ -0,0 +1,61 @@ +import { graphql } from 'msw'; +import { RecoilRoot } from 'recoil'; +import { ThemeProvider } from '@emotion/react'; +import { MemoryRouter } from 'react-router-dom'; +import { ApolloProvider } from '@apollo/client'; + +import { filterAndSortData } from '../../../testing/mock-data'; +import { mockedPeopleData } from '../../../testing/mock-data/people'; +import { mockedCompaniesData } from '../../../testing/mock-data/companies'; +import { GraphqlQueryCompany } from '../../../interfaces/entities/company.interface'; +import { GraphqlQueryPerson } from '../../../interfaces/entities/person.interface'; + +import { lightTheme } from '../../../layout/styles/themes'; +import { FullHeightStorybookLayout } from '../../../testing/FullHeightStorybookLayout'; +import { mockedClient } from '../../../testing/mockedClient'; +import People from '../People'; + +export const mocks = [ + graphql.query('GetPeople', (req, res, ctx) => { + const returnedMockedData = filterAndSortData( + mockedPeopleData, + req.variables.where, + req.variables.orderBy, + req.variables.limit, + ); + return res( + ctx.data({ + people: returnedMockedData, + }), + ); + }), + graphql.query('SearchCompanyQuery', (req, res, ctx) => { + const returnedMockedData = filterAndSortData( + mockedCompaniesData, + req.variables.where, + req.variables.orderBy, + req.variables.limit, + ); + return res( + ctx.data({ + searchResults: returnedMockedData, + }), + ); + }), +]; + +export function render() { + return ( + + + + + + + + + + + + ); +} diff --git a/front/src/services/api/search/search.ts b/front/src/services/api/search/search.ts index adc677a42..8222cd307 100644 --- a/front/src/services/api/search/search.ts +++ b/front/src/services/api/search/search.ts @@ -40,7 +40,7 @@ export const EMPTY_QUERY = gql` `; export const SEARCH_COMPANY_QUERY = gql` - query SearchQuery($where: CompanyWhereInput, $limit: Int) { + query SearchCompanyQuery($where: CompanyWhereInput, $limit: Int) { searchResults: companies(where: $where, take: $limit) { id name diff --git a/front/src/testing/mock-data/companies.ts b/front/src/testing/mock-data/companies.ts index 80bc4b856..261c8879b 100644 --- a/front/src/testing/mock-data/companies.ts +++ b/front/src/testing/mock-data/companies.ts @@ -1,6 +1,6 @@ import { GraphqlQueryCompany } from '../../interfaces/entities/company.interface'; -export const mockCompaniesData: Array = [ +export const mockedCompaniesData: Array = [ { id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', domainName: 'airbnb.com', @@ -8,7 +8,12 @@ export const mockCompaniesData: Array = [ createdAt: '2023-04-26T10:08:54.724515+00:00', address: '17 rue de clignancourt', employees: 12, - accountOwner: null, + accountOwner: { + email: 'charles@test.com', + displayName: 'Charles Test', + id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', + __typename: 'users', + }, __typename: 'companies', }, { diff --git a/front/src/testing/mock-data/index.ts b/front/src/testing/mock-data/index.ts index 344c42d25..e12bac6e2 100644 --- a/front/src/testing/mock-data/index.ts +++ b/front/src/testing/mock-data/index.ts @@ -23,7 +23,10 @@ function filterData( if (filterElement.is) { const nestedKey = Object.keys(filterElement.is)[0] as string; - if (typeof item[key as keyof typeof item] === 'object') { + if ( + item[key as keyof typeof item] && + typeof item[key as keyof typeof item] === 'object' + ) { const nestedItem = item[key as keyof typeof item]; return ( nestedItem[nestedKey as keyof typeof nestedItem] === @@ -71,6 +74,7 @@ export function filterAndSortData( limit: number, ): Array { let filteredData = filterData(data, where); + console.log(filteredData); if (orderBy) { const firstOrderBy = orderBy[0]; @@ -84,8 +88,12 @@ export function filterAndSortData( return 0; } + const sortDirection = + firstOrderBy[key as unknown as keyof typeof firstOrderBy]; if (typeof itemAValue === 'string' && typeof itemBValue === 'string') { - return itemBValue.localeCompare(itemAValue); + return sortDirection === 'desc' + ? itemBValue.localeCompare(itemAValue) + : -itemBValue.localeCompare(itemAValue); } return 0; }); diff --git a/front/src/testing/mock-data/users.ts b/front/src/testing/mock-data/users.ts new file mode 100644 index 000000000..e4c44db0c --- /dev/null +++ b/front/src/testing/mock-data/users.ts @@ -0,0 +1,16 @@ +import { GraphqlQueryUser } from '../../interfaces/entities/user.interface'; + +export const mockedUsersData: Array = [ + { + id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', + __typename: 'User', + email: 'charles@test.com', + displayName: 'Charles Test', + }, + { + id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c', + __typename: 'User', + email: 'felix@test.com', + displayName: 'Felix Test', + }, +]; diff --git a/front/src/testing/mockedClient.ts b/front/src/testing/mockedClient.ts new file mode 100644 index 000000000..10fafb6c1 --- /dev/null +++ b/front/src/testing/mockedClient.ts @@ -0,0 +1,16 @@ +import { ApolloClient, InMemoryCache } from '@apollo/client'; + +export const mockedClient = new ApolloClient({ + uri: process.env.REACT_APP_API_URL, + cache: new InMemoryCache(), + defaultOptions: { + watchQuery: { + fetchPolicy: 'no-cache', + errorPolicy: 'all', + }, + query: { + fetchPolicy: 'no-cache', + errorPolicy: 'all', + }, + }, +});