Skip to content
This repository has been archived by the owner on Apr 6, 2022. It is now read-only.

Commit

Permalink
Merge #281
Browse files Browse the repository at this point in the history
281: enforce isEnrollmentPaused for required recipes a320faa  r=tiftran a=rehandalal

Merging in @tiftran's changes from `master` to `main`.

Co-authored-by: tiftran <ttran@mozilla.com>
Co-authored-by: bors[bot] <26634292+bors[bot]@users.noreply.github.com>
  • Loading branch information
bors[bot] and tiftran authored Jun 29, 2020
2 parents b8d496c + e36ebac commit c0f1431
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 30 deletions.
5 changes: 5 additions & 0 deletions extension/content/components/pages/RecipeFormPage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { useParams } from "react-router-dom";

import { INITIAL_ACTION_ARGUMENTS } from "devtools/components/recipes/form/ActionArguments";
import RecipeForm from "devtools/components/recipes/form/RecipeForm";
import RecipeFormHeader from "devtools/components/recipes/form/RecipeFormHeader";
import {
Expand Down Expand Up @@ -35,6 +36,10 @@ export default function RecipeFormPage() {
.then(({ comment, action_name, ...recipeData }) => {
setData({
...recipeData,
arguments: {
...INITIAL_ACTION_ARGUMENTS[action_name],
...recipeData.arguments,
},
action: actions.find((a) => a.name === action_name),
});
setImportInstructions(comment);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const INITIAL_ACTION_ARGUMENTS = {
"multi-preference-experiment": {
branches: [],
experimentDocumentUrl: "",
isEnrollmentPaused: false,
slug: "",
userFacingDescription: "",
userFacingName: "",
Expand Down
153 changes: 123 additions & 30 deletions tests/RecipeForm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
within,
} from "@testing-library/react";
import React from "react";

import "@testing-library/jest-dom/extend-expect";

import App from "devtools/components/App";
import RecipeFormPage from "devtools/components/pages/RecipeFormPage";
import ExperimenterAPI from "devtools/utils/experimenterApi";
import NormandyAPI from "devtools/utils/normandyApi";

import { ActionsResponse, FiltersFactory } from "./factories/filterFactory";
Expand All @@ -17,14 +19,14 @@ import {
ChannelFilterObjectFactory,
BucketSampleFilterObjectFactory,
} from "./factories/filterObjectFactory";
import { RecipeFactory } from "./factories/recipeFactory";
import {
RecipeFactory,
MultiPrefBranchFactory,
MultiPreferenceFactory,
} from "./factories/recipeFactory";

describe("The `RecipeForm` component", () => {
afterEach(() => {
jest.clearAllMocks();
cleanup();
});

afterEach(cleanup);
const findForm = (formGroups, formName) => {
const forms = formGroups.filter((form) =>
within(form).queryByText(formName),
Expand Down Expand Up @@ -91,24 +93,7 @@ describe("The `RecipeForm` component", () => {
};
};

const setup = () => {
const versions = VersionFilterObjectFactory.build(
{},
{ generateVersionsCount: 2 },
);
const channels = ChannelFilterObjectFactory.build(
{},
{ generateChannelsCount: 1 },
);
const sample = BucketSampleFilterObjectFactory.build();
const filterObject = [versions, sample, channels];
const recipe = RecipeFactory.build(
{},
{
actionName: "console-log",
filterObject,
},
);
const setup = (recipe) => {
const pageResponse = { results: [recipe] };
const filtersResponse = FiltersFactory.build(
{},
Expand All @@ -132,12 +117,58 @@ describe("The `RecipeForm` component", () => {
jest
.spyOn(NormandyAPI.prototype, "fetchRecipe")
.mockImplementation(() => Promise.resolve(recipe));
};

const consoleLogRecipeSetup = () => {
const versions = VersionFilterObjectFactory.build(
{},
{ generateVersionsCount: 2 },
);
const channels = ChannelFilterObjectFactory.build(
{},
{ generateChannelsCount: 1 },
);
const sample = BucketSampleFilterObjectFactory.build();
const filterObject = [versions, sample, channels];
return RecipeFactory.build(
{},
{
actionName: "console-log",
filterObject,
},
);
};

const multiprefExperimenterRecipeSetUp = () => {
const versions = VersionFilterObjectFactory.build(
{},
{ generateVersionsCount: 2 },
);
const recipe = RecipeFactory.build(
{},
{
actionName: "multi-preference-experiment",
filterObject: [versions],
},
);
const branch1 = MultiPrefBranchFactory.build(
{},
{ generatePreferenceCount: 2 },
);
const branch2 = MultiPrefBranchFactory.build(
{},
{ generatePreferenceCount: 1 },
);
const branches = [branch1, branch2];
const multiPrefArguments = MultiPreferenceFactory.build({ branches });
recipe.arguments = multiPrefArguments;
recipe.action_name = "multi-preference-experiment";
return recipe;
};

it("creation recipe form", async () => {
setup();
const recipe = RecipeFactory.build();
setup(recipe);
const { getByText, getAllByRole } = await render(<App />);
fireEvent.click(getByText("Create Recipe"));
await waitFor(() =>
Expand Down Expand Up @@ -277,9 +308,9 @@ describe("The `RecipeForm` component", () => {
});

it("edit recipe form", async () => {
const recipeData = setup();
const recipeData = consoleLogRecipeSetup();
setup(recipeData);
const { getByText, getAllByRole } = await render(<App />);

await waitFor(() => {
expect(getByText("Edit Recipe")).toBeInTheDocument();
});
Expand Down Expand Up @@ -362,11 +393,11 @@ describe("The `RecipeForm` component", () => {
});

it("save button is re-enabled when form errors are addressed", async () => {
const recipeData = setup();
const recipeData = consoleLogRecipeSetup();
setup(recipeData);
const { getByText, getAllByRole } = await render(<App />);

fireEvent.click(getByText("Recipes"));

await waitFor(() => {
expect(getByText("Edit Recipe")).toBeInTheDocument();
});
Expand Down Expand Up @@ -440,4 +471,66 @@ describe("The `RecipeForm` component", () => {
updatedRecipeData,
);
});

it("should have isEnrollmentPaused set when import from experimenter", async () => {
const recipeData = multiprefExperimenterRecipeSetUp();
setup(recipeData);
jest
.spyOn(ExperimenterAPI.prototype, "fetchRecipe")
.mockImplementation(() => Promise.resolve(recipeData));
jest
.spyOn(NormandyAPI.prototype, "fetchAllActions")
.mockImplementation(() =>
Promise.resolve([{ id: 1, name: "multi-preference-experiment" }]),
);

/* global renderWithContext */
const { getByText, getAllByRole } = await renderWithContext(
<RecipeFormPage />,
{
route: "/prod/recipes/import/experimenter-slug",
path: "/prod/recipes/import/:experimenterSlug",
},
);
expect(ExperimenterAPI.prototype.fetchRecipe).toHaveBeenCalled();

await waitFor(() => {
expect(getByText("Experimenter Slug")).toBeInTheDocument();
});

const formGroups = getAllByRole("group");
const highVolumeForm = findForm(formGroups, "High Volume Recipe");
const highVolumeToggle = within(highVolumeForm).getByRole("button");

fireEvent.click(highVolumeToggle);

fireEvent.click(getByText("Save"));

const modalDialog = getAllByRole("dialog")[0];
const commentInput = modalDialog.querySelector("textArea");
const saveMessage = "Edited Recipe";
fireEvent.change(commentInput, { target: { value: saveMessage } });

fireEvent.click(within(modalDialog).getByText("Save"));

/* eslint-disable prefer-const */ let {
action_name: _omitActionName,
comment: _omitComment,
...updatedRecipeData
} = recipeData;
/* eslint-enable prefer-const */ updatedRecipeData = {
...updatedRecipeData,
comment: saveMessage,
action_id: 1,
arguments: {
...updatedRecipeData.arguments,
isHighPopulation: true,
isEnrollmentPaused: false,
},
};
expect(NormandyAPI.prototype.saveRecipe).toBeCalledWith(
undefined,
updatedRecipeData,
);
});
});
53 changes: 53 additions & 0 deletions tests/factories/recipeFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export class RecipeFactory extends Factory {
actionArgs = ConsoleLogArgumentFactory.build();
}

if (actionName === "multi-preference-experiment") {
actionArgs = MultiPreferenceFactory.build();
}

this.data.latest_revision = {
...this.data.latest_revision,
arguments: actionArgs,
Expand Down Expand Up @@ -93,3 +97,52 @@ class ConsoleLogArgumentFactory extends Factory {
return { message: new Field(faker.lorem.words) };
}
}

export class MultiPreferenceFactory extends Factory {
getFields() {
return {
branches: [],
experimentDocumentUrl: new Field(faker.internet.url),
slug: new Field(faker.lorem.slug),
userFacingDescription: new Field(faker.lorem.sentence),
userFacingName: new Field(faker.lorem.words),
};
}
}

export class MultiPrefBranchFactory extends Factory {
getFields() {
return {
slug: new Field(faker.lorem.slug),
ratio: new Field(faker.random.number),
preferences: [],
};
}

postGeneration() {
const { generatePreferenceCount } = this.options;
const preferences = {};
const prefBranchOptions = ["user", "default"];
const prefTypeOptions = ["integer", "string", "boolean"];
const prefValueOptions = {
integer: faker.random.number(),
string: faker.random.word(),
boolean: faker.random.boolean(),
};

for (let i = 0; i < generatePreferenceCount; i++) {
const preferenceName = faker.lorem.slug();
const preferenceBranchType = faker.random.arrayElement(prefBranchOptions);
const preferenceType = faker.random.arrayElement(prefTypeOptions);
const preferenceValue = prefValueOptions[preferenceType];
const preference = {
preferenceBranchType,
preferenceType,
preferenceValue,
};
preferences[preferenceName] = preference;
}

this.data.preferences = preferences;
}
}
36 changes: 36 additions & 0 deletions tests/jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
/* eslint-env node */
import crypto from "crypto";

import { render } from "@testing-library/react";
import { createMemoryHistory } from "history";
import PropTypes from "prop-types";
import React from "react";
import { Router, Route } from "react-router-dom";

import { EnvironmentProvider } from "devtools/contexts/environment";

Object.defineProperty(global.self, "crypto", {
value: {
getRandomValues: (arr) => crypto.randomBytes(arr.length),
Expand Down Expand Up @@ -35,3 +43,31 @@ global.document.body.createTextRange = () => ({
getBoundingClientRect: () => {},
getClientRects: () => [],
});

global.renderWithContext = (
ui,
{
route = "/",
path = "/",
history = createMemoryHistory({ initialEntries: [route] }),
} = {},
) => {
const Wrapper = ({ children }) => (
<Router history={history}>
<EnvironmentProvider>
<Route path={path}>{children}</Route>
</EnvironmentProvider>
</Router>
);

Wrapper.propTypes = {
children: PropTypes.object,
};
return {
...render(ui, { wrapper: Wrapper }),
// adding `history` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
};
};

0 comments on commit c0f1431

Please sign in to comment.