code:
import {
  settingsNavButton,
  getNthStatusContent,
  instanceSettingNotificationReblogs,
  notificationBadge,
  instanceSettingNotificationFavs,
  instanceSettingNotificationMentions,
  instanceSettingNotificationFollows,
  notificationsNavButton,
  getUrl,
  sleep, showMoreButton, scrollToBottom, scrollToTop
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
import { favoriteStatusAs, followAs, postAs, reblogStatusAs, unfollowAs } from '../serverActions'

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

test('Notification timeline filters correctly affect counts - boosts', async t => {
  let timeout = 20000
  let { id: statusId } = await postAs('foobar', 'I do not care if you boost this')
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('I do not care if you boost this')
  await reblogStatusAs('admin', statusId)
  await t
    .expect(notificationBadge.innerText).eql('1', { timeout })
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingNotificationReblogs)
    .expect(instanceSettingNotificationReblogs.checked).notOk()
    .expect(notificationBadge.exists).notOk({ timeout })
    .click(instanceSettingNotificationReblogs)
    .expect(instanceSettingNotificationReblogs.checked).ok()
    .expect(notificationBadge.innerText).eql('1', { timeout })
})

test('Notification timeline filters correctly affect counts - favs', async t => {
  let timeout = 20000
  let { id: statusId } = await postAs('foobar', 'I do not care if you fav this')
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('I do not care if you fav this')
  await favoriteStatusAs('admin', statusId)
  await t
    .expect(notificationBadge.innerText).eql('1', { timeout })
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingNotificationFavs)
    .expect(instanceSettingNotificationFavs.checked).notOk()
    .expect(notificationBadge.exists).notOk({ timeout })
    .click(instanceSettingNotificationFavs)
    .expect(instanceSettingNotificationFavs.checked).ok()
    .expect(notificationBadge.innerText).eql('1', { timeout })
})

test('Notification timeline filters correctly affect counts - favs', async t => {
  let timeout = 20000
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).exists).ok()
  await postAs('admin', 'hey yo @foobar')
  await t
    .expect(notificationBadge.innerText).eql('1', { timeout })
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingNotificationMentions)
    .expect(instanceSettingNotificationMentions.checked).notOk()
    .expect(notificationBadge.exists).notOk({ timeout })
    .click(instanceSettingNotificationMentions)
    .expect(instanceSettingNotificationMentions.checked).ok()
    .expect(notificationBadge.innerText).eql('1', { timeout })
})

test('Notification timeline filters correctly affect counts - follows', async t => {
  let timeout = 20000
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).exists).ok()
  await followAs('ExternalLinks', 'foobar')
  await t
    .expect(notificationBadge.innerText).eql('1', { timeout })
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingNotificationFollows)
    .expect(instanceSettingNotificationFollows.checked).notOk()
    .expect(notificationBadge.exists).notOk({ timeout })
    .click(instanceSettingNotificationFollows)
    .expect(instanceSettingNotificationMentions.checked).ok()
    .expect(notificationBadge.innerText).eql('1', { timeout })
  await unfollowAs('ExternalLinks', 'foobar')
})

test('Notification timeline filters correctly show "show more" button', async t => {
  await loginAsFoobar(t)
  await t
    .click(settingsNavButton)
    .click($('a').withText('Instances'))
    .click($('a').withText('localhost:3000'))
    .click(instanceSettingNotificationMentions)
    .expect(instanceSettingNotificationMentions.checked).notOk()
    .click(notificationsNavButton)
    .expect(getUrl()).contains('/notifications')
    .expect(getNthStatusContent(1).exists).ok()
  await scrollToBottom()
  await sleep(1000)
  await postAs('admin', 'hey @foobar you should ignore this')
  await sleep(1000)
  await scrollToTop()
  await sleep(1000)
  await t
    .expect(showMoreButton.innerText).contains('Show 0 more') // not shown
  await scrollToBottom()
  await sleep(1000)
  await followAs('ExternalLinks', 'foobar')
  await sleep(1000)
  await scrollToTop()
  await sleep(1000)
  await t
    .expect(showMoreButton.innerText).contains('Show 1 more', { timeout: 20000 })
  await unfollowAs('ExternalLinks', 'foobar')
})


code:
import {
  getNthStatusContent,
  getNthStatusPollOption,
  getNthStatusPollVoteButton,
  getNthStatusPollForm,
  getNthStatusPollResult,
  sleep,
  getNthStatusPollRefreshButton,
  getNthStatusPollVoteCount,
  getNthStatusRelativeDate, getUrl, goBack
} from '../utils'
import { loginAsFoobar } from '../roles'
import { createPollAs, voteOnPollAs } from '../serverActions'

fixture`126-polls.js`
  .page`http://localhost:4002`

test('Can vote on polls', async t => {
  await loginAsFoobar(t)
  await createPollAs('admin', 'vote on my cool poll', ['yes', 'no'], false)
  await t
    .expect(getNthStatusContent(1).innerText).contains('vote on my cool poll')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
    .click(getNthStatusPollOption(1, 2))
    .click(getNthStatusPollVoteButton(1))
    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
    .expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
    .expect(getNthStatusPollResult(1, 2).innerText).eql('100% no')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
})

test('Can vote on multiple-choice polls', async t => {
  await loginAsFoobar(t)
  await createPollAs('admin', 'vote on my other poll', ['yes', 'no', 'maybe'], true)
  await t
    .expect(getNthStatusContent(1).innerText).contains('vote on my other poll')
    .click(getNthStatusPollOption(1, 1))
    .click(getNthStatusPollOption(1, 3))
    .click(getNthStatusPollVoteButton(1))
    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
    .expect(getNthStatusPollResult(1, 1).innerText).eql('50% yes')
    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
    .expect(getNthStatusPollResult(1, 3).innerText).eql('50% maybe')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('2 votes')
})

test('Can update poll results', async t => {
  const { poll } = await createPollAs('admin', 'vote on this poll', ['yes', 'no', 'maybe'], false)
  const { id: pollId } = poll
  await voteOnPollAs('baz', pollId, [1])
  await voteOnPollAs('ExternalLinks', pollId, [1])
  await voteOnPollAs('foobar', pollId, [2])
  await sleep(1000)
  await loginAsFoobar(t)
  await t
    .expect(getNthStatusContent(1).innerText).contains('vote on this poll')
    .expect(getNthStatusPollForm(1).exists).notOk()
    .expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
    .expect(getNthStatusPollResult(1, 2).innerText).eql('67% no')
    .expect(getNthStatusPollResult(1, 3).innerText).eql('33% maybe')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('3 votes')
  await sleep(1000)
  await voteOnPollAs('quux', pollId, [0])
  await sleep(1000)
  await t
    .click(getNthStatusPollRefreshButton(1))
    .expect(getNthStatusPollResult(1, 1).innerText).eql('25% yes', { timeout: 20000 })
    .expect(getNthStatusPollResult(1, 2).innerText).eql('50% no')
    .expect(getNthStatusPollResult(1, 3).innerText).eql('25% maybe')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('4 votes')
})

test('Poll results refresh everywhere', async t => {
  await loginAsFoobar(t)
  await createPollAs('admin', 'another poll', ['yes', 'no'], false)
  await t
    .expect(getNthStatusContent(1).innerText).contains('another poll')
    .click(getNthStatusRelativeDate(1))
    .expect(getUrl()).contains('/statuses')
    .expect(getNthStatusContent(1).innerText).contains('another poll')
    .click(getNthStatusPollOption(1, 1))
    .click(getNthStatusPollVoteButton(1))
    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
    .expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes')
    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
  await goBack()
  await t
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
    .expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes')
    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
    .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
})

repo_name: RayoGundead/pinny

license: mit


code:
import {
  getNthStatusContent,
  getNthStatusPollForm,
  getNthStatusPollResult,
  getNthStatusPollVoteCount,
  pollButton,
  getComposePollNthInput,
  composePoll,
  composePollMultipleChoice,
  composePollExpiry, composePollAddButton, getComposePollRemoveNthButton, postStatusButton, composeInput
} from '../utils'
import { loginAsFoobar } from '../roles'
import { POLL_EXPIRY_DEFAULT } from '../../src/routes/_static/polls'

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

test('Can add and remove poll', async t => {
  await loginAsFoobar(t)
  await t
    .expect(composePoll.exists).notOk()
    .expect(pollButton.getAttribute('aria-label')).eql('Add poll')
    .click(pollButton)
    .expect(composePoll.exists).ok()
    .expect(getComposePollNthInput(1).value).eql('')
    .expect(getComposePollNthInput(2).value).eql('')
    .expect(getComposePollNthInput(3).exists).notOk()
    .expect(getComposePollNthInput(4).exists).notOk()
    .expect(composePollMultipleChoice.checked).notOk()
    .expect(composePollExpiry.value).eql(POLL_EXPIRY_DEFAULT.toString())
    .expect(pollButton.getAttribute('aria-label')).eql('Remove poll')
    .click(pollButton)
    .expect(composePoll.exists).notOk()
})

test('Can add and remove poll options', async t => {
  await loginAsFoobar(t)
  await t
    .expect(composePoll.exists).notOk()
    .expect(pollButton.getAttribute('aria-label')).eql('Add poll')
    .click(pollButton)
    .expect(composePoll.exists).ok()
    .typeText(getComposePollNthInput(1), 'first', { paste: true })
    .typeText(getComposePollNthInput(2), 'second', { paste: true })
    .click(composePollAddButton)
    .typeText(getComposePollNthInput(3), 'third', { paste: true })
    .expect(getComposePollNthInput(1).value).eql('first')
    .expect(getComposePollNthInput(2).value).eql('second')
    .expect(getComposePollNthInput(3).value).eql('third')
    .expect(getComposePollNthInput(4).exists).notOk()
    .click(getComposePollRemoveNthButton(2))
    .expect(getComposePollNthInput(1).value).eql('first')
    .expect(getComposePollNthInput(2).value).eql('third')
    .expect(getComposePollNthInput(3).exists).notOk()
    .expect(getComposePollNthInput(4).exists).notOk()
    .click(composePollAddButton)
    .typeText(getComposePollNthInput(3), 'fourth', { paste: true })
    .typeText(composeInput, 'Vote on my poll!!!', { paste: true })
    .click(postStatusButton)
    .expect(getNthStatusContent(1).innerText).contains('Vote on my poll!!!')
    .expect(getNthStatusPollForm(1).exists).notOk()
    .expect(getNthStatusPollResult(1, 1).innerText).eql('0% first')
    .expect(getNthStatusPollResult(1, 2).innerText).eql('0% third')
    .expect(getNthStatusPollResult(1, 3).innerText).eql('0% fourth')
    .expect(getNthStatusPollResult(1, 4).exists).notOk()
    .expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
})


code:
import {
  settingsNavButton,
  homeNavButton,
  disableInfiniteScroll,
  getFirstVisibleStatus,
  getUrl,
  showMoreButton, getNthStatusContent
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
import { postAs } from '../serverActions'

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

test('Can disable loading items at top of timeline', async t => {
  await loginAsFoobar(t)
  await t.click(settingsNavButton)
    .click($('a').withText('General'))
    .click(disableInfiniteScroll)
    .expect(disableInfiniteScroll.checked).ok()
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
    .expect(getFirstVisibleStatus().exists).ok()
  await postAs('admin', 'hey hey hey this is new')
  await t
    .expect(showMoreButton.innerText).contains('Show 1 more', {
      timeout: 20000
    })
    .click(showMoreButton)
    .expect(getNthStatusContent(1).innerText).contains('hey hey hey this is new')
})


code:
import FormData from 'form-data'
import fs from 'fs'
import path from 'path'
import { auth } from '../src/routes/_api/utils'

export async function submitMedia (accessToken, filename, alt) {
  let form = new FormData()
  form.append('file', fs.createReadStream(path.join(__dirname, 'images', filename)))
  form.append('description', alt)
  return new Promise((resolve, reject) => {
    form.submit({
      host: 'localhost',
      port: 3000,
      path: '/api/v1/media',
      headers: auth(accessToken)
    }, (err, res) => {
      if (err) {
        return reject(err)
      }
      let data = ''

      res.on('data', chunk => {
        data += chunk
      })

      res.on('end', () => resolve(JSON.parse(data)))
    })
  })
}


code:
/* global it describe beforeEach afterEach */

import '../indexedDBShims'
import assert from 'assert'
import { closeDatabase, deleteDatabase, getDatabase } from '../../src/routes/_database/databaseLifecycle'
import * as dbApi from '../../src/routes/_database/databaseApis'
import times from 'lodash-es/times'
import cloneDeep from 'lodash-es/cloneDeep'
import {
  TIMESTAMP, ACCOUNT_ID, STATUS_ID, REBLOG_ID, USERNAME_LOWERCASE,
  CURRENT_TIME, DB_VERSION_CURRENT, DB_VERSION_SEARCH_ACCOUNTS, DB_VERSION_SNOWFLAKE_IDS
} from '../../src/routes/_database/constants'
import { cleanup, TIME_AGO } from '../../src/routes/_database/cleanup'

const INSTANCE_NAME = 'localhost:3000'

const INSTANCE_INFO = {
  'uri': 'localhost:3000',
  'title': 'lolcathost',
  'description': 'blah',
  'foo': {
    'bar': true
  }
}

const createStatus = i => ({
  id: i.toString(),
  created_at: new Date().toISOString(),
  content: `Status #4{id}`,
  account: {
    id: '1'
  }
})

const stripDBFields = item => {
  let res = cloneDeep(item)
  delete res[TIMESTAMP]
  delete res[ACCOUNT_ID]
  delete res[STATUS_ID]
  delete res[REBLOG_ID]
  delete res[USERNAME_LOWERCASE]
  if (res.account) {
    delete res.account[TIMESTAMP]
  }
  return res
}

describe('test-database.js', function () {
  this.timeout(60000)

  describe('db-basic', () => {
    beforeEach(async () => {
      await getDatabase(INSTANCE_NAME)
    })

    afterEach(async () => {
      await deleteDatabase(INSTANCE_NAME)
    })

    it('basic indexeddb test', async () => {
      let info = await dbApi.getInstanceInfo(INSTANCE_NAME)
      assert(!info)
      await dbApi.setInstanceInfo(INSTANCE_NAME, INSTANCE_INFO)
      info = await dbApi.getInstanceInfo(INSTANCE_NAME)
      assert.deepStrictEqual(info, INSTANCE_INFO)
    })

    it('basic indexeddb test 2', async () => {
      // sanity check to make sure that we have a clean DB between each test
      let info = await dbApi.getInstanceInfo(INSTANCE_NAME)
      assert(!info)
      await dbApi.setInstanceInfo(INSTANCE_NAME, INSTANCE_INFO)
      info = await dbApi.getInstanceInfo(INSTANCE_NAME)
      assert.deepStrictEqual(info, INSTANCE_INFO)
    })

    it('stores and sorts some statuses', async () => {
      let allStatuses = times(40, createStatus)
      await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', allStatuses)
      let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20)
      let expected = allStatuses.slice().reverse().slice(0, 20)
      assert.deepStrictEqual(statuses.map(stripDBFields), expected)

      statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', statuses[statuses.length - 1].id, 20)
      expected = allStatuses.slice().reverse().slice(20, 40)
      assert.deepStrictEqual(statuses.map(stripDBFields), expected)
    })

    it('cleans up old statuses', async () => {
      // Pretend we are inserting a status from a long time ago. Note that we
      // set a timestamp based on the *current* date when the status is inserted,
      // not the date that the status was composed.

      let longAgo = Date.now() - (TIME_AGO * 2)

      let oldStatus = {
        id: '1',
        created_at: new Date(longAgo).toISOString(),
        content: 'This is old',
        account: {
          id: '1'
        }
      }

      let previousNow = CURRENT_TIME.now
      CURRENT_TIME.now = () => longAgo

      await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', [oldStatus])

      CURRENT_TIME.now = previousNow

      let newStatus = {
        id: '2',
        created_at: new Date().toISOString(),
        content: 'This is new',
        account: {
          id: '2'
        }
      }

      await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', [newStatus])
      let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20)
      assert.deepStrictEqual(statuses.map(stripDBFields), [newStatus, oldStatus])

      let status1 = await dbApi.getStatus(INSTANCE_NAME, '1')
      let status2 = await dbApi.getStatus(INSTANCE_NAME, '2')

      assert.deepStrictEqual(stripDBFields(status1), oldStatus)
      assert.deepStrictEqual(stripDBFields(status2), newStatus)

      await cleanup(INSTANCE_NAME)

      statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 20)
      assert.deepStrictEqual(statuses.map(stripDBFields), [newStatus])

      status1 = await dbApi.getStatus(INSTANCE_NAME, '1')
      status2 = await dbApi.getStatus(INSTANCE_NAME, '2')

      assert(!!status1)
      assert.deepStrictEqual(stripDBFields(status2), newStatus)
    })
  })

  describe('db-migrations', () => {
    let oldCurrentVersion

    beforeEach(async () => {
      oldCurrentVersion = DB_VERSION_CURRENT.version
    })

    afterEach(async () => {
      DB_VERSION_CURRENT.version = oldCurrentVersion
      await deleteDatabase(INSTANCE_NAME)
    })

    it('migrates to snowflake IDs', async () => {
      // open the db using the old version
      DB_VERSION_CURRENT.version = DB_VERSION_SEARCH_ACCOUNTS
      await getDatabase(INSTANCE_NAME)

      // insert some statuses
      let allStatuses = times(40, createStatus)
      await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', allStatuses)

      let statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 1000)
      let expected = allStatuses.slice().reverse()
      assert.deepStrictEqual(statuses.map(stripDBFields), expected)

      // close the database
      closeDatabase(INSTANCE_NAME)

      // do a version upgrade
      DB_VERSION_CURRENT.version = DB_VERSION_SNOWFLAKE_IDS
      await getDatabase(INSTANCE_NAME)

      // check that the old statuses are correct
      statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 1000)
      expected = allStatuses.slice().reverse()
      assert.deepStrictEqual(statuses.map(stripDBFields), expected)

      // insert some more statuses for good measure
      let moreStatuses = times(20, i => 40 + i).map(createStatus)

      await dbApi.insertTimelineItems(INSTANCE_NAME, 'local', moreStatuses)

      statuses = await dbApi.getTimeline(INSTANCE_NAME, 'local', null, 1000)
      expected = moreStatuses.slice().reverse().concat(allStatuses.reverse())

      assert.deepStrictEqual(statuses.map(stripDBFields), expected)
    })
  })
})


code:
/* global describe, it */

import { replaceEmoji } from '../../src/routes/_utils/replaceEmoji'
import assert from 'assert'

const mindBlown = String.fromCodePoint(0x1F92F)
const elephant = String.fromCodePoint(0x1F418)
const womanBowing = [0x1f647, 0x200d, 0x2640, 0xfe0f].map(_ => String.fromCodePoint(_)).join('')

describe('test-emoji.js', function () {
  it('does emoji replacement correctly', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji('hello world', replacer),
      'hello world'
    )
    assert.strictEqual(
      replaceEmoji(`${mindBlown}`, replacer),
      `<div>${mindBlown}</div>`
    )
    assert.strictEqual(
      replaceEmoji(`${mindBlown} ${elephant}`, replacer),
      `<div>${mindBlown}</div> <div>${elephant}</div>`
    )
    assert.strictEqual(
      replaceEmoji(`${elephant} woot ${mindBlown}`, replacer),
      `<div>${elephant}</div> woot <div>${mindBlown}</div>`
    )
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown}`, replacer),
      `woot <div>${mindBlown}</div>`
    )
    assert.strictEqual(
      replaceEmoji(`${mindBlown} woot`, replacer),
      `<div>${mindBlown}</div> woot`
    )
  })

  it('handles multi-code emoji', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji(`hello ${womanBowing}`, replacer),
      `hello <div>${womanBowing}</div>`
    )
  })

  it('handles emoji mixed with custom emoji', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji(`hello ${womanBowing} and :blobpats: and ${elephant}`, replacer),
      `hello <div>${womanBowing}</div> and :blobpats: and <div>${elephant}</div>`
    )
  })

  it('handles sequential emoji', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji(`${elephant}${elephant}${womanBowing}${mindBlown}`, replacer),
      `<div>${elephant}</div><div>${elephant}</div><div>${womanBowing}</div><div>${mindBlown}</div>`
    )
  })

  it('does not replace non-emoji characters', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji(`it's over #9000`, replacer),
      `it's over #9000`
    )
    assert.strictEqual(
      replaceEmoji(`woot !@#$%^&*()~` + '`' + `{[}]:;"'<,>.?/£™℠®`, replacer),
      `woot !@#$%^&*()~` + '`' + `{[}]:;"'<,>.?/£™℠®`
    )

    assert.strictEqual(
      replaceEmoji(`woot !@#$%^&*()~` + '`' + `{[}]:;"'<,>.?/£™℠®`, replacer),
      `woot !@#$%^&*()~` + '`' + `{[}]:;"'<,>.?/£™℠®`
    )

    // hidden VARIATION SELECTOR character is in here
    assert.strictEqual(
      replaceEmoji("
It's shapes™ ... continued
", replacer),
      "
It's shapes™ ... continued
"
    )
  })

  it('does not replace emoji inside HTML tags', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji(`check this cool link: <a href='http://example.com?q=${mindBlown}'>link</a>`, replacer),
      `check this cool link: <a href='http://example.com?q=${mindBlown}'>link</a>`
    )
    assert.strictEqual(
      replaceEmoji(
        `<a href='http://foo.com?q=${mindBlown}'>link</a> and <a href='http://foo.com?q=${mindBlown}'>link</a>`,
        replacer
      ),
      `<a href='http://foo.com?q=${mindBlown}'>link</a> and <a href='http://foo.com?q=${mindBlown}'>link</a>`
    )
    assert.strictEqual(
      replaceEmoji(
        `<a href='http://foo.com?q=${mindBlown}'>link</a> and ${mindBlown}`,
        replacer
      ),
      `<a href='http://foo.com?q=${mindBlown}'>link</a> and <div>${mindBlown}</div>`
    )
    assert.strictEqual(
      replaceEmoji(
        `<a href='http://foo.com?q=${mindBlown}'>link</a> and ${mindBlown} and ` +
        `<a href='http://foo.com?q=${mindBlown}'>link</a>`,
        replacer
      ),
      `<a href='http://foo.com?q=${mindBlown}'>link</a> and <div>${mindBlown}</div> and ` +
      `<a href='http://foo.com?q=${mindBlown}'>link</a>`
    )
  })

  it('removes emoji', function () {
    let replacer = _ => ''
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown}`, replacer),
      `woot `
    )
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown} woot`, replacer),
      `woot  woot`
    )
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown}${elephant}`, replacer),
      `woot `
    )
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown}${elephant} woot`, replacer),
      `woot  woot`
    )
  })

  it('can handle a dangling left angle bracket for some reason', function () {
    let replacer = _ => `<div>${_}</div>`
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown} <`, replacer),
      `woot <div>${mindBlown}</div> <`
    )
    assert.strictEqual(
      replaceEmoji(`woot ${mindBlown} <hahahahaha`, replacer),
      `woot <div>${mindBlown}</div> <hahahahaha`
    )
    assert.strictEqual(
      replaceEmoji(`<woot ${mindBlown} <hahahahaha`, replacer),
      `<woot ${mindBlown} <hahahahaha`
    )
  })
})


code:
/* global describe, it */

import { toPaddedBigInt, toReversePaddedBigInt } from '../../src/routes/_utils/statusIdSorting'
import assert from 'assert'
import times from 'lodash-es/times'

function lt (a, b) {
  assert(a < b, `Failed: ${a} < ${b}`)
}

function gt (a, b) {
  assert(a > b, `Failed: ${a} > ${b}`)
}

describe('test-id-sorting.js', () => {
  it('basic id sorting', () => {
    assert.deepStrictEqual(toPaddedBigInt('0'), '000000000000000000000000000000')
    assert.deepStrictEqual(toPaddedBigInt('1'), '000000000000000000000000000001')
    assert.deepStrictEqual(toPaddedBigInt('z'), '00000000000000000000000000000z')
    assert.deepStrictEqual(toReversePaddedBigInt('0'), 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz')
    assert.deepStrictEqual(toReversePaddedBigInt('1'), 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzy')
    assert.deepStrictEqual(toReversePaddedBigInt('z'), 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzz0')
  })

  it('can sort mastodon IDs correctly', () => {
    let id1 = '1'
    let id2 = '2'
    let id3 = '101687554574502736'
    let id4 = '101688993168288745'
    let id5 = '101689076270570796'

    lt(toPaddedBigInt(id1), toPaddedBigInt(id2))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id3))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id4))
    lt(toPaddedBigInt(id4), toPaddedBigInt(id5))

    assert.deepStrictEqual(toPaddedBigInt(id1), toPaddedBigInt(id1))
    assert.deepStrictEqual(toPaddedBigInt(id2), toPaddedBigInt(id2))
    assert.deepStrictEqual(toPaddedBigInt(id3), toPaddedBigInt(id3))
    assert.deepStrictEqual(toPaddedBigInt(id4), toPaddedBigInt(id4))
    assert.deepStrictEqual(toPaddedBigInt(id5), toPaddedBigInt(id5))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id2))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id3))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id4))
    gt(toReversePaddedBigInt(id4), toReversePaddedBigInt(id5))

    assert.deepStrictEqual(toReversePaddedBigInt(id1), toReversePaddedBigInt(id1))
    assert.deepStrictEqual(toReversePaddedBigInt(id2), toReversePaddedBigInt(id2))
    assert.deepStrictEqual(toReversePaddedBigInt(id3), toReversePaddedBigInt(id3))
    assert.deepStrictEqual(toReversePaddedBigInt(id4), toReversePaddedBigInt(id4))
    assert.deepStrictEqual(toReversePaddedBigInt(id5), toReversePaddedBigInt(id5))
  })

  it('can sort mastodon IDs correctly - more examples', () => {
    let ids = times(1000, i => i.toString())

    for (let i = 1; i < ids.length; i++) {
      let prev = ids[i - 1]
      let next = ids[i]
      lt(toPaddedBigInt(prev), toPaddedBigInt(next))
      gt(toReversePaddedBigInt(prev), toReversePaddedBigInt(next))
    }
  })

  it('can sort base62 IDs correctly', () => {
    let id1 = '0'
    let id2 = 'A'
    let id3 = 'Z'
    let id4 = 'a'
    let id5 = 't'

    lt(toPaddedBigInt(id1), toPaddedBigInt(id2))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id3))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id4))
    lt(toPaddedBigInt(id4), toPaddedBigInt(id5))

    lt(toPaddedBigInt(id1), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id4))

    assert.deepStrictEqual(toPaddedBigInt(id1), toPaddedBigInt(id1))
    assert.deepStrictEqual(toPaddedBigInt(id2), toPaddedBigInt(id2))
    assert.deepStrictEqual(toPaddedBigInt(id3), toPaddedBigInt(id3))
    assert.deepStrictEqual(toPaddedBigInt(id4), toPaddedBigInt(id4))
    assert.deepStrictEqual(toPaddedBigInt(id5), toPaddedBigInt(id5))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id2))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id3))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id4))
    gt(toReversePaddedBigInt(id4), toReversePaddedBigInt(id5))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id4))

    assert.deepStrictEqual(toReversePaddedBigInt(id1), toReversePaddedBigInt(id1))
    assert.deepStrictEqual(toReversePaddedBigInt(id2), toReversePaddedBigInt(id2))
    assert.deepStrictEqual(toReversePaddedBigInt(id3), toReversePaddedBigInt(id3))
    assert.deepStrictEqual(toReversePaddedBigInt(id4), toReversePaddedBigInt(id4))
    assert.deepStrictEqual(toReversePaddedBigInt(id5), toReversePaddedBigInt(id5))
  })

  it('can sort base62 IDs correctly 2', () => {
    let id1 = '0'
    let id2 = 'A'
    let id3 = 'T'
    let id4 = 'a'
    let id5 = 'z'

    lt(toPaddedBigInt(id1), toPaddedBigInt(id2))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id3))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id4))
    lt(toPaddedBigInt(id4), toPaddedBigInt(id5))

    lt(toPaddedBigInt(id1), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id4))

    assert.deepStrictEqual(toPaddedBigInt(id1), toPaddedBigInt(id1))
    assert.deepStrictEqual(toPaddedBigInt(id2), toPaddedBigInt(id2))
    assert.deepStrictEqual(toPaddedBigInt(id3), toPaddedBigInt(id3))
    assert.deepStrictEqual(toPaddedBigInt(id4), toPaddedBigInt(id4))
    assert.deepStrictEqual(toPaddedBigInt(id5), toPaddedBigInt(id5))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id2))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id3))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id4))
    gt(toReversePaddedBigInt(id4), toReversePaddedBigInt(id5))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id4))

    assert.deepStrictEqual(toReversePaddedBigInt(id1), toReversePaddedBigInt(id1))
    assert.deepStrictEqual(toReversePaddedBigInt(id2), toReversePaddedBigInt(id2))
    assert.deepStrictEqual(toReversePaddedBigInt(id3), toReversePaddedBigInt(id3))
    assert.deepStrictEqual(toReversePaddedBigInt(id4), toReversePaddedBigInt(id4))
    assert.deepStrictEqual(toReversePaddedBigInt(id5), toReversePaddedBigInt(id5))
  })

  it('can sort base62 IDs correctly 3', () => {
    let id1 = 'a'
    let id2 = 'z'
    let id3 = 'a0'
    let id4 = 'xx0'
    let id5 = 'a000'

    lt(toPaddedBigInt(id1), toPaddedBigInt(id2))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id3))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id4))
    lt(toPaddedBigInt(id4), toPaddedBigInt(id5))

    lt(toPaddedBigInt(id1), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id3), toPaddedBigInt(id5))
    lt(toPaddedBigInt(id2), toPaddedBigInt(id4))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id2))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id3))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id4))
    gt(toReversePaddedBigInt(id4), toReversePaddedBigInt(id5))

    gt(toReversePaddedBigInt(id1), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id3), toReversePaddedBigInt(id5))
    gt(toReversePaddedBigInt(id2), toReversePaddedBigInt(id4))
  })

  it('can sort pleroma ids', () => {
    // these are already in base62 sorted order
    let ids = [
      '9gP7cpqqJWyp93GxRw',
      '9gP7p4Ng7RdTgOSsro',
      '9gP8XTjVDpsT3Iqgb2',
      '9gP8eQQFvdZgoQ9tw0',
      '9gP99enEY6IAMJnaXA',
      '9gP9WIcp8QCIGbj6ES',
      '9gPA897muEuxo0FxCa',
      '9gPAG1uvaSBblj05Y0',
      '9gPAaSqTB8Rv4nev0C',
      '9gPAhfTCdeRCG5D9IO',
      '9gPBA9SYjPFVNUUZTU',
      '9gPBOzteZJZO3wFCQy',
      '9gPBatpwvN76kABf7Y',
      '9gPC7jAtaS1vEQdcnY',
      '9gPC9Ps4KQMLwRdZWy',
      '9gPCF0G8SvCKFHYg52',
      '9gPCJoNY42C4qZJo0W',
      '9gPEBGmBJX3YDntYBM',
      '9gPELIqcT0BhXgksSG',
      '9gPISh6j4FMCcu4Js0'
    ]

    for (let i = 1; i < ids.length; i++) {
      let prev = ids[i - 1]
      let next = ids[i]
      lt(toPaddedBigInt(prev), toPaddedBigInt(next))
      gt(toReversePaddedBigInt(prev), toReversePaddedBigInt(next))
    }
  })

  it('can sort pleroma ids 2', () => {
    let ids = [
      '9gTv5mTEiXL6ZpqYHg',
      '9gTv5mK1Gny07FXBuy',
      '9gTv5kXlshmKbJx94i',
      '9gTv5f9TvaNVsXYB1M',
      '9gTv5Fj2SlN71HzCpk',
      '9gTv5DibvNqCnNlptA',
      '9gTv4ttvbb0hKguaki',
      '9gTv4n17CTbFyxDjU0',
      '9gTv43wGndzCAtbjBQ',
      '9gTv3zP9ep7W725E0m',
      '9gTv3mQRuhQnrHaZiy',
      '9gTv3mOK2bjJkgasPQ',
      '9gTv3kpXqQJiuXJaYy',
      '9gTv3JliSYDAyqJeLY',
      '9gTv36jSvbgeY6AAXA',
      '9gTv2udLuVfP1fD7L6',
      '9gTv2gbQ4tnnVcCNNo',
      '9gTv2FSH0nRXJQiBsW',
      '9gTv1tfzz5LllcxqDY',
      '9gTv1t1EQejxjBtHfs'
    ].reverse()

    for (let i = 1; i < ids.length; i++) {
      let prev = ids[i - 1]
      let next = ids[i]
      lt(toPaddedBigInt(prev), toPaddedBigInt(next))
      gt(toReversePaddedBigInt(prev), toReversePaddedBigInt(next))
    }
  })

  it('can sort pleroma ids - 3', () => {
    let ids = [
      '9gTCUO2xe7vfkLbHRA',
      '9gT35b559J1tLPhGj2',
      '9gRax4YxAwDuIdr83U',
      '9gQqktJiZ6ha3Tz0fA',
      '9gOyRmT0DkfWcmnw7k',
      '9gOvJd0nBsvd7a1wUy',
      '9gOsyLgqCKARPQQKPo',
      '9gOsiEkAFyEnERINOq',
      '9gOsHBDvEh0EUoJRom',
      '9gOrx9MURrsivilkjw',
      '9gOrpQWsjga3rEwmw4',
      '9gOriOTFzUcLRjtjkm',
      '9gOraOfrLpz3lBynXE',
      '9gOrLvdMWe0Ldgudbk',
      '9gOrB5vttTXiOaPxCq',
      '9gOpJz8uJEGa2Ac47E',
      '9gOhCP31JNzWQXjVEu',
      '9gOdCdne43SIMZOp8K',
      '9gOcJKlz6VfvQRFZyq',
      '9gOcGtLW6bmXmchoGG',
      '9gOc78e2N27GKriYdM',
      '9gOc4BmYFXBvCHIGzw',
      '9gObs3Wx6Rt888DCHQ',
      '9gObp0SU4J0tjXfSZk',
      '9gOaaY4UiJOngVzN6e',
      '9gOaAiTbPDbQQTPnpQ',
      '9gOZjQH0yaB29SVrIe',
      '9gOZTFEsEbV1IMnUDA',
      '9gOZQA9yf55sfs2NYu',
      '9gOZNK8p5vaulK8yKO',
      '9gOZMvOZ6GKe0uJgtU',
      '9gOZIXp4ndv9kMd1SC',
      '9gOWmmfFsr2obOlJuy',
      '9gOWivHDvAoO9egFmK',
      '9gOWZDwXfIAH9TNkbQ',
      '9gOWUUUkhMq92R7v6m',
      '9gOWChzRml0fgE0MJk',
      '9gOVO8G0MitkNRdIjw',
      '9gNEd8FWme8oJn15Jg',
      '9gNEUoR2ZKkgY4Qzce'
    ].reverse()

    for (let i = 1; i < ids.length; i++) {
      let prev = ids[i - 1]
      let next = ids[i]
      lt(toPaddedBigInt(prev), toPaddedBigInt(next))
      gt(toReversePaddedBigInt(prev), toReversePaddedBigInt(next))
    }
  })
})


code:
/* global describe, it, beforeEach, afterEach */

import {
  initShortcuts,
  addShortcutFallback,
  addToShortcutScope,
  onKeyDownInShortcutScope,
  popShortcutScope,
  pushShortcutScope,
  removeFromShortcutScope } from '../../src/routes/_utils/shortcuts'
import assert from 'assert'

function KeyDownEvent (key) {
  this.key = key
  this.metaKey = false
  this.ctrlKey = false
  this.shiftKey = false
  this.altKey = false
  this.target = null
}

function Component (keyDownFunction) {
  this.lastEvent = null
  this.eventCount = 0
  this.onKeyDown = function (event) {
    this.lastEvent = event
    this.eventCount++
  }
  this.pressed = function () {
    return this.eventCount > 0
  }
  this.notPressed = function () {
    return this.eventCount === 0
  }
}

describe('test-shortcuts.js', function () {
  let eventListener
  let originalWindow

  beforeEach(function () {
    originalWindow = global.window
    global.window = {
      addEventListener: function (eventname, listener) {
        assert.strictEqual(eventname, 'keydown')
        eventListener = listener
      },
      removeEventListener: function (eventname, listener) {
        assert.strictEqual(eventname, 'keydown')
        if (listener === eventListener) {
          eventListener = null
        }
      }
    }
    initShortcuts()
  })
  afterEach(function () {
    global.window = originalWindow
  })

  it('sets and unsets event listener', function () {
    let component = new Component()

    addToShortcutScope('global', 'k', component)
    assert(eventListener != null, 'event listener not set')
    removeFromShortcutScope('global', 'k', component)
    assert(eventListener == null, 'event listener not reset')
  })

  it('forwards the right global key event', function () {
    let component = new Component()

    addToShortcutScope('global', 'k', component)

    eventListener(new KeyDownEvent('l'))
    assert.ok(component.notPressed())

    let kEvent = new KeyDownEvent('k')
    eventListener(kEvent)
    assert.ok(component.pressed())
    assert.strictEqual(component.lastEvent, kEvent)
  })

  it('register multiple keys', function () {
    let lmn = new Component()

    addToShortcutScope('global', 'l|m|n', lmn)

    eventListener(new KeyDownEvent('x'))
    assert.strictEqual(lmn.eventCount, 0)
    eventListener(new KeyDownEvent('m'))
    assert.strictEqual(lmn.eventCount, 1)
    eventListener(new KeyDownEvent('l'))
    assert.strictEqual(lmn.eventCount, 2)
    eventListener(new KeyDownEvent('n'))
    assert.strictEqual(lmn.eventCount, 3)
  })

  it('skips events with modifiers', function () {
    let component = new Component()

    addToShortcutScope('global', 'k', component)

    let kEvent = new KeyDownEvent('k')
    kEvent.shiftKey = true
    eventListener(kEvent)
    assert.ok(component.notPressed())

    kEvent = new KeyDownEvent('k')
    kEvent.ctrlKey = true
    eventListener(kEvent)
    assert.ok(component.notPressed())

    kEvent = new KeyDownEvent('k')
    kEvent.metaKey = true
    eventListener(kEvent)
    assert.ok(component.notPressed())
  })

  it('does not skip events for ?', function () {
    let component = new Component()

    addToShortcutScope('global', '?', component)

    let qEvent = new KeyDownEvent('?')
    qEvent.shiftKey = true
    eventListener(qEvent)
    assert.ok(component.pressed())
  })

  it('skips events for editable elements', function () {
    let component = new Component()

    addToShortcutScope('global', 'k', component)

    let kEvent = new KeyDownEvent('k')
    kEvent.target = { isContentEditable: true }
    eventListener(kEvent)
    assert.ok(component.notPressed())
  })

  it('handles multi-key events', function () {
    let a = new Component()
    let ga = new Component()
    let gb = new Component()

    addToShortcutScope('global', 'a', a)
    addToShortcutScope('global', 'g a', ga)
    addToShortcutScope('global', 'g b', gb)

    eventListener(new KeyDownEvent('g'))
    eventListener(new KeyDownEvent('a'))
    assert.ok(ga.pressed())
    assert.ok(gb.notPressed())
    assert.ok(a.notPressed())
  })

  it('falls back to single-key events if no sequence matches', function () {
    let b = new Component()
    let ga = new Component()

    addToShortcutScope('global', 'b', b)
    addToShortcutScope('global', 'g a', ga)

    eventListener(new KeyDownEvent('g'))
    eventListener(new KeyDownEvent('b'))
    assert.ok(b.pressed())
    assert.ok(ga.notPressed())
  })

  it('sends unhandled events to fallback', function () {
    let fallback = new Component()

    addToShortcutScope('global', 'b', new Component())
    addShortcutFallback('global', fallback)

    eventListener(new KeyDownEvent('x'))
    assert.ok(fallback.pressed())
  })

  it('directs events to appropriate component in arbitrary scope', function () {
    let globalB = new Component()
    let inScopeB = new Component()

    addToShortcutScope('global', 'b', globalB)
    addToShortcutScope('inscope', 'b', inScopeB)

    onKeyDownInShortcutScope('inscope', new KeyDownEvent('b'))
    assert.ok(inScopeB.pressed())
    assert.ok(globalB.notPressed())
  })

  it('makes shortcuts modal', function () {
    let globalA = new Component()
    let globalB = new Component()
    let modal1A = new Component()
    let modal2A = new Component()

    addToShortcutScope('global', 'a', globalA)
    addToShortcutScope('global', 'b', globalB)
    addToShortcutScope('modal1', 'a', modal1A)
    addToShortcutScope('modal2', 'a', modal2A)

    pushShortcutScope('modal1')
    pushShortcutScope('modal2')

    eventListener(new KeyDownEvent('b'))
    assert.ok(globalB.notPressed())

    eventListener(new KeyDownEvent('a'))
    assert.ok(globalA.notPressed())
    assert.ok(modal1A.notPressed())
    assert.ok(modal2A.pressed())

    popShortcutScope('modal2')

    eventListener(new KeyDownEvent('a'))
    assert.ok(globalA.notPressed())
    assert.ok(modal1A.pressed())

    popShortcutScope('modal1')

    eventListener(new KeyDownEvent('a'))
    assert.ok(globalA.pressed())
  })

  it('ignores alt key', function () {
    let component = new Component()

    addToShortcutScope('global', '1', component)

    let event = new KeyDownEvent('1')
    event.altKey = true
    eventListener(event)
    assert.ok(component.notPressed())
  })
})


code:
export const users = {
  admin: {
    username: 'admin',
    email: 'admin@localhost:3000',
    password: 'mastodonadmin',
    accessToken: 'f954c8de1fcc0080ff706fa2516d05b60de0d8f5b536255a85ef85a6c32e4afb',
    id: 1
  },
  foobar: {
    username: 'foobar',
    email: 'foobar@localhost:3000',
    password: 'foobarfoobar',
    accessToken: 'b48d72074a467e77a18eafc0d52e373dcf2492bcb3fefadc302a81300ec69002',
    id: 2
  },
  quux: {
    username: 'quux',
    email: 'quux@localhost:3000',
    password: 'quuxquuxquux',
    accessToken: '894d3583dbf7d0f4f4784a06db86bdadb6ef0d99453d15afbc03e0c103bd78af',
    id: 3
  },
  ExternalLinks: {
    username: 'ExternalLinks',
    email: 'ExternalLinks@localhost:3000',
    password: 'ExternalLinksExternalLink',
    accessToken: 'e9a463ba1729ae0049a97a312af702cb3d08d84de1cc8d6da3fad90af068117b',
    id: 4
  },
  baz: {
    username: 'baz',
    email: 'baz@localhost:3000',
    password: 'bazbazbaz',
    accessToken: '0639238783efdfde849304bc89ec0c4b60b5ef5f261f60859fcd597de081cfdc',
    id: 5
  },
  LockedAccount: {
    username: 'LockedAccount',
    email: 'LockedAccount@localhost:3000',
    password: 'LockedAccountLockedAccount',
    accessToken: '39ed9aeffa4b25eda4940f22f29fea66e625c6282c2a8bf0430203c9779e9e98',
    id: 6
  }
}


code:
import { ClientFunction as exec, Selector as $ } from 'testcafe'
import * as images from './images'
import * as blobUtils from './blobUtils'

export const settingsButton = $('nav a[aria-label=Settings]')
export const instanceInput = $('#instanceInput')
export const modalDialog = $('.modal-dialog')
export const visibleModalDialog = $('.modal-dialog:not([aria-hidden="true"])')
export const modalDialogContents = $('.modal-dialog-contents')
export const modalDialogBackdrop = $('.modal-dialog-backdrop')
export const closeDialogButton = $('.modal-dialog:not([aria-hidden="true"]) .close-dialog-button')
export const notificationsNavButton = $('nav a[href="/notifications"]')
export const homeNavButton = $('nav a[href="/"]')
export const localTimelineNavButton = $('nav a[href="/local"]')
export const searchNavButton = $('nav a[href="/search"]')
export const communityNavButton = $('nav a[href="/community"]')
export const settingsNavButton = $('nav a[href="/settings"]')
export const formError = $('.form-error-user-error')
export const composeInput = $('.compose-box-input')
export const composeContentWarning = $('.content-warning-input')
export const composeButton = $('.compose-box-button')
export const composeLengthIndicator = $('.compose-box-length')
export const emojiButton = $('.compose-box-toolbar button:first-child')
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(4)')
export const contentWarningButton = $('.compose-box-toolbar button:nth-child(5)')
export const emailInput = $('input#user_email')
export const passwordInput = $('input#user_password')
export const authorizeInput = $('button[type=submit]:not(.negative)')
export const logInToInstanceLink = $('a[href="/settings/instances/add"]')
export const searchInput = $('.search-input')
export const postStatusButton = $('.compose-box-button')
export const showMoreButton = $('.more-items-header button')
export const accountProfileName = $('.account-profile .account-profile-name')
export const accountProfileUsername = $('.account-profile .account-profile-username')
export const accountProfileFollowedBy = $('.account-profile .account-profile-followed-by')
export const accountProfileFollowButton = $('.account-profile .account-profile-follow button')
export const goBackButton = $('.dynamic-page-go-back')
export const accountProfileMoreOptionsButton = $('.account-profile-more-options button')
export const addInstanceButton = $('#submitButton')
export const mastodonLogInButton = $('button[type="submit"]')
export const followsButton = $('.account-profile-details > *:nth-child(2)')
export const followersButton = $('.account-profile-details > *:nth-child(3)')
export const avatarInComposeBox = $('.compose-box-avatar')
export const displayNameInComposeBox = $('.compose-box-display-name')
export const generalSettingsButton = $('a[href="/settings/general"]')
export const markMediaSensitiveInput = $('#choice-mark-media-sensitive')
export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive')
export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names')
export const disableInfiniteScroll = $('#choice-disable-infinite-scroll')
export const dialogOptionsOption = $(`.modal-dialog button`)
export const emojiSearchInput = $('.emoji-mart-search input')
export const confirmationDialogOKButton = $('.confirmation-dialog-form-flex button:nth-child(1)')
export const confirmationDialogCancelButton = $('.confirmation-dialog-form-flex button:nth-child(2)')

export const loadMoreButton = $('.loading-footer button')

export const composeModalInput = $('.modal-dialog .compose-box-input')
export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(4)')

export const composePoll = $('.compose-poll')
export const composePollMultipleChoice = $('.compose-poll input[type="checkbox"]')
export const composePollExpiry = $('.compose-poll select')
export const composePollAddButton = $('.compose-poll button:last-of-type')

export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')

export const accountProfileFilterStatuses = $('.account-profile-filters li:nth-child(1)')
export const accountProfileFilterStatusesAndReplies = $('.account-profile-filters li:nth-child(2)')
export const accountProfileFilterMedia = $('.account-profile-filters li:nth-child(3)')

export const notificationsTabAll = $('.notification-filters li:nth-child(1)')
export const notificationsTabMentions = $('.notification-filters li:nth-child(2)')

export const instanceSettingHomeReblogs = $('#instance-option-homeReblogs')
export const instanceSettingNotificationFollows = $('#instance-option-notificationFollows')
export const instanceSettingNotificationFavs = $('#instance-option-notificationFavs')
export const instanceSettingNotificationReblogs = $('#instance-option-notificationReblogs')
export const instanceSettingNotificationMentions = $('#instance-option-notificationMentions')

export const notificationBadge = $('#main-nav li:nth-child(2) .nav-link-badge')

export function getComposeModalNthMediaAltInput (n) {
  return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
}

export function getComposeModalNthMediaImg (n) {
  return $(`.modal-dialog .compose-media:nth-child(${n}) img`)
}

export const favoritesCountElement = $('.status-favs').addCustomDOMProperties({
  innerCount: el => parseInt(el.innerText, 10)
})

export const reblogsCountElement = $('.status-reblogs').addCustomDOMProperties({
  innerCount: el => parseInt(el.innerText, 10)
})

export const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout))

export const getUrl = exec(() => window.location.href)

export const getMediaScrollLeft = exec(() => document.querySelector('.media-scroll').scrollLeft || 0)

export const getActiveElementClassList = exec(() =>
  (document.activeElement && (document.activeElement.getAttribute('class') || '').split(/\s+/)) || []
)

export const getActiveElementTagName = exec(() =>
  (document.activeElement && document.activeElement.tagName) || ''
)

export const getActiveElementInnerText = exec(() =>
  (document.activeElement && document.activeElement.innerText) || ''
)

export const getActiveElementRectTop = exec(() => (
  (document.activeElement && document.activeElement.getBoundingClientRect().top) || -1
))

export const getActiveElementAriaPosInSet = exec(() => (
  (document.activeElement && document.activeElement.getAttribute('aria-posinset')) || ''
))

export const getActiveElementInsideNthStatus = exec(() => {
  let element = document.activeElement
  while (element) {
    if (element.hasAttribute('aria-posinset')) {
      return element.getAttribute('aria-posinset')
    }
    element = element.parentElement
  }
  return ''
})

export const getTitleText = exec(() => document.head.querySelector('title') && document.head.querySelector('title').innerHTML)

export const goBack = exec(() => window.history.back())

export const goForward = exec(() => window.history.forward())

export const reload = exec(() => window.location.reload())

export const forceOffline = exec(() => window.__forceOnline(false))

export const forceOnline = exec(() => window.__forceOnline(true))

export const getComposeSelectionStart = exec(() => composeInput().selectionStart, {
  dependencies: { composeInput }
})

export const getOpacity = selector => exec(() => window.getComputedStyle(document.querySelector(selector)).opacity, {
  dependencies: { selector }
})

export const getCurrentTheme = exec(() => {
  let themeLink = document.head.querySelector('link[rel=stylesheet][href^="/theme-"]')
  if (themeLink) {
    return themeLink.getAttribute('href').match(/^\/theme-(.*)\.css$/, '')[1]
  }
  return 'default'
})

export const uploadKittenImage = i => (exec(() => {
  let image = images[`kitten${i}`]
  let blob = blobUtils.base64StringToBlob(image.data, 'image/png')
  blob.name = image.name
  let fileDrop = document.querySelector('file-drop')
  let event = new Event('filedrop', { bubbles: false })
  event.files = [blob]
  fileDrop.dispatchEvent(event)
}, {
  dependencies: {
    images,
    blobUtils,
    i
  }
}))

export const focus = (selector) => (exec(() => {
  document.querySelector(selector).focus()
}, {
  dependencies: {
    selector
  }
}))

export const isNthStatusActive = (idx) => (exec(() => {
  return document.activeElement &&
    document.activeElement.getAttribute('aria-posinset') === idx.toString()
}, {
  dependencies: { idx }
}))

export const isActiveStatusPinned = exec(() => {
  let el = document.activeElement
  return el &&
    (
      (el.parentElement.getAttribute('class') || '').includes('pinned') ||
      (el.parentElement.parentElement.getAttribute('class') || '').includes('pinned')
    )
})

export const scrollToBottom = exec(() => {
  document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight
})

export const scrollToTop = exec(() => {
  document.scrollingElement.scrollTop = 0
})

export const getScrollTop = exec(() => {
  return document.scrollingElement.scrollTop || 0
})

export function getNthMediaAltInput (n) {
  return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt textarea`)
}

export function getNthComposeReplyInput (n) {
  return $(`${getNthStatusSelector(n)} .compose-box-input`)
}

export function getNthComposeReplyButton (n) {
  return $(`${getNthStatusSelector(n)} .compose-box-button`)
}

export function getNthPostPrivacyButton (n) {
  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
}

export function getNthStatusPollOption (n, i) {
  return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i}) input`)
}

export function getNthStatusPollVoteButton (n) {
  return $(`${getNthStatusSelector(n)} .poll button`)
}

export function getNthStatusPollForm (n) {
  return $(`${getNthStatusSelector(n)} .poll form`)
}

export function getNthStatusPollResult (n, i) {
  return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i})`)
}

export function getNthStatusPollRefreshButton (n) {
  return $(`${getNthStatusSelector(n)} button.poll-stat`)
}

export function getNthStatusPollVoteCount (n) {
  return $(`${getNthStatusSelector(n)} .poll .poll-stat:nth-child(1) .poll-stat-text`)
}

export function getComposePollNthInput (n) {
  return $(`.compose-poll input[type="text"]:nth-of-type(${n})`)
}

export function getComposePollRemoveNthButton (n) {
  return $(`.compose-poll button:nth-of-type(${n})`)
}

export function getNthAutosuggestionResult (n) {
  return $(`.compose-autosuggest-list-item:nth-child(${n})`)
}

export function getSearchResultByHref (href) {
  return $(`.search-result a[href="${href}"]`)
}

export function getNthSearchResult (n) {
  return $(`.search-result:nth-child(${n}) a`)
}

export function getNthMedia (n) {
  return $(`.compose-media:nth-child(${n}) img`)
}

export function getNthDeleteMediaButton (n) {
  return $(`.compose-media:nth-child(${n}) .compose-media-delete-button`)
}

export function getAriaSetSize () {
  return getNthStatus(1 + 0).getAttribute('aria-setsize')
}

export function getNthStatus (n) {
  return $(getNthStatusSelector(n))
}

export function getNthStatusSelector (n) {
  return `.list-item > article[aria-posinset="${n}"]`
}

export function getNthStatusContent (n) {
  return $(`${getNthStatusSelector(n)} .status-content`)
}

export function getNthStatusSpoiler (n) {
  return $(`${getNthStatusSelector(n)} .status-spoiler`)
}

export function getNthStatusSensitiveMediaButton (n) {
  return $(`${getNthStatusSelector(n)} .status-sensitive-media-button`)
}

export function getNthStatusMedia (n) {
  return $(`${getNthStatusSelector(n)} .status-media`)
}

export function getNthStatusMediaButton (n) {
  return $(`${getNthStatusSelector(n)} .status-media button`)
}

export function getNthStatusRelativeDate (n) {
  return $(`${getNthStatusSelector(n)} .status-relative-date`)
}

export function getNthStatusMediaImg (n) {
  return $(`${getNthStatusSelector(n)} .status-media img`)
}

export function getNthStatusHeader (n) {
  return $(`${getNthStatusSelector(n)} .status-header`)
}

export function getNthStatusAndImage (nStatus, nImage) {
  return $(`${getNthStatusSelector(nStatus)} .status-media .show-image-button:nth-child(${nImage + 1}) img`)
}

export function getFirstVisibleStatus () {
  return $(`.list-item > article[aria-posinset]`).nth(0)
}

export function getNthReplyButton (n) {
  return $(`${getNthStatusSelector(n)} .status-toolbar button:nth-child(1)`)
}

export function getNthReplyContentWarningInput (n) {
  return $(`${getNthStatusSelector(n)} .content-warning-input`)
}

export function getNthReplyContentWarningButton (n) {
  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(5)`)
}

export function getNthReplyPostPrivacyButton (n) {
  return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
}

export function getNthPostPrivacyOptionInDialog (n) {
  return $(`.generic-dialog-list li:nth-child(${n}) button`)
}

export function getNthFavoriteButton (n) {
  return $(`${getNthStatusSelector(n)} .status-toolbar button:nth-child(3)`)
}

export function getNthStatusOptionsButton (n) {
  return $(`${getNthStatusSelector(n)} .status-toolbar button:nth-child(4)`)
}

export function getNthFavorited (n) {
  return getNthFavoriteButton(n).getAttribute('aria-pressed')
}

export function getNthShowOrHideButton (n) {
  return $(`${getNthStatusSelector(n)} .status-spoiler-button button`)
}

export function getFavoritesCount () {
  return favoritesCountElement.innerCount
}

export function getNthReblogButton (n) {
  return $(`${getNthStatusSelector(n)} .status-toolbar button:nth-child(2)`)
}

export function getNthReblogged (n) {
  return getNthReblogButton(n).getAttribute('aria-pressed')
}

export function getNthDialogOptionsOption (n) {
  return $(`.modal-dialog li:nth-child(${n}) button`)
}

export function getReblogsCount () {
  return reblogsCountElement.innerCount
}

function getNthPinnedStatusSelector (n) {
  return `.pinned-statuses article[aria-posinset="${n}"]`
}

export function getNthPinnedStatus (n) {
  return $(getNthPinnedStatusSelector(n))
}

export function getNthPinnedStatusFavoriteButton (n) {
  return $(`${getNthPinnedStatusSelector(n)} .status-toolbar button:nth-child(3)`)
}

export async function validateTimeline (t, timeline) {
  const timeout = 30000
  for (let i = 0; i < timeline.length; i++) {
    let status = timeline[i]
    // hovering forces TestCafé to scroll to that element: https://git.io/vABV2
    await t.hover(getNthStatus(1 + i))
    if (status.content) {
      await t.expect(getNthStatusContent(1 + i).innerText)
        .contains(status.content, { timeout })
    }
    if (status.spoiler) {
      await t.expect(getNthStatusSpoiler(1 + i).innerText)
        .contains(status.spoiler, { timeout })
    }
    if (status.followedBy) {
      await t.expect(getNthStatusHeader(1 + i).innerText)
        .match(new RegExp(status.followedBy + '\\s+followed you'), { timeout })
    }
    if (status.rebloggedBy) {
      await t.expect(getNthStatusHeader(1 + i).innerText)
        .match(new RegExp(status.rebloggedBy + '\\s+boosted your status'), { timeout })
    }
    if (status.favoritedBy) {
      await t.expect(getNthStatusHeader(1 + i).innerText)
        .match(new RegExp(status.favoritedBy + '\\s+favorited your status'), { timeout })
    }
  }
}

export async function scrollToStatus (t, n) {
  return scrollFromStatusToStatus(t, 1, n)
}

export async function scrollFromStatusToStatus (t, start, end) {
  let timeout = 20000
  for (let i = start; i < end; i++) {
    await t.expect(getNthStatus(i).exists).ok({ timeout })
      .hover(getNthStatus(i))
      .expect($(`${getNthStatusSelector(i)} .status-toolbar`).exists).ok({ timeout })
      .hover($(`${getNthStatusSelector(i)} .status-toolbar`))
  }
  await t
    .expect(getNthStatus(end).exists).ok({ timeout })
    .hover(getNthStatus(end))
}

export async function clickToNotificationsAndBackHome (t) {
  await t.click(notificationsNavButton)
    .expect(getUrl()).eql('http://localhost:4002/notifications')
    .click(homeNavButton)
    .expect(getUrl()).eql('http://localhost:4002/')
}

// like lodash.times but I don't want to try to figure out esm
// just to import lodash-es
export function times (n, cb) {
  let arr = []
  for (let i = 0; i < n; i++) {
    arr.push(cb(i))
  }
  return arr
}


code:
const client = require('./webpack/client.config')
const server = require('./webpack/server.config')
const serviceworker = require('./webpack/service-worker.config')

module.exports = {
  client,
  server,
  serviceworker
}


code:
const webpack = require('webpack')
const config = require('sapper/config/webpack.js')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
const terser = require('./terser.config')
const CircularDependencyPlugin = require('circular-dependency-plugin')
const { mode, dev, resolve, inlineSvgs } = require('./shared.config')

const urlRegex = require('../src/routes/_utils/urlRegexSource.js')()

const output = Object.assign(config.client.output(), {
  // enables HMR in workers
  globalObject: 'this',
  // Zeit does not like filenames with "$" in them, so just keep things simple
  filename: '[hash]/[id].js',
  chunkFilename: '[hash]/[id].js'
})

module.exports = {
  entry: config.client.entry(),
  output,
  resolve,
  mode,
  module: {
    rules: [
      {
        test: /\.html$/,
        use: {
          loader: 'svelte-loader',
          options: {
            dev,
            hydratable: true,
            store: true,
            hotReload: dev
          }
        }
      },
      {
        test: /\.m?js$/,
        include: /node_modules\/emoji-mart/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [
              [
                'transform-react-remove-prop-types',
                {
                  removeImport: true,
                  additionalLibraries: [
                    '../../utils/shared-props'
                  ]
                }
              ]
            ]
          }
        }
      }
    ].filter(Boolean)
  },
  node: {
    setImmediate: false
  },
  optimization: dev ? {} : {
    minimizer: [
      terser()
    ],
    splitChunks: {
      chunks: 'async',
      minSize: 5000,
      maxAsyncRequests: Infinity,
      maxInitialRequests: Infinity,
      name: false // these chunk names can be annoyingly long
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.browser': true,
      'process.env.NODE_ENV': JSON.stringify(mode),
      'process.env.INLINE_SVGS': JSON.stringify(inlineSvgs),
      'process.env.URL_REGEX': urlRegex.toString()
    }),
    new webpack.NormalModuleReplacementPlugin(
      /\/_database\/database\.js$/, // this version plays nicer with IDEs
      './database.prod.js'
    ),
    new LodashModuleReplacementPlugin(),
    new CircularDependencyPlugin({
      exclude: /node_modules/,
      failOnError: true,
      cwd: process.cwd()
    })
  ].concat(dev ? [
    new webpack.HotModuleReplacementPlugin({
      requestTimeout: 120000
    })
  ] : [

    new BundleAnalyzerPlugin({ // generates report.html
      analyzerMode: 'static',
      openAnalyzer: false,
      logLevel: 'silent'
    })
  ]),
  devtool: dev ? 'inline-source-map' : 'source-map',
  performance: {
    hints: dev ? false : 'error' // fail if we exceed the default performance budgets
  }
}


code:
const webpack = require('webpack')
const config = require('sapper/config/webpack.js')
const pkg = require('../package.json')
const { mode, dev, resolve, inlineSvgs } = require('./shared.config')

const serverResolve = JSON.parse(JSON.stringify(resolve))
serverResolve.alias['page-lifecycle/dist/lifecycle.mjs'] = 'lodash-es/noop' // page lifecycle fails in Node

module.exports = {
  entry: config.server.entry(),
  output: config.server.output(),
  target: 'node',
  resolve: serverResolve,
  externals: Object.keys(pkg.dependencies),
  module: {
    rules: [
      {
        test: /\.html$/,
        exclude: /node_modules/,
        use: {
          loader: 'svelte-loader',
          options: {
            css: false,
            store: true,
            generate: 'ssr',
            dev
          }
        }
      }
    ]
  },
  mode,
  performance: {
    hints: false // it doesn't matter if server.js is large
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.INLINE_SVGS': JSON.stringify(inlineSvgs)
    })
  ]
}


code:
const config = require('sapper/config/webpack.js')
const terser = require('./terser.config')
const webpack = require('webpack')
const { mode, dev, resolve } = require('./shared.config')

module.exports = {
  entry: config.serviceworker.entry(),
  output: config.serviceworker.output(),
  resolve,
  mode,
  devtool: dev ? 'inline-source-map' : 'source-map',
  plugins: [
    new webpack.DefinePlugin({
      'process.browser': true,
      'process.env.NODE_ENV': JSON.stringify(mode),
      'process.env.SAPPER_TIMESTAMP': process.env.SAPPER_TIMESTAMP || Date.now()
    }),
    terser()
  ]
}


code:
const svgs = require('../bin/svgs')

const inlineSvgs = svgs.filter(_ => _.inline).map(_ => `#${_.id}`)
const mode = process.env.NODE_ENV || 'production'
const dev = mode === 'development'

const resolve = {
  extensions: ['.js', '.json', '.html'],
  mainFields: ['svelte', 'module', 'browser', 'main'],
  alias: {
    'react': 'preact/compat/dist/compat.module.js',
    'react-dom': 'preact/compat/dist/compat.module.js'
  }
}

module.exports = {
  mode,
  dev,
  resolve,
  inlineSvgs
}


code:
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = () => new TerserWebpackPlugin({
  cache: true,
  parallel: true,
  sourceMap: true,
  terserOptions: {
    ecma: 6,
    mangle: true,
    compress: {
      pure_funcs: ['console.log']
    },
    output: {
      comments: false
    },
    safari10: true
  }
})