approve/reject follow requests, unblock, unmute (#230)
* approve/reject follow requests, unblock, unmute * make tests less flaky
This commit is contained in:
		
							parent
							
								
									e342eadbd0
								
							
						
					
					
						commit
						ffb00fcc5c
					
				
					 17 changed files with 250 additions and 18 deletions
				
			
		| 
						 | 
				
			
			@ -31,8 +31,7 @@ Lint:
 | 
			
		|||
 | 
			
		||||
Automatically fix most linting issues:
 | 
			
		||||
 | 
			
		||||
    npx standard --fix
 | 
			
		||||
    npx standard --fix --plugin html 'routes/**/*.html'
 | 
			
		||||
    npm run lint-fix
 | 
			
		||||
 | 
			
		||||
## Testing
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
  "version": "0.2.3",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "lint": "standard && standard --plugin html 'routes/**/*.html'",
 | 
			
		||||
    "lint-fix": "standard --fix && standard --fix --plugin html 'routes/**/*.html'",
 | 
			
		||||
    "dev": "run-s build-svg build-inline-script serve-dev",
 | 
			
		||||
    "serve-dev": "run-p --race build-sass-watch serve",
 | 
			
		||||
    "serve": "node server.js",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import { store } from '../_store/store'
 | 
			
		|||
import { blockAccount, unblockAccount } from '../_api/block'
 | 
			
		||||
import { toast } from '../_utils/toast'
 | 
			
		||||
import { updateProfileAndRelationship } from './accounts'
 | 
			
		||||
import { emit } from '../_utils/eventBus'
 | 
			
		||||
 | 
			
		||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
 | 
			
		||||
  let { currentInstance, accessToken } = store.get()
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +20,7 @@ export async function setAccountBlocked (accountId, block, toastOnSuccess) {
 | 
			
		|||
        toast.say('Unblocked account')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    emit('refreshAccountsList')
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e)
 | 
			
		||||
    toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || ''))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import { store } from '../_store/store'
 | 
			
		|||
import { muteAccount, unmuteAccount } from '../_api/mute'
 | 
			
		||||
import { toast } from '../_utils/toast'
 | 
			
		||||
import { updateProfileAndRelationship } from './accounts'
 | 
			
		||||
import { emit } from '../_utils/eventBus'
 | 
			
		||||
 | 
			
		||||
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
 | 
			
		||||
  let { currentInstance, accessToken } = store.get()
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +20,7 @@ export async function setAccountMuted (accountId, mute, toastOnSuccess) {
 | 
			
		|||
        toast.say('Unmuted account')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    emit('refreshAccountsList')
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e)
 | 
			
		||||
    toast.say(`Unable to ${mute ? 'mute' : 'unmute'} account: ` + (e.message || ''))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								routes/_actions/requests.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								routes/_actions/requests.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
import { store } from '../_store/store'
 | 
			
		||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
 | 
			
		||||
import { emit } from '../_utils/eventBus'
 | 
			
		||||
import { toast } from '../_utils/toast'
 | 
			
		||||
 | 
			
		||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
 | 
			
		||||
  let {
 | 
			
		||||
    currentInstance,
 | 
			
		||||
    accessToken
 | 
			
		||||
  } = store.get()
 | 
			
		||||
  try {
 | 
			
		||||
    if (approved) {
 | 
			
		||||
      await approveFollowRequest(currentInstance, accessToken, accountId)
 | 
			
		||||
    } else {
 | 
			
		||||
      await rejectFollowRequest(currentInstance, accessToken, accountId)
 | 
			
		||||
    }
 | 
			
		||||
    if (toastOnSuccess) {
 | 
			
		||||
      if (approved) {
 | 
			
		||||
        toast.say('Approved follow request')
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.say('Rejected follow request')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    emit('refreshAccountsList')
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e)
 | 
			
		||||
    toast.say(`Unable to ${approved ? 'approve' : 'reject'} account: ` + (e.message || ''))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								routes/_api/requests.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								routes/_api/requests.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
import { postWithTimeout } from '../_utils/ajax'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function approveFollowRequest (instanceName, accessToken, accountId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/authorize`
 | 
			
		||||
  return postWithTimeout(url, null, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function rejectFollowRequest (instanceName, accessToken, accountId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/reject`
 | 
			
		||||
  return postWithTimeout(url, null, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,11 @@
 | 
			
		|||
  {{elseif accounts && accounts.length}}
 | 
			
		||||
    <ul class="accounts-results">
 | 
			
		||||
      {{#each accounts as account}}
 | 
			
		||||
        <AccountSearchResult :account />
 | 
			
		||||
        <AccountSearchResult
 | 
			
		||||
          :account
 | 
			
		||||
          actions={{accountActions}}
 | 
			
		||||
          on:click="onClickAction(event)"
 | 
			
		||||
        />
 | 
			
		||||
      {{/each}}
 | 
			
		||||
    </ul>
 | 
			
		||||
  {{/if}}
 | 
			
		||||
| 
						 | 
				
			
			@ -31,19 +35,19 @@
 | 
			
		|||
  import LoadingPage from '../_components/LoadingPage.html'
 | 
			
		||||
  import AccountSearchResult from '../_components/search/AccountSearchResult.html'
 | 
			
		||||
  import { toast } from '../_utils/toast'
 | 
			
		||||
  import { on } from '../_utils/eventBus'
 | 
			
		||||
 | 
			
		||||
  // TODO: paginate
 | 
			
		||||
  export default {
 | 
			
		||||
    async oncreate () {
 | 
			
		||||
      let { accountsFetcher } = this.get()
 | 
			
		||||
      try {
 | 
			
		||||
        // TODO: paginate
 | 
			
		||||
        let accounts = await accountsFetcher()
 | 
			
		||||
        this.set({ accounts: accounts })
 | 
			
		||||
        await this.refreshAccounts()
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        toast.say('Error: ' + (e.name || '') + ' ' + (e.message || ''))
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.set({loading: false})
 | 
			
		||||
      }
 | 
			
		||||
      on('refreshAccountsList', this, () => this.refreshAccounts())
 | 
			
		||||
    },
 | 
			
		||||
    data: () => ({
 | 
			
		||||
      loading: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +57,17 @@
 | 
			
		|||
    components: {
 | 
			
		||||
      LoadingPage,
 | 
			
		||||
      AccountSearchResult
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      onClickAction (event) {
 | 
			
		||||
        let { action, accountId } = event
 | 
			
		||||
        action.onclick(accountId)
 | 
			
		||||
      },
 | 
			
		||||
      async refreshAccounts () {
 | 
			
		||||
        let { accountsFetcher } = this.get()
 | 
			
		||||
        let accounts = await accountsFetcher()
 | 
			
		||||
        this.set({ accounts: accounts })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +9,7 @@
 | 
			
		|||
    {{#if pinnable}}
 | 
			
		||||
      <IconButton pressable="true"
 | 
			
		||||
                  pressed="{{$pinnedPage === href}}"
 | 
			
		||||
                  label="Pin page"
 | 
			
		||||
                  label="{{$pinnedPage === href ? 'Unpin timeline' : 'Pin timeline'}}"
 | 
			
		||||
                  href="#fa-thumb-tack"
 | 
			
		||||
                  on:click="onPinClick(event)" />
 | 
			
		||||
    {{/if}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,16 +7,28 @@
 | 
			
		|||
    <div class="search-result-account-username">
 | 
			
		||||
      {{'@' + account.acct}}
 | 
			
		||||
    </div>
 | 
			
		||||
    {{#if actions && actions.length}}
 | 
			
		||||
      <div class="search-result-account-buttons">
 | 
			
		||||
        {{#each actions as action}}
 | 
			
		||||
          <IconButton
 | 
			
		||||
            label="{{action.label}}"
 | 
			
		||||
            on:click="onButtonClick(event, action, account.id)"
 | 
			
		||||
            href="{{action.icon}}"
 | 
			
		||||
            big="true"
 | 
			
		||||
          />
 | 
			
		||||
        {{/each}}
 | 
			
		||||
      </div>
 | 
			
		||||
    {{/if}}
 | 
			
		||||
  </div>
 | 
			
		||||
</SearchResult>
 | 
			
		||||
<style>
 | 
			
		||||
  .search-result-account {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-areas:
 | 
			
		||||
      "avatar    name"
 | 
			
		||||
      "avatar    username";
 | 
			
		||||
      "avatar    name     buttons"
 | 
			
		||||
      "avatar    username buttons";
 | 
			
		||||
    grid-column-gap: 20px;
 | 
			
		||||
    grid-template-columns: max-content 1fr;
 | 
			
		||||
    grid-template-columns: max-content 1fr max-content;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
  :global(.search-result-account-avatar) {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,19 +48,45 @@
 | 
			
		|||
    text-overflow: ellipsis;
 | 
			
		||||
    color: var(--deemphasized-text-color);
 | 
			
		||||
  }
 | 
			
		||||
  .search-result-account-buttons {
 | 
			
		||||
    grid-area: buttons;
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
  :global(.search-result-account-buttons .icon-button) {
 | 
			
		||||
    margin-right: 20px;
 | 
			
		||||
  }
 | 
			
		||||
  :global(.search-result-account-buttons .icon-button:last-child) {
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 767px) {
 | 
			
		||||
    .search-result-account {
 | 
			
		||||
      grid-column-gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
    :global(.search-result-account-buttons .icon-button) {
 | 
			
		||||
      margin-right: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
<script>
 | 
			
		||||
  import Avatar from '../Avatar.html'
 | 
			
		||||
  import SearchResult from './SearchResult.html'
 | 
			
		||||
  import IconButton from '../IconButton.html'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    components: {
 | 
			
		||||
      Avatar,
 | 
			
		||||
      SearchResult
 | 
			
		||||
      SearchResult,
 | 
			
		||||
      IconButton
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      onButtonClick (event, action, accountId) {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
        event.stopPropagation()
 | 
			
		||||
        this.fire('click', {
 | 
			
		||||
          action,
 | 
			
		||||
          accountId
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,22 @@
 | 
			
		|||
<DynamicPageBanner title="Blocked users" icon="#fa-ban" />
 | 
			
		||||
<AccountsListPage :accountsFetcher />
 | 
			
		||||
<AccountsListPage :accountsFetcher :accountActions />
 | 
			
		||||
<script>
 | 
			
		||||
  import AccountsListPage from '.././_components/AccountsListPage.html'
 | 
			
		||||
  import { store } from '.././_store/store'
 | 
			
		||||
  import { getBlockedAccounts } from '.././_api/blockedAndMuted'
 | 
			
		||||
  import DynamicPageBanner from '.././_components/DynamicPageBanner.html'
 | 
			
		||||
  import { setAccountBlocked } from '../_actions/block'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    data: () => ({
 | 
			
		||||
      accountActions: [
 | 
			
		||||
        {
 | 
			
		||||
          icon: '#fa-unlock',
 | 
			
		||||
          label: 'Unblock',
 | 
			
		||||
          onclick: (accountId) => setAccountBlocked(accountId, false, true)
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }),
 | 
			
		||||
    computed: {
 | 
			
		||||
      accountsFetcher: ($currentInstance, $accessToken) => () => getBlockedAccounts($currentInstance, $accessToken)
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,22 @@
 | 
			
		|||
<DynamicPageBanner title="Muted users" icon="#fa-volume-off" />
 | 
			
		||||
<AccountsListPage :accountsFetcher />
 | 
			
		||||
<AccountsListPage :accountsFetcher :accountActions />
 | 
			
		||||
<script>
 | 
			
		||||
  import AccountsListPage from '.././_components/AccountsListPage.html'
 | 
			
		||||
  import { store } from '.././_store/store'
 | 
			
		||||
  import { getMutedAccounts } from '.././_api/blockedAndMuted'
 | 
			
		||||
  import DynamicPageBanner from '.././_components/DynamicPageBanner.html'
 | 
			
		||||
  import { setAccountMuted } from '../_actions/mute'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    data: () => ({
 | 
			
		||||
      accountActions: [
 | 
			
		||||
        {
 | 
			
		||||
          icon: '#fa-volume-up',
 | 
			
		||||
          label: 'Unmute',
 | 
			
		||||
          onclick: (accountId) => setAccountMuted(accountId, false, true)
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }),
 | 
			
		||||
    computed: {
 | 
			
		||||
      accountsFetcher: ($currentInstance, $accessToken) => () => getMutedAccounts($currentInstance, $accessToken)
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,29 @@
 | 
			
		|||
<DynamicPageBanner title="Follow requests" icon="#fa-user-plus" />
 | 
			
		||||
<AccountsListPage :accountsFetcher />
 | 
			
		||||
<AccountsListPage :accountsFetcher :accountActions />
 | 
			
		||||
<script>
 | 
			
		||||
  import AccountsListPage from '.././_components/AccountsListPage.html'
 | 
			
		||||
  import { store } from '.././_store/store'
 | 
			
		||||
  import { getFollowRequests } from '../_actions/followRequests'
 | 
			
		||||
  import DynamicPageBanner from '.././_components/DynamicPageBanner.html'
 | 
			
		||||
  import { setFollowRequestApprovedOrRejected } from '../_actions/requests'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    data: () => ({
 | 
			
		||||
      accountActions: [
 | 
			
		||||
        {
 | 
			
		||||
          icon: '#fa-check',
 | 
			
		||||
          label: 'Approve',
 | 
			
		||||
          onclick: (accountId) => setFollowRequestApprovedOrRejected(accountId, true, true)
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          icon: '#fa-times',
 | 
			
		||||
          label: 'Reject',
 | 
			
		||||
          onclick: (accountId) => setFollowRequestApprovedOrRejected(accountId, false, true)
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }),
 | 
			
		||||
    computed: {
 | 
			
		||||
      statusId: params => params.statusId,
 | 
			
		||||
      accountsFetcher: ($currentInstance, $accessToken, statusId) => () => getFollowRequests($currentInstance, $accessToken, statusId)
 | 
			
		||||
      accountsFetcher: ($currentInstance, $accessToken) => () => getFollowRequests($currentInstance, $accessToken)
 | 
			
		||||
    },
 | 
			
		||||
    store: () => store,
 | 
			
		||||
    components: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import {
 | 
			
		|||
  authorizeInput, emailInput, getUrl, instanceInput, mastodonLogInButton,
 | 
			
		||||
  passwordInput
 | 
			
		||||
} from './utils'
 | 
			
		||||
import { users } from './users'
 | 
			
		||||
 | 
			
		||||
function login (t, username, password) {
 | 
			
		||||
  return t.typeText(instanceInput, 'localhost:3000', {paste: true})
 | 
			
		||||
| 
						 | 
				
			
			@ -18,5 +19,9 @@ function login (t, username, password) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export const foobarRole = Role('http://localhost:4002/settings/instances/add', async t => {
 | 
			
		||||
  await login(t, 'foobar@localhost:3000', 'foobarfoobar')
 | 
			
		||||
  await login(t, users.foobar.email, users.foobar.password)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const lockedAccountRole = Role('http://localhost:4002/settings/instances/add', async t => {
 | 
			
		||||
  await login(t, users.LockedAccount.email, users.LockedAccount.password)
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import { users } from './users'
 | 
			
		|||
import { postStatus } from '../routes/_api/statuses'
 | 
			
		||||
import { deleteStatus } from '../routes/_api/delete'
 | 
			
		||||
import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests'
 | 
			
		||||
import { followAccount, unfollowAccount } from '../routes/_api/follow'
 | 
			
		||||
 | 
			
		||||
global.fetch = fetch
 | 
			
		||||
global.File = FileApi.File
 | 
			
		||||
| 
						 | 
				
			
			@ -37,3 +38,11 @@ export async function getFollowRequestsAs (username) {
 | 
			
		|||
export async function authorizeFollowRequestAs (username, id) {
 | 
			
		||||
  return authorizeFollowRequest(instanceName, users[username].accessToken, id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function followAs (username, userToFollow) {
 | 
			
		||||
  return followAccount(instanceName, users[username].accessToken, users[userToFollow].id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function unfollowAs (username, userToFollow) {
 | 
			
		||||
  return unfollowAccount(instanceName, users[username].accessToken, users[userToFollow].id)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										76
									
								
								tests/spec/116-follow-requests.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								tests/spec/116-follow-requests.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
import { lockedAccountRole } from '../roles'
 | 
			
		||||
import { followAs, unfollowAs } from '../serverActions'
 | 
			
		||||
import {
 | 
			
		||||
  communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
 | 
			
		||||
  homeNavButton, sleep
 | 
			
		||||
} from '../utils'
 | 
			
		||||
import { users } from '../users'
 | 
			
		||||
import { Selector as $ } from 'testcafe'
 | 
			
		||||
 | 
			
		||||
fixture`116-follow-requests.js`
 | 
			
		||||
  .page`http://localhost:4002`
 | 
			
		||||
 | 
			
		||||
const timeout = 30000
 | 
			
		||||
 | 
			
		||||
test('Can approve and reject follow requests', async t => {
 | 
			
		||||
  await t.useRole(lockedAccountRole)
 | 
			
		||||
 | 
			
		||||
  // necessary for re-running this test in local testing
 | 
			
		||||
  await Promise.all([
 | 
			
		||||
    unfollowAs('admin', 'LockedAccount'),
 | 
			
		||||
    unfollowAs('baz', 'LockedAccount'),
 | 
			
		||||
    unfollowAs('quux', 'LockedAccount')
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  await Promise.all([
 | 
			
		||||
    followAs('admin', 'LockedAccount'),
 | 
			
		||||
    followAs('baz', 'LockedAccount'),
 | 
			
		||||
    followAs('quux', 'LockedAccount')
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  await sleep(2000)
 | 
			
		||||
 | 
			
		||||
  const approveAdminButton = () => getSearchResultByHref(`/accounts/${users.admin.id}`).find('button:nth-child(1)')
 | 
			
		||||
  const rejectBazButton = () => getSearchResultByHref(`/accounts/${users.baz.id}`).find('button:nth-child(2)')
 | 
			
		||||
  const approveQuuxButton = () => getSearchResultByHref(`/accounts/${users.quux.id}`).find('button:nth-child(1)')
 | 
			
		||||
 | 
			
		||||
  await t.click(communityNavButton)
 | 
			
		||||
    .click($('a[href="/requests"]'))
 | 
			
		||||
    // no guaranteed order on these
 | 
			
		||||
    .expect(getNthSearchResult(1).innerText).match(/(@admin|@baz|@quux)/)
 | 
			
		||||
    .expect(getNthSearchResult(2).innerText).match(/(@admin|@baz|@quux)/)
 | 
			
		||||
    .expect(getNthSearchResult(3).innerText).match(/(@admin|@baz|@quux)/)
 | 
			
		||||
    .expect(getNthSearchResult(4).exists).notOk()
 | 
			
		||||
    // approve admin
 | 
			
		||||
    .expect(approveAdminButton().getAttribute('aria-label')).eql('Approve')
 | 
			
		||||
    .hover(approveAdminButton())
 | 
			
		||||
    .click(approveAdminButton())
 | 
			
		||||
    .expect(getNthSearchResult(1).innerText).match(/(@baz|@quux)/, {timeout})
 | 
			
		||||
    .expect(getNthSearchResult(2).innerText).match(/(@baz|@quux)/)
 | 
			
		||||
    .expect(getNthSearchResult(3).exists).notOk()
 | 
			
		||||
  await goBack()
 | 
			
		||||
  await t
 | 
			
		||||
    .click($('a[href="/requests"]'))
 | 
			
		||||
    // reject baz
 | 
			
		||||
    .expect(rejectBazButton().getAttribute('aria-label')).eql('Reject')
 | 
			
		||||
    .hover(rejectBazButton())
 | 
			
		||||
    .click(rejectBazButton())
 | 
			
		||||
    .expect(getNthSearchResult(1).innerText).contains('@quux', {timeout})
 | 
			
		||||
    .expect(getNthSearchResult(2).exists).notOk()
 | 
			
		||||
  await goBack()
 | 
			
		||||
  await t
 | 
			
		||||
    .click($('a[href="/requests"]'))
 | 
			
		||||
    // approve quux
 | 
			
		||||
    .expect(approveQuuxButton().getAttribute('aria-label')).eql('Approve')
 | 
			
		||||
    .hover(approveQuuxButton())
 | 
			
		||||
    .click(approveQuuxButton())
 | 
			
		||||
    .expect(getNthSearchResult(1).exists).notOk({timeout})
 | 
			
		||||
    // check our follow list to make sure they follow us
 | 
			
		||||
    .click(homeNavButton)
 | 
			
		||||
    .click($('.compose-box-avatar'))
 | 
			
		||||
    .expect(getUrl()).contains(`/accounts/${users.LockedAccount.id}`)
 | 
			
		||||
    .click(followersButton)
 | 
			
		||||
    .expect(getNthSearchResult(1).innerText).match(/(@admin|@quux)/)
 | 
			
		||||
    .expect(getNthSearchResult(2).innerText).match(/(@admin|@quux)/)
 | 
			
		||||
    .expect(getNthSearchResult(3).exists).notOk()
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +1,42 @@
 | 
			
		|||
export const users = {
 | 
			
		||||
  admin: {
 | 
			
		||||
    username: 'admin',
 | 
			
		||||
    email: 'admin@localhost:3000',
 | 
			
		||||
    password: 'mastodonadmin',
 | 
			
		||||
    accessToken: 'f954c8de1fcc0080ff706fa2516d05b60de0d8f5b536255a85ef85a6c32e4afb',
 | 
			
		||||
    id: 1
 | 
			
		||||
  },
 | 
			
		||||
  foobar: {
 | 
			
		||||
    username: 'foobar',
 | 
			
		||||
    email: 'foobar@localhost:3000',
 | 
			
		||||
    password: 'foobarfoobar',
 | 
			
		||||
    accessToken: 'b48d72074a467e77a18eafc0d52e373dcf2492bcb3fefadc302a81300ec69002',
 | 
			
		||||
    id: 2
 | 
			
		||||
  },
 | 
			
		||||
  quux: {
 | 
			
		||||
    username: 'quux',
 | 
			
		||||
    email: 'quux@localhost:3000',
 | 
			
		||||
    password: 'quuxquuxquux',
 | 
			
		||||
    accessToken: '894d3583dbf7d0f4f4784a06db86bdadb6ef0d99453d15afbc03e0c103bd78af',
 | 
			
		||||
    id: 3
 | 
			
		||||
  },
 | 
			
		||||
  ExternalLinks: {
 | 
			
		||||
    username: 'ExternalLinks',
 | 
			
		||||
    email: 'ExternalLinks@localhost:3000',
 | 
			
		||||
    password: 'ExternalLinksExternalLink',
 | 
			
		||||
    accessToken: 'e9a463ba1729ae0049a97a312af702cb3d08d84de1cc8d6da3fad90af068117b',
 | 
			
		||||
    id: 4
 | 
			
		||||
  },
 | 
			
		||||
  baz: {
 | 
			
		||||
    username: 'baz',
 | 
			
		||||
    email: 'baz@localhost:3000',
 | 
			
		||||
    password: 'bazbazbaz',
 | 
			
		||||
    accessToken: '0639238783efdfde849304bc89ec0c4b60b5ef5f261f60859fcd597de081cfdc',
 | 
			
		||||
    id: 5
 | 
			
		||||
  },
 | 
			
		||||
  LockedAccount: {
 | 
			
		||||
    username: 'LockedAccount',
 | 
			
		||||
    email: 'LockedAccount@localhost:3000',
 | 
			
		||||
    password: 'LockedAccountLockedAccount',
 | 
			
		||||
    accessToken: '39ed9aeffa4b25eda4940f22f29fea66e625c6282c2a8bf0430203c9779e9e98',
 | 
			
		||||
    id: 6
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -132,6 +132,10 @@ export function getNthAutosuggestionResult (n) {
 | 
			
		|||
  return $(`.compose-autosuggest-list-item:nth-child(${n}) button`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSearchResultByHref (href) {
 | 
			
		||||
  return $(`.search-result a[href="${href}"]`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNthSearchResult (n) {
 | 
			
		||||
  return $(`.search-result:nth-child(${n}) a`)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue