add favorite/unfavorite feature
This commit is contained in:
parent
3a17f7ff7b
commit
1b7a01f1ee
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,4 +4,5 @@ node_modules
|
|||
yarn.lock
|
||||
templates/.*
|
||||
assets/*.css
|
||||
/mastodon
|
||||
/mastodon
|
||||
mastodon.log
|
|
@ -60,13 +60,10 @@ async function runMastodon () {
|
|||
await exec(cmd, {cwd: mastodonDir})
|
||||
}
|
||||
const promise = spawn('foreman', ['start'], {cwd: mastodonDir})
|
||||
const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
|
||||
childProc = promise.childProcess
|
||||
childProc.stdout.on('data', function (data) {
|
||||
console.log(data.toString('utf8').replace(/\n$/, ''))
|
||||
})
|
||||
childProc.stderr.on('data', function (data) {
|
||||
console.error(data.toString('utf8').replace(/\n$/, ''))
|
||||
})
|
||||
childProc.stdout.pipe(log)
|
||||
childProc.stderr.pipe(log)
|
||||
|
||||
await waitForMastodonToStart()
|
||||
}
|
||||
|
|
|
@ -3,20 +3,28 @@ import { store } from '../_store/store'
|
|||
import { database } from '../_database/database'
|
||||
import { toast } from '../_utils/toast'
|
||||
|
||||
export async function setFavorited(statusId, favorited) {
|
||||
export async function setFavorited (statusId, favorited) {
|
||||
if (!store.get('online')) {
|
||||
toast.say('You cannot favorite or unfavorite while offline.')
|
||||
return
|
||||
}
|
||||
let instanceName = store.get('currentInstance')
|
||||
let accessToken = store.get('accessToken')
|
||||
try {
|
||||
let status = await (favorited
|
||||
let result = await (favorited
|
||||
? favoriteStatus(instanceName, accessToken, statusId)
|
||||
: unfavoriteStatus(instanceName, accessToken, statusId))
|
||||
await database.insertStatus(instanceName, status)
|
||||
if (result.error) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
await database.setStatusFavorited(instanceName, statusId, favorited)
|
||||
let statusModifications = store.get('statusModifications')
|
||||
let currentStatusModifications = statusModifications[instanceName] =
|
||||
(statusModifications[instanceName] || {favorites: {}, reblogs: {}})
|
||||
currentStatusModifications.favorites[statusId] = favorited
|
||||
store.set({statusModifications: statusModifications})
|
||||
} catch (e) {
|
||||
toast.say('Failed to favorite/unfavorite. Please try again.')
|
||||
console.error(e)
|
||||
toast.say('Failed to favorite or unfavorite. ' + (e.message || ''))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
|
||||
let url = `${basename(instanceName)}/api/v1/blocks`
|
||||
url += '?' + paramsString({ limit })
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
||||
export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
|
||||
let url = `${basename(instanceName)}/api/v1/mutes`
|
||||
url += '?' + paramsString({ limit })
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { post } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { basename, auth } from './utils'
|
||||
|
||||
export async function favoriteStatus(instanceName, accessToken, statusId) {
|
||||
export async function favoriteStatus (instanceName, accessToken, statusId) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourite`
|
||||
return post(url, null, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return post(url, null, auth(accessToken))
|
||||
}
|
||||
|
||||
export async function unfavoriteStatus(instanceName, accessToken, statusId) {
|
||||
export async function unfavoriteStatus (instanceName, accessToken, statusId) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
|
||||
return post(url, null, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
}
|
||||
return post(url, null, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { getWithTimeout } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export function getLists (instanceName, accessToken) {
|
||||
let url = `${basename(instanceName)}/api/v1/lists`
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export async function getPinnedStatuses (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/statuses`
|
||||
|
@ -7,7 +7,5 @@ export async function getPinnedStatuses (instanceName, accessToken, accountId) {
|
|||
limit: 40,
|
||||
pinned: true
|
||||
})
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
|
||||
url += '?' + paramsString({ limit })
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
||||
export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
|
||||
url += '?' + paramsString({ limit })
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export function search (instanceName, accessToken, query) {
|
||||
let url = `${basename(instanceName)}/api/v1/search?` + paramsString({
|
||||
q: query,
|
||||
resolve: true
|
||||
})
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
function getTimelineUrlPath (timeline) {
|
||||
switch (timeline) {
|
||||
|
@ -57,14 +57,12 @@ export function getTimeline (instanceName, accessToken, timeline, maxId, since)
|
|||
// special case - this is a list of descendents and ancestors
|
||||
let statusUrl = `${basename(instanceName)}/api/v1/statuses/${timeline.split('/').slice(-1)[0]}}`
|
||||
return Promise.all([
|
||||
getWithTimeout(url, {'Authorization': `Bearer ${accessToken}`}),
|
||||
getWithTimeout(statusUrl, {'Authorization': `Bearer ${accessToken}`})
|
||||
getWithTimeout(url, auth(accessToken)),
|
||||
getWithTimeout(statusUrl, auth(accessToken))
|
||||
]).then(res => {
|
||||
return [].concat(res[0].ancestors).concat([res[1]]).concat(res[0].descendants)
|
||||
})
|
||||
}
|
||||
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||
import { basename } from './utils'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export function getVerifyCredentials (instanceName, accessToken) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
||||
export function getAccount (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
|
||||
return getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
return getWithTimeout(url, auth(accessToken))
|
||||
}
|
||||
|
||||
export async function getRelationship (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/relationships`
|
||||
url += '?' + paramsString({id: accountId})
|
||||
let res = await getWithTimeout(url, {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
})
|
||||
let res = await getWithTimeout(url, auth(accessToken))
|
||||
return res[0]
|
||||
}
|
||||
|
|
|
@ -13,3 +13,9 @@ export function basename (instanceName) {
|
|||
}
|
||||
return `https://${instanceName}`
|
||||
}
|
||||
|
||||
export function auth (accessToken) {
|
||||
return {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
<button type="button"
|
||||
aria-label="{{label}}"
|
||||
aria-pressed="{{!!pressed}}"
|
||||
class="icon-button {{pressed ? 'pressed' : ''}} {{big ? 'big-icon' : ''}}"
|
||||
class="{{computedClass}}"
|
||||
disabled="{{disabled}}"
|
||||
delegate-click-key="{{delegateKey}}"
|
||||
delegate-keydown-key="{{delegateKey}}"
|
||||
on:click
|
||||
>
|
||||
<svg>
|
||||
|
@ -13,8 +15,10 @@
|
|||
{{else}}
|
||||
<button type="button"
|
||||
aria-label="{{label}}"
|
||||
class="icon-button {{big ? 'big-icon' : ''}}"
|
||||
class="{{computedClass}}"
|
||||
disabled="{{disabled}}"
|
||||
delegate-click-key="{{delegateKey}}"
|
||||
delegate-keydown-key="{{delegateKey}}"
|
||||
on:click
|
||||
>
|
||||
<svg>
|
||||
|
@ -44,7 +48,7 @@
|
|||
fill: var(--action-button-fill-color-hover);
|
||||
}
|
||||
|
||||
button.icon-button:active svg {
|
||||
button.icon-button.not-pressable:active svg {
|
||||
fill: var(--action-button-fill-color-active);
|
||||
}
|
||||
|
||||
|
@ -59,4 +63,20 @@
|
|||
button.icon-button.pressed:active svg {
|
||||
fill: var(--action-button-fill-color-pressed-active);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
<script>
|
||||
import identity from 'lodash/identity'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
computedClass: (pressable, pressed, big) => {
|
||||
return [
|
||||
'icon-button',
|
||||
!pressable && 'not-pressable',
|
||||
pressed && 'pressed',
|
||||
big && 'big-icon',
|
||||
].filter(identity).join(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -28,7 +28,7 @@
|
|||
{{#if isStatusInOwnThread}}
|
||||
<StatusDetails status="{{originalStatus}}" />
|
||||
{{/if}}
|
||||
<StatusToolbar :status :isStatusInOwnThread />
|
||||
<StatusToolbar status="{{originalStatus}}" :isStatusInOwnThread :timelineType :timelineValue />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
pressable="true"
|
||||
pressed="{{favorited}}"
|
||||
href="#fa-star"
|
||||
delegateKey="{{favoriteKey}}"
|
||||
ref:favoriteNode
|
||||
/>
|
||||
<IconButton
|
||||
label="Show more actions"
|
||||
|
@ -34,12 +36,33 @@
|
|||
<script>
|
||||
import IconButton from '../IconButton.html'
|
||||
import { store } from '../../_store/store'
|
||||
import { registerDelegate, unregisterDelegate } from '../../_utils/delegate'
|
||||
import { setFavorited } from '../../_actions/favorite'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
this.onFavoriteClick = this.onFavoriteClick.bind(this)
|
||||
|
||||
let favoriteKey = this.get('favoriteKey')
|
||||
registerDelegate('click', favoriteKey, this.onFavoriteClick)
|
||||
registerDelegate('keydown', favoriteKey, this.onFavoriteClick)
|
||||
},
|
||||
ondestroy() {
|
||||
let favoriteKey = this.get('favoriteKey')
|
||||
unregisterDelegate('click', favoriteKey, this.onFavoriteClick)
|
||||
unregisterDelegate('keydown', favoriteKey, this.onFavoriteClick)
|
||||
},
|
||||
components: {
|
||||
IconButton
|
||||
},
|
||||
store: () => store,
|
||||
methods: {
|
||||
onFavoriteClick() {
|
||||
let statusId = this.get('statusId')
|
||||
let favorited = this.get('favorited')
|
||||
/* no await */ setFavorited(statusId, !favorited)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibility: (status) => status.visibility,
|
||||
boostLabel: (visibility) => {
|
||||
|
@ -70,7 +93,9 @@
|
|||
return $currentStatusModifications.favorites[status.id]
|
||||
}
|
||||
return status.favourited
|
||||
}
|
||||
},
|
||||
statusId: (status) => status.id,
|
||||
favoriteKey: (statusId, timelineType, timelineValue) => `fav-${timelineType}-${timelineValue}-${statusId}`
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -390,13 +390,29 @@ export async function getNotificationIdsForStatus (instanceName, statusId) {
|
|||
}
|
||||
|
||||
//
|
||||
// insert statuses
|
||||
// update statuses
|
||||
//
|
||||
|
||||
export async function insertStatus(instanceName, status) {
|
||||
async function updateStatus (instanceName, statusId, updateFunc) {
|
||||
const db = await getDatabase(instanceName)
|
||||
cacheStatus(statusesCache, status)
|
||||
if (hasInCache(statusesCache, instanceName, statusId)) {
|
||||
let status = getInCache(statusesCache, instanceName, statusId)
|
||||
updateFunc(status)
|
||||
cacheStatus(status, instanceName)
|
||||
}
|
||||
return dbPromise(db, STATUSES_STORE, 'readwrite', (statusesStore) => {
|
||||
putStatus(statusesStore, status)
|
||||
statusesStore.get(statusId).onsuccess = e => {
|
||||
let status = e.target.result
|
||||
updateFunc(status)
|
||||
putStatus(statusesStore, status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function setStatusFavorited (instanceName, statusId, favorited) {
|
||||
return updateStatus(instanceName, statusId, status => {
|
||||
let delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
|
||||
status.favourited = favorited
|
||||
status.favourites_count = (status.favourites_count || 0) + delta
|
||||
})
|
||||
}
|
||||
|
|
|
@ -97,8 +97,8 @@ export function instanceComputations (store) {
|
|||
)
|
||||
|
||||
store.compute('currentStatusModifications',
|
||||
['statusModifications', 'instanceName'],
|
||||
(statusModifications, instanceName) => {
|
||||
return statusModifications[instanceName]
|
||||
['statusModifications', 'currentInstance'],
|
||||
(statusModifications, currentInstance) => {
|
||||
return statusModifications[currentInstance]
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,14 +9,21 @@ function fetchWithTimeout (url, options) {
|
|||
|
||||
async function _post (url, body, headers, timeout) {
|
||||
let fetchFunc = timeout ? fetchWithTimeout : fetch
|
||||
return (await fetchFunc(url, {
|
||||
method: 'POST',
|
||||
headers: Object.assign(headers, {
|
||||
let opts = {
|
||||
method: 'POST'
|
||||
}
|
||||
if (body) {
|
||||
opts.headers = Object.assign(headers, {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
body: JSON.stringify(body)
|
||||
})).json()
|
||||
})
|
||||
opts.body = JSON.stringify(body)
|
||||
} else {
|
||||
opts.headers = Object.assign(headers, {
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
}
|
||||
return (await fetchFunc(url, opts)).json()
|
||||
}
|
||||
|
||||
async function _get (url, headers, timeout) {
|
||||
|
@ -51,4 +58,4 @@ export function paramsString (paramsObject) {
|
|||
params.set(key, paramsObject[key])
|
||||
})
|
||||
return params.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">
|
||||
|
||||
{{#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch}}
|
||||
<div class="form-error" role="alert">
|
||||
<div class="form-error form-error-user-error" role="alert">
|
||||
Error: {{$logInToInstanceError}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { Selector as $ } from 'testcafe'
|
||||
import { addInstanceButton, getUrl, instanceInput, settingsButton } from '../utils'
|
||||
import { addInstanceButton, formError, getUrl, instanceInput, settingsButton } from '../utils'
|
||||
|
||||
fixture`02-login-spec.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
const formError = $('.form-error')
|
||||
|
||||
function manualLogin (t, username, password) {
|
||||
return t.click($('a').withText('log in to an instance'))
|
||||
.expect(getUrl()).contains('/settings/instances/add')
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import { Selector as $ } from 'testcafe'
|
||||
import { getUrl, validateTimeline } from '../utils'
|
||||
import { getFirstVisibleStatus, getUrl, validateTimeline } from '../utils'
|
||||
import { homeTimeline, notifications, localTimeline, favorites } from '../fixtures'
|
||||
import { foobarRole } from '../roles'
|
||||
|
||||
fixture`03-basic-timeline-spec.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
const firstArticle = $('.virtual-list-item[aria-hidden=false] .status-article')
|
||||
|
||||
test('Shows the home timeline', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.expect(firstArticle.hasAttribute('aria-setsize')).ok()
|
||||
.expect(firstArticle.getAttribute('aria-posinset')).eql('0')
|
||||
.expect(getFirstVisibleStatus().exists).ok()
|
||||
.expect(getFirstVisibleStatus().hasAttribute('aria-setsize')).ok()
|
||||
.expect(getFirstVisibleStatus().getAttribute('aria-posinset')).eql('0')
|
||||
|
||||
await validateTimeline(t, homeTimeline)
|
||||
|
||||
await t.expect(firstArticle.getAttribute('aria-setsize')).eql('49')
|
||||
await t.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('49')
|
||||
})
|
||||
|
||||
test('Shows notifications', async t => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Selector as $ } from 'testcafe'
|
||||
import { getNthStatus, getUrl } from '../utils'
|
||||
import { getFavoritesCount, getNthStatus, getReblogsCount, getUrl } from '../utils'
|
||||
import { foobarRole } from '../roles'
|
||||
|
||||
fixture`11-reblog-favorites-count.js`
|
||||
|
@ -9,7 +9,7 @@ test('shows favorites', async t => {
|
|||
await t.useRole(foobarRole)
|
||||
.click(getNthStatus(0))
|
||||
.expect(getUrl()).contains('/statuses/99549266679020981')
|
||||
.expect($('.status-favs-reblogs').nth(0).getAttribute('aria-label')).eql('Favorited 2 times')
|
||||
.expect(getFavoritesCount()).eql(2)
|
||||
.expect($('.icon-button[aria-label="Favorite"]').getAttribute('aria-pressed')).eql('true')
|
||||
.click($('.status-favs-reblogs').nth(1))
|
||||
.expect(getUrl()).contains('/statuses/99549266679020981/favorites')
|
||||
|
@ -23,7 +23,7 @@ test('shows boosts', async t => {
|
|||
await t.useRole(foobarRole)
|
||||
.click(getNthStatus(0))
|
||||
.expect(getUrl()).contains('/statuses/99549266679020981')
|
||||
.expect($('.status-favs-reblogs').nth(1).getAttribute('aria-label')).eql('Boosted 1 time')
|
||||
.expect(getReblogsCount()).eql(1)
|
||||
.expect($('.icon-button[aria-label="Boost"]').getAttribute('aria-pressed')).eql('false')
|
||||
.click($('.status-favs-reblogs').nth(0))
|
||||
.expect(getUrl()).contains('/statuses/99549266679020981/reblogs')
|
||||
|
|
75
tests/spec/12-favorite-unfavorite.js
Normal file
75
tests/spec/12-favorite-unfavorite.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
getFavoritesCount,
|
||||
getNthFavoriteButton, getNthFavorited, getNthStatus, getUrl, homeNavButton, notificationsNavButton,
|
||||
scrollToBottomOfTimeline, scrollToTopOfTimeline
|
||||
} from '../utils'
|
||||
import { foobarRole } from '../roles'
|
||||
|
||||
fixture`12-favorite-unfavorite.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('favorites a status', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.hover(getNthStatus(4))
|
||||
.expect(getNthFavorited(4)).eql('false')
|
||||
.click(getNthFavoriteButton(4))
|
||||
.expect(getNthFavorited(4)).eql('true')
|
||||
|
||||
// scroll down and back up to force an unrender
|
||||
await scrollToBottomOfTimeline(t)
|
||||
await scrollToTopOfTimeline(t)
|
||||
await t
|
||||
.hover(getNthStatus(4))
|
||||
.expect(getNthFavorited(4)).eql('true')
|
||||
.click(notificationsNavButton)
|
||||
.click(homeNavButton)
|
||||
.expect(getNthFavorited(4)).eql('true')
|
||||
.click(notificationsNavButton)
|
||||
.expect(getUrl()).contains('/notifications')
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getNthFavorited(4)).eql('true')
|
||||
.click(getNthFavoriteButton(4))
|
||||
.expect(getNthFavorited(4)).eql('false')
|
||||
})
|
||||
|
||||
test('unfavorites a status', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.expect(getNthFavorited(1)).eql('true')
|
||||
.click(getNthFavoriteButton(1))
|
||||
.expect(getNthFavorited(1)).eql('false')
|
||||
|
||||
// scroll down and back up to force an unrender
|
||||
await scrollToBottomOfTimeline(t)
|
||||
await scrollToTopOfTimeline(t)
|
||||
await t
|
||||
.expect(getNthFavorited(1)).eql('false')
|
||||
.click(notificationsNavButton)
|
||||
.click(homeNavButton)
|
||||
.expect(getNthFavorited(1)).eql('false')
|
||||
.click(notificationsNavButton)
|
||||
.navigateTo('/')
|
||||
.expect(getNthFavorited(1)).eql('false')
|
||||
.click(getNthFavoriteButton(1))
|
||||
.expect(getNthFavorited(1)).eql('true')
|
||||
})
|
||||
|
||||
test('Keeps the correct count', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.hover(getNthStatus(4))
|
||||
.click(getNthFavoriteButton(4))
|
||||
.expect(getNthFavorited(4)).eql('true')
|
||||
.click(getNthStatus(4))
|
||||
.expect(getUrl()).contains('/status')
|
||||
.expect(getNthFavorited(0)).eql('true')
|
||||
.expect(getFavoritesCount()).eql(2)
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.hover(getNthStatus(4))
|
||||
.click(getNthFavoriteButton(4))
|
||||
.expect(getNthFavorited(4)).eql('false')
|
||||
.click(getNthStatus(4))
|
||||
.expect(getUrl()).contains('/status')
|
||||
.expect(getNthFavorited(0)).eql('false')
|
||||
.expect(getFavoritesCount()).eql(1)
|
||||
})
|
|
@ -1,10 +1,23 @@
|
|||
import { ClientFunction as exec, Selector as $ } from 'testcafe'
|
||||
|
||||
const SCROLL_INTERVAL = 3
|
||||
|
||||
export const settingsButton = $('nav a[aria-label=Settings]')
|
||||
export const instanceInput = $('#instanceInput')
|
||||
export const addInstanceButton = $('.add-new-instance button')
|
||||
export const modalDialogContents = $('.modal-dialog-contents')
|
||||
export const closeDialogButton = $('.close-dialog-button')
|
||||
export const notificationsNavButton = $('nav a[href="/notifications"]')
|
||||
export const homeNavButton = $('nav a[href="/"]')
|
||||
export const formError = $('.form-error-user-error')
|
||||
|
||||
export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
|
||||
innerCount: el => parseInt(el.innerText, 10)
|
||||
})
|
||||
|
||||
export const reblogsCountElement = $('.status-favs-reblogs:nth-child(2)').addCustomDOMProperties({
|
||||
innerCount: el => parseInt(el.innerText, 10)
|
||||
})
|
||||
|
||||
export const getUrl = exec(() => window.location.href)
|
||||
|
||||
|
@ -15,11 +28,31 @@ export const getActiveElementClass = exec(() =>
|
|||
export const goBack = exec(() => window.history.back())
|
||||
|
||||
export function getNthStatus (n) {
|
||||
return $(`[aria-hidden="false"] > article[aria-posinset="${n}"]`)
|
||||
return $(`div[aria-hidden="false"] > article[aria-posinset="${n}"]`)
|
||||
}
|
||||
|
||||
export function getLastVisibleStatus () {
|
||||
return $(`[aria-hidden="false"] > article[aria-posinset]`).nth(-1)
|
||||
return $(`div[aria-hidden="false"] > article[aria-posinset]`).nth(-1)
|
||||
}
|
||||
|
||||
export function getFirstVisibleStatus () {
|
||||
return $(`div[aria-hidden="false"] > article[aria-posinset]`).nth(0)
|
||||
}
|
||||
|
||||
export function getNthFavoriteButton (n) {
|
||||
return getNthStatus(n).find('.status-toolbar button:nth-child(3)')
|
||||
}
|
||||
|
||||
export function getNthFavorited (n) {
|
||||
return getNthFavoriteButton(n).getAttribute('aria-pressed')
|
||||
}
|
||||
|
||||
export function getFavoritesCount () {
|
||||
return favoritesCountElement.innerCount
|
||||
}
|
||||
|
||||
export function getReblogsCount () {
|
||||
return reblogsCountElement.innerCount
|
||||
}
|
||||
|
||||
export async function validateTimeline (t, timeline) {
|
||||
|
@ -47,23 +80,47 @@ export async function validateTimeline (t, timeline) {
|
|||
}
|
||||
|
||||
// hovering forces TestCafé to scroll to that element: https://git.io/vABV2
|
||||
if (i % 3 === 2) { // only scroll every nth element
|
||||
if (i % SCROLL_INTERVAL === (SCROLL_INTERVAL - 1)) { // only scroll every nth element
|
||||
await t.hover(getNthStatus(i))
|
||||
.expect($('.loading-footer').exist).notOk()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function scrollToBottomOfTimeline (t) {
|
||||
let lastSize = null
|
||||
export async function scrollTimelineUp (t) {
|
||||
let oldFirstItem = await getFirstVisibleStatus().getAttribute('aria-posinset')
|
||||
await t.hover(getFirstVisibleStatus())
|
||||
let newFirstItem
|
||||
while (true) {
|
||||
await t.hover(getLastVisibleStatus())
|
||||
.expect($('.loading-footer').exist).notOk()
|
||||
let newSize = await getLastVisibleStatus().getAttribute('aria-setsize')
|
||||
if (newSize === lastSize) {
|
||||
newFirstItem = await getFirstVisibleStatus().getAttribute('aria-posinset')
|
||||
if (newFirstItem === '0' || newFirstItem !== oldFirstItem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function scrollToTopOfTimeline (t) {
|
||||
let i = await getFirstVisibleStatus().getAttribute('aria-posinset')
|
||||
while (true) {
|
||||
await t.hover(getNthStatus(i))
|
||||
.expect($('.loading-footer').exist).notOk()
|
||||
i -= SCROLL_INTERVAL
|
||||
if (i <= 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function scrollToBottomOfTimeline (t) {
|
||||
let i = 0
|
||||
while (true) {
|
||||
await t.hover(getNthStatus(i))
|
||||
.expect($('.loading-footer').exist).notOk()
|
||||
let size = await getNthStatus(i).getAttribute('aria-setsize')
|
||||
i += SCROLL_INTERVAL
|
||||
if (i >= size - 1) {
|
||||
break
|
||||
}
|
||||
lastSize = newSize
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue