add favorite/unfavorite feature
This commit is contained in:
parent
3a17f7ff7b
commit
1b7a01f1ee
|
@ -4,4 +4,5 @@ node_modules
|
||||||
yarn.lock
|
yarn.lock
|
||||||
templates/.*
|
templates/.*
|
||||||
assets/*.css
|
assets/*.css
|
||||||
/mastodon
|
/mastodon
|
||||||
|
mastodon.log
|
|
@ -60,13 +60,10 @@ async function runMastodon () {
|
||||||
await exec(cmd, {cwd: mastodonDir})
|
await exec(cmd, {cwd: mastodonDir})
|
||||||
}
|
}
|
||||||
const promise = spawn('foreman', ['start'], {cwd: mastodonDir})
|
const promise = spawn('foreman', ['start'], {cwd: mastodonDir})
|
||||||
|
const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
|
||||||
childProc = promise.childProcess
|
childProc = promise.childProcess
|
||||||
childProc.stdout.on('data', function (data) {
|
childProc.stdout.pipe(log)
|
||||||
console.log(data.toString('utf8').replace(/\n$/, ''))
|
childProc.stderr.pipe(log)
|
||||||
})
|
|
||||||
childProc.stderr.on('data', function (data) {
|
|
||||||
console.error(data.toString('utf8').replace(/\n$/, ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
await waitForMastodonToStart()
|
await waitForMastodonToStart()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,20 +3,28 @@ import { store } from '../_store/store'
|
||||||
import { database } from '../_database/database'
|
import { database } from '../_database/database'
|
||||||
import { toast } from '../_utils/toast'
|
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 instanceName = store.get('currentInstance')
|
||||||
let accessToken = store.get('accessToken')
|
let accessToken = store.get('accessToken')
|
||||||
try {
|
try {
|
||||||
let status = await (favorited
|
let result = await (favorited
|
||||||
? favoriteStatus(instanceName, accessToken, statusId)
|
? favoriteStatus(instanceName, accessToken, statusId)
|
||||||
: unfavoriteStatus(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 statusModifications = store.get('statusModifications')
|
||||||
let currentStatusModifications = statusModifications[instanceName] =
|
let currentStatusModifications = statusModifications[instanceName] =
|
||||||
(statusModifications[instanceName] || {favorites: {}, reblogs: {}})
|
(statusModifications[instanceName] || {favorites: {}, reblogs: {}})
|
||||||
currentStatusModifications.favorites[statusId] = favorited
|
currentStatusModifications.favorites[statusId] = favorited
|
||||||
store.set({statusModifications: statusModifications})
|
store.set({statusModifications: statusModifications})
|
||||||
} catch (e) {
|
} 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 { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
|
export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/blocks`
|
let url = `${basename(instanceName)}/api/v1/blocks`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
|
export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/mutes`
|
let url = `${basename(instanceName)}/api/v1/mutes`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { post } from '../_utils/ajax'
|
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`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourite`
|
||||||
return post(url, null, {
|
return post(url, null, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unfavoriteStatus(instanceName, accessToken, statusId) {
|
export async function unfavoriteStatus (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
|
||||||
return post(url, null, {
|
return post(url, null, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { getWithTimeout } from '../_utils/ajax'
|
import { getWithTimeout } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export function getLists (instanceName, accessToken) {
|
export function getLists (instanceName, accessToken) {
|
||||||
let url = `${basename(instanceName)}/api/v1/lists`
|
let url = `${basename(instanceName)}/api/v1/lists`
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function getPinnedStatuses (instanceName, accessToken, accountId) {
|
export async function getPinnedStatuses (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/statuses`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/statuses`
|
||||||
|
@ -7,7 +7,5 @@ export async function getPinnedStatuses (instanceName, accessToken, accountId) {
|
||||||
limit: 40,
|
limit: 40,
|
||||||
pinned: true
|
pinned: true
|
||||||
})
|
})
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
|
export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
|
export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export function search (instanceName, accessToken, query) {
|
export function search (instanceName, accessToken, query) {
|
||||||
let url = `${basename(instanceName)}/api/v1/search?` + paramsString({
|
let url = `${basename(instanceName)}/api/v1/search?` + paramsString({
|
||||||
q: query,
|
q: query,
|
||||||
resolve: true
|
resolve: true
|
||||||
})
|
})
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
function getTimelineUrlPath (timeline) {
|
function getTimelineUrlPath (timeline) {
|
||||||
switch (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
|
// special case - this is a list of descendents and ancestors
|
||||||
let statusUrl = `${basename(instanceName)}/api/v1/statuses/${timeline.split('/').slice(-1)[0]}}`
|
let statusUrl = `${basename(instanceName)}/api/v1/statuses/${timeline.split('/').slice(-1)[0]}}`
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
getWithTimeout(url, {'Authorization': `Bearer ${accessToken}`}),
|
getWithTimeout(url, auth(accessToken)),
|
||||||
getWithTimeout(statusUrl, {'Authorization': `Bearer ${accessToken}`})
|
getWithTimeout(statusUrl, auth(accessToken))
|
||||||
]).then(res => {
|
]).then(res => {
|
||||||
return [].concat(res[0].ancestors).concat([res[1]]).concat(res[0].descendants)
|
return [].concat(res[0].ancestors).concat([res[1]]).concat(res[0].descendants)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
import { getWithTimeout, paramsString } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export function getVerifyCredentials (instanceName, accessToken) {
|
export function getVerifyCredentials (instanceName, accessToken) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
|
let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccount (instanceName, accessToken, accountId) {
|
export function getAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
|
||||||
return getWithTimeout(url, {
|
return getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRelationship (instanceName, accessToken, accountId) {
|
export async function getRelationship (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/relationships`
|
let url = `${basename(instanceName)}/api/v1/accounts/relationships`
|
||||||
url += '?' + paramsString({id: accountId})
|
url += '?' + paramsString({id: accountId})
|
||||||
let res = await getWithTimeout(url, {
|
let res = await getWithTimeout(url, auth(accessToken))
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
})
|
|
||||||
return res[0]
|
return res[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,3 +13,9 @@ export function basename (instanceName) {
|
||||||
}
|
}
|
||||||
return `https://${instanceName}`
|
return `https://${instanceName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function auth (accessToken) {
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
<button type="button"
|
<button type="button"
|
||||||
aria-label="{{label}}"
|
aria-label="{{label}}"
|
||||||
aria-pressed="{{!!pressed}}"
|
aria-pressed="{{!!pressed}}"
|
||||||
class="icon-button {{pressed ? 'pressed' : ''}} {{big ? 'big-icon' : ''}}"
|
class="{{computedClass}}"
|
||||||
disabled="{{disabled}}"
|
disabled="{{disabled}}"
|
||||||
|
delegate-click-key="{{delegateKey}}"
|
||||||
|
delegate-keydown-key="{{delegateKey}}"
|
||||||
on:click
|
on:click
|
||||||
>
|
>
|
||||||
<svg>
|
<svg>
|
||||||
|
@ -13,8 +15,10 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
aria-label="{{label}}"
|
aria-label="{{label}}"
|
||||||
class="icon-button {{big ? 'big-icon' : ''}}"
|
class="{{computedClass}}"
|
||||||
disabled="{{disabled}}"
|
disabled="{{disabled}}"
|
||||||
|
delegate-click-key="{{delegateKey}}"
|
||||||
|
delegate-keydown-key="{{delegateKey}}"
|
||||||
on:click
|
on:click
|
||||||
>
|
>
|
||||||
<svg>
|
<svg>
|
||||||
|
@ -44,7 +48,7 @@
|
||||||
fill: var(--action-button-fill-color-hover);
|
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);
|
fill: var(--action-button-fill-color-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,4 +63,20 @@
|
||||||
button.icon-button.pressed:active svg {
|
button.icon-button.pressed:active svg {
|
||||||
fill: var(--action-button-fill-color-pressed-active);
|
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}}
|
{{#if isStatusInOwnThread}}
|
||||||
<StatusDetails status="{{originalStatus}}" />
|
<StatusDetails status="{{originalStatus}}" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<StatusToolbar :status :isStatusInOwnThread />
|
<StatusToolbar status="{{originalStatus}}" :isStatusInOwnThread :timelineType :timelineValue />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
pressable="true"
|
pressable="true"
|
||||||
pressed="{{favorited}}"
|
pressed="{{favorited}}"
|
||||||
href="#fa-star"
|
href="#fa-star"
|
||||||
|
delegateKey="{{favoriteKey}}"
|
||||||
|
ref:favoriteNode
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Show more actions"
|
label="Show more actions"
|
||||||
|
@ -34,12 +36,33 @@
|
||||||
<script>
|
<script>
|
||||||
import IconButton from '../IconButton.html'
|
import IconButton from '../IconButton.html'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
|
import { registerDelegate, unregisterDelegate } from '../../_utils/delegate'
|
||||||
|
import { setFavorited } from '../../_actions/favorite'
|
||||||
|
|
||||||
export default {
|
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: {
|
components: {
|
||||||
IconButton
|
IconButton
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
methods: {
|
||||||
|
onFavoriteClick() {
|
||||||
|
let statusId = this.get('statusId')
|
||||||
|
let favorited = this.get('favorited')
|
||||||
|
/* no await */ setFavorited(statusId, !favorited)
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
visibility: (status) => status.visibility,
|
visibility: (status) => status.visibility,
|
||||||
boostLabel: (visibility) => {
|
boostLabel: (visibility) => {
|
||||||
|
@ -70,7 +93,9 @@
|
||||||
return $currentStatusModifications.favorites[status.id]
|
return $currentStatusModifications.favorites[status.id]
|
||||||
}
|
}
|
||||||
return status.favourited
|
return status.favourited
|
||||||
}
|
},
|
||||||
|
statusId: (status) => status.id,
|
||||||
|
favoriteKey: (statusId, timelineType, timelineValue) => `fav-${timelineType}-${timelineValue}-${statusId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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)
|
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) => {
|
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',
|
store.compute('currentStatusModifications',
|
||||||
['statusModifications', 'instanceName'],
|
['statusModifications', 'currentInstance'],
|
||||||
(statusModifications, instanceName) => {
|
(statusModifications, currentInstance) => {
|
||||||
return statusModifications[instanceName]
|
return statusModifications[currentInstance]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,21 @@ function fetchWithTimeout (url, options) {
|
||||||
|
|
||||||
async function _post (url, body, headers, timeout) {
|
async function _post (url, body, headers, timeout) {
|
||||||
let fetchFunc = timeout ? fetchWithTimeout : fetch
|
let fetchFunc = timeout ? fetchWithTimeout : fetch
|
||||||
return (await fetchFunc(url, {
|
let opts = {
|
||||||
method: 'POST',
|
method: 'POST'
|
||||||
headers: Object.assign(headers, {
|
}
|
||||||
|
if (body) {
|
||||||
|
opts.headers = Object.assign(headers, {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}),
|
})
|
||||||
body: JSON.stringify(body)
|
opts.body = JSON.stringify(body)
|
||||||
})).json()
|
} else {
|
||||||
|
opts.headers = Object.assign(headers, {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (await fetchFunc(url, opts)).json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _get (url, headers, timeout) {
|
async function _get (url, headers, timeout) {
|
||||||
|
@ -51,4 +58,4 @@ export function paramsString (paramsObject) {
|
||||||
params.set(key, paramsObject[key])
|
params.set(key, paramsObject[key])
|
||||||
})
|
})
|
||||||
return params.toString()
|
return params.toString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">
|
<form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">
|
||||||
|
|
||||||
{{#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch}}
|
{{#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch}}
|
||||||
<div class="form-error" role="alert">
|
<div class="form-error form-error-user-error" role="alert">
|
||||||
Error: {{$logInToInstanceError}}
|
Error: {{$logInToInstanceError}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Selector as $ } from 'testcafe'
|
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`
|
fixture`02-login-spec.js`
|
||||||
.page`http://localhost:4002`
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
const formError = $('.form-error')
|
|
||||||
|
|
||||||
function manualLogin (t, username, password) {
|
function manualLogin (t, username, password) {
|
||||||
return t.click($('a').withText('log in to an instance'))
|
return t.click($('a').withText('log in to an instance'))
|
||||||
.expect(getUrl()).contains('/settings/instances/add')
|
.expect(getUrl()).contains('/settings/instances/add')
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
import { Selector as $ } from 'testcafe'
|
import { Selector as $ } from 'testcafe'
|
||||||
import { getUrl, validateTimeline } from '../utils'
|
import { getFirstVisibleStatus, getUrl, validateTimeline } from '../utils'
|
||||||
import { homeTimeline, notifications, localTimeline, favorites } from '../fixtures'
|
import { homeTimeline, notifications, localTimeline, favorites } from '../fixtures'
|
||||||
import { foobarRole } from '../roles'
|
import { foobarRole } from '../roles'
|
||||||
|
|
||||||
fixture`03-basic-timeline-spec.js`
|
fixture`03-basic-timeline-spec.js`
|
||||||
.page`http://localhost:4002`
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
const firstArticle = $('.virtual-list-item[aria-hidden=false] .status-article')
|
|
||||||
|
|
||||||
test('Shows the home timeline', async t => {
|
test('Shows the home timeline', async t => {
|
||||||
await t.useRole(foobarRole)
|
await t.useRole(foobarRole)
|
||||||
.expect(firstArticle.hasAttribute('aria-setsize')).ok()
|
.expect(getFirstVisibleStatus().exists).ok()
|
||||||
.expect(firstArticle.getAttribute('aria-posinset')).eql('0')
|
.expect(getFirstVisibleStatus().hasAttribute('aria-setsize')).ok()
|
||||||
|
.expect(getFirstVisibleStatus().getAttribute('aria-posinset')).eql('0')
|
||||||
|
|
||||||
await validateTimeline(t, homeTimeline)
|
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 => {
|
test('Shows notifications', async t => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Selector as $ } from 'testcafe'
|
import { Selector as $ } from 'testcafe'
|
||||||
import { getNthStatus, getUrl } from '../utils'
|
import { getFavoritesCount, getNthStatus, getReblogsCount, getUrl } from '../utils'
|
||||||
import { foobarRole } from '../roles'
|
import { foobarRole } from '../roles'
|
||||||
|
|
||||||
fixture`11-reblog-favorites-count.js`
|
fixture`11-reblog-favorites-count.js`
|
||||||
|
@ -9,7 +9,7 @@ test('shows favorites', async t => {
|
||||||
await t.useRole(foobarRole)
|
await t.useRole(foobarRole)
|
||||||
.click(getNthStatus(0))
|
.click(getNthStatus(0))
|
||||||
.expect(getUrl()).contains('/statuses/99549266679020981')
|
.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')
|
.expect($('.icon-button[aria-label="Favorite"]').getAttribute('aria-pressed')).eql('true')
|
||||||
.click($('.status-favs-reblogs').nth(1))
|
.click($('.status-favs-reblogs').nth(1))
|
||||||
.expect(getUrl()).contains('/statuses/99549266679020981/favorites')
|
.expect(getUrl()).contains('/statuses/99549266679020981/favorites')
|
||||||
|
@ -23,7 +23,7 @@ test('shows boosts', async t => {
|
||||||
await t.useRole(foobarRole)
|
await t.useRole(foobarRole)
|
||||||
.click(getNthStatus(0))
|
.click(getNthStatus(0))
|
||||||
.expect(getUrl()).contains('/statuses/99549266679020981')
|
.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')
|
.expect($('.icon-button[aria-label="Boost"]').getAttribute('aria-pressed')).eql('false')
|
||||||
.click($('.status-favs-reblogs').nth(0))
|
.click($('.status-favs-reblogs').nth(0))
|
||||||
.expect(getUrl()).contains('/statuses/99549266679020981/reblogs')
|
.expect(getUrl()).contains('/statuses/99549266679020981/reblogs')
|
||||||
|
|
|
@ -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'
|
import { ClientFunction as exec, Selector as $ } from 'testcafe'
|
||||||
|
|
||||||
|
const SCROLL_INTERVAL = 3
|
||||||
|
|
||||||
export const settingsButton = $('nav a[aria-label=Settings]')
|
export const settingsButton = $('nav a[aria-label=Settings]')
|
||||||
export const instanceInput = $('#instanceInput')
|
export const instanceInput = $('#instanceInput')
|
||||||
export const addInstanceButton = $('.add-new-instance button')
|
export const addInstanceButton = $('.add-new-instance button')
|
||||||
export const modalDialogContents = $('.modal-dialog-contents')
|
export const modalDialogContents = $('.modal-dialog-contents')
|
||||||
export const closeDialogButton = $('.close-dialog-button')
|
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)
|
export const getUrl = exec(() => window.location.href)
|
||||||
|
|
||||||
|
@ -15,11 +28,31 @@ export const getActiveElementClass = exec(() =>
|
||||||
export const goBack = exec(() => window.history.back())
|
export const goBack = exec(() => window.history.back())
|
||||||
|
|
||||||
export function getNthStatus (n) {
|
export function getNthStatus (n) {
|
||||||
return $(`[aria-hidden="false"] > article[aria-posinset="${n}"]`)
|
return $(`div[aria-hidden="false"] > article[aria-posinset="${n}"]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLastVisibleStatus () {
|
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) {
|
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
|
// 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))
|
await t.hover(getNthStatus(i))
|
||||||
.expect($('.loading-footer').exist).notOk()
|
.expect($('.loading-footer').exist).notOk()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function scrollToBottomOfTimeline (t) {
|
export async function scrollTimelineUp (t) {
|
||||||
let lastSize = null
|
let oldFirstItem = await getFirstVisibleStatus().getAttribute('aria-posinset')
|
||||||
|
await t.hover(getFirstVisibleStatus())
|
||||||
|
let newFirstItem
|
||||||
while (true) {
|
while (true) {
|
||||||
await t.hover(getLastVisibleStatus())
|
newFirstItem = await getFirstVisibleStatus().getAttribute('aria-posinset')
|
||||||
.expect($('.loading-footer').exist).notOk()
|
if (newFirstItem === '0' || newFirstItem !== oldFirstItem) {
|
||||||
let newSize = await getLastVisibleStatus().getAttribute('aria-setsize')
|
break
|
||||||
if (newSize === lastSize) {
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
break
|
||||||
}
|
}
|
||||||
lastSize = newSize
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue