Skip to content

Commit

Permalink
feat: visual regression testing and dev server app (#94)
Browse files Browse the repository at this point in the history
<img height=20 src=https://readme.com/static/favicon.ico align=center> [Demo][demo] | 🎫 [Ticket][ticket]
:---:|:---:

### 🧰  Changes

Adds an example app to develop against. To start the dev server:

```
npm run start
```

And you can also run visual regression tests against it:

```
npm run test.browser
```

This runs `jest-image-snapshot` against the dev server. It uses saved
screenshots in `__tests__/browser/__image_snapshots__` to compare. If it
finds differences, it'll save diffs to
`__tests__/browser/__image_snapshots__/__diff_output__`.

Currently, there's little guarantee that your environment will match the
Github Actions environment. So, if you need to update snapshots, the
steps are:

* `sed -i -e 's/test.browser/test.browser -- -u/' .github/workflows/ci.yml`
* commit and push to github
* download 'Artifacts' from last build
* commit updated snapshots from 'Artifacts'

---

### TODO

- [x] update snapshots from CI
- [x] trim CI adornments

---

### Next steps

- [x] Add PR apps via heroku pipeline. Adding PR apps will make it nice
  to snapshot changes in PRs.
- [ ] dockerize puppeteer. Dockerizing the puppeteer browser should
  reduce broswer rendering fragility with the tests, and make it easier
  to create snapshots that match CI.

---

### Things that are broken

- [ ] testing snapshots currently fails locally. Presumably until we use
  docker locally, we'll be out of sync with CI.
- [ ] `npm run test.browser` seems to have broken jest's cache. You need
  to run `npx jest --clearCache` between runs
- [ ] `npm run test.browser.watch` does not kill the dev server. We
  typically won't be running it in watch mode.

[demo]: https://markdown-pr-94.herokuapp.com/#
[ticket]: https://app.asana.com/0/1199089258199423/1199546355080189/f
  • Loading branch information
kellyjosephprice committed Dec 19, 2020
1 parent f4bd718 commit 0774166
Show file tree
Hide file tree
Showing 49 changed files with 2,262 additions and 88 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ jobs:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm ci
run: |
npm ci
sudo apt install fonts-noto-color-emoji
make emojis
- name: Run tests
run: npm test

- name: Run visual regression tests
run: CI=true npm run test.browser

- uses: actions/upload-artifact@v2
with:
name: image-snapshots
path: __tests__/browser/ci
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ node_modules
*.code-*
*.sublime-*

.DS_Store
.DS_Store

__image_snapshots__

example/img/emojis
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.DEFAULT_GOAL := help
.PHONY: help

emojis: example/img/emojis ## Install our emojis

example/img/emojis: node_modules/@readme/emojis
rm -rf example/img/emojis
mkdir -p example/img/emojis
cp node_modules/@readme/emojis/src/img/*.png example/img/emojis/

help: ## Show this help.
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: npx http-server example --port $PORT
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions __tests__/browser/global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'babel-polyfill';
import { setup as setupPuppeteer } from 'jest-environment-puppeteer';

module.exports = async function globalSetup(globalConfig) {
await setupPuppeteer(globalConfig);
};
38 changes: 38 additions & 0 deletions __tests__/browser/markdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* global page */

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

describe('visual regression tests', () => {
describe('rdmd syntax', () => {
beforeEach(async () => {
await page.setViewport({ width: 800, height: 800 });
});

const docs = [
'callouts',
'codeBlockTests',
'codeBlockVarsTest',
'codeBlocks',
'embeds',
'features',
'headings',
'images',
'lists',
'tables',
];

it.each(docs)(
'renders "%s" without surprises',
async doc => {
const uri = `http://localhost:9966/?ci=true#${doc}`;
await page.goto(uri, { waitUntil: 'networkidle0' });
await sleep(500);

const image = await page.screenshot({ fullPage: true });

expect(image).toMatchImageSnapshot();
},
10000
);
});
});
12 changes: 12 additions & 0 deletions __tests__/browser/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
import path from 'path';

const opts = {};

if (process.env.CI) {
opts.customSnapshotsDir = path.resolve('__tests__/browser/ci/');
}

const toMatchImageSnapshot = configureToMatchImageSnapshot(opts);

expect.extend({ toMatchImageSnapshot });
76 changes: 76 additions & 0 deletions example/Demo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';

import markdown from '../index';
import Fixtures from './Fixtures';
import Header from './Header';
import Router from './Router';

require('./demo.scss');

function DemoContent({ ci, children, fixture, name, onChange, opts }) {
if (ci) {
return (
<div className="rdmd-demo--display">
<div className="markdown-body">{markdown(fixture, opts)}</div>
</div>
);
}

return (
<React.Fragment>
<div className="rdmd-demo--editor">
<div className="rdmd-demo--editor-container">
{children}
<textarea name="demo-editor" onChange={onChange} value={fixture} />
</div>
</div>
<div className="rdmd-demo--display">
<h2 className="rdmd-demo--markdown-header">{name}</h2>
<div className="markdown-body">{markdown(fixture, opts)}</div>
</div>
</React.Fragment>
);
}

DemoContent.propTypes = {
children: PropTypes.node.isRequired,
ci: PropTypes.string,
fixture: PropTypes.string,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
opts: PropTypes.obj,
};

function Demo({ opts }) {
// eslint-disable-next-line no-restricted-globals
const ci = new URLSearchParams(location.search).get('ci');

return (
<React.Fragment>
{!ci && <Header />}
<div className="rdmd-demo--container">
<div className="rdmd-demo--content">
<Router
render={({ route, getRoute }) => {
return (
<Fixtures
ci={ci}
getRoute={getRoute}
render={props => <DemoContent {...props} ci={ci} opts={opts} />}
selected={route}
/>
);
}}
/>
</div>
</div>
</React.Fragment>
);
}

Demo.propTypes = {
opts: PropTypes.obj,
};

export default Demo;
179 changes: 179 additions & 0 deletions example/Fixtures/Syntax/callouts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
[block:api-header]
{
"title": "Syntax"
}
[/block]
Callouts are very nearly equivalent to standard Markdown block quotes in their syntax, other than some specific requirements on their content: To be considered a “callout”, a block quote must start with an initial emoji. This is used to determine the callout's theme. Here's an example of how you might write a warning callout.

> 👍 Success
>
> Vitae reprehenderit at aliquid error voluptates eum dignissimos.

### Emoji Themes

Default themes are specified using one of the following emojis. (If you don't like the one we've chosen, you can always switch to the alternate!)

| Emoji | Class | Alternate |
|:-----:|:--------|-------------:|
| 📘 | `.callout_info` | ℹ️ |
| 👍 | `.callout_okay` ||
| 🚧 | `.callout_warn` | ⚠️ |
| ❗️ | `.callout_error` | 🛑 |

<hr>
<details><summary><em>Tips & Tricks </em></summary><br>

If you have a block quote that starts with an initial emoji which *should not* be rendered as a ReadMe callout, just bold the emoji. It's a bit of a hack for sure, but it's easy enough, and hey: it works! So this:

> **👋** Lorem ipsum dolor sit amet consectetur adipisicing elit.

Renders to a plain ol' block quote:

> **👋** Lorem ipsum dolor sit amet consectetur adipisicing elit.
</details><hr>
[block:api-header]
{
"title": "Examples"
}
[/block]

[block:callout]
{
"type": "success",
"body": "Vitae reprehenderit at aliquid error voluptates eum dignissimos.",
"title": "[Success](#edge-cases)"
}
[/block]
> 📘 Info
>
> Lorem ipsum dolor sit amet consectetur adipisicing elit.
> 🚧 Warning
>
> Hic, neque a nisi adipisci non repudiandae ratione id natus.
> ❗️ Error
> Sunt eius porro assumenda sequi, explicabo dolorem unde.
If a callout starts with an emoji that's not dedicated to one of the themes (above), the component will fall back to a default block quote-style color scheme.

> 🥇 Themeless
>
> Lorem ipsum dolor sit amet consectetur adipisicing elit.
[block:api-header]
{
"title": "Custom CSS"
}
[/block]
Callouts come in [various themes](#section--examples-). These can be customized using the following CSS selectors and variables:


```scss CSS Variables
.markdown-body .callout.callout_warn {
--text: #6a737d; // theme text color default
--title: inherit; // theme title color (falls back to text color by default)
--background: #f8f8f9;
--border: #8b939c;
}
```
```scss Theme Selectors
.markdown-body .callout.callout_default {} /* gray */
.markdown-body .callout.callout_info {} /* blue */
.markdown-body .callout.callout_okay {} /* green */
.markdown-body .callout.callout_warn {} /* orange */
.markdown-body .callout.callout_error {} /* red */
```

### Extended Themes

Each callout will also have a `theme` attribute that's set to it's emoji prefix. Combined with a basic attribute selector, we should be able to create entirely new styles per-emoji, in addition to the built in themes above!

```css Custom CSS
.markdown-body .callout[theme="🎅"] {
--background: #c54245;
--border: #ffffff6b;
--text: #f5fffa;
}
```
```markdown Markdown Syntax
> 🎅 Old Saint Nick
>
> 'Twas the night before Christmas, when all through the house not a creature was stirring, not even a mouse. The stockings were hung by the chimney with care, in hopes that St. Nicholas soon would be there. The children were nestled all snug in their beds, while visions of sugar plums danced in their heads.
```
```html Generated HTML
<!-- condensed for clarity! -->
<blockquote class="callout callout_default" theme="🎅">
<h3>🎅 Old Saint Nick</h3>
<p>'Twas the night before Christmas, when all through the house not a creature was stirring, not even a mouse. The stockings were hung by the chimney with care, in hopes that St. Nicholas soon would be there. The children were nestled all snug in their beds, while visions of sugar plums danced in their heads.</p>
</blockquote>
```

And voilà...

> 🎅 Old Saint Nick
>
> 'Twas the night before Christmas, when all through the house not a creature was stirring, not even a mouse. The stockings were hung by the chimney with care, in hopes that St. Nicholas soon would be there. The children were nestled all snug in their beds, while visions of sugar plums danced in their heads.
### Custom Icons

Emojis are already a pretty good starting point as far as default icon options go! There are a *lot* of 'em, and they're supported across nearly all platforms. But what if we're going for a different look, or need to match our docs to a branding kit? Icons are a big part of setting the "tone" for your site.

With a touch of Custom CSS, we should be able to get a callout using the 📷 emoji to display an icon font glyph!

```css Custom CSS
.callout[theme=📷] {
--emoji: unset;
--icon: "\f083"; /* copied front FontAwesome */
--icon-color: #c50a50;
}
```
``` Markdown Syntax
> 📷 Cool pix!
>
> Vitae reprehenderit at aliquid error voluptates eum dignissimos.
```

This works like a charm:

<div id="my-theme">

> 📸 Cool pix!
> Vitae reprehenderit at aliquid error voluptates eum dignissimos.
[block:html]
{
"html": "<style>\n #my-theme .callout[theme=📸] {\n --emoji: unset;\n --icon: \"\";\n }\n #my-theme .callout[theme=📷],\n #my-theme .callout[theme=📸] {\n --icon-color: #c50a50;\n --border: var(--icon-color);\n --title: var(--icon-color);\n }\n summary {\n outline: none;\n user-select: none;\n }\n</style>"
}
[/block]
</div>

<hr><details><summary><em>Setting the Custom Icon Font</em></summary><br>

The custom icon font defaults to `FontAwesome`, but you can use any font family available on the page by setting the `--icon-font` variable!

```css
.callout[theme=📷] {
--icon-font-family: FontAwesome; /* copied from https://fontawesome.com/v4.7.0/icon/camera-retro */
}
```

</details><hr>
[block:api-header]
{
"title": "Edge Cases"
}
[/block]
Callouts don't need to have any body text:

> 🥇 No body text.
You can also skip the title, if you're so inclined!

> 🥈
>
> Lorem ipsum dolor sit amet consectetur adipisicing elit. Error eos animi obcaecati quod repudiandae aliquid nemo veritatis ex, quos delectus minus sit omnis vel dolores libero, recusandae ea dignissimos iure?
[block:html]
{
"html": "<style>\n.markdown-body .callout[theme=\"🎅\"] {\n --background: #c50a4f;\n --border: #ffffff6b;\n --text: #f5fffa;\n}\n</style>"
}
[/block]
Loading

0 comments on commit 0774166

Please sign in to comment.