Add Vue 3 compatibility

- Update docs
- Update tests
- Add Vue 2 to docs site
- Add Vue 2 & 3 dependencies for tests
This commit is contained in:
John Hildenbiddle 2020-10-07 13:13:03 -05:00
parent 9b794f5bd1
commit b135f8417d
8 changed files with 320 additions and 113 deletions

View File

@ -162,5 +162,6 @@
}
})();
</script>
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
</body>
</html>

View File

@ -1,24 +1,34 @@
# Vue compatibility
Docsify allows [Vue.js](https://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 [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.
To get started, load either the production (minified) or development (unminified) version of Vue in your `index.html`:
To get started, load either the production or development version of Vue in your `index.html`:
#### Vue 2.x
```html
<!-- Production (minified) -->
<!-- Production -->
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
<!-- Development (unminified, with debugging info via console) -->
<!-- Development (debugging and Vue.js devtools support) -->
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
```
#### Vue 3.x
```html
<!-- Production -->
<script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<!-- Development (debugging and Vue.js devtools support) -->
<script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
```
## Basic rendering
Docsify will automatically render basic Vue content that does not require `data`, `methods`, or other instance features.
```markdown
<button v-on:click.native="this.alert('Hello, World!')">Say Hello</button>
<ul>
<li v-for="i in 3">{{ i }}</li>
</ul>
@ -26,8 +36,6 @@ Docsify will automatically render basic Vue content that does not require `data`
The HTML above will render the following:
<button v-on:click="this.alert('Hello, World!')">Say Hello</button>
<ul>
<li v-for="i in 3">{{ i }}</li>
</ul>
@ -36,6 +44,8 @@ The HTML above will render the following:
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.
<!-- prettier-ignore-start -->
```markdown
<div id="example-1">
<p>{{ message }}</p>
@ -48,13 +58,19 @@ Vue components and templates that require `data`, `methods`, computed properties
</div>
```
<!-- prettier-ignore-end -->
#### Vue 2.x
```markdown
<script>
new Vue({
el: "#example-1",
data: function() {
counter: 0,
message: "Hello, World!"
return {
counter: 0,
message: "Hello, World!"
};
},
methods: {
hello: function() {
@ -65,8 +81,30 @@ Vue components and templates that require `data`, `methods`, computed properties
</script>
```
#### Vue 3.x
```markdown
<script>
Vue.createApp({
data: function() {
return {
counter: 0,
message: "Hello, World!"
};
},
methods: {
hello: function() {
alert(this.message);
}
}
}).mount("#example-1");
</script>
```
The HTML & JavaScript above will render the following:
<!-- prettier-ignore-start -->
<div id="example-1">
<p>{{ message }}</p>
@ -77,64 +115,18 @@ The HTML & JavaScript above will render the following:
<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.
## Vuep playgrounds
[Vuep](https://github.com/QingWei-Li/vuep) is a Vue component that provides a live editor and preview for Vue content. See the [vuep documentation](https://qingwei-li.github.io/vuep/) for details.
Add Vuep CSS and JavaScript to your `index.html`:
```html
<!-- Vuep CSS -->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/vuep/dist/vuep.css">
<!-- Vuep JavaScript -->
<script src="//cdn.jsdelivr.net/npm/vuep/dist/vuep.min.js"></script>
```
Add vuep markup to a markdown file (e.g. `README.md`):
```markdown
<vuep template="#example-2"></vuep>
<script v-pre type="text/x-template" id="example-2">
<template>
<div>Hello, {{ name }}!</div>
</template>
<script>
module.exports = {
data: function() {
return { name: 'Vue' }
}
}
</script>
</script>
```
<vuep template="#example-2"></vuep>
<script v-pre type="text/x-template" id="example-2">
<template>
<div>Hello, {{ name }}!</div>
</template>
<script>
module.exports = {
data: function() {
return { name: 'World' }
}
}
</script>
</script>
<script>
new Vue({
el: "#example-1",
data: {
counter: 0,
message: "Hello, World!"
data: function() {
return {
counter: 0,
message: "Hello, World!"
};
},
methods: {
hello: function() {

View File

@ -4,7 +4,7 @@ const { globals: serverGlobals } = require('./test/config/server.js');
const sharedConfig = {
errorOnDeprecated: true,
globals: {
...serverGlobals, // BLANK_URL, DOCS_URL, LIB_URL, TEST_HOST
...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'),

96
package-lock.json generated
View File

@ -4340,6 +4340,79 @@
}
}
},
"@vue/compiler-core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.0.tgz",
"integrity": "sha512-XqPC7vdv4rFE77S71oCHmT1K4Ks3WE2Gi6Lr4B5wn0Idmp+NyQQBUHsCNieMDRiEpgtJrw+yOHslrsV0AfAsfQ==",
"dev": true,
"requires": {
"@babel/parser": "^7.11.5",
"@babel/types": "^7.11.5",
"@vue/shared": "3.0.0",
"estree-walker": "^2.0.1",
"source-map": "^0.6.1"
},
"dependencies": {
"estree-walker": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz",
"integrity": "sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"@vue/compiler-dom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.0.tgz",
"integrity": "sha512-ukDEGOP8P7lCPyStuM3F2iD5w2QPgUu2xwCW2XNeqPjFKIlR2xMsWjy4raI/cLjN6W16GtlMFaZdK8tLj5PRog==",
"dev": true,
"requires": {
"@vue/compiler-core": "3.0.0",
"@vue/shared": "3.0.0"
}
},
"@vue/reactivity": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.0.tgz",
"integrity": "sha512-mEGkztGQrAPZRhV7C6PorrpT3+NtuA4dY2QjMzzrW31noKhssWTajRZTwpLF39NBRrF5UU6cp9+1I0FfavMgEQ==",
"dev": true,
"requires": {
"@vue/shared": "3.0.0"
}
},
"@vue/runtime-core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.0.tgz",
"integrity": "sha512-3ABMLeA0ZbeVNLbGGLXr+pNUwqXILOqz8WCVGfDWwQb+jW114Cm8djOHVVDoqdvRETQvDf8yHSUmpKHZpQuTkA==",
"dev": true,
"requires": {
"@vue/reactivity": "3.0.0",
"@vue/shared": "3.0.0"
}
},
"@vue/runtime-dom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.0.tgz",
"integrity": "sha512-f312n5w9gK6mVvkDSj6/Xnot1XjlKXzFBYybmoy6ahAVC8ExbQ+LOWti1IZM/adU8VMNdKaw7Q53Hxz3y5jX8g==",
"dev": true,
"requires": {
"@vue/runtime-core": "3.0.0",
"@vue/shared": "3.0.0",
"csstype": "^2.6.8"
}
},
"@vue/shared": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.0.tgz",
"integrity": "sha512-4XWL/avABGxU2E2ZF1eZq3Tj7fvksCMssDZUHOykBIMmh5d+KcAnQMC5XHMhtnA0NAvktYsA2YpdsVwVmhWzvA==",
"dev": true
},
"@zkochan/cmd-shim": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz",
@ -6770,6 +6843,12 @@
}
}
},
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -19236,6 +19315,23 @@
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
"dev": true
},
"vue2": {
"version": "npm:vue@2.6.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==",
"dev": true
},
"vue3": {
"version": "npm:vue@3.0.0",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.0.0.tgz",
"integrity": "sha512-ZMrAARZ32sGIaYKr7Fk2GZEBh/VhulSrGxcGBiAvbN4fhjl3tuJyNFbbbLFqGjndbLoBW66I2ECq8ICdvkKdJw==",
"dev": true,
"requires": {
"@vue/compiler-dom": "3.0.0",
"@vue/runtime-dom": "3.0.0",
"@vue/shared": "3.0.0"
}
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",

View File

@ -103,6 +103,8 @@
"rollup-plugin-uglify": "^6.0.4",
"serve-handler": "^6.1.2",
"stylus": "^0.54.5",
"vue2": "npm:vue@^2.6.12",
"vue3": "npm:vue@^3.0.0",
"xhr-mock": "^2.5.1"
},
"keywords": [

View File

@ -62,18 +62,38 @@ function renderMain(html) {
if ('Vue' in window) {
const mainElm = document.querySelector('#main') || {};
const childElms = mainElm.children || [];
const vueVersion =
window.Vue.version && Number(window.Vue.version.charAt(0));
for (let i = 0, len = childElms.length; i < len; i++) {
const elm = childElms[i];
const isValid = elm.tagName !== 'SCRIPT';
const isVue = Boolean(elm.__vue__ && elm.__vue__._isVue);
if (isValid && !isVue) {
new window.Vue({
mounted: function() {
this.$destroy;
},
}).$mount(elm);
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);
}
}
// 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);
}
}
}
}

View File

@ -1,33 +1,34 @@
# Docsify Testing
### Environment
## Environment
- [Jest](https://jestjs.io): A test framework used for assertions, mocks, spies, etc.
- [Playwright](https://playwright.dev): A test automation tool for launching browsers and manipulating the DOM.
- [Jest-Playwright](https://github.com/playwright-community/jest-playwright): A Jest preset that simplifies using Jest and Playwright together
### Test files
## Test files
- E2E tests are located in `/test/e2e/` and use [Jest](https://jestjs.io) + [Playwright](https://playwright.dev).
- Integration tests are located in `/test/integration/` and use [Jest](https://jestjs.io).
- Unit tests located in `/test/unit/` and use [Jest](https://jestjs.io).
### Global Variables
## Global Variables
#### File paths
### File paths
- `DOCS_PATH`: File path to docsify's `/docs` directory
- `LIB_PATH`: File path to docsify's `/lib` directory
- `SRC_PATH`: File path to docsify's `/src` directory
- `DOCS_PATH`: File path to `/docs` directory
- `LIB_PATH`: File path to `/lib` directory
- `SRC_PATH`: File path to `/src` directory
#### URLs
### URLs
- `BLANK_URL`: Test server route to virtual `_blank.html` file
- `DOCS_URL`: Test server route to docsify's `/docs` directory
- `LIB_URL`: Test server route to docsify's `/lib` directory
- `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
## CLI commands
```bash
# Run all tests
@ -75,7 +76,7 @@ npm run test -- -u -i /path/to/file.test.js -t \"describe() or test() name\"
node ./test/config/server.js --start
```
### Resource
## Resource
- [UI Testing Best Practices](https://github.com/NoriSte/ui-testing-best-practices)
- [Using Jest with Playwright](https://playwright.tech/blog/using-jest-with-playwright)

View File

@ -1,46 +1,141 @@
const docsifyInit = require('../helpers/docsify-init');
describe(`Vue.js Rendering`, function() {
const sharedConfig = {
markdown: {
homepage: '<div id="test">test<span v-for="i in 5">{{ i }}</span></div>',
},
scriptURLs: ['https://unpkg.com/vue@2/dist/vue.js'],
};
describe('Vue.js Rendering', function() {
const vueURLs = [
`${NODE_MODULES_URL}/vue2/dist/vue.js`,
`${NODE_MODULES_URL}/vue3/dist/vue.global.js`,
];
// Tests
// ---------------------------------------------------------------------------
test('does render Vue content when executeScript is unspecified', async () => {
await docsifyInit(sharedConfig);
const testResult = await page.textContent('#test');
expect(testResult).toBe('test12345');
});
test('does render Vue content when executeScript:true', async () => {
test(`ignores Vue content when window.Vue is not present`, async () => {
await docsifyInit({
...sharedConfig,
config: {
executeScript: true,
markdown: {
homepage: `
<div id="test">test<span v-for="i in 5">{{ i }}</span></div>
`,
},
});
const testResult = await page.textContent('#test');
expect(testResult).toBe('test12345');
await page.evaluate(() => {
return 'Vue' in window === false;
});
await expect(page).toEqualText('#test', 'test{{ i }}');
});
test('does not render Vue content when executeScript:false', async () => {
await docsifyInit({
...sharedConfig,
config: {
executeScript: false,
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>
`,
},
scriptURLs: vueURL,
};
if (executeScript !== 'unspecified') {
docsifyInitConfig.config = {
executeScript,
};
}
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>
`,
},
});
vue3: {
markdown: `
<div id="test">
<button v-on:click="counter += 1">+</button>
<span>{{ counter }}<span>
</div>
const testResult = await page.textContent('#test');
<script>
Vue.createApp({
data() {
return {
counter: 0
}
},
}).mount('#test');
</script>
`,
},
};
expect(testResult).toBe('test{{ i }}');
for (const vueURL of vueURLs) {
const vueVersion = vueURL.match(/vue(\d+)/)[1]; // vue2|vue3
const vueData = testData[`vue${vueVersion}`];
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,
};
if (executeScript !== 'unspecified') {
docsifyInitConfig.config = {
executeScript,
};
}
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#test span', '0');
await page.click('#test button');
await expect(page).toEqualText('#test 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,
};
await docsifyInit(docsifyInitConfig);
const textContent = await page.textContent('#test span');
expect(textContent).toBe('');
});
}
});
});