kopia lustrzana https://github.com/nolanlawson/pinafore
				
				
				
			Fix alts for image uploads (#54)
* Fix alts for image uploads Fixes #41 * fix alts mixed with no-altsfix-timeouts
							rodzic
							
								
									bca959d1a3
								
							
						
					
					
						commit
						7ae3212c55
					
				| 
						 | 
				
			
			@ -4,6 +4,7 @@ import { postStatus as postStatusToServer } from '../_api/statuses'
 | 
			
		|||
import { addStatusOrNotification } from './addStatusOrNotification'
 | 
			
		||||
import { database } from '../_database/database'
 | 
			
		||||
import { emit } from '../_utils/eventBus'
 | 
			
		||||
import { putMediaDescription } from '../_api/media'
 | 
			
		||||
 | 
			
		||||
export async function insertHandleForReply (statusId) {
 | 
			
		||||
  let instanceName = store.get('currentInstance')
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +21,8 @@ export async function insertHandleForReply (statusId) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
 | 
			
		||||
                                  sensitive, spoilerText, visibility) {
 | 
			
		||||
                                  sensitive, spoilerText, visibility,
 | 
			
		||||
                                  mediaDescriptions = []) {
 | 
			
		||||
  let instanceName = store.get('currentInstance')
 | 
			
		||||
  let accessToken = store.get('accessToken')
 | 
			
		||||
  let online = store.get('online')
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +36,9 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
 | 
			
		|||
    postingStatus: true
 | 
			
		||||
  })
 | 
			
		||||
  try {
 | 
			
		||||
    await Promise.all(mediaDescriptions.map(async (description, i) => {
 | 
			
		||||
      return description && putMediaDescription(instanceName, accessToken, mediaIds[i], description)
 | 
			
		||||
    }))
 | 
			
		||||
    let status = await postStatusToServer(instanceName, accessToken, text,
 | 
			
		||||
      inReplyToId, mediaIds, sensitive, spoilerText, visibility)
 | 
			
		||||
    addStatusOrNotification(instanceName, 'home', status)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,9 +36,15 @@ export function deleteMedia (realm, i) {
 | 
			
		|||
  let composeText = store.getComposeData(realm, 'text') || ''
 | 
			
		||||
  composeText = composeText.replace(' ' + deletedMedia.data.text_url, '')
 | 
			
		||||
 | 
			
		||||
  let mediaDescriptions = store.getComposeData(realm, 'mediaDescriptions') || []
 | 
			
		||||
  if (mediaDescriptions[i]) {
 | 
			
		||||
    mediaDescriptions[i] = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  store.setComposeData(realm, {
 | 
			
		||||
    media: composeMedia,
 | 
			
		||||
    text: composeText
 | 
			
		||||
    text: composeText,
 | 
			
		||||
    mediaDescriptions: mediaDescriptions
 | 
			
		||||
  })
 | 
			
		||||
  scheduleIdleTask(() => store.save())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,17 @@
 | 
			
		|||
import { auth, basename } from './utils'
 | 
			
		||||
import { postWithTimeout } from '../_utils/ajax'
 | 
			
		||||
import { postWithTimeout, putWithTimeout } from '../_utils/ajax'
 | 
			
		||||
 | 
			
		||||
export async function uploadMedia (instanceName, accessToken, file, description) {
 | 
			
		||||
  let formData = new FormData()
 | 
			
		||||
  formData.append('file', file)
 | 
			
		||||
  formData.append('description', description)
 | 
			
		||||
  if (description) {
 | 
			
		||||
    formData.append('description', description)
 | 
			
		||||
  }
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/media`
 | 
			
		||||
  return postWithTimeout(url, formData, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function putMediaDescription (instanceName, accessToken, mediaId, description) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
 | 
			
		||||
  return putWithTimeout(url, {description}, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
  <ComposeLengthGauge :length :overLimit />
 | 
			
		||||
  <ComposeToolbar :realm :postPrivacy :media :contentWarningShown :text />
 | 
			
		||||
  <ComposeLengthIndicator :length :overLimit />
 | 
			
		||||
  <ComposeMedia :realm :media />
 | 
			
		||||
  <ComposeMedia :realm :media :mediaDescriptions />
 | 
			
		||||
</div>
 | 
			
		||||
<div class="compose-box-button-sentinel {{hideAndFadeIn}}" ref:sentinel></div>
 | 
			
		||||
<div class="compose-box-button-wrapper {{realm === 'home' ? 'compose-button-sticky' : ''}} {{hideAndFadeIn}}" >
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +170,8 @@
 | 
			
		|||
      overLimit: (length) => length > CHAR_LIMIT,
 | 
			
		||||
      contentWarningShown: (composeData) => composeData.contentWarningShown,
 | 
			
		||||
      contentWarning: (composeData) => composeData.contentWarning || '',
 | 
			
		||||
      timelineInitialized: ($timelineInitialized) => $timelineInitialized
 | 
			
		||||
      timelineInitialized: ($timelineInitialized) => $timelineInitialized,
 | 
			
		||||
      mediaDescriptions: (composeData) => composeData.mediaDescriptions || []
 | 
			
		||||
    },
 | 
			
		||||
    transitions: {
 | 
			
		||||
      slide
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +194,7 @@
 | 
			
		|||
          let mediaIds = media.map(_ => _.data.id)
 | 
			
		||||
          let inReplyTo = (realm === 'home' || realm === 'dialog') ? null : realm
 | 
			
		||||
          let overLimit = this.get('overLimit')
 | 
			
		||||
          let mediaDescriptions = this.get('mediaDescriptions')
 | 
			
		||||
 | 
			
		||||
          if (!text || overLimit) {
 | 
			
		||||
            return // do nothing if invalid
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +202,7 @@
 | 
			
		|||
 | 
			
		||||
          /* no await */
 | 
			
		||||
          postStatus(realm, text, inReplyTo, mediaIds,
 | 
			
		||||
            sensitive, contentWarning, postPrivacyKey)
 | 
			
		||||
            sensitive, contentWarning, postPrivacyKey, mediaDescriptions)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      setupStickyObserver() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,13 +67,13 @@
 | 
			
		|||
        })
 | 
			
		||||
      },
 | 
			
		||||
      setupSyncToStore() {
 | 
			
		||||
        const saveText = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
 | 
			
		||||
        const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
 | 
			
		||||
 | 
			
		||||
        this.observe('rawText', rawText => {
 | 
			
		||||
          mark('observe rawText')
 | 
			
		||||
          let realm = this.get('realm')
 | 
			
		||||
          this.store.setComposeData(realm, {text: rawText})
 | 
			
		||||
          saveText()
 | 
			
		||||
          saveStore()
 | 
			
		||||
          stop('observe rawText')
 | 
			
		||||
        }, {init: false})
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,7 @@
 | 
			
		|||
{{#if media.length}}
 | 
			
		||||
  <div class="compose-media-container" style="grid-template-columns: repeat({{media.length}}, 1fr);">
 | 
			
		||||
    {{#each media as mediaItem, i}}
 | 
			
		||||
      <div class="compose-media">
 | 
			
		||||
        <img src="{{mediaItem.data.preview_url}}" alt="{{mediaItem.file.name}}"/>
 | 
			
		||||
        <div class="compose-media-delete">
 | 
			
		||||
          <button class="compose-media-delete-button"
 | 
			
		||||
                  aria-label="Delete {{mediaItem.file.name}}"
 | 
			
		||||
                  on:click="onDeleteMedia(i)" >
 | 
			
		||||
            <svg class="compose-media-delete-button-svg">
 | 
			
		||||
              <use xlink:href="#fa-times" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="compose-media-alt">
 | 
			
		||||
          <input type="text"
 | 
			
		||||
                 placeholder="Description"
 | 
			
		||||
                 aria-label="Describe {{mediaItem.file.name}} for the visually impaired">
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    {{#each media as mediaItem, index}}
 | 
			
		||||
      <ComposeMediaItem :realm :mediaItem :index :mediaDescriptions />
 | 
			
		||||
    {{/each}}
 | 
			
		||||
  </div>
 | 
			
		||||
{{/if}}
 | 
			
		||||
| 
						 | 
				
			
			@ -33,72 +17,15 @@
 | 
			
		|||
    padding: 5px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media {
 | 
			
		||||
    height: 200px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    background: var(--main-bg);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media img {
 | 
			
		||||
    object-fit: contain;
 | 
			
		||||
    object-position: center center;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-alt {
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-alt input {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
    background: var(--alt-input-bg);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-alt input:focus {
 | 
			
		||||
    background: var(--main-bg);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    margin: 2px;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete-button {
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete-button:hover {
 | 
			
		||||
    background: var(--toast-border);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete-button-svg {
 | 
			
		||||
    fill: var(--button-text);
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
<script>
 | 
			
		||||
  import { store } from '../../_store/store'
 | 
			
		||||
  import { deleteMedia } from '../../_actions/media'
 | 
			
		||||
  import ComposeMediaItem from './ComposeMediaItem.html'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    store: () => store,
 | 
			
		||||
    methods: {
 | 
			
		||||
      onDeleteMedia(i) {
 | 
			
		||||
        deleteMedia(this.get('realm'), i)
 | 
			
		||||
      }
 | 
			
		||||
    components: {
 | 
			
		||||
      ComposeMediaItem
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,126 @@
 | 
			
		|||
<div class="compose-media">
 | 
			
		||||
  <img src="{{mediaItem.data.preview_url}}" alt="{{mediaItem.file.name}}"/>
 | 
			
		||||
  <div class="compose-media-delete">
 | 
			
		||||
    <button class="compose-media-delete-button"
 | 
			
		||||
            aria-label="Delete {{mediaItem.file.name}}"
 | 
			
		||||
            on:click="onDeleteMedia()" >
 | 
			
		||||
      <svg class="compose-media-delete-button-svg">
 | 
			
		||||
        <use xlink:href="#fa-times" />
 | 
			
		||||
      </svg>
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="compose-media-alt">
 | 
			
		||||
    <input type="text"
 | 
			
		||||
           placeholder="Description"
 | 
			
		||||
           aria-label="Describe {{mediaItem.file.name}} for the visually impaired"
 | 
			
		||||
           bind:value=rawText
 | 
			
		||||
    >
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<style>
 | 
			
		||||
  .compose-media {
 | 
			
		||||
    height: 200px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    background: var(--main-bg);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media img {
 | 
			
		||||
    object-fit: contain;
 | 
			
		||||
    object-position: center center;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-alt {
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-alt input {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
    background: var(--alt-input-bg);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-alt input:focus {
 | 
			
		||||
    background: var(--main-bg);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    margin: 2px;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete-button {
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete-button:hover {
 | 
			
		||||
    background: var(--toast-border);
 | 
			
		||||
  }
 | 
			
		||||
  .compose-media-delete-button-svg {
 | 
			
		||||
    fill: var(--button-text);
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
<script>
 | 
			
		||||
  import { store } from '../../_store/store'
 | 
			
		||||
  import { deleteMedia } from '../../_actions/media'
 | 
			
		||||
  import debounce from 'lodash-es/debounce'
 | 
			
		||||
  import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    oncreate() {
 | 
			
		||||
      this.setupSyncFromStore()
 | 
			
		||||
      this.setupSyncToStore()
 | 
			
		||||
    },
 | 
			
		||||
    data: () => ({
 | 
			
		||||
      rawText: ''
 | 
			
		||||
    }),
 | 
			
		||||
    store: () => store,
 | 
			
		||||
    methods: {
 | 
			
		||||
      setupSyncFromStore() {
 | 
			
		||||
        this.observe('mediaDescriptions', mediaDescriptions => {
 | 
			
		||||
          mediaDescriptions = mediaDescriptions || []
 | 
			
		||||
          let index = this.get('index')
 | 
			
		||||
          let text = mediaDescriptions[index] || ''
 | 
			
		||||
          if (this.get('rawText') !== text) {
 | 
			
		||||
            this.set({rawText: text})
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      setupSyncToStore() {
 | 
			
		||||
        const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
 | 
			
		||||
 | 
			
		||||
        this.observe('rawText', rawText => {
 | 
			
		||||
          let realm = this.get('realm')
 | 
			
		||||
          let index = this.get('index')
 | 
			
		||||
          let mediaDescriptions = store.getComposeData(realm, 'mediaDescriptions') || []
 | 
			
		||||
          if (mediaDescriptions[index] === rawText) {
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
          while (mediaDescriptions.length <= index) {
 | 
			
		||||
            mediaDescriptions.push(null)
 | 
			
		||||
          }
 | 
			
		||||
          mediaDescriptions[index] = rawText
 | 
			
		||||
          store.setComposeData(realm, {mediaDescriptions})
 | 
			
		||||
          saveStore()
 | 
			
		||||
        }, {init: false})
 | 
			
		||||
      },
 | 
			
		||||
      onDeleteMedia() {
 | 
			
		||||
        deleteMedia(this.get('realm'), this.get('index'))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -2,4 +2,4 @@ let count = -1
 | 
			
		|||
 | 
			
		||||
export function createDialogId () {
 | 
			
		||||
  return ++count
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,6 +41,26 @@ async function _post (url, body, headers, timeout) {
 | 
			
		|||
  return throwErrorIfInvalidResponse(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _put (url, body, headers, timeout) {
 | 
			
		||||
  let fetchFunc = timeout ? fetchWithTimeout : fetch
 | 
			
		||||
  let opts = {
 | 
			
		||||
    method: 'PUT'
 | 
			
		||||
  }
 | 
			
		||||
  if (body) {
 | 
			
		||||
    opts.headers = Object.assign(headers, {
 | 
			
		||||
      'Accept': 'application/json',
 | 
			
		||||
      'Content-Type': 'application/json'
 | 
			
		||||
    })
 | 
			
		||||
    opts.body = JSON.stringify(body)
 | 
			
		||||
  } else {
 | 
			
		||||
    opts.headers = Object.assign(headers, {
 | 
			
		||||
      'Accept': 'application/json'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  let response = await fetchFunc(url, opts)
 | 
			
		||||
  return throwErrorIfInvalidResponse(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _get (url, headers, timeout) {
 | 
			
		||||
  let fetchFunc = timeout ? fetchWithTimeout : fetch
 | 
			
		||||
  let response = await fetchFunc(url, {
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +83,14 @@ async function _delete (url, headers, timeout) {
 | 
			
		|||
  return throwErrorIfInvalidResponse(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function put (url, body, headers = {}) {
 | 
			
		||||
  return _put(url, body, headers, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function putWithTimeout (url, body, headers = {}) {
 | 
			
		||||
  return _put(url, body, headers, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function post (url, body, headers = {}) {
 | 
			
		||||
  return _post(url, body, headers, false)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,7 @@
 | 
			
		|||
import { composeInput, getNthDeleteMediaButton, getNthMedia, mediaButton, uploadKittenImage } from '../utils'
 | 
			
		||||
import {
 | 
			
		||||
  composeInput, getNthDeleteMediaButton, getNthMedia, mediaButton,
 | 
			
		||||
  uploadKittenImage
 | 
			
		||||
} from '../utils'
 | 
			
		||||
import { foobarRole } from '../roles'
 | 
			
		||||
 | 
			
		||||
fixture`013-compose-media.js`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,4 +48,4 @@ test('can use emoji dialog within compose dialog', async t => {
 | 
			
		|||
    .expect(showMoreButton.innerText).contains('Show 1 more')
 | 
			
		||||
    .click(showMoreButton)
 | 
			
		||||
  await t.expect(getNthStatus(0).find('img[alt=":blobpats:"]').exists).ok({timeout: 20000})
 | 
			
		||||
})
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
import {
 | 
			
		||||
  composeButton, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, getNthStatusAndImage, getUrl,
 | 
			
		||||
  homeNavButton,
 | 
			
		||||
  mediaButton, notificationsNavButton,
 | 
			
		||||
  uploadKittenImage
 | 
			
		||||
} from '../utils'
 | 
			
		||||
import { foobarRole } from '../roles'
 | 
			
		||||
 | 
			
		||||
fixture`109-compose-media.js`
 | 
			
		||||
  .page`http://localhost:4002`
 | 
			
		||||
 | 
			
		||||
async function uploadTwoKittens (t) {
 | 
			
		||||
  await (uploadKittenImage(1)())
 | 
			
		||||
  await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
 | 
			
		||||
  await (uploadKittenImage(2)())
 | 
			
		||||
  await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
 | 
			
		||||
    .expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test('uploads alts for media', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .expect(mediaButton.hasAttribute('disabled')).notOk()
 | 
			
		||||
  await uploadTwoKittens(t)
 | 
			
		||||
  await t.typeText(getNthMediaAltInput(2), 'kitten 2')
 | 
			
		||||
    .typeText(getNthMediaAltInput(1), 'kitten 1')
 | 
			
		||||
    .click(composeButton)
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('kitten 1')
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 1).getAttribute('alt')).eql('kitten 2')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('uploads alts when deleting and re-uploading media', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .expect(mediaButton.hasAttribute('disabled')).notOk()
 | 
			
		||||
  await (uploadKittenImage(1)())
 | 
			
		||||
  await t.typeText(getNthMediaAltInput(1), 'this will be deleted')
 | 
			
		||||
    .click(getNthDeleteMediaButton(1))
 | 
			
		||||
    .expect(getNthMedia(1).exists).notOk()
 | 
			
		||||
  await (uploadKittenImage(2)())
 | 
			
		||||
  await t.expect(getNthMediaAltInput(1).value).eql('')
 | 
			
		||||
    .expect(getNthMedia(1).getAttribute('alt')).eql('kitten2.jpg')
 | 
			
		||||
    .typeText(getNthMediaAltInput(1), 'this will not be deleted')
 | 
			
		||||
    .click(composeButton)
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('this will not be deleted')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('uploads alts mixed with no-alts', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .expect(mediaButton.hasAttribute('disabled')).notOk()
 | 
			
		||||
  await uploadTwoKittens(t)
 | 
			
		||||
  await t.typeText(getNthMediaAltInput(2), 'kitten numero dos')
 | 
			
		||||
    .click(composeButton)
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('')
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 1).getAttribute('alt')).eql('kitten numero dos')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('saves alts to local storage', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .expect(mediaButton.hasAttribute('disabled')).notOk()
 | 
			
		||||
  await uploadTwoKittens(t)
 | 
			
		||||
  await t.typeText(getNthMediaAltInput(1), 'kitten numero uno')
 | 
			
		||||
    .typeText(getNthMediaAltInput(2), 'kitten numero dos')
 | 
			
		||||
    .click(notificationsNavButton)
 | 
			
		||||
    .expect(getUrl()).contains('/notifications')
 | 
			
		||||
    .click(homeNavButton)
 | 
			
		||||
    .expect(getUrl()).eql('http://localhost:4002/')
 | 
			
		||||
    .expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
 | 
			
		||||
    .expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
 | 
			
		||||
    .expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
 | 
			
		||||
    .expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
 | 
			
		||||
    .click(composeButton)
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('kitten numero uno')
 | 
			
		||||
    .expect(getNthStatusAndImage(0, 1).getAttribute('alt')).eql('kitten numero dos')
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +96,10 @@ export const uploadKittenImage = i => (exec(() => {
 | 
			
		|||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export function getNthMediaAltInput (n) {
 | 
			
		||||
  return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt input`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNthComposeReplyInput (n) {
 | 
			
		||||
  return getNthStatus(n).find('.compose-box-input')
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +132,10 @@ export function getNthStatus (n) {
 | 
			
		|||
  return $(`div[aria-hidden="false"] > article[aria-posinset="${n}"]`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNthStatusAndImage (nStatus, nImage) {
 | 
			
		||||
  return getNthStatus(nStatus).find(`.status-media .show-image-button:nth-child(${nImage + 1}) img`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getLastVisibleStatus () {
 | 
			
		||||
  return $(`div[aria-hidden="false"] > article[aria-posinset]`).nth(-1)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue