Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23,268 changes: 23,184 additions & 84 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@testing-library/react": "^11.2.3",
"babel-eslint": "^8.1.2",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
Expand All @@ -44,8 +43,11 @@
"webpack-dev-server": "3.11.0"
},
"dependencies": {
"@testing-library/react": "^12.1.2",
"react": "^17.0.1",
"react-dom": "^17.0.1"
"react-dom": "^17.0.1",
"react-redux": "^7.2.6",
"redux": "^4.1.2"
},
"jest": {
"roots": [
Expand Down
17 changes: 17 additions & 0 deletions src/__mocks__/test-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { render } from '@testing-library/react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { reducer } from '../redux/reducer';

export const renderWithState = (
ui,
{ initialState, ...renderOptions } = {}
) => {
const store = createStore(reducer, initialState);
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);

return render(ui, { wrapper: Wrapper, ...renderOptions });
};
5 changes: 5 additions & 0 deletions src/_variables.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$black: #0b121d;
$gray: #4e5358;
$light-gray: #eff2f2;
$white: #fff;
$red: red;
10 changes: 5 additions & 5 deletions src/api/__tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { request } from '../helpers';

jest.mock('../helpers');

describe.skip('getData Tests', () => {
describe('getData Tests', () => {
const safelyCallApi = async () => {
try {
return await getData();
Expand Down Expand Up @@ -38,23 +38,23 @@ describe.skip('getData Tests', () => {
expect(request).toBeCalledWith('/api/vehicle_xj.json');
});

it('Should ignore failed API calls during traversing', () => {
it('Should ignore failed API calls during traversing', async() => {
request.mockResolvedValueOnce([{ apiUrl: '/api/vehicle_ftype.json' }, { apiUrl: '/api/vehicle_xj.json' }]);
request.mockResolvedValueOnce({ id: 'ftype', price: '£36,000' });
request.mockRejectedValueOnce('An error occurred');

expect(safelyCallApi()).resolves.toEqual([
expect(await safelyCallApi()).toEqual([
{ apiUrl: '/api/vehicle_ftype.json', id: 'ftype', price: '£36,000' }
]);
});

it('Should ignore vehicles without valid price during traversing', () => {
it('Should ignore vehicles without valid price during traversing', async () => {
request.mockResolvedValueOnce([{ apiUrl: '/api/ftype.json' }, { apiUrl: '/api/xe.json' }, { apiUrl: '/api/xj.json' }]);
request.mockResolvedValueOnce({ id: 'ftype', price: '' });
request.mockResolvedValueOnce({ id: 'xe' });
request.mockResolvedValueOnce({ id: 'xj', price: '£40,000' });

return expect(safelyCallApi()).resolves.toEqual([
return expect(await safelyCallApi()).toEqual([
{ apiUrl: '/api/xj.json', id: 'xj', price: '£40,000' }
]);
});
Expand Down
15 changes: 15 additions & 0 deletions src/api/api_docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@
* @property {string} url - URL of image
*/

/**
* @typedef {Object} emissions
* @property {string} template - Template string
* @property {string} value - Occurrence value
*/

/**
* @typedef {Object} vehicleMeta
* @property {string} passengers - Quantity of passengers
* @property {Array.<string>} drivetrain - Drivetrain
* @property {Array.<string>} bodystyles - Bodystyles
* @property {Object.<emissions>} emissions - Emissions template
*/

/**
* @typedef {Object} vehicleSummaryPayload
* @property {string} id - ID of the vehicle
* @property {string} apiUrl - API URL for price, description & other details
* @property {string} description - Description
* @property {string} price - Price
* @property {Array.<vehicleMedia>} media - Array of vehicle images
* @property {Object.<vehicleMeta>} meta - Vehicle meta data
*/
7 changes: 6 additions & 1 deletion src/api/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@
* @return {Promise<Object>}
*/
export async function request(apiUrl) {
return apiUrl;
const res = await fetch(apiUrl);
if (res.ok) {
return res.json();
}

throw new Error(res.statusText || res.status);
}
19 changes: 16 additions & 3 deletions src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
// eslint-disable-next-line no-unused-vars
import { request } from './helpers';

/**
* Pull vehicles information
*
* @return {Promise<Array.<vehicleSummaryPayload>>}
*/
// TODO: All API related logic should be made inside this function.
export default async function getData() {
return [];
const url = '/api/vehicles.json';

return request(url)
.then(async (data) => {
const promises = data.map((res) => request(res.apiUrl));

return Promise.allSettled(promises).then((carInfos) => data
.map((item, index) => {
const { value, status } = carInfos[index];
return status === 'fulfilled' ? { ...item, ...value } : false;
})
.filter((item) => item && item.price));
})
.catch((err) => {
throw new Error(err);
});
}
45 changes: 45 additions & 0 deletions src/components/VehicleInfo/VehicleInfoAdditional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';

export default function VehicleInfoAdditional({ meta, className }) {
const setEmissionValue = (option) => {
const { template, ...arr } = option;
let updatedTemplate = template;

Object.keys(arr).forEach((v) => {
updatedTemplate = updatedTemplate.replaceAll(`$${v}`, arr[v]);
});

return updatedTemplate;
};

const createMetaValue = (id) => {
const value = meta[id];

switch (id) {
case 'drivetrain':
case 'bodystyles':
return value.join(', ');
case 'emissions':
return setEmissionValue(value);
default:
return value;
}
};

return (
<div data-testid="info" className={`${className}-info-additional`}>
<table className={`${className}-info-additional__table`}>
<tbody>
{Object.keys(meta).map((option) => {
return (
<tr key={option}>
<td>{option}</td>
<td>{createMetaValue(option)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
28 changes: 28 additions & 0 deletions src/components/VehicleInfo/VehicleMedia.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

export default function VehicleMedia({ media, className }) {
const smallPictureIndex = media.length - 1;
const largePictureIndex = 0;

const getMediaUrl = (index) => {
const { url } = media[index];
return url;
};

const getMediaName = (index) => {
const { name } = media[index];
return name;
};

return (
<div data-testid="media" className={`${className}-media`}>
{media.length
&& (
<picture>
<source srcSet={getMediaUrl(largePictureIndex)} media="(min-width: 768px)" />
<img srcSet={getMediaUrl(smallPictureIndex)} alt={getMediaName(smallPictureIndex)} />
</picture>
)}
</div>
);
}
30 changes: 30 additions & 0 deletions src/components/VehicleInfo/__tests__/VehicleInfo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { render } from '@testing-library/react';
import VehicleInfo from '..';

const basicClass = 'classname';
const data = {
id: 'id',
price: 'price',
description: 'description'
};

describe('<VehicleInfo /> Tests', () => {
let rendered;

beforeEach(() => {
rendered = render(<VehicleInfo data={data} className={basicClass} />);
});

it('Should show block of vehicle information', () => {
expect(rendered.queryByTestId('info')).not.toBeNull();
});

it('Should show vehicle name', () => {
expect(rendered.getByText(data.id)).not.toBeNull();
});

it('Block should have a class "classname-info-main"', () => {
expect(rendered.queryByTestId('info').className).toBe(`${basicClass}-info-main`);
});
});
21 changes: 21 additions & 0 deletions src/components/VehicleInfo/__tests__/VehicleInfoAdditional.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { render } from '@testing-library/react';
import VehicleInfoAdditional from '../VehicleInfoAdditional';

const basicClass = 'classname';

describe('<VehicleInfoAdditional /> Tests', () => {
let rendered;

beforeEach(() => {
rendered = render(<VehicleInfoAdditional meta={{}} className={basicClass} />);
});

it('Should show block of vehicle additional information', () => {
expect(rendered.queryByTestId('info')).not.toBeNull();
});

it('Block should have a class "classname-info-additional"', () => {
expect(rendered.queryByTestId('info').className).toBe(`${basicClass}-info-additional`);
});
});
27 changes: 27 additions & 0 deletions src/components/VehicleInfo/__tests__/VehicleMedia.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render } from '@testing-library/react';
import VehicleMedia from '../VehicleMedia';

const basicClass = 'classname';
const data = [
{
name: '1',
url: '/'
}
];

describe('<VehicleMedia /> Tests', () => {
let rendered;

beforeEach(() => {
rendered = render(<VehicleMedia media={data} className={basicClass} />);
});

it('Should show block of vehicle media', () => {
expect(rendered.queryByTestId('media')).not.toBeNull();
});

it('Block should have a class "classname-media"', () => {
expect(rendered.queryByTestId('media').className).toBe(`${basicClass}-media`);
});
});
22 changes: 22 additions & 0 deletions src/components/VehicleInfo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

export default function VehicleInfo({ data, className }) {
const {
id, price, description
} = data;

return (
<div data-testid="info" className={`${className}-info-main`}>
<h2 className={`${className}-info__name`}>
{id}
</h2>
<p className={`${className}-info__price`}>
From
<span className={`${className}-info__price-value`}>
{price}
</span>
</p>
<p className={`${className}-info__description`}>{description}</p>
</div>
);
}
32 changes: 25 additions & 7 deletions src/components/VehicleList/__tests__/VehicleList.test.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
import React from 'react';
import { render } from '@testing-library/react';
import { renderWithState } from '../../../__mocks__/test-util';
import VehicleList from '..';
import useData from '../useData';

jest.mock('../useData');

jest.mock('../../VehicleModal', () => () => (
<div data-testid="modal">Modal</div>
));

describe('<VehicleList /> Tests', () => {
it('Should show loading state if it not falsy', () => {
useData.mockReturnValue([true, 'An error occurred', 'results']);
const { queryByTestId } = render(<VehicleList />);
useData.mockReturnValue([true, 'An error occurred', []]);
const { queryByTestId } = renderWithState(<VehicleList />);

expect(queryByTestId('loading')).not.toBeNull();
expect(queryByTestId('error')).toBeNull();
expect(queryByTestId('results')).toBeNull();
});

it('Should show error if it is not falsy and loading is finished', () => {
useData.mockReturnValue([false, 'An error occurred', 'results']);
const { queryByTestId } = render(<VehicleList />);
useData.mockReturnValue([false, 'An error occurred', []]);
const { queryByTestId } = renderWithState(<VehicleList />);

expect(queryByTestId('loading')).toBeNull();
expect(queryByTestId('error')).not.toBeNull();
expect(queryByTestId('results')).toBeNull();
});

it('Should show results if loading successfully finished', () => {
useData.mockReturnValue([false, false, 'results']);
const { queryByTestId } = render(<VehicleList />);
useData.mockReturnValue([false, false, []]);
const { queryByTestId } = renderWithState(<VehicleList />);

expect(queryByTestId('loading')).toBeNull();
expect(queryByTestId('error')).toBeNull();
expect(queryByTestId('results')).not.toBeNull();
});

it('Should show results if loading successfully finished', () => {
useData.mockReturnValue([false, false, []]);
const { queryByTestId } = renderWithState(<VehicleList />);

expect(queryByTestId('modal')).toBeNull();
});

it('Should show results if loading successfully finished', () => {
useData.mockReturnValue([false, false, []]);
const { queryByTestId } = renderWithState(<VehicleList />, { initialState: { visibleModal: true } });

expect(queryByTestId('modal')).not.toBeNull();
});
});
Loading