code:
import {
  closeDialogButton,
  composeModalInput,
  getNthFavorited,
  getNthStatus,
  getNthStatusContent,
  getNthStatusMedia,
  getNthStatusSensitiveMediaButton,
  getNthStatusSpoiler,
  getUrl, modalDialog,
  scrollToStatus,
  isNthStatusActive, getActiveElementRectTop, scrollToTop, isActiveStatusPinned
} from '../utils'
import { homeTimeline } from '../fixtures'
import { loginAsFoobar } from '../roles'

import { Selector as $ } from 'testcafe'

fixture`025-shortcuts-status.js`
  .page`http://localhost:4002`

async function activateStatus (t, idx) {
  let timeout = 20000
  for (let i = 0; i <= idx; i++) {
    await t.expect(getNthStatus(1 + i).exists).ok({ timeout })
      .pressKey('j')
      .expect(isNthStatusActive(1 + i)()).ok()
  }
}

test('Shortcut j/k change the active status', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .expect(isNthStatusActive(1)()).notOk()
    .pressKey('j')
    .expect(isNthStatusActive(1)()).ok()
    .pressKey('j')
    .expect(isNthStatusActive(2)()).ok()
    .pressKey('j')
    .expect(isNthStatusActive(3)()).ok()
    .pressKey('j')
    .expect(isNthStatusActive(4)()).ok()
    .pressKey('k')
    .expect(isNthStatusActive(3)()).ok()
    .pressKey('k')
    .expect(isNthStatusActive(2)()).ok()
    .pressKey('k')
    .expect(isNthStatusActive(1)()).ok()
    .expect(isNthStatusActive(2)()).notOk()
    .expect(isNthStatusActive(3)()).notOk()
    .expect(isNthStatusActive(4)()).notOk()
})

test('Shortcut j goes to the first visible status', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
  await scrollToStatus(t, 11)
  await t
    .expect(getNthStatus(11).exists).ok({ timeout: 30000 })
    .pressKey('j')
    .expect(getActiveElementRectTop()).gte(0)
})

test('Shortcut o opens active status, backspace goes back', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatus(3).exists).ok({ timeout: 30000 })
    .pressKey('j') // activates status 0
    .pressKey('j') // activates status 1
    .pressKey('j') // activates status 2
    .expect(isNthStatusActive(3)()).ok()
    .pressKey('o')
    .expect(getUrl()).contains('/statuses/')
    .pressKey('Backspace')
    .expect(isNthStatusActive(3)()).ok()
})

test('Shortcut x shows/hides spoilers', async t => {
  let idx = homeTimeline.findIndex(_ => _.spoiler === 'kitten CW')
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
  await activateStatus(t, idx)
  await t
    .expect(isNthStatusActive(1 + idx)()).ok()
    .expect(getNthStatusSpoiler(1 + idx).innerText).contains('kitten CW')
    .expect(getNthStatusContent(1 + idx).hasClass('shown')).notOk()
    .pressKey('x')
    .expect(getNthStatusContent(1 + idx).hasClass('shown')).ok()
    .pressKey('x')
    .expect(getNthStatusContent(1 + idx).hasClass('shown')).notOk()
})

test('Shortcut y shows/hides sensitive image', async t => {
  let idx = homeTimeline.findIndex(_ => _.content === "here's a secret kitten")
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
  await activateStatus(t, idx)
  await t
    .expect(isNthStatusActive(1 + idx)()).ok()
    .expect(getNthStatusSensitiveMediaButton(1 + idx).exists).ok()
    .expect(getNthStatusMedia(1 + idx).exists).notOk()
    .pressKey('y')
    .expect(getNthStatusMedia(1 + idx).exists).ok()
    .pressKey('y')
    .expect(getNthStatusMedia(1 + idx).exists).notOk()
})

test('Shortcut f toggles favorite status', async t => {
  let idx = homeTimeline.findIndex(_ => _.content === 'this is unlisted')
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatus(1 + idx).exists).ok({ timeout: 30000 })
    .expect(getNthFavorited(1 + idx)).eql('false')
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('f')
    .expect(getNthFavorited(1 + idx)).eql('true')
    .pressKey('f')
    .expect(getNthFavorited(1 + idx)).eql('false')
})

test('Shortcut p toggles profile', async t => {
  let idx = homeTimeline.findIndex(_ => _.content === 'pinned toot 1')
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatus(1 + idx).exists).ok({ timeout: 30000 })
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('p')
    .expect(getUrl()).contains('/accounts/3')
})

test('Shortcut m toggles mention', async t => {
  let idx = homeTimeline.findIndex(_ => _.content === 'pinned toot 1')
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatus(1 + idx).exists).ok({ timeout: 30000 })
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('m')
    .expect(composeModalInput.value).eql('@quux ')
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
})

test('Shortcut j/k change the active status on a thread', async t => {
  await loginAsFoobar(t)
  await t
    .click($('a').withText('quux'))
  await scrollToStatus(t, 3)
  await t
    .click(getNthStatus(3))
    .expect(getUrl()).contains('/statuses')
  await scrollToStatus(t, 1)
  await scrollToTop()
  await t
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .expect(isNthStatusActive(1)()).notOk()
    .pressKey('j')
    .expect(isNthStatusActive(1)()).ok()
    .pressKey('j')
    .expect(isNthStatusActive(2)()).ok()
    .pressKey('j')
    .expect(isNthStatusActive(3)()).ok()
    .pressKey('j')
    .expect(isNthStatusActive(4)()).ok()
    .pressKey('k')
    .expect(isNthStatusActive(3)()).ok()
    .pressKey('k')
    .expect(isNthStatusActive(2)()).ok()
    .pressKey('k')
    .expect(isNthStatusActive(1)()).ok()
    .expect(isNthStatusActive(2)()).notOk()
    .expect(isNthStatusActive(3)()).notOk()
    .expect(isNthStatusActive(4)()).notOk()
})

test('Shortcut j/k change the active status on pinned statuses', async t => {
  await loginAsFoobar(t)
  await t
    .click($('a').withText('quux'))
    .expect(getUrl()).contains('/accounts')
  await t
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .expect(isNthStatusActive(1)()).notOk()
    .pressKey('j')
    .expect(isNthStatusActive(1)()).ok()
    .expect(isActiveStatusPinned()).eql(true)
    .pressKey('j')
    .expect(isNthStatusActive(2)()).ok()
    .expect(isActiveStatusPinned()).eql(true)
    .pressKey('j')
    .expect(isNthStatusActive(1)()).ok()
    .expect(isActiveStatusPinned()).eql(false)
    .pressKey('j')
    .expect(isNthStatusActive(2)()).ok()
    .expect(isActiveStatusPinned()).eql(false)
    .pressKey('k')
    .expect(isNthStatusActive(1)()).ok()
    .expect(isActiveStatusPinned()).eql(false)
    .pressKey('k')
    .expect(isNthStatusActive(2)()).ok()
    .expect(isActiveStatusPinned()).eql(true)
    .pressKey('k')
    .expect(isNthStatusActive(1)()).ok()
    .expect(isActiveStatusPinned()).eql(true)
})


code:
import {
  closeDialogButton,
  composeModalInput,
  getNthFavorited,
  getNthStatus,
  getUrl, modalDialog, notificationsNavButton,
  isNthStatusActive, goBack
} from '../utils'
import { loginAsFoobar } from '../roles'

fixture`026-shortcuts-notification.js`
  .page`http://localhost:4002`

test('Shortcut f toggles favorite status in notification', async t => {
  let idx = 0
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(getNthStatus(1 + idx).exists).ok({ timeout: 30000 })
    .expect(getNthFavorited(1 + idx)).eql('false')
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('f')
    .expect(getNthFavorited(1 + idx)).eql('true')
    .pressKey('f')
    .expect(getNthFavorited(1 + idx)).eql('false')
})

test('Shortcut p toggles profile in a follow notification', async t => {
  let idx = 5 // "@quux followed you"
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('p')
    .expect(getUrl()).contains('/accounts/3')
  await goBack()
  await t
    .expect(isNthStatusActive(1 + idx)()).ok() // focus preserved
})

test('Shortcut m toggles mention in a follow notification', async t => {
  let idx = 5 // "@quux followed you"
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('m')
    .expect(composeModalInput.value).eql('@quux ')
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
})

test('Shortcut p refers to booster in a boost notification', async t => {
  let idx = 1 // "@admin boosted your status"
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('p')
    .expect(getUrl()).contains('/accounts/1')
})

test('Shortcut m refers to favoriter in a favorite notification', async t => {
  let idx = 0 // "@admin favorited your status"
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .pressKey('j '.repeat(idx + 1))
    .expect(isNthStatusActive(1 + idx)()).ok()
    .pressKey('m')
    .expect(composeModalInput.value).eql('@admin ')
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
})

repo_name: RayoGundead/pinny

license: mit


code:
import {
  closeDialogButton,
  composeModalInput,
  getUrl, goBack, modalDialog, notificationsNavButton
} from '../utils'
import { loginAsFoobar } from '../roles'

fixture`027-share-target.js`
  .page`http://localhost:4002`

const SHARE_URL = 'http://localhost:4002/share?' +
  'title=My+cool+title&' +
  'text=This+is+a+bit+clich%C3%A9&' +
  'url=http%3A%2F%2Fexample.com'

const SHARE_TEXT = 'My cool title This is a bit cliché http://example.com'

test('Share target works when page is not open', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo('about:blank')
    .navigateTo(SHARE_URL)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql(SHARE_TEXT)
})

test('Share target works when page is open', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo(SHARE_URL)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql(SHARE_TEXT)
})

test('Share target page replaces itself in back nav history', async t => {
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .navigateTo(SHARE_URL)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql(SHARE_TEXT)
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
  await goBack()
  await t
    .expect(getUrl()).contains('/notifications')
})

test('Share target does nothing when not logged in', async t => {
  await t
    .navigateTo(SHARE_URL)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(modalDialog.exists).notOk()
})


code:
import {
  accountProfileMoreOptionsButton,
  confirmationDialogCancelButton, getNthStatus,
  getNthStatusOptionsButton,
  modalDialog
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'

fixture`028-report-ui.js`
  .page`http://localhost:4002`

test('Can open a report UI from a status', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .click(getNthStatusOptionsButton(1))
    .click($('.modal-dialog button').withText('Report'))
    .expect(modalDialog.innerText).contains('You are reporting @quux')
    .expect(modalDialog.find('.recent-statuses').innerText).contains('pinned toot 2', {
      timeout: 30000
    })
    .click(confirmationDialogCancelButton)
    .expect(modalDialog.exists).notOk()
})

test('Can open a report UI from an account', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo('/accounts/3')
    .click(accountProfileMoreOptionsButton)
    .click($('.modal-dialog button').withText('Report'))
    .expect(modalDialog.innerText).contains('You are reporting @quux')
    .expect(modalDialog.find('.recent-statuses').innerText).contains('pinned toot 2', {
      timeout: 30000
    })
    .click(confirmationDialogCancelButton)
    .expect(modalDialog.exists).notOk()
})


code:
import {
  closeDialogButton,
  composeButton,
  composeModalInput,
  composeModalPostPrivacyButton,
  dialogOptionsOption,
  getNthStatus,
  getNthStatusMediaButton,
  getNthStatusOptionsButton, getScrollTop,
  getUrl,
  goBack,
  goForward,
  homeNavButton,
  modalDialog,
  modalDialogBackdrop,
  notificationsNavButton,
  postPrivacyDialogButtonUnlisted,
  scrollToStatus,
  sleep,
  visibleModalDialog
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
import { homeTimeline } from '../fixtures'

fixture`029-back-button-modal.js`
  .page`http://localhost:4002`

test('Back button dismisses the modal', async t => {
  await loginAsFoobar(t)
  let idx = homeTimeline.findIndex(_ => (_.content || '').includes('2 kitten photos'))
  await scrollToStatus(t, idx + 1)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .hover(getNthStatus(idx + 1))
    .click(getNthStatusMediaButton(idx + 1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
  await goBack()
  await t
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Back button dismisses a nested modal', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .hover(getNthStatus(1))
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .click(dialogOptionsOption.withText('Report'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(modalDialog.innerText).contains('Report')
    .expect(getUrl()).eql('http://localhost:4002/')
  await goBack()
  await t
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Forward and back buttons', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .hover(getNthStatus(1))
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
  await goBack()
  await t
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
  await goForward()
  await t
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Closing dialog pops history state', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).contains('/status')
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).contains('/status')
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Pressing backspace pops history state', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).contains('/status')
  await sleep(1000)
  await t
    .pressKey('backspace')
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).contains('/status')
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Pressing Esc pops history state', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).contains('/status')
  await sleep(1000)
  await t
    .pressKey('esc')
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).contains('/status')
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Clicking outside dialog pops history state', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).contains('/status')
    .click(modalDialogBackdrop, {
      offsetX: 1,
      offsetY: 1
    })
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).contains('/status')
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('Closing nested modal pops history state', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getUrl()).contains('/status')
  await sleep(1000)
  await t
    .click(dialogOptionsOption.withText('Report'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(modalDialog.innerText).contains('Report')
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).contains('/status')
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
})

test('History works correctly for nested modal', async t => {
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .click(homeNavButton)
  await scrollToStatus(t, 10)
  await t
    .click(composeButton)
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
    .click(composeModalPostPrivacyButton)
    .expect(visibleModalDialog.textContent).contains('Post privacy')
  await sleep(1000)
  await t
    .pressKey('backspace')
  await t
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
  await sleep(1000)
  await t
    .pressKey('backspace')
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
  await goBack()
  await t
    .expect(getUrl()).contains('/notifications')
})

test('History works correctly for nested modal 2', async t => {
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .click(homeNavButton)
  await scrollToStatus(t, 10)
  await t
    .click(composeButton)
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
    .click(composeModalPostPrivacyButton)
    .expect(visibleModalDialog.textContent).contains('Post privacy')
  await sleep(1000)
  await t
    .click(postPrivacyDialogButtonUnlisted)
  await t
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
    .expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Unlisted)')
  await sleep(1000)
  await goBack()
  await t
    .expect(modalDialog.exists).notOk()
    .expect(getUrl()).eql('http://localhost:4002/')
  await goBack()
  await t
    .expect(getUrl()).contains('/notifications')
})

test('History works correctly for nested modal 3', async t => {
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .click(homeNavButton)
  await scrollToStatus(t, 10)
  await t
    .click(composeButton)
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
    .click(composeModalPostPrivacyButton)
    .expect(visibleModalDialog.textContent).contains('Post privacy')
  await sleep(1000)
  await t
    .click(closeDialogButton)
  await t
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
  await sleep(1000)
  await t
    .click(closeDialogButton)
    .expect(modalDialog.exists).notOk()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
  await goBack()
  await t
    .expect(getUrl()).contains('/notifications')
})

test('History and scroll position work correctly for link in compose dialog', async t => {
  await loginAsFoobar(t)
  await scrollToStatus(t, 10)
  await t
    .expect(getScrollTop()).notEql(0)
    .click(composeButton)
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.exists).ok()
    .click($('.modal-dialog-document .compose-box-display-name'))
    .expect(getUrl()).contains('/accounts')
    .expect(getScrollTop()).eql(0)
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getScrollTop()).notEql(0)
})


code:
import {
  getMediaScrollLeft,
  getNthStatusMediaButton, getNthStatusOptionsButton,
  modalDialog, scrollToStatus, sleep
} from '../utils'
import { loginAsFoobar } from '../roles'

import { homeTimeline } from '../fixtures'

fixture`030-shortcuts-modal.js`
  .page`http://localhost:4002`

test('Backspace dismisses modal', async t => {
  await loginAsFoobar(t)
  await t
    .click(getNthStatusOptionsButton(1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
  await sleep(1000)
  await t
    .pressKey('backspace')
    .expect(modalDialog.exists).false
})

test('Backspace dismisses media modal', async t => {
  await loginAsFoobar(t)
  let idx = homeTimeline.findIndex(_ => (_.content || '').includes('2 kitten photos'))
  await scrollToStatus(t, idx + 1)
  await t
    .click(getNthStatusMediaButton(idx + 1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
  await sleep(1000)
  await t
    .pressKey('backspace')
    .expect(modalDialog.exists).false
})

test('Left/right changes active media in modal', async t => {
  await loginAsFoobar(t)
  let idx = homeTimeline.findIndex(_ => (_.content || '').includes('2 kitten photos'))
  await scrollToStatus(t, idx + 1)
  await t
    .click(getNthStatusMediaButton(idx + 1))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(getMediaScrollLeft()).eql(0)
  await sleep(1000)
  await t
    .pressKey('right')
    .expect(getMediaScrollLeft()).gt(0)
  await sleep(1000)
  await t
    .pressKey('backspace')
    .expect(modalDialog.exists).false
})


code:
import {
  accountProfileFilterMedia, accountProfileFilterStatuses,
  accountProfileFilterStatusesAndReplies,
  avatarInComposeBox,
  getNthPinnedStatus, getNthStatus,
  getUrl
} from '../utils'
import { loginAsFoobar } from '../roles'

fixture`031-account-filters.js`
  .page`http://localhost:4002`

test('Basic account filters test', async t => {
  await loginAsFoobar(t)
  await t
    .click(avatarInComposeBox)
    .expect(getUrl()).contains('/accounts/2')
    .expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
    .expect(getNthStatus(1).innerText).contains('this is unlisted')
    .click(accountProfileFilterStatusesAndReplies)
    .expect(getUrl()).contains('/accounts/2/with_replies')
    .expect(getNthPinnedStatus(1).exists).notOk()
    .expect(getNthStatus(1).innerText).contains('this is unlisted')
    .click(accountProfileFilterMedia)
    .expect(getNthPinnedStatus(1).exists).notOk()
    .expect(getNthStatus(1).innerText).contains('kitten CW')
    .click(accountProfileFilterStatuses)
    .expect(getUrl()).contains('/accounts/2')
    .expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
    .expect(getNthStatus(1).innerText).contains('this is unlisted')
})


code:
import {
  getNthDialogOptionsOption,
  getNthStatus, getNthStatusOptionsButton,
  modalDialog, sleep, visibleModalDialog
} from '../utils'
import { loginAsFoobar } from '../roles'

fixture`032-mute-dialog.js`
  .page`http://localhost:4002`

test('Can open the mute dialog twice', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getNthStatus(1).exists).ok({ timeout: 30000 })
    .hover(getNthStatus(1))
    .click(getNthStatusOptionsButton(1))
    .click(getNthDialogOptionsOption(3))
    .expect(visibleModalDialog.innerText).contains('Mute notifications')
  await sleep(500)
  await t
    .pressKey('esc')
    .expect(modalDialog.exists).notOk()
  await sleep(500)
  await t
    .click(getNthStatusOptionsButton(1))
    .click(getNthDialogOptionsOption(3))
    .expect(visibleModalDialog.innerText).contains('Mute notifications')
  await sleep(500)
  await t
    .pressKey('esc')
    .expect(modalDialog.exists).notOk()
})


code:
import {
  getUrl, notificationsTabAll, notificationsTabMentions,
  notificationsNavButton, validateTimeline
} from '../utils'
import { loginAsFoobar } from '../roles'
import { notificationsMentions, notifications } from '../fixtures'

fixture`033-notification-mentions.js`
  .page`http://localhost:4002`

test('Shows notification mentions', async t => {
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .expect(getUrl()).match(/\/notifications$/)
    .click(notificationsTabMentions)
    .expect(getUrl()).match(/\/notifications\/mentions$/)
  await validateTimeline(t, notificationsMentions)
  await t.click(notificationsTabAll)
    .expect(getUrl()).match(/\/notifications$/)
  await validateTimeline(t, notifications)
})


code:
import {
  validateTimeline, settingsNavButton, instanceSettingHomeReblogs, homeNavButton
} from '../utils'
import { loginAsFoobar } from '../roles'
import { homeTimeline } from '../fixtures'
import { Selector as $ } from 'testcafe'

fixture`034-home-timeline-filters.js`
  .page`http://localhost:4002`

test('Filters reblogs from home timeline', async t => {
  await loginAsFoobar(t)
  await t
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingHomeReblogs)
    .expect(instanceSettingHomeReblogs.checked).notOk()
    .click(homeNavButton)
  await validateTimeline(t, homeTimeline.filter(({ content }) => {
    return content !== 'pinned toot 1'
  }))
})


code:
import {
  validateTimeline,
  settingsNavButton,
  instanceSettingNotificationReblogs,
  notificationsNavButton,
  instanceSettingNotificationFavs,
  instanceSettingNotificationFollows,
  instanceSettingNotificationMentions
} from '../utils'
import { loginAsFoobar } from '../roles'
import { notifications } from '../fixtures'
import { Selector as $ } from 'testcafe'

fixture`035-notification-timeline-filters.js`
  .page`http://localhost:4002`

function setSettingAndGoToNotifications (t, setting) {
  return t.click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(setting)
    .expect(setting.checked).notOk()
    .click(notificationsNavButton)
}

test('Filters reblogs from notification timeline', async t => {
  await loginAsFoobar(t)
  await setSettingAndGoToNotifications(t, instanceSettingNotificationReblogs)
  await validateTimeline(t, notifications.filter(_ => !_.rebloggedBy))
})

test('Filters favs from notification timeline', async t => {
  await loginAsFoobar(t)
  await setSettingAndGoToNotifications(t, instanceSettingNotificationFavs)
  await validateTimeline(t, notifications.filter(_ => !_.favoritedBy))
})

test('Filters follows from notification timeline', async t => {
  await loginAsFoobar(t)
  await setSettingAndGoToNotifications(t, instanceSettingNotificationFollows)
  await validateTimeline(t, notifications.filter(_ => !_.followedBy))
})

test('Filters mentions from notification timeline', async t => {
  await loginAsFoobar(t)
  await setSettingAndGoToNotifications(t, instanceSettingNotificationMentions)
  await validateTimeline(t, notifications.filter(_ => !_.content))
})


code:
import {
  settingsNavButton,
  homeNavButton,
  disableInfiniteScroll,
  scrollToStatus,
  loadMoreButton, getFirstVisibleStatus, scrollFromStatusToStatus, sleep, getActiveElementAriaPosInSet
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'

fixture`036-disable-infinite-load.js`
  .page`http://localhost:4002`

test('Can disable loading items at bottom of timeline', async t => {
  await loginAsFoobar(t)
  await t.click(settingsNavButton)
    .click($('a').withText('General'))
    .click(disableInfiniteScroll)
    .expect(disableInfiniteScroll.checked).ok()
    .click(homeNavButton)
    .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('20')
  await scrollToStatus(t, 20)
  await t
    .click(loadMoreButton)
    .expect(getActiveElementAriaPosInSet()).eql('20')
    .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('40')
  await scrollFromStatusToStatus(t, 20, 40)
  await t
    .click(loadMoreButton)
    .expect(getActiveElementAriaPosInSet()).eql('40')
    .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47')
  await scrollFromStatusToStatus(t, 40, 47)
  await t
    .click(loadMoreButton)
  await sleep(1000)
  await t
    .expect(loadMoreButton.exists).ok()
    .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47')
})


code:
import {
  getFavoritesCount,
  getNthFavoriteButton, getNthFavorited, getNthStatus, getUrl, homeNavButton, notificationsNavButton,
  scrollToBottom, scrollToTop, sleep
} from '../utils'
import { loginAsFoobar } from '../roles'

import { homeTimeline } from '../fixtures'

fixture`100-favorite-unfavorite.js`
  .page`http://localhost:4002`

test('favorites a status', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(5))
    .expect(getNthFavorited(5)).eql('false')
    .click(getNthFavoriteButton(5))
    .expect(getNthFavorited(5)).eql('true')

  // scroll down and back up to force an unrender
  await scrollToBottom()
  await sleep(1)
  await scrollToTop()
  await t
    .hover(getNthStatus(5))
    .expect(getNthFavorited(5)).eql('true')
    .click(notificationsNavButton)
    .click(homeNavButton)
    .expect(getNthFavorited(5)).eql('true')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
    .hover(getNthStatus(5))
    .expect(getNthFavorited(5)).eql('true')
    .click(getNthFavoriteButton(5))
    .expect(getNthFavorited(5)).eql('false')
})

test('unfavorites a status', async t => {
  await loginAsFoobar(t)
  await t
    .expect(getNthFavorited(2)).eql('true')
    .click(getNthFavoriteButton(2))
    .expect(getNthFavorited(2)).eql('false')

  // scroll down and back up to force an unrender
  await scrollToBottom()
  await sleep(1)
  await scrollToTop()
  await t
    .expect(getNthFavorited(2)).eql('false')
    .click(notificationsNavButton)
    .click(homeNavButton)
    .expect(getNthFavorited(2)).eql('false')
    .click(notificationsNavButton)
    .navigateTo('/')
    .expect(getNthFavorited(2)).eql('false')
    .click(getNthFavoriteButton(2))
    .expect(getNthFavorited(2)).eql('true')
})

test('Keeps the correct favorites count', async t => {
  await loginAsFoobar(t)
  let idx = homeTimeline.findIndex(_ => _.content === 'this is unlisted')
  await t
    .hover(getNthStatus(1 + idx))
    .click(getNthFavoriteButton(1 + idx))
    .expect(getNthFavorited(1 + idx)).eql('true')
    .click(getNthStatus(1 + idx))
    .expect(getUrl()).contains('/status')
    .expect(getNthFavorited(1)).eql('true')
    .expect(getFavoritesCount()).eql(2)
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
    .hover(getNthStatus(1 + idx))
    .click(getNthFavoriteButton(1 + idx))
    .expect(getNthFavorited(1 + idx)).eql('false')
    .click(getNthStatus(1 + idx))
    .expect(getUrl()).contains('/status')
    .expect(getNthFavorited(1)).eql('false')
    .expect(getFavoritesCount()).eql(1)
})


code:
import {
  getNthReblogButton, getNthReblogged, getNthStatus, getNthStatusContent, getReblogsCount, getUrl, homeNavButton,
  notificationsNavButton, scrollToBottom, scrollToTop, sleep
} from '../utils'
import { loginAsFoobar } from '../roles'
import { postAs, reblogStatusAs } from '../serverActions'

fixture`101-reblog-unreblog.js`
  .page`http://localhost:4002`

test('reblogs a status', async t => {
  await postAs('foobar', 'hello this should be reblogged')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('should be reblogged')
    .expect(getNthReblogged(1)).eql('false')
    .click(getNthReblogButton(1))
    .expect(getNthReblogged(1)).eql('true')

  // scroll down and back up to force an unrender
  await scrollToBottom()
  await sleep(1)
  await scrollToTop()
  await t
    .hover(getNthStatus(1))
    .expect(getNthReblogged(1)).eql('true')
    .click(notificationsNavButton)
    .click(homeNavButton)
    .expect(getNthReblogged(1)).eql('true')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthReblogged(1)).eql('true')
    .click(getNthReblogButton(1))
    .expect(getNthReblogged(1)).eql('false')
})

test('unreblogs a status', async t => {
  await postAs('foobar', 'woot i wanna reblog this')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('woot i wanna')
    .expect(getNthReblogged(1)).eql('false')
    .click(getNthReblogButton(1))
    .expect(getNthReblogged(1)).eql('true')
    .click(getNthReblogButton(1))
    .expect(getNthReblogged(1)).eql('false')

  // scroll down and back up to force an unrender
  await scrollToBottom()
  await sleep(1)
  await scrollToTop()
  await t
    .hover(getNthStatus(1))
    .expect(getNthReblogged(1)).eql('false')
    .click(notificationsNavButton)
    .click(homeNavButton)
    .expect(getNthReblogged(1)).eql('false')
    .click(notificationsNavButton)
    .navigateTo('/')
    .expect(getNthReblogged(1)).eql('false')
    .click(getNthReblogButton(1))
    .expect(getNthReblogged(1)).eql('true')
})

test('Keeps the correct reblogs count', async t => {
  let { id } = await postAs('foobar', 'this will be reblogged')
  await reblogStatusAs('foobar', id)
  await reblogStatusAs('admin', id)
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('this will be reblogged')
    .expect(getNthReblogged(1)).eql('true')
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .expect(getNthReblogged(1)).eql('true')
    .expect(getReblogsCount()).eql(2)
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
    .hover(getNthStatus(1))
    .click(getNthReblogButton(1))
    .expect(getNthReblogged(1)).eql('false')
    .click(getNthStatus(1))
    .expect(getUrl()).contains('/status')
    .expect(getNthReblogged(1)).eql('false')
    .expect(getReblogsCount()).eql(1)
})


code:
import { loginAsFoobar } from '../roles'
import {
  getNthStatus, getNthStatusContent, getTitleText, getUrl, homeNavButton, notificationsNavButton
} from '../utils'
import { favoriteStatusAs, postAs } from '../serverActions'

fixture`102-notifications.js`
  .page`http://localhost:4002`

test('shows unread notification', async t => {
  let { id } = await postAs('foobar', 'somebody please favorite this to validate me')
  await loginAsFoobar(t)
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
    .expect(getTitleText()).eql('localhost:3000 · Home')
    .expect(getNthStatusContent(1).innerText).contains('somebody please favorite this to validate me', {
      timeout: 20000
    })
  await favoriteStatusAs('admin', id)
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (1 notification)', {
      timeout: 20000
    })
    .expect(getTitleText()).eql('(1) localhost:3000 · Home')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)')
    .expect(getTitleText()).eql('localhost:3000 · Notifications')
    .expect(getNthStatus(1).innerText).contains('somebody please favorite this to validate me')
    .expect(getNthStatus(1).innerText).match(/admin\s+favorited your status/)
  await t
    .click(homeNavButton)
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
    .expect(getTitleText()).eql('localhost:3000 · Home')
})

test('shows unread notifications, more than one', async t => {
  let { id } = await postAs('foobar', 'I need lots of favorites on this one')
  await loginAsFoobar(t)
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
    .expect(getTitleText()).eql('localhost:3000 · Home')
    .expect(getNthStatusContent(1).innerText).contains('I need lots of favorites on this one', {
      timeout: 20000
    })
  await favoriteStatusAs('admin', id)
  await favoriteStatusAs('quux', id)
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (2 notifications)', {
      timeout: 20000
    })
    .expect(getTitleText()).eql('(2) localhost:3000 · Home')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)')
    .expect(getTitleText()).eql('localhost:3000 · Notifications')
    .expect(getNthStatus(1).innerText).contains('I need lots of favorites on this one')
  await t
    .click(homeNavButton)
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
    .expect(getTitleText()).eql('localhost:3000 · Home')
})


code:
import { loginAsFoobar } from '../roles'
import {
  composeInput, getNthComposeReplyButton, getNthComposeReplyInput, getNthReplyButton, getNthStatus, getUrl,
  homeNavButton, notificationsNavButton,
  postStatusButton
} from '../utils'

fixture`103-compose.js`
  .page`http://localhost:4002`

test('statuses show up in home timeline', async t => {
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, 'hello world', { paste: true })
    .click(postStatusButton)
    .expect(getNthStatus(1).innerText).contains('hello world')
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatus(1).innerText).contains('hello world')
    .navigateTo('/')
    .expect(getNthStatus(1).innerText).contains('hello world')
})

test('statuses in threads show up in right order', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo('/accounts/5')
    .click(getNthStatus(3))
    .expect(getUrl()).contains('/statuses')
    .click(getNthReplyButton(4))
    .typeText(getNthComposeReplyInput(4), 'my reply!', { paste: true })
    .click(getNthComposeReplyButton(4))
    .expect(getNthComposeReplyInput(4).exists).notOk()
    .expect(getNthStatus(6).innerText).contains('@baz my reply!')
    .navigateTo('/accounts/5')
    .click(getNthStatus(3))
    .expect(getNthStatus(6).innerText).contains('@baz my reply!')
})


code:
import { loginAsFoobar } from '../roles'
import {
  getNthStatus, scrollToTop, showMoreButton, sleep
} from '../utils'
import { postAs } from '../serverActions'

fixture`104-streaming.js`
  .page`http://localhost:4002`

test('new incoming statuses show up immediately', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
  await postAs('admin', 'hello my baby hello my honey')
  await t.expect(getNthStatus(1).innerText).contains('hello my baby hello my honey')
})

test('new incoming toots show a button if scrolled down', async t => {
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .hover(getNthStatus(3))
    .hover(getNthStatus(5))
  await sleep(1000)
  await postAs('admin', 'hello my ragtime gal')
  await postAs('admin', 'send me a kiss by wire')
  await sleep(4000)
  await t.hover(getNthStatus(3))
    .hover(getNthStatus(1))
  await scrollToTop()
  await sleep(1000)
  await t
    .expect(showMoreButton.innerText).contains('Show 2 more')
  await postAs('admin', "baby my heart's on fire")
  await sleep(4000)
  await t.expect(showMoreButton.innerText).contains('Show 3 more')
    .click(showMoreButton)
  await t
    .expect(getNthStatus(1).innerText).contains("baby my heart's on fire")
    .expect(getNthStatus(2).innerText).contains('send me a kiss by wire')
    .expect(getNthStatus(3).innerText).contains('hello my ragtime gal')
    .navigateTo('/')
    .expect(getNthStatus(1).innerText).contains("baby my heart's on fire")
    .expect(getNthStatus(2).innerText).contains('send me a kiss by wire')
    .expect(getNthStatus(3).innerText).contains('hello my ragtime gal')
})


code:
import { loginAsFoobar } from '../roles'
import {
  clickToNotificationsAndBackHome, forceOffline, forceOnline, getNthStatus, getUrl, homeNavButton,
  notificationsNavButton
} from '../utils'
import { deleteAs, postAs, postReplyAs } from '../serverActions'

fixture`105-deletes.js`
  .page`http://localhost:4002`

test('deleted statuses are removed from the timeline', async t => {
  let timeout = 20000
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
  let status = await postAs('admin', "I'm gonna delete this")
  await t.expect(getNthStatus(1).innerText).contains("I'm gonna delete this", { timeout })
  await deleteAs('admin', status.id)
  await t.expect(getNthStatus(1).innerText).notContains("I'm gonna delete this", { timeout })
  await clickToNotificationsAndBackHome(t)
  await t.expect(getNthStatus(1).innerText).notContains("I'm gonna delete this", { timeout })
  await t.navigateTo('/notifications')
  await forceOffline()
  await t.click(homeNavButton)
  await t.expect(getNthStatus(1).innerText).notContains("I'm gonna delete this", { timeout })
  await forceOnline()
  await t
    .navigateTo('/')
    .expect(getNthStatus(1).innerText).notContains("I'm gonna delete this", { timeout })
})

test('deleted statuses are removed from threads', async t => {
  let timeout = 20000
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
  let status = await postAs('admin', "I won't delete this")
  let reply = await postReplyAs('admin', 'But I will delete this', status.id)
  await t.expect(getNthStatus(1).innerText).contains('But I will delete this', { timeout })
    .expect(getNthStatus(2).innerText).contains("I won't delete this", { timeout })
    .click(getNthStatus(2))
    .expect(getUrl()).contains('/statuses')
    .expect(getNthStatus(1).innerText).contains("I won't delete this", { timeout })
    .expect(getNthStatus(2).innerText).contains('But I will delete this', { timeout })
  await deleteAs('admin', reply.id)
  await t.expect(getNthStatus(2).exists).notOk()
    .expect(getNthStatus(1).innerText).contains("I won't delete this", { timeout })
  await t.navigateTo('/')
  await forceOffline()
  await t.click(getNthStatus(1))
    .expect(getUrl()).contains('/statuses')
    .expect(getNthStatus(2).exists).notOk()
    .expect(getNthStatus(1).innerText).contains("I won't delete this", { timeout })
  await forceOnline()
})

test('deleted statuses result in deleted notifications', async t => {
  let timeout = 20000
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
  let status = await postAs('admin', "@foobar yo yo foobar what's up")
  await t.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (1 notification)', { timeout })
  await deleteAs('admin', status.id)
  await t.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications', { timeout })
})


code:
import { loginAsFoobar } from '../roles'
import {
  accountProfileFollowButton,
  getNthStatus,
  sleep
} from '../utils'
import {
  authorizeFollowRequestAs, getFollowRequestsAs
} from '../serverActions'

fixture`106-follow-requests.js`
  .page`http://localhost:4002`

test('can request to follow an account', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo('/accounts/6')
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow (follow requested)')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow (follow requested)')

  let requests = await getFollowRequestsAs('LockedAccount')
  await authorizeFollowRequestAs('LockedAccount', requests.slice(-1)[0].id)

  await sleep(2000)

  await t.navigateTo('/accounts/6')
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
    .expect(getNthStatus(1).innerText).contains('This account is locked')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
})


code:
import { loginAsFoobar } from '../roles'
import {
  getNthStatus, homeNavButton, localTimelineNavButton, sleep
} from '../utils'
import {
  postAs
} from '../serverActions'

fixture`107-streaming-gap.js`
  .page`http://localhost:4002`

test('fills in a status posted while away from timeline', async t => {
  let timeout = 30000

  await loginAsFoobar(t)
  await t
    .click(localTimelineNavButton)
    .expect(getNthStatus(1).exists).ok({ timeout })
    .hover(getNthStatus(1))
  await postAs('admin', 'heyo')
  await t.expect(getNthStatus(1).innerText).contains('heyo', { timeout })
    .click(homeNavButton)
    .hover(getNthStatus(1))
  await postAs('admin', 'posted this while you were away!')
  await t.expect(getNthStatus(1).innerText).contains('posted this while you were away!', { timeout })
    .click(localTimelineNavButton)
    .expect(getNthStatus(1).innerText).contains('posted this while you were away!', { timeout })
    .expect(getNthStatus(2).innerText).contains('heyo', { timeout })
  await sleep(5000)
  await postAs('admin', 'posted this while you were watching')
  await t.expect(getNthStatus(1).innerText).contains('posted this while you were watching', { timeout })
})


code:
import {
  composeButton,
  getNthStatus,
  scrollToStatus,
  modalDialog,
  sleep,
  notificationsNavButton,
  getUrl,
  getNthStatusSelector,
  composeModalEmojiButton,
  composeModalInput,
  composeModalComposeButton, emojiSearchInput
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'

fixture`108-compose-dialog.js`
  .page`http://localhost:4002`

test('can compose using a dialog', async t => {
  await loginAsFoobar(t)
  await scrollToStatus(t, 16)
  await t.expect(modalDialog.exists).notOk()
    .expect(composeButton.getAttribute('aria-label')).eql('Compose')
  await sleep(2000)
  await t.click(composeButton)
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .typeText(composeModalInput, 'hello from the modal')
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .navigateTo('/')
    .hover(getNthStatus(1))
    .expect(getNthStatus(1).innerText).contains('hello from the modal', { timeout: 20000 })
})

test('can use emoji dialog within compose dialog', async t => {
  await loginAsFoobar(t)
  await scrollToStatus(t, 16)
  await t.expect(composeButton.getAttribute('aria-label')).eql('Compose')
  await sleep(2000)
  await t.click(composeButton)
    .click(composeModalEmojiButton)
    .typeText(emojiSearchInput, 'blobpats')
    .pressKey('enter')
    .expect(composeModalInput.value).eql(':blobpats: ')
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .navigateTo('/')
  await t.expect($(`${getNthStatusSelector(1)} img[alt=":blobpats:"]`).exists).ok({ timeout: 20000 })
})


code:
import {
  composeButton, composeInput, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, getNthStatusAndImage, getUrl,
  homeNavButton,
  mediaButton, notificationsNavButton,
  uploadKittenImage
} from '../utils'
import { loginAsFoobar } 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 loginAsFoobar(t)
  await t
    .expect(mediaButton.hasAttribute('disabled')).notOk()
  await uploadTwoKittens(t)
  await t.typeText(getNthMediaAltInput(2), 'kitten 2')
    .typeText(getNthMediaAltInput(1), 'kitten 1')
    .click(composeButton)
    .expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('kitten 1')
    .expect(getNthStatusAndImage(1, 0).getAttribute('title')).eql('kitten 1')
    .expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 2')
    .expect(getNthStatusAndImage(1, 1).getAttribute('title')).eql('kitten 2')
})

test('uploads alts when deleting and re-uploading media', async t => {
  await loginAsFoobar(t)
  await t
    .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(1, 0).getAttribute('alt')).eql('this will not be deleted')
})

test('uploads alts mixed with no-alts', async t => {
  await loginAsFoobar(t)
  await t
    .expect(mediaButton.hasAttribute('disabled')).notOk()
  await uploadTwoKittens(t)
  await t.typeText(getNthMediaAltInput(2), 'kitten numero dos')
    .click(composeButton)
    .expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('')
    .expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero dos')
})

test('saves alts to local storage', async t => {
  await loginAsFoobar(t)
  await t
    .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(1, 0).getAttribute('alt')).eql('kitten numero uno')
    .expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero dos')
})

test('can post a status with empty content if there is media', async t => {
  await loginAsFoobar(t)
  await t
    .expect(mediaButton.hasAttribute('disabled')).notOk()
    .typeText(composeInput, 'this is a toot')
  await (uploadKittenImage(1)())
  await t
    .typeText(getNthMediaAltInput(1), 'just an image!')
  await t.click(composeButton)
    .expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('just an image!')
})


code:
import {
  composeButton, composeContentWarning, composeInput, contentWarningButton,
  getNthShowOrHideButton, getNthStatus, getNthStatusSelector
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'

fixture`110-compose-content-warnings.js`
  .page`http://localhost:4002`

test('content warnings are posted', async t => {
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, 'hello this is a toot', { paste: true })
    .click(contentWarningButton)
    .typeText(composeContentWarning, 'CW', { paste: true })
    .click(composeButton)
    .expect($(`${getNthStatusSelector(1)} .status-spoiler`).innerText).contains('CW', { timeout: 30000 })
    .click(getNthShowOrHideButton(1))
    .expect($(`${getNthStatusSelector(1)} .status-content`).innerText).contains('hello this is a toot')
    .click(getNthShowOrHideButton(1))
    .expect(getNthStatus(1).innerText).notContains('hello this is a toot')
})

test('content warnings are not posted if removed', async t => {
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, 'hi this is another toot', { paste: true })
    .click(contentWarningButton)
    .typeText(composeContentWarning, 'content warning!', { paste: true })
    .click(contentWarningButton)
    .expect(composeContentWarning.exists).notOk()
    .click(composeButton)
    .expect(getNthStatus(1).innerText).contains('hi this is another toot', { timeout: 30000 })
    .expect(getNthStatus(1).innerText).notContains('content warning!')
    .expect($(`${getNthStatusSelector(1)} .status-content`).innerText).contains('hi this is another toot')
})

test('content warnings can have emoji', async t => {
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, 'I can: :blobnom:')
    .click(contentWarningButton)
    .typeText(composeContentWarning, 'can you feel the :blobpats: tonight')
    .click(composeButton)
    .expect(getNthStatus(1).innerText).contains('can you feel the', { timeout: 30000 })
    .expect($(`${getNthStatusSelector(1)} .status-spoiler img.inline-custom-emoji`).getAttribute('alt')).eql(':blobpats:')
    .click(getNthShowOrHideButton(1))
    .expect($(`${getNthStatusSelector(1)} .status-content img.inline-custom-emoji`).getAttribute('alt')).eql(':blobnom:')
})

test('no XSS in content warnings or text', async t => {
  let pwned1 = `<script>alert("pwned!")</script>`
  let pwned2 = `<script>alert("pwned from CW!")</script>`
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, pwned1)
    .click(contentWarningButton)
    .typeText(composeContentWarning, pwned2)
    .click(composeButton)
    .expect($(`${getNthStatusSelector(1)} .status-spoiler`).innerText).contains(pwned2)
    .click(getNthShowOrHideButton(1))
    .expect($(`${getNthStatusSelector(1)} .status-content`).innerText).contains(pwned1)
})


code:
import {
  composeInput, getActiveElementClassList,
  getNthComposeReplyButton,
  getNthComposeReplyInput, getNthReplyButton,
  getNthStatusSelector
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'

fixture`111-focus.js`
  .page`http://localhost:4002`

test('replying to a toot returns focus to reply button', async t => {
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, 'I would like, if I may, to take you on a strange journey', { paste: true })
    .pressKey('ctrl+enter')
    .expect($(`${getNthStatusSelector(1)} .status-content`).innerText).contains('I would like, if I may, to take you on a strange journey')
    .click(getNthReplyButton(1))
    .typeText(getNthComposeReplyInput(1), 'How strange was it?', { paste: true })
    .click(getNthComposeReplyButton(1))
    .expect(getActiveElementClassList()).contains('status-toolbar-reply-button', { timeout: 20000 })
})


code:
import {
  composeButton,
  composeInput,
  getNthStatus, getNthStatusSelector
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'

fixture`112-status-links.js`
  .page`http://localhost:4002`

test('External links, hashtags, and mentions have correct attributes', async t => {
  let text = 'Why hello there @admin and @baz and @quux ' +
    'and also #tag and #anotherTag and #yetAnotherTag ' +
    'and also http://example.com and https://joinmastodon.org and ' +
    'https://mastodon.social.'

  const nthAnchor = n => $(`${getNthStatusSelector(1)} .status-content a`).nth(n)

  await loginAsFoobar(t)
  await t
    .typeText(composeInput, text, { paste: true })
    .click(composeButton)
    .expect(getNthStatus(1).innerText).contains('Why hello there', { timeout: 20000 })
    .expect(nthAnchor(0).getAttribute('href')).eql('/accounts/1')
    .expect(nthAnchor(0).hasAttribute('rel')).notOk()
    .expect(nthAnchor(0).getAttribute('title')).eql('@admin')
    .expect(nthAnchor(0).hasAttribute('target')).notOk()
    .expect(nthAnchor(1).getAttribute('href')).eql('/accounts/5')
    .expect(nthAnchor(1).hasAttribute('rel')).notOk()
    .expect(nthAnchor(1).getAttribute('title')).eql('@baz')
    .expect(nthAnchor(1).hasAttribute('target')).notOk()
    .expect(nthAnchor(2).getAttribute('href')).eql('/accounts/3')
    .expect(nthAnchor(2).hasAttribute('rel')).notOk()
    .expect(nthAnchor(2).getAttribute('title')).eql('@quux')
    .expect(nthAnchor(2).hasAttribute('target')).notOk()
    .expect(nthAnchor(3).getAttribute('href')).eql('/tags/tag')
    .expect(nthAnchor(3).hasAttribute('rel')).notOk()
    .expect(nthAnchor(3).hasAttribute('target')).notOk()
    .expect(nthAnchor(4).getAttribute('href')).eql('/tags/anothertag')
    .expect(nthAnchor(4).hasAttribute('rel')).notOk()
    .expect(nthAnchor(4).hasAttribute('target')).notOk()
    .expect(nthAnchor(5).getAttribute('href')).eql('/tags/yetanothertag')
    .expect(nthAnchor(5).hasAttribute('rel')).notOk()
    .expect(nthAnchor(5).hasAttribute('target')).notOk()
    .expect(nthAnchor(6).getAttribute('href')).eql('http://example.com')
    .expect(nthAnchor(6).getAttribute('rel')).eql('nofollow noopener')
    .expect(nthAnchor(6).getAttribute('title')).eql('http://example.com')
    .expect(nthAnchor(6).getAttribute('target')).eql('_blank')
    .expect(nthAnchor(7).getAttribute('href')).eql('https://joinmastodon.org')
    .expect(nthAnchor(7).getAttribute('rel')).eql('nofollow noopener')
    .expect(nthAnchor(7).getAttribute('title')).eql('https://joinmastodon.org')
    .expect(nthAnchor(7).getAttribute('target')).eql('_blank')
    .expect(nthAnchor(8).getAttribute('href')).eql('https://mastodon.social')
    .expect(nthAnchor(8).getAttribute('rel')).eql('nofollow noopener')
    .expect(nthAnchor(8).getAttribute('title')).eql('https://mastodon.social')
    .expect(nthAnchor(8).getAttribute('target')).eql('_blank')
})


code:
import {
  accountProfileFollowButton,
  accountProfileFollowedBy, accountProfileMoreOptionsButton, communityNavButton, getNthSearchResult,
  getNthStatus, getNthStatusOptionsButton, getNthDialogOptionsOption, getUrl, modalDialog
} from '../utils'
import { Selector as $ } from 'testcafe'
import { loginAsFoobar } from '../roles'
import { postAs } from '../serverActions'

fixture`113-block-unblock.js`
  .page`http://localhost:4002`

test('Can block and unblock an account from a status', async t => {
  let post = 'a very silly statement that should probably get me blocked'
  await postAs('admin', post)
  await loginAsFoobar(t)
  await t
    .expect(getNthStatus(1).innerText).contains(post, { timeout: 30000 })
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(1).innerText).contains('Unfollow @admin')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin')
    .click(getNthDialogOptionsOption(2))
    .expect(modalDialog.exists).notOk()
    .click(communityNavButton)
    .click($('a[href="/blocked"]'))
    .expect(getNthSearchResult(1).innerText).contains('@admin')
    .click(getNthSearchResult(1))
    .expect(getUrl()).contains('/accounts/1')
    .expect(accountProfileFollowedBy.innerText).match(/blocked/i)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unblock')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowedBy.innerText).contains('')
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
})

test('Can block and unblock an account from the account profile page', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo('/accounts/5')
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileMoreOptionsButton)
    .expect(getNthDialogOptionsOption(1).innerText).contains('Mention @baz')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Follow @baz')
    .expect(getNthDialogOptionsOption(3).innerText).contains('Block @baz')
    .click(getNthDialogOptionsOption(3))
    .expect(accountProfileFollowedBy.innerText).match(/blocked/i)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unblock')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowedBy.innerText).contains('')
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
    .click(accountProfileFollowButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
})


code:
import {
  accountProfileFollowButton,
  accountProfileMoreOptionsButton,
  communityNavButton,
  getNthSearchResult,
  getNthStatus,
  getNthStatusOptionsButton,
  getNthDialogOptionsOption,
  getUrl,
  modalDialog,
  closeDialogButton,
  confirmationDialogOKButton, sleep
} from '../utils'
import { Selector as $ } from 'testcafe'
import { loginAsFoobar } from '../roles'
import { postAs } from '../serverActions'

fixture`114-mute-unmute.js`
  .page`http://localhost:4002`

test('Can mute and unmute an account', async t => {
  await loginAsFoobar(t)
  let post = 'blah blah blah'
  await postAs('admin', post)

  await t.expect(getNthStatus(1).innerText).contains(post, { timeout: 20000 })
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(1).innerText).contains('Unfollow @admin')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin')
    .expect(getNthDialogOptionsOption(3).innerText).contains('Mute @admin')
  await sleep(1000)
  await t
    .click(getNthDialogOptionsOption(3))
  await sleep(1000)
  await t
    .click(confirmationDialogOKButton)
    .expect(modalDialog.exists).notOk()
  await sleep(1000)
  await t
    .click(communityNavButton)
    .click($('a[href="/muted"]'))
    .expect(getNthSearchResult(1).innerText).contains('@admin')
    .click(getNthSearchResult(1))
    .expect(getUrl()).contains('/accounts/1')
    .click(accountProfileMoreOptionsButton)
    .expect(getNthDialogOptionsOption(1).innerText).contains('Mention @admin')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @admin')
    .expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin')
    .expect(getNthDialogOptionsOption(4).innerText).contains('Unmute @admin')
  await sleep(1000)
  await t
    .click(getNthDialogOptionsOption(4))
  await sleep(1000)
  await t
    .click(accountProfileMoreOptionsButton)
    .expect(getNthDialogOptionsOption(1).innerText).contains('Mention @admin')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @admin')
    .expect(getNthDialogOptionsOption(3).innerText).contains('Block @admin')
    .expect(getNthDialogOptionsOption(4).innerText).contains('Mute @admin')
  await sleep(1000)
  await t
    .click(closeDialogButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
})


code:
import {
  accountProfileFollowButton,
  accountProfileMoreOptionsButton, closeDialogButton,
  getNthDialogOptionsOption
} from '../utils'
import { loginAsFoobar } from '../roles'

fixture`115-follow-unfollow.js`
  .page`http://localhost:4002`

test('Can follow and unfollow an account from the profile page', async t => {
  await loginAsFoobar(t)
  await t
    .navigateTo('/accounts/5')
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileMoreOptionsButton)
    .expect(getNthDialogOptionsOption(1).innerText).contains('Mention @baz')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Follow @baz')
    .click(getNthDialogOptionsOption(2))
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
    .click(accountProfileMoreOptionsButton)
    .expect(getNthDialogOptionsOption(2).innerText).contains('Unfollow @baz')
    .click(getNthDialogOptionsOption(2))
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
    .click(accountProfileMoreOptionsButton)
    .expect(getNthDialogOptionsOption(2).innerText).contains('Follow @baz')
    .click(closeDialogButton)
    .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
})


code:
import { loginAsLockedAccount } from '../roles'
import { followAs, unfollowAs } from '../serverActions'
import {
  avatarInComposeBox,
  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 loginAsLockedAccount(t)

  // 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(avatarInComposeBox)
    .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()
})


code:
import { loginAsFoobar } from '../roles'
import {
  avatarInComposeBox, closeDialogButton, composeInput, getNthDialogOptionsOption, getNthPinnedStatus,
  getNthPinnedStatusFavoriteButton,
  getNthStatus, getNthStatusContent,
  getNthStatusOptionsButton, getUrl, homeNavButton, postStatusButton, scrollToTop, scrollToBottom,
  settingsNavButton, sleep
} from '../utils'
import { users } from '../users'
import { postAs } from '../serverActions'

fixture`117-pin-unpin.js`
  .page`http://localhost:4002`

test('Can pin statuses', async t => {
  await loginAsFoobar(t)
  await t
    .typeText(composeInput, 'I am going to pin this', { paste: true })
    .click(postStatusButton)
    .expect(getNthStatus(1).innerText).contains('I am going to pin this')
    .click(avatarInComposeBox)
    .expect(getUrl()).contains(`/accounts/${users.foobar.id}`)
    .expect(getNthPinnedStatus(1).getAttribute('aria-setsize')).eql('1')
    .expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
    .expect(getNthStatus(1).innerText).contains('I am going to pin this')
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(1).innerText).contains('Delete')
    .expect(getNthDialogOptionsOption(2).innerText).contains('Pin to profile')
    .click(getNthDialogOptionsOption(2))
    .expect(getNthPinnedStatus(1).getAttribute('aria-setsize')).eql('2')
    .expect(getNthPinnedStatus(1).innerText).contains('I am going to pin this')
    .expect(getNthPinnedStatus(2).innerText).contains('this is unlisted')
    .expect(getNthStatus(1).innerText).contains('I am going to pin this')
    .click(getNthStatusOptionsButton(1))
    .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(1).getAttribute('aria-setsize')).eql('1')
    .expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
    .expect(getNthStatus(1).innerText).contains('I am going to pin this')
})

test('Can favorite a pinned status', async t => {
  await loginAsFoobar(t)
  await t
    .click(avatarInComposeBox)
    .expect(getNthPinnedStatus(1).getAttribute('aria-setsize')).eql('1')
    .expect(getNthPinnedStatusFavoriteButton(1).getAttribute('aria-pressed')).eql('false')
    .click(getNthPinnedStatusFavoriteButton(1))
    .expect(getNthPinnedStatusFavoriteButton(1).getAttribute('aria-pressed')).eql('true')
    .click(getNthPinnedStatusFavoriteButton(1))
    .expect(getNthPinnedStatusFavoriteButton(1).getAttribute('aria-pressed')).eql('false')
})

test('Saved pinned/unpinned state of status', async t => {
  const timeout = 20000
  await postAs('foobar', 'hey I am going to pin and unpin this')
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('hey I am going to pin and unpin this', { timeout })
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(2).innerText).contains('Pin to profile')
    .click(getNthDialogOptionsOption(2))
  await sleep(1)
  await t
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile')
    .click(closeDialogButton)

  // scroll down and back up to force an unrender
  await scrollToBottom()
  await sleep(1)
  await scrollToTop()

  await t
    .expect(getNthStatusContent(1).innerText).contains('hey I am going to pin and unpin this', { timeout })
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile', { timeout })
    // navigate to another page and back to force another unrender
    .click(settingsNavButton)
    .click(homeNavButton)
    .expect(getNthStatusContent(1).innerText).contains('hey I am going to pin and unpin this', { timeout })
    .click(getNthStatusOptionsButton(1))
    .expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile', { timeout })
})


code:
import { loginAsFoobar } from '../roles'
import {
  displayNameInComposeBox,
  generalSettingsButton,
  getNthStatus,
  getNthStatusSelector,
  getUrl,
  homeNavButton,
  removeEmojiFromDisplayNamesInput,
  settingsNavButton,
  sleep
} from '../utils'
import { postAs, updateUserDisplayNameAs } from '../serverActions'
import { Selector as $ } from 'testcafe'

const rainbow = String.fromCodePoint(0x1F308)

fixture`118-display-name-custom-emoji.js`
  .page`http://localhost:4002`

test('Can put custom emoji in display name', async t => {
  await updateUserDisplayNameAs('foobar', 'foobar :blobpats:')
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .expect(displayNameInComposeBox.innerText).match(/\s*foobar\s*/)
    .expect($('.compose-box-display-name img').getAttribute('alt')).eql(':blobpats:')
    .click(displayNameInComposeBox)
    .expect(getUrl()).contains('/accounts/2')
    .expect($(`${getNthStatusSelector(1)} .status-author-name img`).getAttribute('alt')).eql(':blobpats:')
})

test('Cannot XSS using display name HTML', async t => {
  await updateUserDisplayNameAs('foobar', '<script>alert("pwn")</script>')
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .expect(displayNameInComposeBox.innerText).eql('<script>alert("pwn")</script>')
})

test('Can remove emoji from user display names', async t => {
  await updateUserDisplayNameAs('foobar', `${rainbow} foo :blobpats: ${rainbow}`)
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .expect($('.compose-box-display-name img').exists).ok()
    .expect(displayNameInComposeBox.innerText).eql(`${rainbow} foo  ${rainbow}`)
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).ok()
    .click(homeNavButton)
    .expect($('.compose-box-display-name img').exists).notOk()
    .expect(displayNameInComposeBox.innerText).eql('foo')
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).notOk()
    .click(homeNavButton)
    .expect($('.compose-box-display-name img').exists).ok()
    .expect(displayNameInComposeBox.innerText).eql(`${rainbow} foo  ${rainbow}`)
})

test('Cannot remove emoji from user display names if result would be empty', async t => {
  await updateUserDisplayNameAs('foobar', `${rainbow} :blobpats: ${rainbow}`)
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .expect(displayNameInComposeBox.innerText).eql(`${rainbow}  ${rainbow}`)
    .expect($('.compose-box-display-name img').exists).ok()
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).ok()
    .click(homeNavButton)
    .expect(displayNameInComposeBox.innerText).eql(`${rainbow}  ${rainbow}`)
    .expect($('.compose-box-display-name img').exists).ok()
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).notOk()
    .click(homeNavButton)
    .expect(displayNameInComposeBox.innerText).eql(`${rainbow}  ${rainbow}`)
    .expect($('.compose-box-display-name img').exists).ok()
})

test('Check status aria labels for de-emojified text', async t => {
  await updateUserDisplayNameAs('foobar', `${rainbow} foo :blobpats: ${rainbow}`)
  await postAs('foobar', 'hey ho lotsa emojos')
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .click(displayNameInComposeBox)
    .expect(getNthStatus(1).getAttribute('aria-label')).match(
      new RegExp(`${rainbow} foo :blobpats: ${rainbow}, hey ho lotsa emojos, (.* ago|just now), @foobar, Public`, 'i')
    )
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).ok()
    .click(homeNavButton)
    .click(displayNameInComposeBox)
    .expect(getNthStatus(1).getAttribute('aria-label')).match(
      new RegExp(`foo, hey ho lotsa emojos, (.* ago|just now), @foobar, Public`, 'i')
    )
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).notOk()
    .click(homeNavButton)
    .click(displayNameInComposeBox)
    .expect(getNthStatus(1).getAttribute('aria-label')).match(
      new RegExp(`${rainbow} foo :blobpats: ${rainbow}, hey ho lotsa emojos, (.* ago|just now), @foobar, Public`, 'i')
    )
})

test('Check some odd emoji', async t => {
  await updateUserDisplayNameAs('foobar', 'foo 🕹📺')
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .expect(displayNameInComposeBox.innerText).eql('foo 🕹📺')
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).ok()
    .click(homeNavButton)
    .expect(displayNameInComposeBox.innerText).eql('foo')
    .click(settingsNavButton)
    .click(generalSettingsButton)
    .click(removeEmojiFromDisplayNamesInput)
    .expect(removeEmojiFromDisplayNamesInput.checked).notOk()
    .click(homeNavButton)
    .expect(displayNameInComposeBox.innerText).eql('foo 🕹📺')

  // clean up after all these tests are done
  await updateUserDisplayNameAs('foobar', 'foobar')
})


code:
import { loginAsFoobar } from '../roles'
import {
  getFavoritesCount,
  getNthStatus, getNthStatusContent, getReblogsCount, homeNavButton
} from '../utils'
import { favoriteStatusAs, postAs, reblogStatusAs } from '../serverActions'

fixture`119-status-counts-update.js`
  .page`http://localhost:4002`

test('Fav stats update', async t => {
  let status = await postAs('foobar', 'hey hello look at this toot')
  let statusId = status.id
  await favoriteStatusAs('admin', statusId)
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('hey hello look at this toot')
    .click(getNthStatus(1))
    .expect(getFavoritesCount()).eql(1)
    .click(homeNavButton)
  await favoriteStatusAs('quux', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getFavoritesCount()).eql(2)
    .click(homeNavButton)
  await favoriteStatusAs('baz', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getFavoritesCount()).eql(3)
    .click(homeNavButton)
  await favoriteStatusAs('LockedAccount', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getFavoritesCount()).eql(4)
})

test('Reblog stats update', async t => {
  let status = await postAs('foobar', 'oh why hello it looks like another toot')
  let statusId = status.id
  await reblogStatusAs('admin', statusId)
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('oh why hello it looks like another toot')
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(1)
    .click(homeNavButton)
  await reblogStatusAs('quux', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(2)
    .click(homeNavButton)
  await reblogStatusAs('baz', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(3)
    .click(homeNavButton)
  await reblogStatusAs('ExternalLinks', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(4)
})

test('Fav and reblog stats update for a boosted toot', async t => {
  let status = await postAs('ExternalLinks', 'this will get boosted')
  let statusId = status.id
  await reblogStatusAs('admin', statusId)
  await favoriteStatusAs('admin', statusId)
  await favoriteStatusAs('quux', statusId)
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('this will get boosted')
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(1)
    .expect(getFavoritesCount()).eql(2)
    .click(homeNavButton)
  await favoriteStatusAs('baz', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(1)
    .expect(getFavoritesCount()).eql(3)
    .click(homeNavButton)
  await favoriteStatusAs('LockedAccount', statusId)
  await t
    .click(getNthStatus(1))
    .expect(getReblogsCount()).eql(1)
    .expect(getFavoritesCount()).eql(4)
})


code:
import { loginAsFoobar } from '../roles'
import { getNthStatus } from '../utils'
import { postAs, postEmptyStatusWithMediaAs } from '../serverActions'

fixture`120-status-aria-label.js`
  .page`http://localhost:4002`

test('aria-labels for statuses with no content text', async t => {
  await postEmptyStatusWithMediaAs('foobar', 'kitten1.jpg', 'kitteh')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatus(1).getAttribute('aria-label')).match(
      /foobar, (.+ ago|just now), @foobar, Public/i
    )
})

test('aria-labels with links', async t => {
  let content = 'Text here\n\nMore text\n\n' +
    'https://example.com/long/url/here/very/long/yes/so/long\n\n' +
    '#woot #yolo'

  let expected = 'admin, Text here More text ' +
    'https://example.com/long/url/here/very/long/yes/so/long #woot #yolo,'

  await postAs('admin', content)

  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatus(1).getAttribute('aria-label')).contains(expected)
})


code:
import { loginAsFoobar } from '../roles'
import {
  composeModalComposeButton,
  getNthStatus,
  getNthStatusContent,
  getNthStatusOptionsButton,
  modalDialog,
  composeModalInput,
  getNthStatusMediaImg,
  composeModalPostPrivacyButton,
  getComposeModalNthMediaImg,
  getComposeModalNthMediaAltInput,
  getNthStatusSpoiler,
  composeModalContentWarningInput,
  dialogOptionsOption,
  getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl, sleep
} from '../utils'
import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions'

fixture`121-delete-and-redraft.js`
  .page`http://localhost:4002`

test('basic delete and redraft', async t => {
  await postAs('foobar', 'hey ho this is grate')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('hey ho this is grate')
    .click(getNthStatusOptionsButton(1))
    .click(dialogOptionsOption.withText('Delete and redraft'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).contains('hey ho this is grate')
    .expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
    .typeText(composeModalInput, 'hey ho this is great', { replace: true, paste: true })
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusContent(1).innerText).contains('hey ho this is great')
})

test('image with empty text delete and redraft', async t => {
  await postEmptyStatusWithMediaAs('foobar', 'kitten2.jpg', 'what a kitteh')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusMediaImg(1).getAttribute('alt')).eql('what a kitteh')
    .click(getNthStatusOptionsButton(1))
    .click(dialogOptionsOption.withText('Delete and redraft'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql('')
    .expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
    .expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('what a kitteh')
    .expect(getComposeModalNthMediaAltInput(1).value).eql('what a kitteh')
    .typeText(composeModalInput, 'I love this kitteh', { replace: true, paste: true })
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusContent(1).innerText).contains('I love this kitteh')
    .expect(getNthStatusMediaImg(1).getAttribute('alt')).eql('what a kitteh')
})

test('image with no alt delete and redraft', async t => {
  await postEmptyStatusWithMediaAs('foobar', 'kitten3.jpg', '')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusMediaImg(1).getAttribute('alt')).eql('')
    .click(getNthStatusOptionsButton(1))
    .click(dialogOptionsOption.withText('Delete and redraft'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql('')
    .expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
    .expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('')
    .expect(getComposeModalNthMediaAltInput(1).value).eql('')
    .typeText(composeModalInput, 'oops forgot an alt', { replace: true, paste: true })
    .typeText(getComposeModalNthMediaAltInput(1), 'lovely kitteh', { replace: true, paste: true })
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusContent(1).innerText).contains('oops forgot an alt')
    .expect(getNthStatusMediaImg(1).getAttribute('alt')).eql('lovely kitteh')
})

test('privacy and spoiler delete and redraft', async t => {
  await postWithSpoilerAndPrivacyAs('foobar', 'this is hidden', 'click to see!', 'private')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusSpoiler(1).innerText).contains('click to see!')
    .click(getNthStatusOptionsButton(1))
    .click(dialogOptionsOption.withText('Delete and redraft'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql('this is hidden')
    .expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Followers-only)')
    .expect(composeModalContentWarningInput.value).eql('click to see!')
    .typeText(composeModalContentWarningInput, 'no really, you should click this!', { replace: true, paste: true })
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusSpoiler(1).innerText).contains('no really, you should click this!')
})

test('delete and redraft reply', async t => {
  await postAs('admin', 'hey hello')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('hey hello')
    .click(getNthReplyButton(1))
    .typeText(getNthComposeReplyInput(1), 'hello there admin', { paste: true })
    .click(getNthComposeReplyButton(1))
    .expect(getNthStatus(1).innerText).contains('@admin hello there admin')
    .click(getNthStatusOptionsButton(1))
    .click(dialogOptionsOption.withText('Delete and redraft'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .typeText(composeModalInput, ' oops forgot to say thank you')
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusContent(1).innerText).match(/@admin hello there admin\s+oops forgot to say thank you/, {
      timeout: 30000
    })
    .click(getNthStatus(1))
    .expect(getUrl()).match(/statuses/)
    .expect(getNthStatusContent(1).innerText).contains('hey hello')
    .expect(getNthStatusContent(2).innerText).match(/@admin hello there admin\s+oops forgot to say thank you/)
})

test('delete and redraft reply within thread', async t => {
  await postAs('admin', 'this is a thread')
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('this is a thread')
    .click(getNthStatus(1))
    .expect(getUrl()).match(/statuses/)
  await sleep(1000)
  await t
    .expect(getNthStatusContent(1).innerText).contains('this is a thread')
    .click(getNthReplyButton(1))
  await sleep(1000)
  await t
    .typeText(getNthComposeReplyInput(1), 'heyo', { paste: true })
    .click(getNthComposeReplyButton(1))
  await sleep(1000)
  await t
    .expect(getNthStatus(2).innerText).contains('@admin heyo')
    .click(getNthStatusOptionsButton(2))
  await sleep(500)
  await t
    .click(dialogOptionsOption.withText('Delete and redraft'))
  await sleep(500)
  await t
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .typeText(composeModalInput, ' update!', { paste: true })
  await sleep(1000)
  await t
    .click(composeModalComposeButton)
  await sleep(1000)
  await t
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusContent(2).innerText).match(/@admin heyo\s+update!/, {
      timeout: 30000
    })
})

test('multiple paragraphs', async t => {
  let text = 'hey ho\n\ndouble newline!\njust one newline\njust another newline\n\nanother double newline!'
  await postAs('foobar', text)
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains(text)
    .click(getNthStatusOptionsButton(1))
    .click(dialogOptionsOption.withText('Delete and redraft'))
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
    .expect(composeModalInput.value).eql(text)
    .typeText(composeModalInput, '\n\nwoot', { paste: true })
    .click(composeModalComposeButton)
    .expect(modalDialog.exists).notOk()
    .expect(getNthStatusContent(1).innerText).contains(text + '\n\nwoot')
})


code:
import { loginAsFoobar } from '../roles'
import {
  getNthStatus,
  getNthStatusContent,
  getUrl,
  getNthReplyButton, getNthComposeReplyInput, sleep,
  getAriaSetSize,
  getNthComposeReplyButton, goBack, goForward
} from '../utils'
import { postAs, postReplyAs } from '../serverActions'

fixture`122-replies-in-threads.js`
  .page`http://localhost:4002`

const TIMEOUT = 1500

async function goBackAndForward (t) {
  await goBack()
  await t.expect(getUrl()).eql('http://localhost:4002/')
  await goForward()
  await t.expect(getUrl()).match(/statuses/)
}

async function verifyAriaSetSize (t, size) {
  await sleep(TIMEOUT)
  await t.expect(getAriaSetSize()).eql(size)
  await goBackAndForward(t)
  await sleep(TIMEOUT)
  await t.expect(getAriaSetSize()).eql(size)
}

// You click on status B, which is a reply to status A.
// Now status C comes in, which is a response to status A.
// Status C is inserted automatically, even though it shouldn't be in that thread.
test('reply to non-focused parent status in a thread', async t => {
  const { id } = await postAs('admin', 'here is my awesome thread')
  await postReplyAs('admin', 'and here it is continued', id)
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('and here it is continued')
    .click(getNthStatus(1))
    .expect(getUrl()).match(/statuses/)
    .click(getNthReplyButton(1))
    .typeText(getNthComposeReplyInput(1), 'haha I totally agree', { paste: true })
    .click(getNthComposeReplyButton(1))
  await verifyAriaSetSize(t, '2')
})

// In this case it's the same as the previous, except the focused status is status A.
test('reply to focused status in a thread', async t => {
  const { id } = await postAs('admin', 'whoa here is my other awesome thread')
  await postReplyAs('admin', 'whoa and here it is probably continued', id)
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('whoa and here it is probably continued')
    .hover(getNthStatusContent(2))
    .click(getNthStatus(2))
    .expect(getUrl()).match(/statuses/)
    .click(getNthReplyButton(1))
    .typeText(getNthComposeReplyInput(1), 'haha I totally agree', { paste: true })
    .click(getNthComposeReplyButton(1))
  await verifyAriaSetSize(t, '3')
})

// In this case the thread is A-B-C, the focused status is C,
// and the replied-to status is A.
test('reply to non-focused grandparent status in a thread', async t => {
  const { id: id1 } = await postAs('admin', 'cool thread 1/3')
  const { id: id2 } = await postReplyAs('admin', 'cool thread 2/3', id1)
  await postReplyAs('admin', 'cool thread 3/3', id2)
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('cool thread 3/3')
    .click(getNthStatus(1))
    .expect(getUrl()).match(/statuses/)
    .hover(getNthStatus(3))
    .hover(getNthStatus(2))
    .hover(getNthStatus(1))
    .click(getNthReplyButton(1))
    .typeText(getNthComposeReplyInput(1), 'this is sweet', { paste: true })
    .click(getNthComposeReplyButton(1))
  await verifyAriaSetSize(t, '3')
})

// Thread is A-B-C, focused status is A, replied-to status is C.
test('reply to non-focused grandchild status in a thread', async t => {
  const { id: id1 } = await postAs('admin', 'sweet thread 1/3')
  const { id: id2 } = await postReplyAs('admin', 'sweet thread 2/3', id1)
  await postReplyAs('admin', 'sweet thread 3/3', id2)
  await loginAsFoobar(t)
  await t
    .hover(getNthStatus(1))
    .expect(getNthStatusContent(1).innerText).contains('sweet thread 3/3')
    .hover(getNthStatusContent(2))
    .hover(getNthStatus(3))
    .click(getNthStatus(3))
    .expect(getUrl()).match(/statuses/)
    .hover(getNthStatus(1))
    .hover(getNthStatus(2))
    .hover(getNthStatus(3))
    .click(getNthReplyButton(3))
    .typeText(getNthComposeReplyInput(3), 'this is sweet', { paste: true })
    .click(getNthComposeReplyButton(3))
  await verifyAriaSetSize(t, '4')
})


code:
import {
  getNthStatusContent,
  getUrl, notificationsTabAll, notificationsTabMentions,
  notificationsNavButton, sleep
} from '../utils'
import { loginAsFoobar } from '../roles'
import { favoriteStatusAs, postAs } from '../serverActions'

fixture`123-notification-mentions.js`
  .page`http://localhost:4002`

// maybe in the "mentions" view it should prevent the notification icon from showing (1), (2) etc
// if those particular notifications were seen by the user... but this is too hard to implement,
// so I'm going to punt on it. Only the "all" view affects those (1) / (2) / etc badges.
test('Handles incoming notifications that are mentions', async t => {
  const timeout = 20000
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .expect(getUrl()).match(/\/notifications$/)
    .click(notificationsTabMentions)
    .expect(getUrl()).match(/\/notifications\/mentions$/)
  await sleep(2000)
  await postAs('admin', 'hey @foobar I am mentioning you')
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page) (1 notification)', {
      timeout
    })
    .expect(getNthStatusContent(1).innerText).contains('hey @foobar I am mentioning you')
    .click(notificationsTabAll)
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
})

test('Handles incoming notifications that are not mentions', async t => {
  const timeout = 20000
  let { id: statusId } = await postAs('foobar', 'this is a post that I hope somebody will favorite')
  await sleep(2000)
  await loginAsFoobar(t)
  await t
    .click(notificationsNavButton)
    .expect(getUrl()).match(/\/notifications$/)
    .click(notificationsTabMentions)
    .expect(getUrl()).match(/\/notifications\/mentions$/)
  await sleep(2000)
  await postAs('admin', 'woot I am mentioning you again @foobar')
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page) (1 notification)', {
      timeout
    })
    .expect(getNthStatusContent(1).innerText).contains('woot I am mentioning you again @foobar')
  await sleep(2000)
  await favoriteStatusAs('admin', statusId)
  await t
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page) (2 notifications)', {
      timeout
    })
  await sleep(2000)
  await t
    .expect(getNthStatusContent(1).innerText).contains('woot I am mentioning you again @foobar')
    .click(notificationsTabAll)
    .expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
  await t
    .expect(getNthStatusContent(1).innerText).contains('this is a post that I hope somebody will favorite')
    .expect(getNthStatusContent(2).innerText).contains('woot I am mentioning you again @foobar')
})


code:
import {
  settingsNavButton, instanceSettingHomeReblogs, homeNavButton, sleep, getNthStatusContent
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
import { postAs, reblogStatusAs } from '../serverActions'

fixture`124-home-timeline-filters.js`
  .page`http://localhost:4002`

test('Filters favs from home timeline', async t => {
  await postAs('foobar', 'Nobody should boost this')
  await sleep(1000)
  let { id: statusId } = await postAs('quux', 'I hope someone cool boosts this')
  await reblogStatusAs('admin', statusId)
  await sleep(2000)
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('I hope someone cool boosts this')
    .expect(getNthStatusContent(2).innerText).contains('Nobody should boost this')
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingHomeReblogs)
    .expect(instanceSettingHomeReblogs.checked).notOk()
    .click(homeNavButton)
  await t
    .expect(getNthStatusContent(1).innerText).contains('Nobody should boost this')
})