* initial rough version * link to relevant rules, extract image text, only show if more than 0 errors * jsdoc and tweaks * refactors * add tests * open info links in new tab * add new window link icon
This commit is contained in:
parent
7e322e4f7b
commit
573c78d032
8 changed files with 9581 additions and 3 deletions
6009
app/assets/javascripts/markdown-it.min.js
vendored
Normal file
6009
app/assets/javascripts/markdown-it.min.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
2827
app/assets/javascripts/markdownlint-browser.min.js
vendored
Normal file
2827
app/assets/javascripts/markdownlint-browser.min.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,12 @@ import { KeyboardShortcuts } from '../shared/components/useKeyboardShortcuts';
|
|||
import { submitArticle, previewArticle } from './actions';
|
||||
import { EditorActions, Form, Header, Help, Preview } from './components';
|
||||
import { Button, Modal } from '@crayons';
|
||||
import {
|
||||
noDefaultAltTextRule,
|
||||
noEmptyAltTextRule,
|
||||
noLevelOneHeadingsRule,
|
||||
headingIncrement,
|
||||
} from '@utilities/markdown/markdownLintCustomRules';
|
||||
|
||||
/* global activateRunkitTags */
|
||||
|
||||
|
|
@ -14,6 +20,26 @@ import { Button, Modal } from '@crayons';
|
|||
editing are not used in this file, they are important to the
|
||||
editor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Settings for the markdownlint library we use to identify potential accessibility failings in posts
|
||||
*/
|
||||
const LINT_OPTIONS = {
|
||||
customRules: [
|
||||
noDefaultAltTextRule,
|
||||
noLevelOneHeadingsRule,
|
||||
headingIncrement,
|
||||
noEmptyAltTextRule,
|
||||
],
|
||||
config: {
|
||||
default: false, // disable all default rules
|
||||
[noDefaultAltTextRule.names[0]]: true,
|
||||
[noLevelOneHeadingsRule.names[0]]: true,
|
||||
[headingIncrement.names[0]]: true,
|
||||
[noEmptyAltTextRule.names[0]]: true,
|
||||
},
|
||||
};
|
||||
|
||||
export class ArticleForm extends Component {
|
||||
static handleGistPreview() {
|
||||
const els = document.getElementsByClassName('ltag_gist-liquid-tag');
|
||||
|
|
@ -99,6 +125,7 @@ export class ArticleForm extends Component {
|
|||
helpFor: null,
|
||||
helpPosition: null,
|
||||
isModalOpen: false,
|
||||
markdownLintErrors: [],
|
||||
...previousContentState,
|
||||
};
|
||||
}
|
||||
|
|
@ -160,7 +187,41 @@ export class ArticleForm extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
lintMarkdown = () => {
|
||||
const options = {
|
||||
...LINT_OPTIONS,
|
||||
strings: {
|
||||
content: this.state.bodyMarkdown,
|
||||
},
|
||||
};
|
||||
const { content: markdownLintErrors } = window.markdownlint.sync(options);
|
||||
this.setState({ markdownLintErrors });
|
||||
};
|
||||
|
||||
fetchMarkdownLint = async () => {
|
||||
if (!window.markdownlint) {
|
||||
const markdownItScript = document.createElement('script');
|
||||
markdownItScript.setAttribute('src', '/assets/markdown-it.min.js');
|
||||
document.body.appendChild(markdownItScript);
|
||||
|
||||
// The markdownlint script needs the first script to have finished loading first
|
||||
markdownItScript.addEventListener('load', () => {
|
||||
const markdownLintScript = document.createElement('script');
|
||||
markdownLintScript.setAttribute(
|
||||
'src',
|
||||
'/assets/markdownlint-browser.min.js',
|
||||
);
|
||||
document.body.appendChild(markdownLintScript);
|
||||
|
||||
markdownLintScript.addEventListener('load', this.lintMarkdown);
|
||||
});
|
||||
} else {
|
||||
this.lintMarkdown();
|
||||
}
|
||||
};
|
||||
|
||||
showPreview = (response) => {
|
||||
this.fetchMarkdownLint();
|
||||
this.setState({
|
||||
...this.setCommonProps({ previewShowing: true }),
|
||||
previewResponse: response,
|
||||
|
|
@ -303,6 +364,7 @@ export class ArticleForm extends Component {
|
|||
helpFor,
|
||||
helpPosition,
|
||||
siteLogo,
|
||||
markdownLintErrors,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
|
|
@ -328,6 +390,7 @@ export class ArticleForm extends Component {
|
|||
previewResponse={previewResponse}
|
||||
articleState={this.state}
|
||||
errors={errors}
|
||||
markdownLintErrors={markdownLintErrors}
|
||||
/>
|
||||
) : (
|
||||
<Form
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Limit the number of suggestions shown so that the UI isn't overwhelmed
|
||||
const MAX_SUGGESTIONS = 3;
|
||||
|
||||
const ExternalUrlSVG = () => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="15"
|
||||
height="15"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M10 6v2H5v11h11v-5h2v6a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const extractRelevantErrors = (lintErrors) => {
|
||||
const imageErrors = [];
|
||||
const otherErrors = [];
|
||||
|
||||
lintErrors.forEach((lintError) => {
|
||||
if (
|
||||
lintError.ruleNames.includes('no-default-alt-text') ||
|
||||
lintError.ruleNames.includes('no-empty-alt-text')
|
||||
) {
|
||||
imageErrors.push({ ...lintError, errorType: 'image' });
|
||||
} else {
|
||||
otherErrors.push({ ...lintError, errorType: 'other' });
|
||||
}
|
||||
});
|
||||
|
||||
// Truncate the errors, favouring image errors (as these accessibility suggestions are more impactful)
|
||||
if (imageErrors.length > MAX_SUGGESTIONS) {
|
||||
imageErrors.length = MAX_SUGGESTIONS;
|
||||
}
|
||||
|
||||
const remainingErrors = MAX_SUGGESTIONS - imageErrors.length;
|
||||
if (otherErrors.length > remainingErrors) {
|
||||
otherErrors.length = remainingErrors;
|
||||
}
|
||||
|
||||
return [...imageErrors, ...otherErrors];
|
||||
};
|
||||
|
||||
/**
|
||||
* An information notice displayed to users in the Preview window when accessibility improvements could be made to their post.
|
||||
* This component displays a maximum of 3 suggestions, favouring image-related suggestions (as these changes are more impactful).
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object[]} props.markdownLintErrors The array of error objects returned from the markdownlint library
|
||||
*
|
||||
* @example
|
||||
* <AccessibilitySuggestions
|
||||
* markdownLintErrors={[
|
||||
* {
|
||||
* errorContext: "Consider adding an image description in the square brackets of the image ",
|
||||
* errorDetail: "/p/editor_guide#alt-text-for-images"
|
||||
* ruleNames: ["no-empty-alt-text"]
|
||||
* }
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const AccessibilitySuggestions = ({ markdownLintErrors }) => {
|
||||
return (
|
||||
<div
|
||||
className="crayons-notice crayons-notice--info mb-6"
|
||||
aria-live="polite"
|
||||
>
|
||||
<h2 className="fs-l mb-2 fw-bold">
|
||||
Improve the accessibility of your post
|
||||
</h2>
|
||||
<ul>
|
||||
{extractRelevantErrors(markdownLintErrors).map((lintError, index) => {
|
||||
return (
|
||||
<li key={`linterror-${index}`}>
|
||||
{lintError.errorContext}
|
||||
<span className="fs-s">
|
||||
{' '}
|
||||
<a
|
||||
href={lintError.errorDetail}
|
||||
aria-label={`Learn more about accessible ${
|
||||
lintError.errorType === 'image' ? 'images' : 'headings'
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more <ExternalUrlSVG />
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AccessibilitySuggestions.propTypes = {
|
||||
markdownLintErrors: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
errorContext: PropTypes.string,
|
||||
errorDetail: PropTypes.string,
|
||||
ruleNames: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
|
@ -2,8 +2,14 @@ import { h } from 'preact';
|
|||
import PropTypes from 'prop-types';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { ErrorList } from './ErrorList';
|
||||
import { AccessibilitySuggestions } from './AccessibilitySuggestions';
|
||||
|
||||
function titleArea(previewResponse, articleState, errors) {
|
||||
function titleArea({
|
||||
previewResponse,
|
||||
articleState,
|
||||
errors,
|
||||
markdownLintErrors,
|
||||
}) {
|
||||
const tagArray = previewResponse.tags || articleState.tagList.split(', ');
|
||||
let tags = '';
|
||||
if (tagArray.length > 0 && tagArray[0].length > 0) {
|
||||
|
|
@ -51,6 +57,9 @@ function titleArea(previewResponse, articleState, errors) {
|
|||
)}
|
||||
<div className="crayons-article__header__meta">
|
||||
{errors && <ErrorList errors={errors} />}
|
||||
{!errors && markdownLintErrors?.length > 0 && (
|
||||
<AccessibilitySuggestions markdownLintErrors={markdownLintErrors} />
|
||||
)}
|
||||
<h1 className="fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight mb-6 spec-article__title">
|
||||
{previewTitle}
|
||||
</h1>
|
||||
|
|
@ -68,7 +77,12 @@ const previewResponsePropTypes = PropTypes.shape({
|
|||
cover_image: PropTypes.string,
|
||||
});
|
||||
|
||||
export const Preview = ({ previewResponse, articleState, errors }) => {
|
||||
export const Preview = ({
|
||||
previewResponse,
|
||||
articleState,
|
||||
errors,
|
||||
markdownLintErrors,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (previewResponse.processed_html.includes('twitter-timeline')) {
|
||||
attachTwitterTimelineScript();
|
||||
|
|
@ -78,7 +92,12 @@ export const Preview = ({ previewResponse, articleState, errors }) => {
|
|||
return (
|
||||
<div className="crayons-article-form__content crayons-card">
|
||||
<article className="crayons-article">
|
||||
{titleArea(previewResponse, articleState, errors)}
|
||||
{titleArea({
|
||||
previewResponse,
|
||||
articleState,
|
||||
errors,
|
||||
markdownLintErrors,
|
||||
})}
|
||||
<div className="crayons-article__main">
|
||||
<div
|
||||
className="crayons-article__body text-styles"
|
||||
|
|
@ -104,6 +123,7 @@ function attachTwitterTimelineScript() {
|
|||
Preview.propTypes = {
|
||||
previewResponse: previewResponsePropTypes.isRequired,
|
||||
errors: PropTypes.string.isRequired,
|
||||
markdownLintErrors: PropTypes.arrayOf(PropTypes.object),
|
||||
articleState: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import '@testing-library/jest-dom';
|
||||
import { AccessibilitySuggestions } from '../AccessibilitySuggestions';
|
||||
|
||||
describe('<AccessibilitySuggestions />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<AccessibilitySuggestions
|
||||
markdownLintErrors={[
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['example-rule'],
|
||||
errorContext: 'Example suggestion',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should display a list of maximum 3 suggestions', () => {
|
||||
const lintErrors = [1, 2, 3, 4].map((item) => ({
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['no-empty-alt-text'],
|
||||
errorContext: `Suggestion ${item}`,
|
||||
}));
|
||||
|
||||
const { getAllByRole, getByText, queryByText } = render(
|
||||
<AccessibilitySuggestions markdownLintErrors={lintErrors} />,
|
||||
);
|
||||
|
||||
const listItems = getAllByRole('listitem');
|
||||
expect(listItems).toHaveLength(3);
|
||||
expect(getByText('Suggestion 1')).toBeInTheDocument();
|
||||
expect(getByText('Suggestion 2')).toBeInTheDocument();
|
||||
expect(getByText('Suggestion 3')).toBeInTheDocument();
|
||||
expect(queryByText('Suggestion 4')).not.toBeInTheDocument();
|
||||
|
||||
const detailLinks = getAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
});
|
||||
expect(detailLinks).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should only show image errors if there are 3 or more', () => {
|
||||
const imageErrors = [
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['no-empty-alt-text'],
|
||||
errorContext: 'No empty alt text 1',
|
||||
},
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['no-default-alt-text'],
|
||||
errorContext: 'No default alt text 1',
|
||||
},
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['no-empty-alt-text'],
|
||||
errorContext: 'No empty alt text 2',
|
||||
},
|
||||
];
|
||||
|
||||
const otherErrors = [
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['other'],
|
||||
errorContext: 'Other 1',
|
||||
},
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['other'],
|
||||
errorContext: 'Other 2',
|
||||
},
|
||||
];
|
||||
|
||||
const { getAllByRole, getByText, queryByText } = render(
|
||||
<AccessibilitySuggestions
|
||||
markdownLintErrors={[...otherErrors, ...imageErrors]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const listItems = getAllByRole('listitem');
|
||||
expect(listItems).toHaveLength(3);
|
||||
imageErrors.forEach((imageError) => {
|
||||
expect(getByText(imageError.errorContext)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const imageDetailLinks = getAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
});
|
||||
expect(imageDetailLinks).toHaveLength(3);
|
||||
|
||||
otherErrors.forEach((otherError) => {
|
||||
expect(queryByText(otherError.errorContext)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show other errors if there are fewer than 3 image errors', () => {
|
||||
const imageErrors = [
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['no-empty-alt-text'],
|
||||
errorContext: 'No empty alt text 1',
|
||||
},
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['no-default-alt-text'],
|
||||
errorContext: 'No default alt text 1',
|
||||
},
|
||||
];
|
||||
|
||||
const otherErrors = [
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['other'],
|
||||
errorContext: 'Other 1',
|
||||
},
|
||||
{
|
||||
errorDetail: '/detailUrl',
|
||||
ruleNames: ['other'],
|
||||
errorContext: 'Other 2',
|
||||
},
|
||||
];
|
||||
|
||||
const { getAllByRole, getByText, queryByText } = render(
|
||||
<AccessibilitySuggestions
|
||||
markdownLintErrors={[...otherErrors, ...imageErrors]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const listItems = getAllByRole('listitem');
|
||||
expect(listItems).toHaveLength(3);
|
||||
imageErrors.forEach((imageError) => {
|
||||
expect(getByText(imageError.errorContext)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
getAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
}),
|
||||
).toHaveLength(2);
|
||||
|
||||
expect(getByText('Other 1')).toBeInTheDocument();
|
||||
expect(queryByText('Other 2')).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
getAllByRole('link', {
|
||||
name: 'Learn more about accessible headings',
|
||||
}),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
146
app/javascript/utilities/markdown/markdownLintCustomRules.js
Normal file
146
app/javascript/utilities/markdown/markdownLintCustomRules.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Helper function for the image markdown lint rules.
|
||||
*
|
||||
* It takes a full line of text which includes an image with empty or default alt text (i.e. format "![]()") and returns the image portion only.
|
||||
* This allows us to point users towards the exact image markdown text that triggered the rule.
|
||||
*
|
||||
* @param {string} contentLine The full line of content as provided by markdownlint
|
||||
* @returns {string} a substring containing only the image text - e.g. "![alt text]()"
|
||||
*/
|
||||
const getImageTextString = (contentLine) => {
|
||||
let indexOfImageStart = contentLine.indexOf('!');
|
||||
while (contentLine.charAt(indexOfImageStart + 1) !== '[') {
|
||||
// It's possible for an image to be inserted on a line with text preceding it,
|
||||
// this check helps ensure that the '!' is actually the image start
|
||||
indexOfImageStart = contentLine.indexOf('!', indexOfImageStart + 1);
|
||||
if (indexOfImageStart === -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Find the next closing bracket from the image start
|
||||
// We don't need to worry about brackets inside the alt text as this check is only run on images with default or no alt text
|
||||
const indexOfImageEnd = contentLine.indexOf(')', indexOfImageStart);
|
||||
return contentLine.substring(indexOfImageStart, indexOfImageEnd + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom markdown lint rule that detects if a user has uploaded an image, but not changed the default alt text
|
||||
*/
|
||||
export const noDefaultAltTextRule = {
|
||||
names: ['no-default-alt-text'],
|
||||
description: 'Images should not have the default alt text',
|
||||
tags: ['images'],
|
||||
function: (params, onError) => {
|
||||
params.tokens
|
||||
.filter((token) => token.type === 'inline')
|
||||
.forEach(({ children }) => {
|
||||
children.forEach((contentChild) => {
|
||||
if (
|
||||
contentChild.type === 'image' &&
|
||||
contentChild.line.toLowerCase().includes('![alt text]')
|
||||
) {
|
||||
onError({
|
||||
lineNumber: contentChild.lineNumber,
|
||||
detail: '/p/editor_guide#alt-text-for-images',
|
||||
context: `Consider replacing the 'alt text' in square brackets at ${getImageTextString(
|
||||
contentChild.line,
|
||||
)} with a description of the image`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom rule that mirrors the default "no-alt-text" rule, but with a more helpful error message
|
||||
*/
|
||||
export const noEmptyAltTextRule = {
|
||||
names: ['no-empty-alt-text'],
|
||||
description: 'Images should not have empty alt text',
|
||||
tags: ['images'],
|
||||
function: (params, onError) => {
|
||||
params.tokens
|
||||
.filter((token) => token.type === 'inline')
|
||||
.forEach((inlineToken) => {
|
||||
inlineToken.children.forEach((contentChild) => {
|
||||
if (
|
||||
contentChild.type === 'image' &&
|
||||
contentChild.line.toLowerCase().includes('![]')
|
||||
) {
|
||||
onError({
|
||||
lineNumber: inlineToken.lineNumber,
|
||||
detail: '/p/editor_guide#alt-text-for-images',
|
||||
context: `Consider adding an image description in the square brackets at ${getImageTextString(
|
||||
contentChild.line,
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom markdown lint rule that detects if a level one heading has been used in a post
|
||||
*/
|
||||
export const noLevelOneHeadingsRule = {
|
||||
names: ['no-level-one-heading'],
|
||||
description: 'Heading level one should not be used in posts',
|
||||
tags: ['headings'],
|
||||
function: (params, onError) => {
|
||||
const levelOneHeadings = [];
|
||||
params.tokens.filter((token, index) => {
|
||||
const isHeadingOneStart =
|
||||
token.type === 'heading_open' && token.tag === 'h1';
|
||||
if (isHeadingOneStart) {
|
||||
// The next token is the actual content of the heading
|
||||
levelOneHeadings.push(params.tokens[index + 1]);
|
||||
}
|
||||
});
|
||||
|
||||
levelOneHeadings.forEach((heading) => {
|
||||
onError({
|
||||
lineNumber: heading.lineNumber,
|
||||
context: `Consider changing "${heading.line}" to a level two heading by using "##"`,
|
||||
detail: '/p/editor_guide#accessible-headings',
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom rule that mirrors the default "heading-increment" rule, but with a more helpful error message
|
||||
*/
|
||||
export const headingIncrement = {
|
||||
names: ['custom-heading-increment'],
|
||||
description: 'Heading levels should only increment by one level at a time',
|
||||
tags: ['headings', 'headers'],
|
||||
function: (params, onError) => {
|
||||
let prevLevel = 0;
|
||||
|
||||
const headings = params.tokens.filter(
|
||||
(token) => token.type === 'heading_open',
|
||||
);
|
||||
headings.forEach((heading) => {
|
||||
const level = Number.parseInt(heading.tag.slice(1), 10);
|
||||
if (prevLevel && level > prevLevel) {
|
||||
// Heading level has increased
|
||||
const suggestedHeadingLevel = prevLevel + 1;
|
||||
|
||||
if (suggestedHeadingLevel !== level) {
|
||||
const suggestedHeadingStart = Array(suggestedHeadingLevel)
|
||||
.fill('#')
|
||||
.join('');
|
||||
|
||||
onError({
|
||||
detail: '/p/editor_guide#accessible-headings',
|
||||
lineNumber: heading.lineNumber,
|
||||
context: `Consider changing the heading "${heading.line}" to a level ${suggestedHeadingLevel} heading by using "${suggestedHeadingStart}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
prevLevel = level;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -45,6 +45,24 @@ describe('Post Editor', () => {
|
|||
cy.findByTestId('error-message').should('be.visible');
|
||||
});
|
||||
|
||||
it('should show the accessibility suggestions notice', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
// Add a heading level one which should cause an accessibility lint error
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('# Heading level one');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByRole('heading', {
|
||||
name: 'Improve the accessibility of your post',
|
||||
}).should('exist');
|
||||
});
|
||||
|
||||
it('should show the Edit tab by default', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i })
|
||||
.findByRole('navigation', {
|
||||
|
|
@ -110,6 +128,24 @@ describe('Post Editor', () => {
|
|||
cy.findByTestId('error-message').should('be.visible');
|
||||
});
|
||||
|
||||
it('should show the accessibility suggestions notice', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
// Add a heading level one which should cause an accessibility lint error
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('# Heading level one');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByRole('heading', {
|
||||
name: 'Improve the accessibility of your post',
|
||||
}).should('exist');
|
||||
});
|
||||
|
||||
it('should show the Edit tab by default', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i })
|
||||
.findByRole('navigation', {
|
||||
|
|
@ -128,4 +164,215 @@ describe('Post Editor', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility suggestions', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/articleEditorV2User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginUser(user).then(() => {
|
||||
cy.visit('/new');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("shouldn't show accessibility suggestions if an error notice is present", () => {
|
||||
const postTextWithError = '# Heading level one';
|
||||
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
// Cause an error by having a non tag liquid tag without a tag name in the post body.
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type(`${postTextWithError}\n{%tag %}`, {
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByTestId('error-message').should('be.visible');
|
||||
|
||||
cy.findByRole('heading', {
|
||||
name: 'Improve the accessibility of your post',
|
||||
}).should('not.exist');
|
||||
});
|
||||
|
||||
it('should show a maximum of 3 accessibility suggestions', () => {
|
||||
const postTextWithFourErrors =
|
||||
'# Heading level 1\n\n\n#### Heading level 4';
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type(postTextWithFourErrors);
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
// Check the notice has appeared
|
||||
cy.findByRole('heading', {
|
||||
name: 'Improve the accessibility of your post',
|
||||
});
|
||||
|
||||
// Check each expected description has appeared
|
||||
cy.findByText(
|
||||
"Consider replacing the 'alt text' in square brackets at  with a description of the image",
|
||||
);
|
||||
cy.findByText(
|
||||
'Consider adding an image description in the square brackets at ',
|
||||
);
|
||||
cy.findByText(
|
||||
'Consider changing "# Heading level 1" to a level two heading by using "##"',
|
||||
);
|
||||
|
||||
// Check details links are shown for 3 errors
|
||||
cy.findAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
}).should('have.length', 2);
|
||||
cy.findAllByRole('link', {
|
||||
name: 'Learn more about accessible headings',
|
||||
}).should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should display image suggestions over heading suggestions', () => {
|
||||
const textWithThreeImageErrors =
|
||||
'\n\n';
|
||||
const textWithHeadingErrors = '# Heading level 1\n #### Heading level 4';
|
||||
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type(`${textWithHeadingErrors}\n${textWithThreeImageErrors}`);
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
// Verify the image suggestions are the only ones shown
|
||||
cy.findByRole('link', {
|
||||
name: 'Learn more about accessible headings',
|
||||
}).should('not.exist');
|
||||
cy.findAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
}).should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show a suggestion for level one headings', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('# Level one heading');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByText(
|
||||
'Consider changing "# Level one heading" to a level two heading by using "##"',
|
||||
);
|
||||
|
||||
cy.findByRole('link', {
|
||||
name: 'Learn more about accessible headings',
|
||||
}).should('have.attr', 'href', '/p/editor_guide#accessible-headings');
|
||||
});
|
||||
|
||||
it('should show a suggestion when heading level increases by more than one', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('## Level two heading\n#### Level four heading');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByText(
|
||||
'Consider changing the heading "#### Level four heading" to a level 3 heading by using "###"',
|
||||
);
|
||||
|
||||
cy.findByRole('link', {
|
||||
name: 'Learn more about accessible headings',
|
||||
}).should('have.attr', 'href', '/p/editor_guide#accessible-headings');
|
||||
});
|
||||
|
||||
it('should show a suggestion for empty alt text on images', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByText(
|
||||
'Consider adding an image description in the square brackets at ',
|
||||
);
|
||||
|
||||
cy.findByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
}).should('have.attr', 'href', '/p/editor_guide#alt-text-for-images');
|
||||
});
|
||||
|
||||
it('should show a suggestion for default alt text on images', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('\n');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByText(
|
||||
"Consider replacing the 'alt text' in square brackets at  with a description of the image",
|
||||
);
|
||||
|
||||
cy.findByText(
|
||||
"Consider replacing the 'alt text' in square brackets at  with a description of the image",
|
||||
);
|
||||
|
||||
cy.findAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
}).should('have.length', 2);
|
||||
cy.findAllByRole('link', {
|
||||
name: 'Learn more about accessible images',
|
||||
})
|
||||
.first()
|
||||
.should('have.attr', 'href', '/p/editor_guide#alt-text-for-images');
|
||||
});
|
||||
|
||||
it('should show the correct suggestion for alt text when other text exists on the same line', () => {
|
||||
cy.findByRole('form', { name: /^Edit post$/i }).as('articleForm');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByLabelText('Post Content')
|
||||
.clear()
|
||||
.type('Some text  Some more text');
|
||||
|
||||
cy.get('@articleForm')
|
||||
.findByRole('button', { name: /^Preview$/i })
|
||||
.click();
|
||||
|
||||
cy.findByText(
|
||||
"Consider replacing the 'alt text' in square brackets at  with a description of the image",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue