forked from cybrespace/pinafore
		
	add ability to pin and unpin statuses (#235)
* add ability to pin and unpin statuses * add another test
This commit is contained in:
		
							parent
							
								
									d079b6d9e1
								
							
						
					
					
						commit
						8089202977
					
				
					 14 changed files with 175 additions and 48 deletions
				
			
		| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { actions } from './mastodon-data'
 | 
					import { actions } from './mastodon-data'
 | 
				
			||||||
import { users } from '../tests/users'
 | 
					import { users } from '../tests/users'
 | 
				
			||||||
import { pinStatus, postStatus } from '../routes/_api/statuses'
 | 
					import { postStatus } from '../routes/_api/statuses'
 | 
				
			||||||
import { followAccount } from '../routes/_api/follow'
 | 
					import { followAccount } from '../routes/_api/follow'
 | 
				
			||||||
import { favoriteStatus } from '../routes/_api/favorite'
 | 
					import { favoriteStatus } from '../routes/_api/favorite'
 | 
				
			||||||
import { reblogStatus } from '../routes/_api/reblog'
 | 
					import { reblogStatus } from '../routes/_api/reblog'
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ import path from 'path'
 | 
				
			||||||
import fs from 'fs'
 | 
					import fs from 'fs'
 | 
				
			||||||
import FormData from 'form-data'
 | 
					import FormData from 'form-data'
 | 
				
			||||||
import { auth } from '../routes/_api/utils'
 | 
					import { auth } from '../routes/_api/utils'
 | 
				
			||||||
 | 
					import { pinStatus } from '../routes/_api/pin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
global.File = FileApi.File
 | 
					global.File = FileApi.File
 | 
				
			||||||
global.FormData = FileApi.FormData
 | 
					global.FormData = FileApi.FormData
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										28
									
								
								routes/_actions/pin.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								routes/_actions/pin.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					import { store } from '../_store/store'
 | 
				
			||||||
 | 
					import { toast } from '../_utils/toast'
 | 
				
			||||||
 | 
					import { pinStatus, unpinStatus } from '../_api/pin'
 | 
				
			||||||
 | 
					import { setStatusPinned as setStatusPinnedInDatabase } from '../_database/timelines/updateStatus'
 | 
				
			||||||
 | 
					import { emit } from '../_utils/eventBus'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
 | 
				
			||||||
 | 
					  let { currentInstance, accessToken } = store.get()
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    if (pinned) {
 | 
				
			||||||
 | 
					      await pinStatus(currentInstance, accessToken, statusId)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      await unpinStatus(currentInstance, accessToken, statusId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (toastOnSuccess) {
 | 
				
			||||||
 | 
					      if (pinned) {
 | 
				
			||||||
 | 
					        toast.say('Pinned status')
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toast.say('Unpinned status')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await setStatusPinnedInDatabase(currentInstance, statusId, pinned)
 | 
				
			||||||
 | 
					    emit('updatePinnedStatuses')
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error(e)
 | 
				
			||||||
 | 
					    toast.say(`Unable to ${pinned ? 'pin' : 'unpin'} status: ` + (e.message || ''))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								routes/_api/pin.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								routes/_api/pin.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { postWithTimeout } from '../_utils/ajax'
 | 
				
			||||||
 | 
					import { auth, basename } from './utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function pinStatus (instanceName, accessToken, statusId) {
 | 
				
			||||||
 | 
					  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/pin`
 | 
				
			||||||
 | 
					  return postWithTimeout(url, null, auth(accessToken))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function unpinStatus (instanceName, accessToken, statusId) {
 | 
				
			||||||
 | 
					  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unpin`
 | 
				
			||||||
 | 
					  return postWithTimeout(url, null, auth(accessToken))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -23,13 +23,3 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return postWithTimeout(url, body, auth(accessToken))
 | 
					  return postWithTimeout(url, body, auth(accessToken))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function pinStatus (instanceName, accessToken, statusId) {
 | 
					 | 
				
			||||||
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/pin`
 | 
					 | 
				
			||||||
  return postWithTimeout(url, null, auth(accessToken))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function unpinStatus (instanceName, accessToken, statusId) {
 | 
					 | 
				
			||||||
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unpin`
 | 
					 | 
				
			||||||
  return postWithTimeout(url, null, auth(accessToken))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@ import { close } from '../helpers/closeDialog'
 | 
				
			||||||
import { oncreate } from '../helpers/onCreateDialog'
 | 
					import { oncreate } from '../helpers/onCreateDialog'
 | 
				
			||||||
import { setAccountBlocked } from '../../../_actions/block'
 | 
					import { setAccountBlocked } from '../../../_actions/block'
 | 
				
			||||||
import { setAccountMuted } from '../../../_actions/mute'
 | 
					import { setAccountMuted } from '../../../_actions/mute'
 | 
				
			||||||
 | 
					import { setStatusPinnedOrUnpinned } from '../../../_actions/pin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  oncreate,
 | 
					  oncreate,
 | 
				
			||||||
| 
						 | 
					@ -24,6 +25,8 @@ export default {
 | 
				
			||||||
    relationship: ($currentAccountRelationship) => $currentAccountRelationship,
 | 
					    relationship: ($currentAccountRelationship) => $currentAccountRelationship,
 | 
				
			||||||
    account: ($currentAccountProfile) => $currentAccountProfile,
 | 
					    account: ($currentAccountProfile) => $currentAccountProfile,
 | 
				
			||||||
    verifyCredentials: ($currentVerifyCredentials) => $currentVerifyCredentials,
 | 
					    verifyCredentials: ($currentVerifyCredentials) => $currentVerifyCredentials,
 | 
				
			||||||
 | 
					    statusId: (status) => status.id,
 | 
				
			||||||
 | 
					    pinned: (status) => status.pinned,
 | 
				
			||||||
    // begin account data copypasta
 | 
					    // begin account data copypasta
 | 
				
			||||||
    verifyCredentialsId: (verifyCredentials) => verifyCredentials.id,
 | 
					    verifyCredentialsId: (verifyCredentials) => verifyCredentials.id,
 | 
				
			||||||
    following: (relationship) => relationship && relationship.following,
 | 
					    following: (relationship) => relationship && relationship.following,
 | 
				
			||||||
| 
						 | 
					@ -52,16 +55,21 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    muteIcon: (muting) => muting ? '#fa-volume-up' : '#fa-volume-off',
 | 
					    muteIcon: (muting) => muting ? '#fa-volume-up' : '#fa-volume-off',
 | 
				
			||||||
    // end account data copypasta
 | 
					    // end account data copypasta
 | 
				
			||||||
    items: (blockLabel, blocking, blockIcon, muteLabel, muteIcon,
 | 
					    isUser: (accountId, verifyCredentialsId) => accountId === verifyCredentialsId,
 | 
				
			||||||
      followLabel, followIcon, following, followRequested,
 | 
					    pinLabel: (pinned, isUser) => isUser ? (pinned ? 'Unpin from profile' : 'Pin to profile') : '',
 | 
				
			||||||
      accountId, verifyCredentialsId) => {
 | 
					    items: (blockLabel, blocking, blockIcon, muteLabel, muteIcon, followLabel, followIcon,
 | 
				
			||||||
      let isUser = accountId === verifyCredentialsId
 | 
					      following, followRequested, pinLabel, isUser) => {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        isUser && {
 | 
					        isUser && {
 | 
				
			||||||
          key: 'delete',
 | 
					          key: 'delete',
 | 
				
			||||||
          label: 'Delete',
 | 
					          label: 'Delete',
 | 
				
			||||||
          icon: '#fa-trash'
 | 
					          icon: '#fa-trash'
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        isUser && {
 | 
				
			||||||
 | 
					          key: 'pin',
 | 
				
			||||||
 | 
					          label: pinLabel,
 | 
				
			||||||
 | 
					          icon: '#fa-thumb-tack'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        !isUser && !blocking && {
 | 
					        !isUser && !blocking && {
 | 
				
			||||||
          key: 'follow',
 | 
					          key: 'follow',
 | 
				
			||||||
          label: followLabel,
 | 
					          label: followLabel,
 | 
				
			||||||
| 
						 | 
					@ -93,6 +101,8 @@ export default {
 | 
				
			||||||
      switch (item.key) {
 | 
					      switch (item.key) {
 | 
				
			||||||
        case 'delete':
 | 
					        case 'delete':
 | 
				
			||||||
          return this.onDeleteClicked()
 | 
					          return this.onDeleteClicked()
 | 
				
			||||||
 | 
					        case 'pin':
 | 
				
			||||||
 | 
					          return this.onPinClicked()
 | 
				
			||||||
        case 'follow':
 | 
					        case 'follow':
 | 
				
			||||||
          return this.onFollowClicked()
 | 
					          return this.onFollowClicked()
 | 
				
			||||||
        case 'block':
 | 
					        case 'block':
 | 
				
			||||||
| 
						 | 
					@ -106,6 +116,11 @@ export default {
 | 
				
			||||||
      this.close()
 | 
					      this.close()
 | 
				
			||||||
      await doDeleteStatus(statusId)
 | 
					      await doDeleteStatus(statusId)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    async onPinClicked () {
 | 
				
			||||||
 | 
					      let { statusId, pinned } = this.get()
 | 
				
			||||||
 | 
					      this.close()
 | 
				
			||||||
 | 
					      await setStatusPinnedOrUnpinned(statusId, !pinned, true)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    async onFollowClicked () {
 | 
					    async onFollowClicked () {
 | 
				
			||||||
      let { accountId, following } = this.get()
 | 
					      let { accountId, following } = this.get()
 | 
				
			||||||
      this.close()
 | 
					      this.close()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,14 +2,14 @@ import StatusOptionsDialog from '../components/StatusOptionsDialog.html'
 | 
				
			||||||
import { createDialogElement } from '../helpers/createDialogElement'
 | 
					import { createDialogElement } from '../helpers/createDialogElement'
 | 
				
			||||||
import { createDialogId } from '../helpers/createDialogId'
 | 
					import { createDialogId } from '../helpers/createDialogId'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function showStatusOptionsDialog (statusId) {
 | 
					export default function showStatusOptionsDialog (status) {
 | 
				
			||||||
  let dialog = new StatusOptionsDialog({
 | 
					  let dialog = new StatusOptionsDialog({
 | 
				
			||||||
    target: createDialogElement(),
 | 
					    target: createDialogElement(),
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      id: createDialogId(),
 | 
					      id: createDialogId(),
 | 
				
			||||||
      label: 'Status options dialog',
 | 
					      label: 'Status options dialog',
 | 
				
			||||||
      title: '',
 | 
					      title: '',
 | 
				
			||||||
      statusId: statusId
 | 
					      status: status
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  dialog.show()
 | 
					  dialog.show()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,11 +107,11 @@
 | 
				
			||||||
      async onOptionsClick (e) {
 | 
					      async onOptionsClick (e) {
 | 
				
			||||||
        e.preventDefault()
 | 
					        e.preventDefault()
 | 
				
			||||||
        e.stopPropagation()
 | 
					        e.stopPropagation()
 | 
				
			||||||
        let { originalStatusId, originalAccountId } = this.get()
 | 
					        let { originalStatus, originalAccountId } = this.get()
 | 
				
			||||||
        let updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
 | 
					        let updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
 | 
				
			||||||
        let showStatusOptionsDialog = await importShowStatusOptionsDialog()
 | 
					        let showStatusOptionsDialog = await importShowStatusOptionsDialog()
 | 
				
			||||||
        await updateRelationshipPromise
 | 
					        await updateRelationshipPromise
 | 
				
			||||||
        showStatusOptionsDialog(originalStatusId)
 | 
					        showStatusOptionsDialog(originalStatus)
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      onPostedStatus (realm, inReplyToUuid) {
 | 
					      onPostedStatus (realm, inReplyToUuid) {
 | 
				
			||||||
        let {
 | 
					        let {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,33 +1,38 @@
 | 
				
			||||||
<div role="feed" aria-label="Pinned toots" class="pinned-statuses">
 | 
					<div role="feed" aria-label="Pinned toots" class="pinned-statuses">
 | 
				
			||||||
  {{#if pinnedStatuses}}
 | 
					  {{#each pinnedStatuses as status, index @id}}
 | 
				
			||||||
    {{#each pinnedStatuses as status, index}}
 | 
					    <Status :status
 | 
				
			||||||
      <Status :status
 | 
					            timelineType="pinned"
 | 
				
			||||||
              timelineType="pinned"
 | 
					            timelineValue="{{accountId}}"
 | 
				
			||||||
              timelineValue="{{accountId}}"
 | 
					            :index
 | 
				
			||||||
              :index
 | 
					            length="{{pinnedStatuses.length}}"
 | 
				
			||||||
              length="{{pinnedStatuses.length}}"
 | 
					    />
 | 
				
			||||||
      />
 | 
					  {{/each}}
 | 
				
			||||||
    {{/each}}
 | 
					 | 
				
			||||||
  {{/if}}
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  import { store } from '../../_store/store'
 | 
					  import { store } from '../../_store/store'
 | 
				
			||||||
  import Status from '../status/Status.html'
 | 
					  import Status from '../status/Status.html'
 | 
				
			||||||
  import { updatePinnedStatusesForAccount } from '../../_actions/pinnedStatuses'
 | 
					  import { updatePinnedStatusesForAccount } from '../../_actions/pinnedStatuses'
 | 
				
			||||||
 | 
					  import { on } from '../../_utils/eventBus'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					  export default {
 | 
				
			||||||
    async oncreate () {
 | 
					    async oncreate () {
 | 
				
			||||||
      let { accountId } = this.get()
 | 
					      on('updatePinnedStatuses', this, () => this.updatePinnedStatuses())
 | 
				
			||||||
      await updatePinnedStatusesForAccount(accountId)
 | 
					      await this.updatePinnedStatuses()
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
      pinnedStatuses: ($pinnedStatuses, $currentInstance, accountId) => {
 | 
					      pinnedStatuses: ($pinnedStatuses, $currentInstance, accountId) => {
 | 
				
			||||||
        return $pinnedStatuses[$currentInstance] && $pinnedStatuses[$currentInstance][accountId]
 | 
					        return ($pinnedStatuses[$currentInstance] && $pinnedStatuses[$currentInstance][accountId]) || []
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    store: () => store,
 | 
					    store: () => store,
 | 
				
			||||||
    components: {
 | 
					    components: {
 | 
				
			||||||
      Status
 | 
					      Status
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					      async updatePinnedStatuses () {
 | 
				
			||||||
 | 
					        let { accountId } = this.get()
 | 
				
			||||||
 | 
					        await updatePinnedStatusesForAccount(accountId)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -13,10 +13,19 @@ export async function insertPinnedStatuses (instanceName, accountId, statuses) {
 | 
				
			||||||
  let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
 | 
					  let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
 | 
				
			||||||
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
 | 
					  await dbPromise(db, storeNames, 'readwrite', (stores) => {
 | 
				
			||||||
    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
 | 
					    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
 | 
				
			||||||
    statuses.forEach((status, i) => {
 | 
					
 | 
				
			||||||
      storeStatus(statusesStore, accountsStore, status)
 | 
					    let keyRange = createPinnedStatusKeyRange(accountId)
 | 
				
			||||||
      pinnedStatusesStore.put(status.id, createPinnedStatusId(accountId, i))
 | 
					    pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
 | 
				
			||||||
    })
 | 
					      // if there was e.g. 1 pinned status before and 2 now, then we need to delete the old one
 | 
				
			||||||
 | 
					      let existingPinnedStatuses = e.target.result
 | 
				
			||||||
 | 
					      for (let i = statuses.length; i < existingPinnedStatuses.length; i++) {
 | 
				
			||||||
 | 
					        pinnedStatusesStore.delete(createPinnedStatusKeyRange(accountId, i))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      statuses.forEach((status, i) => {
 | 
				
			||||||
 | 
					        storeStatus(statusesStore, accountsStore, status)
 | 
				
			||||||
 | 
					        pinnedStatusesStore.put(status.id, createPinnedStatusId(accountId, i))
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,3 +39,9 @@ export async function setStatusReblogged (instanceName, statusId, reblogged) {
 | 
				
			||||||
    status.reblogs_count = (status.reblogs_count || 0) + delta
 | 
					    status.reblogs_count = (status.reblogs_count || 0) + delta
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function setStatusPinned (instanceName, statusId, pinned) {
 | 
				
			||||||
 | 
					  return updateStatus(instanceName, statusId, status => {
 | 
				
			||||||
 | 
					    status.pinned = pinned
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { Selector as $ } from 'testcafe'
 | 
					import { Selector as $ } from 'testcafe'
 | 
				
			||||||
import { getUrl } from '../utils'
 | 
					import { communityNavButton, getNthPinnedStatus, getUrl } from '../utils'
 | 
				
			||||||
import { foobarRole } from '../roles'
 | 
					import { foobarRole } from '../roles'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fixture`004-pinned-statuses.js`
 | 
					fixture`004-pinned-statuses.js`
 | 
				
			||||||
| 
						 | 
					@ -7,9 +7,9 @@ fixture`004-pinned-statuses.js`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("shows a user's pinned statuses", async t => {
 | 
					test("shows a user's pinned statuses", async t => {
 | 
				
			||||||
  await t.useRole(foobarRole)
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
    .click($('nav a[aria-label=Community]'))
 | 
					    .click(communityNavButton)
 | 
				
			||||||
    .expect(getUrl()).contains('/community')
 | 
					    .expect(getUrl()).contains('/community')
 | 
				
			||||||
    .click($('a').withText(('Pinned')))
 | 
					    .click($('a[href="/pinned"]'))
 | 
				
			||||||
    .expect(getUrl()).contains('/pinned')
 | 
					    .expect(getUrl()).contains('/pinned')
 | 
				
			||||||
    .expect($('.status-article').getAttribute('aria-posinset')).eql('0')
 | 
					    .expect($('.status-article').getAttribute('aria-posinset')).eql('0')
 | 
				
			||||||
    .expect($('.status-article').getAttribute('aria-setsize')).eql('1')
 | 
					    .expect($('.status-article').getAttribute('aria-setsize')).eql('1')
 | 
				
			||||||
| 
						 | 
					@ -19,17 +19,17 @@ test("shows a user's pinned statuses", async t => {
 | 
				
			||||||
test("shows pinned statuses on a user's account page", async t => {
 | 
					test("shows pinned statuses on a user's account page", async t => {
 | 
				
			||||||
  await t.useRole(foobarRole)
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
    .navigateTo('/accounts/2')
 | 
					    .navigateTo('/accounts/2')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article').getAttribute('aria-posinset')).eql('0')
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-posinset')).eql('0')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article').getAttribute('aria-setsize')).eql('1')
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-setsize')).eql('1')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article').innerText).contains('this is unlisted')
 | 
					    .expect(getNthPinnedStatus(0).innerText).contains('this is unlisted')
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("shows pinned statuses on a user's account page 2", async t => {
 | 
					test("shows pinned statuses on a user's account page 2", async t => {
 | 
				
			||||||
  await t.useRole(foobarRole)
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
    .navigateTo('/accounts/3')
 | 
					    .navigateTo('/accounts/3')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article').getAttribute('aria-posinset')).eql('0')
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-posinset')).eql('0')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article').getAttribute('aria-setsize')).eql('2')
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-setsize')).eql('2')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article').innerText).contains('pinned toot 1')
 | 
					    .expect(getNthPinnedStatus(0).innerText).contains('pinned toot 1')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article[aria-posinset="1"]').getAttribute('aria-setsize')).eql('2')
 | 
					    .expect(getNthPinnedStatus(1).getAttribute('aria-setsize')).eql('2')
 | 
				
			||||||
    .expect($('.pinned-statuses .status-article[aria-posinset="1"]').innerText).contains('pinned toot 2')
 | 
					    .expect(getNthPinnedStatus(1).innerText).contains('pinned toot 2')
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import { lockedAccountRole } from '../roles'
 | 
					import { lockedAccountRole } from '../roles'
 | 
				
			||||||
import { followAs, unfollowAs } from '../serverActions'
 | 
					import { followAs, unfollowAs } from '../serverActions'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  avatarInComposeBox,
 | 
				
			||||||
  communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
 | 
					  communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
 | 
				
			||||||
  homeNavButton, sleep
 | 
					  homeNavButton, sleep
 | 
				
			||||||
} from '../utils'
 | 
					} from '../utils'
 | 
				
			||||||
| 
						 | 
					@ -67,7 +68,7 @@ test('Can approve and reject follow requests', async t => {
 | 
				
			||||||
    .expect(getNthSearchResult(1).exists).notOk({timeout})
 | 
					    .expect(getNthSearchResult(1).exists).notOk({timeout})
 | 
				
			||||||
    // check our follow list to make sure they follow us
 | 
					    // check our follow list to make sure they follow us
 | 
				
			||||||
    .click(homeNavButton)
 | 
					    .click(homeNavButton)
 | 
				
			||||||
    .click($('.compose-box-avatar'))
 | 
					    .click(avatarInComposeBox)
 | 
				
			||||||
    .expect(getUrl()).contains(`/accounts/${users.LockedAccount.id}`)
 | 
					    .expect(getUrl()).contains(`/accounts/${users.LockedAccount.id}`)
 | 
				
			||||||
    .click(followersButton)
 | 
					    .click(followersButton)
 | 
				
			||||||
    .expect(getNthSearchResult(1).innerText).match(/(@admin|@quux)/)
 | 
					    .expect(getNthSearchResult(1).innerText).match(/(@admin|@quux)/)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										51
									
								
								tests/spec/117-pin-unpin.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/spec/117-pin-unpin.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					import { foobarRole } from '../roles'
 | 
				
			||||||
 | 
					import { postAs } from '../serverActions'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  avatarInComposeBox, getNthDialogOptionsOption, getNthPinnedStatus, getNthPinnedStatusFavoriteButton, getNthStatus,
 | 
				
			||||||
 | 
					  getNthStatusOptionsButton, getUrl, sleep
 | 
				
			||||||
 | 
					} from '../utils'
 | 
				
			||||||
 | 
					import { users } from '../users'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fixture`117-pin-unpin.js`
 | 
				
			||||||
 | 
					  .page`http://localhost:4002`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can pin statuses', async t => {
 | 
				
			||||||
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await postAs('foobar', 'I am going to pin this')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await sleep(2000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await t.click(avatarInComposeBox)
 | 
				
			||||||
 | 
					    .expect(getUrl()).contains(`/accounts/${users.foobar.id}`)
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-setsize')).eql('1')
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).innerText).contains('this is unlisted')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).contains('I am going to pin this')
 | 
				
			||||||
 | 
					    .click(getNthStatusOptionsButton(0))
 | 
				
			||||||
 | 
					    .expect(getNthDialogOptionsOption(1).innerText).contains('Delete')
 | 
				
			||||||
 | 
					    .expect(getNthDialogOptionsOption(2).innerText).contains('Pin to profile')
 | 
				
			||||||
 | 
					    .click(getNthDialogOptionsOption(2))
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-setsize')).eql('2')
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).innerText).contains('I am going to pin this')
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).contains('I am going to pin this')
 | 
				
			||||||
 | 
					    .click(getNthStatusOptionsButton(0))
 | 
				
			||||||
 | 
					    .expect(getNthDialogOptionsOption(1).innerText).contains('Delete')
 | 
				
			||||||
 | 
					    .expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile')
 | 
				
			||||||
 | 
					    .click(getNthDialogOptionsOption(2))
 | 
				
			||||||
 | 
					    .expect(getUrl()).contains(`/accounts/${users.foobar.id}`)
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-setsize')).eql('1')
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).innerText).contains('this is unlisted')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).contains('I am going to pin this')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Can favorite a pinned status', async t => {
 | 
				
			||||||
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
 | 
					    .click(avatarInComposeBox)
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatus(0).getAttribute('aria-setsize')).eql('1')
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatusFavoriteButton(0).getAttribute('aria-pressed')).eql('false')
 | 
				
			||||||
 | 
					    .click(getNthPinnedStatusFavoriteButton(0))
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatusFavoriteButton(0).getAttribute('aria-pressed')).eql('true')
 | 
				
			||||||
 | 
					    .click(getNthPinnedStatusFavoriteButton(0))
 | 
				
			||||||
 | 
					    .expect(getNthPinnedStatusFavoriteButton(0).getAttribute('aria-pressed')).eql('false')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,7 @@ export const addInstanceButton = $('#submitButton')
 | 
				
			||||||
export const mastodonLogInButton = $('button[type="submit"]')
 | 
					export const mastodonLogInButton = $('button[type="submit"]')
 | 
				
			||||||
export const followsButton = $('.account-profile-details > *:nth-child(2)')
 | 
					export const followsButton = $('.account-profile-details > *:nth-child(2)')
 | 
				
			||||||
export const followersButton = $('.account-profile-details > *:nth-child(3)')
 | 
					export const followersButton = $('.account-profile-details > *:nth-child(3)')
 | 
				
			||||||
 | 
					export const avatarInComposeBox = $('.compose-box-avatar')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
 | 
					export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
 | 
				
			||||||
  innerCount: el => parseInt(el.innerText, 10)
 | 
					  innerCount: el => parseInt(el.innerText, 10)
 | 
				
			||||||
| 
						 | 
					@ -224,6 +225,14 @@ export function getReblogsCount () {
 | 
				
			||||||
  return reblogsCountElement.innerCount
 | 
					  return reblogsCountElement.innerCount
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthPinnedStatus (n) {
 | 
				
			||||||
 | 
					  return $(`.pinned-statuses article[aria-posinset="${n}"]`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getNthPinnedStatusFavoriteButton (n) {
 | 
				
			||||||
 | 
					  return getNthPinnedStatus(n).find('.status-toolbar button:nth-child(3)')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function validateTimeline (t, timeline) {
 | 
					export async function validateTimeline (t, timeline) {
 | 
				
			||||||
  for (let i = 0; i < timeline.length; i++) {
 | 
					  for (let i = 0; i < timeline.length; i++) {
 | 
				
			||||||
    let status = timeline[i]
 | 
					    let status = timeline[i]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue