Просмотр исходного кода

flattened team state, tests for TeamMembers

Peter Holmberg 7 лет назад
Родитель
Сommit
d494ebc730

+ 79 - 0
public/app/features/teams/TeamMembers.test.tsx

@@ -0,0 +1,79 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { TeamMembers, Props } from './TeamMembers';
+import { TeamMember } from '../../types';
+import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    members: [] as TeamMember[],
+    searchMemberQuery: '',
+    setSearchMemberQuery: jest.fn(),
+    loadTeamMembers: jest.fn(),
+    addTeamMember: jest.fn(),
+    removeTeamMember: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<TeamMembers {...props} />);
+  const instance = wrapper.instance() as TeamMembers;
+
+  return {
+    wrapper,
+    instance,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render team members', () => {
+    const { wrapper } = setup({
+      members: getMockTeamMembers(5),
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('Functions', () => {
+  describe('on search member query change', () => {
+    it('it should call setSearchMemberQuery', () => {
+      const { instance } = setup();
+      const mockEvent = { target: { value: 'member' } };
+
+      instance.onSearchQueryChange(mockEvent);
+
+      expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member');
+    });
+  });
+
+  describe('on remove member', () => {
+    const { instance } = setup();
+    const mockTeamMember = getMockTeamMember();
+
+    instance.onRemoveMember(mockTeamMember);
+
+    expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
+  });
+
+  describe('on add user to team', () => {
+    const { wrapper, instance } = setup();
+
+    wrapper.state().newTeamMember = {
+      id: 1,
+      label: '',
+      avatarUrl: '',
+      login: '',
+    };
+
+    instance.onAddUserToTeam();
+
+    expect(instance.props.addTeamMember).toHaveBeenCalledWith(1);
+  });
+});

+ 10 - 14
public/app/features/teams/TeamMembers.tsx

@@ -1,16 +1,14 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
-import { hot } from 'react-hot-loader';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
 import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
-import { Team, TeamMember } from '../../types';
+import { TeamMember } from '../../types';
 import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
-import { getSearchMemberQuery, getTeam } from './state/selectors';
-import { getRouteParamsId } from '../../core/selectors/location';
+import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
 
-interface Props {
-  team: Team;
+export interface Props {
+  members: TeamMember[];
   searchMemberQuery: string;
   loadTeamMembers: typeof loadTeamMembers;
   addTeamMember: typeof addTeamMember;
@@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent<Props, State> {
     this.props.setSearchMemberQuery(event.target.value);
   };
 
-  removeMember(member: TeamMember) {
+  onRemoveMember(member: TeamMember) {
     this.props.removeTeamMember(member.userId);
   }
 
@@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent<Props, State> {
         <td>{member.login}</td>
         <td>{member.email}</td>
         <td className="text-right">
-          <DeleteButton onConfirmDelete={() => this.removeMember(member)} />
+          <DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
         </td>
       </tr>
     );
@@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent<Props, State> {
 
   render() {
     const { newTeamMember, isAdding } = this.state;
-    const { team, searchMemberQuery } = this.props;
+    const { searchMemberQuery, members } = this.props;
     const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
 
     return (
@@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent<Props, State> {
                 <th style={{ width: '1%' }} />
               </tr>
             </thead>
-            <tbody>{team.members && team.members.map(member => this.renderMember(member))}</tbody>
+            <tbody>{members && members.map(member => this.renderMember(member))}</tbody>
           </table>
         </div>
       </div>
@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent<Props, State> {
 }
 
 function mapStateToProps(state) {
-  const teamId = getRouteParamsId(state.location);
-
   return {
-    team: getTeam(state.team, teamId),
+    members: getTeamMembers(state.team),
     searchMemberQuery: getSearchMemberQuery(state.team),
   };
 }
@@ -149,4 +145,4 @@ const mapDispatchToProps = {
   setSearchMemberQuery,
 };
 
-export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers));
+export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers);

+ 2 - 2
public/app/features/teams/TeamPages.tsx

@@ -41,10 +41,10 @@ export class TeamPages extends PureComponent<Props, State> {
   }
 
   componentDidMount() {
-    this.loadTeam();
+    this.fetchTeam();
   }
 
-  async loadTeam() {
+  async fetchTeam() {
     const { loadTeam, teamId } = this.props;
 
     await loadTeam(teamId);

+ 18 - 8
public/app/features/teams/__mocks__/teamMocks.ts

@@ -1,4 +1,4 @@
-import { Team } from '../../../types';
+import { Team, TeamMember } from '../../../types';
 
 export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
   let teams: Team[] = [];
@@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
       avatarUrl: 'some/url/',
       email: `test-${i}@test.com`,
       memberCount: i,
-      search: '',
-      members: [],
-      groups: [],
     });
   }
 
@@ -25,13 +22,26 @@ export const getMockTeam = (): Team => {
     avatarUrl: 'some/url/',
     email: 'test@test.com',
     memberCount: 1,
-    search: '',
-    members: [],
-    groups: [],
   };
 };
 
-export const getMockTeamMember = () => {
+export const getMockTeamMembers = (amount: number): TeamMember[] => {
+  let teamMembers: TeamMember[] = [];
+
+  for (let i = 1; i <= amount; i++) {
+    teamMembers.push({
+      userId: i,
+      teamId: 1,
+      avatarUrl: 'some/url/',
+      email: 'test@test.com',
+      login: `testUser-${i}`,
+    });
+  }
+
+  return teamMembers;
+};
+
+export const getMockTeamMember = (): TeamMember => {
   return {
     userId: 1,
     teamId: 1,

+ 317 - 0
public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap

@@ -0,0 +1,317 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <div
+    className="page-action-bar"
+  >
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <label
+        className="gf-form--has-input-icon gf-form--grow"
+      >
+        <input
+          className="gf-form-input"
+          onChange={[Function]}
+          placeholder="Search members"
+          type="text"
+          value=""
+        />
+        <i
+          className="gf-form-input-icon fa fa-search"
+        />
+      </label>
+    </div>
+    <div
+      className="page-action-bar__spacer"
+    />
+    <button
+      className="btn btn-success pull-right"
+      disabled={false}
+      onClick={[Function]}
+    >
+      <i
+        className="fa fa-plus"
+      />
+       Add a member
+    </button>
+  </div>
+  <Component
+    in={false}
+  >
+    <div
+      className="cta-form"
+    >
+      <button
+        className="cta-form__close btn btn-transparent"
+        onClick={[Function]}
+      >
+        <i
+          className="fa fa-close"
+        />
+      </button>
+      <h5>
+        Add Team Member
+      </h5>
+      <div
+        className="gf-form-inline"
+      >
+        <UserPicker
+          className="width-30"
+          onSelected={[Function]}
+          value={null}
+        />
+      </div>
+    </div>
+  </Component>
+  <div
+    className="admin-list-table"
+  >
+    <table
+      className="filter-table filter-table--hover form-inline"
+    >
+      <thead>
+        <tr>
+          <th />
+          <th>
+            Name
+          </th>
+          <th>
+            Email
+          </th>
+          <th
+            style={
+              Object {
+                "width": "1%",
+              }
+            }
+          />
+        </tr>
+      </thead>
+      <tbody />
+    </table>
+  </div>
+</div>
+`;
+
+exports[`Render should render team members 1`] = `
+<div>
+  <div
+    className="page-action-bar"
+  >
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <label
+        className="gf-form--has-input-icon gf-form--grow"
+      >
+        <input
+          className="gf-form-input"
+          onChange={[Function]}
+          placeholder="Search members"
+          type="text"
+          value=""
+        />
+        <i
+          className="gf-form-input-icon fa fa-search"
+        />
+      </label>
+    </div>
+    <div
+      className="page-action-bar__spacer"
+    />
+    <button
+      className="btn btn-success pull-right"
+      disabled={false}
+      onClick={[Function]}
+    >
+      <i
+        className="fa fa-plus"
+      />
+       Add a member
+    </button>
+  </div>
+  <Component
+    in={false}
+  >
+    <div
+      className="cta-form"
+    >
+      <button
+        className="cta-form__close btn btn-transparent"
+        onClick={[Function]}
+      >
+        <i
+          className="fa fa-close"
+        />
+      </button>
+      <h5>
+        Add Team Member
+      </h5>
+      <div
+        className="gf-form-inline"
+      >
+        <UserPicker
+          className="width-30"
+          onSelected={[Function]}
+          value={null}
+        />
+      </div>
+    </div>
+  </Component>
+  <div
+    className="admin-list-table"
+  >
+    <table
+      className="filter-table filter-table--hover form-inline"
+    >
+      <thead>
+        <tr>
+          <th />
+          <th>
+            Name
+          </th>
+          <th>
+            Email
+          </th>
+          <th
+            style={
+              Object {
+                "width": "1%",
+              }
+            }
+          />
+        </tr>
+      </thead>
+      <tbody>
+        <tr
+          key="1"
+        >
+          <td
+            className="width-4 text-center"
+          >
+            <img
+              className="filter-table__avatar"
+              src="some/url/"
+            />
+          </td>
+          <td>
+            testUser-1
+          </td>
+          <td>
+            test@test.com
+          </td>
+          <td
+            className="text-right"
+          >
+            <DeleteButton
+              onConfirmDelete={[Function]}
+            />
+          </td>
+        </tr>
+        <tr
+          key="2"
+        >
+          <td
+            className="width-4 text-center"
+          >
+            <img
+              className="filter-table__avatar"
+              src="some/url/"
+            />
+          </td>
+          <td>
+            testUser-2
+          </td>
+          <td>
+            test@test.com
+          </td>
+          <td
+            className="text-right"
+          >
+            <DeleteButton
+              onConfirmDelete={[Function]}
+            />
+          </td>
+        </tr>
+        <tr
+          key="3"
+        >
+          <td
+            className="width-4 text-center"
+          >
+            <img
+              className="filter-table__avatar"
+              src="some/url/"
+            />
+          </td>
+          <td>
+            testUser-3
+          </td>
+          <td>
+            test@test.com
+          </td>
+          <td
+            className="text-right"
+          >
+            <DeleteButton
+              onConfirmDelete={[Function]}
+            />
+          </td>
+        </tr>
+        <tr
+          key="4"
+        >
+          <td
+            className="width-4 text-center"
+          >
+            <img
+              className="filter-table__avatar"
+              src="some/url/"
+            />
+          </td>
+          <td>
+            testUser-4
+          </td>
+          <td>
+            test@test.com
+          </td>
+          <td
+            className="text-right"
+          >
+            <DeleteButton
+              onConfirmDelete={[Function]}
+            />
+          </td>
+        </tr>
+        <tr
+          key="5"
+        >
+          <td
+            className="width-4 text-center"
+          >
+            <img
+              className="filter-table__avatar"
+              src="some/url/"
+            />
+          </td>
+          <td>
+            testUser-5
+          </td>
+          <td>
+            test@test.com
+          </td>
+          <td
+            className="text-right"
+          >
+            <DeleteButton
+              onConfirmDelete={[Function]}
+            />
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+`;

+ 1 - 20
public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap

@@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = `
         Object {
           "avatarUrl": "some/url/",
           "email": "test@test.com",
-          "groups": Array [],
           "id": 1,
           "memberCount": 1,
-          "members": Array [],
           "name": "test",
-          "search": "",
         }
       }
     />
@@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
   <div
     className="page-container page-body"
   >
-    <TeamMembers
-      team={
-        Object {
-          "avatarUrl": "some/url/",
-          "email": "test@test.com",
-          "groups": Array [],
-          "id": 1,
-          "memberCount": 1,
-          "members": Array [],
-          "name": "test",
-          "search": "",
-        }
-      }
-    />
+    <Connect(TeamMembers) />
   </div>
 </div>
 `;
@@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = `
         Object {
           "avatarUrl": "some/url/",
           "email": "test@test.com",
-          "groups": Array [],
           "id": 1,
           "memberCount": 1,
-          "members": Array [],
           "name": "test",
-          "search": "",
         }
       }
     />

+ 1 - 0
public/app/features/teams/state/actions.ts

@@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult<void> {
 }
 
 export function loadTeamMembers(): ThunkResult<void> {
+  console.log('loading team members');
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
 

+ 28 - 8
public/app/features/teams/state/reducers.test.ts

@@ -31,22 +31,42 @@ describe('teams reducer', () => {
 });
 
 describe('team reducer', () => {
+  it('should set team', () => {
+    const payload = getMockTeam();
+
+    const action: Action = {
+      type: ActionTypes.LoadTeam,
+      payload,
+    };
+
+    const result = teamReducer(initialTeamState, action);
+
+    expect(result.team).toEqual(payload);
+  });
+
   it('should set team members', () => {
     const mockTeamMember = getMockTeamMember();
-    const mockTeam = getMockTeam();
-    const state = {
-      ...initialTeamState,
-      team: mockTeam,
-    };
 
     const action: Action = {
       type: ActionTypes.LoadTeamMembers,
       payload: [mockTeamMember],
     };
 
-    const result = teamReducer(state, action);
-    const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' };
+    const result = teamReducer(initialTeamState, action);
+
+    expect(result.members).toEqual([mockTeamMember]);
+  });
+
+  it('should set member search query', () => {
+    const payload = 'member';
+
+    const action: Action = {
+      type: ActionTypes.SetSearchMemberQuery,
+      payload,
+    };
+
+    const result = teamReducer(initialTeamState, action);
 
-    expect(result).toEqual(expectedState);
+    expect(result.searchMemberQuery).toEqual('member');
   });
 });

+ 8 - 3
public/app/features/teams/state/reducers.ts

@@ -1,8 +1,13 @@
-import { Team, TeamsState, TeamState } from '../../../types';
+import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types';
 import { Action, ActionTypes } from './actions';
 
 export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };
-export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' };
+export const initialTeamState: TeamState = {
+  team: {} as Team,
+  members: [] as TeamMember[],
+  groups: [] as TeamGroup[],
+  searchMemberQuery: '',
+};
 
 export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
   switch (action.type) {
@@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState
       return { ...state, team: action.payload };
 
     case ActionTypes.LoadTeamMembers:
-      return { ...state, team: { ...state.team, members: action.payload } };
+      return { ...state, members: action.payload };
 
     case ActionTypes.SetSearchMemberQuery:
       return { ...state, searchMemberQuery: action.payload };

+ 18 - 4
public/app/features/teams/state/selectors.test.ts

@@ -1,8 +1,8 @@
-import { getTeams } from './selectors';
-import { getMultipleMockTeams } from '../__mocks__/teamMocks';
-import { TeamsState } from '../../../types';
+import { getTeam, getTeams } from './selectors';
+import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks';
+import { TeamsState, TeamState } from '../../../types';
 
-describe('Team selectors', () => {
+describe('Teams selectors', () => {
   describe('Get teams', () => {
     const mockTeams = getMultipleMockTeams(5);
 
@@ -23,3 +23,17 @@ describe('Team selectors', () => {
     });
   });
 });
+
+describe('Team selectors', () => {
+  describe('Get team', () => {
+    const mockTeam = getMockTeam();
+
+    it('should return team if matching with location team', () => {
+      const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' };
+
+      const team = getTeam(mockState, '1');
+
+      expect(team).toEqual(mockTeam);
+    });
+  });
+});

+ 9 - 2
public/app/features/teams/state/selectors.ts

@@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery;
 export const getSearchMemberQuery = state => state.searchMemberQuery;
 
 export const getTeam = (state, currentTeamId) => {
-  if (state.team.id === currentTeamId) {
-    console.log('yes');
+  if (state.team.id === parseInt(currentTeamId)) {
     return state.team;
   }
 };
@@ -15,3 +14,11 @@ export const getTeams = state => {
     return regex.test(team.name);
   });
 };
+
+export const getTeamMembers = state => {
+  const regex = RegExp(state.searchMemberQuery, 'i');
+
+  return state.members.filter(member => {
+    return regex.test(member.login) || regex.test(member.email);
+  });
+};

+ 2 - 3
public/app/types/index.ts

@@ -63,9 +63,6 @@ export interface Team {
   avatarUrl: string;
   email: string;
   memberCount: number;
-  search?: string;
-  members?: TeamMember[];
-  groups?: TeamGroup[];
 }
 
 export interface TeamMember {
@@ -124,6 +121,8 @@ export interface TeamsState {
 
 export interface TeamState {
   team: Team;
+  members: TeamMember[];
+  groups: TeamGroup[];
   searchMemberQuery: string;
 }