Re-write test with storybook testing library (#150)

* Re-write test with storybook testing library

* Update CI
This commit is contained in:
Charles Bochet
2023-05-29 11:02:38 +02:00
committed by GitHub
parent 8f88605f32
commit f935a6b723
65 changed files with 8085 additions and 5164 deletions

View File

@ -4,12 +4,21 @@ on:
jobs:
front-test:
runs-on: ubuntu-latest
env:
REACT_APP_API_URL: http://127.0.0.1:3000/graphql
REACT_APP_AUTH_URL: http://127.0.0.1:3000/auth
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Write .env
run: |
cd front
touch .env
echo "REACT_APP_API_URL: $REACT_APP_API_URL" >> .env
echo "REACT_APP_AUTH_URL: $REACT_APP_AUTH_URL" >> .env
- name: Cache node modules
uses: actions/cache@v2
env:
@ -23,7 +32,14 @@ jobs:
${{ runner.os }}-
- name: Install Dependencies
run: cd front && npm install
- name: Install Playwright
run: cd front && npx playwright install --with-deps
- name: Run linter
run: cd front && npm run lint
- name: Run tests and code coverage
run: cd front && npm run coverage-ci
- name: Build Storybook
run: cd front && npm run build-storybook --quiet
- name: Serve Storybook and run 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"

View File

@ -21,13 +21,16 @@ module.exports = {
config.resolve.extensions.push('.mjs');
return config;
},
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/preset-create-react-app", '@storybook/addon-mdx-gfm'],
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app",
"@storybook/addon-coverage"
],
framework: {
name: '@storybook/react-webpack5',
options: {}
},
docs: {
autodocs: true
}
};

View File

@ -1,9 +0,0 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}

View File

@ -0,0 +1,19 @@
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { Preview } from '@storybook/react';
initialize();
const preview: Preview = {
decorators: [mswDecorator],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

10449
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,10 +30,10 @@
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint src --max-warnings=0",
"storybook": "storybook dev -p 6006 -s public",
"storybook": "storybook dev -p 6006 -s ../public",
"test-storybook": "test-storybook",
"build-storybook": "storybook build -s public",
"coverage": "react-scripts test --coverage --watchAll",
"coverage-ci": "react-scripts test --coverage --watchAll=false",
"coverage": "test-storybook --coverage && npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook --check-coverage --lines 50",
"graphql:generate": "REACT_APP_GRAPHQL_ADMIN_SECRET=$REACT_APP_GRAPHQL_ADMIN_SECRET graphql-codegen --config codegen.js"
},
"eslintConfig": {
@ -56,24 +56,10 @@
"react-refresh": "0.14.0"
},
"jest": {
"coveragePathIgnorePatterns": [
".stories.tsx$",
"graphql.tsx$",
"apollo.tsx$",
"src/index.tsx$"
],
"testMatch": [
"<rootDir>/**/*.test.ts",
"<rootDir>/**/*.test.tsx"
],
"coverageThreshold": {
"global": {
"branches": 70,
"functions": 75,
"lines": 80,
"statements": 80
}
}
]
},
"browserslist": {
"production": [
@ -93,13 +79,16 @@
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@storybook/addon-actions": "^7.0.2",
"@storybook/addon-coverage": "^0.0.8",
"@storybook/addon-essentials": "^7.0.2",
"@storybook/addon-interactions": "^7.0.2",
"@storybook/addon-links": "^7.0.2",
"@storybook/jest": "^0.1.0",
"@storybook/node-logger": "^7.0.2",
"@storybook/preset-create-react-app": "^7.0.2",
"@storybook/react": "^7.0.2",
"@storybook/react-webpack5": "^7.0.2",
"@storybook/test-runner": "^0.10.0",
"@storybook/testing-library": "^0.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
@ -109,6 +98,7 @@
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"babel-plugin-named-exports-order": "^0.0.2",
"concurrently": "^8.0.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-react-app": "^7.0.1",
@ -119,12 +109,18 @@
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-storybook": "^0.6.11",
"http-server": "^14.1.1",
"mock-apollo-client": "^1.2.1",
"msw": "^1.2.1",
"msw-storybook-addon": "^1.8.0",
"prettier": "^2.8.0",
"prop-types": "^15.8.1",
"react-scripts": "5.0.1",
"storybook": "^7.0.2",
"typescript": "^4.9.3",
"webpack": "^5.75.0"
},
"msw": {
"workerDirectory": "public"
}
}

View File

@ -0,0 +1,303 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (1.2.1).
* @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 activeClientIds = new Set()
self.addEventListener('install', function () {
self.skipWaiting()
})
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
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') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
// 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}`,
)
}),
)
})
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// 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,
},
})
})()
}
return response
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function getResponse(event, client, requestId) {
const { request } = event
const clonedRequest = 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())
// 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']
return fetch(clonedRequest, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
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,
},
})
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'MOCK_NOT_FOUND': {
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) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [channel.port2])
})
}
function sleep(timeMs) {
return new Promise((resolve) => {
setTimeout(resolve, timeMs)
})
}
async function respondWithMock(response) {
await sleep(response.delay)
return new Response(response.body, response)
}

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
import { render, waitFor } from '@testing-library/react';
import { RegularApp } from '../__stories__/App.stories';
const assignMock = jest.fn();
delete window.location;
window.location = { assign: assignMock };
it('Checks the App component renders', async () => {
const { getByText } = render(<RegularApp />);
expect(getByText('Companies')).toBeDefined();
await waitFor(() => {
expect(getByText('Twenty')).toBeDefined();
});
});

View File

@ -10,6 +10,8 @@ import { RestLink } from 'apollo-link-rest';
import { onError } from '@apollo/client/link/error';
import { refreshAccessToken } from './services/auth/AuthService';
console.log(process.env.REACT_APP_API_URL);
const apiLink = createHttpLink({
uri: `${process.env.REACT_APP_API_URL}`,
});

View File

@ -1,17 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import RequireAuth from '../RequireAuth';
const component = {
title: 'RequireAuth',
component: RequireAuth,
};
export default component;
export const RequireAuthWithHelloChild = () => (
<MemoryRouter>
<RequireAuth>
<div>Hello</div>
</RequireAuth>
</MemoryRouter>
);

View File

@ -1,9 +0,0 @@
import { render } from '@testing-library/react';
import { RequireAuthWithHelloChild } from '../__stories__/RequireAuth.stories';
it('Checks the Require Auth renders', () => {
const { getAllByText } = render(<RequireAuthWithHelloChild />);
expect(getAllByText('Hello')).toBeTruthy();
});

View File

@ -1,26 +0,0 @@
import CompanyChip from '../CompanyChip';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
const component = {
title: 'CompanyChip',
component: CompanyChip,
};
export default component;
export const RegularCompanyChip = () => {
return (
<ThemeProvider theme={lightTheme}>
<CompanyChip name="selected-company-1" />
</ThemeProvider>
);
};
export const RegularCompanyChipWithImage = () => {
return (
<ThemeProvider theme={lightTheme}>
<CompanyChip name="selected-company-1" picture="coucou.fr" />
</ThemeProvider>
);
};

View File

@ -1,26 +0,0 @@
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import PersonChip from '../PersonChip';
const component = {
title: 'PersonChip',
component: PersonChip,
};
export default component;
export const RegularPersonChip = () => {
return (
<ThemeProvider theme={lightTheme}>
<PersonChip name="selected-company-1" />
</ThemeProvider>
);
};
export const RegularPersonChipWithImage = () => {
return (
<ThemeProvider theme={lightTheme}>
<PersonChip name="selected-company-1" picture="coucou.fr" />
</ThemeProvider>
);
};

View File

@ -1,18 +0,0 @@
import { render } from '@testing-library/react';
import {
RegularCompanyChip,
RegularCompanyChipWithImage,
} from '../__stories__/CompanyChip.stories';
it('Checks the CompanyChip renders', () => {
const { getByText } = render(<RegularCompanyChip />);
expect(getByText('selected-company-1')).toBeDefined();
});
it('Checks the CompanyChip img renders', () => {
const { getByTestId } = render(<RegularCompanyChipWithImage />);
expect(getByTestId('company-chip-image')).toHaveAttribute('src', 'coucou.fr');
});

View File

@ -1,22 +0,0 @@
import { render } from '@testing-library/react';
import {
RegularPersonChip,
RegularPersonChipWithImage,
} from '../__stories__/PersonChip.stories';
it('Checks the PersonChip renders', () => {
const { getByText, getByTestId } = render(<RegularPersonChip />);
expect(getByText('selected-company-1')).toBeDefined();
expect(getByTestId('person-chip-image')).toHaveAttribute(
'src',
'person-placeholder.png',
);
});
it('Checks the PersonChip img renders', () => {
const { getByTestId } = render(<RegularPersonChipWithImage />);
expect(getByTestId('person-chip-image')).toHaveAttribute('src', 'coucou.fr');
});

View File

@ -1,33 +0,0 @@
import EditableChip, { EditableChipProps } from '../EditableChip';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
import CompanyChip from '../../chips/CompanyChip';
const component = {
title: 'EditableChip',
component: EditableChip,
};
export default component;
const Template: StoryFn<typeof EditableChip> = (args: EditableChipProps) => {
return (
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableChip {...args} />
</div>
</ThemeProvider>
);
};
export const EditableChipStory = Template.bind({});
EditableChipStory.args = {
ChipComponent: CompanyChip,
placeholder: 'Test',
value: 'Test',
picture: 'https://picsum.photos/200',
changeHandler: () => {
console.log('changed');
},
};

View File

@ -1,29 +0,0 @@
import EditableDate, { EditableDateProps } from '../EditableDate';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
const component = {
title: 'EditableDate',
component: EditableDate,
};
export default component;
const Template: StoryFn<typeof EditableDate> = (args: EditableDateProps) => {
return (
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableDate {...args} />
</div>
</ThemeProvider>
);
};
export const EditableDateStory = Template.bind({});
EditableDateStory.args = {
value: new Date(),
changeHandler: () => {
console.log('changed');
},
};

View File

@ -1,39 +0,0 @@
import { EditablePeopleFullName } from '../../people/EditablePeopleFullName';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
import { MemoryRouter } from 'react-router-dom';
const component = {
title: 'EditableFullName',
component: EditablePeopleFullName,
};
type OwnProps = {
firstname: string;
lastname: string;
onChange: (firstname: string, lastname: string) => void;
};
export default component;
const Template: StoryFn<typeof EditablePeopleFullName> = (args: OwnProps) => {
return (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditablePeopleFullName {...args} />
</div>
</ThemeProvider>
</MemoryRouter>
);
};
export const EditableFullNameStory = Template.bind({});
EditableFullNameStory.args = {
firstname: 'John',
lastname: 'Doe',
onChange: () => {
console.log('validated');
},
};

View File

@ -1,38 +0,0 @@
import EditablePhone from '../EditablePhone';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
import { MemoryRouter } from 'react-router-dom';
const component = {
title: 'EditablePhone',
component: EditablePhone,
};
type OwnProps = {
value: string;
changeHandler: (updated: string) => void;
};
export default component;
const Template: StoryFn<typeof EditablePhone> = (args: OwnProps) => {
return (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditablePhone {...args} />
</div>
</ThemeProvider>
</MemoryRouter>
);
};
export const EditablePhoneStory = Template.bind({});
EditablePhoneStory.args = {
placeholder: 'Test placeholder',
value: '+33657646543',
changeHandler: () => {
console.log('changed');
},
};

View File

@ -1,103 +0,0 @@
import EditableRelation, { EditableRelationProps } from '../EditableRelation';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
import CompanyChip, { CompanyChipPropsType } from '../../chips/CompanyChip';
import {
Company,
mapToCompany,
} from '../../../interfaces/entities/company.interface';
import { MockedProvider } from '@apollo/client/testing';
import { SEARCH_COMPANY_QUERY } from '../../../services/api/search/search';
import styled from '@emotion/styled';
import { SearchConfigType } from '../../../interfaces/search/interface';
import { QueryMode } from '../../../generated/graphql';
const component = {
title: 'editable-cell/EditableRelation',
component: EditableRelation,
};
export default component;
const StyledParent = styled.div`
height: 400px;
`;
const mocks = [
{
request: {
query: SEARCH_COMPANY_QUERY,
variables: {
where: undefined,
},
},
result: {
data: {
companies: [],
},
},
},
{
request: {
query: SEARCH_COMPANY_QUERY,
variables: {
where: { name: { contains: '%%', mode: QueryMode.Insensitive } },
limit: 5,
},
},
result: {
data: {
searchResults: [
{ id: 'abnb', name: 'Airbnb', domain_name: 'abnb.com' },
],
},
},
},
];
const Template: StoryFn<
typeof EditableRelation<Company, CompanyChipPropsType>
> = (args: EditableRelationProps<Company, CompanyChipPropsType>) => {
return (
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<StyledParent data-testid="content-editable-parent">
<EditableRelation<Company, CompanyChipPropsType> {...args} />
</StyledParent>
</ThemeProvider>
</MockedProvider>
);
};
export const EditableRelationStory = Template.bind({});
EditableRelationStory.args = {
relation: {
__typename: 'companies',
id: '123',
name: 'Heroku',
domain_name: 'heroku.com',
} as Company,
ChipComponent: CompanyChip,
chipComponentPropsMapper: (company: Company): CompanyChipPropsType => {
return {
name: company.name || '',
picture: company.domainName
? `https://www.google.com/s2/favicons?domain=${company.domainName}&sz=256`
: undefined,
};
},
onChange: (relation: Company) => {
console.log('changed', relation);
},
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput: string) => ({
name: { contains: `%${searchInput}%`, mode: QueryMode.Insensitive },
}),
resultMapper: (company) => ({
render: (company) => company.name,
value: mapToCompany(company),
}),
} satisfies SearchConfigType<Company>,
};

View File

@ -1,35 +0,0 @@
import EditableText from '../EditableText';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
const component = {
title: 'EditableText',
component: EditableText,
};
type OwnProps = {
content: string;
changeHandler: (updated: string) => void;
};
export default component;
const Template: StoryFn<typeof EditableText> = (args: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableText {...args} />
</div>
</ThemeProvider>
);
};
export const EditableTextStory = Template.bind({});
EditableTextStory.args = {
placeholder: 'Test placeholder',
content: 'Test string',
changeHandler: () => {
console.log('changed');
},
};

View File

@ -1,34 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { EditableChipStory } from '../__stories__/EditableChip.stories';
import CompanyChip from '../../chips/CompanyChip';
it('Checks the EditableChip editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(
<EditableChipStory
value="test"
picture="http://"
changeHandler={func}
ChipComponent={CompanyChip}
/>,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
if (!wrapper) {
throw new Error('Editable input not found');
}
fireEvent.click(wrapper);
const editableInput = parent.querySelector('input');
if (!editableInput) {
throw new Error('Editable input not found');
}
fireEvent.change(editableInput, { target: { value: 'Test' } });
expect(func).toBeCalledWith('Test');
});

View File

@ -1,36 +0,0 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { EditableDateStory } from '../__stories__/EditableDate.stories';
import { act } from 'react-dom/test-utils';
it('Checks the EditableDate editing event bubbles up', async () => {
const changeHandler = jest.fn(() => null);
const { getByTestId, getByText } = render(
<EditableDateStory
value={new Date('2021-03-03')}
changeHandler={changeHandler}
/>,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
if (!wrapper) {
throw new Error('Cell Wrapper not found');
}
act(() => {
fireEvent.click(wrapper);
const dateDisplay = parent.querySelector('div');
if (!dateDisplay) {
throw new Error('Editable input not found');
}
});
waitFor(() => {
expect(getByText('March 2021')).toBeInTheDocument();
});
fireEvent.click(getByText('5'));
expect(changeHandler).toHaveBeenCalledWith(new Date('2021-03-05'));
});

View File

@ -1,37 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { EditableFullNameStory } from '../__stories__/EditableFullName.stories';
it('Checks the EditableFullName editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(
<EditableFullNameStory firstname="Jone" lastname="Doe" onChange={func} />,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
if (!wrapper) {
throw new Error('Editable input not found');
}
fireEvent.click(wrapper);
const firstnameInput = parent.querySelector('input:first-child');
if (!firstnameInput) {
throw new Error('Editable input not found');
}
fireEvent.change(firstnameInput, { target: { value: 'Jo' } });
expect(func).toBeCalledWith('Jo', 'Doe');
const lastnameInput = parent.querySelector('input:last-child');
if (!lastnameInput) {
throw new Error('Editable input not found');
}
fireEvent.change(lastnameInput, { target: { value: 'Do' } });
expect(func).toBeCalledWith('Jo', 'Do');
});

View File

@ -1,28 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { EditablePhoneStory } from '../__stories__/EditablePhone.stories';
it('Checks the EditablePhone editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(
<EditablePhoneStory value="+33786405315" changeHandler={func} />,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
if (!wrapper) {
throw new Error('Editable input not found');
}
fireEvent.click(wrapper);
const editableInput = parent.querySelector('input');
if (!editableInput) {
throw new Error('Editable input not found');
}
fireEvent.change(editableInput, { target: { value: '23' } });
expect(func).toBeCalledWith('23');
});

View File

@ -1,64 +0,0 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { EditableRelationStory } from '../__stories__/EditableRelation.stories';
import { CompanyChipPropsType } from '../../chips/CompanyChip';
import { EditableRelationProps } from '../EditableRelation';
import { act } from 'react-dom/test-utils';
import { Company } from '../../../interfaces/company.interface';
it('Checks the EditableRelation editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId, getByText } = render(
<EditableRelationStory
{...(EditableRelationStory.args as EditableRelationProps<
Company,
CompanyChipPropsType
>)}
onChange={func}
/>,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
await waitFor(() => {
expect(getByText('Heroku')).toBeInTheDocument();
});
if (!wrapper) {
throw new Error('Editable relation not found');
}
fireEvent.click(wrapper);
const input = parent.querySelector('input');
if (!input) {
throw new Error('Search input not found');
}
act(() => {
fireEvent.change(input, { target: { value: 'Ai' } });
});
await waitFor(() => {
expect(getByText('Airbnb')).toBeInTheDocument();
});
act(() => {
fireEvent.click(getByText('Airbnb'));
});
await waitFor(() => {
expect(func).toBeCalledWith({
__typename: 'companies',
accountOwner: undefined,
address: undefined,
domainName: 'abnb.com',
employees: undefined,
creationDate: undefined,
id: 'abnb',
name: 'Airbnb',
pipes: [],
});
});
});

View File

@ -1,28 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { EditableTextStory } from '../__stories__/EditableText.stories';
it('Checks the EditableText editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(
<EditableTextStory content="test" changeHandler={func} />,
);
const parent = getByTestId('content-editable-parent');
const wrapper = parent.querySelector('div');
if (!wrapper) {
throw new Error('Editable input not found');
}
fireEvent.click(wrapper);
const editableInput = parent.querySelector('input');
if (!editableInput) {
throw new Error('Editable input not found');
}
fireEvent.change(editableInput, { target: { value: '23' } });
expect(func).toBeCalledWith('23');
});

View File

@ -1,18 +0,0 @@
import Checkbox from '../Checkbox';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
const component = {
title: 'Checkbox',
component: Checkbox,
};
export default component;
export const RegularCheckbox = () => {
return (
<ThemeProvider theme={lightTheme}>
<Checkbox name="selected-company-1" id="selected-company--1" />
</ThemeProvider>
);
};

View File

@ -1,36 +0,0 @@
import DatePicker, { DatePickerProps } from '../DatePicker';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
import styled from '@emotion/styled';
const component = {
title: 'DatePicker',
component: DatePicker,
};
export default component;
const StyledContainer = styled.div`
height: 300px;
width: 200px;
}`;
const Template: StoryFn<typeof DatePicker> = (args: DatePickerProps) => {
return (
<ThemeProvider theme={lightTheme}>
<StyledContainer>
<DatePicker {...args} />
</StyledContainer>
</ThemeProvider>
);
};
export const DatePickerStory = Template.bind({});
DatePickerStory.args = {
isOpen: true,
date: new Date(),
onChangeHandler: () => {
console.log('changed');
},
};

View File

@ -1,12 +0,0 @@
import { render } from '@testing-library/react';
import { RegularCheckbox } from '../__stories__/Checkbox.stories';
it('Checks the Checkbox renders', () => {
const { getByTestId } = render(<RegularCheckbox />);
expect(getByTestId('input-checkbox')).toHaveAttribute(
'name',
'selected-company-1',
);
});

