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

React Native technical test solution #2

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
API_KEY=
USERNAME=
REASON=
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: '@react-native-community',
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ web-build/

# macOS
.DS_Store

.env
6 changes: 6 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
bracketSpacing: true,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
};
28 changes: 0 additions & 28 deletions App.js

This file was deleted.

34 changes: 34 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import { UserCalendar } from './src/components/UserCalendar';
import EventDetails from './src/components/EventDetails';

import { Event } from './src/calendarData';
import { EventLikesProvider } from './src/modules/eventLikes/context/eventLikesProvider';
import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

export type RootStackParamList = {
Calendar: undefined;
Event: { event: Event };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function App() {
return (
<QueryClientProvider client={queryClient}>
<EventLikesProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Calendar" component={UserCalendar} />
<Stack.Screen name="Event" component={EventDetails} />
</Stack.Navigator>
</NavigationContainer>
</EventLikesProvider>
</QueryClientProvider>
);
}
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,55 @@ We would like you to make the following improvements:
A perfect looking screen is much less important than making sure you are pleased with your code quality and have something that works end-to-end at least minimally.

*Good luck, and have fun!*

## My Additions

My first task was to convert the project to use TypeScript, ESLint and Prettier. I consider these standard
requirements for any project I work on these days, and the time savings lost by implementing them up front
I gained later by not having to mentally keep track of APIs as TypeScript can handle these for me.

Next, I installed React Native Test Library and updated the existing test for the EventDetails screen.

Next I removed the snapshot test as I find these utterly pointless and raise too many false
positives during development, as half the time when updating project code it breaks snapshots for no actual
reason other than they don't match - even if the functionality is identical. For this reason I prefer to
test how the screens and components would be actually used by an end user, as the only time I want to know
a test has failed is when it functionally breaks. For testing how a component actually looks there are better
tools such as visual regression testing.

Following this, I added a failing test that checked if a like button existed on the screen and was set to
the default 'not-liked' setting. If I'm 100% honest, this was my intent but I still hadn't fully formed the
actual code in my head yet so did the opposite of this state. Either way, I had a failing test I could then
build my code against.

My next step was to create the code to pass the test. I chose to create a context provider that would allow
passing an array of 'liked' event IDs, and subsequently created a custom hook that passed this value and a
toggle function. I could then use the values from this hook in the event details screen to check whether
the screen's event id was included in this list and display the appropriate like/unlike button accordingly -
with a press handler than took care of this functionality.

After a small refactor to encapsulate this feature within its own component, I was able to update the failing
unit test to make sure when pressing the like/unlike button the component updated accordingly.

Next I imported this same hook into the calendar screen and was able to check each event for whether it was
liked as it was being rendered. If so, I added the like-enabled image to the end of the calendar item.

It might be worth noting that I turned the renderItem/renderSectionHeader functions back into inline functions
in the SectionList component. This was primarily done to simplify typing these functions, but it also served
a practical purpose for the renderItem function as I was unable to include my custom hook when this function
was extracted and, due to the time pressure, I figured it wasn't worth refactoring it to allow it.

Finally, I began to work on the calendar API call but ran out of time on this feature. I began by installing
the react-query package and adding its context provider, and I began to add its query hook into the calendar
screen (this code is still there, but commented out so you can see the approach I was going for).

I then created an external API file that included the fetch call to the API response, which would then return
the normalised response back to the component in order to be rendered. I was able to call the API successfully
using the provided API string and credentials, however I didn't think it'd be very smart to commit secret
data to a public github repository so planned to use a `.env` file to include those values securely. However,
I wasn't able to get this feature of Expo to work properly and by then I'd run out of time, so decided to call
it a day.

As the code to make the API call was correct and returning data back to the Calendar screen, the only main
piece of code required to complete was to normalise the API response into a properly typed and SectionList-ready
format.
79 changes: 0 additions & 79 deletions __tests__/components/__snapshots__/eventDetails.test.js.snap

This file was deleted.

14 changes: 0 additions & 14 deletions __tests__/components/eventDetails.test.js

This file was deleted.

49 changes: 49 additions & 0 deletions __tests__/components/eventDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react-native';

import EventDetails from '../../src/components/EventDetails';

import { event } from '../fixtures/events';
import { EventLikesProvider } from '../../src/modules/eventLikes/context/eventLikesProvider';

describe('EventDetails', () => {
test('renders event info correctly', () => {
const props = {
navigation: jest.fn(),
route: { params: { event } },
};

// @ts-expect-error incorrect props being passed (for now)
const { getByText } = render(<EventDetails {...props} />);

const artistTitle = getByText(
`${event.performance[0].displayName} at ${event.venue.displayName}`,
);

expect(artistTitle).toBeDefined();
});

test('like and dislike button', () => {
const props = {
navigation: jest.fn(),
route: { params: { event } },
};

const { getByA11yLabel } = render(
<EventLikesProvider>
{/* @ts-expect-error incorrect props being passed (for now) */}
<EventDetails {...props} />
</EventLikesProvider>,
);

const notLikedButton = getByA11yLabel('not-liked');

expect(notLikedButton).toBeDefined();

fireEvent.press(notLikedButton);

const likedButton = getByA11yLabel('liked');

expect(likedButton).toBeDefined();
});
});
20 changes: 10 additions & 10 deletions __tests__/fixtures/events.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
export const event = {
id: 39657660,
displayName: "Biffy Clyro at O2 Forum Kentish Town (November 2, 2021)",
status: "ok",
displayName: 'Biffy Clyro at O2 Forum Kentish Town (November 2, 2021)',
status: 'ok',
start: {
date: "2021-11-02",
datetime: "2021-11-02T19:00:00+0000",
time: "19:00:00",
date: '2021-11-02',
datetime: '2021-11-02T19:00:00+0000',
time: '19:00:00',
},
performance: [
{
id: 75006083,
displayName: "Biffy Clyro",
billing: "headline",
displayName: 'Biffy Clyro',
billing: 'headline',
billingIndex: 1,
},
],
venue: {
id: 37414,
displayName: "O2 Forum Kentish Town",
displayName: 'O2 Forum Kentish Town',
metroArea: {
displayName: "London",
country: { displayName: "UK" },
displayName: 'London',
country: { displayName: 'UK' },
},
},
};
3 changes: 2 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = function(api) {
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['transform-inline-environment-variables'],
};
};
6 changes: 6 additions & 0 deletions jest/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-env jest */

import '@testing-library/jest-native/extend-expect';

// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
Loading