mock cachedCrop in all tests

pull/1218/head^2
gabrielburnworth 2019-06-03 19:09:49 -07:00
parent 341eaa4ac7
commit 1f36ddd57e
16 changed files with 145 additions and 137 deletions

View File

@ -1,3 +1,7 @@
jest.mock("browser-speech", () => ({
talk: jest.fn(),
}));
jest.mock("../open_farm/cached_crop", () => ({
cachedCrop: jest.fn(() => Promise.resolve({ svg_icon: "icon" })),
}));

View File

@ -13,7 +13,7 @@ import { MovePlantProps } from "../interfaces";
import { fakePlant } from "../../__test_support__/fake_state/resources";
import { edit } from "../../api/crud";
import { Actions } from "../../constants";
import { DEFAULT_ICON } from "../../open_farm/icons";
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { history } from "../../history";
describe("movePlant", () => {
@ -82,7 +82,7 @@ describe("setDragIcon()", () => {
const e = { currentTarget: new Image(), dataTransfer: { setDragImage } };
setDragIcon("icon")(e);
const img = new Image();
img.src = "data:image/svg+xml;utf8,icon";
img.src = svgToUrl("icon");
expect(setDragImage).toHaveBeenCalledWith(img, 0, 0);
});

View File

@ -5,7 +5,7 @@ jest.mock("../../../../../api/crud", () => ({
}));
const mockSpreads: { [x: string]: number } = { mint: 100 };
jest.mock("../../../../../open_farm/icons", () => ({
jest.mock("../../../../../open_farm/cached_crop", () => ({
cachedCrop: jest.fn(p => Promise.resolve({ spread: mockSpreads[p] })),
}));
@ -24,7 +24,7 @@ import {
} from "../plant_actions";
import { fakePlant } from "../../../../../__test_support__/fake_state/resources";
import { edit, save, initSave } from "../../../../../api/crud";
import { cachedCrop } from "../../../../../open_farm/icons";
import { cachedCrop } from "../../../../../open_farm/cached_crop";
import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";

View File

@ -1,10 +1,11 @@
import * as React from "react";
import { GardenPlantProps, GardenPlantState } from "../../interfaces";
import { cachedCrop, DEFAULT_ICON, svgToUrl } from "../../../../open_farm/icons";
import { DEFAULT_ICON, svgToUrl } from "../../../../open_farm/icons";
import { round, transformXY } from "../../util";
import { DragHelpers } from "../../active_plant/drag_helpers";
import { Color } from "../../../../ui/index";
import { Actions } from "../../../../constants";
import { cachedCrop } from "../../../../open_farm/cached_crop";
export class GardenPlant extends
React.Component<GardenPlantProps, Partial<GardenPlantState>> {

View File

@ -1,4 +1,3 @@
import { error } from "farmbot-toastr";
import { Content } from "../../../../constants";
import { initSave, edit, save } from "../../../../api/crud";
@ -14,7 +13,7 @@ import { getPathArray } from "../../../../history";
import { findBySlug } from "../../../search_selectors";
import { transformXY, round } from "../../util";
import { movePlant } from "../../../actions";
import { cachedCrop } from "../../../../open_farm/icons";
import { cachedCrop } from "../../../../open_farm/cached_crop";
import { t } from "../../../../i18next_wrapper";
/** Return a new plant or plantTemplate object. */

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { round, transformXY } from "../../util";
import { cachedCrop } from "../../../../open_farm/icons";
import { cachedCrop } from "../../../../open_farm/cached_crop";
import { MapTransformProps, TaggedPlant } from "../../interfaces";
import { SpreadOverlapHelper } from "./spread_overlap_helper";
import { BotPosition } from "../../../../devices/interfaces";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { SpreadOverlapHelperProps } from "../../interfaces";
import { round, transformXY } from "../../util";
import { BotPosition } from "../../../../devices/interfaces";
import { cachedCrop } from "../../../../open_farm/icons";
import { cachedCrop } from "../../../../open_farm/cached_crop";
import { isUndefined } from "lodash";
enum OverlapColor {

View File

@ -1,6 +1,4 @@
jest.mock("react-redux", () => ({
connect: jest.fn()
}));
jest.mock("react-redux", () => ({ connect: jest.fn() }));
let mockPath = "";
jest.mock("../../../history", () => ({
@ -15,6 +13,7 @@ import { history } from "../../../history";
import {
fakeCropLiveSearchResult
} from "../../../__test_support__/fake_crop_search_result";
import { svgToUrl } from "../../../open_farm/icons";
describe("<AddPlant />", () => {
const fakeProps = (): AddPlantProps => {
@ -33,7 +32,7 @@ describe("<AddPlant />", () => {
expect(wrapper.text()).toContain("Mint");
expect(wrapper.text()).toContain("Done");
expect(wrapper.find("img").props().src)
.toEqual("data:image/svg+xml;utf8,fake_mint_svg");
.toEqual(svgToUrl("fake_mint_svg"));
});
it("goes back", () => {

View File

@ -1,6 +1,4 @@
jest.mock("react-redux", () => ({
connect: jest.fn()
}));
jest.mock("react-redux", () => ({ connect: jest.fn() }));
let mockPath = "";
jest.mock("../../../history", () => ({
@ -27,6 +25,7 @@ import {
fakeCropLiveSearchResult
} from "../../../__test_support__/fake_crop_search_result";
import { unselectPlant } from "../../actions";
import { svgToUrl } from "../../../open_farm/icons";
describe("<CropInfo />", () => {
const fakeProps = (): CropInfoProps => {
@ -51,7 +50,7 @@ describe("<CropInfo />", () => {
expect(wrapper.text()).toContain("Drag and drop into map");
expect(wrapper.text()).toContain("Row Spacing1000mm");
expect(wrapper.find("img").last().props().src)
.toEqual("data:image/svg+xml;utf8,fake_mint_svg");
.toEqual(svgToUrl("fake_mint_svg"));
});
it("navigates to /add", () => {

View File

@ -1,6 +1,5 @@
jest.mock("../../../open_farm/icons", () => ({
jest.mock("../../../open_farm/cached_crop", () => ({
cachedCrop: jest.fn(() => Promise.resolve({ svg_icon: "icon" })),
svgToUrl: jest.fn(x => x),
}));
jest.mock("../../../history", () => ({ push: jest.fn() }));
@ -15,6 +14,7 @@ import {
} from "../../../__test_support__/fake_state/resources";
import { Actions } from "../../../constants";
import { push } from "../../../history";
import { svgToUrl } from "../../../open_farm/icons";
describe("<PlantInventoryItem />", () => {
const fakeProps = (): PlantInventoryItemProps => {
@ -93,6 +93,6 @@ describe("<PlantInventoryItem />", () => {
shallow<PlantInventoryItem>(<PlantInventoryItem {...fakeProps()} />);
const img = new Image;
await wrapper.find("img").simulate("load", { currentTarget: img });
expect(wrapper.state().icon).toEqual("icon");
expect(wrapper.state().icon).toEqual(svgToUrl("icon"));
});
});

View File

@ -1,13 +1,13 @@
import * as React from "react";
import moment from "moment";
import { DEFAULT_ICON, cachedCrop, svgToUrl } from "../../open_farm/icons";
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { push } from "../../history";
import { Actions } from "../../constants";
import { TaggedPlant } from "../map/interfaces";
import { get } from "lodash";
import { unpackUUID } from "../../util";
import { t } from "../../i18next_wrapper";
import { cachedCrop } from "../../open_farm/cached_crop";
type IMGEvent = React.SyntheticEvent<HTMLImageElement>;

View File

@ -0,0 +1,30 @@
jest.mock("axios", () => ({
get: () => Promise.resolve({
data: {
id: 0,
data: {
attributes: {
svg_icon: "<svg>Wow</svg>",
slug: "lettuce"
}
}
}
})
}));
jest.unmock("../cached_crop");
import { cachedCrop } from "../cached_crop";
describe("cachedIcon()", () => {
it("does an HTTP request if the icon can't be found locally", (done) => {
cachedCrop("lettuce")
.then((item) => {
expect(item.svg_icon).toContain("<svg>Wow</svg>");
done();
})
.catch((error) => {
expect(error).toBeFalsy();
done();
});
});
});

View File

@ -0,0 +1,13 @@
import { OpenFarmAPI, svgToUrl } from "../icons";
describe("OpenFarmAPI", () => {
it("has a base URL", () => {
expect(OpenFarmAPI.OFBaseURL).toContain("openfarm.cc");
});
});
describe("svgToUrl()", () => {
it("returns svg url", () => {
expect(svgToUrl("icon")).toEqual("data:image/svg+xml;utf8,icon");
});
});

View File

@ -1,37 +0,0 @@
jest.mock("axios", function () {
return {
get: function () {
return Promise.resolve({
data: {
id: 0,
data: {
attributes: {
svg_icon: "<svg>Wow</svg>",
slug: "lettuce"
}
}
}
});
}
};
});
import { cachedCrop, OpenFarmAPI } from "../icons";
describe("cachedIcon()", () => {
it("does an HTTP request if the icon can't be found locally", (done) => {
cachedCrop("lettuce")
.then(function (item) {
expect(item.svg_icon).toContain("<svg>Wow</svg>");
done();
})
.catch((error) => {
expect(error).toBeFalsy();
done();
});
});
it("has a base URL", () => {
expect(OpenFarmAPI.OFBaseURL).toContain("openfarm.cc");
});
});

View File

@ -0,0 +1,78 @@
import axios, { AxiosResponse } from "axios";
import { Dictionary } from "farmbot";
import { isObject } from "lodash";
import { OFCropAttrs, OFCropResponse, OpenFarmAPI } from "./icons";
type OFIcon = Readonly<OFCropAttrs>;
const STORAGE_KEY = "openfarm_icons_with_spread";
function initLocalStorage() {
localStorage.setItem(STORAGE_KEY, "{}");
return {};
}
type IconDictionary = Dictionary<OFIcon | undefined>;
function getAllIconsFromCache(): IconDictionary {
try {
const dictionary = JSON.parse(localStorage.getItem(STORAGE_KEY) || "");
return isObject(dictionary) ? dictionary as IconDictionary : initLocalStorage();
} catch (error) {
return initLocalStorage();
}
}
function localStorageIconFetch(slug: string): Promise<OFIcon> | undefined {
const icon = getAllIconsFromCache()[slug];
return icon ? Promise.resolve(icon) : undefined;
}
function localStorageIconSet(icon: OFIcon): void {
const dictionary = getAllIconsFromCache();
dictionary[icon.slug] = icon;
localStorage.setItem(STORAGE_KEY, JSON.stringify(dictionary));
}
/** PROBLEM: HTTP requests get fired too fast. If you have 10 garlic plants,
* and the garlic icon is not cached locally, and you try to render 10 garlic
* icons in the first 100ms, and HTTP requests take more than 100ms, you will
* end up performing 10 HTTP requests at application start time. Not very
* efficient */
const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
function HTTPIconFetch(slug: string) {
const url = OpenFarmAPI.OFBaseURL + slug;
if (promiseCache[url]) {
return promiseCache[url];
} else {
promiseCache[url] = axios
.get<OFCropResponse>(url)
.then(cacheTheIcon(slug), cacheTheIcon(slug));
return promiseCache[url];
}
}
/** PROBLEM: You have 100 lettuce plants. You don't want to download an SVG icon
* 100 times.
* SOLUTION: Cache stuff. */
export function cachedCrop(slug: string): Promise<OFIcon> {
return localStorageIconFetch(slug) || HTTPIconFetch(slug);
}
const cacheTheIcon = (slug: string) =>
(resp: AxiosResponse<OFCropResponse>): OFIcon => {
if (resp
&& resp.data
&& resp.data.data
&& resp.data.data.attributes) {
const icon = {
slug: resp.data.data.attributes.slug,
spread: resp.data.data.attributes.spread,
svg_icon: resp.data.data.attributes.svg_icon
};
localStorageIconSet(icon);
return icon;
} else {
return { slug, spread: undefined, svg_icon: undefined };
}
};

View File

@ -1,7 +1,3 @@
import axios, { AxiosResponse } from "axios";
import { Dictionary } from "farmbot";
import { isObject } from "lodash";
const BASE = "https://openfarm.cc/api/v1/crops/";
export const DATA_URI = "data:image/svg+xml;utf8,";
export const DEFAULT_ICON = "/app-resources/img/generic-plant.svg";
@ -28,80 +24,6 @@ export namespace OpenFarmAPI {
export let OFBaseURL = BASE;
}
type OFIcon = Readonly<OFCropAttrs>;
const STORAGE_KEY = "openfarm_icons_with_spread";
function initLocalStorage() {
localStorage.setItem(STORAGE_KEY, "{}");
return {};
}
type IconDictionary = Dictionary<OFIcon | undefined>;
function getAllIconsFromCache(): IconDictionary {
try {
const dictionary = JSON.parse(localStorage.getItem(STORAGE_KEY) || "");
return isObject(dictionary) ? dictionary as IconDictionary : initLocalStorage();
} catch (error) {
return initLocalStorage();
}
}
function localStorageIconFetch(slug: string): Promise<OFIcon> | undefined {
const icon = getAllIconsFromCache()[slug];
return icon ? Promise.resolve(icon) : undefined;
}
function localStorageIconSet(icon: OFIcon): void {
const dictionary = getAllIconsFromCache();
dictionary[icon.slug] = icon;
localStorage.setItem(STORAGE_KEY, JSON.stringify(dictionary));
}
/** PROBLEM: HTTP requests get fired too fast. If you have 10 garlic plants,
* and the garlic icon is not cached locally, and you try to render 10 garlic
* icons in the first 100ms, and HTTP requests take more than 100ms, you will
* end up performing 10 HTTP requests at application start time. Not very
* efficient */
const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
function HTTPIconFetch(slug: string) {
const url = BASE + slug;
if (promiseCache[url]) {
return promiseCache[url];
} else {
promiseCache[url] = axios
.get<OFCropResponse>(url)
.then(cacheTheIcon(slug), cacheTheIcon(slug));
return promiseCache[url];
}
}
/** PROBLEM: You have 100 lettuce plants. You don't want to download an SVG icon
* 100 times.
* SOLUTION: Cache stuff. */
export function cachedCrop(slug: string): Promise<OFIcon> {
return localStorageIconFetch(slug) || HTTPIconFetch(slug);
}
const cacheTheIcon = (slug: string) =>
(resp: AxiosResponse<OFCropResponse>): OFIcon => {
if (resp
&& resp.data
&& resp.data.data
&& resp.data.data.attributes) {
const icon = {
slug: resp.data.data.attributes.slug,
spread: resp.data.data.attributes.spread,
svg_icon: resp.data.data.attributes.svg_icon
};
localStorageIconSet(icon);
return icon;
} else {
return { slug, spread: undefined, svg_icon: undefined };
}
};
export function svgToUrl(xml: string | undefined): string {
return xml ?
(DATA_URI + encodeURIComponent(xml)) : DEFAULT_ICON;