docsify/packages/docsify-server-renderer/index.js
John Hildenbiddle 7f0c42eda5
Jest + Playwright Testing (#1276)
* Initial Jest+Playwright setup

* Fix eslint warnings

* Add placeholder for common options

* Fix load order of scriptURLs

* Add docsifyURL and waitForSelector options

* Add executeScript scenarios

* Enable firefox and webkit tests by default

* Prevent prettier from reformatting browsers list

* Update options and add JSDoc comments

* Initial commit

* Complete initial example tests

* Minor tweaks

* Change suite name

* Rename file

* Add npm script to run jest+playwright example test

* Clean up server properties

* Isolate e2e, integration, and unit test environments

- Setup projects for e2e, integration, and unit tests in Jest configuration
- Setup /tests/e2e/ tests for Jest+PlayWright
- Setup /tests/integration/ tests for Jest
- Setup /tests/unit/ tests for Jest
- Setup eslint for Jest and Jest+Playwright environments
- Move e2e configuration files into separate folder
- Update e2e example tests
- Update unit example tests
- Update HTML fixtures

* Update docsifyInit helper

- Add `debug` option
- Append `Markdown` suffix to markdown-related options
- Reorder options alphabetically

* Add jestPlaywright.debug() to e2e examples

* Removed need to pass page as arg to docsifyInit()

* Add VSCode debug configurations

* Change test server port from 8080 to 3001

* Clean up test names and expect() order

* Update npm scripts to ignore example tests

* Add CLI commands and resources section

* Add Jest unit test snapshot example

* Added Jest unit and e2e tests to Github workflows

* Add npm script to run Jest examples

Added for new devs who are exploring the examples, as the need for passing --testPathIgnorePatterns is not obvious. This flag is required because the example tests are excluded by default.

* Remove node 10.x and add 14.x from tests

Required for jest+playwright testitng

* Temporarily disable testPathIgnorePatterns for ci

* Temporarily remove node 14 from matrix

Cypress fails on node 14.x

* Alternate workflow setup for new test env

* Update workflow platforms and node versions

Also cleaning up naming

* Restoring original workflow names

* Remove Cypress tests

* Remove/Reset GitHub actions

* Restore/reset workflows

* Bump actions/checkout@v1 to v2

* Use playright GH action

* Update playwright GH action configuration

* Remove unnecessary steps and update checkout version

* Add install step

* Add --ignore-scripts flag

Linting does not require running scripts automatically executred after install.

* Swicth from ci to i for install

* Add new Build & Test workflow

* Add OS tests

* Wait for network idle for more reliable ready state

* Configure image snapshot testing and add example

* Remove example fixture file

* Adjust image snapshot threshold for local & CI tests

* Upload diff-output artifact

* Add failure() check to upload-artifact

* Bump image snapshot threshold for local & CI tests

* Set diffDirection

* Fix XHR caching in playwright tests

* Update image snapshot example and theshold

* Bump image snapshot threshold for local & CI tests

* Remove old workflows

* Bump image snapshot threshold for local & CI tests

* Bump image snapshot failureThreshold for local & CI tests

* Set failureThresholdType to percent

* Change image snapshot comparisonMethod to ssim

* Remove pixelMatch options (incompatible with ssim)

* Bump image snapshot failureThreshold

* Bump image snapshot failureThreshold

* Disable fast-fail to allow all checks to complete

* Rename workflow

* Disable fast-fail to allow all checks to complete

* Store pixel and ssim comparison configs

* Add link to image snapshot test

* Fix CI errors on Windows due to image size mismatch

* Attempt to fix macos e2e exit code 134 issue

* Update test.yml

* Update test.yml

* Update test.yml

* Fix --ci flag

* Set Jets to use --runInBand for macOS e2e

* Remove unnecessary macOS check

* Set image snapshot to runInProcess (macOS CI fix)

* Update test.yml

* Temporary failureThreshold change for debugging

* Upload os+node-specific diff artifacts

* Remove node version from diff artifact

* Revert "Remove node version from diff artifact"

This reverts commit 9cfcc4342bb22f18da30363a4c52758f13ba0cc9.

* Revert "Revert "Remove node version from diff artifact""

This reverts commit ad6c1891e48c70c9973a5c0d7b876b2d4621f3b9.

* Restoring failureThreshold after debugging

* Remove runInProcess option

* Revert "Remove runInProcess option"

This reverts commit 667ed6c870fc56f0349fc5eccc2cdeead3eff4ea.

* Add node-specific artifact uploads

* Set ssim mode to fast and restore runInProcess

* Set failureThreshold to 0.01

* Updating to playwright@next for webkit fix

* Restore optimal ssim configuration

* Testing pixel vs. ssim image comparison

* Move shared test setup to unit/int/e2e setups

* Refactor test server setup

* Replace live-server with browser-sync

* Update script for running local docs site

- Only init GA plugin on public site
- Only init Gitter plugin on public site
- Only init Matomo plugin on public site

NOTE: Large diff is a result of initial Prettier formatting. Actual change limited to last <script> block on page.

* Allow launching test server preview with --start

* Add integration tests and refactor unit + e2e

* Unify docsifyInit() and cleanup

- Create unified docsifyInit() for jest and playwright
- Move shared helpers to /tests/helpers
- Update tests
- Update globals

* Fixed webkit routes by specifying ContentType

* Update snapshot

* Update dependencies

* Update tests to use unified docsifyInit()

* Remove Cypress (old e2e test environment)

* Update tests to run (unit/integration/e2e)

- Add new integration tests (Jest)
- Remove old unit tests (mocha+jsdom)
- Remove old e2e tests (Cypress)

* Remove Cypress-related dependency

* Remove mocha+chai+jsdom (old unit/int test env)

* Rename testing directories and scripts

- Rename /tests/ directory to /test/
- Rename script/task names by removing “jest” identifier
- Remove “test:jest-examples” script

* - Configure test server for availability with all tests (previously e2e only)
- Create identical docsifyInit() tests using Jest (integration) and Playwright (e2e)
- Update docsifyInit() to convert relative URLs to absolute URLs to work in both JSDOM and Playwright
- Update docsifyInit() to append style- and js-related tags using createElement instead of insertAdjacentHTML
- Update paths in test files to use unified docsifyInit()
- Added option to docsifyInit() to enable/disable formatted HTML output to console
- Removed vue.css as default docsify theme from docsifyInit()
- Removed outdated files

* Reorganize test files

* Fix basePath option

* Replace do-mock-ajax with xhr-mock

- Allows mocking all XMLHttpRequests instead of just those initiated via /src/core/fetch/ajax.js
- Allows JSDOM tests to use /lib/docsify.js instead of /src/core/index.js (same as Playwright tests)
- Allows JSDOM tests to use /docs site as test content

* Added new waitFor helpers

* Clean up globals

- Import globals from various files instead of manually adding them to ensure they are availability in Jest and eslint configurations
- Add middleware to server configuration for serving virtual “_blank.html” file
- Add BLANK_URL
- Rename TEST_URL to TEST_HOST
- Removed ./test/fixtures/ directory (blank page now served via server.js middleware)
- Added page.goto(BLANK_URL) call to global Playwright beforeEach() setup

* Add try/catch for waitForFunction callback

* Move playwright config into jest.config.js

* Add runInBand option to tests for reliability

* Remove unnecessary XHR Mock teardown

* Add —runInBand to test script

* Merge develop

* Cleanup

- Relocate carbon plugin script with other scripts
- Update zh-cn docs URL to align with other translatins (GitHub, not jsdelivr)
- Add major version locks to URLs

* Removed fixed host value from docs

* Updated test after merge (docsify version change)

* Added startPath for manual instance

* Remove Node 10 from CI tests

* Remove Node 10 from CI lint tests
2020-10-05 14:10:30 -05:00

212 lines
5.3 KiB
JavaScript

import { readFileSync } from 'fs';
import { resolve, basename } from 'path';
import resolvePathname from 'resolve-pathname';
import fetch from 'node-fetch';
import debug from 'debug';
import DOMPurify from 'dompurify';
import { AbstractHistory } from '../../src/core/router/history/abstract';
import { Compiler } from '../../src/core/render/compiler';
import { isAbsolutePath } from '../../src/core/router/util';
import * as tpl from '../../src/core/render/tpl';
import { prerenderEmbed } from '../../src/core/render/embed';
function cwd(...args) {
return resolve(process.cwd(), ...args);
}
function isExternal(url) {
let match = url.match(
/^([^:/?#]+:)?(?:\/\/([^/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/
);
if (
typeof match[1] === 'string' &&
match[1].length > 0 &&
match[1].toLowerCase() !== location.protocol
) {
return true;
}
if (
typeof match[2] === 'string' &&
match[2].length > 0 &&
match[2].replace(
new RegExp(
':(' + { 'http:': 80, 'https:': 443 }[location.protocol] + ')?$'
),
''
) !== location.host
) {
return true;
}
return false;
}
function mainTpl(config) {
let html = `<nav class="app-nav${
config.repo ? '' : ' no-badge'
}"><!--navbar--></nav>`;
if (config.repo) {
html += tpl.corner(config.repo);
}
if (config.coverpage) {
html += tpl.cover();
}
html += tpl.main(config);
return html;
}
export default class Renderer {
constructor({ template, config, cache }) {
this.html = template;
this.config = config = Object.assign({}, config, {
routerMode: 'history',
});
this.cache = cache;
this.router = new AbstractHistory(config);
this.compiler = new Compiler(config, this.router);
this.router.getCurrentPath = () => this.url;
this._renderHtml(
'inject-config',
`<script>window.$docsify = ${JSON.stringify(config)}</script>`
);
this._renderHtml('inject-app', mainTpl(config));
this.template = this.html;
}
_getPath(url) {
const file = this.router.getFile(url);
return isAbsolutePath(file) ? file : cwd(`./${file}`);
}
async renderToString(url) {
this.url = url = this.router.parse(url).path;
this.isRemoteUrl = isExternal(this.url);
const { loadSidebar, loadNavbar, coverpage } = this.config;
const mainFile = this._getPath(url);
this._renderHtml('main', await this._render(mainFile, 'main'));
if (loadSidebar) {
const name = loadSidebar === true ? '_sidebar.md' : loadSidebar;
const sidebarFile = this._getPath(resolve(url, `./${name}`));
this._renderHtml('sidebar', await this._render(sidebarFile, 'sidebar'));
}
if (loadNavbar) {
const name = loadNavbar === true ? '_navbar.md' : loadNavbar;
const navbarFile = this._getPath(resolve(url, `./${name}`));
this._renderHtml('navbar', await this._render(navbarFile, 'navbar'));
}
if (coverpage) {
let path = null;
if (typeof coverpage === 'string') {
if (url === '/') {
path = coverpage;
}
} else if (Array.isArray(coverpage)) {
path = coverpage.indexOf(url) > -1 && '_coverpage.md';
} else {
const cover = coverpage[url];
path = cover === true ? '_coverpage.md' : cover;
}
const coverFile = this._getPath(resolve(url, `./${path}`));
this._renderHtml('cover', await this._render(coverFile), 'cover');
}
const html = this.isRemoteUrl ? DOMPurify.sanitize(this.html) : this.html;
this.html = this.template;
return html;
}
_renderHtml(match, content) {
this.html = this.html.replace(new RegExp(`<!--${match}-->`, 'g'), content);
return this.html;
}
async _render(path, type) {
let html = await this._loadFile(path);
const { subMaxLevel, maxLevel } = this.config;
let tokens;
switch (type) {
case 'sidebar':
html =
this.compiler.sidebar(html, maxLevel) +
`<script>window.__SUB_SIDEBAR__ = ${JSON.stringify(
this.compiler.subSidebar(subMaxLevel)
)}</script>`;
break;
case 'cover':
html = this.compiler.cover(html);
break;
case 'main':
tokens = await new Promise(r => {
prerenderEmbed(
{
fetch: url => this._loadFile(this._getPath(url)),
compiler: this.compiler,
raw: html,
},
r
);
});
html = this.compiler.compile(tokens);
break;
case 'navbar':
case 'article':
default:
html = this.compiler.compile(html);
break;
}
return html;
}
async _loadFile(filePath) {
debug('docsify')(`load > ${filePath}`);
let content;
try {
if (isAbsolutePath(filePath)) {
const res = await fetch(filePath);
if (!res.ok) {
throw Error();
}
content = await res.text();
this.lock = 0;
} else {
content = await readFileSync(filePath, 'utf8');
this.lock = 0;
}
return content;
} catch (e) {
this.lock = this.lock || 0;
if (++this.lock > 10) {
this.lock = 0;
return;
}
const fileName = basename(filePath);
const result = await this._loadFile(
resolvePathname(`../${fileName}`, filePath)
);
return result;
}
}
}
Renderer.version = '__VERSION__';