View File

@ -1,27 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { DatePickerStory } from '../__stories__/Datepicker.stories';
import { act } from 'react-dom/test-utils';
it('Checks the datepicker renders', async () => {
const changeHandler = jest.fn();
const { getByText } = render(
<DatePickerStory
date={new Date('2021-03-03')}
onChangeHandler={changeHandler}
/>,
);
await act(async () => {
expect(getByText('Mar 3, 2021')).toBeInTheDocument();
});
act(() => {
fireEvent.click(getByText('Mar 3, 2021'));
});
expect(getByText('March 2021')).toBeInTheDocument();
act(() => {
fireEvent.click(getByText('5'));
});
expect(changeHandler).toHaveBeenCalledWith(new Date('2021-03-05'));
});

View File

@ -1,102 +0,0 @@
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { FilterDropdownButton } from '../FilterDropdownButton';
import styled from '@emotion/styled';
import { useCallback, useState } from 'react';
import { SEARCH_COMPANY_QUERY } from '../../../../services/api/search/search';
import { MockedProvider } from '@apollo/client/testing';
import { availableFilters } from '../../../../pages/people/people-filters';
import { Person } from '../../../../interfaces/entities/person.interface';
import {
FilterableFieldsType,
SelectedFilterType,
} from '../../../../interfaces/filters/interface';
import { mockCompaniesData } from '../../../../pages/companies/__tests__/__data__/mock-data';
import { QueryMode } from '../../../../generated/graphql';
const component = {
title: 'FilterDropdownButton',
component: FilterDropdownButton,
};
export default component;
type OwnProps<FilterProperties extends FilterableFieldsType> = {
setFilter: (filters: SelectedFilterType<FilterProperties>) => void;
};
const mocks = [
{
request: {
query: SEARCH_COMPANY_QUERY,
variables: {
where: { name: { contains: '%%', mode: QueryMode.Insensitive } },
limit: 5,
},
},
result: {
data: {
searchResults: mockCompaniesData,
},
},
},
{
request: {
query: SEARCH_COMPANY_QUERY,
variables: {
where: { name: { contains: '%Airc%', mode: QueryMode.Insensitive } },
limit: 5,
},
},
result: {
data: {
searchResults: mockCompaniesData.filter(
(company) => company.name === 'Aircall',
),
},
},
},
];
const StyleDiv = styled.div`
height: 200px;
width: 200px;
`;
const InnerRegularFilterDropdownButton = ({
setFilter: setFilters,
}: OwnProps<Person>) => {
const [, innerSetFilters] = useState<SelectedFilterType<Person>>();
const outerSetFilters = useCallback(
(filter: SelectedFilterType<Person>) => {
innerSetFilters(filter);
setFilters(filter);
},
[setFilters],
);
return (
<StyleDiv>
<FilterDropdownButton<Person>
availableFilters={availableFilters}
isFilterSelected={true}
onFilterSelect={outerSetFilters}
onFilterRemove={(filterId) => {
console.log(filterId);
}}
/>
</StyleDiv>
);
};
export const RegularFilterDropdownButton = ({
setFilter: setFilters,
}: OwnProps<Person>) => {
return (
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<InnerRegularFilterDropdownButton setFilter={setFilters} />
</ThemeProvider>
</MockedProvider>
);
};

View File

@ -1,75 +0,0 @@
import SortAndFilterBar from '../SortAndFilterBar';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { FaArrowDown } from 'react-icons/fa';
import { Person } from '../../../../interfaces/entities/person.interface';
import { SelectedFilterType } from '../../../../interfaces/filters/interface';
const component = {
title: 'SortAndFilterBar',
component: SortAndFilterBar,
};
export default component;
type OwnProps = {
removeFunction: () => void;
cancelFunction: () => void;
};
export const RegularSortAndFilterBar = ({
removeFunction,
cancelFunction,
}: OwnProps) => {
const personFilter = {
label: 'People',
operand: {
label: 'Is',
id: 'is',
whereTemplate: (person: Person) => {
return { email: { equals: person.email } };
},
},
key: 'test_filter',
icon: <FaArrowDown />,
displayValue: 'john@doedoe.com',
value: {
__typename: 'people',
id: 'test',
email: 'john@doedoe.com',
firstname: 'John',
lastname: 'Doe',
phone: '123456789',
company: null,
creationDate: new Date(),
pipes: null,
city: 'Paris',
},
} satisfies SelectedFilterType<Person, Person>;
return (
<ThemeProvider theme={lightTheme}>
<SortAndFilterBar
sorts={[
{
label: 'Test sort',
order: 'asc',
key: 'test_sort',
icon: <FaArrowDown />,
_type: 'default_sort',
},
{
label: 'Test sort 2',
order: 'desc',
key: 'test_sort_2',
icon: <FaArrowDown />,
_type: 'default_sort',
},
]}
onRemoveSort={removeFunction}
onRemoveFilter={removeFunction}
onCancelClick={cancelFunction}
filters={[personFilter] as SelectedFilterType<Person>[]}
/>
</ThemeProvider>
);
};

View File

@ -1,88 +0,0 @@
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { SortDropdownButton } from '../SortDropdownButton';
import styled from '@emotion/styled';
import {
SortOrder as Order_By,
PersonOrderByWithRelationInput as People_Order_By,
} from '../../../../generated/graphql';
import { SortType } from '../../../../interfaces/sorts/interface';
import {
TbBuilding,
TbCalendar,
TbMail,
TbMapPin,
TbPhone,
TbUser,
} from 'react-icons/tb';
const component = {
title: 'SortDropdownButton',
component: SortDropdownButton,
};
export default component;
type OwnProps = {
setSorts: () => void;
};
const availableSorts = [
{
key: 'fullname',
label: 'People',
icon: <TbUser size={16} />,
_type: 'custom_sort',
orderByTemplates: [() => ({ email: Order_By.Asc })],
},
{
key: 'company_name',
label: 'Company',
icon: <TbBuilding size={16} />,
_type: 'custom_sort',
orderByTemplates: [() => ({ email: Order_By.Asc })],
},
{
key: 'email',
label: 'Email',
icon: <TbMail size={16} />,
_type: 'default_sort',
},
{
key: 'phone',
label: 'Phone',
icon: <TbPhone size={16} />,
_type: 'default_sort',
},
{
key: 'createdAt',
label: 'Created at',
icon: <TbCalendar size={16} />,
_type: 'default_sort',
},
{
key: 'city',
label: 'City',
icon: <TbMapPin size={16} />,
_type: 'default_sort',
},
] satisfies SortType<People_Order_By>[];
const StyleDiv = styled.div`
height: 200px;
width: 200px;
`;
export const RegularSortDropdownButton = ({ setSorts }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<StyleDiv>
<SortDropdownButton<People_Order_By>
isSortSelected={true}
availableSorts={availableSorts}
onSortSelect={setSorts}
/>
</StyleDiv>
</ThemeProvider>
);
};

View File

@ -1,43 +0,0 @@
import SortOrFilterChip from '../SortOrFilterChip';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { FaArrowDown } from 'react-icons/fa';
import { TbUser } from 'react-icons/tb';
const component = {
title: 'SortOrFilterChip',
component: SortOrFilterChip,
};
export default component;
type OwnProps = {
removeFunction: () => void;
};
export const RegularFilterChip = ({ removeFunction }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<SortOrFilterChip
id="test_sort"
icon={<TbUser size={16} />}
labelKey="Account owner"
labelValue="is Charles"
onRemove={removeFunction}
/>
</ThemeProvider>
);
};
export const RegularSortChip = ({ removeFunction }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<SortOrFilterChip
id="test_sort"
icon={<FaArrowDown />}
labelValue="Created at"
onRemove={removeFunction}
/>
</ThemeProvider>
);
};

View File

@ -1,51 +0,0 @@
import TableHeader from '../TableHeader';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { SortType } from '../../../../interfaces/sorts/interface';
import { MockedProvider } from '@apollo/client/testing';
import { EMPTY_QUERY } from '../../../../services/api/search/search';
import { TbBuilding, TbCalendar } from 'react-icons/tb';
const component = {
title: 'TableHeader',
component: TableHeader,
};
const mocks = [
{
request: {
query: EMPTY_QUERY,
variables: {
where: undefined,
},
},
result: {
data: {
searchResults: [],
},
},
},
];
export default component;
export const RegularTableHeader = () => {
const availableSorts: Array<SortType<Record<'created_at', 'asc'>>> = [
{
key: 'created_at',
label: 'Created at',
icon: <TbCalendar size={16} />,
_type: 'default_sort',
},
];
return (
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<TableHeader
viewName="Test"
viewIcon={<TbBuilding size={16} />}
availableSorts={availableSorts}
/>
</ThemeProvider>
</MockedProvider>
);
};

View File

@ -1,117 +0,0 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { RegularFilterDropdownButton } from '../__stories__/FilterDropdownButton.stories';
it('Checks the default top option is Is', async () => {
const setFilters = jest.fn();
const { getByText } = render(
<RegularFilterDropdownButton setFilter={setFilters} />,
);
const sortDropdownButton = getByText('Filter');
fireEvent.click(sortDropdownButton);
const filterByCompany = getByText('Company');
fireEvent.click(filterByCompany);
await waitFor(() => {
const firstSearchResult = getByText('Airbnb');
expect(firstSearchResult).toBeDefined();
});
const filterByAirbnb = getByText('Airbnb');
fireEvent.click(filterByAirbnb);
expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({
displayValue: 'Airbnb',
key: 'company_name',
label: 'Company',
}),
);
});
it('Checks the selection of top option for Is Not', async () => {
const setFilters = jest.fn();
const { getByText } = render(
<RegularFilterDropdownButton setFilter={setFilters} />,
);
const sortDropdownButton = getByText('Filter');
fireEvent.click(sortDropdownButton);
const filterByCompany = getByText('Company');
fireEvent.click(filterByCompany);
const openOperandOptions = getByText('Is');
fireEvent.click(openOperandOptions);
const selectOperand = getByText('Is not');
fireEvent.click(selectOperand);
await waitFor(() => {
const firstSearchResult = getByText('Airbnb');
expect(firstSearchResult).toBeDefined();
});
const filterByAirbnb = getByText('Airbnb');
fireEvent.click(filterByAirbnb);
expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({
displayValue: 'Airbnb',
key: 'company_name',
label: 'Company',
}),
);
const blueSortDropdownButton = getByText('Filter');
await waitFor(() => {
expect(blueSortDropdownButton).toHaveAttribute('aria-selected', 'true');
});
});
it('Calls the filters when typing a new name', async () => {
const setFilters = jest.fn();
const { getByText, getByPlaceholderText, queryByText, getByTestId } = render(
<RegularFilterDropdownButton setFilter={setFilters} />,
);
const sortDropdownButton = getByText('Filter');
fireEvent.click(sortDropdownButton);
const filterByCompany = getByText('Company');
fireEvent.click(filterByCompany);
const filterSearch = getByPlaceholderText('Company');
fireEvent.click(filterSearch);
fireEvent.change(filterSearch, { target: { value: 'Airc' } });
await waitFor(() => {
const loadingDiv = getByTestId('loading-search-results');
expect(loadingDiv).toBeDefined();
});
await waitFor(() => {
const firstSearchResult = getByText('Aircall');
expect(firstSearchResult).toBeDefined();
const airbnbResult = queryByText('Airbnb');
expect(airbnbResult).not.toBeInTheDocument();
});
const filterByAircall = getByText('Aircall');
fireEvent.click(filterByAircall);
expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({
key: 'company_name',
displayValue: 'Aircall',
label: 'Company',
}),
);
const blueSortDropdownButton = getByText('Filter');
await waitFor(() => {
expect(blueSortDropdownButton).toHaveAttribute('aria-selected', 'true');
});
});

View File

@ -1,36 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularSortAndFilterBar } from '../__stories__/SortAndFilterBar.stories';
it('Checks the SortAndFilterBar renders', async () => {
const removeFunction = jest.fn();
const cancelFunction = jest.fn();
const { getByText, getByTestId } = render(
<RegularSortAndFilterBar
removeFunction={removeFunction}
cancelFunction={cancelFunction}
/>,
);
expect(getByText('Test sort')).toBeDefined();
const removeIcon = getByTestId('remove-icon-test_sort');
fireEvent.click(removeIcon);
expect(removeFunction).toHaveBeenCalled();
});
it('Removes sorts when cancel is pressed', async () => {
const removeFunction = jest.fn();
const cancelFunction = jest.fn();
const { getByTestId } = render(
<RegularSortAndFilterBar
removeFunction={removeFunction}
cancelFunction={cancelFunction}
/>,
);
const cancel = getByTestId('cancel-button');
fireEvent.click(cancel);
expect(cancelFunction).toHaveBeenCalled();
});

View File

@ -1,74 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularSortDropdownButton } from '../__stories__/SortDropdownButton.stories';
import { TbBuilding, TbMail } from 'react-icons/tb';
it('Checks the default top option is Ascending', async () => {
const setSorts = jest.fn();
const { getByText } = render(
<RegularSortDropdownButton setSorts={setSorts} />,
);
const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
expect(setSorts).toHaveBeenCalledWith({
label: 'Email',
key: 'email',
icon: <TbMail size={16} />,
order: 'asc',
_type: 'default_sort',
});
});
it('Checks the selection of Descending', async () => {
const setSorts = jest.fn();
const { getByText } = render(
<RegularSortDropdownButton setSorts={setSorts} />,
);
const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const openTopOption = getByText('Ascending');
fireEvent.click(openTopOption);
const selectDescending = getByText('Descending');
fireEvent.click(selectDescending);
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
expect(setSorts).toHaveBeenCalledWith({
label: 'Email',
key: 'email',
icon: <TbMail size={16} />,
order: 'desc',
_type: 'default_sort',
});
});
it('Checks custom_sort is working', async () => {
const setSorts = jest.fn();
const { getByText } = render(
<RegularSortDropdownButton setSorts={setSorts} />,
);
const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const sortByCompany = getByText('Company');
fireEvent.click(sortByCompany);
expect(setSorts).toHaveBeenCalledWith(
expect.objectContaining({
key: 'company_name',
label: 'Company',
icon: <TbBuilding size={16} />,
_type: 'custom_sort',
order: 'asc',
}),
);
});

View File

@ -1,32 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import {
RegularFilterChip,
RegularSortChip,
} from '../__stories__/SortOrFilterChip.stories';
const removeFunction = jest.fn();
it('Checks the filter chip renders', async () => {
const { getByText, getByTestId } = render(
<RegularFilterChip removeFunction={removeFunction} />,
);
expect(getByText('Account owner:')).toBeDefined();
const removeIcon = getByTestId('remove-icon-test_sort');
fireEvent.click(removeIcon);
expect(removeFunction).toHaveBeenCalled();
});
it('Checks the sort chip renders', async () => {
const { getByText, getByTestId } = render(
<RegularSortChip removeFunction={removeFunction} />,
);
expect(getByText('Created at')).toBeDefined();
const removeIcon = getByTestId('remove-icon-test_sort');
fireEvent.click(removeIcon);
expect(removeFunction).toHaveBeenCalled();
});

View File

@ -1,20 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularTableHeader } from '../__stories__/TableHeader.stories';
it('Checks the TableHeader renders', async () => {
const { getByText, queryByText } = render(<RegularTableHeader />);
const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const sortByCreatedAt = getByText('Created at');
fireEvent.click(sortByCreatedAt);
expect(getByText('Created at')).toBeDefined();
const cancelButton = getByText('Cancel');
fireEvent.click(cancelButton);
expect(queryByText('Created at')).toBeNull();
});

View File

@ -1,21 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import AppLayout from '../AppLayout';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../styles/themes';
const component = {
title: 'AppLayout',
component: AppLayout,
};
export default component;
export const AppLayoutDefault = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<AppLayout>
<div data-testid="content">Test</div>
</AppLayout>
</MemoryRouter>
</ThemeProvider>
);

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react';
import { AppLayoutDefault } from '../__stories__/AppLayout.stories';
it('Checks the AppLayout render', () => {
const { getByTestId } = render(<AppLayoutDefault />);
const title = getByTestId('content');
expect(title).toHaveTextContent('Test');
});

View File

@ -1,34 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@emotion/react';
import NavItem from '../../../layout/navbar/NavItem';
import { lightTheme } from '../../styles/themes';
import { TbUser } from 'react-icons/tb';
const component = {
title: 'NavItem',
component: NavItem,
};
export default component;
export const NavItemDefault = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<NavItem label="Test" to="/test" icon={<TbUser size={16} />} />
</MemoryRouter>
</ThemeProvider>
);
export const NavItemActive = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter initialEntries={['/test']}>
<NavItem
label="Test"
to="/test"
active={true}
icon={<TbUser size={16} />}
/>
</MemoryRouter>
</ThemeProvider>
);

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
import { render, fireEvent } from '@testing-library/react';
import { NavItemDefault } from '../__stories__/NavItem.stories';
const mockedNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedNavigate,
}));
it('Checks the NavItem renders', () => {
const { getByRole } = render(<NavItemDefault />);
const button = getByRole('button');
expect(button).toHaveTextContent('Test');
fireEvent.click(button);
expect(mockedNavigate).toHaveBeenCalledWith('/test');
});

View File

@ -1,17 +0,0 @@
import { render } from '@testing-library/react';
import { NavbarOnCompanies } from '../__stories__/Navbar.stories';
it('Checks the NavItem renders', () => {
const { getByRole } = render(<NavbarOnCompanies />);
expect(getByRole('button', { name: 'Companies' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(getByRole('button', { name: 'People' })).toHaveAttribute(
'aria-selected',
'false',
);
});

View File

@ -1,6 +1,6 @@
import { useSetRecoilState } from 'recoil';
import { currentRowSelectionState } from '../states/rowSelectionState';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
export function useResetTableRowSelection() {
const setCurrentRowSelectionState = useSetRecoilState(

View File

@ -1,19 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import Callback from '../Callback';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
const component = {
title: 'Callback',
component: Callback,
};
export default component;
export const CallbackDefault = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<Callback />
</MemoryRouter>
</ThemeProvider>
);

View File

@ -1,19 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import Login from '../Login';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
const component = {
title: 'Login',
component: Login,
};
export default component;
export const LoginDefault = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<Login />
</MemoryRouter>
</ThemeProvider>
);

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react';
import { CallbackDefault } from '../__stories__/Callback.stories';
import { act } from 'react-dom/test-utils';
it('Checks the Callback page render', async () => {
await act(async () => {
render(<CallbackDefault />);
});
});

View File

@ -1,15 +0,0 @@
import { render } from '@testing-library/react';
import { LoginDefault } from '../__stories__/Login.stories';
const assignMock = jest.fn();
delete window.location;
window.location = { assign: assignMock };
afterEach(() => {
assignMock.mockClear();
});
it('Checks the Login page render', () => {
render(<LoginDefault />);
});

View File

@ -1,75 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import Companies from '../Companies';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { GET_COMPANIES } from '../../../services/api/companies';
import { mockCompaniesData } from '../__tests__/__data__/mock-data';
import { MockedProvider } from '@apollo/client/testing';
import { QueryMode } from '../../../generated/graphql';
const component = {
title: 'Companies',
component: Companies,
};
export default component;
const mocks = [
{
request: {
query: GET_COMPANIES,
variables: {
orderBy: [{ createdAt: 'desc' }],
where: {},
},
},
result: {
data: {
companies: mockCompaniesData,
},
},
},
{
request: {
query: GET_COMPANIES,
variables: {
orderBy: [{ createdAt: 'desc' }],
where: {},
},
},
result: {
data: {
companies: mockCompaniesData,
},
},
},
{
request: {
query: GET_COMPANIES,
variables: {
orderBy: [{ createdAt: 'desc' }],
where: {
domainName: { contains: '%aircal%', mode: QueryMode.Insensitive },
},
},
},
result: {
data: {
companies: mockCompaniesData.filter(
(company) =>
company.domain_name && company.domain_name.includes('aircal'),
),
},
},
},
];
export const CompaniesDefault = () => (
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<Companies />
</MemoryRouter>
</ThemeProvider>
</MockedProvider>
);

View File

@ -1,169 +0,0 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { CompaniesDefault } from '../__stories__/Companies.stories';
import { act } from 'react-dom/test-utils';
import {
GraphqlMutationCompany,
GraphqlQueryCompany,
} from '../../../interfaces/entities/company.interface';
jest.mock('../../../apollo', () => {
const companyInterface = jest.requireActual(
'../../../interfaces/entities/company.interface',
);
return {
apiClient: {
mutate: (arg: {
mutation: unknown;
variables: GraphqlMutationCompany;
}) => {
const gqlCompany = arg.variables as unknown as GraphqlQueryCompany;
return {
data: { updateOneCompany: companyInterface.mapToCompany(gqlCompany) },
};
},
},
};
});
it('Checks company name edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('Airbnb')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('Airbnb'));
});
await waitFor(() => {
expect(getByDisplayValue('Airbnb')).toBeInTheDocument();
});
act(() => {
const nameInput = getByDisplayValue('Airbnb');
if (!nameInput) {
throw new Error('nameInput is null');
}
fireEvent.change(nameInput, { target: { value: 'Airbnbb' } });
fireEvent.click(getByText('All Companies')); // Click outside
});
await waitFor(() => {
expect(getByText('Airbnbb')).toBeDefined();
});
});
it('Checks company url edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('airbnb.com')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('airbnb.com'));
});
await waitFor(() => {
expect(getByDisplayValue('airbnb.com')).toBeInTheDocument();
});
act(() => {
const urlInput = getByDisplayValue('airbnb.com');
if (!urlInput) {
throw new Error('urlInput is null');
}
fireEvent.change(urlInput, { target: { value: 'airbnb.co' } });
fireEvent.click(getByText('All Companies')); // Click outside
});
await waitFor(() => {
expect(getByText('airbnb.co')).toBeInTheDocument();
});
});
it.only('Checks company address edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('17 rue de clignancourt')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('17 rue de clignancourt'));
});
await waitFor(() => {
expect(getByDisplayValue('17 rue de clignancourt')).toBeInTheDocument();
});
act(() => {
const addressInput = getByDisplayValue('17 rue de clignancourt');
if (!addressInput) {
throw new Error('addressInput is null');
}
fireEvent.change(addressInput, {
target: { value: '21 rue de clignancourt' },
});
fireEvent.click(getByText('All Companies')); // Click outside
});
await waitFor(() => {
expect(getByText('21 rue de clignancourt')).toBeInTheDocument();
});
});
it('Checks insert data is appending a new line', async () => {
const { getByText, getByTestId, container } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('Airbnb')).toBeDefined();
});
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
expect(tableRows.length).toBe(6);
act(() => {
fireEvent.click(getByTestId('add-button'));
});
await waitFor(() => {
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
expect(tableRows.length).toBe(7);
});
});
it('Checks filters are working', async () => {
const { getByText, queryByText, getByPlaceholderText } = render(
<CompaniesDefault />,
);
await waitFor(() => {
expect(getByText('Airbnb')).toBeDefined();
});
const filterDropdown = getByText('Filter');
fireEvent.click(filterDropdown);
await waitFor(() => {
expect(getByText('Url')).toBeDefined();
});
const urlFilter = getByText('Url');
fireEvent.click(urlFilter);
const filterSearch = getByPlaceholderText('Url');
fireEvent.change(filterSearch, { target: { value: 'aircal' } });
await waitFor(() => {
expect(getByText('aircall.io')).toBeDefined();
const airbnbResult = queryByText('Airbnb');
expect(airbnbResult).not.toBeInTheDocument();
});
});

