Merge pull request #1498 from FarmBot/groups_perf

Release Candidate I
pull/1503/head
Rick Carlino 2019-10-10 12:14:36 -05:00 committed by GitHub
commit 4931d96d74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 257 additions and 196 deletions

View File

@ -27,8 +27,9 @@ if Rails.env == "development"
SavedGarden,
SensorReading,
FarmwareInstallation,
Device,
PointGroup,
Tool,
Device,
Delayed::Job,
Delayed::Backend::ActiveRecord::Job,
].map(&:delete_all)

View File

@ -9,7 +9,8 @@ import {
} from "../resources/selectors";
import { Props } from "./interfaces";
import {
validFwConfig, shouldDisplay as shouldDisplayFunc,
validFwConfig,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../util";
import { getWebAppConfigValue } from "../config_storage/actions";

View File

@ -181,7 +181,7 @@
transition: all 0.2s ease;
}
}
.plant-search-item {
%search-item {
cursor: pointer;
padding: 0.5rem 1rem;
&:hover,
@ -195,6 +195,19 @@
width: 4rem;
}
}
.plant-search-item {
@extend %search-item;
}
.group-search-item {
@extend %search-item;
&:hover,
&.hovered {
background: #d7eaea;
}
}
%panel-item-base {
text-align: right;
font-size: 1rem;

View File

@ -229,9 +229,8 @@
max-height: calc(100vh - 10rem);
overflow-y: auto;
overflow-x: hidden;
.plant-search-item {
pointer-events: none;
}
.plant-search-item,
.group-search-item { pointer-events: none; }
img {
filter: grayscale(100%);
}

View File

@ -1,7 +1,7 @@
import * as React from "react";
import axios from "axios";
import { t } from "../../i18next_wrapper";
import { FarmbotOsProps, FarmbotOsState } from "../interfaces";
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
import { save, edit } from "../../api/crud";
import { MustBeOnline, isBotOnline } from "../must_be_online";
@ -157,7 +157,7 @@ export class FarmbotOsSettings
</label>
</Col>
<Col xs={7}>
<BootSequenceSelector />
{this.props.shouldDisplay(Feature.boot_sequence) && <BootSequenceSelector />}
</Col>
</Row>
<PowerAndReset

View File

@ -65,29 +65,31 @@ export type SourceFwConfig = (config: McuParamName) =>
export type ShouldDisplay = (x: Feature) => boolean;
/** Names of features that use minimum FBOS version checking. */
export enum Feature {
assertion_block = "assertion_block",
named_pins = "named_pins",
sensors = "sensors",
change_ownership = "change_ownership",
variables = "variables",
loops = "loops",
api_pin_bindings = "api_pin_bindings",
farmduino_k14 = "farmduino_k14",
jest_feature = "jest_feature", // for tests
backscheduled_regimens = "backscheduled_regimens",
endstop_invert = "endstop_invert",
diagnostic_dumps = "diagnostic_dumps",
rpi_led_control = "rpi_led_control",
mark_as_step = "mark_as_step",
firmware_restart = "firmware_restart",
api_farmware_installations = "api_farmware_installations",
api_farmware_env = "api_farmware_env",
use_update_channel = "use_update_channel",
long_scaling_factor = "long_scaling_factor",
flash_firmware = "flash_firmware",
api_farmware_installations = "api_farmware_installations",
api_pin_bindings = "api_pin_bindings",
assertion_block = "assertion_block",
backscheduled_regimens = "backscheduled_regimens",
boot_sequence = "boot_sequence",
change_ownership = "change_ownership",
diagnostic_dumps = "diagnostic_dumps",
endstop_invert = "endstop_invert",
express_k10 = "express_k10",
farmduino_k14 = "farmduino_k14",
firmware_restart = "firmware_restart",
flash_firmware = "flash_firmware",
groups = "groups",
jest_feature = "jest_feature",
long_scaling_factor = "long_scaling_factor",
mark_as_step = "mark_as_step",
named_pins = "named_pins",
none_firmware = "none_firmware",
rpi_led_control = "rpi_led_control",
sensors = "sensors",
use_update_channel = "use_update_channel",
variables = "variables"
}
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;

View File

@ -11,7 +11,7 @@ import {
} from "./components/source_config_value";
import {
determineInstalledOsVersion, validFwConfig, validFbosConfig,
shouldDisplay as shouldDisplayFunc
createShouldDisplayFn as shouldDisplayFunc
} from "../util";
import {
saveOrEditFarmwareEnv, reduceFarmwareEnv
@ -27,8 +27,9 @@ export function mapStateToProps(props: Everything): Props {
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const shouldDisplay = shouldDisplayFunc(installedOsVersion,
props.bot.minOsFeatureData,
fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;

View File

@ -21,7 +21,7 @@ describe("mapStateToPropsAddEdit()", () => {
describe("handleTime()", () => {
const { handleTime } = mapStateToPropsAddEdit(fakeState());
it("start_time", () => {
it("handles an element with name `start_time`", () => {
const e = {
currentTarget: { value: "10:54", name: "start_time" }
} as React.SyntheticEvent<HTMLInputElement>;
@ -29,13 +29,21 @@ describe("mapStateToPropsAddEdit()", () => {
expect(result).toContain("54");
});
it("end_time", () => {
it("handles an element with name `end_time`", () => {
const e = {
currentTarget: { value: "10:53", name: "end_time" }
} as React.SyntheticEvent<HTMLInputElement>;
const result = handleTime(e, "2017-05-21T22:00:00.000");
expect(result).toContain("53");
});
it("crashes on other names", () => {
const e = {
currentTarget: { value: "10:52", name: "other" }
} as React.SyntheticEvent<HTMLInputElement>;
const boom = () => handleTime(e, "2017-05-21T22:00:00.000");
expect(boom).toThrowError("Expected a name attribute from time field.");
});
});
describe("executableOptions()", () => {

View File

@ -24,7 +24,7 @@ import {
import { DropDownItem } from "../../ui/index";
import {
validFbosConfig,
shouldDisplay as shouldDisplayFunc,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../../util";
import {
@ -134,7 +134,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
switch (kind) {
case "Sequence": return findSequenceById(props.resources.index, id);
case "Regimen": return findRegimenById(props.resources.index, id);
default: throw new Error("GOT A BAD `KIND` STRING");
}
};
const dev = getDeviceAccountSettings(props.resources.index);

View File

@ -9,14 +9,12 @@ jest.mock("../../actions", () => ({
}));
import React from "react";
import { GroupDetailActive, LittleIcon } from "../group_detail_active";
import { GroupDetailActive } from "../group_detail_active";
import { mount, shallow } from "enzyme";
import {
fakePointGroup, fakePlant
} from "../../../__test_support__/fake_state/resources";
import { save, overwrite, edit } from "../../../api/crud";
import { toggleHoveredPlant } from "../../actions";
import { DEFAULT_ICON } from "../../../open_farm/icons";
import { save, edit } from "../../../api/crud";
import { SpecialStatus } from "farmbot";
describe("<GroupDetailActive/>", () => {
@ -30,47 +28,6 @@ describe("<GroupDetailActive/>", () => {
group.body.point_ids = [plant.body.id];
return { dispatch: jest.fn(), group, plants };
}
const icon = "doge.jpg";
it("removes points onClick", () => {
const { plants, dispatch, group } = fakeProps();
const el = shallow(<LittleIcon
plant={plants[0]}
group={group}
dispatch={dispatch}
icon="doge.jpg" />);
el.simulate("click");
const emptyGroup = expect.objectContaining({
name: "XYZ",
point_ids: []
});
expect(overwrite).toHaveBeenCalledWith(group, emptyGroup);
expect(dispatch).toHaveBeenCalled();
});
it("toggles onMouseEnter", () => {
const { plants, dispatch, group } = fakeProps();
const plant = plants[0];
const el = shallow(<LittleIcon
plant={plant}
group={group}
dispatch={dispatch}
icon={icon} />);
el.simulate("mouseEnter");
expect(toggleHoveredPlant).toHaveBeenCalledWith(plant.uuid, icon);
});
it("toggled onMouseLeave", () => {
const { plants, dispatch, group } = fakeProps();
const plant = plants[0];
const el = shallow(<LittleIcon
plant={plant}
group={group}
dispatch={dispatch}
icon={icon} />);
el.simulate("mouseLeave");
expect(toggleHoveredPlant).toHaveBeenCalledWith(undefined, icon);
});
it("saves", () => {
const p = fakeProps();
@ -87,19 +44,6 @@ describe("<GroupDetailActive/>", () => {
expect(el.find("input").prop("defaultValue")).toContain("XYZ");
});
it("provides the DEFAULT_ICON when OF has no icon to provide", () => {
const plant = fakePlant();
const comp = new GroupDetailActive(fakeProps());
comp.state = {
[plant.uuid]: {
slug: plant.uuid,
svg_icon: undefined
}
};
const result = comp.findIcon(plant);
expect(result).toEqual(DEFAULT_ICON);
});
it("changes group name", () => {
const NEW_NAME = "new group name";
const wrapper = shallow(<GroupDetailActive {...fakeProps()} />);
@ -123,7 +67,7 @@ describe("<GroupDetailActive/>", () => {
},
kind: "PointGroup",
specialStatus: "DIRTY",
uuid: "PointGroup.0.16",
uuid: p.group.uuid,
},
{ sort_type: "random" });
});

View File

@ -39,7 +39,7 @@ describe("<GroupListPanel />", () => {
it("renders relevant group data as a list", () => {
const p = fakeProps();
const wrapper = mount(<GroupListPanel {...p} />);
wrapper.find(".plant-search-item").first().simulate("click");
wrapper.find(".group-search-item").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/groups/9");
["3 items",

View File

@ -0,0 +1,81 @@
jest.mock("../../actions", () => ({ toggleHoveredPlant: jest.fn() }));
jest.mock("../../../api/crud", () => ({ overwrite: jest.fn() }));
import React from "react";
import { PointGroupItem } from "../point_group_item";
import { shallow } from "enzyme";
import { fakePlant, fakePointGroup } from "../../../__test_support__/fake_state/resources";
import { DeepPartial } from "redux";
import { cachedCrop } from "../../../open_farm/cached_crop";
import { toggleHoveredPlant } from "../../actions";
import { overwrite } from "../../../api/crud";
describe("<PointGroupItem/>", () => {
const newProps = (): PointGroupItem["props"] => ({
dispatch: jest.fn(),
plant: fakePlant(),
group: fakePointGroup(),
hovered: true
});
it("renders", () => {
const props = newProps();
const el = shallow<HTMLSpanElement>(<PointGroupItem {...props} />);
const i = el.instance() as PointGroupItem;
expect(el.first().prop("onMouseEnter")).toEqual(i.enter);
expect(el.first().prop("onMouseLeave")).toEqual(i.leave);
expect(el.first().prop("onClick")).toEqual(i.click);
});
it("handles hovering", async () => {
const i = new PointGroupItem(newProps());
i.setState = jest.fn();
type E = React.SyntheticEvent<HTMLImageElement, Event>;
const partialE: DeepPartial<E> = {
currentTarget: {
getAttribute: jest.fn(),
setAttribute: jest.fn(),
}
};
const e = partialE as E;
await i.maybeGetCachedIcon(e as E);
const slug = i.props.plant.body.openfarm_slug;
expect(cachedCrop).toHaveBeenCalledWith(slug);
const icon = "data:image/svg+xml;utf8,icon";
expect(i.setState).toHaveBeenCalledWith({ icon });
expect(e.currentTarget.setAttribute).toHaveBeenCalledWith("src", icon);
});
it("handles mouse enter", () => {
const i = new PointGroupItem(newProps());
i.state.icon = "X";
i.enter();
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
expect(toggleHoveredPlant)
.toHaveBeenCalledWith(i.props.plant.uuid, "X");
});
it("handles mouse exit", () => {
const i = new PointGroupItem(newProps());
i.state.icon = "X";
i.leave();
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
expect(toggleHoveredPlant).toHaveBeenCalledWith(undefined, "");
});
it("handles clicks", () => {
const i = new PointGroupItem(newProps());
i.click();
expect(i.props.dispatch).toHaveBeenCalledTimes(1);
expect(overwrite).toHaveBeenCalledWith({
body: { name: "Fake", point_ids: [], sort_type: "xy_ascending" },
kind: "PointGroup",
specialStatus: "",
uuid: expect.any(String),
}, {
name: "Fake",
point_ids: [],
sort_type: "xy_ascending",
});
});
});

View File

@ -8,14 +8,13 @@ import {
} from "../plants/designer_panel";
import { TaggedPointGroup } from "farmbot";
import { DeleteButton } from "../../controls/pin_form_fields";
import { svgToUrl, DEFAULT_ICON } from "../../open_farm/icons";
import { overwrite, save, edit } from "../../api/crud";
import { save, edit } from "../../api/crud";
import { Dictionary } from "lodash";
import { cachedCrop, OFIcon } from "../../open_farm/cached_crop";
import { toggleHoveredPlant } from "../actions";
import { OFIcon } from "../../open_farm/cached_crop";
import { TaggedPlant } from "../map/interfaces";
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { PointGroupItem } from "./point_group_item";
interface GroupDetailActiveProps {
dispatch: Function;
@ -24,34 +23,6 @@ interface GroupDetailActiveProps {
}
type State = Dictionary<OFIcon | undefined>;
const removePoint = (group: TaggedPointGroup, pointId: number) => {
type Body = (typeof group)["body"];
const nextGroup: Body = { ...group.body };
nextGroup.point_ids = nextGroup.point_ids.filter(x => x !== pointId);
return overwrite(group, nextGroup);
};
interface LittleIconProps {
/** URL (or even a data-url) to the icon image. */
icon: string;
group: TaggedPointGroup;
plant: TaggedPlant;
dispatch: Function;
}
export const LittleIcon =
({ group, plant: point, icon, dispatch }: LittleIconProps) => {
const { body } = point;
const p = point;
const plantUUID = point.uuid;
return <span
key={plantUUID}
onMouseEnter={() => dispatch(toggleHoveredPlant(plantUUID, icon))}
onMouseLeave={() => dispatch(toggleHoveredPlant(undefined, icon))}
onClick={() => dispatch(removePoint(group, body.id || 0))}>
<img src={icon} alt={p.body.name} width={32} height={32} />
</span>;
};
export class GroupDetailActive
extends React.Component<GroupDetailActiveProps, State> {
@ -61,41 +32,14 @@ export class GroupDetailActive
this.props.dispatch(edit(this.props.group, { name: currentTarget.value }));
};
handleIcon =
(uuid: string) =>
(icon: Readonly<OFIcon>) =>
this.setState({ [uuid]: icon });
performLookup = (plant: TaggedPlant) => {
cachedCrop(plant.body.openfarm_slug).then(this.handleIcon(plant.uuid));
return DEFAULT_ICON;
}
findIcon = (plant: TaggedPlant) => {
const svg = this.state[plant.uuid];
if (svg) {
if (svg.svg_icon) {
return svgToUrl(svg.svg_icon);
}
return DEFAULT_ICON;
}
return this.performLookup(plant);
}
get name() {
const { group } = this.props;
return group ? group.body.name : "Group Not found";
}
get icons() {
const plants = sortGroupBy(this.props.group.body.sort_type,
this.props.plants);
return plants.map(point => {
return <LittleIcon
return <PointGroupItem
key={point.uuid}
icon={this.findIcon(point)}
hovered={false}
group={this.props.group}
plant={point}
dispatch={this.props.dispatch} />;
@ -130,7 +74,7 @@ export class GroupDetailActive
panelName={"groups"}>
<label>{t("GROUP NAME")}{this.saved ? "" : "*"}</label>
<input
defaultValue={this.name}
defaultValue={this.props.group.body.name}
onChange={this.update}
onBlur={this.saveGroup} />
<PointGroupSortSelector

View File

@ -12,8 +12,8 @@ interface GroupInventoryItemProps {
export function GroupInventoryItem(props: GroupInventoryItemProps) {
return <div
onClick={props.onClick}
className={`plant-search-item ${props.hovered ? "hovered" : ""}`}>
<span className="plant-search-item-name">
className={`group-search-item ${props.hovered ? "hovered" : ""}`}>
<span className="group-search-item-name">
{props.group.body.name}
</span>
<i className="group-item-count">

View File

@ -0,0 +1,69 @@
import * as React from "react";
import { DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { TaggedPlant } from "../map/interfaces";
import { cachedCrop } from "../../open_farm/cached_crop";
import { toggleHoveredPlant } from "../actions";
import { TaggedPointGroup, uuid } from "farmbot";
import { overwrite } from "../../api/crud";
type IMGEvent = React.SyntheticEvent<HTMLImageElement>;
export interface PointGroupItemProps {
plant: TaggedPlant;
group: TaggedPointGroup;
dispatch: Function;
hovered: boolean;
}
interface PointGroupItemState { icon: string; }
const removePoint = (group: TaggedPointGroup, pointId: number) => {
type Body = (typeof group)["body"];
const nextGroup: Body = { ...group.body };
nextGroup.point_ids = nextGroup.point_ids.filter(x => x !== pointId);
return overwrite(group, nextGroup);
};
// The individual plants in the point group detail page.
export class PointGroupItem extends React.Component<PointGroupItemProps, PointGroupItemState> {
state: PointGroupItemState = { icon: "" };
key = uuid();
enter = () => this
.props
.dispatch(toggleHoveredPlant(this.props.plant.uuid, this.state.icon));
leave = () => this
.props
.dispatch(toggleHoveredPlant(undefined, ""));
click = () => this
.props
.dispatch(removePoint(this.props.group, this.props.plant.body.id || 0));
maybeGetCachedIcon = ({ currentTarget }: IMGEvent) => {
return cachedCrop(this.props.plant.body.openfarm_slug).then((crop) => {
const i = svgToUrl(crop.svg_icon);
if (i !== currentTarget.getAttribute("src")) {
currentTarget.setAttribute("src", i);
}
this.setState({ icon: i });
});
};
render() {
return <span
key={this.key}
onMouseEnter={this.enter}
onMouseLeave={this.leave}
onClick={this.click}>
<img
src={DEFAULT_ICON}
onLoad={this.maybeGetCachedIcon}
width={32}
height={32} />
</span>;
}
}

View File

@ -14,7 +14,7 @@ import {
} from "../resources/selectors";
import {
validBotLocationData, validFwConfig, unpackUUID,
shouldDisplay as shouldDisplayFunc,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../util";
import { getWebAppConfigValue } from "../config_storage/actions";

View File

@ -11,7 +11,7 @@ import {
} from "../resources/selectors_by_kind";
import {
determineInstalledOsVersion,
shouldDisplay as shouldDisplayFunc,
createShouldDisplayFn as shouldDisplayFunc,
betterCompact
} from "../util";
import { ResourceIndex } from "../resources/interfaces";

View File

@ -8,7 +8,7 @@ import {
} from "../devices/components/source_config_value";
import {
validFbosConfig, determineInstalledOsVersion,
shouldDisplay as shouldDisplayFunc
createShouldDisplayFn as shouldDisplayFunc
} from "../util";
import { ResourceIndex } from "../resources/interfaces";
import { TaggedLog } from "farmbot";

View File

@ -4,6 +4,8 @@ import { isObject } from "lodash";
import { OFCropAttrs, OFCropResponse, OpenFarmAPI } from "./icons";
export type OFIcon = Readonly<OFCropAttrs>;
type IconDictionary = Dictionary<OFIcon | undefined>;
const STORAGE_KEY = "openfarm_icons_with_spread";
function initLocalStorage() {
@ -11,8 +13,6 @@ function initLocalStorage() {
return {};
}
type IconDictionary = Dictionary<OFIcon | undefined>;
function getAllIconsFromCache(): IconDictionary {
try {
const dictionary = JSON.parse(localStorage.getItem(STORAGE_KEY) || "");
@ -40,21 +40,6 @@ function localStorageIconSet(icon: OFIcon): void {
* efficient */
const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
function HTTPIconFetch(slug: string) {
const url = OpenFarmAPI.OFBaseURL + slug;
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
@ -72,3 +57,18 @@ const cacheTheIcon = (slug: string) =>
return { slug, spread: undefined, svg_icon: undefined };
}
};
function HTTPIconFetch(slug: string) {
const url = OpenFarmAPI.OFBaseURL + slug;
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);
}

View File

@ -18,7 +18,7 @@ import moment from "moment";
import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces";
import {
randomColor, determineInstalledOsVersion,
shouldDisplay as shouldDisplayFunc,
createShouldDisplayFn as shouldDisplayFunc,
timeFormatString
} from "../util";
import { resourceUsageList } from "../resources/in_use";

View File

@ -55,7 +55,7 @@ export const LocationForm =
const variableListItems = displayVariables ? [PARENT(determineVarDDILabel({
label: "parent", resources, uuid: sequenceUuid, forceExternal: headerForm
}))] : [];
const displayGroups = props.shouldDisplay(Feature.loops) && !disallowGroups;
const displayGroups = props.shouldDisplay(Feature.groups) && !disallowGroups;
const list = locationFormList(resources, variableListItems, displayGroups);
/** Variable name. */
const { label } = celeryNode.args;

View File

@ -6,7 +6,7 @@ import {
import { getStepTag } from "../resources/sequence_tagging";
import { enabledAxisMap } from "../devices/components/axis_tracking_status";
import {
shouldDisplay as shouldDisplayFunc,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion, validFwConfig
} from "../util";
import { BooleanSetting } from "../session_keys";

View File

@ -2,7 +2,7 @@ import {
semverCompare,
SemverResult,
minFwVersionCheck,
shouldDisplay,
createShouldDisplayFn,
determineInstalledOsVersion,
versionOK,
} from "../version";
@ -121,34 +121,34 @@ describe("shouldDisplay()", () => {
const fakeMinOsData = { jest_feature: "1.0.0" };
it("should display", () => {
expect(shouldDisplay("1.0.0", fakeMinOsData, undefined)(
expect(createShouldDisplayFn("1.0.0", fakeMinOsData, undefined)(
Feature.jest_feature)).toBeTruthy();
expect(shouldDisplay("10.0.0", fakeMinOsData, undefined)(
expect(createShouldDisplayFn("10.0.0", fakeMinOsData, undefined)(
Feature.jest_feature)).toBeTruthy();
expect(shouldDisplay("10.0.0",
expect(createShouldDisplayFn("10.0.0",
{ jest_feature: "1.0.0" }, undefined)(
Feature.jest_feature)).toBeTruthy();
});
it("shouldn't display", () => {
expect(shouldDisplay("0.9.0", fakeMinOsData, undefined)(
expect(createShouldDisplayFn("0.9.0", fakeMinOsData, undefined)(
Feature.jest_feature)).toBeFalsy();
expect(shouldDisplay(undefined, fakeMinOsData, undefined)(
expect(createShouldDisplayFn(undefined, fakeMinOsData, undefined)(
Feature.jest_feature)).toBeFalsy();
// tslint:disable-next-line:no-any
const unknown_feature = "unknown_feature" as any;
expect(shouldDisplay("1.0.0", fakeMinOsData, undefined)(
expect(createShouldDisplayFn("1.0.0", fakeMinOsData, undefined)(
unknown_feature)).toBeFalsy();
expect(shouldDisplay("1.0.0", undefined, undefined)(
expect(createShouldDisplayFn("1.0.0", undefined, undefined)(
unknown_feature)).toBeFalsy();
// tslint:disable-next-line:no-any
expect(shouldDisplay("1.0.0", "" as any, undefined)(
expect(createShouldDisplayFn("1.0.0", "" as any, undefined)(
unknown_feature)).toBeFalsy();
// tslint:disable-next-line:no-any
expect(shouldDisplay("1.0.0", "{}" as any, undefined)(
expect(createShouldDisplayFn("1.0.0", "{}" as any, undefined)(
unknown_feature)).toBeFalsy();
// tslint:disable-next-line:no-any
expect(shouldDisplay("1.0.0", "bad" as any, undefined)(
expect(createShouldDisplayFn("1.0.0", "bad" as any, undefined)(
unknown_feature)).toBeFalsy();
});
});

View File

@ -109,14 +109,15 @@ export enum MinVersionOverride {
* @param current installed OS version string to compare against data ("0.0.0")
* @param lookupData min req versions data, for example {"feature": "1.0.0"}
*/
export function shouldDisplay(
export function createShouldDisplayFn(
current: string | undefined,
lookupData: MinOsFeatureLookup | undefined,
override: string | undefined) {
return function (feature: Feature): boolean {
const target = override || current;
if (isString(target)) {
const min = (lookupData || {})[feature] || MinVersionOverride.NEVER;
const table = lookupData || {};
const min = table[feature] || MinVersionOverride.NEVER;
switch (semverCompare(target, min)) {
case SemverResult.LEFT_IS_GREATER:
case SemverResult.EQUAL:
@ -143,8 +144,6 @@ export function determineInstalledOsVersion(
return fromBotState === "" ? undefined : fromBotState;
case SemverResult.RIGHT_IS_GREATER:
return fromAPI === "" ? undefined : fromAPI;
default:
return undefined;
}
}