Repository: ocadotechnology/aimmo

Issue:
[New UI] Introduce different reducers aside from editor
## New UI - Fix on `UI_new_design` branch

We currently only have one reducer that we use, editor. This should be splitted per feature into different reducers.


Discussion:

Resolving patch:
diff --git a/players/game_renderer.py b/players/game_renderer.py
new file mode 100644
--- /dev/null
+++ b/players/game_renderer.py
@@ -0,0 +1,41 @@
+"""
+Any helper functions used for the unity game.
+"""
+
+from django.shortcuts import render
+from players import app_settings
+
+
+def render_game(request, game):
+    """
+    :param request: Request object used to generate this response.
+    :param game: Game object.
+    :return: HttpResponse object with a given context dictionary and a template.
+    """
+    context = {'current_user_player_key': request.user.pk, 'active': game.is_active,
+               'static_data': game.static_data or '{}'
+               }
+
+    connection_settings = get_environment_connection_settings(game.id)
+
+    context.update(connection_settings)
+
+    return render(request, 'players/viewer.html', context)
+
+
+def get_environment_connection_settings(game_id):
+    """
+    This function will return the correct URL parts and a SSL flag
+    based on the environment of which it exists in.
+
+    :param request: Request object used to generate this response.
+    :param game_id: Integer with the ID of the game.
+    :return: A dict object with all relevant settings.
+    """
+
+    return {
+        'game_url_base': app_settings.GAME_SERVER_URL_FUNCTION(game_id)[0],
+        'game_url_path': app_settings.GAME_SERVER_URL_FUNCTION(game_id)[1],
+        'game_url_port': app_settings.GAME_SERVER_PORT_FUNCTION(game_id),
+        'game_ssl_flag': app_settings.GAME_SERVER_SSL_FLAG, 'game_id': game_id
+    }
diff --git a/players/urls.py b/players/urls.py
--- a/players/urls.py
+++ b/players/urls.py
@@ -21,9 +21,11 @@
     url(r'^statistics/$', TemplateView.as_view(template_name='players/statistics.html'), name='aimmo/statistics'),
     url(r'^game_ide/$', TemplateView.as_view(template_name='players/game_ide.html'), name='aimmo/game_ide'),
 
+    url(r'^api/csrf_token', views.csrfToken, name='aimmo/csrf_token'),
     url(r'^api/code/(?P<id>[0-9]+)/$', views.code, name='aimmo/code'),
     url(r'^api/games/$', views.list_games, name='aimmo/games'),
     url(r'^api/games/(?P<id>[0-9]+)/$', views.get_game, name='aimmo/game_details'),
+    url(r'^api/games/(?P<id>[0-9]+)/connection_parameters/$', views.connection_parameters, name='aimmo/connection_parameters'),
     url(r'^api/games/(?P<id>[0-9]+)/complete/$', views.mark_game_complete, name='aimmo/complete_game'),
     url(r'^api/games/(?P<game_id>[0-9]+)/current_avatar/$', views.current_avatar_in_game, name='aimmo/current_avatar_in_game'),
 
diff --git a/players/views.py b/players/views.py
--- a/players/views.py
+++ b/players/views.py
@@ -4,23 +4,19 @@
 
 from django.contrib.auth.decorators import login_required
 from django.core.exceptions import ValidationError
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse, Http404
-from django.http import JsonResponse
+from django.http import HttpResponse, JsonResponse, Http404, HttpResponseForbidden
 from django.shortcuts import redirect, render, get_object_or_404
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_http_methods
 from django.views.generic import TemplateView
+from django.middleware.csrf import get_token
 
 from models import Avatar, Game, LevelAttempt
-from players import forms
-from . import app_settings
-from app_settings import get_users_for_new_game
+from players import forms, game_renderer
+from app_settings import get_users_for_new_game, preview_user_required
 
 LOGGER = logging.getLogger(__name__)
 
-preview_user_required = app_settings.preview_user_required
-
 
 def _post_code_success_response(message):
     return _create_response("SUCCESS", message)
@@ -37,6 +33,8 @@ def _create_response(status, message):
 @login_required
 @preview_user_required
 def code(request, id):
+    if not request.user:
+        return HttpResponseForbidden()
     game = get_object_or_404(Game, id=id)
     if not game.can_user_play(request.user):
         raise Http404
@@ -54,10 +52,9 @@ def code(request, id):
     if request.method == 'POST':
         avatar.code = request.POST['code']
         avatar.save()
-        return _post_code_success_response(
-            'Your code was saved!<br><br><a href="%s">Watch</a>' % reverse('aimmo/watch', kwargs={'id': game.id}))
+        return HttpResponse(status=200)
     else:
-        return HttpResponse(avatar.code)
+        return JsonResponse({'code': avatar.code})
 
 
 def list_games(request):
@@ -90,6 +87,19 @@ def get_game(request, id):
     return JsonResponse(response)
 
 
+def connection_parameters(request, id):
+    """
+    An API view which returns the correct connection settings required
+    to run the game in different environments. These values will change
+    depending on where the project is started (ie. local, etc).
+    :param id: Integer with the ID of the game.
+    :return: JsonResponse object with the contents.
+    """
+    return JsonResponse(
+        game_renderer.get_environment_connection_settings(id)
+    )
+
+
 @csrf_exempt
 @require_http_methods(['POST'])
 def mark_game_complete(request, id):
@@ -122,26 +132,11 @@ def program_level(request, num):
     return render(request, 'players/program.html', {'game_id': game.id})
 
 
-def _render_game(request, game):
-    context = {
-        'current_user_player_key': request.user.pk,
-        'active': game.is_active,
-        'static_data': game.static_data or '{}',
-    }
-    context['game_url_base'], context['game_url_path'] = app_settings.GAME_SERVER_URL_FUNCTION(game.id)
-    context['game_url_port'] = app_settings.GAME_SERVER_PORT_FUNCTION(game.id)
-    context['game_ssl_flag'] = app_settings.GAME_SERVER_SSL_FLAG
-    context['game_id'] = game.id
-    context['web_host_base'] = request.get_host()
-
-    return render(request, 'players/viewer.html', context)
-
-
 def watch_game(request, id):
     game = get_object_or_404(Game, id=id)
     if not game.can_user_play(request.user):
         raise Http404
-    return _render_game(request, game)
+    return game_renderer.render_game(request, game)
 
 
 def watch_level(request, num):
@@ -151,7 +146,7 @@ def watch_level(request, num):
         LOGGER.debug('Adding level')
         game = _add_and_return_level(num, request.user)
     LOGGER.debug('Displaying game with id %s', game.id)
-    return _render_game(request, game)
+    return game_renderer.render_game(request, game)
 
 
 def _add_and_return_level(num, user):
@@ -200,3 +195,11 @@ def current_avatar_in_game(request, game_id):
         return HttpResponse('Avatar does not exist for this user', status=404)
 
     return JsonResponse({'current_avatar_id': avatar.id})
+
+
+def csrfToken(request):
+    if request.method == 'GET':
+        token = get_token(request)
+        return JsonResponse({'csrfToken': token})
+    else:
+        return HttpResponse(status=405)


Test patch:
diff --git a/game_frontend/src/components/Code/__snapshots__/index.test.js.snap b/game_frontend/src/components/Code/__snapshots__/index.test.js.snap
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/components/Code/__snapshots__/index.test.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Code /> renders correctly 1`] = `
+.c0 {
+  font-family: 'Source Code Pro',monospace;
+  color: #FFFFFF;
+}
+
+<p
+  className="c0"
+>
+  Code
+</p>
+`;
diff --git a/game_frontend/src/components/Code/index.test.js b/game_frontend/src/components/Code/index.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/components/Code/index.test.js
@@ -0,0 +1,11 @@
+/* eslint-env jest */
+import React from 'react'
+import Code from 'components/Code'
+import createShallowWithTheme from 'testHelpers/createShallow'
+
+describe('<Code />', () => {
+  it('renders correctly', () => {
+    const tree = createShallowWithTheme(<Code>Code</Code>)
+    expect(tree).toMatchSnapshot()
+  })
+})
diff --git a/game_frontend/src/components/Game/__snapshots__/index.test.js.snap b/game_frontend/src/components/Game/__snapshots__/index.test.js.snap
--- a/game_frontend/src/components/Game/__snapshots__/index.test.js.snap
+++ b/game_frontend/src/components/Game/__snapshots__/index.test.js.snap
@@ -3,6 +3,6 @@
 exports[`<Game /> renders correctly 1`] = `
 <React.Fragment>
   <styled.nav />
-  <styled.div />
+  <Connect(GameView) />
 </React.Fragment>
 `;
diff --git a/game_frontend/src/components/GameMenu/__snapshots__/index.test.js.snap b/game_frontend/src/components/GameMenu/__snapshots__/index.test.js.snap
--- a/game_frontend/src/components/GameMenu/__snapshots__/index.test.js.snap
+++ b/game_frontend/src/components/GameMenu/__snapshots__/index.test.js.snap
@@ -2,7 +2,7 @@
 
 exports[`<GameMenu /> renders correctly 1`] = `
 .c0 {
-  background-color: pink;
+  background-color: #303030;
   grid-area: game-menu;
 }
 
diff --git a/game_frontend/src/components/GameMenu/index.test.js b/game_frontend/src/components/GameMenu/index.test.js
--- a/game_frontend/src/components/GameMenu/index.test.js
+++ b/game_frontend/src/components/GameMenu/index.test.js
@@ -1,11 +1,11 @@
 /* eslint-env jest */
 import React from 'react'
 import GameMenu from 'components/GameMenu'
-import renderer from 'react-test-renderer'
+import createShallowWithTheme from 'testHelpers/createShallow'
 
 describe('<GameMenu />', () => {
   it('renders correctly', () => {
-    const tree = renderer.create(<GameMenu />).toJSON()
+    const tree = createShallowWithTheme(<GameMenu />)
     expect(tree).toMatchSnapshot()
   })
 })
diff --git a/game_frontend/src/components/GamePage/__snapshots__/index.test.js.snap b/game_frontend/src/components/GamePage/__snapshots__/index.test.js.snap
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/components/GamePage/__snapshots__/index.test.js.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<GamePage /> matches snapshot 1`] = `
+<styled.div>
+  <IDE />
+  <Game />
+</styled.div>
+`;
+
+exports[`<GamePageLayout /> matches snapshot 1`] = `
+.c0 {
+  display: grid;
+  grid-template: 80px 1fr 150px / 1fr 1fr;
+  grid-template-areas: "ide-menu game-menu" "ide-editor game-view" "ide-console game-view";
+  width: 100vw;
+  height: 100vh;
+}
+
+<div
+  className="c0"
+/>
+`;
diff --git a/game_frontend/src/components/GamePage/index.test.js b/game_frontend/src/components/GamePage/index.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/components/GamePage/index.test.js
@@ -0,0 +1,21 @@
+/* eslint-env jest */
+import React from 'react'
+import GamePage, { GamePageLayout } from 'components/GamePage'
+import createShallowWithTheme from 'testHelpers/createShallow'
+import { shallow } from 'enzyme'
+
+describe('<GamePage />', () => {
+  it('matches snapshot', () => {
+    const tree = shallow(<GamePage />)
+
+    expect(tree).toMatchSnapshot()
+  })
+})
+
+describe('<GamePageLayout />', () => {
+  it('matches snapshot', () => {
+    const tree = createShallowWithTheme(<GamePageLayout />)
+
+    expect(tree).toMatchSnapshot()
+  })
+})
diff --git a/game_frontend/src/components/GameView/__snapshots__/index.test.js.snap b/game_frontend/src/components/GameView/__snapshots__/index.test.js.snap
deleted file mode 100644
--- a/game_frontend/src/components/GameView/__snapshots__/index.test.js.snap
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`<GameView /> renders correctly 1`] = `
-.c0 {
-  background-color: purple;
-  grid-area: game-view;
-}
-
-<div
-  className="c0"
-/>
-`;
diff --git a/game_frontend/src/components/GameView/index.test.js b/game_frontend/src/components/GameView/index.test.js
deleted file mode 100644
--- a/game_frontend/src/components/GameView/index.test.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-env jest */
-import React from 'react'
-import GameView from 'components/GameView'
-import renderer from 'react-test-renderer'
-
-describe('<GameView />', () => {
-  it('renders correctly', () => {
-    const tree = renderer.create(<GameView />).toJSON()
-    expect(tree).toMatchSnapshot()
-  })
-})
diff --git a/game_frontend/src/components/IDE/__snapshots__/index.test.js.snap b/game_frontend/src/components/IDE/__snapshots__/index.test.js.snap
--- a/game_frontend/src/components/IDE/__snapshots__/index.test.js.snap
+++ b/game_frontend/src/components/IDE/__snapshots__/index.test.js.snap
@@ -2,8 +2,8 @@
 
 exports[`<IDE /> renders correctly 1`] = `
 <React.Fragment>
-  <styled.nav />
-  <styled.div />
+  <Connect(IDEMenu) />
+  <Connect(WithTheme(IDEEditor)) />
   <styled.div />
 </React.Fragment>
 `;
diff --git a/game_frontend/src/components/IDEConsole/__snapshots__/index.test.js.snap b/game_frontend/src/components/IDEConsole/__snapshots__/index.test.js.snap
--- a/game_frontend/src/components/IDEConsole/__snapshots__/index.test.js.snap
+++ b/game_frontend/src/components/IDEConsole/__snapshots__/index.test.js.snap
@@ -2,7 +2,7 @@
 
 exports[`<IDEConsole /> renders correctly 1`] = `
 .c0 {
-  background-color: #DDDDDD;
+  background-color: #303030;
   grid-area: ide-console;
 }
 
diff --git a/game_frontend/src/components/IDEConsole/index.test.js b/game_frontend/src/components/IDEConsole/index.test.js
--- a/game_frontend/src/components/IDEConsole/index.test.js
+++ b/game_frontend/src/components/IDEConsole/index.test.js
@@ -1,11 +1,11 @@
 /* eslint-env jest */
 import React from 'react'
 import IDEConsole from 'components/IDEConsole'
-import renderer from 'react-test-renderer'
+import createShallowWithTheme from 'testHelpers/createShallow'
 
 describe('<IDEConsole />', () => {
   it('renders correctly', () => {
-    const tree = renderer.create(<IDEConsole></IDEConsole>).toJSON()
+    const tree = createShallowWithTheme(<IDEConsole />)
     expect(tree).toMatchSnapshot()
   })
 })
diff --git a/game_frontend/src/components/IDEEditor/__snapshots__/index.test.js.snap b/game_frontend/src/components/IDEEditor/__snapshots__/index.test.js.snap
deleted file mode 100644
--- a/game_frontend/src/components/IDEEditor/__snapshots__/index.test.js.snap
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`<IDEEditor /> renders correctly 1`] = `
-.c0 {
-  background-color: #2F4F4F;
-  grid-area: ide-editor;
-}
-
-<div
-  className="c0"
-/>
-`;
diff --git a/game_frontend/src/components/IDEEditor/index.test.js b/game_frontend/src/components/IDEEditor/index.test.js
deleted file mode 100644
--- a/game_frontend/src/components/IDEEditor/index.test.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-env jest */
-import React from 'react'
-import IDEEditor from 'components/IDEEditor'
-import renderer from 'react-test-renderer'
-
-describe('<IDEEditor />', () => {
-  it('renders correctly', () => {
-    const tree = renderer.create(<IDEEditor></IDEEditor>).toJSON()
-    expect(tree).toMatchSnapshot()
-  })
-})
diff --git a/game_frontend/src/components/IDEMenu/__snapshots__/index.test.js.snap b/game_frontend/src/components/IDEMenu/__snapshots__/index.test.js.snap
deleted file mode 100644
--- a/game_frontend/src/components/IDEMenu/__snapshots__/index.test.js.snap
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`<IDEMenu /> renders correctly 1`] = `
-.c0 {
-  background-color: pink;
-  grid-area: ide-menu;
-}
-
-<nav
-  className="c0"
-/>
-`;
diff --git a/game_frontend/src/components/IDEMenu/index.test.js b/game_frontend/src/components/IDEMenu/index.test.js
deleted file mode 100644
--- a/game_frontend/src/components/IDEMenu/index.test.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-env jest */
-import React from 'react'
-import IDEMenu from 'components/IDEMenu'
-import renderer from 'react-test-renderer'
-
-describe('<IDEMenu />', () => {
-  it('renders correctly', () => {
-    const tree = renderer.create(<IDEMenu></IDEMenu>).toJSON()
-    expect(tree).toMatchSnapshot()
-  })
-})
diff --git a/game_frontend/src/containers/GameView/__snapshots__/index.test.js.snap b/game_frontend/src/containers/GameView/__snapshots__/index.test.js.snap
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/containers/GameView/__snapshots__/index.test.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<GameView /> matches snapshot 1`] = `
+<styled.div>
+  <Unity
+    loader="/static/unity/Build/UnityLoader.js"
+    src="/static/unity/Build/unity.json"
+  />
+</styled.div>
+`;
+
+exports[`<GameViewLayout /> matches snapshot 1`] = `
+.c0 {
+  grid-area: game-view;
+}
+
+<div
+  className="c0"
+/>
+`;
diff --git a/game_frontend/src/containers/GameView/index.test.js b/game_frontend/src/containers/GameView/index.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/containers/GameView/index.test.js
@@ -0,0 +1,72 @@
+/* eslint-env jest */
+import React from 'react'
+import { GameView, GameViewLayout } from 'containers/GameView'
+import { shallow } from 'enzyme/build/index'
+
+describe('<GameView />', () => {
+  it('matches snapshot', () => {
+    const props = {
+      gameURL: 'test',
+      gamePath: '/test',
+      gamePort: 8000,
+      gameSSL: false,
+      getConnectionParameters: jest.fn(),
+      setGameURL: jest.fn(),
+      setGamePath: jest.fn(),
+      setGamePort: jest.fn(),
+      setGameSSL: jest.fn(),
+      establishGameConnection: jest.fn()
+    }
+
+    const component = shallow(<GameView {...props} />)
+    expect(component).toMatchSnapshot()
+  })
+
+  it('serialisedSSLFlag function returns correctly', () => {
+    const props = {
+      getConnectionParameters: jest.fn(),
+      gameSSL: false
+    }
+
+    const flagReturned = shallow(<GameView {...props} />).instance().serialisedSSLFlag()
+    expect(flagReturned).toBe('False')
+  })
+
+  it('sendAllConnect function calls all action dispatchers', () => {
+    const setGameURL = jest.fn()
+    const setGamePath = jest.fn()
+    const setGamePort = jest.fn()
+    const setGameSSL = jest.fn()
+    const establishGameConnection = jest.fn()
+
+    const props = {
+      gameURL: 'test',
+      gamePath: '/test',
+      gamePort: 8000,
+      gameSSL: false,
+      getConnectionParameters: jest.fn(),
+      setGameURL,
+      setGamePath,
+      setGamePort,
+      setGameSSL,
+      establishGameConnection
+    }
+
+    const wrapper = shallow(<GameView {...props} />)
+
+    wrapper.instance().sendAllConnect()
+
+    expect(setGameURL.mock.calls.length).toBe(1)
+    expect(setGamePath.mock.calls.length).toBe(1)
+    expect(setGamePort.mock.calls.length).toBe(1)
+    expect(setGameSSL.mock.calls.length).toBe(1)
+    expect(establishGameConnection.mock.calls.length).toBe(1)
+  })
+})
+
+describe('<GameViewLayout />', () => {
+  it('matches snapshot', () => {
+    const tree = shallow(<GameViewLayout />)
+    expect(tree).toMatchSnapshot()
+  })
+})
diff --git a/game_frontend/src/containers/IDEEditor/__snapshots__/index.test.js.snap b/game_frontend/src/containers/IDEEditor/__snapshots__/index.test.js.snap
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/containers/IDEEditor/__snapshots__/index.test.js.snap
@@ -0,0 +1,62 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<IDEEditor /> matches snapshot 1`] = `
+<styled.div>
+  <ReactAce
+    cursorStart={1}
+    editorProps={Object {}}
+    enableBasicAutocompletion={false}
+    enableLiveAutocompletion={false}
+    focus={false}
+    fontSize="1rem"
+    height="100%"
+    highlightActiveLine={true}
+    maxLines={null}
+    minLines={null}
+    mode="python"
+    name="ace_editor"
+    onChange={null}
+    onLoad={[MockFunction]}
+    onPaste={null}
+    onScroll={null}
+    readOnly={false}
+    scrollMargin={
+      Array [
+        0,
+        0,
+        0,
+        0,
+      ]
+    }
+    setOptions={
+      Object {
+        "enableBasicAutocompletion": true,
+        "enableLiveAutocompletion": true,
+        "enableSnippets": true,
+        "fontFamily": "'Source Code Pro', monospace",
+        "showLineNumbers": true,
+        "tabSize": 2,
+      }
+    }
+    showGutter={true}
+    showPrintMargin={true}
+    style={Object {}}
+    tabSize={4}
+    theme="idle_fingers"
+    value="class Avatar"
+    width="100%"
+    wrapEnabled={false}
+  />
+</styled.div>
+`;
+
+exports[`<IDEEditorLayout /> renders correctly 1`] = `
+.c0 {
+  background-color: #2F4F4F;
+  grid-area: ide-editor;
+}
+
+<div
+  className="c0"
+/>
+`;
diff --git a/game_frontend/src/containers/IDEEditor/index.test.js b/game_frontend/src/containers/IDEEditor/index.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/containers/IDEEditor/index.test.js
@@ -0,0 +1,25 @@
+/* eslint-env jest */
+import React from 'react'
+import { IDEEditor, IDEEditorLayout } from 'containers/IDEEditor'
+import createShallowWithTheme from 'testHelpers/createShallow'
+
+describe('<IDEEditor />', () => {
+  it('matches snapshot', () => {
+    const props = {
+      code: 'class Avatar',
+      getCode: jest.fn(),
+      postCode: jest.fn()
+    }
+
+    const component = createShallowWithTheme(<IDEEditor {...props} />)
+
+    expect(component).toMatchSnapshot()
+  })
+})
+
+describe('<IDEEditorLayout />', () => {
+  it('renders correctly', () => {
+    const tree = createShallowWithTheme(<IDEEditorLayout />)
+    expect(tree).toMatchSnapshot()
+  })
+})
diff --git a/game_frontend/src/containers/IDEMenu/__snapshots__/index.test.js.snap b/game_frontend/src/containers/IDEMenu/__snapshots__/index.test.js.snap
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/containers/IDEMenu/__snapshots__/index.test.js.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<IDEMenu /> renders correctly 1`] = `
+<styled.nav>
+  <button
+    id="post-code-button"
+  >
+    Post Code
+  </button>
+</styled.nav>
+`;
+
+exports[`<IDEMenuLayout /> renders correctly 1`] = `
+.c0 {
+  background-color: #303030;
+  grid-area: ide-menu;
+}
+
+<nav
+  className="c0"
+/>
+`;
diff --git a/game_frontend/src/containers/IDEMenu/index.test.js b/game_frontend/src/containers/IDEMenu/index.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/containers/IDEMenu/index.test.js
@@ -0,0 +1,31 @@
+/* eslint-env jest */
+import React from 'react'
+import { IDEMenu, IDEMenuLayout } from 'containers/IDEMenu'
+import { shallow } from 'enzyme'
+import createShallowWithTheme from 'testHelpers/createShallow'
+
+describe('<IDEMenu />', () => {
+  it('renders correctly', () => {
+    const tree = createShallowWithTheme(<IDEMenu />)
+    expect(tree).toMatchSnapshot()
+  })
+
+  it('calls the postCode function in props when Post code button is pressed', () => {
+    const postCode = jest.fn()
+    const props = {
+      postCode
+    }
+
+    const component = shallow(<IDEMenu {...props} />)
+
+    component.find('#post-code-button').simulate('click')
+    expect(postCode.mock.calls.length).toBe(1)
+  })
+})
+
+describe('<IDEMenuLayout />', () => {
+  it('renders correctly', () => {
+    const tree = createShallowWithTheme(<IDEMenuLayout />)
+    expect(tree).toMatchSnapshot()
+  })
+})
diff --git a/game_frontend/src/redux/features/Editor/epics.test.js b/game_frontend/src/redux/features/Editor/epics.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/redux/features/Editor/epics.test.js
@@ -0,0 +1,150 @@
+/* eslint-env jest */
+import { Observable, TestScheduler } from 'rxjs'
+import { ActionsObservable } from 'redux-observable'
+import epics from './epics'
+import actions from './actions'
+import types from './types'
+import configureStore from 'redux-mock-store'
+
+const middlewares = []
+const mockStore = configureStore(middlewares)
+
+const deepEquals = (actual, expected) =>
+  expect(actual).toEqual(expected)
+
+const createTestScheduler = (frameTimeFactor = 10) => {
+  TestScheduler.frameTimeFactor = frameTimeFactor
+  return new TestScheduler(deepEquals)
+}
+
+describe('getCodeEpic', () => {
+  it('gets the code', () => {
+    const code = 'class Avatar'
+
+    const marbles1 = '-a-'
+    const marbles2 = '-b-'
+    const values = {
+      a: actions.getCodeRequest(),
+      b: actions.getCodeReceived(code)
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+    const mockGetJSON = () => {
+      return Observable.of({ code })
+    }
+
+    const mockAPI = { api: { get: mockGetJSON } }
+
+    const actual = epics.getCodeEpic(source$, mockStore(
+      { game:
+        { connectionParameters:
+          { id: 1 }
+        }
+      }), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('postCodeEpic', () => {
+  it('posts the code', () => {
+    const code = 'class Avatar'
+
+    const marbles1 = '-a-'
+    const marbles2 = '-b-'
+    const values = {
+      a: actions.postCodeRequest(),
+      b: actions.postCodeReceived()
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockPost = (url, body) => action$ => action$.mapTo({})
+    const mockAPI = { api: { post: mockPost } }
+
+    const state = {
+      game: {
+        connectionParameters: {
+          id: 1
+        }
+      },
+      editor: {
+        code: code
+      }
+    }
+
+    const actual = epics.postCodeEpic(source$, mockStore(state), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+
+  it('catches an error', () => {
+    const code = 'class Avatar'
+
+    const marbles1 = '-a-'
+    const marbles2 = '-(b|)-'
+    const values = {
+      a: actions.postCodeRequest(),
+      b: {
+        type: types.POST_CODE_FAILURE,
+        payload: 'oh no!',
+        error: true
+      }
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const error = { xhr: { response: 'oh no!' } }
+    const mockPost = (url, body) => action$ => action$.mergeMapTo(Observable.throw(error))
+    const mockAPI = { api: { post: mockPost } }
+
+    const state = {
+      game: {
+        connectionParameters: {
+          id: 1
+        }
+      },
+      editor: {
+        code: code
+      }
+    }
+
+    const actual = epics.postCodeEpic(source$, mockStore(state), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('changeCodeEpic', () => {
+  it('makes sure the state is not constantly updating due to changes in the editor', () => {
+    const sourceMarbles = '-a-a--------'
+    const expectMarbles = '---------b--'
+
+    const values = {
+      a: actions.keyPressed(''),
+      b: actions.changeCode('')
+    }
+
+    const testScheduler = createTestScheduler(50)
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(sourceMarbles, values)
+    )
+
+    const actual = epics.changeCodeEpic(source$, mockStore({}), {}, testScheduler)
+
+    testScheduler.expectObservable(actual).toBe(expectMarbles, values)
+    testScheduler.flush()
+  })
+})
diff --git a/game_frontend/src/redux/features/Editor/reducers.test.js b/game_frontend/src/redux/features/Editor/reducers.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/redux/features/Editor/reducers.test.js
@@ -0,0 +1,25 @@
+/* eslint-env jest */
+import editorReducer from './reducers'
+import actions from './actions'
+
+describe('editorReducer', () => {
+  it('should return the initial state', () => {
+    expect(editorReducer(undefined, {})).toEqual({})
+  })
+
+  it('should handle GET_CODE_SUCCESS', () => {
+    const expectedState = {
+      code: 'class Avatar'
+    }
+    const action = actions.getCodeReceived('class Avatar')
+    expect(editorReducer({}, action)).toEqual(expectedState)
+  })
+
+  it('should handle CHANGE_CODE', () => {
+    const expectedState = {
+      code: 'class Avatar'
+    }
+    const action = actions.changeCode('class Avatar')
+    expect(editorReducer({}, action)).toEqual(expectedState)
+  })
+})
diff --git a/game_frontend/src/redux/features/Game/epics.test.js b/game_frontend/src/redux/features/Game/epics.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/redux/features/Game/epics.test.js
@@ -0,0 +1,269 @@
+/* eslint-env jest */
+import { Observable, TestScheduler } from 'rxjs'
+import { ActionsObservable } from 'redux-observable'
+import epics from './epics'
+import actions from './actions'
+import configureStore from 'redux-mock-store'
+import api from '../../api'
+
+const middlewares = []
+const mockStore = configureStore(middlewares)
+
+const deepEquals = (actual, expected) =>
+  expect(actual).toEqual(expected)
+
+const createTestScheduler = (frameTimeFactor = 10) => {
+  TestScheduler.frameTimeFactor = frameTimeFactor
+  return new TestScheduler(deepEquals)
+}
+
+describe('getConnectionParametersEpic', () => {
+  it('gets a ID connection param', () => {
+    const gameIDRequested = 1
+    const connectionParameters = {
+      id: 1
+    }
+
+    const marbles1 = '-a-'
+    const marbles2 = '-b-'
+    const values = {
+      a: actions.getConnectionParametersRequest(gameIDRequested),
+      b: actions.getConnectionParametersSuccess(connectionParameters)
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+    const mockGetJSON = () => {
+      return Observable.of({id: connectionParameters.id})
+    }
+
+    const mockAPI = { api: { get: mockGetJSON } }
+
+    const actual = epics.getConnectionParametersEpic(source$, mockStore({
+      game: {
+        connectionParameters:
+            {
+              id: 1
+            }
+      }
+    }), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('setGameURLEpic', () => {
+  it('sets the game URL', () => {
+    const gameURL = 'test'
+
+    const marbles1 = '-a--'
+    const marbles2 = '-b--'
+    const values = {
+      a: actions.setGameURL(gameURL),
+      b: actions.setGameURLSuccess()
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockEmitToUnity = () => {
+      return Observable.of(values.b)
+    }
+
+    const mockAPI = {
+      api: {
+        unity: {
+          ...api.unity,
+          emitToUnity: mockEmitToUnity
+        }
+      }
+    }
+
+    const actual = epics.setGameURLEpic(source$, mockStore({}), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('setGamePath', () => {
+  it('sets the game path', () => {
+    const gamePath = 'test'
+
+    const marbles1 = '-a--'
+    const marbles2 = '-b--'
+    const values = {
+      a: actions.setGamePath(gamePath),
+      b: actions.setGamePathSuccess()
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockEmitToUnity = () => {
+      return Observable.of(values.b)
+    }
+
+    const mockAPI = {
+      api: {
+        unity: {
+          ...api.unity,
+          emitToUnity: mockEmitToUnity
+        }
+      }
+    }
+
+    const actual = epics.setGamePathEpic(source$, mockStore({}), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('setGamePort', () => {
+  it('sets the game port', () => {
+    const gamePort = 8000
+
+    const marbles1 = '-a--'
+    const marbles2 = '-b--'
+    const values = {
+      a: actions.setGamePort(gamePort),
+      b: actions.setGamePortSuccess()
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockEmitToUnity = () => {
+      return Observable.of(values.b)
+    }
+
+    const mockAPI = {
+      api: {
+        unity: {
+          ...api.unity,
+          emitToUnity: mockEmitToUnity
+        }
+      }
+    }
+
+    const actual = epics.setGamePortEpic(source$, mockStore({}), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('setGameSSL', () => {
+  it('sets the game SSL flag', () => {
+    const gameSSL = false
+
+    const marbles1 = '-a--'
+    const marbles2 = '-b--'
+    const values = {
+      a: actions.setGameSSL(gameSSL),
+      b: actions.setGameSSLSuccess()
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockEmitToUnity = () => {
+      return Observable.of(values.b)
+    }
+
+    const mockAPI = {
+      api: {
+        unity: {
+          ...api.unity,
+          emitToUnity: mockEmitToUnity
+        }
+      }
+    }
+
+    const actual = epics.setGameSSLEpic(source$, mockStore({}), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+
+  it('catches the error and returns a fail action without completing the observable', () => {
+    const gameSSL = false
+    const error = new Error('Noooo!!')
+
+    const marbles1 = '-a--'
+    const marbles2 = '-b--'
+    const values = {
+      a: actions.setGameSSL(gameSSL),
+      b: actions.setGameSSLFail(error)
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockEmitToUnity = () => {
+      return Observable.throw(error)
+    }
+
+    const mockAPI = {
+      api: {
+        unity: {
+          ...api.unity,
+          emitToUnity: mockEmitToUnity
+        }
+      }
+    }
+
+    const actual = epics.setGameSSLEpic(source$, mockStore({}), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
+
+describe('establishGameConnection', () => {
+  it('establishes the connection with the game', () => {
+    const marbles1 = '-a--'
+    const marbles2 = '-b--'
+    const values = {
+      a: actions.establishGameConnection(),
+      b: actions.establishGameConnectionSuccess()
+    }
+
+    const testScheduler = createTestScheduler()
+    const source$ = ActionsObservable.from(
+      testScheduler.createColdObservable(marbles1, values)
+    )
+
+    const mockEmitToUnity = () => {
+      return Observable.of(values.b)
+    }
+
+    const mockAPI = {
+      api: {
+        unity: {
+          ...api.unity,
+          emitToUnity: mockEmitToUnity
+        }
+      }
+    }
+
+    const actual = epics.establishGameConnectionEpic(source$, mockStore({}), mockAPI)
+
+    testScheduler.expectObservable(actual).toBe(marbles2, values)
+    testScheduler.flush()
+  })
+})
diff --git a/game_frontend/src/redux/features/Game/reducers.test.js b/game_frontend/src/redux/features/Game/reducers.test.js
new file mode 100644
--- /dev/null
+++ b/game_frontend/src/redux/features/Game/reducers.test.js
@@ -0,0 +1,20 @@
+/* eslint-env jest */
+import actions from './actions'
+import gameReducer from './reducers'
+
+describe('gameReducer', () => {
+  it('should return the initial state', () => {
+    expect(gameReducer(undefined, {})).toEqual({})
+  })
+
+  it('should handle GET_CONNECTION_PARAMETERS_SUCCESS', () => {
+    const expectedState = {
+      connectionParameters: {
+        id: 1
+      },
+      initialState: 'someValue'
+    }
+    const action = actions.getConnectionParametersSuccess({ id: 1 })
+    expect(gameReducer({initialState: 'someValue'}, action)).toEqual(expectedState)
+  })
+})
diff --git a/game_frontend/src/redux/features/GhibliMovies/reducers.test.js b/game_frontend/src/redux/features/GhibliMovies/reducers.test.js
--- a/game_frontend/src/redux/features/GhibliMovies/reducers.test.js
+++ b/game_frontend/src/redux/features/GhibliMovies/reducers.test.js
@@ -1,10 +1,10 @@
 /* eslint-env jest */
-import reducers from './reducers'
+import movieReducer from './reducers'
 import actions from './actions'
 
 describe('movieReducer', () => {
   it('should return the initial state', () => {
-    expect(reducers.movieReducer(undefined, {})).toEqual({})
+    expect(movieReducer(undefined, {})).toEqual({})
   })
 
   it('should handle RECEIVE_MOVIES', () => {
@@ -24,7 +24,7 @@ describe('movieReducer', () => {
       }
     ]
     const action = actions.receiveMovies(movies)
-    expect(reducers.movieReducer({}, action)).toEqual({
+    expect(movieReducer({}, action)).toEqual({
       movies
     })
   })
diff --git a/players/tests.py b/players/tests.py
--- a/players/tests.py
+++ b/players/tests.py
@@ -6,11 +6,11 @@
 from django.core.urlresolvers import reverse
 from django.test import Client, TestCase
 
-from players import models, views
+from players import models, app_settings
 
-views.app_settings.GAME_SERVER_URL_FUNCTION = lambda game_id: ('base %s' % game_id, 'path %s' % game_id)
-views.app_settings.GAME_SERVER_PORT_FUNCTION = lambda game_id: 0
-views.app_settings.GAME_SERVER_SSL_FLAG = True
+app_settings.GAME_SERVER_URL_FUNCTION = lambda game_id: ('base %s' % game_id, 'path %s' % game_id)
+app_settings.GAME_SERVER_PORT_FUNCTION = lambda game_id: 0
+app_settings.GAME_SERVER_SSL_FLAG = True
 
 
 class TestViews(TestCase):
@@ -69,14 +69,14 @@ def test_default_code(self):
         c = self.login()
         response = c.get(reverse('aimmo/code', kwargs={'id': 1}))
         self.assertEqual(response.status_code, 200)
-        self.assertTrue(response.content.startswith('class Avatar'))
+        self.assertTrue(json.loads(response.content)['code'].startswith('class Avatar'))
 
     def test_retrieve_code(self):
         models.Avatar(owner=self.user, code=self.CODE, game=self.game).save()
         c = self.login()
         response = c.get(reverse('aimmo/code', kwargs={'id': 1}))
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.content, self.CODE)
+        self.assertJSONEqual(response.content, {'code': self.CODE})
 
     def test_program(self):
         c = self.login()