├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc
├── .travis.yml
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── client.js
├── index.js
├── registerServiceWorker.js
└── test
├── client-mock.js
├── schema.js
└── setup.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: rwieruch
4 | patreon: # rwieruch
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with a single custom sponsorship URL
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 70,
6 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - stable
5 |
6 | install:
7 | - npm install
8 |
9 | script:
10 | - npm test
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-apollo-client-testing-example
2 |
3 | [](https://travis-ci.org/the-road-to-graphql/react-apollo-client-testing-example) [](https://slack-the-road-to-learn-react.wieruch.com/) [](https://greenkeeper.io/)
4 |
5 | A minimal React application using Apollo Client with a **mocked** GitHub's GraphQL API by implementing an own GraphQL schema. [Read more about it here.](https://www.robinwieruch.de/graphql-server-mock-apollo-client/) In addition, it uses the mocked GraphQL server to conduct tests for Apollo's Query and Mutation components. [Read more about it here.](https://www.robinwieruch.de/react-apollo-client-testing)
6 |
7 | ## Installation
8 |
9 | * `git clone git@github.com:the-road-to-graphql/react-apollo-client-testing-example.git`
10 | * cd react-apollo-client-testing-example
11 | * npm install
12 | * [add your own REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN in .env file](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)
13 | * scopes/permissions you need to check: admin:org, repo, user, notifications
14 | * npm test
15 |
16 | ## Want to learn more about React + GraphQL + Apollo?
17 |
18 | * Don't miss [upcoming Tutorials and Courses](https://www.getrevue.co/profile/rwieruch)
19 | * Check out current [React Courses](https://roadtoreact.com)
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-apollo-client-testing-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-cache-inmemory": "^1.3.6",
7 | "apollo-client": "^2.4.3",
8 | "apollo-link-http": "^1.5.5",
9 | "apollo-link-schema": "^1.1.1",
10 | "graphql": "^14.0.2",
11 | "graphql-tag": "^2.10.0",
12 | "graphql-tools": "^4.0.2",
13 | "react": "^16.6.0",
14 | "react-apollo": "^2.2.4",
15 | "react-dom": "^16.6.0",
16 | "react-scripts": "3.1.0"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject"
23 | },
24 | "devDependencies": {
25 | "enzyme": "^3.7.0",
26 | "enzyme-adapter-react-16": "^1.6.0",
27 | "sinon": "^7.0.0"
28 | },
29 | "browserslist": [
30 | ">0.2%",
31 | "not dead",
32 | "not ie <= 11",
33 | "not op_mini all"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-road-to-graphql/react-apollo-client-testing-example/2a5fcb4d2212ad17e3be1b9c753f9acbcbecbc7b/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .row {
2 | padding: 5px;
3 | }
4 |
5 | .row:hover {
6 | background-color: lightblue;
7 | }
8 |
9 | .row_selected {
10 | background-color: orange;
11 | }
12 |
13 | .row_selected:hover {
14 | background-color: orange;
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import gql from 'graphql-tag';
3 | import { Query, Mutation } from 'react-apollo';
4 |
5 | import './App.css';
6 |
7 | const GET_REPOSITORIES_OF_ORGANIZATION = gql`
8 | {
9 | organization(login: "the-road-to-learn-react") {
10 | repositories(first: 20) {
11 | edges {
12 | node {
13 | id
14 | name
15 | url
16 | viewerHasStarred
17 | }
18 | }
19 | }
20 | }
21 | }
22 | `;
23 |
24 | const STAR_REPOSITORY = gql`
25 | mutation($id: ID!) {
26 | addStar(input: { starrableId: $id }) {
27 | starrable {
28 | id
29 | viewerHasStarred
30 | }
31 | }
32 | }
33 | `;
34 |
35 | const App = () => (
36 |
37 | {({ data: { organization }, loading }) => {
38 | if (loading || !organization) {
39 | return Loading ...
;
40 | }
41 |
42 | return (
43 |
44 | );
45 | }}
46 |
47 | );
48 |
49 | class Repositories extends React.Component {
50 | state = {
51 | selectedRepositoryIds: [],
52 | };
53 |
54 | toggleSelectRepository = (id, isSelected) => {
55 | let { selectedRepositoryIds } = this.state;
56 |
57 | selectedRepositoryIds = isSelected
58 | ? selectedRepositoryIds.filter(itemId => itemId !== id)
59 | : selectedRepositoryIds.concat(id);
60 |
61 | this.setState({ selectedRepositoryIds });
62 | };
63 |
64 | render() {
65 | return (
66 |
71 | );
72 | }
73 | }
74 |
75 | const RepositoryList = ({
76 | repositories,
77 | selectedRepositoryIds,
78 | toggleSelectRepository,
79 | }) => (
80 |
81 | {repositories.edges.map(({ node }) => {
82 | const isSelected = selectedRepositoryIds.includes(node.id);
83 |
84 | const rowClassName = ['row'];
85 |
86 | if (isSelected) {
87 | rowClassName.push('row_selected');
88 | }
89 |
90 | return (
91 | -
92 | {' '}
97 | {node.name}{' '}
98 | {!node.viewerHasStarred && }
99 |
100 | );
101 | })}
102 |
103 | );
104 |
105 | const Star = ({ id }) => (
106 |
107 | {starRepository => (
108 |
111 | )}
112 |
113 | );
114 |
115 | const Select = ({ id, isSelected, toggleSelectRepository }) => (
116 |
122 | );
123 |
124 | export { Star, STAR_REPOSITORY, GET_REPOSITORIES_OF_ORGANIZATION };
125 |
126 | export default App;
127 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ApolloProvider } from 'react-apollo';
3 | import { mount } from 'enzyme';
4 | import { spy } from 'sinon';
5 |
6 | import './test/setup';
7 | import clientMock from './test/client-mock';
8 |
9 | import App, {
10 | Star,
11 | STAR_REPOSITORY,
12 | GET_REPOSITORIES_OF_ORGANIZATION,
13 | } from './App';
14 |
15 | describe('Star', () => {
16 | it('calls the mutate method on Apollo Client', () => {
17 | spy(clientMock, 'mutate');
18 |
19 | const wrapper = mount(
20 |
21 |
22 | ,
23 | );
24 |
25 | wrapper.find('button').simulate('click');
26 |
27 | expect(clientMock.mutate.calledOnce).toEqual(true);
28 |
29 | expect(clientMock.mutate.getCall(0).args[0].variables).toEqual({
30 | id: '1',
31 | });
32 |
33 | expect(clientMock.mutate.getCall(0).args[0].mutation).toEqual(
34 | STAR_REPOSITORY,
35 | );
36 |
37 | clientMock.mutate.restore();
38 | });
39 | });
40 |
41 | describe('App', () => {
42 | it('calls the query method on Apollo Client', () => {
43 | spy(clientMock, 'watchQuery');
44 |
45 | const wrapper = mount(
46 |
47 |
48 | ,
49 | );
50 |
51 | expect(clientMock.watchQuery.calledOnce).toEqual(true);
52 |
53 | expect(clientMock.watchQuery.getCall(0).args[0].query).toEqual(
54 | GET_REPOSITORIES_OF_ORGANIZATION,
55 | );
56 |
57 | clientMock.watchQuery.restore();
58 | });
59 |
60 | it('renders correctly after the query method on Apollo Client executed', () => {
61 | const wrapper = mount(
62 |
63 |
64 | ,
65 | );
66 |
67 | expect(
68 | wrapper
69 | .find('Repositories')
70 | .find('RepositoryList')
71 | .find('li').length,
72 | ).toEqual(2);
73 |
74 | expect(
75 | wrapper.find('Repositories').props().repositories.edges[0].node
76 | .id,
77 | ).toEqual('1');
78 |
79 | expect(
80 | wrapper.find('Repositories').props().repositories.edges[1].node
81 | .id,
82 | ).toEqual('2');
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client';
2 | import { HttpLink } from 'apollo-link-http';
3 | import { InMemoryCache } from 'apollo-cache-inmemory';
4 |
5 | const cache = new InMemoryCache();
6 |
7 | const GITHUB_BASE_URL = 'https://api.github.com/graphql';
8 |
9 | const httpLink = new HttpLink({
10 | uri: GITHUB_BASE_URL,
11 | headers: {
12 | authorization: `Bearer ${
13 | process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN
14 | }`,
15 | },
16 | });
17 |
18 | export default new ApolloClient({
19 | link: httpLink,
20 | cache,
21 | });
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { ApolloProvider } from 'react-apollo';
4 |
5 | import App from './App';
6 | import client from './client';
7 |
8 | import registerServiceWorker from './registerServiceWorker';
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root'),
15 | );
16 |
17 | registerServiceWorker();
18 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/test/client-mock.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client';
2 | import { InMemoryCache } from 'apollo-cache-inmemory';
3 | import { SchemaLink } from 'apollo-link-schema';
4 | import { makeExecutableSchema } from 'graphql-tools';
5 |
6 | import { schema, resolvers } from './schema';
7 |
8 | const cache = new InMemoryCache();
9 |
10 | const executableSchema = makeExecutableSchema({
11 | typeDefs: schema,
12 | resolvers,
13 | resolverValidationOptions: {
14 | requireResolversForResolveType: false,
15 | },
16 | });
17 |
18 | export default new ApolloClient({
19 | link: new SchemaLink({ schema: executableSchema }),
20 | cache,
21 | });
22 |
--------------------------------------------------------------------------------
/src/test/schema.js:
--------------------------------------------------------------------------------
1 | export const schema = `
2 | type Query {
3 | organization(login: String!): Organization!
4 | }
5 |
6 | interface Starrable {
7 | id: ID!
8 | viewerHasStarred: Boolean!
9 | }
10 |
11 | type Organization {
12 | name: String!
13 | url: String!
14 | repositories: RepositoryConnection!
15 | }
16 |
17 | type RepositoryConnection {
18 | edges: [RepositoryEdge!]!
19 | }
20 |
21 | type RepositoryEdge {
22 | node: Repository!
23 | }
24 |
25 | type Repository implements Starrable {
26 | id: ID!
27 | name: String!
28 | url: String!
29 | viewerHasStarred: Boolean!
30 | }
31 |
32 | type Mutation {
33 | addStar(input: AddStarInput!): AddStarPayload!
34 | }
35 |
36 | input AddStarInput {
37 | starrableId: ID!
38 | }
39 |
40 | type AddStarPayload {
41 | starrable: Starrable!
42 | }
43 | `;
44 |
45 | export const resolvers = {
46 | Query: {
47 | organization: (parent, { login }) => ({
48 | name: login,
49 | url: `https://github.com/${login}`,
50 | repositories: {
51 | edges: [
52 | {
53 | node: {
54 | id: '1',
55 | name: 'the-road-to-learn-react',
56 | url: `https://github.com/${login}/the-road-to-learn-react`,
57 | viewerHasStarred: false,
58 | },
59 | },
60 | {
61 | node: {
62 | id: '2',
63 | name: 'the-road-to-learn-react-chinese',
64 | url: `https://github.com/${login}/the-road-to-learn-react-chinese`,
65 | viewerHasStarred: false,
66 | },
67 | },
68 | ],
69 | },
70 | }),
71 | },
72 | Mutation: {
73 | addStar: (parent, { input }) => ({
74 | starrable: {
75 | id: input.starrableId,
76 | viewerHasStarred: true,
77 | },
78 | }),
79 | },
80 | Starrable: {
81 | __resolveType: () => 'Repository',
82 | },
83 | };
84 |
--------------------------------------------------------------------------------
/src/test/setup.js:
--------------------------------------------------------------------------------
1 | import Adapter from 'enzyme-adapter-react-16';
2 | import { configure } from 'enzyme';
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------