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:
Nolan Lawson 2018-12-01 00:10:30 -08:00 committed by GitHub
parent 12892d0032
commit 0515133ece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 73 additions and 34 deletions

View File

@ -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 = {}

View File

@ -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),

View File

@ -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()

View File

@ -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')

View File

@ -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')
}) })

View 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
View 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)))
})
})
}