Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

!!DRAFT task-solution Szczepan Micek #1

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9768dec
feat: add TypeScript definition file for crew types
xyashino Dec 27, 2023
7bbd197
chore: add .idea directory to .gitignore
xyashino Dec 27, 2023
f20b715
feat: implement readJsonFile and readYamlFile for static data parsing
xyashino Dec 27, 2023
6de2c8f
feat: add schemas for crew member data validation
xyashino Dec 27, 2023
89beb41
refactor: centralize data file paths in constants.ts
xyashino Dec 27, 2023
54e5677
refactor: update and streamline crew types
xyashino Dec 27, 2023
bc67c85
feat: add mapJsonMemberToValidCrewMember and mapYamlMemberToValidCrew…
xyashino Dec 27, 2023
71ab0e5
feat: implement getCrewMembers to process crew data
xyashino Dec 27, 2023
5074aef
feat(validation): add pages for handling form validation errors
xyashino Dec 27, 2023
a553a3a
feat(components): create new pagination component
xyashino Dec 27, 2023
0ef27f9
feat(crew-routing): create paginated route for crew listings
xyashino Dec 27, 2023
619eee3
feat(crew-fetch): implement fetch method for crew data retrieval
xyashino Dec 27, 2023
76d2d59
feat(CrewList): add CrewList component for displaying crew members
xyashino Dec 27, 2023
03c40b5
feat(crew-SSR): implement pagination for crew list using SSR
xyashino Dec 27, 2023
cece3c5
style: update min-h styles for improved layout consistency
xyashino Dec 27, 2023
d587b7b
feat: implement react-query for data fetching
xyashino Dec 27, 2023
0bb7d32
refactor: update default fetch logic for enhanced performance and rel…
xyashino Dec 27, 2023
426f345
feat: implement React Query-based pagination for crew list
xyashino Dec 27, 2023
11279a7
chore: install and configure prettier for code formatting
xyashino Dec 27, 2023
09d25b5
style: apply prettier formatting to entire codebase
xyashino Dec 27, 2023
47402b1
feat: add param validation in getServerSideProps for enhanced security
xyashino Dec 27, 2023
2358ad5
refactor: rename 'usePagination' to 'useReactQueryPagination' for cla…
xyashino Dec 27, 2023
6632a84
refactor: move logic from 'Pagination' to 'usePaginationLogic' hook f…
xyashino Dec 27, 2023
65544f0
refactor: improve variable and method naming conventions
xyashino Dec 28, 2023
4b59cb5
feat: install React Testing Library and Vitest, set up basic testing …
xyashino Jan 1, 2024
82356b0
refactor: move static data into constants.ts for better maintainability
xyashino Jan 1, 2024
8e12887
test: prepare mocks for unit testing
xyashino Jan 1, 2024
075c71d
test: add unit tests for all utility functions
xyashino Jan 1, 2024
2cc38f3
test: add data-testid attributes to Pagination.tsx for testing ease
xyashino Jan 1, 2024
f650e9c
test: add unit tests for specific components
xyashino Jan 1, 2024
f4f883c
chore: extend Vitest types with @testing-library/jest-dom
xyashino Jan 1, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.idea
21 changes: 21 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"semi": true,
"tabWidth": 2,
"singleQuote": true,
"jsxSingleQuote": true,
"importOrder": [
"^@(.*)$",
"^@/(.*)$",
"^@/types/(.*)$",
"^@/app/(.*)$",
"^@/lib/(.*)$",
"^@/components/(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": [
"prettier-plugin-tailwindcss",
"@trivago/prettier-plugin-sort-imports"
]
}
32 changes: 32 additions & 0 deletions __tests__/api/fetch-api-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect, vi , beforeEach } from 'vitest';
import { fetchApiData } from '@/lib/api/fetch-api-data';

global.fetch = vi.fn();

describe('fetchApiData', () => {

beforeEach(() => {
vi.clearAllMocks();
});

it('constructs URL correctly for GET requests with query params', async () => {
const path = '/test';
const query = { param1: 'value1', param2: 'value2' };

await fetchApiData({ path, query });

expect(global.fetch).toHaveBeenCalledWith('/test?param1=value1&param2=value2', expect.anything());
});

it('sets default method to GET and adds default headers', async () => {
const path = '/test';

await fetchApiData({ path });

expect(global.fetch).toHaveBeenCalledWith(path, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
});

});
39 changes: 39 additions & 0 deletions __tests__/api/get-crew.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { fetchApiData } from '@/lib/api/fetch-api-data';
import { getCrew } from '@/lib/api/get-crew';

vi.mock('@/lib/api/fetch-api-data', () => ({
fetchApiData: vi.fn(),
}));

describe('getCrew', () => {
beforeEach(() => {
vi.mocked(fetchApiData).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: 'mockData' }),
} as Response);

vi.clearAllMocks();
});

it('calls fetchApiData with correct parameters', async () => {
await getCrew({ page: 2, take: 5 });
expect(fetchApiData).toHaveBeenCalledWith({
path: '/api/crew',
query: { take: 5, page: 2 },
});
});

it(`shouldn't call fetchApiData if page less or equal 0`, async () => {
await getCrew({ page: 0, take: 5 });
await getCrew({ page: -1, take: 5 });
expect(fetchApiData).not.toHaveBeenCalled();
});

it(`shouldn't call fetchApiData if page is NaN`, async () => {
await getCrew({ page: NaN, take: 5 });
expect(fetchApiData).not.toHaveBeenCalled();
});

});
25 changes: 25 additions & 0 deletions __tests__/components/CrewCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { CrewCard } from '@/components/crew-list/CrewCard';

describe('CrewCard', () => {
it('displays crew member details', () => {
const crewMember = {
fullName: 'Jane Doe',
age: 30,
nationality: 'American',
profession: 'Engineer',
};

render(<CrewCard {...crewMember} />);

expect(screen.getByText('Full Name:')).toBeInTheDocument();
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
expect(screen.getByText('Age:')).toBeInTheDocument();
expect(screen.getByText('30')).toBeInTheDocument();
expect(screen.getByText('Nationality:')).toBeInTheDocument();
expect(screen.getByText('American')).toBeInTheDocument();
expect(screen.getByText('Profession:')).toBeInTheDocument();
expect(screen.getByText('Engineer')).toBeInTheDocument();
});
});
37 changes: 37 additions & 0 deletions __tests__/components/CrewList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ComponentProps } from 'react';
import { describe, expect, it, vi } from 'vitest';

import { CrewCard } from '@/components/crew-list/CrewCard';
import { CrewList } from '@/components/crew-list/CrewList';
import { render, screen } from '@testing-library/react';

type Props = ComponentProps<typeof CrewCard>;

vi.mock('@/components/crew-list/CrewCard', () => ({
CrewCard: ({ fullName }: Props) => <div>{fullName}</div>,
}));

describe('CrewList', () => {
it('renders a list of crew members', () => {
const crewMembers = [
{
fullName: 'John Doe',
age: 30,
nationality: 'American',
profession: 'Engineer',
},
{
fullName: 'Jane Smith',
age: 28,
nationality: 'Canadian',
profession: 'Pilot',
},
];

render(<CrewList crew={crewMembers} />);

expect(screen.getAllByText(/John Doe|Jane Smith/)).toHaveLength(
crewMembers.length,
);
});
});
94 changes: 94 additions & 0 deletions __tests__/components/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { Pagination } from '@/components/pagination/Pagination';
import { usePaginationLogic } from '@/lib/hooks/usePaginationLogic';
import { fireEvent, render, screen } from '@testing-library/react';

vi.mock('@/lib/hooks/usePaginationLogic');

describe('Pagination', () => {
const mockGoToPreviousPage = vi.fn();
const mockGoToNextPage = vi.fn();
const mockChangePage = vi.fn();

let mockPaginationResponse: PaginationResponse;

beforeEach(() => {
vi.mocked(usePaginationLogic).mockReturnValue({
goToPreviousPage: mockGoToPreviousPage,
goToNextPage: mockGoToNextPage,
changePage: mockChangePage,
});

mockPaginationResponse = {
nextPage: 2,
previousPage: 1,
lastPage: 3,
currentPage: 1,
totalItems: 40,
totalPages: 3,
itemsPerPage: 8,
};
vi.clearAllMocks();
});

it('renders pagination buttons correctly', () => {
render(<Pagination {...mockPaginationResponse} />);

const previousButton = screen.getByTestId('prev-btn');
const nextButton = screen.getByTestId('next-btn');
const pageButtons = screen.getAllByRole('button');

expect(previousButton).toBeInTheDocument();
expect(nextButton).toBeInTheDocument();
expect(pageButtons).toHaveLength(mockPaginationResponse.lastPage + 2);
});

it('calls the correct function when the prev/next button is clicked', () => {
render(<Pagination {...mockPaginationResponse} />);

const previousButton = screen.getByTestId('prev-btn');
fireEvent.click(previousButton);
const nextButton = screen.getByTestId('next-btn');
fireEvent.click(nextButton);

expect(mockGoToNextPage).toHaveBeenCalledTimes(1);
expect(mockGoToPreviousPage).toHaveBeenCalledTimes(1);
});

it('calls the correct function when a page button is clicked', () => {
render(<Pagination {...mockPaginationResponse} />);

const pageButton =
screen.getAllByRole('button')[mockPaginationResponse.currentPage + 1];
fireEvent.click(pageButton);

expect(mockChangePage).toHaveBeenCalledTimes(1);
});

it('disables the prev/next button when there is no previous/next page', () => {
render(
<Pagination
{...mockPaginationResponse}
previousPage={null}
nextPage={null}
/>,
);

const previousButton = screen.getByTestId('prev-btn');
const nextButton = screen.getByTestId('next-btn');

expect(previousButton).toBeDisabled();
expect(nextButton).toBeDisabled();
});

it('disables the page button when it is the current page', () => {
render(<Pagination {...mockPaginationResponse} />);

const pageButton =
screen.getAllByRole('button')[mockPaginationResponse.currentPage];
fireEvent.click(pageButton);
expect(pageButton).toBeDisabled();
expect(mockChangePage).not.toHaveBeenCalled();
});
});
30 changes: 30 additions & 0 deletions __tests__/components/PaginationButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it , vi } from 'vitest';

import { render, screen , fireEvent} from '@testing-library/react';
import { PaginationButton } from '@/components/pagination/PaginationButton';


describe('PaginationButton', () => {
it('renders a button with the correct classes', () => {
const className = 'custom-class';
render(<PaginationButton className={className}>Click me</PaginationButton>);
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toHaveClass(className);
});

it('includes screen reader text when srText is provided', () => {
const srText = 'Screen reader text';
render(<PaginationButton srText={srText}>Click me</PaginationButton>);
expect(screen.getByText(srText)).toBeInTheDocument();
});

it('handles click events', () => {
const handleClick = vi.fn();
const text = 'Click me';
render(<PaginationButton onClick={handleClick} >{text}</PaginationButton>);
const button = screen.getByRole('button', { name: text });

fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
42 changes: 42 additions & 0 deletions __tests__/hooks/usePaginationLogic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect, vi, Mock , beforeEach} from 'vitest';
import { renderHook } from '@testing-library/react';
import { usePaginationLogic } from '@/lib/hooks/usePaginationLogic';
import { act } from 'react-dom/test-utils';
import { useRouter } from 'next/router';

vi.mock('next/router', () => ({
useRouter: vi.fn(),
}));

describe('usePaginationLogic', () => {
let mockPush: Mock;

beforeEach(() => {
mockPush = vi.fn();
vi.mocked(useRouter).mockReturnValue({ push: mockPush } as any);
vi.clearAllMocks();
});

it('navigates to the correct page on changePage', () => {
const { result } = renderHook(() =>
usePaginationLogic({ nextPage: 3, previousPage: 1, lastPage: 5 })
);

act(() => {
result.current.changePage(2);
});

expect(mockPush).toHaveBeenCalledWith('/task/2');
});

it('does not navigate on invalid page number', () => {
const { result } = renderHook(() =>
usePaginationLogic({ nextPage: 3, previousPage: 1, lastPage: 5 })
);

act(() => {
result.current.changePage(6);
});
expect(mockPush).not.toHaveBeenCalled();
});
});
15 changes: 15 additions & 0 deletions __tests__/mocks/get-crew-response.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MEMBERS_MOCK_RESULT } from '@/__tests__/mocks/members.mock';
import { CREW_MEMBERS_PER_PAGE } from '@/config/constants';

export const GET_CREW_RESPONSE_MOCK_DATA: CrewResponse = {
data: MEMBERS_MOCK_RESULT,
pagination: {
currentPage: 1,
totalPages: Math.ceil(MEMBERS_MOCK_RESULT.length / CREW_MEMBERS_PER_PAGE),
totalItems: MEMBERS_MOCK_RESULT.length,
itemsPerPage: CREW_MEMBERS_PER_PAGE,
lastPage: Math.ceil(MEMBERS_MOCK_RESULT.length / CREW_MEMBERS_PER_PAGE),
previousPage: null,
nextPage: null,
},
};
Loading