Merge pull request #1409 from docsifyjs/feat/vue-options

feat: Vue components, mount options, global options, and v3 support
This commit is contained in:
John Hildenbiddle 2020-11-23 14:39:10 -06:00 committed by GitHub
commit bd662c421a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1066 additions and 418 deletions

View File

@ -1,3 +0,0 @@
{
"video": false
}

View File

@ -245,10 +245,10 @@ window.$docsify = {
// Custom file name
coverpage: 'cover.md',
// mutiple covers
// multiple covers
coverpage: ['/', '/zh-cn/'],
// mutiple covers and custom file name
// multiple covers and custom file name
coverpage: {
'/': 'cover.md',
'/zh-cn/': 'cover.md',
@ -410,7 +410,7 @@ window.$docsify = {
};
```
?> If this options is `false` but you dont want to emojify some specific colons , [Refer this](https://github.com/docsifyjs/docsify/issues/742#issuecomment-586313143)
?> If this options is `false` but you don't want to emojify some specific colons , [Refer this](https://github.com/docsifyjs/docsify/issues/742#issuecomment-586313143)
## mergeNavbar
@ -494,15 +494,15 @@ window.$docsify = {
```
## crossOriginLinks
- type: `Array`
When `routerMode: 'history'`, you may face the cross-origin issues, See [#1379](https://github.com/docsifyjs/docsify/issues/1379).
In Markdown content, there is a simple way to solve it, see extends Markdown syntax `Cross-Origin link` in [helpers](helpers.md).
- type: `Array`
When `routerMode: 'history'`, you may face the cross-origin issues, See [#1379](https://github.com/docsifyjs/docsify/issues/1379).
In Markdown content, there is a simple way to solve it, see extends Markdown syntax `Cross-Origin link` in [helpers](helpers.md).
```js
window.$docsify = {
crossOriginLinks:[
"https://example.com/cross-origin-link",
],
crossOriginLinks: ['https://example.com/cross-origin-link'],
};
```
@ -604,7 +604,7 @@ window.$docsify = {
};
```
Load the right 404 page according to the localisation:
Load the right 404 page according to the localization:
```js
window.$docsify = {
@ -629,3 +629,104 @@ window.$docsify = {
topMargin: 90, // default: 0
};
```
## vueComponents
- type: `Object`
Creates and registers global [Vue components](https://vuejs.org/v2/guide/components.html). Components are specified using the component name as the key with an object containing Vue options as the value. Component `data` is unique for each instance and will not persist as users navigate the site.
```js
window.$docsify = {
vueComponents: {
'button-counter': {
template: `
<button @click="count += 1">
You clicked me {{ count }} times
</button>
`,
data() {
return {
count: 0,
};
},
},
},
};
```
```markdown
<button-counter></button-counter>
```
<output data-lang="output">
<button-counter></button-counter>
</output>
## vueGlobalOptions
- type: `Object`
Specifies [Vue options](https://vuejs.org/v2/api/#Options-Data) for use with Vue content not explicitly mounted with [vueMounts](#mounting-dom-elements), [vueComponents](#components), or a [markdown script](#markdown-script). Changes to global `data` will persist and be reflected anywhere global references are used.
```js
window.$docsify = {
vueGlobalOptions: {
data() {
return {
count: 0,
};
},
},
};
```
```markdown
<p>
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</p>
```
<output data-lang="output">
<p>
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</p>
</output>
## vueMounts
- type: `Object`
Specifies DOM elements to mount as [Vue instances](https://vuejs.org/v2/guide/instance.html) and their associated options. Mount elements are specified using a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) as the key with an object containing Vue options as their value. Docsify will mount the first matching element in the main content area each time a new page is loaded. Mount element `data` is unique for each instance and will not persist as users navigate the site.
```js
window.$docsify = {
vueMounts: {
'#counter': {
data() {
return {
count: 0,
};
},
},
},
};
```
```markdown
<div id="counter">
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</div>
```
<output id="counter">
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</output>

View File

@ -101,6 +101,62 @@
'/': 'Search',
},
},
vueComponents: {
'button-counter': {
template: `
<button @click="count += 1">
You clicked me {{ count }} times
</button>
`,
data() {
return {
count: 0,
};
},
},
},
vueGlobalOptions: {
data() {
return {
count: 0,
message: 'Hello, World!',
// Fake API response
images: [
{ title: 'Image 1', url: 'https://picsum.photos/150?random=1' },
{ title: 'Image 2', url: 'https://picsum.photos/150?random=2' },
{ title: 'Image 3', url: 'https://picsum.photos/150?random=3' },
],
};
},
computed: {
timeOfDay() {
const date = new Date();
const hours = date.getHours();
if (hours < 12) {
return 'morning';
} else if (hours < 18) {
return 'afternoon';
} else {
return 'evening';
}
},
},
methods: {
hello: function() {
alert(this.message);
},
},
},
vueMounts: {
'#counter': {
data() {
return {
count: 0,
};
},
},
},
plugins: [
function(hook, vm) {
hook.beforeEach(function(html) {
@ -163,5 +219,6 @@
})();
</script>
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
<!-- <script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script> -->
</body>
</html>

View File

@ -1,8 +1,8 @@
# Vue compatibility
Docsify allows Vue [v2.x](https://vuejs.org) and [v3.x](https://v3.vuejs.org) components to be added directly to you Markdown files. These components can greatly simplify working with data and adding reactivity to your content.
Docsify allows Vue content to be added directly to you markdown pages. This can greatly simplify working with data and adding reactivity to your site.
To get started, load either the production or development version of Vue in your `index.html`:
To get started, add Vue [2.x](https://vuejs.org) or [3.x](https://v3.vuejs.org) to your `index.html` file. Choose the production version for your live site or the development version for helpful console warnings and [Vue.js devtools](https://github.com/vuejs/vue-devtools) support.
#### Vue 2.x
@ -10,7 +10,7 @@ To get started, load either the production or development version of Vue in your
<!-- Production -->
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
<!-- Development (debugging and Vue.js devtools support) -->
<!-- Development -->
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
```
@ -20,118 +20,327 @@ To get started, load either the production or development version of Vue in your
<!-- Production -->
<script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<!-- Development (debugging and Vue.js devtools support) -->
<!-- Development -->
<script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
```
## Basic rendering
## Template syntax
Docsify will automatically render basic Vue content that does not require `data`, `methods`, or other instance features.
Vue [template syntax](https://vuejs.org/v2/guide/syntax.html) is used to create dynamic content. With no additional configuration, this syntax offers several useful features like support for [JavaScript expressions](https://vuejs.org/v2/guide/syntax.html#Using-JavaScript-Expressions) and Vue [directives](https://vuejs.org/v2/guide/syntax.html#Directives) for loops and conditional rendering.
```markdown
<!-- Hide in docsify, show elsewhere (e.g. GitHub) -->
<p v-if="false">Text for GitHub</p>
<!-- Sequenced content (i.e. loop)-->
<ul>
<li v-for="i in 3">{{ i }}</li>
<li v-for="i in 3">Item {{ i }}</li>
</ul>
<!-- JavaScript expressions -->
<p>2 + 2 = {{ 2 + 2 }}</p>
```
The HTML above will render the following:
<output data-lang="output">
<p v-if="false">Text for GitHub</p>
<ul>
<li v-for="i in 3">{{ i }}</li>
</ul>
<ul>
<li v-for="i in 3">Item {{ i }}</li>
</ul>
## Advanced usage
<p>2 + 2 = {{ 2 + 2 }}</p>
</output>
Vue components and templates that require `data`, `methods`, computed properties, lifecycle hooks, etc. require manually creating a new `Vue()` instance within a `<script>` tag in your markdown.
[View output on GitHub](https://github.com/docsifyjs/docsify/blob/develop/docs/vue.md#template-syntax)
Vue content becomes more interesting when [data](#data), [computed properties](#computed-properties), [methods](#methods), and [lifecycle hooks](#lifecycle-hooks) are used. These options can be specified as [global options](#global-options) or within DOM [mounts](#mounts) and [components](#components).
### Data
```js
{
data() {
return {
message: 'Hello, World!'
};
}
}
```
<!-- prettier-ignore-start -->
```markdown
<!-- Show message in docsify, show "{{ message }}" elsewhere (e.g. GitHub) -->
{{ message }}
<!-- Show message in docsify, hide elsewhere (e.g. GitHub) -->
<p v-text="message"></p>
<!-- Show message in docsify, show text elsewhere (e.g. GitHub) -->
<p v-text="message">Text for GitHub</p>
```
<!-- prettier-ignore-end -->
<output data-lang="output">
{{ message }}
<p v-text="message"></p>
<p v-text="message">Text for GitHub</p>
</output>
[View output on GitHub](https://github.com/docsifyjs/docsify/blob/develop/docs/vue.md#data)
### Computed properties
```js
{
computed: {
timeOfDay() {
const date = new Date();
const hours = date.getHours();
if (hours < 12) {
return 'morning';
}
else if (hours < 18) {
return 'afternoon';
}
else {
return 'evening'
}
}
},
}
```
```markdown
<div id="example-1">
<p>{{ message }}</p>
Good {{ timeOfDay }}!
```
<button v-on:click="hello">Say Hello</button>
<output data-lang="output">
<button v-on:click="counter -= 1">-</button>
{{ counter }}
<button v-on:click="counter += 1">+</button>
Good {{ timeOfDay }}!
</output>
### Methods
```js
{
data() {
return {
message: 'Hello, World!'
};
},
methods: {
hello() {
alert(this.message);
}
},
}
```
```markdown
<button @click="hello">Say Hello</button>
```
<output data-lang="output">
<p><button @click="hello">Say Hello</button></p>
</output>
### Lifecycle Hooks
```js
{
data() {
return {
images: null,
};
},
created() {
fetch('https://api.domain.com/')
.then(response => response.json())
.then(data => (this.images = data))
.catch(err => console.log(err));
}
}
// API response:
// [
// { title: 'Image 1', url: 'https://domain.com/1.jpg' },
// { title: 'Image 2', url: 'https://domain.com/2.jpg' },
// { title: 'Image 3', url: 'https://domain.com/3.jpg' },
// ];
```
```markdown
<div style="display: flex;">
<figure style="flex: 1;">
<img v-for="image in images" :src="image.url" :title="image.title">
<figcaption>{{ image.title }}</figcaption>
</figure>
</div>
```
<!-- prettier-ignore-end -->
<output data-lang="output">
<div style="display: flex;">
<figure v-for="image in images" style="flex: 1; text-align: center;">
<img :src="image.url">
<figcaption>{{ image.title }}</figcaption>
</figure>
</div>
</output>
#### Vue 2.x
## Global options
```markdown
<script>
new Vue({
el: "#example-1",
data: function() {
Use `vueGlobalOptions` to specify [Vue options](https://vuejs.org/v2/api/#Options-Data) for use with Vue content not explicitly mounted with [vueMounts](#mounts), [vueComponents](#components), or a [markdown script](#markdown-script). Changes to global `data` will persist and be reflected anywhere global references are used.
```js
window.$docsify = {
vueGlobalOptions: {
data() {
return {
counter: 0,
message: "Hello, World!"
count: 0,
};
},
methods: {
hello: function() {
alert(this.message);
}
}
},
};
```
```markdown
<p>
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</p>
```
<output data-lang="output">
<p>
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</p>
</output>
Notice the behavior when multiple global counters are rendered:
<output data-lang="output">
<p>
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</p>
</output>
Changes made to one counter affect the both counters. This is because both instances reference the same global `count` value. Now, navigate to a new page and return to this section to see how changes made to global data persist between page loads.
## Mounts
Use `vueMounts` to specify DOM elements to mount as [Vue instances](https://vuejs.org/v2/guide/instance.html) and their associated options. Mount elements are specified using a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) as the key with an object containing Vue options as their value. Docsify will mount the first matching element in the main content area each time a new page is loaded. Mount element `data` is unique for each instance and will not persist as users navigate the site.
```js
window.$docsify = {
vueMounts: {
'#counter': {
data() {
return {
count: 0,
};
},
},
},
};
```
```markdown
<div id="counter">
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</div>
```
<output id="counter">
<button @click="count -= 1">-</button>
{{ count }}
<button @click="count += 1">+</button>
</output>
## Components
Use `vueComponents` to create and register global [Vue components](https://vuejs.org/v2/guide/components.html). Components are specified using the component name as the key with an object containing Vue options as the value. Component `data` is unique for each instance and will not persist as users navigate the site.
```js
window.$docsify = {
vueComponents: {
'button-counter': {
template: `
<button @click="count += 1">
You clicked me {{ count }} times
</button>
`,
data() {
return {
count: 0,
};
},
},
},
};
```
```markdown
<button-counter></button-counter>
<button-counter></button-counter>
```
<output data-lang="output">
<button-counter></button-counter>
<button-counter></button-counter>
</output>
## Markdown script
Vue content can mounted using a `<script>` tag in your markdown pages.
!> Only the first `<script>` tag in a markdown file is executed. If you wish to mount multiple Vue instances using a script tag, all instances must be mounted within the first script tag in your markdown.
```html
<!-- Vue 2.x -->
<script>
new Vue({
el: '#example',
// Options...
});
</script>
```
#### Vue 3.x
```markdown
```html
<!-- Vue 3.x -->
<script>
Vue.createApp({
data: function() {
return {
counter: 0,
message: "Hello, World!"
};
},
methods: {
hello: function() {
alert(this.message);
}
}
}).mount("#example-1");
// Options...
}).mount('#example');
</script>
```
The HTML & JavaScript above will render the following:
## Technical Notes
<!-- prettier-ignore-start -->
<div id="example-1">
<p>{{ message }}</p>
<button v-on:click="hello">Say Hello</button>
<button v-on:click="counter -= 1">-</button>
{{ counter }}
<button v-on:click="counter += 1">+</button>
</div>
<!-- prettier-ignore-end -->
!> Only the first `<script>` tag in a markdown file is executed. If you are working with multiple Vue components, all `Vue` instances must be created within this tag.
<script>
new Vue({
el: "#example-1",
data: function() {
return {
counter: 0,
message: "Hello, World!"
};
},
methods: {
hello: function() {
alert(this.message);
}
}
});
</script>
- Docsify processes Vue content in the following order on each page load:
1. Execute markdown `<script>`
1. Register global `vueComponents`
1. Mount `vueMounts`
1. Auto-mount unmounted `vueComponents`
1. Auto-mount unmounted Vue template syntax using `vueGlobalOptions`
- When auto-mounting Vue content, docsify will mount each top-level element in your markdown that contains template syntax or a component. For example, in the following HTML the top-level `<p>`, `<my-component />`, and `<div>` elements will be mounted.
```html
<p>{{ foo }}</p>
<my-component />
<div>
<span>{{ bar }}</span>
<some-other-component />
</div>
```
- Docsify will not mount an existing Vue instance or an element that contains an existing Vue instance.
- Docsify will automatically destroy/unmount all Vue instances it creates before each page load.

View File

@ -1,13 +1,9 @@
const path = require('path');
const { globals: serverGlobals } = require('./test/config/server.js');
const { TEST_HOST } = require('./test/config/server.js');
const sharedConfig = {
errorOnDeprecated: true,
globals: {
...serverGlobals, // BLANK_URL, DOCS_URL, LIB_URL, NODE_MODULES_URL, TEST_HOST
DOCS_PATH: path.resolve(__dirname, 'docs'),
LIB_PATH: path.resolve(__dirname, 'lib'),
SRC_PATH: path.resolve(__dirname, 'src'),
TEST_HOST,
},
globalSetup: './test/config/jest.setup.js',
globalTeardown: './test/config/jest.teardown.js',
@ -27,7 +23,7 @@ module.exports = {
displayName: 'unit',
setupFilesAfterEnv: ['<rootDir>/test/config/jest.setup-tests.js'],
testMatch: ['<rootDir>/test/unit/*.test.js'],
testURL: serverGlobals.BLANK_URL,
testURL: `${TEST_HOST}/_blank.html`,
},
// Integration Tests (Jest)
{
@ -35,7 +31,7 @@ module.exports = {
displayName: 'integration',
setupFilesAfterEnv: ['<rootDir>/test/config/jest.setup-tests.js'],
testMatch: ['<rootDir>/test/integration/*.test.js'],
testURL: serverGlobals.BLANK_URL,
testURL: `${TEST_HOST}/_blank.html`,
},
// E2E Tests (Jest + Playwright)
{

View File

@ -27,7 +27,7 @@
"dev:ssr": "run-p serve:ssr watch:*",
"lint": "eslint .",
"fixlint": "eslint . --fix",
"test": "jest --runInBand",
"test": "jest",
"test:e2e": "jest --selectProjects e2e",
"test:integration": "jest --selectProjects integration",
"test:unit": "jest --selectProjects unit",

View File

@ -13,6 +13,8 @@ import { Compiler } from './compiler';
import * as tpl from './tpl';
import { prerenderEmbed } from './embed';
let vueGlobalData;
function executeScript() {
const script = dom
.findAll('.markdown-section>script')
@ -41,58 +43,179 @@ function formatUpdated(html, updated, fn) {
}
function renderMain(html) {
const docsifyConfig = this.config;
const markdownElm = dom.find('.markdown-section');
const vueVersion =
'Vue' in window &&
window.Vue.version &&
Number(window.Vue.version.charAt(0));
const isMountedVue = elm => {
const isVue2 = Boolean(elm.__vue__ && elm.__vue__._isVue);
const isVue3 = Boolean(elm._vnode && elm._vnode.__v_skip);
return isVue2 || isVue3;
};
if (!html) {
html = '<h1>404 - Not found</h1>';
}
this._renderTo('.markdown-section', html);
if ('Vue' in window) {
const mountedElms = dom
.findAll('.markdown-section > *')
.filter(elm => isMountedVue(elm));
// Destroy/unmount existing Vue instances
for (const mountedElm of mountedElms) {
if (vueVersion === 2) {
mountedElm.__vue__.$destroy();
} else if (vueVersion === 3) {
mountedElm.__vue_app__.unmount();
}
}
}
this._renderTo(markdownElm, html);
// Render sidebar with the TOC
!this.config.loadSidebar && this._renderSidebar();
!docsifyConfig.loadSidebar && this._renderSidebar();
// Execute markdown <script>
if (
this.config.executeScript ||
('Vue' in window && this.config.executeScript !== false)
docsifyConfig.executeScript ||
('Vue' in window && docsifyConfig.executeScript !== false)
) {
executeScript();
}
// Handle Vue content not handled by markdown <script>
// Handle Vue content not mounted by markdown <script>
if ('Vue' in window) {
const mainElm = document.querySelector('#main') || {};
const childElms = mainElm.children || [];
const vueVersion =
window.Vue.version && Number(window.Vue.version.charAt(0));
const vueMountData = [];
const vueComponentNames = Object.keys(docsifyConfig.vueComponents || {});
for (let i = 0, len = childElms.length; i < len; i++) {
const elm = childElms[i];
const isValid = elm.tagName !== 'SCRIPT';
// Register global vueComponents
if (vueVersion === 2 && vueComponentNames.length) {
vueComponentNames.forEach(name => {
const isNotRegistered = !window.Vue.options.components[name];
if (!isValid) {
continue;
}
// Vue 3
if (vueVersion === 3) {
const isAlreadyVue = Boolean(elm._vnode && elm._vnode.__v_skip);
if (!isAlreadyVue) {
const app = window.Vue.createApp({});
app.mount(elm);
if (isNotRegistered) {
window.Vue.component(name, docsifyConfig.vueComponents[name]);
}
}
// Vue 2
else if (vueVersion === 2) {
const isAlreadyVue = Boolean(elm.__vue__ && elm.__vue__._isVue);
});
}
if (!isAlreadyVue) {
new window.Vue({
mounted: function() {
this.$destroy();
},
}).$mount(elm);
// Store global data() return value as shared data object
if (
!vueGlobalData &&
docsifyConfig.vueGlobalOptions &&
typeof docsifyConfig.vueGlobalOptions.data === 'function'
) {
vueGlobalData = docsifyConfig.vueGlobalOptions.data();
}
// vueMounts
vueMountData.push(
...Object.entries(docsifyConfig.vueMounts || {})
.map(([cssSelector, vueConfig]) => [
dom.find(markdownElm, cssSelector),
vueConfig,
])
.filter(([elm, vueConfig]) => elm)
);
// Template syntax, vueComponents, vueGlobalOptions
if (docsifyConfig.vueGlobalOptions || vueComponentNames.length) {
const reHasBraces = /{{2}[^{}]*}{2}/;
// Matches Vue full and shorthand syntax as attributes in HTML tags.
//
// Full syntax examples:
// v-foo, v-foo[bar], v-foo-bar, v-foo:bar-baz.prop
//
// Shorthand syntax examples:
// @foo, @foo.bar, @foo.bar.baz, @[foo], :foo, :[foo]
//
// Markup examples:
// <div v-html>{{ html }}</div>
// <div v-text="msg"></div>
// <div v-bind:text-content.prop="text">
// <button v-on:click="doThis"></button>
// <button v-on:click.once="doThis"></button>
// <button v-on:[event]="doThis"></button>
// <button @click.stop.prevent="doThis">
// <a :href="url">
// <a :[key]="url">
const reHasDirective = /<[^>/]+\s([@:]|v-)[\w-:.[\]]+[=>\s]/;
vueMountData.push(
...dom
.findAll('.markdown-section > *')
// Remove duplicates
.filter(elm => !vueMountData.some(([e, c]) => e === elm))
// Detect Vue content
.filter(elm => {
const isVueMount =
// is a component
elm.tagName.toLowerCase() in
(docsifyConfig.vueComponents || {}) ||
// has a component(s)
elm.querySelector(vueComponentNames.join(',') || null) ||
// has curly braces
reHasBraces.test(elm.outerHTML) ||
// has content directive
reHasDirective.test(elm.outerHTML);
return isVueMount;
})
.map(elm => {
// Clone global configuration
const vueConfig = Object.assign(
{},
docsifyConfig.vueGlobalOptions || {}
);
// Replace vueGlobalOptions data() return value with shared data object.
// This provides a global store for all Vue instances that receive
// vueGlobalOptions as their configuration.
if (vueGlobalData) {
vueConfig.data = function() {
return vueGlobalData;
};
}
return [elm, vueConfig];
})
);
}
// Mount
for (const [mountElm, vueConfig] of vueMountData) {
const isVueAttr = 'data-isvue';
const isSkipElm =
// Is an invalid tag
mountElm.matches('pre, script') ||
// Is a mounted instance
isMountedVue(mountElm) ||
// Has mounted instance(s)
mountElm.querySelector(`[${isVueAttr}]`);
if (!isSkipElm) {
mountElm.setAttribute(isVueAttr, '');
if (vueVersion === 2) {
vueConfig.el = undefined;
new window.Vue(vueConfig).$mount(mountElm);
} else if (vueVersion === 3) {
const app = window.Vue.createApp(vueConfig);
// Register global vueComponents
vueComponentNames.forEach(name => {
const config = docsifyConfig.vueComponents[name];
app.component(name, config);
});
app.mount(mountElm);
}
}
}

View File

@ -103,28 +103,85 @@ body
.markdown-section em
color #7f8c8d
.markdown-section code
.markdown-section code,
.markdown-section pre,
.markdown-section output::after
font-family 'Roboto Mono', Monaco, courier, monospace
.markdown-section code,
.markdown-section pre
background-color #f8f8f8
.markdown-section pre,
.markdown-section output
margin 1.2em 0
position relative
.markdown-section pre > code,
.markdown-section output
border-radius 2px
display block
.markdown-section pre > code,
.markdown-section output::after
-moz-osx-font-smoothing initial
-webkit-font-smoothing initial
.markdown-section code
border-radius 2px
color #e96900
font-family 'Roboto Mono', Monaco, courier, monospace
font-size 0.8rem
margin 0 2px
padding 3px 5px
white-space pre-wrap
.markdown-section pre
-moz-osx-font-smoothing initial
-webkit-font-smoothing initial
background-color #f8f8f8
font-family 'Roboto Mono', Monaco, courier, monospace
line-height 1.5rem
margin 1.2em 0
overflow auto
padding 0 1.4rem
position relative
line-height 1.5rem
overflow auto
word-wrap normal
.markdown-section pre > code
color #525252
font-size 0.8rem
padding 2.2em 5px
line-height inherit
margin 0 2px
max-width inherit
overflow inherit
white-space inherit
.markdown-section output
padding: 1.7rem 1.4rem
border 1px dotted #ccc
.markdown-section output > :first-child
margin-top: 0;
.markdown-section output > :last-child
margin-bottom: 0;
.markdown-section code::after, .markdown-section code::before,
.markdown-section output::after, .markdown-section output::before
letter-spacing 0.05rem
.markdown-section pre::after,
.markdown-section output::after
color #ccc
font-size 0.6rem
font-weight 600
height 15px
line-height 15px
padding 5px 10px 0
position absolute
right 0
text-align right
top 0
.markdown-section pre::after
.markdown-section output::after
content attr(data-lang)
/* code highlight */
.token.comment, .token.prolog, .token.doctype, .token.cdata
color #8e908c
@ -187,41 +244,9 @@ body
.token.entity
cursor help
.markdown-section pre > code
-moz-osx-font-smoothing initial
-webkit-font-smoothing initial
background-color #f8f8f8
border-radius 2px
color #525252
display block
font-family 'Roboto Mono', Monaco, courier, monospace
font-size 0.8rem
line-height inherit
margin 0 2px
max-width inherit
overflow inherit
padding 2.2em 5px
white-space inherit
.markdown-section code::after, .markdown-section code::before
letter-spacing 0.05rem
code .token
-moz-osx-font-smoothing initial
-webkit-font-smoothing initial
min-height 1.5rem
position: relative
left: auto
pre::after
color #ccc
content attr(data-lang)
font-size 0.6rem
font-weight 600
height 15px
line-height 15px
padding 5px 10px 0
position absolute
right 0
text-align right
top 0

View File

@ -5,8 +5,6 @@ module.exports = {
'jest/globals': true,
},
extends: ['plugin:jest/recommended', 'plugin:jest/style'],
globals: {
...jestConfig.globals,
},
globals: jestConfig.globals,
plugins: ['jest'],
};

View File

@ -14,18 +14,6 @@
## Global Variables
### File paths
- `DOCS_PATH`: File path to `/docs` directory
- `LIB_PATH`: File path to `/lib` directory
- `SRC_PATH`: File path to `/src` directory
### URLs
- `BLANK_URL`: Test server route to virtual `_blank.html` file
- `DOCS_URL`: Test server route to `/docs` directory
- `LIB_URL`: Test server route to `/lib` directory
- `NODE_MODULES_URL`: Test server route to `/node_modules` directory
- `TEST_HOST`: Test server ip:port
## CLI commands

View File

@ -50,5 +50,5 @@ beforeEach(async () => {
// will cause operations that require the window location to be a valid URL
// to fail (e.g. AJAX requests). To avoid these issues, this hook ensures
// that each tests begins by a blank HTML page.
await page.goto(BLANK_URL);
await page.goto(`${TEST_HOST}/_blank.html`);
});

View File

@ -1,42 +1,83 @@
import mock from 'xhr-mock';
const sideEffects = {
document: {
addEventListener: {
fn: document.addEventListener,
refs: [],
},
keys: Object.keys(document),
},
window: {
addEventListener: {
fn: window.addEventListener,
refs: [],
},
keys: Object.keys(window),
},
};
// Lifecycle Hooks
// -----------------------------------------------------------------------------
// Soft-reset jsdom. This clears the DOM and removes all attribute from the
// root element, however it does not undo changes made to jsdom globals like
// the window or document object. Tests requiring a full jsdom reset should be
// stored in separate files, as this is the only way (?) to do a complete
// reset of JSDOM with Jest.
beforeAll(async () => {
// Spy addEventListener
['document', 'window'].forEach(obj => {
const fn = sideEffects[obj].addEventListener.fn;
const refs = sideEffects[obj].addEventListener.refs;
function addEventListenerSpy(type, listener, options) {
// Store listener reference so it can be removed during reset
refs.push({ type, listener, options });
// Call original window.addEventListener
fn(type, listener, options);
}
// Add to default key array to prevent removal during reset
sideEffects[obj].keys.push('addEventListener');
// Replace addEventListener with mock
global[obj].addEventListener = addEventListenerSpy;
});
});
// Reset JSDOM. This attempts to remove side effects from tests, however it does
// not reset all changes made to globals like the window and document
// objects. Tests requiring a full JSDOM reset should be stored in separate
// files, which is only way to do a complete JSDOM reset with Jest.
beforeEach(async () => {
const rootElm = document.documentElement;
// Remove elements (faster the setting innerHTML)
// Remove attributes on root element
[...rootElm.attributes].forEach(attr => rootElm.removeAttribute(attr.name));
// Remove elements (faster than setting innerHTML)
while (rootElm.firstChild) {
rootElm.removeChild(rootElm.firstChild);
}
// Remove docsify side-effects
[
'__current_docsify_compiler__',
'_paq',
'$docsify',
'Docsify',
'DocsifyCompiler',
'ga',
'gaData',
'gaGlobal',
'gaplugins',
'gitter',
'google_tag_data',
'marked',
'Prism',
].forEach(prop => {
if (global[prop]) {
delete global[prop];
// Remove global listeners and keys
['document', 'window'].forEach(obj => {
const refs = sideEffects[obj].addEventListener.refs;
// Listeners
while (refs.length) {
const { type, listener, options } = refs.pop();
global[obj].removeEventListener(type, listener, options);
}
// Keys
Object.keys(global[obj])
.filter(key => !sideEffects[obj].keys.includes(key))
.forEach(key => {
delete global[obj][key];
});
});
// Remove attributes
[...rootElm.attributes].forEach(attr => rootElm.removeAttribute(attr.name));
// Restore base elements
rootElm.innerHTML = '<html><head></head><body></body></html>';
rootElm.innerHTML = '<head></head><body></body>';
});
afterEach(async () => {
// Restore the global XMLHttpRequest object to its original state
mock.teardown();
});

View File

@ -59,7 +59,6 @@ function startServer(options = {}, cb = Function.prototype) {
},
},
},
startPath: '/docs',
ui: false,
};
@ -96,6 +95,7 @@ if (hasStartArg) {
open: true,
port: serverConfig.port + 1,
directory: true,
startPath: '/docs',
});
}
// Display friendly message about manually starting a server instance
@ -104,22 +104,8 @@ else if (require.main === module) {
}
module.exports = {
globals: {
get BLANK_URL() {
return `${this.TEST_HOST}/_blank.html`;
},
get DOCS_URL() {
return `${this.TEST_HOST}/docs`;
},
get LIB_URL() {
return `${this.TEST_HOST}/lib`;
},
get NODE_MODULES_URL() {
return `${this.TEST_HOST}/node_modules`;
},
TEST_HOST: `http://${serverConfig.host}:${serverConfig.port}`,
},
start: startServer,
startAsync: startServerAsync,
stop: stopServer,
TEST_HOST: `http://${serverConfig.host}:${serverConfig.port}`,
};

View File

@ -92,14 +92,14 @@ describe(`Example Tests`, function() {
test('manual docsify site using playwright methods', async () => {
// Goto URL
// https://playwright.dev/#path=docs%2Fapi.md&q=pagegotourl-options
await page.goto(BLANK_URL);
await page.goto(`${TEST_HOST}/_blank.html`);
// Set docsify configuration
// https://playwright.dev/#path=docs%2Fapi.md&q=pageevaluatepagefunction-arg
await page.evaluate(() => {
window.$docsify = {
el: '#app',
basePath: '/docs',
basePath: '/docs/',
themeColor: 'red',
};
});
@ -112,11 +112,11 @@ describe(`Example Tests`, function() {
// Inject docsify theme (vue.css)
// https://playwright.dev/#path=docs%2Fapi.md&q=pageaddstyletagoptions
await page.addStyleTag({ url: `${LIB_URL}/themes/vue.css` });
await page.addStyleTag({ url: '/lib/themes/vue.css' });
// Inject docsify.js
// https://playwright.dev/#path=docs%2Fapi.md&q=pageaddscripttagoptions
await page.addScriptTag({ url: `${LIB_URL}/docsify.js` });
await page.addScriptTag({ url: '/lib/docsify.js' });
// Wait for docsify to initialize
// https://playwright.dev/#path=docs%2Fapi.md&q=pagewaitforselectorselector-options
@ -182,12 +182,12 @@ describe(`Example Tests`, function() {
`,
},
routes: {
'/test.md': `
'test.md': `
# Test Page
This is a custom route.
`,
'/data-test-scripturls.js': `
'data-test-scripturls.js': `
document.body.setAttribute('data-test-scripturls', 'pass');
`,
},
@ -196,7 +196,7 @@ describe(`Example Tests`, function() {
`,
scriptURLs: [
// docsifyInit() route
'/data-test-scripturls.js',
'data-test-scripturls.js',
// Server route
'/lib/plugins/search.min.js',
],

View File

@ -1,141 +1,206 @@
const stripIndent = require('common-tags/lib/stripIndent');
const docsifyInit = require('../helpers/docsify-init');
describe('Vue.js Rendering', function() {
const vueURLs = [
`${NODE_MODULES_URL}/vue2/dist/vue.js`,
`${NODE_MODULES_URL}/vue3/dist/vue.global.js`,
];
const vueURLs = [
'/node_modules/vue2/dist/vue.js',
'/node_modules/vue3/dist/vue.global.js',
];
// Tests
// ---------------------------------------------------------------------------
test(`ignores Vue content when window.Vue is not present`, async () => {
await docsifyInit({
markdown: {
homepage: `
<div id="test">test<span v-for="i in 5">{{ i }}</span></div>
`,
},
});
await page.evaluate(() => {
return 'Vue' in window === false;
});
await expect(page).toEqualText('#test', 'test{{ i }}');
});
describe('Basic rendering', function() {
for (const vueURL of vueURLs) {
const vueVersion = vueURL.match(/vue(\d+)/)[1]; // vue2|vue3
for (const executeScript of ['unspecified', true, false]) {
test(`handles Vue v${vueVersion}.x basic rendering when executeScript is ${executeScript}`, async () => {
const docsifyInitConfig = {
markdown: {
homepage: `
<div id="test">test<span v-for="i in 5">{{ i }}</span></div>
`,
describe('Vue.js Compatibility', function() {
function getSharedConfig() {
const config = {
config: {
vueComponents: {
'button-counter': {
template: `
<button @click="counter++">{{ counter }}</button>
`,
data: function() {
return {
counter: 0,
};
},
scriptURLs: vueURL,
};
if (executeScript !== 'unspecified') {
docsifyInitConfig.config = {
executeScript,
},
},
vueGlobalOptions: {
data: function() {
return {
counter: 0,
msg: 'vueglobaloptions',
};
}
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#test', 'test12345');
});
}
}
});
describe('Advanced usage', function() {
const testData = {
vue2: {
markdown: `
<div id="test">
<button v-on:click="counter += 1">+</button>
<span>{{ counter }}<span>
</div>
<script>
new Vue({
el: '#test',
data() {
return {
counter: 0
}
},
});
</script>
`,
},
},
vueMounts: {
'#vuemounts': {
data: function() {
return {
counter: 0,
msg: 'vuemounts',
};
},
},
},
},
vue3: {
markdown: `
<div id="test">
<button v-on:click="counter += 1">+</button>
markdown: {
homepage: stripIndent`
<div id="vuefor"><span v-for="i in 5">{{ i }}</span></div>
<button-counter id="vuecomponent">---</button-counter>
<div id="vueglobaloptions">
<p v-text="msg">---</p>
<button @click="counter += 1">+</button>
<span>{{ counter }}<span>
</div>
<div id="vuemounts">
<p v-text="msg">---</p>
<button @click="counter += 1">+</button>
<span>{{ counter }}<span>
</div>
<div id="vuescript">
<p v-text="msg">---</p>
<button @click="counter += 1">+</button>
<span>{{ counter }}<span>
</div>
<script>
Vue.createApp({
const vueConfig = {
data() {
return {
counter: 0
counter: 0,
msg: 'vuescript'
}
},
}).mount('#test');
}
const vueMountElm = '#vuescript';
const vueVersion = Number(window.Vue.version.charAt(0));
if (vueVersion === 2) {
new Vue(vueConfig).$mount(vueMountElm);
}
else if (vueVersion === 3) {
Vue.createApp(vueConfig).mount(vueMountElm);
}
</script>
`,
},
};
for (const vueURL of vueURLs) {
const vueVersion = vueURL.match(/vue(\d+)/)[1]; // vue2|vue3
const vueData = testData[`vue${vueVersion}`];
return config;
}
for (const executeScript of ['unspecified', true]) {
test(`handles Vue v${vueVersion}.x advanced usage when executeScript is ${executeScript}`, async () => {
const docsifyInitConfig = {
markdown: {
homepage: vueData.markdown,
},
scriptURLs: vueURL,
};
// Tests
// ---------------------------------------------------------------------------
for (const vueURL of vueURLs) {
const vueVersion = Number(vueURL.match(/vue(\d+)/)[1]); // 2|3
if (executeScript !== 'unspecified') {
docsifyInitConfig.config = {
executeScript,
};
}
describe(`Vue v${vueVersion}`, function() {
for (const executeScript of [true, undefined]) {
test(`renders content when executeScript is ${executeScript}`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.executeScript = executeScript;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#test span', '0');
await page.click('#test button');
await expect(page).toEqualText('#test span', '1');
// Static
await expect(page).toEqualText('#vuefor', '12345');
await expect(page).toEqualText('#vuecomponent', '0');
await expect(page).toEqualText(
'#vueglobaloptions p',
'vueglobaloptions'
);
await expect(page).toEqualText('#vueglobaloptions span', '0');
await expect(page).toEqualText('#vuemounts p', 'vuemounts');
await expect(page).toEqualText('#vuemounts span', '0');
await expect(page).toEqualText('#vuescript p', 'vuescript');
await expect(page).toEqualText('#vuescript span', '0');
// Reactive
await page.click('#vuecomponent');
await expect(page).toEqualText('#vuecomponent', '1');
await page.click('#vueglobaloptions button');
await expect(page).toEqualText('#vueglobaloptions span', '1');
await page.click('#vuemounts button');
await expect(page).toEqualText('#vuemounts span', '1');
await page.click('#vuescript button');
await expect(page).toEqualText('#vuescript span', '1');
});
}
test(`handles Vue v${vueVersion}.x advanced usage when executeScript is false`, async () => {
const docsifyInitConfig = {
config: {
executeScript: false,
},
markdown: {
homepage: vueData.markdown,
},
scriptURLs: vueURL,
};
test(`ignores content when Vue is not present`, async () => {
const docsifyInitConfig = getSharedConfig();
await docsifyInit(docsifyInitConfig);
const textContent = await page.textContent('#test span');
expect(textContent).toBe('');
await page.evaluate(() => {
return 'Vue' in window === false;
});
await expect(page).toEqualText('#vuefor', '{{ i }}');
await expect(page).toEqualText('#vuecomponent', '---');
await expect(page).toEqualText('#vueglobaloptions p', '---');
await expect(page).toEqualText('#vuemounts p', '---');
await expect(page).toEqualText('#vuescript p', '---');
});
}
});
test(`ignores content when vueComponents, vueMounts, and vueGlobalOptions are undefined`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.vueComponents = undefined;
docsifyInitConfig.config.vueGlobalOptions = undefined;
docsifyInitConfig.config.vueMounts = undefined;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuefor', '{{ i }}');
await expect(page).toEqualText('#vuecomponent', '---');
await expect(page).toEqualText('#vueglobaloptions p', '---');
await expect(page).toEqualText('#vuemounts p', '---');
await expect(page).toEqualText('#vuescript p', 'vuescript');
});
test(`ignores content when vueGlobalOptions is undefined`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.vueGlobalOptions = undefined;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuefor', '12345');
await expect(page).toEqualText('#vuecomponent', '0');
expect(await page.innerText('#vueglobaloptions p')).toBe('');
await expect(page).toEqualText('#vuemounts p', 'vuemounts');
await expect(page).toEqualText('#vuescript p', 'vuescript');
});
test(`ignores content when vueMounts is undefined`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.vueMounts['#vuemounts'] = undefined;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuefor', '12345');
await expect(page).toEqualText('#vuecomponent', '0');
await expect(page).toEqualText(
'#vueglobaloptions p',
'vueglobaloptions'
);
await expect(page).toEqualText('#vuemounts p', 'vueglobaloptions');
await expect(page).toEqualText('#vuescript p', 'vuescript');
});
test(`ignores <script> when executeScript is false`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.executeScript = false;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuescript p', 'vueglobaloptions');
});
});
}
});

View File

@ -6,8 +6,8 @@ const axios = require('axios');
const prettier = require('prettier');
const stripIndent = require('common-tags/lib/stripIndent');
const docsifyPATH = `${LIB_PATH}/docsify.js`; // JSDOM
const docsifyURL = `${LIB_URL}/docsify.js`; // Playwright
const docsifyPATH = '../../lib/docsify.js'; // JSDOM
const docsifyURL = '/lib/docsify.js'; // Playwright
const isJSDOM = 'window' in global;
const isPlaywright = 'page' in global;
@ -30,8 +30,9 @@ const isPlaywright = 'page' in global;
* @param {String} [options.testURL] URL to set as window.location.href
* @param {String} [options.waitForSelector='#main'] Element to wait for before returning promsie
* @param {String} [options._debug] initiate debugger after site is created
* @param {Boolean|Object} [options._logHTML] Logs HTML to console after initialization
* @param {Boolean|Object|String} [options._logHTML] Logs HTML to console after initialization. Accepts CSS selector.
* @param {Boolean} [options._logHTML.format=true] Formats HTML output
* @param {String} [options._logHTML.selector='html'] CSS selector(s) to match and log HTML for
* @returns {Promise}
*/
async function docsifyInit(options = {}) {
@ -63,7 +64,7 @@ async function docsifyInit(options = {}) {
style: '',
styleURLs: [],
testURL: `${TEST_HOST}/docsify-init.html`,
waitForSelector: '#main',
waitForSelector: '#main > *',
};
const settings = {
...defaults,
@ -77,15 +78,29 @@ async function docsifyInit(options = {}) {
loadSidebar: Boolean(settings.markdown.sidebar),
};
const updateBasePath = config => {
if (config.basePath) {
config.basePath = new URL(config.basePath, TEST_HOST).href;
}
};
// Config as function
if (typeof options.config === 'function') {
return function(vm) {
return { ...sharedConfig, ...options.config(vm) };
const config = { ...sharedConfig, ...options.config(vm) };
updateBasePath(config);
return config;
};
}
// Config as object
else {
return { ...sharedConfig, ...options.config };
const config = { ...sharedConfig, ...options.config };
updateBasePath(config);
return config;
}
},
get markdown() {
@ -101,10 +116,10 @@ async function docsifyInit(options = {}) {
get routes() {
const helperRoutes = {
[settings.testURL]: stripIndent`${settings.html}`,
['/README.md']: settings.markdown.homepage,
['/_coverpage.md']: settings.markdown.coverpage,
['/_navbar.md']: settings.markdown.navbar,
['/_sidebar.md']: settings.markdown.sidebar,
['README.md']: settings.markdown.homepage,
['_coverpage.md']: settings.markdown.coverpage,
['_navbar.md']: settings.markdown.navbar,
['_sidebar.md']: settings.markdown.sidebar,
};
const finalRoutes = Object.fromEntries(
@ -116,7 +131,7 @@ async function docsifyInit(options = {}) {
.filter(([url, responseText]) => url && responseText)
.map(([url, responseText]) => [
// Convert relative to absolute URL
new URL(url, TEST_HOST).href,
new URL(url, settings.config.basePath || TEST_HOST).href,
// Strip indentation from responseText
stripIndent`${responseText}`,
])
@ -197,9 +212,21 @@ async function docsifyInit(options = {}) {
if (isJSDOM) {
window.$docsify = settings.config;
} else if (isPlaywright) {
// Convert config functions to strings
const configString = JSON.stringify(settings.config, (key, val) =>
typeof val === 'function' ? `__FN__${val.toString()}` : val
);
await page.evaluate(config => {
window.$docsify = config;
}, settings.config);
// Restore config functions from strings
const configObj = JSON.parse(config, (key, val) =>
/^__FN__/.test(val)
? new Function(`return ${val.split('__FN__')[1]}`)()
: val
);
window.$docsify = configObj;
}, configString);
}
// Style URLs
@ -285,16 +312,42 @@ async function docsifyInit(options = {}) {
// Log HTML to console
if (settings._logHTML) {
const html = isJSDOM
? document.documentElement.innerHTML
: await page.content();
const output =
settings._logHTML.format === false
? html
: prettier.format(html, { parser: 'html' });
const selector =
typeof settings._logHTML === 'string'
? settings._logHTML
: settings._logHTML.selector;
// eslint-disable-next-line no-console
console.log(output);
let htmlArr = [];
if (selector) {
if (isJSDOM) {
htmlArr = [...document.querySelectorAll(selector)].map(
elm => elm.outerHTML
);
} else {
htmlArr = await page.$$eval(selector, elms =>
elms.map(e => e.outerHTML)
);
}
} else {
htmlArr = [
isJSDOM ? document.documentElement.outerHTML : await page.content(),
];
}
if (htmlArr.length) {
htmlArr.forEach(html => {
if (settings._logHTML.format !== false) {
html = prettier.format(html, { parser: 'html' });
}
// eslint-disable-next-line no-console
console.log(html);
});
} else {
// eslint-disable-next-line no-console
console.warn(`docsify-init(): unable to match selector '${selector}'`);
}
}
// Debug

View File

@ -37,9 +37,11 @@ export function waitForFunction(fn, arg, options = {}) {
timeElapsed += settings.delay;
if (timeElapsed >= settings.timeout) {
reject(
`waitForFunction() did not return a truthy value\n\n${fn.toString()}\n\n`
);
const msg = `waitForFunction did not return a truthy value (${
settings.timeout
} ms): ${fn.toString()}\n`;
reject(msg);
}
}, settings.delay);
});
@ -71,9 +73,9 @@ export function waitForSelector(cssSelector, options = {}) {
timeElapsed += settings.delay;
if (timeElapsed >= settings.timeout) {
reject(
`waitForSelector() was unable to find CSS selector ${cssSelector}`
);
const msg = `waitForSelector did not match CSS selector '${cssSelector}' (${settings.timeout} ms)`;
reject(msg);
}
}, settings.delay);
});
@ -94,20 +96,31 @@ export function waitForText(cssSelector, text, options = {}) {
};
return new Promise((resolve, reject) => {
let timeElapsed = 0;
waitForSelector(cssSelector, settings)
.then(elm => {
const isMatch = elm.textContent.includes(text);
const int = setInterval(() => {
const isMatch = elm.textContent.includes(text);
if (isMatch) {
resolve(elm);
} else {
reject(
`waitForText() did not find "${text}" in CSS selector ${cssSelector}`
);
}
if (isMatch) {
clearInterval(int);
resolve(true);
}
timeElapsed += settings.delay;
if (timeElapsed >= settings.timeout) {
const msg = `waitForText did not find '${text}' in CSS selector '${cssSelector}' (${settings.timeout} ms): '${elm.textContent}'`;
reject(msg);
}
}, settings.delay);
})
.catch(() => {
reject(`waitForText() was unable to find CSS selector ${cssSelector}`);
const msg = `waitForText did not match CSS selector '${cssSelector}' (${settings.timeout} ms)`;
reject(msg);
});
});
}

View File

@ -1,5 +1,3 @@
const fs = require('fs').promises;
const path = require('path');
const docsifyInit = require('../helpers/docsify-init');
// Suite
@ -13,14 +11,13 @@ describe('Docs Site', function() {
const mathSpy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
await docsifyInit({
config: {
coverpage: 'docs/_coverpage.md',
},
markdown: {
homepage: '# Hello World',
coverpage: await fs.readFile(
path.resolve(DOCS_PATH, '_coverpage.md'),
'utf8'
),
},
// _logHTML: true,
waitForSelector: '.cover-main > *',
});
const coverpageElm = document.querySelector('section.cover');
@ -33,14 +30,13 @@ describe('Docs Site', function() {
test('sidebar renders and is unchanged', async () => {
await docsifyInit({
config: {
loadSidebar: 'docs/_sidebar.md',
},
markdown: {
homepage: '# Hello World',
sidebar: await fs.readFile(
path.resolve(DOCS_PATH, '_sidebar.md'),
'utf8'
),
},
// _logHTML: true,
waitForSelector: '.sidebar-nav > ul',
});
const sidebarElm = document.querySelector('.sidebar');
@ -52,14 +48,13 @@ describe('Docs Site', function() {
test('navbar renders and is unchanged', async () => {
await docsifyInit({
config: {
loadNavbar: 'docs/_navbar.md',
},
markdown: {
homepage: '# Hello World',
navbar: await fs.readFile(
path.resolve(DOCS_PATH, '_navbar.md'),
'utf8'
),
},
// _logHTML: true,
waitForSelector: '.app-nav > ul',
});
const navbarElm = document.querySelector('nav.app-nav');

View File

@ -10,7 +10,7 @@ describe('Example Tests', function() {
test('Docsify /docs/ site using docsifyInit()', async () => {
await docsifyInit({
config: {
basePath: `${TEST_HOST}/docs`,
basePath: '/docs/',
},
// _logHTML: true,
});
@ -51,12 +51,12 @@ describe('Example Tests', function() {
`,
},
routes: {
'/test.md': `
'test.md': `
# Test Page
This is a custom route.
`,
'/data-test-scripturls.js': `
'data-test-scripturls.js': `
document.body.setAttribute('data-test-scripturls', 'pass');
`,
},
@ -65,7 +65,7 @@ describe('Example Tests', function() {
`,
scriptURLs: [
// docsifyInit() route
'/data-test-scripturls.js',
'data-test-scripturls.js',
// Server route
'/lib/plugins/search.min.js',
],

View File

@ -1,5 +1,6 @@
const { removeAtag } = require(`${SRC_PATH}/core/render/utils`);
const { tree } = require(`${SRC_PATH}/core/render/tpl`);
const { removeAtag } = require('../../src/core/render/utils');
const { tree } = require(`../../src/core/render/tpl`);
// Suite
// -----------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
const { History } = require(`${SRC_PATH}/core/router/history/base`);
const { History } = require('../../src/core/router/history/base');
class MockHistory extends History {
parse(path) {

View File

@ -1,4 +1,4 @@
const { resolvePath } = require(`${SRC_PATH}/core/util`);
const { resolvePath } = require('../../src/core/util');
// Suite
// -----------------------------------------------------------------------------