Forráskód Böngészése

GameCreationForm tests

Kirk Trombley 4 éve
szülő
commit
a22fc6d0e3

+ 1 - 1
client/src/components/util/GameCreationForm/Dropdown.jsx

@@ -193,7 +193,7 @@ export const DropdownGroup = ({ children }) => {
   const [open, setOpen] = useState(null);
   return (
     <>
-      {children.map(child =>
+      {children?.map(child =>
         React.cloneElement(child, {
           open: open === child.props.open,
           onClick: () =>

+ 407 - 0
client/src/tests/Dropdown.test.js

@@ -0,0 +1,407 @@
+import React from "react";
+import { shallow } from "enzyme";
+import {
+  CountryDropdown,
+  Dropdown,
+  DropdownGroup,
+  Item,
+} from "../components/util/GameCreationForm/Dropdown";
+
+jest.mock("../domain/flagLookup");
+
+import flagLookup from "../domain/flagLookup";
+
+describe("DropdownGroup", () => {
+  it("renders with no dropdowns", () => {
+    const rendered = shallow(<DropdownGroup />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("renders with dropdowns", () => {
+    const rendered = shallow(
+      <DropdownGroup>
+        <Dropdown open="dd1" />
+        <Dropdown open="dd2" />
+        <Dropdown open="dd3" />
+      </DropdownGroup>
+    );
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("can have a dropdown opened", () => {
+    const rendered = shallow(
+      <DropdownGroup>
+        <Dropdown open="dd1" />
+        <Dropdown open="dd2" />
+        <Dropdown open="dd3" />
+      </DropdownGroup>
+    );
+    rendered.find("Dropdown").at(1).prop("onClick")();
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("can have a dropdown closed after opening", () => {
+    const rendered = shallow(
+      <DropdownGroup>
+        <Dropdown open="dd1" />
+        <Dropdown open="dd2" />
+        <Dropdown open="dd3" />
+      </DropdownGroup>
+    );
+    rendered.find("Dropdown").at(1).prop("onClick")();
+    rendered.find("Dropdown").at(1).prop("onClick")();
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("selecting a dropdown re-closes it", () => {
+    const onSelect = jest.fn();
+    const rendered = shallow(
+      <DropdownGroup>
+        <Dropdown open="dd1" />
+        <Dropdown open="dd2" onSelect={onSelect} />
+        <Dropdown open="dd3" />
+      </DropdownGroup>
+    );
+    rendered.find("Dropdown").at(1).prop("onClick")();
+    rendered.find("Dropdown").at(1).prop("onSelect")("test");
+    expect(rendered).toMatchSnapshot();
+    expect(onSelect).toHaveBeenCalledWith("test");
+  });
+});
+
+describe("Dropdown", () => {
+  it("renders closed", () => {
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(<Dropdown {...{ onSelect, onClick }} />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("renders with no items", () => {
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(<Dropdown open {...{ onSelect, onClick }} />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("renders with items", () => {
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <Dropdown open {...{ onSelect, onClick }}>
+        <Item value="test1" />
+        <Item value="test2" />
+      </Dropdown>
+    );
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("can be clicked", () => {
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <Dropdown {...{ onSelect, onClick }}>
+        <Item value="test1" />
+        <Item value="test2" />
+      </Dropdown>
+    );
+    rendered.find("div.button").first().simulate("click");
+    expect(onClick).toHaveBeenCalled();
+  });
+
+  it("can be selected with Enter", () => {
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <Dropdown {...{ onSelect, onClick }}>
+        <Item value="test1" />
+        <Item value="test2" />
+      </Dropdown>
+    );
+    rendered.find("div.button").first().simulate("keydown", { key: "Enter" });
+    expect(onClick).toHaveBeenCalled();
+  });
+
+  it("responds to item selection", () => {
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <Dropdown open {...{ onSelect, onClick }}>
+        <Item value="test1" />
+        <Item value="test2" />
+      </Dropdown>
+    );
+    rendered.find("Item").first().prop("onSelect")("value", "display");
+    expect(onSelect).toHaveBeenCalledWith("value");
+    expect(rendered).toMatchSnapshot();
+  });
+});
+
+describe("CountryDropdown", () => {
+  it("renders closed", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown {...{ countryLookup, selected, onSelect, onClick }} />
+    );
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("renders", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("renders with countries", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("can be clicked", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("div.button").first().simulate("click");
+    expect(onClick).toHaveBeenCalled();
+  });
+
+  it("can be selected with Enter", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("div.button").first().simulate("keydown", { key: "Enter" });
+    expect(onClick).toHaveBeenCalled();
+  });
+
+  it("can have a country selected", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("div.item").first().simulate("click");
+    expect(onSelect).toHaveBeenCalledWith("a21");
+  });
+
+  it("can have a country selected with Enter", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("div.item").at(1).simulate("keydown", { key: "Enter" });
+    expect(onSelect).toHaveBeenCalledWith("a22");
+  });
+
+  it("can have world selected", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("div.item").last().simulate("click");
+    expect(onSelect).toHaveBeenCalledWith(null);
+  });
+
+  it("can have world selected with Enter", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("div.item").last().simulate("keydown", { key: "Enter" });
+    expect(onSelect).toHaveBeenCalledWith(null);
+  });
+
+  it("selects country based on searchbox", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered
+      .find("input")
+      .first()
+      .simulate("change", { target: { value: "changed" } });
+    expect(countryLookup).toHaveBeenCalledWith("changed");
+  });
+
+  it("can have first option selected with Enter", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("input").first().simulate("keydown", { key: "Enter" });
+    expect(onSelect).toHaveBeenCalledWith("a21");
+  });
+
+  it("can have previously selected option selected with Escape", () => {
+    flagLookup.mockReturnValue("flag");
+    const countryLookup = jest.fn();
+    countryLookup.mockReturnValue([
+      { item: { country: "c1", alpha2: "a21" } },
+      { item: { country: "c2", alpha2: "a22" } },
+      { item: { country: "c3", alpha2: "a23" } },
+    ]);
+    const selected = "selected";
+    const onSelect = jest.fn();
+    const onClick = jest.fn();
+    const rendered = shallow(
+      <CountryDropdown
+        open
+        {...{ countryLookup, selected, onSelect, onClick }}
+      />
+    );
+    rendered.find("input").first().simulate("keydown", { key: "Escape" });
+    expect(onSelect).toHaveBeenCalledWith("selected");
+  });
+});
+
+describe("Item", () => {
+  it("renders with no text", () => {
+    const onSelect = jest.fn();
+    const display = "display";
+    const value = "value";
+    const rendered = shallow(<Item {...{ onSelect, display, value }} />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("responds to click", () => {
+    const onSelect = jest.fn();
+    const display = "display";
+    const value = "value";
+    const rendered = shallow(<Item {...{ onSelect, display, value }} />);
+    rendered.simulate("click");
+    expect(onSelect).toHaveBeenCalledWith(value, display);
+  });
+
+  it("responds to enter", () => {
+    const onSelect = jest.fn();
+    const display = "display";
+    const value = "value";
+    const rendered = shallow(<Item {...{ onSelect, display, value }} />);
+    rendered.simulate("keydown", { key: "Enter" });
+    expect(onSelect).toHaveBeenCalledWith(value, display);
+  });
+});

+ 24 - 0
client/src/tests/ErrorModal.test.js

@@ -0,0 +1,24 @@
+import React from "react";
+import { shallow } from "enzyme";
+import ErrorModal from "../components/util/GameCreationForm/ErrorModal";
+
+describe("ErrorModal", () => {
+  it("renders", () => {
+    const onClose = jest.fn();
+    const rendered = shallow(<ErrorModal onClose={onClose} />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("renders when open", () => {
+    const onClose = jest.fn();
+    const rendered = shallow(<ErrorModal open onClose={onClose} />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("calls onClose when closed", () => {
+    const onClose = jest.fn();
+    const rendered = shallow(<ErrorModal open onClose={onClose} />);
+    rendered.find("button").first().simulate("click");
+    expect(onClose).toHaveBeenCalled();
+  });
+});

+ 75 - 0
client/src/tests/GameCreationForm.test.js

@@ -0,0 +1,75 @@
+import React from "react";
+import { shallow } from "enzyme";
+import GameCreationForm from "../components/util/GameCreationForm";
+import { URBAN } from "../domain/genMethods";
+import { FROZEN } from "../domain/ruleSets";
+
+jest.mock("../domain/apiMethods");
+jest.mock("../hooks/useCountryLookup");
+
+import { createGame } from "../domain/apiMethods";
+import useCountryLookup from "../hooks/useCountryLookup";
+
+describe("GameCreationForm", () => {
+  it("renders", () => {
+    useCountryLookup.mockReturnValue("country-lookup");
+    const afterCreate = jest.fn();
+    const rendered = shallow(<GameCreationForm afterCreate={afterCreate} />);
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("creates a game", async () => {
+    useCountryLookup.mockReturnValue("country-lookup");
+    createGame.mockReturnValue("test-game-id");
+    const afterCreate = jest.fn();
+    const rendered = shallow(<GameCreationForm afterCreate={afterCreate} />);
+    await rendered.find("button").first().simulate("click");
+    expect(createGame).toHaveBeenCalled();
+    expect(afterCreate).toHaveBeenCalledWith("test-game-id");
+  });
+
+  it("does nothing after creating a game if no afterCreate given", async () => {
+    useCountryLookup.mockReturnValue("country-lookup");
+    createGame.mockReturnValue("test-game-id");
+    const rendered = shallow(<GameCreationForm />);
+    await rendered.find("button").first().simulate("click");
+    expect(createGame).toHaveBeenCalled();
+  });
+
+  it("handles error creating a game", async () => {
+    useCountryLookup.mockReturnValue("country-lookup");
+    createGame.mockImplementation(() => {
+      throw new Error();
+    });
+    const afterCreate = jest.fn();
+    const rendered = shallow(<GameCreationForm afterCreate={afterCreate} />);
+    await rendered.find("button").first().simulate("click");
+    expect(createGame).toHaveBeenCalled();
+    expect(afterCreate).not.toHaveBeenCalled();
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("can have the error modal closed", async () => {
+    useCountryLookup.mockReturnValue("country-lookup");
+    createGame.mockImplementation(() => {
+      throw new Error();
+    });
+    const rendered = shallow(<GameCreationForm />);
+    await rendered.find("button").first().simulate("click");
+    rendered.find("ErrorModal").first().prop("onClose")();
+    expect(rendered).toMatchSnapshot();
+  });
+
+  it("can have presets selected", () => {
+    useCountryLookup.mockReturnValue("country-lookup");
+    const rendered = shallow(<GameCreationForm />);
+    rendered.find("Dropdown").first().prop("onSelect")({
+      timer: 30,
+      rounds: 3,
+      countryLock: "us",
+      genMethod: URBAN,
+      ruleSet: FROZEN,
+    });
+    expect(rendered).toMatchSnapshot();
+  });
+});

+ 494 - 0
client/src/tests/__snapshots__/Dropdown.test.js.snap

@@ -0,0 +1,494 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CountryDropdown renders 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  >
+    flag
+  </div>
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    in={true}
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    >
+      <input
+        autoFocus={true}
+        className="search"
+        onChange={[Function]}
+        onKeyDown={[Function]}
+        type="text"
+        value=""
+      />
+      <div
+        className="item"
+        onClick={[Function]}
+        onKeyDown={[Function]}
+        role="menuitem"
+        tabIndex="0"
+      >
+        flag
+         - All Countries
+      </div>
+    </div>
+  </CSSTransition>
+</div>
+`;
+
+exports[`CountryDropdown renders closed 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  >
+    flag
+  </div>
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    >
+      <input
+        autoFocus={true}
+        className="search"
+        onChange={[Function]}
+        onKeyDown={[Function]}
+        type="text"
+        value=""
+      />
+      <div
+        className="item"
+        onClick={[Function]}
+        onKeyDown={[Function]}
+        role="menuitem"
+        tabIndex="0"
+      >
+        flag
+         - All Countries
+      </div>
+    </div>
+  </CSSTransition>
+</div>
+`;
+
+exports[`CountryDropdown renders with countries 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  >
+    flag
+  </div>
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    in={true}
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    >
+      <input
+        autoFocus={true}
+        className="search"
+        onChange={[Function]}
+        onKeyDown={[Function]}
+        type="text"
+        value=""
+      />
+      <div
+        className="item"
+        key="a21"
+        onClick={[Function]}
+        onKeyDown={[Function]}
+        role="button"
+        tabIndex="0"
+      >
+        flag
+         - 
+        c1
+      </div>
+      <div
+        className="item"
+        key="a22"
+        onClick={[Function]}
+        onKeyDown={[Function]}
+        role="button"
+        tabIndex="0"
+      >
+        flag
+         - 
+        c2
+      </div>
+      <div
+        className="item"
+        key="a23"
+        onClick={[Function]}
+        onKeyDown={[Function]}
+        role="button"
+        tabIndex="0"
+      >
+        flag
+         - 
+        c3
+      </div>
+      <div
+        className="item"
+        onClick={[Function]}
+        onKeyDown={[Function]}
+        role="menuitem"
+        tabIndex="0"
+      >
+        flag
+         - All Countries
+      </div>
+    </div>
+  </CSSTransition>
+</div>
+`;
+
+exports[`Dropdown renders closed 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  />
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    />
+  </CSSTransition>
+</div>
+`;
+
+exports[`Dropdown renders with items 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  />
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    in={true}
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    >
+      <Item
+        key="\\"test1\\""
+        onSelect={[Function]}
+        value="test1"
+      />
+      <Item
+        key="\\"test2\\""
+        onSelect={[Function]}
+        value="test2"
+      />
+    </div>
+  </CSSTransition>
+</div>
+`;
+
+exports[`Dropdown renders with no items 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  />
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    in={true}
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    />
+  </CSSTransition>
+</div>
+`;
+
+exports[`Dropdown responds to item selection 1`] = `
+<div
+  className="container"
+>
+  <div
+    className="button"
+    onClick={[MockFunction]}
+    onKeyDown={[Function]}
+    role="button"
+    tabIndex="0"
+  >
+    display
+  </div>
+  <CSSTransition
+    classNames={
+      Object {
+        "enter": "list-enter",
+        "enterActive": "list-enter-active",
+        "exit": "list-exit",
+        "exitActive": "list-exit-active",
+      }
+    }
+    in={true}
+    mountOnEnter={true}
+    nodeRef={
+      Object {
+        "current": null,
+      }
+    }
+    timeout={200}
+    unmountOnExit={true}
+  >
+    <div
+      className="list"
+      role="menu"
+    >
+      <Item
+        key="\\"test1\\""
+        onSelect={[Function]}
+        value="test1"
+      />
+      <Item
+        key="\\"test2\\""
+        onSelect={[Function]}
+        value="test2"
+      />
+    </div>
+  </CSSTransition>
+</div>
+`;
+
+exports[`DropdownGroup can have a dropdown closed after opening 1`] = `
+<Fragment>
+  <Dropdown
+    key="dd1"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd2"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd3"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+</Fragment>
+`;
+
+exports[`DropdownGroup can have a dropdown opened 1`] = `
+<Fragment>
+  <Dropdown
+    key="dd1"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd2"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={true}
+  />
+  <Dropdown
+    key="dd3"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+</Fragment>
+`;
+
+exports[`DropdownGroup renders with dropdowns 1`] = `
+<Fragment>
+  <Dropdown
+    key="dd1"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd2"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd3"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+</Fragment>
+`;
+
+exports[`DropdownGroup renders with no dropdowns 1`] = `<Fragment />`;
+
+exports[`DropdownGroup selecting a dropdown re-closes it 1`] = `
+<Fragment>
+  <Dropdown
+    key="dd1"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd2"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+  <Dropdown
+    key="dd3"
+    onClick={[Function]}
+    onSelect={[Function]}
+    open={false}
+  />
+</Fragment>
+`;
+
+exports[`Item renders with no text 1`] = `
+<div
+  className="item"
+  onClick={[Function]}
+  onKeyDown={[Function]}
+  role="menuitem"
+  tabIndex="0"
+>
+  display
+</div>
+`;

+ 70 - 0
client/src/tests/__snapshots__/ErrorModal.test.js.snap

@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ErrorModal renders 1`] = `
+<CSSTransition
+  classNames="fade"
+  mountOnEnter={true}
+  nodeRef={
+    Object {
+      "current": null,
+    }
+  }
+  timeout={200}
+  unmountOnExit={true}
+>
+  <div
+    className="background"
+  >
+    <div
+      className="content"
+    >
+      <p
+        className="text"
+      >
+        Sorry! The server took too long to generate points for that game - your configurations may be too restrictive.
+      </p>
+      <button
+        onClick={[MockFunction]}
+        type="button"
+      >
+        Close
+      </button>
+    </div>
+  </div>
+</CSSTransition>
+`;
+
+exports[`ErrorModal renders when open 1`] = `
+<CSSTransition
+  classNames="fade"
+  in={true}
+  mountOnEnter={true}
+  nodeRef={
+    Object {
+      "current": null,
+    }
+  }
+  timeout={200}
+  unmountOnExit={true}
+>
+  <div
+    className="background"
+  >
+    <div
+      className="content"
+    >
+      <p
+        className="text"
+      >
+        Sorry! The server took too long to generate points for that game - your configurations may be too restrictive.
+      </p>
+      <button
+        onClick={[MockFunction]}
+        type="button"
+      >
+        Close
+      </button>
+    </div>
+  </div>
+</CSSTransition>
+`;

+ 841 - 0
client/src/tests/__snapshots__/GameCreationForm.test.js.snap

@@ -0,0 +1,841 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GameCreationForm can have presets selected 1`] = `
+<div
+  className="form"
+>
+  <ErrorModal
+    onClose={[Function]}
+    open={false}
+  />
+  <button
+    className="start"
+    onClick={[Function]}
+    type="button"
+  >
+    New Game
+  </button>
+  <div
+    className="dropdowns"
+  >
+    <DropdownGroup>
+      <Dropdown
+        onSelect={[Function]}
+        open="presets"
+        selected={
+          Object {
+            "countryLock": null,
+            "genMethod": "RANDOMSTREETVIEW",
+            "rounds": 5,
+            "ruleSet": "NORMAL",
+            "timer": 300,
+          }
+        }
+      >
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Default
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": "us",
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban America
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban Global
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 3,
+              "ruleSet": "FROZEN",
+              "timer": 30,
+            }
+          }
+        >
+          Fast Frozen
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="timer"
+        selected={30}
+      >
+        <Item
+          display="30s"
+          value={30}
+        >
+          30 Seconds
+        </Item>
+        <Item
+          display="2m"
+          value={120}
+        >
+          2 Minutes
+        </Item>
+        <Item
+          display="5m"
+          value={300}
+        >
+          5 Minutes
+        </Item>
+        <Item
+          display="1h"
+          value={3600}
+        >
+          1 Hour
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="rounds"
+        selected={3}
+      >
+        <Item
+          value={1}
+        >
+          1 Round
+        </Item>
+        <Item
+          value={3}
+        >
+          3 Rounds
+        </Item>
+        <Item
+          value={5}
+        >
+          5 Rounds
+        </Item>
+        <Item
+          value={10}
+        >
+          10 Rounds
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="gen"
+        selected="URBAN"
+      >
+        <Item
+          display="🎲"
+          value="RANDOMSTREETVIEW"
+        >
+          Random Street View
+        </Item>
+        <Item
+          display="🏙️"
+          value="URBAN"
+        >
+          Urban Centers
+        </Item>
+      </Dropdown>
+      <CountryDropdown
+        countryLookup="country-lookup"
+        onSelect={[Function]}
+        open="country"
+        selected="us"
+      />
+      <Dropdown
+        onSelect={[Function]}
+        open="rule"
+        selected="FROZEN"
+      >
+        <Item
+          display="⏰"
+          value="NORMAL"
+        >
+          Normal
+        </Item>
+        <Item
+          display="🏦"
+          value="TIMEBANK"
+        >
+          Time Bank
+        </Item>
+        <Item
+          display="❄️"
+          value="FROZEN"
+        >
+          Frozen
+        </Item>
+        <Item
+          display="🏃"
+          value="RACE"
+        >
+          Race
+        </Item>
+        <Item
+          display="🗾"
+          value="COUNTRYRACE"
+        >
+          Country Race
+        </Item>
+      </Dropdown>
+    </DropdownGroup>
+  </div>
+</div>
+`;
+
+exports[`GameCreationForm can have the error modal closed 1`] = `
+<div
+  className="form"
+>
+  <ErrorModal
+    onClose={[Function]}
+    open={false}
+  />
+  <button
+    className="start"
+    onClick={[Function]}
+    type="button"
+  >
+    New Game
+  </button>
+  <div
+    className="dropdowns"
+  >
+    <DropdownGroup>
+      <Dropdown
+        onSelect={[Function]}
+        open="presets"
+        selected={
+          Object {
+            "countryLock": null,
+            "genMethod": "RANDOMSTREETVIEW",
+            "rounds": 5,
+            "ruleSet": "NORMAL",
+            "timer": 300,
+          }
+        }
+      >
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Default
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": "us",
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban America
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban Global
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 3,
+              "ruleSet": "FROZEN",
+              "timer": 30,
+            }
+          }
+        >
+          Fast Frozen
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="timer"
+        selected={300}
+      >
+        <Item
+          display="30s"
+          value={30}
+        >
+          30 Seconds
+        </Item>
+        <Item
+          display="2m"
+          value={120}
+        >
+          2 Minutes
+        </Item>
+        <Item
+          display="5m"
+          value={300}
+        >
+          5 Minutes
+        </Item>
+        <Item
+          display="1h"
+          value={3600}
+        >
+          1 Hour
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="rounds"
+        selected={5}
+      >
+        <Item
+          value={1}
+        >
+          1 Round
+        </Item>
+        <Item
+          value={3}
+        >
+          3 Rounds
+        </Item>
+        <Item
+          value={5}
+        >
+          5 Rounds
+        </Item>
+        <Item
+          value={10}
+        >
+          10 Rounds
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="gen"
+        selected="RANDOMSTREETVIEW"
+      >
+        <Item
+          display="🎲"
+          value="RANDOMSTREETVIEW"
+        >
+          Random Street View
+        </Item>
+        <Item
+          display="🏙️"
+          value="URBAN"
+        >
+          Urban Centers
+        </Item>
+      </Dropdown>
+      <CountryDropdown
+        countryLookup="country-lookup"
+        onSelect={[Function]}
+        open="country"
+        selected={null}
+      />
+      <Dropdown
+        onSelect={[Function]}
+        open="rule"
+        selected="NORMAL"
+      >
+        <Item
+          display="⏰"
+          value="NORMAL"
+        >
+          Normal
+        </Item>
+        <Item
+          display="🏦"
+          value="TIMEBANK"
+        >
+          Time Bank
+        </Item>
+        <Item
+          display="❄️"
+          value="FROZEN"
+        >
+          Frozen
+        </Item>
+        <Item
+          display="🏃"
+          value="RACE"
+        >
+          Race
+        </Item>
+        <Item
+          display="🗾"
+          value="COUNTRYRACE"
+        >
+          Country Race
+        </Item>
+      </Dropdown>
+    </DropdownGroup>
+  </div>
+</div>
+`;
+
+exports[`GameCreationForm handles error creating a game 1`] = `
+<div
+  className="form"
+>
+  <ErrorModal
+    onClose={[Function]}
+    open={true}
+  />
+  <button
+    className="start"
+    onClick={[Function]}
+    type="button"
+  >
+    New Game
+  </button>
+  <div
+    className="dropdowns"
+  >
+    <DropdownGroup>
+      <Dropdown
+        onSelect={[Function]}
+        open="presets"
+        selected={
+          Object {
+            "countryLock": null,
+            "genMethod": "RANDOMSTREETVIEW",
+            "rounds": 5,
+            "ruleSet": "NORMAL",
+            "timer": 300,
+          }
+        }
+      >
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Default
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": "us",
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban America
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban Global
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 3,
+              "ruleSet": "FROZEN",
+              "timer": 30,
+            }
+          }
+        >
+          Fast Frozen
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="timer"
+        selected={300}
+      >
+        <Item
+          display="30s"
+          value={30}
+        >
+          30 Seconds
+        </Item>
+        <Item
+          display="2m"
+          value={120}
+        >
+          2 Minutes
+        </Item>
+        <Item
+          display="5m"
+          value={300}
+        >
+          5 Minutes
+        </Item>
+        <Item
+          display="1h"
+          value={3600}
+        >
+          1 Hour
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="rounds"
+        selected={5}
+      >
+        <Item
+          value={1}
+        >
+          1 Round
+        </Item>
+        <Item
+          value={3}
+        >
+          3 Rounds
+        </Item>
+        <Item
+          value={5}
+        >
+          5 Rounds
+        </Item>
+        <Item
+          value={10}
+        >
+          10 Rounds
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="gen"
+        selected="RANDOMSTREETVIEW"
+      >
+        <Item
+          display="🎲"
+          value="RANDOMSTREETVIEW"
+        >
+          Random Street View
+        </Item>
+        <Item
+          display="🏙️"
+          value="URBAN"
+        >
+          Urban Centers
+        </Item>
+      </Dropdown>
+      <CountryDropdown
+        countryLookup="country-lookup"
+        onSelect={[Function]}
+        open="country"
+        selected={null}
+      />
+      <Dropdown
+        onSelect={[Function]}
+        open="rule"
+        selected="NORMAL"
+      >
+        <Item
+          display="⏰"
+          value="NORMAL"
+        >
+          Normal
+        </Item>
+        <Item
+          display="🏦"
+          value="TIMEBANK"
+        >
+          Time Bank
+        </Item>
+        <Item
+          display="❄️"
+          value="FROZEN"
+        >
+          Frozen
+        </Item>
+        <Item
+          display="🏃"
+          value="RACE"
+        >
+          Race
+        </Item>
+        <Item
+          display="🗾"
+          value="COUNTRYRACE"
+        >
+          Country Race
+        </Item>
+      </Dropdown>
+    </DropdownGroup>
+  </div>
+</div>
+`;
+
+exports[`GameCreationForm renders 1`] = `
+<div
+  className="form"
+>
+  <ErrorModal
+    onClose={[Function]}
+    open={false}
+  />
+  <button
+    className="start"
+    onClick={[Function]}
+    type="button"
+  >
+    New Game
+  </button>
+  <div
+    className="dropdowns"
+  >
+    <DropdownGroup>
+      <Dropdown
+        onSelect={[Function]}
+        open="presets"
+        selected={
+          Object {
+            "countryLock": null,
+            "genMethod": "RANDOMSTREETVIEW",
+            "rounds": 5,
+            "ruleSet": "NORMAL",
+            "timer": 300,
+          }
+        }
+      >
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Default
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": "us",
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban America
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "URBAN",
+              "rounds": 5,
+              "ruleSet": "NORMAL",
+              "timer": 300,
+            }
+          }
+        >
+          Urban Global
+        </Item>
+        <Item
+          display="⭐"
+          value={
+            Object {
+              "countryLock": null,
+              "genMethod": "RANDOMSTREETVIEW",
+              "rounds": 3,
+              "ruleSet": "FROZEN",
+              "timer": 30,
+            }
+          }
+        >
+          Fast Frozen
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="timer"
+        selected={300}
+      >
+        <Item
+          display="30s"
+          value={30}
+        >
+          30 Seconds
+        </Item>
+        <Item
+          display="2m"
+          value={120}
+        >
+          2 Minutes
+        </Item>
+        <Item
+          display="5m"
+          value={300}
+        >
+          5 Minutes
+        </Item>
+        <Item
+          display="1h"
+          value={3600}
+        >
+          1 Hour
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="rounds"
+        selected={5}
+      >
+        <Item
+          value={1}
+        >
+          1 Round
+        </Item>
+        <Item
+          value={3}
+        >
+          3 Rounds
+        </Item>
+        <Item
+          value={5}
+        >
+          5 Rounds
+        </Item>
+        <Item
+          value={10}
+        >
+          10 Rounds
+        </Item>
+      </Dropdown>
+      <Dropdown
+        onSelect={[Function]}
+        open="gen"
+        selected="RANDOMSTREETVIEW"
+      >
+        <Item
+          display="🎲"
+          value="RANDOMSTREETVIEW"
+        >
+          Random Street View
+        </Item>
+        <Item
+          display="🏙️"
+          value="URBAN"
+        >
+          Urban Centers
+        </Item>
+      </Dropdown>
+      <CountryDropdown
+        countryLookup="country-lookup"
+        onSelect={[Function]}
+        open="country"
+        selected={null}
+      />
+      <Dropdown
+        onSelect={[Function]}
+        open="rule"
+        selected="NORMAL"
+      >
+        <Item
+          display="⏰"
+          value="NORMAL"
+        >
+          Normal
+        </Item>
+        <Item
+          display="🏦"
+          value="TIMEBANK"
+        >
+          Time Bank
+        </Item>
+        <Item
+          display="❄️"
+          value="FROZEN"
+        >
+          Frozen
+        </Item>
+        <Item
+          display="🏃"
+          value="RACE"
+        >
+          Race
+        </Item>
+        <Item
+          display="🗾"
+          value="COUNTRYRACE"
+        >
+          Country Race
+        </Item>
+      </Dropdown>
+    </DropdownGroup>
+  </div>
+</div>
+`;