View File

@ -1,19 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import Opportunities from '../Opportunities';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
const component = {
title: 'Opportunities',
component: Opportunities,
};
export default component;
export const OpportunitiesDefault = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<Opportunities />
</MemoryRouter>
</ThemeProvider>
);

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react';
import { OpportunitiesDefault } from '../__stories__/Opportunities.stories';
it('Checks the Companies page render', () => {
const { getByTestId } = render(<OpportunitiesDefault />);
const title = getByTestId('top-bar-title');
expect(title).toHaveTextContent('Opportunities');
});

View File

@ -0,0 +1,11 @@
{ /* People.mdx */ }
import { Canvas, Meta } from '@storybook/blocks';
import * as People from './People.stories';
<Meta of={People} />
# People View
<Canvas of={People.Default} />

View File

@ -1,73 +1,113 @@
import { MemoryRouter } from 'react-router-dom';
import People from '../People';
import { expect } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { MockedProvider } from '@apollo/client/testing';
import { mockPeopleData } from '../__tests__/__data__/mock-data';
import { GET_PEOPLE } from '../../../services/api/people';
import { SEARCH_PEOPLE_QUERY } from '../../../services/api/search/search';
import {
GraphqlMutationPerson,
GraphqlQueryPerson,
} from '../../../interfaces/entities/person.interface';
import { MemoryRouter } from 'react-router-dom';
import { graphql } from 'msw';
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { userEvent, within } from '@storybook/testing-library';
const component = {
title: 'People',
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';
const meta: Meta<typeof People> = {
title: 'Pages/People',
component: People,
};
export default component;
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',
},
},
});
const mocks = [
{
request: {
query: GET_PEOPLE,
variables: {
orderBy: [{ createdAt: 'desc' }],
where: {},
},
},
result: {
data: {
people: mockPeopleData,
},
},
},
{
request: {
query: GET_PEOPLE,
variables: {
orderBy: [{ createdAt: 'desc' }],
where: {},
},
},
result: {
data: {
people: mockPeopleData,
},
},
},
{
request: {
query: SEARCH_PEOPLE_QUERY, // TODO this should not be called for empty filters
variables: {
where: undefined,
},
},
result: {
data: {
people: [],
},
},
},
export default meta;
type Story = StoryObj<typeof People>;
const render = () => (
<RecoilRoot>
<ApolloProvider client={mockedClient}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<FullHeightStorybookLayout>
<People />
</FullHeightStorybookLayout>
</MemoryRouter>
</ThemeProvider>
</ApolloProvider>
</RecoilRoot>
);
const defaultMocks = [
graphql.query('GetPeople', (req, res, ctx) => {
const returnedMockedData = filterAndSortData<GraphqlQueryPerson>(
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<GraphqlQueryCompany>(
mockCompaniesData,
req.variables.where,
req.variables.orderBy,
req.variables.limit,
);
return res(
ctx.data({
searchResults: returnedMockedData,
}),
);
}),
];
export const PeopleDefault = () => (
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<People />
</MemoryRouter>
</ThemeProvider>
</MockedProvider>
);
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,
},
};

View File

@ -1,109 +0,0 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { PeopleDefault } from '../__stories__/People.stories';
import { act } from 'react-dom/test-utils';
import {
GraphqlMutationPerson,
GraphqlQueryPerson,
} from '../../../interfaces/entities/person.interface';
jest.mock('../../../apollo', () => {
const personInterface = jest.requireActual(
'../../../interfaces/entities/person.interface',
);
return {
apiClient: {
mutate: (arg: {
mutation: unknown;
variables: GraphqlMutationPerson;
}) => {
const gqlPerson = arg.variables as unknown as GraphqlQueryPerson;
return {
data: { updateOnePerson: personInterface.mapToPerson(gqlPerson) },
};
},
},
};
});
it('Checks people full name edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<PeopleDefault />);
await waitFor(() => {
expect(getByText('John Doe')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('John Doe'));
});
await waitFor(() => {
expect(getByDisplayValue('John')).toBeInTheDocument();
});
act(() => {
const nameInput = getByDisplayValue('John');
if (!nameInput) {
throw new Error('firstNameInput is null');
}
fireEvent.change(nameInput, { target: { value: 'Jo' } });
expect(nameInput).toHaveValue('Jo');
fireEvent.click(getByText('All People')); // Click outside
});
await waitFor(() => {
expect(getByText('John Doe')).toBeInTheDocument();
});
});
it('Checks people email edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<PeopleDefault />);
await waitFor(() => {
expect(getByText('john@linkedin.com')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('john@linkedin.com'));
});
await waitFor(() => {
expect(getByDisplayValue('john@linkedin.com')).toBeInTheDocument();
});
act(() => {
const emailInput = getByDisplayValue('john@linkedin.com');
if (!emailInput) {
throw new Error('emailInput is null');
}
fireEvent.change(emailInput, { target: { value: 'john@linkedin.c' } });
fireEvent.click(getByText('All People')); // Click outside
});
await waitFor(() => {
expect(getByText('john@linkedin.c')).toBeInTheDocument();
});
});
it('Checks insert data is appending a new line', async () => {
const { getByText, getByTestId, container } = render(<PeopleDefault />);
await waitFor(() => {
expect(getByText('John Doe')).toBeDefined();
});
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
expect(tableRows.length).toBe(4);
act(() => {
fireEvent.click(getByTestId('add-button'));
});
await waitFor(() => {
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
expect(tableRows.length).toBe(5);
});
});

View File

@ -0,0 +1,16 @@
import styled from '@emotion/styled';
const StyledLayout = styled.div`
display: flex;
flex-direction: row;
width: calc(100vw - 32px);
height: calc(100vh - 32px);
`;
type OwnProps = {
children: JSX.Element;
};
export function FullHeightStorybookLayout({ children }: OwnProps) {
return <StyledLayout>{children}</StyledLayout>;
}

View File

@ -1,4 +1,4 @@
import { GraphqlQueryCompany } from '../../../../interfaces/entities/company.interface';
import { GraphqlQueryCompany } from '../../interfaces/entities/company.interface';
export const mockCompaniesData: Array<GraphqlQueryCompany> = [
{
@ -43,8 +43,8 @@ export const mockCompaniesData: Array<GraphqlQueryCompany> = [
},
{
id: '5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
domain_name: 'bereal.com',
name: 'BeReal',
domain_name: 'qonto.com',
name: 'Qonto',
created_at: '2023-04-26T10:13:29.712485+00:00',
address: '10 rue de la Paix',
employees: 1,
@ -53,8 +53,18 @@ export const mockCompaniesData: Array<GraphqlQueryCompany> = [
},
{
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
domain_name: 'claap.com',
name: 'Claap',
domain_name: 'facebook.com',
name: 'Facebook',
created_at: '2023-04-26T10:09:25.656555+00:00',
address: '',
employees: 1,
account_owner: null,
__typename: 'companies',
},
{
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
domain_name: 'sequoia.com',
name: 'Sequoia',
created_at: '2023-04-26T10:09:25.656555+00:00',
address: '',
employees: 1,

View File

@ -0,0 +1,98 @@
import {
CompanyOrderByWithRelationInput,
PersonOrderByWithRelationInput,
StringFilter,
} from '../../generated/graphql';
import { Company } from '../../interfaces/entities/company.interface';
import { BoolExpType } from '../../interfaces/entities/generic.interface';
import { Person } from '../../interfaces/entities/person.interface';
function filterData<DataT>(
data: Array<DataT>,
where: BoolExpType<Company> | BoolExpType<Person>,
): Array<DataT> {
return data.filter((item) => {
// { firstname: {contains: '%string%' }}
// { firstname: {equals: 'string' }}
// { is: { company: { equals: 'string' }}}
let isMatch: boolean = (
Object.keys(where) as Array<keyof typeof where>
).every((key) => {
if (!['OR', 'AND', 'NOT'].includes(key)) {
const filterElement = where[key] as StringFilter & { is?: object };
if (filterElement.is) {
const nestedKey = Object.keys(filterElement.is)[0] as string;
if (typeof item[key as keyof typeof item] === 'object') {
const nestedItem = item[key as keyof typeof item];
return (
nestedItem[nestedKey as keyof typeof nestedItem] ===
(
filterElement.is[
nestedKey as keyof typeof filterElement.is
] as StringFilter
).equals
);
}
}
if (filterElement.equals) {
return item[key as keyof typeof item] === filterElement.equals;
}
if (filterElement.contains) {
return (item[key as keyof typeof item] as string)
.toLocaleLowerCase()
.includes(
filterElement.contains.replaceAll('%', '').toLocaleLowerCase(),
);
}
}
return false;
});
// { OR: [{ firstname: filter }, { lastname: filter }]
if (where.OR && Array.isArray(where.OR)) {
isMatch =
isMatch ||
where.OR.some((orFilter) =>
filterData<DataT>(data, orFilter).includes(item),
);
}
return isMatch;
});
}
export function filterAndSortData<DataT>(
data: Array<DataT>,
where: BoolExpType<Company> | BoolExpType<Person>,
orderBy: Array<
PersonOrderByWithRelationInput & CompanyOrderByWithRelationInput
>,
limit: number,
): Array<DataT> {
let filteredData = filterData<DataT>(data, where);
if (orderBy) {
const firstOrderBy = orderBy[0];
const key = Object.keys(firstOrderBy)[0];
filteredData.sort((itemA, itemB) => {
const itemAValue = itemA[key as unknown as keyof typeof itemA];
const itemBValue = itemB[key as unknown as keyof typeof itemB];
if (!itemAValue || !itemBValue) {
return 0;
}
if (typeof itemAValue === 'string' && typeof itemBValue === 'string') {
return itemBValue.localeCompare(itemAValue);
}
return 0;
});
}
if (limit) {
filteredData = filteredData.slice(0, limit);
}
return filteredData;
}

View File

@ -1,6 +1,6 @@
import { GraphqlQueryPerson } from '../../../../interfaces/entities/person.interface';
import { GraphqlQueryPerson } from '../../interfaces/entities/person.interface';
export const mockPeopleData: Array<GraphqlQueryPerson> = [
export const mockedPeopleData: Array<GraphqlQueryPerson> = [
{
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
__typename: 'Person',

View File

@ -1,17 +1,11 @@
FROM node:18.16.0-alpine as front
RUN apk update && apk upgrade && \
apk add --no-cache bash git openssh && \
apk add libc6-compat && \
apk add python3 && \
apk add make && \
apk add g++
FROM node:18.16.0 as front
WORKDIR /app/front
COPY ../../front/package.json .
COPY ../../front/package-lock.json .
RUN npm install
RUN npx playwright install-deps
WORKDIR /app/server