From 37d3cac7d2f9e8aa1dfc2a21f9969e6042ea0ea3 Mon Sep 17 00:00:00 2001 From: Nolan Lawson <nolan@nolanlawson.com> Date: Mon, 27 May 2019 12:31:35 -0700 Subject: [PATCH] fix: add tests for polls, improve a11y of poll form (#1239) --- src/routes/_components/status/StatusPoll.html | 43 +++++---- tests/serverActions.js | 14 +++ tests/spec/126-polls.js | 94 +++++++++++++++++++ tests/spec/127-compose-polls.js | 67 +++++++++++++ tests/utils.js | 38 ++++++++ 5 files changed, 236 insertions(+), 20 deletions(-) create mode 100644 tests/spec/126-polls.js create mode 100644 tests/spec/127-compose-polls.js diff --git a/src/routes/_components/status/StatusPoll.html b/src/routes/_components/status/StatusPoll.html index b1a8acca..82483e27 100644 --- a/src/routes/_components/status/StatusPoll.html +++ b/src/routes/_components/status/StatusPoll.html @@ -1,6 +1,6 @@ <div class={computedClass} aria-busy={loading} > {#if voted || expired } - <ul class="options" aria-label="Poll results"> + <ul aria-label="Poll results"> {#each options as option} <li class="option"> <div class="option-text"> @@ -14,19 +14,21 @@ </ul> {:else} <form class="poll-form" aria-label="Vote on poll" on:submit="onSubmit(event)" ref:form> - {#each options as option, i} - <div class="poll-form-option"> - <input type="{multiple ? 'checkbox' : 'radio'}" - id="poll-choice-{uuid}-{i}" - name="poll-choice-{uuid}" - value="{i}" - on:change="onChange()" - > - <label for="poll-choice-{uuid}-{i}"> - {option.title} - </label> - </div> - {/each} + <ul aria-label="Poll choices"> + {#each options as option, i} + <li class="poll-form-option"> + <input type="{multiple ? 'checkbox' : 'radio'}" + id="poll-choice-{uuid}-{i}" + name="poll-choice-{uuid}" + value="{i}" + on:change="onChange()" + > + <label for="poll-choice-{uuid}-{i}"> + {option.title} + </label> + </li> + {/each} + </ul> <button disabled={formDisabled} type="submit">Vote</button> </form> {/if} @@ -71,13 +73,18 @@ pointer-events: none; } - ul.options { + ul { list-style: none; margin: 0; padding: 0; } - li.option { + li { + margin: 0; + padding: 0; + } + + .option { margin: 0 0 10px 0; padding: 0; display: flex; @@ -86,10 +93,6 @@ stroke-width: 10px; } - li.option:last-child { - margin: 0; - } - .option-text { word-wrap: break-word; white-space: pre-wrap; diff --git a/tests/serverActions.js b/tests/serverActions.js index 34bc7d36..c55e24dd 100644 --- a/tests/serverActions.js +++ b/tests/serverActions.js @@ -9,6 +9,8 @@ import { followAccount, unfollowAccount } from '../src/routes/_api/follow' import { updateCredentials } from '../src/routes/_api/updateCredentials' import { reblogStatus } from '../src/routes/_api/reblog' import { submitMedia } from './submitMedia' +import { voteOnPoll } from '../src/routes/_api/polls' +import { POLL_EXPIRY_DEFAULT } from '../src/routes/_static/polls' global.fetch = fetch global.File = FileApi.File @@ -68,3 +70,15 @@ export async function unfollowAs (username, userToFollow) { export async function updateUserDisplayNameAs (username, displayName) { return updateCredentials(instanceName, users[username].accessToken, { display_name: displayName }) } + +export async function createPollAs (username, content, options, multiple) { + return postStatus(instanceName, users[username].accessToken, content, null, null, false, null, 'public', { + options, + multiple, + expires_in: POLL_EXPIRY_DEFAULT + }) +} + +export async function voteOnPollAs (username, pollId, choices) { + return voteOnPoll(instanceName, users[username].accessToken, pollId, choices.map(_ => _.toString())) +} diff --git a/tests/spec/126-polls.js b/tests/spec/126-polls.js new file mode 100644 index 00000000..5df62678 --- /dev/null +++ b/tests/spec/126-polls.js @@ -0,0 +1,94 @@ +import { + getNthStatusContent, + getNthStatusPollOption, + getNthStatusPollVoteButton, + getNthStatusPollForm, + getNthStatusPollResult, + sleep, + getNthStatusPollRefreshButton, + getNthStatusPollVoteCount, + getNthStatusRelativeDate, getUrl, goBack +} from '../utils' +import { loginAsFoobar } from '../roles' +import { createPollAs, voteOnPollAs } from '../serverActions' + +fixture`126-polls.js` + .page`http://localhost:4002` + +test('Can vote on polls', async t => { + await loginAsFoobar(t) + await createPollAs('admin', 'vote on my cool poll', ['yes', 'no'], false) + await t + .expect(getNthStatusContent(1).innerText).contains('vote on my cool poll') + .expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes') + .click(getNthStatusPollOption(1, 2)) + .click(getNthStatusPollVoteButton(1)) + .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 }) + .expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes') + .expect(getNthStatusPollResult(1, 2).innerText).eql('100% no') + .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote') +}) + +test('Can vote on multiple-choice polls', async t => { + await loginAsFoobar(t) + await createPollAs('admin', 'vote on my other poll', ['yes', 'no', 'maybe'], true) + await t + .expect(getNthStatusContent(1).innerText).contains('vote on my other poll') + .click(getNthStatusPollOption(1, 1)) + .click(getNthStatusPollOption(1, 3)) + .click(getNthStatusPollVoteButton(1)) + .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 }) + .expect(getNthStatusPollResult(1, 1).innerText).eql('50% yes') + .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no') + .expect(getNthStatusPollResult(1, 3).innerText).eql('50% maybe') + .expect(getNthStatusPollVoteCount(1).innerText).eql('2 votes') +}) + +test('Can update poll results', async t => { + const { poll } = await createPollAs('admin', 'vote on this poll', ['yes', 'no', 'maybe'], false) + const { id: pollId } = poll + await voteOnPollAs('baz', pollId, [1]) + await voteOnPollAs('ExternalLinks', pollId, [1]) + await voteOnPollAs('foobar', pollId, [2]) + await sleep(1000) + await loginAsFoobar(t) + await t + .expect(getNthStatusContent(1).innerText).contains('vote on this poll') + .expect(getNthStatusPollForm(1).exists).notOk() + .expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes') + .expect(getNthStatusPollResult(1, 2).innerText).eql('67% no') + .expect(getNthStatusPollResult(1, 3).innerText).eql('33% maybe') + .expect(getNthStatusPollVoteCount(1).innerText).eql('3 votes') + await sleep(1000) + await voteOnPollAs('quux', pollId, [0]) + await sleep(1000) + await t + .click(getNthStatusPollRefreshButton(1)) + .expect(getNthStatusPollResult(1, 1).innerText).eql('25% yes', { timeout: 20000 }) + .expect(getNthStatusPollResult(1, 2).innerText).eql('50% no') + .expect(getNthStatusPollResult(1, 3).innerText).eql('25% maybe') + .expect(getNthStatusPollVoteCount(1).innerText).eql('4 votes') +}) + +test('Poll results refresh everywhere', async t => { + await loginAsFoobar(t) + await createPollAs('admin', 'another poll', ['yes', 'no'], false) + await t + .expect(getNthStatusContent(1).innerText).contains('another poll') + .click(getNthStatusRelativeDate(1)) + .expect(getUrl()).contains('/statuses') + .expect(getNthStatusContent(1).innerText).contains('another poll') + .click(getNthStatusPollOption(1, 1)) + .click(getNthStatusPollVoteButton(1)) + .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 }) + .expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes') + .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no') + .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote') + await goBack() + await t + .expect(getUrl()).eql('http://localhost:4002/') + .expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 }) + .expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes') + .expect(getNthStatusPollResult(1, 2).innerText).eql('0% no') + .expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote') +}) diff --git a/tests/spec/127-compose-polls.js b/tests/spec/127-compose-polls.js new file mode 100644 index 00000000..785009db --- /dev/null +++ b/tests/spec/127-compose-polls.js @@ -0,0 +1,67 @@ +import { + getNthStatusContent, + getNthStatusPollForm, + getNthStatusPollResult, + getNthStatusPollVoteCount, + pollButton, + getComposePollNthInput, + composePoll, + composePollMultipleChoice, + composePollExpiry, composePollAddButton, getComposePollRemoveNthButton, postStatusButton, composeInput +} from '../utils' +import { loginAsFoobar } from '../roles' +import { POLL_EXPIRY_DEFAULT } from '../../src/routes/_static/polls' + +fixture`127-compose-polls.js` + .page`http://localhost:4002` + +test('Can add and remove poll', async t => { + await loginAsFoobar(t) + await t + .expect(composePoll.exists).notOk() + .expect(pollButton.getAttribute('aria-label')).eql('Add poll') + .click(pollButton) + .expect(composePoll.exists).ok() + .expect(getComposePollNthInput(1).value).eql('') + .expect(getComposePollNthInput(2).value).eql('') + .expect(getComposePollNthInput(3).exists).notOk() + .expect(getComposePollNthInput(4).exists).notOk() + .expect(composePollMultipleChoice.checked).notOk() + .expect(composePollExpiry.value).eql(POLL_EXPIRY_DEFAULT.toString()) + .expect(pollButton.getAttribute('aria-label')).eql('Remove poll') + .click(pollButton) + .expect(composePoll.exists).notOk() +}) + +test('Can add and remove poll options', async t => { + await loginAsFoobar(t) + await t + .expect(composePoll.exists).notOk() + .expect(pollButton.getAttribute('aria-label')).eql('Add poll') + .click(pollButton) + .expect(composePoll.exists).ok() + .typeText(getComposePollNthInput(1), 'first', { paste: true }) + .typeText(getComposePollNthInput(2), 'second', { paste: true }) + .click(composePollAddButton) + .typeText(getComposePollNthInput(3), 'third', { paste: true }) + .expect(getComposePollNthInput(1).value).eql('first') + .expect(getComposePollNthInput(2).value).eql('second') + .expect(getComposePollNthInput(3).value).eql('third') + .expect(getComposePollNthInput(4).exists).notOk() + .click(getComposePollRemoveNthButton(2)) + .expect(getComposePollNthInput(1).value).eql('first') + .expect(getComposePollNthInput(2).value).eql('third') + .expect(getComposePollNthInput(3).exists).notOk() + .expect(getComposePollNthInput(4).exists).notOk() + .click(composePollAddButton) + .typeText(getComposePollNthInput(3), 'fourth', { paste: true }) + .typeText(composeInput, 'Vote on my poll!!!', { paste: true }) + .click(postStatusButton) + .expect(getNthStatusContent(1).innerText).contains('Vote on my poll!!!') + .expect(getNthStatusPollForm(1).exists).notOk() + .expect(getNthStatusPollResult(1, 1).innerText).eql('0% first') + .expect(getNthStatusPollResult(1, 2).innerText).eql('0% third') + .expect(getNthStatusPollResult(1, 3).innerText).eql('0% fourth') + .expect(getNthStatusPollResult(1, 4).exists).notOk() + .expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes') +}) diff --git a/tests/utils.js b/tests/utils.js index 43e78785..8dc14ccc 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -22,6 +22,7 @@ export const composeButton = $('.compose-box-button') export const composeLengthIndicator = $('.compose-box-length') export const emojiButton = $('.compose-box-toolbar button:first-child') export const mediaButton = $('.compose-box-toolbar button:nth-child(2)') +export const pollButton = $('.compose-box-toolbar button:nth-child(3)') export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(4)') export const contentWarningButton = $('.compose-box-toolbar button:nth-child(5)') export const emailInput = $('input#user_email') @@ -58,6 +59,11 @@ export const composeModalContentWarningInput = $('.modal-dialog .content-warning export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)') export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(4)') +export const composePoll = $('.compose-poll') +export const composePollMultipleChoice = $('.compose-poll input[type="checkbox"]') +export const composePollExpiry = $('.compose-poll select') +export const composePollAddButton = $('.compose-poll button:last-of-type') + export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button') export const accountProfileFilterStatuses = $('.account-profile-filters li:nth-child(1)') @@ -220,6 +226,38 @@ export function getNthPostPrivacyButton (n) { return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`) } +export function getNthStatusPollOption (n, i) { + return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i}) input`) +} + +export function getNthStatusPollVoteButton (n) { + return $(`${getNthStatusSelector(n)} .poll button`) +} + +export function getNthStatusPollForm (n) { + return $(`${getNthStatusSelector(n)} .poll form`) +} + +export function getNthStatusPollResult (n, i) { + return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i})`) +} + +export function getNthStatusPollRefreshButton (n) { + return $(`${getNthStatusSelector(n)} button.poll-stat`) +} + +export function getNthStatusPollVoteCount (n) { + return $(`${getNthStatusSelector(n)} .poll .poll-stat:nth-child(1) .poll-stat-text`) +} + +export function getComposePollNthInput (n) { + return $(`.compose-poll input[type="text"]:nth-of-type(${n})`) +} + +export function getComposePollRemoveNthButton (n) { + return $(`.compose-poll button:nth-of-type(${n})`) +} + export function getNthAutosuggestionResult (n) { return $(`.compose-autosuggest-list-item:nth-child(${n})`) }