From 0515133ece4f264ab3273992b9d287997083e99a Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sat, 1 Dec 2018 00:10:30 -0800 Subject: [PATCH] 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 --- bin/restore-mastodon-data.js | 30 +------------------- routes/_a11y/getAccessibleLabelForStatus.js | 19 +++++++++++-- routes/_components/status/StatusContent.html | 3 -- tests/serverActions.js | 7 +++++ tests/spec/118-display-name-custom-emoji.js | 3 ++ tests/spec/120-status-aria-label.js | 16 +++++++++++ tests/submitMedia.js | 29 +++++++++++++++++++ 7 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 tests/spec/120-status-aria-label.js create mode 100644 tests/submitMedia.js diff --git a/bin/restore-mastodon-data.js b/bin/restore-mastodon-data.js index b625fe2d..0c5ad244 100644 --- a/bin/restore-mastodon-data.js +++ b/bin/restore-mastodon-data.js @@ -6,41 +6,13 @@ import { favoriteStatus } from '../routes/_api/favorite' import { reblogStatus } from '../routes/_api/reblog' import fetch from 'node-fetch' 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 { submitMedia } from '../tests/submitMedia' global.File = FileApi.File global.FormData = FileApi.FormData 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 () { console.log('Restoring mastodon data...') let internalIdsToIds = {} diff --git a/routes/_a11y/getAccessibleLabelForStatus.js b/routes/_a11y/getAccessibleLabelForStatus.js index 9b5beb62..2d426397 100644 --- a/routes/_a11y/getAccessibleLabelForStatus.js +++ b/routes/_a11y/getAccessibleLabelForStatus.js @@ -1,6 +1,8 @@ import { getAccountAccessibleName } from './getAccountAccessibleName' -import { htmlToPlainText } from '../_utils/htmlToPlainText' import { POST_PRIVACY_OPTIONS } from '../_static/statuses' +import { htmlToPlainText } from '../_utils/htmlToPlainText' + +const MAX_TEXT_LENGTH = 150 function notificationText (notification, omitEmojiInDisplayNames) { if (!notification) { @@ -30,15 +32,28 @@ function reblogText (reblog, account, omitEmojiInDisplayNames) { 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, timeagoFormattedDate, spoilerText, showContent, reblog, notification, visibility, omitEmojiInDisplayNames) { let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames) + let contentTextToShow = (showContent || !spoilerText) + ? truncateTextForSRs(htmlToPlainText(content)) + : `Content warning: ${truncateTextForSRs(spoilerText)}` let values = [ notificationText(notification, omitEmojiInDisplayNames), originalAccountDisplayName, - (showContent || !spoilerText) ? htmlToPlainText(content) : `Content warning: ${spoilerText}`, + contentTextToShow, timeagoFormattedDate, `@${originalAccount.acct}`, privacyText(visibility), diff --git a/routes/_components/status/StatusContent.html b/routes/_components/status/StatusContent.html index 19f74988..aedb19af 100644 --- a/routes/_components/status/StatusContent.html +++ b/routes/_components/status/StatusContent.html @@ -77,9 +77,6 @@ }, methods: { hydrateContent () { - if (!this.refs.node) { - return - } mark('hydrateContent') let node = this.refs.node let { originalStatus, uuid } = this.get() diff --git a/tests/serverActions.js b/tests/serverActions.js index bdba5472..e99e73b8 100644 --- a/tests/serverActions.js +++ b/tests/serverActions.js @@ -8,6 +8,7 @@ import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/fo import { followAccount, unfollowAccount } from '../routes/_api/follow' import { updateCredentials } from '../routes/_api/updateCredentials' import { reblogStatus } from '../routes/_api/reblog' +import { submitMedia } from './submitMedia' global.fetch = fetch global.File = FileApi.File @@ -28,6 +29,12 @@ export async function postAs (username, text) { 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) { return postStatus(instanceName, users[username].accessToken, text, inReplyTo, null, false, null, 'public') diff --git a/tests/spec/118-display-name-custom-emoji.js b/tests/spec/118-display-name-custom-emoji.js index 0152f052..88cb0f2e 100644 --- a/tests/spec/118-display-name-custom-emoji.js +++ b/tests/spec/118-display-name-custom-emoji.js @@ -138,4 +138,7 @@ test('Check some odd emoji', async t => { .expect(removeEmojiFromDisplayNamesInput.checked).notOk() .click(homeNavButton) .expect(displayNameInComposeBox.innerText).eql('foo 🕹📺') + + // clean up after all these tests are done + await updateUserDisplayNameAs('foobar', 'foobar') }) diff --git a/tests/spec/120-status-aria-label.js b/tests/spec/120-status-aria-label.js new file mode 100644 index 00000000..02e844f1 --- /dev/null +++ b/tests/spec/120-status-aria-label.js @@ -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 + ) +}) diff --git a/tests/submitMedia.js b/tests/submitMedia.js new file mode 100644 index 00000000..4bf1ab93 --- /dev/null +++ b/tests/submitMedia.js @@ -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))) + }) + }) +}