fix(a11y): fix NVDA crash on long aria-label (#702)
* fix(a11y): fix NVDA crash on long aria-label fixes #694 * use the word truncated instead of ellipsis * fix test * really fix tests
This commit is contained in:
parent
12892d0032
commit
0515133ece
|
@ -6,41 +6,13 @@ import { favoriteStatus } from '../routes/_api/favorite'
|
||||||
import { reblogStatus } from '../routes/_api/reblog'
|
import { reblogStatus } from '../routes/_api/reblog'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import FileApi from 'file-api'
|
import FileApi from 'file-api'
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import FormData from 'form-data'
|
|
||||||
import { auth } from '../routes/_api/utils'
|
|
||||||
import { pinStatus } from '../routes/_api/pin'
|
import { pinStatus } from '../routes/_api/pin'
|
||||||
|
import { submitMedia } from '../tests/submitMedia'
|
||||||
|
|
||||||
global.File = FileApi.File
|
global.File = FileApi.File
|
||||||
global.FormData = FileApi.FormData
|
global.FormData = FileApi.FormData
|
||||||
global.fetch = fetch
|
global.fetch = fetch
|
||||||
|
|
||||||
async function submitMedia (accessToken, filename, alt) {
|
|
||||||
let form = new FormData()
|
|
||||||
form.append('file', fs.createReadStream(path.join(__dirname, '../tests/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)))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function restoreMastodonData () {
|
export async function restoreMastodonData () {
|
||||||
console.log('Restoring mastodon data...')
|
console.log('Restoring mastodon data...')
|
||||||
let internalIdsToIds = {}
|
let internalIdsToIds = {}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { getAccountAccessibleName } from './getAccountAccessibleName'
|
import { getAccountAccessibleName } from './getAccountAccessibleName'
|
||||||
import { htmlToPlainText } from '../_utils/htmlToPlainText'
|
|
||||||
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
|
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
|
||||||
|
import { htmlToPlainText } from '../_utils/htmlToPlainText'
|
||||||
|
|
||||||
|
const MAX_TEXT_LENGTH = 150
|
||||||
|
|
||||||
function notificationText (notification, omitEmojiInDisplayNames) {
|
function notificationText (notification, omitEmojiInDisplayNames) {
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
|
@ -30,15 +32,28 @@ function reblogText (reblog, account, omitEmojiInDisplayNames) {
|
||||||
return `Boosted by ${accountDisplayName}`
|
return `Boosted by ${accountDisplayName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Works around a bug in NVDA where it may crash if the string is too long
|
||||||
|
// https://github.com/nolanlawson/pinafore/issues/694
|
||||||
|
function truncateTextForSRs (text) {
|
||||||
|
if (text.length > MAX_TEXT_LENGTH) {
|
||||||
|
text = text.substring(0, MAX_TEXT_LENGTH)
|
||||||
|
text = text.replace(/\S+$/, '') + ' (truncated)'
|
||||||
|
}
|
||||||
|
return text.replace(/\s+/g, ' ').trim()
|
||||||
|
}
|
||||||
|
|
||||||
export function getAccessibleLabelForStatus (originalAccount, account, content,
|
export function getAccessibleLabelForStatus (originalAccount, account, content,
|
||||||
timeagoFormattedDate, spoilerText, showContent,
|
timeagoFormattedDate, spoilerText, showContent,
|
||||||
reblog, notification, visibility, omitEmojiInDisplayNames) {
|
reblog, notification, visibility, omitEmojiInDisplayNames) {
|
||||||
let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
|
let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
|
||||||
|
let contentTextToShow = (showContent || !spoilerText)
|
||||||
|
? truncateTextForSRs(htmlToPlainText(content))
|
||||||
|
: `Content warning: ${truncateTextForSRs(spoilerText)}`
|
||||||
|
|
||||||
let values = [
|
let values = [
|
||||||
notificationText(notification, omitEmojiInDisplayNames),
|
notificationText(notification, omitEmojiInDisplayNames),
|
||||||
originalAccountDisplayName,
|
originalAccountDisplayName,
|
||||||
(showContent || !spoilerText) ? htmlToPlainText(content) : `Content warning: ${spoilerText}`,
|
contentTextToShow,
|
||||||
timeagoFormattedDate,
|
timeagoFormattedDate,
|
||||||
`@${originalAccount.acct}`,
|
`@${originalAccount.acct}`,
|
||||||
privacyText(visibility),
|
privacyText(visibility),
|
||||||
|
|
|
@ -77,9 +77,6 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hydrateContent () {
|
hydrateContent () {
|
||||||
if (!this.refs.node) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mark('hydrateContent')
|
mark('hydrateContent')
|
||||||
let node = this.refs.node
|
let node = this.refs.node
|
||||||
let { originalStatus, uuid } = this.get()
|
let { originalStatus, uuid } = this.get()
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/fo
|
||||||
import { followAccount, unfollowAccount } from '../routes/_api/follow'
|
import { followAccount, unfollowAccount } from '../routes/_api/follow'
|
||||||
import { updateCredentials } from '../routes/_api/updateCredentials'
|
import { updateCredentials } from '../routes/_api/updateCredentials'
|
||||||
import { reblogStatus } from '../routes/_api/reblog'
|
import { reblogStatus } from '../routes/_api/reblog'
|
||||||
|
import { submitMedia } from './submitMedia'
|
||||||
|
|
||||||
global.fetch = fetch
|
global.fetch = fetch
|
||||||
global.File = FileApi.File
|
global.File = FileApi.File
|
||||||
|
@ -28,6 +29,12 @@ export async function postAs (username, text) {
|
||||||
null, null, false, null, 'public')
|
null, null, false, null, 'public')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postEmptyStatusWithMediaAs (username, filename, alt) {
|
||||||
|
let mediaResponse = await submitMedia(users[username].accessToken, filename, alt)
|
||||||
|
return postStatus(instanceName, users[username].accessToken, '',
|
||||||
|
null, [mediaResponse.id], false, null, 'public')
|
||||||
|
}
|
||||||
|
|
||||||
export async function postReplyAs (username, text, inReplyTo) {
|
export async function postReplyAs (username, text, inReplyTo) {
|
||||||
return postStatus(instanceName, users[username].accessToken, text,
|
return postStatus(instanceName, users[username].accessToken, text,
|
||||||
inReplyTo, null, false, null, 'public')
|
inReplyTo, null, false, null, 'public')
|
||||||
|
|
|
@ -138,4 +138,7 @@ test('Check some odd emoji', async t => {
|
||||||
.expect(removeEmojiFromDisplayNamesInput.checked).notOk()
|
.expect(removeEmojiFromDisplayNamesInput.checked).notOk()
|
||||||
.click(homeNavButton)
|
.click(homeNavButton)
|
||||||
.expect(displayNameInComposeBox.innerText).eql('foo 🕹📺')
|
.expect(displayNameInComposeBox.innerText).eql('foo 🕹📺')
|
||||||
|
|
||||||
|
// clean up after all these tests are done
|
||||||
|
await updateUserDisplayNameAs('foobar', 'foobar')
|
||||||
})
|
})
|
||||||
|
|
16
tests/spec/120-status-aria-label.js
Normal file
16
tests/spec/120-status-aria-label.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { loginAsFoobar } from '../roles'
|
||||||
|
import { getNthStatus } from '../utils'
|
||||||
|
import { postEmptyStatusWithMediaAs } from '../serverActions'
|
||||||
|
|
||||||
|
fixture`120-status-aria-label.js`
|
||||||
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
|
test('aria-labels for statuses with no content text', async t => {
|
||||||
|
await postEmptyStatusWithMediaAs('foobar', 'kitten1.jpg', 'kitteh')
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t
|
||||||
|
.hover(getNthStatus(0))
|
||||||
|
.expect(getNthStatus(0).getAttribute('aria-label')).match(
|
||||||
|
/foobar, (.+ ago|just now), @foobar, Public/i
|
||||||
|
)
|
||||||
|
})
|
29
tests/submitMedia.js
Normal file
29
tests/submitMedia.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import FormData from 'form-data'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { auth } from '../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)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue