diff --git a/.circleci/config.yml b/.circleci/config.yml
index cff98bd..4290be4 100755
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -76,7 +76,7 @@ jobs:
- attach-project
- run:
name: E2E SSR Tests
- command: yarn build && yarn test:e2e-ssr
+ command: yarn build && yarn test:e2e-ssr --coverage && yarn coverage
- persist_to_workspace:
root: ~/project
paths:
diff --git a/src/client/updaters/attribute.js b/src/client/updaters/attribute.js
index 19fa3b4..c4b14cb 100644
--- a/src/client/updaters/attribute.js
+++ b/src/client/updaters/attribute.js
@@ -43,8 +43,6 @@ export default function updateAttribute (appId, { attribute } = {}, type, attrs,
if (attrs[attr]) {
data[attr] = data[attr] || {}
data[attr][appId] = attrs[attr]
- } else {
- delete data[attr][appId]
}
}
}
diff --git a/src/index.js b/src/index.js
index 14c3393..16528d8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -28,6 +28,6 @@ function install (Vue, options = {}) {
export default {
version,
install,
- generate: process.server ? generate : () => showWarningNotSupportedInBrowserBundle('generate'),
+ generate: metaInfo => process.server ? generate(metaInfo) : showWarningNotSupportedInBrowserBundle('generate'),
hasMetaInfo
}
diff --git a/src/server/generateServerInjector.js b/src/server/generateServerInjector.js
index 43368ff..a00818d 100644
--- a/src/server/generateServerInjector.js
+++ b/src/server/generateServerInjector.js
@@ -58,13 +58,15 @@ export default function generateServerInjector (options, metaInfo) {
}
}
- for (const appId in serverInjector.extraData) {
- const data = serverInjector.extraData[appId][type]
- if (data) {
- for (const attr in data) {
- attributeData[attr] = {
- ...attributeData[attr],
- [appId]: data[attr]
+ if (serverInjector.extraData) {
+ for (const appId in serverInjector.extraData) {
+ const data = serverInjector.extraData[appId][type]
+ if (data) {
+ for (const attr in data) {
+ attributeData[attr] = {
+ ...attributeData[attr],
+ [appId]: data[attr]
+ }
}
}
}
diff --git a/src/shared/mixin.js b/src/shared/mixin.js
index cf75ea0..fd2ec17 100644
--- a/src/shared/mixin.js
+++ b/src/shared/mixin.js
@@ -155,6 +155,7 @@ export default function createMixin (Vue, options) {
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) {
+ /* istanbul ignore next line */
return
}
diff --git a/test/components/hello-world.vue b/test/components/hello-world.vue
index 9b3721c..88bc599 100644
--- a/test/components/hello-world.vue
+++ b/test/components/hello-world.vue
@@ -6,12 +6,7 @@
export default {
metaInfo() {
return {
- title: this.title
- }
- },
- data() {
- return {
- title: 'Hello World',
+ title: this.title,
htmlAttrs: {
lang: 'en'
},
@@ -19,6 +14,11 @@ export default {
{ charset: 'utf-8' }
]
}
+ },
+ data() {
+ return {
+ title: 'Hello World',
+ }
}
}
diff --git a/test/unit/components.test.js b/test/unit/components.test.js
index aa49fee..d155ba6 100644
--- a/test/unit/components.test.js
+++ b/test/unit/components.test.js
@@ -14,9 +14,9 @@ jest.mock('../../src/utils/window', () => ({
hasGlobalWindow: false
}))
-describe('client', () => {
+describe('components', () => {
let Vue
- let html
+ let elements
beforeAll(() => {
Vue = loadVueMetaPlugin()
@@ -25,15 +25,36 @@ describe('client', () => {
delete window.requestAnimationFrame
delete window.cancelAnimationFrame
- html = document.createElement('html')
+ elements = {
+ html: document.createElement('html'),
+ head: document.createElement('head'),
+ body: document.createElement('body')
+ }
+
+ elements.html.appendChild(elements.head)
+ elements.html.appendChild(elements.body)
+
document._getElementsByTagName = document.getElementsByTagName
jest.spyOn(document, 'getElementsByTagName').mockImplementation((tag) => {
- if (tag === 'html') {
- return [html]
+ if (elements[tag]) {
+ return [elements[tag]]
}
return document._getElementsByTagName(tag)
})
+ jest.spyOn(document, 'querySelectorAll').mockImplementation((query) => {
+ return elements.html.querySelectorAll(query)
+ })
+ })
+
+ afterEach(() => {
+ elements.html.getAttributeNames().forEach(name => elements.html.removeAttribute(name))
+ elements.head.childNodes.forEach(child => child.remove())
+ elements.head.getAttributeNames().forEach(name => elements.head.removeAttribute(name))
+ elements.body.childNodes.forEach(child => child.remove())
+ elements.body.getAttributeNames().forEach(name => elements.body.removeAttribute(name))
+
+ clearClientAttributeMap()
})
test('meta-info refreshed on component\'s data change', () => {
@@ -94,6 +115,22 @@ describe('client', () => {
expect(metaInfo.title).toEqual(undefined)
})
+ test('warns when component doesnt has metaInfo', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
+ const metaInfo = HelloWorld.metaInfo
+ delete HelloWorld.metaInfo
+
+ const wrapper = mount(HelloWorld, { localVue: Vue })
+ wrapper.vm.$meta().inject()
+
+ HelloWorld.metaInfo = metaInfo
+
+ expect(warn).toHaveBeenCalledTimes(1)
+ expect(warn).toHaveBeenCalledWith('This vue app/component has no vue-meta configuration')
+
+ warn.mockRestore()
+ })
+
test('meta-info can be rendered with inject', () => {
const wrapper = mount(HelloWorld, { localVue: Vue })
@@ -101,8 +138,54 @@ describe('client', () => {
expect(metaInfo.title.text()).toEqual('
Hello World')
})
+ test('inject also renders additional app info', () => {
+ HelloWorld.created = function () {
+ const { set } = this.$meta().addApp('inject-test-app')
+ set({
+ htmlAttrs: { lang: 'nl' },
+ meta: [{ name: 'description', content: 'test-description' }]
+ })
+ }
+
+ const wrapper = mount(HelloWorld, { localVue: Vue })
+
+ const metaInfo = wrapper.vm.$meta().inject()
+ expect(metaInfo.title.text()).toEqual('Hello World')
+
+ expect(metaInfo.htmlAttrs.text()).toEqual('lang="en nl" data-vue-meta="%7B%22lang%22:%7B%22ssr%22:%22en%22,%22inject-test-app%22:%22nl%22%7D%7D"')
+ expect(metaInfo.meta.text()).toEqual('')
+
+ delete HelloWorld.created
+ })
+
+ test('attributes with special meaning or functioning correct with inject', () => {
+ HelloWorld.created = function () {
+ const { set } = this.$meta().addApp('inject-test-app')
+ set({
+ meta: [{ skip: true, name: 'description', content: 'test-description' }],
+ script: [{
+ once: true,
+ callback: true,
+ async: false,
+ json: {
+ a: 1
+ }
+ }]
+ })
+ }
+
+ const wrapper = mount(HelloWorld, { localVue: Vue })
+
+ const metaInfo = wrapper.vm.$meta().inject()
+
+ expect(metaInfo.meta.text()).toEqual('')
+ expect(metaInfo.script.text()).toEqual('')
+
+ delete HelloWorld.created
+ })
+
test('doesnt update when ssr attribute is set', () => {
- html.setAttribute(defaultOptions.ssrAttribute, 'true')
+ elements.html.setAttribute(defaultOptions.ssrAttribute, 'true')
const el = document.createElement('div')
el.setAttribute('id', 'app')
@@ -216,6 +299,7 @@ describe('client', () => {
})
test('changes before hydration initialization trigger an update', async () => {
+ const { html } = elements
html.setAttribute(defaultOptions.ssrAttribute, 'true')
const el = document.createElement('div')
@@ -257,14 +341,11 @@ describe('client', () => {
jest.runAllTimers()
expect(html.getAttribute('theme')).toBe('dark')
- html.removeAttribute('theme')
-
wrapper.destroy()
})
test('changes during hydration initialization trigger an update', async () => {
- clearClientAttributeMap()
-
+ const { html } = elements
html.setAttribute(defaultOptions.ssrAttribute, 'true')
const el = document.createElement('div')
@@ -304,8 +385,122 @@ describe('client', () => {
jest.runAllTimers()
expect(html.getAttribute('theme')).toBe('dark')
- html.removeAttribute('theme')
+ wrapper.destroy()
+ })
+
+ test('can add/remove meta info from additional app ', () => {
+ const { html } = elements
+ let app
+
+ HelloWorld.created = function () {
+ // make sure that app's which set data but are removed before mounting
+ // are really removed
+ const { set, remove } = this.$meta().addApp('my-bogus-app')
+ set({
+ meta: [{ name: 'og:description', content: 'test-description' }]
+ })
+ remove()
+
+ app = this.$meta().addApp('my-test-app')
+ app.set({
+ htmlAttrs: { lang: 'nl' },
+ meta: [{ name: 'description', content: 'test-description' }],
+ script: [{ innerHTML: 'var test = true;' }]
+ })
+ }
+
+ const wrapper = mount(HelloWorld, {
+ localVue: Vue
+ })
+
+ wrapper.vm.$meta().refresh()
+
+ expect(html.getAttribute('lang')).toEqual('en nl')
+ expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
+ expect(Array.from(html.querySelectorAll('script')).length).toBe(1)
+ expect(Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length).toBe(2)
+
+ app.remove()
+
+ // add another app to make sure on client data is immediately added
+ const anotherApp = wrapper.vm.$meta().addApp('another-test-app')
+ anotherApp.set({
+ meta: [{ name: 'og:description', content: 'test-description' }]
+ })
+
+ expect(html.getAttribute('lang')).toEqual('en')
+ expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
+ expect(Array.from(html.querySelectorAll('script')).length).toBe(0)
+ expect(Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length).toBe(0)
+ expect(Array.from(html.querySelectorAll('[data-vue-meta="another-test-app"]')).length).toBe(1)
+
+ wrapper.destroy()
+ delete HelloWorld.created
+ })
+
+ test('retrieves ssr app config from attribute', () => {
+ const { html, body } = elements
+ html.setAttribute(defaultOptions.ssrAttribute, 'true')
+
+ body.setAttribute('foo', 'bar')
+ body.setAttribute('data-vue-meta', '%7B%22foo%22:%7B%22ssr%22:%22bar%22%7D%7D')
+
+ const el = document.createElement('div')
+ el.setAttribute('id', 'app')
+ el.setAttribute('data-server-rendered', true)
+ document.body.appendChild(el)
+
+ const Component = Vue.extend({
+ metaInfo: {
+ title: 'Test',
+ bodyAttrs: {
+ foo: 'bar'
+ }
+ },
+ render: h => h('div', null, 'Test')
+ })
+
+ const vm = new Component().$mount(el)
+
+ const wrapper = createWrapper(vm)
+
+ wrapper.vm.$meta().refresh()
+ expect(body.getAttribute('foo')).toBe('bar')
+ expect(body.getAttribute('data-vue-meta')).toBe('%7B%22foo%22:%7B%22ssr%22:%22bar%22%7D%7D')
+
+ wrapper.vm.$meta().refresh()
+ expect(body.getAttribute('foo')).toBe('bar')
+ expect(body.getAttribute('data-vue-meta')).toBeNull()
+
+ wrapper.vm.$meta().refresh()
+ expect(body.getAttribute('foo')).toBe('bar')
+ expect(body.getAttribute('data-vue-meta')).toBeNull()
wrapper.destroy()
})
+
+ test('can toggle refreshOnceOnNavigation runtime', () => {
+ const guards = {}
+ const wrapper = mount(HelloWorld, {
+ localVue: Vue,
+ mocks: {
+ $router: {
+ beforeEach (fn) {
+ guards.before = fn
+ },
+ afterEach (fn) {
+ guards.after = fn
+ }
+ }
+ }
+ })
+
+ expect(guards.before).toBeUndefined()
+ expect(guards.after).toBeUndefined()
+
+ wrapper.vm.$meta().setOptions({ refreshOnceOnNavigation: true })
+
+ expect(guards.before).not.toBeUndefined()
+ expect(guards.after).not.toBeUndefined()
+ })
})
diff --git a/test/unit/generators.test.js b/test/unit/generators.test.js
index cd4d4f9..84de215 100644
--- a/test/unit/generators.test.js
+++ b/test/unit/generators.test.js
@@ -61,7 +61,7 @@ describe('generators', () => {
}
})
-describe('extra tests', () => {
+describe.only('extra tests', () => {
test('empty config doesnt generate a tag', () => {
const { meta } = generateServerInjector({ meta: [] })
@@ -109,4 +109,33 @@ describe('extra tests', () => {
expect(scriptTags.text({ body: true })).toBe('')
expect(scriptTags.text({ pbody: true })).toBe('')
})
+
+ test('add additional app and test head/body injector helpers', () => {
+ const baseInfo = {
+ title: 'hello',
+ htmlAttrs: { lang: 'en' },
+ bodyAttrs: { class: 'base-class' },
+ script: [{ src: '/script.js', body: true }]
+ }
+ const extraInfo = {
+ bodyAttrs: { class: 'extra-class' },
+ script: [{ src: '/script.js', pbody: true }]
+ }
+
+ const serverInjector = _generateServerInjector(defaultOptions, baseInfo)
+ serverInjector.addInfo('test-app', extraInfo)
+
+ const meta = serverInjector.injectors
+
+ expect(meta.script.text()).toBe('')
+ expect(meta.script.text({ body: true })).toBe('')
+ expect(meta.script.text({ pbody: true })).toBe('')
+
+ expect(meta.head(true)).toBe('hello\n')
+ expect(meta.bodyPrepend(true)).toBe('\n')
+ expect(meta.bodyAppend()).toBe('')
+
+ expect(meta.htmlAttrs.text()).toBe('lang="en" data-vue-meta="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D"')
+ expect(meta.bodyAttrs.text()).toBe('class="base-class extra-class" data-vue-meta="%7B%22class%22:%7B%22ssr%22:%22base-class%22,%22test-app%22:%22extra-class%22%7D%7D"')
+ })
})
diff --git a/test/unit/plugin.test.js b/test/unit/plugin.test.js
index ba519b5..65a143d 100644
--- a/test/unit/plugin.test.js
+++ b/test/unit/plugin.test.js
@@ -120,6 +120,7 @@ describe('plugin', () => {
})
test('can use generate export', () => {
+ process.server = true
const rawInfo = {
meta: [{ charset: 'utf-8' }]
}
@@ -131,6 +132,22 @@ describe('plugin', () => {
expect(metaInfo.script.text()).toBe('')
})
+ test('warning when calling generate in browser build', () => {
+ process.server = false
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
+
+ const rawInfo = {
+ meta: [{ charset: 'utf-8' }]
+ }
+
+ const metaInfo = VueMetaPlugin.generate(rawInfo)
+ expect(metaInfo).toBeUndefined()
+ expect(warn).toHaveBeenCalledTimes(1)
+ expect(warn).toHaveBeenCalledWith('generate is not supported in browser builds')
+
+ warn.mockRestore()
+ })
+
test('updates can be paused and resumed', async () => {
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
diff --git a/test/utils/index.js b/test/utils/index.js
index d6d58a4..d0449e2 100644
--- a/test/utils/index.js
+++ b/test/utils/index.js
@@ -1,6 +1,6 @@
import { JSDOM } from 'jsdom'
import { mount, shallowMount, createWrapper, createLocalVue } from '@vue/test-utils'
-import { renderToString } from '@vue/server-test-utils'
+import { render, renderToString } from '@vue/server-test-utils'
import { attributeMap } from '../../src/client/updaters/attribute'
import { defaultOptions } from '../../src/shared/constants'
import VueMetaPlugin from '../../src'
@@ -9,8 +9,10 @@ export {
mount,
shallowMount,
createWrapper,
+ render,
renderToString,
- VueMetaPlugin
+ VueMetaPlugin,
+ attributeMap
}
export function getVue () {