Browse Source

feat(owners): Ownership Settings UI (#7494)

Max Bittker 7 years ago
parent
commit
42bb61917d

+ 7 - 0
src/sentry/static/sentry/app/routes.jsx

@@ -298,6 +298,13 @@ const projectSettingsRoutes = (
         import(/* webpackChunkName: "ProjectReleaseTracking" */ './views/settings/project/projectReleaseTracking')}
       component={errorHandler(LazyLoad)}
     />
+    <Route
+      path="ownership/"
+      name="Issue Ownership"
+      componentPromise={() =>
+        import(/* webpackChunkName: "projectOwnership" */ './views/settings/project/projectOwnership')}
+      component={errorHandler(LazyLoad)}
+    />
     <Route
       path="data-forwarding/"
       name="Data Forwarding"

+ 5 - 0
src/sentry/static/sentry/app/views/settings/project/navigationConfiguration.jsx

@@ -43,6 +43,11 @@ export default function getConfiguration({project}) {
           title: t('Release Tracking'),
           show: ({access}) => access.has('project:write'),
         },
+        {
+          path: `${pathPrefix}/ownership/`,
+          title: t('Issue Ownership'),
+          show: ({access}) => access.has('project:write'),
+        },
         {
           path: `${pathPrefix}/data-forwarding/`,
           title: t('Data Forwarding'),

+ 103 - 0
src/sentry/static/sentry/app/views/settings/project/projectOwnership/index.jsx

@@ -0,0 +1,103 @@
+import React from 'react';
+import styled from 'react-emotion';
+
+import {t} from '../../../../locale';
+import AsyncView from '../../../asyncView';
+
+import Panel from '../../components/panel';
+import PanelBody from '../../components/panelBody';
+import PanelHeader from '../../components/panelHeader';
+import SentryTypes from '../../../../proptypes';
+import SettingsPageHeader from '../../components/settingsPageHeader';
+import Form from '../../components/forms/form';
+import JsonForm from '../../components/forms/jsonForm';
+import OwnerInput from './ownerInput';
+
+const CodeBlock = styled.pre`
+  word-break: break-all;
+  white-space: pre-wrap;
+`;
+
+class ProjectOwnership extends AsyncView {
+  static propTypes = {
+    organization: SentryTypes.Organization,
+    project: SentryTypes.Project,
+  };
+
+  getTitle() {
+    return t('Ownership');
+  }
+
+  getEndpoints() {
+    let {orgId, projectId} = this.props.params;
+    return [
+      ['project', `/projects/${orgId}/${projectId}/`],
+      ['ownership', `/projects/${orgId}/${projectId}/ownership/`],
+    ];
+  }
+
+  renderBody() {
+    let {project, organization} = this.props;
+    let {ownership} = this.state;
+
+    return (
+      <div>
+        <SettingsPageHeader title={t('Issue Ownership')} />
+
+        <div className="alert alert-block alert-info">
+          {t(`Psst! This feature is still a work-in-progress. Thanks for being an early
+          adopter!`)}
+        </div>
+
+        <Panel>
+          <PanelHeader>{t('Ownership Rules')}</PanelHeader>
+          <PanelBody disablePadding={false}>
+            <p>
+              {t(
+                "To configure automated issue ownership in Sentry, you'll need to define rules here. "
+              )}
+            </p>
+            <p>{t('Rules follow the pattern type:glob owner, owner')}</p>
+            <p>
+              {t(
+                'Owners can be team identifiers starting with #, or user emails (use @ to input from list)'
+              )}
+            </p>
+            Examples:
+            <CodeBlock>
+              path:src/sentry/pipeline/* person@sentry.io, #platform
+              {'\n'}
+              url:http://sentry.io/settings/* #workflow
+            </CodeBlock>
+            <OwnerInput {...this.props} initialText={ownership.raw || ''} />
+          </PanelBody>
+        </Panel>
+
+        <Form
+          apiEndpoint={`/projects/${organization.slug}/${project.slug}/ownership/`}
+          apiMethod="PUT"
+          saveOnBlur
+          initialData={{fallthrough: ownership.fallthrough}}
+          hideFooter
+        >
+          <JsonForm
+            forms={[
+              {
+                title: 'Default Ownership',
+                fields: [
+                  {
+                    name: 'fallthrough',
+                    type: 'boolean',
+                    label: 'Default Owner is everyone',
+                  },
+                ],
+              },
+            ]}
+          />
+        </Form>
+      </div>
+    );
+  }
+}
+
+export default ProjectOwnership;

+ 143 - 0
src/sentry/static/sentry/app/views/settings/project/projectOwnership/ownerInput.jsx

@@ -0,0 +1,143 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import styled from 'react-emotion';
+import {MentionsInput, Mention} from 'react-mentions';
+
+import {Client} from '../../../../api';
+import memberListStore from '../../../../stores/memberListStore';
+import TeamStore from '../../../../stores/teamStore';
+import Button from '../../../../components/buttons/button';
+import SentryTypes from '../../../../proptypes';
+
+import {addErrorMessage, addSuccessMessage} from '../../../../actionCreators/indicator';
+import {t} from '../../../../locale';
+import OwnerInputStyle from './ownerInputStyles';
+
+const SyntaxOverlay = styled.div`
+  margin: 5px;
+  padding: 0px;
+  width: calc(100% - 10px);
+  height: 1em;
+  background-color: red;
+  opacity: 0.1;
+  pointer-events: none;
+  position: absolute;
+  top: ${({line}) => line}em;
+`;
+
+class OwnerInput extends React.Component {
+  static propTypes = {
+    organization: SentryTypes.Organization,
+    project: SentryTypes.Project,
+    initialText: PropTypes.string,
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      text: props.initialText,
+      error: null,
+    };
+  }
+
+  handleUpdateOwnership = () => {
+    let {organization, project} = this.props;
+    this.setState({error: null});
+
+    const api = new Client();
+    let request = api.requestPromise(
+      `/projects/${organization.slug}/${project.slug}/ownership/`,
+      {
+        method: 'PUT',
+        data: {raw: this.state.text},
+      }
+    );
+
+    request
+      .then(() => {
+        addSuccessMessage(t('Updated ownership rules'));
+      })
+      .catch(error => {
+        this.setState({error: error.responseJSON});
+        addErrorMessage(t('Error updating ownership rules'));
+      });
+
+    return request;
+  };
+
+  mentionableUsers() {
+    return memberListStore.getAll().map(member => ({
+      id: member.id,
+      display: member.name,
+      email: member.email,
+    }));
+  }
+
+  mentionableTeams() {
+    let {project} = this.props;
+    return TeamStore.getAll()
+      .filter(({projects}) => !!projects.find(p => p.slug === project.slug))
+      .map(team => ({
+        id: team.id,
+        display: team.slug,
+        email: team.id,
+      }));
+  }
+  onChange(v) {
+    this.setState({text: v.target.value});
+  }
+  render() {
+    let {text, error} = this.state;
+    let mentionableUsers = this.mentionableUsers();
+    let mentionableTeams = this.mentionableTeams();
+
+    return (
+      <React.Fragment>
+        <div
+          style={{position: 'relative'}}
+          onKeyDown={e => {
+            if (e.metaKey && e.key == 'Enter') {
+              this.handleUpdateOwnership();
+            }
+          }}
+        >
+          <MentionsInput
+            style={OwnerInputStyle}
+            placeholder={'Project Ownership'}
+            onChange={this.onChange.bind(this)}
+            onBlur={this.onBlur}
+            onKeyDown={this.onKeyDown}
+            value={text}
+            required={true}
+            autoFocus={true}
+            displayTransform={(id, display, type) =>
+              `${type === 'member' ? '' : '#'}${display}`}
+            markup="**[sentry.strip:__type__]__display__**"
+          >
+            <Mention
+              type="member"
+              trigger="@"
+              data={mentionableUsers}
+              appendSpaceOnAdd={true}
+            />
+            <Mention
+              type="team"
+              trigger="#"
+              data={mentionableTeams}
+              appendSpaceOnAdd={true}
+            />
+          </MentionsInput>
+          {error && <SyntaxOverlay line={error.raw[0].match(/line (\d*),/)[1] - 1} />}
+          {error && error.raw.toString()}
+          <div style={{textAlign: 'end', paddingTop: '10px'}}>
+            <Button size="small" priority="primary" onClick={this.handleUpdateOwnership}>
+              {t('Save Changes')}
+            </Button>
+          </div>
+        </div>
+      </React.Fragment>
+    );
+  }
+}
+
+export default OwnerInput;

+ 71 - 0
src/sentry/static/sentry/app/views/settings/project/projectOwnership/ownerInputStyles.js

@@ -0,0 +1,71 @@
+let styles = {
+  control: {
+    backgroundColor: '#fff',
+    fontSize: 15,
+    fontWeight: 'normal',
+  },
+
+  input: {
+    margin: 0,
+    fontFamily: 'Rubik, sans-serif',
+    wordBreak: 'break-all',
+    whiteSpace: 'pre-wrap',
+  },
+
+  '&singleLine': {
+    control: {
+      display: 'inline-block',
+
+      width: 130,
+    },
+
+    highlighter: {
+      padding: 1,
+      border: '2px inset transparent',
+    },
+
+    input: {
+      padding: 1,
+      border: '2px inset',
+    },
+  },
+
+  '&multiLine': {
+    control: {
+      fontFamily: 'Lato, Avenir Next, Helvetica Neue, sans-serif',
+    },
+
+    highlighter: {
+      padding: 20,
+    },
+
+    input: {
+      padding: '5px 5px 0',
+      minHeight: 140,
+      overflow: 'auto',
+      outline: 0,
+      border: '1 solid',
+    },
+  },
+
+  suggestions: {
+    list: {
+      maxHeight: 150,
+      overflow: 'auto',
+      backgroundColor: 'white',
+      border: '1px solid rgba(0,0,0,0.15)',
+      fontSize: 12,
+    },
+
+    item: {
+      padding: '5px 15px',
+      borderBottom: '1px solid rgba(0,0,0,0.15)',
+
+      '&focused': {
+        backgroundColor: '#f8f6f9',
+      },
+    },
+  },
+};
+
+export default styles;

+ 366 - 0
tests/js/spec/views/__snapshots__/ownershipInput.spec.jsx.snap

@@ -0,0 +1,366 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProjectTeamsSettings render() renders 1`] = `
+<OwnerInput
+  initialText="url:src @dummy@example.com"
+  organization={
+    Object {
+      "access": Array [
+        "org:read",
+        "org:write",
+        "org:admin",
+        "project:read",
+        "project:write",
+        "project:admin",
+        "team:read",
+        "team:write",
+        "team:admin",
+      ],
+      "features": Array [],
+      "id": "3",
+      "name": "Organization Name",
+      "onboardingTasks": Array [],
+      "projects": Array [],
+      "slug": "org-slug",
+      "status": Object {
+        "id": "active",
+        "name": "active",
+      },
+      "teams": Array [],
+    }
+  }
+  params={
+    Object {
+      "orgId": "org-slug",
+      "projectId": "project-slug",
+    }
+  }
+  project={
+    Object {
+      "allowedDomains": Array [
+        "example.com",
+        "https://example.com",
+      ],
+      "dataScrubber": false,
+      "dataScrubberDefaults": false,
+      "digestsMaxDelay": 60,
+      "digestsMinDelay": 5,
+      "id": "2",
+      "name": "Project Name",
+      "resolveAge": 48,
+      "safeFields": Array [
+        "business-email",
+        "company",
+      ],
+      "scrapeJavaScript": true,
+      "scrubIPAddresses": false,
+      "securityToken": "security-token",
+      "securityTokenHeader": "x-security-header",
+      "sensitiveFields": Array [
+        "creditcard",
+        "ssn",
+      ],
+      "slug": "project-slug",
+      "subjectTemplate": "[$project] \${tag:level}: $title",
+      "verifySSL": true,
+    }
+  }
+>
+  <div
+    onKeyDown={[Function]}
+    style={
+      Object {
+        "position": "relative",
+      }
+    }
+  >
+    <withDefaultStyle(MentionsInput)
+      autoFocus={true}
+      displayTransform={[Function]}
+      markup="**[sentry.strip:__type__]__display__**"
+      onChange={[Function]}
+      placeholder="Project Ownership"
+      required={true}
+      style={
+        Object {
+          "&multiLine": Object {
+            "control": Object {
+              "fontFamily": "Lato, Avenir Next, Helvetica Neue, sans-serif",
+            },
+            "highlighter": Object {
+              "padding": 20,
+            },
+            "input": Object {
+              "border": "1 solid",
+              "minHeight": 140,
+              "outline": 0,
+              "overflow": "auto",
+              "padding": "5px 5px 0",
+            },
+          },
+          "&singleLine": Object {
+            "control": Object {
+              "display": "inline-block",
+              "width": 130,
+            },
+            "highlighter": Object {
+              "border": "2px inset transparent",
+              "padding": 1,
+            },
+            "input": Object {
+              "border": "2px inset",
+              "padding": 1,
+            },
+          },
+          "control": Object {
+            "backgroundColor": "#fff",
+            "fontSize": 15,
+            "fontWeight": "normal",
+          },
+          "input": Object {
+            "fontFamily": "Rubik, sans-serif",
+            "margin": 0,
+            "whiteSpace": "pre-wrap",
+            "wordBreak": "break-all",
+          },
+          "suggestions": Object {
+            "item": Object {
+              "&focused": Object {
+                "backgroundColor": "#f8f6f9",
+              },
+              "borderBottom": "1px solid rgba(0,0,0,0.15)",
+              "padding": "5px 15px",
+            },
+            "list": Object {
+              "backgroundColor": "white",
+              "border": "1px solid rgba(0,0,0,0.15)",
+              "fontSize": 12,
+              "maxHeight": 150,
+              "overflow": "auto",
+            },
+          },
+        }
+      }
+      value="url:src @dummy@example.com"
+    >
+      <MentionsInput
+        autoFocus={true}
+        displayTransform={[Function]}
+        markup="**[sentry.strip:__type__]__display__**"
+        onBlur={[Function]}
+        onChange={[Function]}
+        onKeyDown={[Function]}
+        onSelect={[Function]}
+        placeholder="Project Ownership"
+        required={true}
+        singleLine={false}
+        style={[Function]}
+        value="url:src @dummy@example.com"
+      >
+        <div
+          style={
+            Object {
+              "overflowY": "visible",
+              "position": "relative",
+            }
+          }
+        >
+          <div
+            style={
+              Object {
+                "backgroundColor": "#fff",
+                "fontFamily": "Lato, Avenir Next, Helvetica Neue, sans-serif",
+                "fontSize": 15,
+                "fontWeight": "normal",
+              }
+            }
+          >
+            <withDefaultStyle(Highlighter)
+              displayTransform={[Function]}
+              inputStyle={
+                Object {
+                  "backgroundColor": "transparent",
+                  "border": "1 solid",
+                  "bottom": 0,
+                  "boxSizing": "border-box",
+                  "display": "block",
+                  "fontFamily": "Rubik, sans-serif",
+                  "height": "100%",
+                  "margin": 0,
+                  "minHeight": 140,
+                  "outline": 0,
+                  "overflow": "auto",
+                  "padding": "5px 5px 0",
+                  "position": "absolute",
+                  "resize": "none",
+                  "top": 0,
+                  "whiteSpace": "pre-wrap",
+                  "width": "100%",
+                  "wordBreak": "break-all",
+                }
+              }
+              markup="**[sentry.strip:__type__]__display__**"
+              onCaretPositionChange={[Function]}
+              selection={
+                Object {
+                  "end": null,
+                  "start": null,
+                }
+              }
+              singleLine={false}
+              style={[Function]}
+              value="url:src @dummy@example.com"
+            >
+              <Highlighter
+                displayTransform={[Function]}
+                inputStyle={
+                  Object {
+                    "backgroundColor": "transparent",
+                    "border": "1 solid",
+                    "bottom": 0,
+                    "boxSizing": "border-box",
+                    "display": "block",
+                    "fontFamily": "Rubik, sans-serif",
+                    "height": "100%",
+                    "margin": 0,
+                    "minHeight": 140,
+                    "outline": 0,
+                    "overflow": "auto",
+                    "padding": "5px 5px 0",
+                    "position": "absolute",
+                    "resize": "none",
+                    "top": 0,
+                    "whiteSpace": "pre-wrap",
+                    "width": "100%",
+                    "wordBreak": "break-all",
+                  }
+                }
+                markup="**[sentry.strip:__type__]__display__**"
+                onCaretPositionChange={[Function]}
+                selection={
+                  Object {
+                    "end": null,
+                    "start": null,
+                  }
+                }
+                singleLine={false}
+                style={[Function]}
+                value="url:src @dummy@example.com"
+              >
+                <div
+                  style={
+                    Object {
+                      "backgroundColor": "transparent",
+                      "border": "1 solid",
+                      "bottom": 0,
+                      "boxSizing": "border-box",
+                      "color": "transparent",
+                      "display": "block",
+                      "fontFamily": "Rubik, sans-serif",
+                      "height": "100%",
+                      "margin": 0,
+                      "minHeight": 140,
+                      "outline": 0,
+                      "overflow": "hidden",
+                      "padding": 20,
+                      "position": "relative",
+                      "resize": "none",
+                      "top": 0,
+                      "whiteSpace": "pre-wrap",
+                      "width": "inherit",
+                      "wordBreak": "break-all",
+                      "wordWrap": "break-word",
+                    }
+                  }
+                >
+                  <span
+                    key="0"
+                    style={
+                      Object {
+                        "visibility": "hidden",
+                      }
+                    }
+                  >
+                    url:src @dummy@example.com
+                  </span>
+                   
+                </div>
+              </Highlighter>
+            </withDefaultStyle(Highlighter)>
+            <textarea
+              autoFocus={true}
+              onBlur={[Function]}
+              onChange={[Function]}
+              onCompositionEnd={[Function]}
+              onCompositionStart={[Function]}
+              onKeyDown={[Function]}
+              onSelect={[Function]}
+              placeholder="Project Ownership"
+              required={true}
+              style={
+                Object {
+                  "backgroundColor": "transparent",
+                  "border": "1 solid",
+                  "bottom": 0,
+                  "boxSizing": "border-box",
+                  "display": "block",
+                  "fontFamily": "Rubik, sans-serif",
+                  "height": "100%",
+                  "margin": 0,
+                  "minHeight": 140,
+                  "outline": 0,
+                  "overflow": "auto",
+                  "padding": "5px 5px 0",
+                  "position": "absolute",
+                  "resize": "none",
+                  "top": 0,
+                  "whiteSpace": "pre-wrap",
+                  "width": "100%",
+                  "wordBreak": "break-all",
+                }
+              }
+              value="url:src @dummy@example.com"
+            />
+          </div>
+        </div>
+      </MentionsInput>
+    </withDefaultStyle(MentionsInput)>
+    <div
+      style={
+        Object {
+          "paddingTop": "10px",
+          "textAlign": "end",
+        }
+      }
+    >
+      <Button
+        disabled={false}
+        onClick={[Function]}
+        priority="primary"
+        size="small"
+      >
+        <button
+          className="button button-primary button-sm"
+          disabled={false}
+          onClick={[Function]}
+          role="button"
+        >
+          <FlowLayout
+            truncate={false}
+          >
+            <div
+              className="flow-layout"
+            >
+              <span
+                className="button-label"
+              >
+                Save Changes
+              </span>
+            </div>
+          </FlowLayout>
+        </button>
+      </Button>
+    </div>
+  </div>
+</OwnerInput>
+`;

+ 149 - 0
tests/js/spec/views/__snapshots__/projectOwnership.spec.jsx.snap

@@ -0,0 +1,149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProjectTeamsSettings render() renders 1`] = `
+<SideEffect(DocumentTitle)
+  title="Ownership - Sentry"
+>
+  <div>
+    <SettingsPageHeading
+      title="Issue Ownership"
+    />
+    <div
+      className="alert alert-block alert-info"
+    >
+      Psst! This feature is still a work-in-progress. Thanks for being an early
+          adopter!
+    </div>
+    <Panel>
+      <PanelHeader>
+        Ownership Rules
+      </PanelHeader>
+      <PanelBody
+        direction="column"
+        disablePadding={false}
+        flex={false}
+      >
+        <p>
+          To configure automated issue ownership in Sentry, you'll need to define rules here. 
+        </p>
+        <p>
+          Rules follow the pattern type:glob owner, owner
+        </p>
+        <p>
+          Owners can be team identifiers starting with #, or user emails (use @ to input from list)
+        </p>
+        Examples:
+        <CodeBlock>
+          path:src/sentry/pipeline/* person@sentry.io, #platform
+          
+
+          url:http://sentry.io/settings/* #workflow
+        </CodeBlock>
+        <OwnerInput
+          initialText="url:src @dummy@example.com"
+          organization={
+            Object {
+              "access": Array [
+                "org:read",
+                "org:write",
+                "org:admin",
+                "project:read",
+                "project:write",
+                "project:admin",
+                "team:read",
+                "team:write",
+                "team:admin",
+              ],
+              "features": Array [],
+              "id": "3",
+              "name": "Organization Name",
+              "onboardingTasks": Array [],
+              "projects": Array [],
+              "slug": "org-slug",
+              "status": Object {
+                "id": "active",
+                "name": "active",
+              },
+              "teams": Array [],
+            }
+          }
+          params={
+            Object {
+              "orgId": "org-slug",
+              "projectId": "project-slug",
+            }
+          }
+          project={
+            Object {
+              "allowedDomains": Array [
+                "example.com",
+                "https://example.com",
+              ],
+              "dataScrubber": false,
+              "dataScrubberDefaults": false,
+              "digestsMaxDelay": 60,
+              "digestsMinDelay": 5,
+              "id": "2",
+              "name": "Project Name",
+              "resolveAge": 48,
+              "safeFields": Array [
+                "business-email",
+                "company",
+              ],
+              "scrapeJavaScript": true,
+              "scrubIPAddresses": false,
+              "securityToken": "security-token",
+              "securityTokenHeader": "x-security-header",
+              "sensitiveFields": Array [
+                "creditcard",
+                "ssn",
+              ],
+              "slug": "project-slug",
+              "subjectTemplate": "[$project] \${tag:level}: $title",
+              "verifySSL": true,
+            }
+          }
+        />
+      </PanelBody>
+    </Panel>
+    <Form
+      allowUndo={false}
+      apiEndpoint="/projects/org-slug/project-slug/ownership/"
+      apiMethod="PUT"
+      cancelLabel="Cancel"
+      className="form-stacked"
+      footerClass="form-actions align-right"
+      hideFooter={true}
+      initialData={
+        Object {
+          "fallthrough": "false",
+        }
+      }
+      onSubmitError={[Function]}
+      onSubmitSuccess={[Function]}
+      requireChanges={false}
+      saveOnBlur={true}
+      submitDisabled={false}
+      submitLabel="Save Changes"
+    >
+      <JsonForm
+        additionalFieldProps={Object {}}
+        forms={
+          Array [
+            Object {
+              "fields": Array [
+                Object {
+                  "label": "Default Owner is everyone",
+                  "name": "fallthrough",
+                  "type": "boolean",
+                },
+              ],
+              "title": "Default Ownership",
+            },
+          ]
+        }
+      />
+    </Form>
+  </div>
+</SideEffect(DocumentTitle)>
+`;

+ 46 - 0
tests/js/spec/views/ownershipInput.spec.jsx

@@ -0,0 +1,46 @@
+import React from 'react';
+import {mount} from 'enzyme';
+
+import {Client} from 'app/api';
+import OwnerInput from 'app/views/settings/project/projectOwnership/ownerInput';
+
+describe('ProjectTeamsSettings', function() {
+  let org;
+  let project;
+  let put;
+
+  beforeEach(function() {
+    org = TestStubs.Organization();
+    project = TestStubs.Project();
+
+    put = Client.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/ownership/`,
+      method: 'PUT',
+      body: {raw: 'url:src @dummy@example.com'},
+    });
+  });
+
+  describe('render()', function() {
+    it('renders', function() {
+      let wrapper = mount(
+        <OwnerInput
+          params={{orgId: org.slug, projectId: project.slug}}
+          organization={org}
+          initialText="url:src @dummy@example.com"
+          project={project}
+        />,
+        TestStubs.routerContext()
+      );
+
+      let submit = wrapper.find('button');
+
+      expect(put).not.toHaveBeenCalled();
+
+      submit.simulate('click');
+
+      expect(put).toHaveBeenCalled();
+
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+});

+ 40 - 0
tests/js/spec/views/projectOwnership.spec.jsx

@@ -0,0 +1,40 @@
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import {Client} from 'app/api';
+import ProjectOwnership from 'app/views/settings/project/projectOwnership';
+
+describe('ProjectTeamsSettings', function() {
+  let org;
+  let project;
+
+  beforeEach(function() {
+    org = TestStubs.Organization();
+    project = TestStubs.Project();
+
+    Client.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/`,
+      method: 'GET',
+      body: project,
+    });
+    Client.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/ownership/`,
+      method: 'GET',
+      body: {raw: 'url:src @dummy@example.com', fallthrough: 'false'},
+    });
+  });
+
+  describe('render()', function() {
+    it('renders', function() {
+      let wrapper = shallow(
+        <ProjectOwnership
+          params={{orgId: org.slug, projectId: project.slug}}
+          organization={org}
+          project={project}
+        />,
+        TestStubs.routerContext()
+      );
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+});