Skip to content

Commit

Permalink
♿ add github user screen reader tests
Browse files Browse the repository at this point in the history
  • Loading branch information
scottnath committed Mar 4, 2024
1 parent 514d57e commit ea8ec8c
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 30 deletions.
10 changes: 10 additions & 0 deletions src/github/dsd.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { parseFetchedRepo } from './repository/content.js';
import { parseFetchedUser } from './user/content.js';
import { repoProfileComponents, repoStorydocker, userScottnath, repoFreeCodeCamp } from './fixtures';
import { getElements, ensureElements, ensureScreenRead } from './repository/repository.shared-spec';
import {
getElements as getElementsUser,
ensureElements as ensureElementsUser,
ensureScreenRead as ensureScreenReadUser
} from './user/user.shared-spec';
import { repo, dsd } from './index.js';
import docs from './dsd.docs.mdx';

Expand Down Expand Up @@ -74,4 +79,9 @@ export const User = {
},
parseFetchedRepo(repoStorydocker)]),
},
play: async ({ args, canvasElement, step }) => {
const elements = await getElementsUser(canvasElement);
await ensureElementsUser(elements, args);
await ensureScreenReadUser(elements, args);
}
}
18 changes: 14 additions & 4 deletions src/github/repository/repository.shared-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from '@storybook/jest';
import { within as shadowWithin } from 'shadow-dom-testing-library';
import { virtual } from '@guidepup/virtual-screen-reader';

import { spokenDLItem } from '../helpers/testing';
import { spokenDLItem } from '../../utils/testing.js';

/**
* Extract elements from an shadow DOM element
Expand Down Expand Up @@ -80,10 +80,13 @@ export const ensureElements = async (elements, args) => {
}
}


/**
* Ensure the screen reader reads the correct content
* Extract the expected screen reader spoken output
* @param {GitHubRepositoryHTML} args - a content object representing a GitHub repository
* @returns {string[]} - array of strings representing the expected screen reader output
*/
export const ensureScreenRead = async (elements, args) => {
export const getExpectedScreenText = (args) => {
const expected = ['region, GitHub repository'];

// uses `spokenDLItem` to create dt/dd spoken pairs
Expand Down Expand Up @@ -114,7 +117,14 @@ export const ensureScreenRead = async (elements, args) => {
}

expected.push('end of region, GitHub repository');

return expected;
}

/**
* Ensure the screen reader reads the correct content
*/
export const ensureScreenRead = async (elements, args) => {
const expected = getExpectedScreenText(args);
// Start virtual screen reader
await virtual.start({ container: elements.container });
while ((await virtual.lastSpokenPhrase()) !== expected[expected.length - 1]) {
Expand Down
6 changes: 3 additions & 3 deletions src/github/styles/user.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ section[itemscope] {
overflow-wrap: anywhere;
overflow: hidden;
}
:host header {
:host header:first-child {
background-color: var(--bg-color-light);
padding: var(--row-spacing);

display: flex;
gap: var(--svg-gap);
height: calc(var(--logo-size) + var(--logo-outline-offset) * 2);

> span:has([itemprop="memberOf"]) {
[itemprop="memberOf"] {
background-color: var(--color-normal);
width: var(--logo-size);
height: var(--logo-size);
Expand Down Expand Up @@ -144,7 +144,7 @@ section[itemscope] {
mask-image: var(--svg-people);
}

> span {
div {
display: inline-flex;
flex-direction: row-reverse;
flex-wrap: nowrap;
Expand Down
16 changes: 15 additions & 1 deletion src/github/user/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const blankPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1
* @property {string} [following] - number of people user is following
* @property {string} [followers] - number of followers
* @property {string} [error] - error message, if any
* @property {Object} [a11y] - accessibility content
* @property {Array<GitHubRepositoryHTML>} [repositories] - array of repositories
*/

Expand Down Expand Up @@ -62,6 +63,7 @@ export const parseFetchedUser = (user = {}) => {
bio: user.bio,
following: user.following,
followers: user.followers,
a11y: user.a11y || {},
}
}

Expand Down Expand Up @@ -136,6 +138,18 @@ export const cleanUserContent = (content = {}) => {
return c;
};

export const a11yContent = (content) => {
let headerLabel = `GitHub user ${content.login}`;
if (content.name) {
headerLabel = headerLabel.replace(content.login, `${content.name}, username ${content.login}`);
}
content.a11y = {
...content.a11y,
headerLabel,
}
return content;
}

/**
* Generates an object of content for the repository HTML
* @param {GitHubUserHTML} content
Expand Down Expand Up @@ -171,6 +185,6 @@ export const generateUserContent = async (content, fetch = false) => {
}
userFromContent.repositories = Array.from(repos);
}
return Object.assign({}, fetched, userFromContent);
return a11yContent(Object.assign({}, fetched, userFromContent));
}

42 changes: 21 additions & 21 deletions src/github/user/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,48 +21,48 @@ function html(content) {

return `
<section aria-label="GitHub user profile" itemscope itemtype="http://schema.org/Person">
<header>
<span><span itemprop="memberOf">GitHub</span> user</span>
<span itemprop="alternativeName">${content.login}</span>
<header aria-label="${content.a11y.headerLabel}">
<span itemprop="memberOf" aria-hidden="true">GitHub</span>
<span itemprop="alternativeName" aria-hidden="true">${content.login}</span>
</header>
<div part="main">
<address>
<a href="https://github.com/${content.login}" aria-label="View @${content.login}'s profile on GitHub" itemprop="url">
<a href="https://github.com/${content.login}" aria-label="${content.name || content.login}'s profile on GitHub" itemprop="url">
<span class="avatar" itemprop="image">
<img src="${content.avatar_url}" alt="Avatar for ${content.name | content.login}" loading="lazy" />
<img src="${content.avatar_url}" alt="Avatar for ${content.name || content.login}" loading="lazy" />
</span>
<span itemprop="creator">
<span itemprop="creator" aria-hidden="true">
<span itemprop="name">${content.name}</span>
<span itemprop="alternativeName">${content.login}</span>
</span>
</a>
</address>
${content.bio ? `<p itemprop="description">${content.bio}</p>` : ''}
${content.following || content.followers ? `
<dl>
<dl aria-label="GitHub user stats">
${content.followers ? `
<span><dt>followers</dt>
<dd itemprop="followee">
<span aria-hidden="true">${intToString(content.followers)}</span>
<div aria-label="followers: ${content.followers}">
<dt aria-hidden="true">followers</dt>
<dd itemprop="followee" aria-hidden="true">
<span>${intToString(content.followers)}</span>
<span class="sr-only">${content.followers}</span>
</dd></span>
</dd></div>
` : ''}
${content.following ? `
<span><dt>following</dt>
<dd itemprop="follows">
<span aria-hidden="true">${intToString(content.following)}</span>
<div aria-label="following: ${content.following}">
<dt aria-hidden="true">following</dt>
<dd itemprop="follows" aria-hidden="true">
<span>${intToString(content.following)}</span>
<span class="sr-only">${content.following}</span>
</dd></span>
</dd></div>
` : ''}
</dl>
` : ''}
${Array.isArray(content.repositories) && content.repositories?.length ? `
<dl>
<dt>Pinned repositories</dt>
${content.repositories.map((repo) => `
<dd>${repositoryHTML(repo)}</dd>
`).join('')}
</dl>
<header aria-label="Pinned repositories">Pinned repositories</header>
${content.repositories.map((repo) => `
${repositoryHTML(repo)}
`).join('')}
` : ''}
</div>
</section>
Expand Down
74 changes: 74 additions & 0 deletions src/github/user/user.shared-spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@

import { expect } from '@storybook/jest';
import { within as shadowWithin } from 'shadow-dom-testing-library';
import { virtual } from '@guidepup/virtual-screen-reader';

import { a11yContent } from './content.js';
import { getExpectedScreenText as getRepoScreenText } from '../repository/repository.shared-spec';
import { intToString } from '../../utils/index.js';
import { spokenDLItem } from '../../utils/testing.js';

/**
* Extract elements from an shadow DOM element
Expand Down Expand Up @@ -81,4 +85,74 @@ export const ensureElements = async (elements, args) => {
} else {
await expect(elements.repos).toHaveLength(0);
}
}

/**
* Extract the expected screen reader spoken output
* @param {GitHubUserHTML} args - a content object representing a GitHub user
* @returns {string[]} - array of strings representing the expected screen reader output
*/
export const getExpectedScreenText = (args) => {
const { a11y } = a11yContent(args);
const expected = ['region, GitHub user profile'];

// uses `spokenDLItem` to create dt/dd spoken pairs
const dlItem = new spokenDLItem(expected);

if (args.error) {
expected.push(args.error);
} else {
expected.push(`banner, ${a11y.headerLabel}`);
expected.push(`link, ${args.name || args.login}'s profile on GitHub`);
expected.push(`img, Avatar for ${args.name || args.login}`);
expected.push(`end of link, ${args.name || args.login}'s profile on GitHub`);


if (args.bio) {
expected.push(args.bio)
}
if (args.followers || args.following) {
expected.push('GitHub user stats');
if (args.followers) {
// dlItem.spoken('Followers', args.followers);
expected.push(`followers: ${args.followers}`);
}
if (args.following) {
// dlItem.spoken('Following', args.following);
expected.push(`following: ${args.following}`);
}
}
if (args.repos) {
const repos = parseify(args.repos);
if (Array.isArray(repos)) {
expected.push('banner, Pinned repositories');
repos.forEach((repo) => {
const repoExpected = getRepoScreenText(repo);
expected.push(...repoExpected);
});
}
}
}

expected.push('end of region, GitHub user profile');
return expected;
}

/**
* Ensure the screen reader reads the correct content
*/
export const ensureScreenRead = async (elements, args) => {
const expected = getExpectedScreenText(args);

// Start virtual screen reader
await virtual.start({ container: elements.container });
while ((await virtual.lastSpokenPhrase()) !== expected[expected.length - 1]) {
await virtual.next();
}

// Compare spoken phrases to expected
expect(await virtual.spokenPhraseLog()).toEqual(expected);

// Stop virtual screen reader
await virtual.stop();
}
10 changes: 9 additions & 1 deletion src/github/user/user.stories.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { expect } from '@storybook/jest';
import { repoProfileComponents, repoStorydocker, userScottnath, userSindresorhus } from '../fixtures';
import { generateMockResponse } from '../helpers/testing';
import { parseFetchedUser } from './content';
import { parseFetchedRepo } from '../repository/content.js';
import { getElements, ensureElements } from './user.shared-spec';
import { getElements, ensureElements, ensureScreenRead } from './user.shared-spec';
import { primerThemes } from '../../../.storybook/primer-preview.js';

import '.';
Expand All @@ -25,6 +26,7 @@ export const User = {
play: async ({ args, canvasElement, step }) => {
const elements = await getElements(canvasElement);
await ensureElements(elements, args);
await ensureScreenRead(elements, args);
}
}

Expand Down Expand Up @@ -66,6 +68,7 @@ export const Fetch = {
...args,
};
await ensureElements(elements, argsAfterFetch);
await ensureScreenRead(elements, argsAfterFetch);
}
};

Expand All @@ -92,6 +95,7 @@ export const FetchOverides = {
...args,
};
await ensureElements(elements, argsAfterFetch);
await ensureScreenRead(elements, argsAfterFetch);
}
}

Expand All @@ -104,15 +108,19 @@ export const ReposFetch = {
parameters: {
mockData: [
generateMockResponse(userScottnath, 'users'),
generateMockResponse(repoProfileComponents, 'repos'),
generateMockResponse(repoStorydocker, 'repos'),
]
},
play: async ({ args, canvasElement, step }) => {
const elements = await getElements(canvasElement);
const argsAfterFetch = {
...parseFetchedUser(userScottnath),
...args,
repos: stringify([repoProfileComponents, repoStorydocker]),
};
await ensureElements(elements, argsAfterFetch);
await ensureScreenRead(elements, argsAfterFetch);
}
}

Expand Down

0 comments on commit ea8ec8c

Please sign in to comment.