Ver Fonte

Tests for geocoding, flagLookup, color gen, markers, localstorage

Kirk Trombley há 4 anos atrás
pai
commit
7e3486d3e3

+ 1 - 1
client/src/domain/geocoding.js

@@ -1,7 +1,7 @@
 import iso from "iso-3166-1";
 /* global google */
 
-const GEOCODER = new google.maps.Geocoder();
+export const GEOCODER = new google.maps.Geocoder();
 
 export const reverseGeocode = async location => {
   try {

+ 3 - 1
client/src/setupTests.js

@@ -26,7 +26,9 @@ const fakeClass = class {
 
 global.google = {
   maps: {
-    Geocoder: fakeClass,
+    Geocoder: class extends fakeClass {
+      geocode = jest.fn();
+    },
     Polyline: fakeClass,
     Marker: fakeClass,
     Point: fakeClass,

+ 13 - 0
client/src/tests/__snapshots__/flagLookup.test.js.snap

@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`flagLookup finds flags 1`] = `"🇺🇸"`;
+
+exports[`flagLookup finds flags 2`] = `"🇬🇧"`;
+
+exports[`flagLookup finds flags 3`] = `"🇨🇳"`;
+
+exports[`flagLookup finds flags 4`] = `"🇧🇷"`;
+
+exports[`flagLookup finds flags 5`] = `"🇕🇕"`;
+
+exports[`flagLookup finds flags 6`] = `"🌎"`;

+ 13 - 0
client/src/tests/__snapshots__/getColorGenerator.test.js.snap

@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`getColorGenerator provides a color generator 1`] = `"#7394e6"`;
+
+exports[`getColorGenerator provides a color generator 2`] = `"#b6e673"`;
+
+exports[`getColorGenerator provides a color generator 3`] = `"#e673d8"`;
+
+exports[`getColorGenerator provides a color generator 4`] = `"#73e6d3"`;
+
+exports[`getColorGenerator provides a color generator 5`] = `"#e6b173"`;
+
+exports[`getColorGenerator provides a color generator 6`] = `"#8f73e6"`;

+ 514 - 0
client/src/tests/__snapshots__/localStorageMethods.test.js.snap

@@ -0,0 +1,514 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`localStorageMethods clearGameInfoFromLocalStorage clears data 1`] = `[MockFunction]`;
+
+exports[`localStorageMethods clearGameInfoFromLocalStorage clears data 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods clearGameInfoFromLocalStorage clears data 3`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+    Array [
+      "terrassumptions:playerId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods clearRoundInfoFromLocalStorage clears data 1`] = `[MockFunction]`;
+
+exports[`localStorageMethods clearRoundInfoFromLocalStorage clears data 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods clearRoundInfoFromLocalStorage clears data 3`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:timer",
+    ],
+    Array [
+      "terrassumptions:pano:lat",
+    ],
+    Array [
+      "terrassumptions:pano:lng",
+    ],
+    Array [
+      "terrassumptions:pano:heading",
+    ],
+    Array [
+      "terrassumptions:pano:pitch",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods getInfoFromLocalStorage gets data 1`] = `
+Object {
+  "gameId": "test-game-id",
+  "playerId": "test-player-id",
+  "playerName": "test-player",
+  "position": Object {
+    "lat": NaN,
+    "lng": NaN,
+  },
+  "pov": Object {
+    "heading": NaN,
+    "pitch": NaN,
+  },
+  "timer": 300,
+}
+`;
+
+exports[`localStorageMethods getInfoFromLocalStorage gets data 2`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:timer",
+    ],
+    Array [
+      "terrassumptions:pano:lat",
+    ],
+    Array [
+      "terrassumptions:pano:lng",
+    ],
+    Array [
+      "terrassumptions:pano:heading",
+    ],
+    Array [
+      "terrassumptions:pano:pitch",
+    ],
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+    Array [
+      "terrassumptions:playerId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": "300",
+    },
+    Object {
+      "type": "return",
+      "value": "test-lat",
+    },
+    Object {
+      "type": "return",
+      "value": "test-lng",
+    },
+    Object {
+      "type": "return",
+      "value": "test-heading",
+    },
+    Object {
+      "type": "return",
+      "value": "test-pitch",
+    },
+    Object {
+      "type": "return",
+      "value": "test-game-id",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player-id",
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods getInfoFromLocalStorage gets data 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods getInfoFromLocalStorage gets data 4`] = `[MockFunction]`;
+
+exports[`localStorageMethods getInfoFromLocalStorage handles bad/missing extra data 1`] = `
+Object {
+  "gameId": "test-game-id",
+  "playerId": "test-player-id",
+  "playerName": "test-player",
+  "position": null,
+  "pov": null,
+  "timer": null,
+}
+`;
+
+exports[`localStorageMethods getInfoFromLocalStorage handles bad/missing extra data 2`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:timer",
+    ],
+    Array [
+      "terrassumptions:pano:lat",
+    ],
+    Array [
+      "terrassumptions:pano:lng",
+    ],
+    Array [
+      "terrassumptions:pano:heading",
+    ],
+    Array [
+      "terrassumptions:pano:pitch",
+    ],
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+    Array [
+      "terrassumptions:playerId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": null,
+    },
+    Object {
+      "type": "return",
+      "value": null,
+    },
+    Object {
+      "type": "return",
+      "value": "test-lng",
+    },
+    Object {
+      "type": "return",
+      "value": null,
+    },
+    Object {
+      "type": "return",
+      "value": "test-pitch",
+    },
+    Object {
+      "type": "return",
+      "value": "test-game-id",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player-id",
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods getInfoFromLocalStorage handles bad/missing extra data 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods getInfoFromLocalStorage handles bad/missing extra data 4`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when all data missing 1`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": null,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods hasSavedGameInfo works when all data missing 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when all data missing 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when getCurrentRound fails 1`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+    Array [
+      "terrassumptions:playerId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": "test-game-id",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player-id",
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods hasSavedGameInfo works when getCurrentRound fails 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when getCurrentRound fails 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when no data missing 1`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+    Array [
+      "terrassumptions:playerId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": "test-game-id",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player-id",
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods hasSavedGameInfo works when no data missing 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when no data missing 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when player info missing 1`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": "test-game-id",
+    },
+    Object {
+      "type": "return",
+      "value": null,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods hasSavedGameInfo works when player info missing 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when player info missing 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when playerId missing 1`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+    ],
+    Array [
+      "terrassumptions:playerName",
+    ],
+    Array [
+      "terrassumptions:playerId",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": "test-game-id",
+    },
+    Object {
+      "type": "return",
+      "value": "test-player",
+    },
+    Object {
+      "type": "return",
+      "value": null,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods hasSavedGameInfo works when playerId missing 2`] = `[MockFunction]`;
+
+exports[`localStorageMethods hasSavedGameInfo works when playerId missing 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods saveGameInfoToLocalStorage saves given data 1`] = `[MockFunction]`;
+
+exports[`localStorageMethods saveGameInfoToLocalStorage saves given data 2`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:gameId",
+      "test-game-id",
+    ],
+    Array [
+      "terrassumptions:playerName",
+      "test-player-name",
+    ],
+    Array [
+      "terrassumptions:playerId",
+      "test-player-id",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods saveGameInfoToLocalStorage saves given data 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods savePanoPositionToLocalStorage saves given data 1`] = `[MockFunction]`;
+
+exports[`localStorageMethods savePanoPositionToLocalStorage saves given data 2`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:pano:lat",
+      "1",
+    ],
+    Array [
+      "terrassumptions:pano:lng",
+      "2",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods savePanoPositionToLocalStorage saves given data 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods savePanoPovToLocalStorage saves given data 1`] = `[MockFunction]`;
+
+exports[`localStorageMethods savePanoPovToLocalStorage saves given data 2`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:pano:heading",
+      "3",
+    ],
+    Array [
+      "terrassumptions:pano:pitch",
+      "4",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods savePanoPovToLocalStorage saves given data 3`] = `[MockFunction]`;
+
+exports[`localStorageMethods saveTimerToLocalStorage saves given data 1`] = `[MockFunction]`;
+
+exports[`localStorageMethods saveTimerToLocalStorage saves given data 2`] = `
+[MockFunction] {
+  "calls": Array [
+    Array [
+      "terrassumptions:timer",
+      "100",
+    ],
+  ],
+  "results": Array [
+    Object {
+      "type": "return",
+      "value": undefined,
+    },
+  ],
+}
+`;
+
+exports[`localStorageMethods saveTimerToLocalStorage saves given data 3`] = `[MockFunction]`;

+ 102 - 0
client/src/tests/__snapshots__/markers.test.js.snap

@@ -0,0 +1,102 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`markers makeFlagMarker makes a flag marker 1`] = `
+fakeClass {
+  "addListener": [Function],
+  "calledWith": Array [
+    Object {
+      "clickable": true,
+      "icon": Object {
+        "anchor": fakeClass {
+          "addListener": [Function],
+          "calledWith": Array [
+            16,
+            512,
+          ],
+          "listeners": Map {},
+        },
+        "fillColor": "#000000",
+        "fillOpacity": 1,
+        "path": "M466.515 66.928C487.731 57.074 512 72.551 512 95.944v243.1c0 10.526-5.161 20.407-13.843 26.358-35.837 24.564-74.335 40.858-122.505 40.858-67.373 0-111.63-34.783-165.217-34.783-50.853 0-86.124 10.058-114.435 22.122V488c0 13.255-10.745 24-24 24H56c-13.255 0-24-10.745-24-24V101.945C17.497 91.825 8 75.026 8 56 8 24.296 34.345-1.254 66.338.048c28.468 1.158 51.779 23.968 53.551 52.404.52 8.342-.81 16.31-3.586 23.562C137.039 68.384 159.393 64 184.348 64c67.373 0 111.63 34.783 165.217 34.783 40.496 0 82.612-15.906 116.95-31.855zM96 134.63v70.49c29-10.67 51.18-17.83 73.6-20.91v-71.57c-23.5 2.17-40.44 9.79-73.6 21.99zm220.8 9.19c-26.417-4.672-49.886-13.979-73.6-21.34v67.42c24.175 6.706 47.566 16.444 73.6 22.31v-68.39zm-147.2 40.39v70.04c32.796-2.978 53.91-.635 73.6 3.8V189.9c-25.247-7.035-46.581-9.423-73.6-5.69zm73.6 142.23c26.338 4.652 49.732 13.927 73.6 21.34v-67.41c-24.277-6.746-47.54-16.45-73.6-22.32v68.39zM96 342.1c23.62-8.39 47.79-13.84 73.6-16.56v-71.29c-26.11 2.35-47.36 8.04-73.6 17.36v70.49zm368-221.6c-21.3 8.85-46.59 17.64-73.6 22.47v71.91c27.31-4.36 50.03-14.1 73.6-23.89V120.5zm0 209.96v-70.49c-22.19 14.2-48.78 22.61-73.6 26.02v71.58c25.07-2.38 48.49-11.04 73.6-27.11zM316.8 212.21v68.16c25.664 7.134 46.616 9.342 73.6 5.62v-71.11c-25.999 4.187-49.943 2.676-73.6-2.67z",
+        "scale": 0.075,
+      },
+      "map": "map",
+      "position": Object {
+        "lat": "lat",
+        "lng": "lng",
+      },
+      "title": "Goal",
+    },
+  ],
+  "listeners": Map {
+    "click" => Array [
+      [Function],
+    ],
+  },
+}
+`;
+
+exports[`markers makeLine makes a line 1`] = `
+fakeClass {
+  "addListener": [Function],
+  "calledWith": Array [
+    Object {
+      "icons": Array [
+        Object {
+          "icon": Object {
+            "path": "M 0,-1 0,1",
+            "scale": 4,
+            "strokeOpacity": 1,
+          },
+          "offset": "0",
+          "repeat": "20px",
+        },
+      ],
+      "map": "map",
+      "path": Array [
+        "p1",
+        "p2",
+      ],
+      "strokeColor": "color",
+      "strokeOpacity": 0,
+    },
+  ],
+  "listeners": Map {},
+}
+`;
+
+exports[`markers makeQuestionMarker makes a question marker 1`] = `
+fakeClass {
+  "addListener": [Function],
+  "calledWith": Array [
+    Object {
+      "clickable": true,
+      "icon": Object {
+        "anchor": fakeClass {
+          "addListener": [Function],
+          "calledWith": Array [
+            32,
+            40,
+          ],
+          "listeners": Map {},
+        },
+        "fillColor": "color",
+        "fillOpacity": 1,
+        "path": "M29.898 26.5722l-4.3921 0c-0.0118,-0.635 -0.0177,-1.0172 -0.0177,-1.1583 0,-1.4229 0.2352,-2.5929 0.7056,-3.5102 0.4704,-0.9231 1.417,-1.952 2.8281,-3.1044 1.4111,-1.1465 2.2578,-1.8991 2.5282,-2.2578 0.4292,-0.5585 0.6409,-1.1818 0.6409,-1.8579 0,-0.9408 -0.3763,-1.7463 -1.1289,-2.4224 -0.7526,-0.6703 -1.7639,-1.0054 -3.0397,-1.0054 -1.2289,0 -2.2578,0.3527 -3.0868,1.0524 -0.8232,0.6997 -1.3935,1.7698 -1.7051,3.2044l-4.4391 -0.5527c0.1234,-2.0578 0.9995,-3.8041 2.6223,-5.2387 1.6286,-1.4346 3.757,-2.152 6.4029,-2.152 2.7752,0 4.9859,0.7291 6.6322,2.1814 1.6404,1.4522 2.4635,3.1397 2.4635,5.0741 0,1.0642 -0.3057,2.0755 -0.9054,3.028 -0.6056,0.9525 -1.8933,2.2519 -3.8688,3.8923 -1.0231,0.8525 -1.6581,1.5346 -1.905,2.052 -0.2469,0.5174 -0.3587,1.4405 -0.3351,2.7752zm-4.3921 6.5087l0 -4.8389 4.8389 0 0 4.8389 -4.8389 0z",
+        "scale": 1,
+      },
+      "map": "map",
+      "position": Object {
+        "lat": "lat",
+        "lng": "lng",
+      },
+      "title": "title",
+    },
+  ],
+  "listeners": Map {
+    "click" => Array [
+      [Function],
+    ],
+  },
+}
+`;

+ 12 - 0
client/src/tests/flagLookup.test.js

@@ -0,0 +1,12 @@
+import flagLookup from "../domain/flagLookup";
+
+describe("flagLookup", () => {
+  it("finds flags", () => {
+    expect(flagLookup("us")).toMatchSnapshot();
+    expect(flagLookup("gb")).toMatchSnapshot();
+    expect(flagLookup("cn")).toMatchSnapshot();
+    expect(flagLookup("br")).toMatchSnapshot();
+    expect(flagLookup("00")).toMatchSnapshot();
+    expect(flagLookup(null)).toMatchSnapshot();
+  });
+});

+ 108 - 0
client/src/tests/geocoding.test.js

@@ -0,0 +1,108 @@
+import {
+  GEOCODER,
+  getCountryBounds,
+  reverseGeocode,
+} from "../domain/geocoding";
+
+jest.mock("iso-3166-1");
+
+import iso from "iso-3166-1";
+
+describe("geocoding", () => {
+  describe("reverseGeocode", () => {
+    it("unpacks reverse geocoding data and returns country", async () => {
+      GEOCODER.geocode.mockReturnValue({
+        results: [
+          {
+            address_components: [
+              {
+                short_name: "us",
+                types: ["country"],
+              },
+            ],
+          },
+        ],
+      });
+      expect(await reverseGeocode("test")).toBe("us");
+      expect(GEOCODER.geocode).toHaveBeenCalledWith(
+        expect.objectContaining({
+          location: "test",
+        })
+      );
+    });
+    it("handles missing data gracefully", async () => {
+      GEOCODER.geocode.mockReturnValue({
+        results: [
+          {
+            address_components: [
+              {
+                short_name: "md",
+                types: ["not-country"],
+              },
+            ],
+          },
+        ],
+      });
+      expect(await reverseGeocode()).toBe(null);
+    });
+    it("suppresses errors and returns null", async () => {
+      GEOCODER.geocode.mockImplementation(() => {
+        throw new Error();
+      });
+      expect(await reverseGeocode()).toBe(null);
+    });
+  });
+
+  describe("getCountryBounds", () => {
+    it("gets bounds for a given country", async () => {
+      iso.whereAlpha2.mockReturnValue({ country: "test-country" });
+      GEOCODER.geocode.mockReturnValue({
+        results: [
+          {
+            geometry: { viewport: "viewport" },
+            types: ["country"],
+          },
+        ],
+      });
+      expect(await getCountryBounds("success-test")).toBe("viewport");
+      expect(GEOCODER.geocode).toHaveBeenCalledWith(
+        expect.objectContaining({
+          address: "test-country",
+        })
+      );
+    });
+    it("handles missing data gracefully", async () => {
+      iso.whereAlpha2.mockReturnValue({ country: "test-country" });
+      GEOCODER.geocode.mockReturnValue({
+        results: [
+          {
+            geometry: { viewport: "viewport" },
+            types: ["not-country"],
+          },
+        ],
+      });
+      expect(await getCountryBounds("bad-data-test")).toBe(null);
+    });
+    it("suppresses errors and returns null", async () => {
+      iso.whereAlpha2.mockReturnValue({ country: "test-country" });
+      GEOCODER.geocode.mockImplementation(() => {
+        throw new Error();
+      });
+      expect(await getCountryBounds("error-test")).toBe(null);
+    });
+    it("uses cache when possible", async () => {
+      iso.whereAlpha2.mockReturnValue({ country: "test-country" });
+      GEOCODER.geocode.mockReturnValue({
+        results: [
+          {
+            geometry: { viewport: "viewport" },
+            types: ["country"],
+          },
+        ],
+      });
+      expect(await getCountryBounds("cache-test")).toBe("viewport");
+      expect(await getCountryBounds("cache-test")).toBe("viewport");
+      expect(GEOCODER.geocode).toHaveBeenCalledTimes(1);
+    });
+  });
+});

+ 13 - 0
client/src/tests/getColorGenerator.test.js

@@ -0,0 +1,13 @@
+import getColorGenerator from "../hooks/useMarkersFromGuesses/getColorGenerator";
+
+describe("getColorGenerator", () => {
+  it("provides a color generator", () => {
+    const gen = getColorGenerator();
+    expect(gen()).toMatchSnapshot();
+    expect(gen()).toMatchSnapshot();
+    expect(gen()).toMatchSnapshot();
+    expect(gen()).toMatchSnapshot();
+    expect(gen()).toMatchSnapshot();
+    expect(gen()).toMatchSnapshot();
+  });
+});

+ 192 - 0
client/src/tests/localStorageMethods.test.js

@@ -0,0 +1,192 @@
+import { getCurrentRound } from "../domain/apiMethods";
+import {
+  clearGameInfoFromLocalStorage,
+  clearRoundInfoFromLocalStorage,
+  getInfoFromLocalStorage,
+  hasSavedGameInfo,
+  saveGameInfoToLocalStorage,
+  savePanoPositionToLocalStorage,
+  savePanoPovToLocalStorage,
+  saveTimerToLocalStorage,
+} from "../domain/localStorageMethods";
+
+const localStorageGameId = "terrassumptions:gameId";
+const localStoragePlayerName = "terrassumptions:playerName";
+const localStoragePlayerId = "terrassumptions:playerId";
+
+const localStorageTimer = "terrassumptions:timer";
+
+const localStoragePanoLat = "terrassumptions:pano:lat";
+const localStoragePanoLng = "terrassumptions:pano:lng";
+
+const localStoragePanoHeading = "terrassumptions:pano:heading";
+const localStoragePanoPitch = "terrassumptions:pano:pitch";
+
+jest.mock("../domain/apiMethods");
+
+describe("localStorageMethods", () => {
+  beforeEach(() => {
+    jest.spyOn(window.localStorage.__proto__, "getItem");
+    jest.spyOn(window.localStorage.__proto__, "setItem");
+    jest.spyOn(window.localStorage.__proto__, "removeItem");
+  });
+
+  describe("hasSavedGameInfo", () => {
+    it("works when all data missing", async () => {
+      expect(await hasSavedGameInfo()).toBe(false);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+      expect(getCurrentRound).not.toHaveBeenCalled();
+    });
+    it("works when player info missing", async () => {
+      const data = {
+        [localStorageGameId]: "test-game-id",
+      };
+      localStorage.getItem.mockImplementation(key => data[key] ?? null);
+
+      expect(await hasSavedGameInfo()).toBe(false);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+      expect(getCurrentRound).not.toHaveBeenCalled();
+    });
+    it("works when playerId missing", async () => {
+      const data = {
+        [localStorageGameId]: "test-game-id",
+        [localStoragePlayerName]: "test-player",
+      };
+      localStorage.getItem.mockImplementation(key => data[key] ?? null);
+
+      expect(await hasSavedGameInfo()).toBe(false);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+      expect(getCurrentRound).not.toHaveBeenCalled();
+    });
+    it("works when getCurrentRound fails", async () => {
+      const data = {
+        [localStorageGameId]: "test-game-id",
+        [localStoragePlayerName]: "test-player",
+        [localStoragePlayerId]: "test-player-id",
+      };
+      localStorage.getItem.mockImplementation(key => data[key] ?? null);
+      getCurrentRound.mockImplementation(() => {
+        throw new Error();
+      });
+      expect(await hasSavedGameInfo()).toBe(false);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+      expect(getCurrentRound).toHaveBeenCalled();
+    });
+    it("works when no data missing", async () => {
+      const data = {
+        [localStorageGameId]: "test-game-id",
+        [localStoragePlayerName]: "test-player",
+        [localStoragePlayerId]: "test-player-id",
+      };
+      localStorage.getItem.mockImplementation(key => data[key] ?? null);
+
+      expect(await hasSavedGameInfo()).toBe(true);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+      expect(getCurrentRound).toHaveBeenCalled();
+    });
+  });
+
+  describe("saveGameInfoToLocalStorage", () => {
+    it("saves given data", () => {
+      saveGameInfoToLocalStorage(
+        "test-game-id",
+        "test-player-name",
+        "test-player-id"
+      );
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+
+  describe("saveTimerToLocalStorage", () => {
+    it("saves given data", () => {
+      saveTimerToLocalStorage(100);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+
+  describe("savePanoPositionToLocalStorage", () => {
+    it("saves given data", () => {
+      savePanoPositionToLocalStorage(1, 2);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+
+  describe("savePanoPovToLocalStorage", () => {
+    it("saves given data", () => {
+      savePanoPovToLocalStorage(3, 4);
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+
+  describe("clearGameInfoFromLocalStorage", () => {
+    it("clears data", () => {
+      clearGameInfoFromLocalStorage();
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+
+  describe("clearRoundInfoFromLocalStorage", () => {
+    it("clears data", () => {
+      clearRoundInfoFromLocalStorage();
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+
+  describe("getInfoFromLocalStorage", () => {
+    it("gets data", () => {
+      const data = {
+        [localStorageGameId]: "test-game-id",
+        [localStoragePlayerName]: "test-player",
+        [localStoragePlayerId]: "test-player-id",
+        [localStorageTimer]: "300",
+        [localStoragePanoLat]: "test-lat",
+        [localStoragePanoLng]: "test-lng",
+        [localStoragePanoHeading]: "test-heading",
+        [localStoragePanoPitch]: "test-pitch",
+      };
+      localStorage.getItem.mockImplementation(key => data[key] ?? null);
+
+      expect(getInfoFromLocalStorage()).toMatchSnapshot();
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+    it("handles bad/missing extra data", () => {
+      const data = {
+        [localStorageGameId]: "test-game-id",
+        [localStoragePlayerName]: "test-player",
+        [localStoragePlayerId]: "test-player-id",
+        [localStoragePanoLng]: "test-lng",
+        [localStoragePanoPitch]: "test-pitch",
+      };
+      localStorage.getItem.mockImplementation(key => data[key] ?? null);
+
+      expect(getInfoFromLocalStorage()).toMatchSnapshot();
+      expect(localStorage.getItem).toMatchSnapshot();
+      expect(localStorage.setItem).toMatchSnapshot();
+      expect(localStorage.removeItem).toMatchSnapshot();
+    });
+  });
+});

+ 38 - 0
client/src/tests/markers.test.js

@@ -0,0 +1,38 @@
+import {
+  makeFlagMarker,
+  makeLine,
+  makeQuestionMarker,
+} from "../hooks/useMarkersFromGuesses/markers";
+
+describe("markers", () => {
+  describe("makeLine", () => {
+    it("makes a line", () => {
+      const result = makeLine("p1", "p2", "map", "color");
+      expect(result).toMatchSnapshot();
+    });
+  });
+
+  describe("makeQuestionMarker", () => {
+    it("makes a question marker", () => {
+      const result = makeQuestionMarker(
+        "map",
+        { lat: "lat", lng: "lng" },
+        "title",
+        "color"
+      );
+      expect(result).toMatchSnapshot();
+      result.listeners.get("click")[0]();
+      expect(global.window.open).toHaveBeenCalledWith(
+        "https://www.google.com/maps?hl=en&q=+lat,+lng",
+        "_blank"
+      );
+    });
+  });
+
+  describe("makeFlagMarker", () => {
+    it("makes a flag marker", () => {
+      const result = makeFlagMarker("map", { lat: "lat", lng: "lng" });
+      expect(result).toMatchSnapshot();
+    });
+  });
+});