diff --git a/test/advanced.js b/test/advanced.js new file mode 100644 index 000000000..23f7e583c --- /dev/null +++ b/test/advanced.js @@ -0,0 +1,272 @@ +process.env.VUE_LOADER_TEST = true + +const path = require('path') +const { expect } = require('chai') +const { + mfs, + loaderPath, + bundle, + test, + mockRender +} = require('./shared') + +const normalizeNewline = require('normalize-newline') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const SourceMapConsumer = require('source-map').SourceMapConsumer + +describe('advanced features', () => { + it('pre/post loaders', done => { + test({ + entry: 'basic.vue', + vue: { + preLoaders: { + js: require.resolve('./mock-loaders/js'), + css: require.resolve('./mock-loaders/css') + }, + postLoaders: { + html: require.resolve('./mock-loaders/html') + } + } + }, (window, module) => { + const vnode = mockRender(module, { + msg: 'hi' + }) + //

{{msg}}

+ expect(vnode.tag).to.equal('h2') + expect(vnode.data.staticClass).to.equal('green') + expect(vnode.children[0].text).to.equal('hi') + + expect(module.data().msg).to.contain('Changed!') + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('comp-a h2 {\n color: #00f;\n}') + done() + }) + }) + + it('support chaining with other loaders', done => { + const mockLoaderPath = require.resolve('./mock-loaders/js') + test({ + entry: 'basic.vue', + modify: config => { + config.module.rules[0].loader = loaderPath + '!' + mockLoaderPath + } + }, (window, module) => { + expect(module.data().msg).to.equal('Changed!') + done() + }) + }) + + it('expose filename', done => { + test({ + entry: 'basic.vue' + }, (window, module, rawModule) => { + expect(module.__file).to.equal(path.normalize('test/fixtures/basic.vue')) + done() + }) + }) + + it('extract CSS', done => { + bundle({ + entry: 'extract-css.vue', + vue: { + loaders: { + css: ExtractTextPlugin.extract('css-loader'), + stylus: ExtractTextPlugin.extract('css-loader?sourceMap!stylus-loader') + } + }, + plugins: [ + new ExtractTextPlugin('test.output.css') + ] + }, (code, warnings) => { + let css = mfs.readFileSync('/test.output.css').toString() + css = normalizeNewline(css) + expect(css).to.contain('h1 {\n color: #f00;\n}') + expect(css).to.contain('h2 {\n color: green;\n}') + done() + }) + }) + + it('extract CSS using option', done => { + bundle({ + entry: 'extract-css.vue', + vue: { + extractCSS: true + }, + plugins: [ + new ExtractTextPlugin('test.output.css') + ] + }, (code, warnings) => { + let css = mfs.readFileSync('/test.output.css').toString() + css = normalizeNewline(css) + expect(css).to.contain('h1 {\n color: #f00;\n}') + expect(css).to.contain('h2 {\n color: green;\n}') + done() + }) + }) + + it('extract CSS using option (passing plugin instance)', done => { + const plugin = new ExtractTextPlugin('test.output.css') + bundle({ + entry: 'extract-css.vue', + vue: { + extractCSS: plugin + }, + plugins: [ + plugin + ] + }, (code, warnings) => { + let css = mfs.readFileSync('/test.output.css').toString() + css = normalizeNewline(css) + expect(css).to.contain('h1 {\n color: #f00;\n}') + expect(css).to.contain('h2 {\n color: green;\n}') + done() + }) + }) + + it('pre-processors with extract css', done => { + test({ + entry: 'pre.vue', + vue: { + extractCSS: true + }, + plugins: [ + new ExtractTextPlugin('test.output.css') + ] + }, (window, module) => { + const vnode = mockRender(module) + + expect(vnode.children[0].tag).to.equal('h1') + expect(vnode.children[1].tag).to.equal('comp-a') + expect(vnode.children[2].tag).to.equal('comp-b') + + expect(module.data().msg).to.contain('Hello from coffee!') + + let css = mfs.readFileSync('/test.output.css').toString() + css = normalizeNewline(css) + expect(css).to.contain('body {\n font: 100% Helvetica, sans-serif;\n color: #999;\n}') + + done() + }) + }) + + it('source map', done => { + bundle({ + entry: 'basic.vue', + devtool: '#source-map' + }, (code, warnings) => { + const map = mfs.readFileSync('/test.build.js.map', 'utf-8') + const smc = new SourceMapConsumer(JSON.parse(map)) + let line + let col + const targetRE = /^\s+msg: 'Hello from Component A!'/ + code.split(/\r?\n/g).some((l, i) => { + if (targetRE.test(l)) { + line = i + 1 + col = 0 + return true + } + }) + const pos = smc.originalPositionFor({ + line: line, + column: col + }) + expect(pos.source.indexOf('basic.vue') > -1) + expect(pos.line).to.equal(9) + done() + }) + }) + + it('multiple rule definitions', done => { + test({ + modify: config => { + // remove default rule + config.module.rules.shift() + }, + entry: './test/fixtures/multiple-rules.js', + module: { + rules: [ + { + test: /\.vue$/, + oneOf: [ + { + include: /-1\.vue$/, + loader: loaderPath, + options: { + postcss: [ + css => { + css.walkDecls('font-size', decl => { + decl.value = `${parseInt(decl.value, 10) * 2}px` + }) + } + ], + compilerModules: [{ + postTransformNode: el => { + el.staticClass = '"multiple-rule-1"' + } + }] + } + }, + { + include: /-2\.vue$/, + loader: loaderPath, + options: { + postcss: [ + css => { + css.walkDecls('font-size', decl => { + decl.value = `${parseInt(decl.value, 10) / 2}px` + }) + } + ], + compilerModules: [{ + postTransformNode: el => { + el.staticClass = '"multiple-rule-2"' + } + }] + } + } + ] + } + ] + } + }, (window, module) => { + const vnode1 = mockRender(window.rules[0]) + const vnode2 = mockRender(window.rules[1]) + expect(vnode1.data.staticClass).to.equal('multiple-rule-1') + expect(vnode2.data.staticClass).to.equal('multiple-rule-2') + const styles = window.document.querySelectorAll('style') + const expr = /\.multiple-rule-\d\s*\{\s*font-size:\s*([.0-9]+)px;/ + for (let i = 0, l = styles.length; i < l; i++) { + const content = styles[i].textContent + if (expr.test(content)) { + expect(parseFloat(RegExp.$1)).to.be.equal(14) + } + } + done() + }) + }) + + it('thread mode', done => { + test({ + entry: 'basic.vue', + vue: { + threadMode: true + } + }, (window, module, rawModule) => { + const vnode = mockRender(module, { + msg: 'hi' + }) + + //

{{msg}}

+ expect(vnode.tag).to.equal('h2') + expect(vnode.data.staticClass).to.equal('red') + expect(vnode.children[0].text).to.equal('hi') + + expect(module.data().msg).to.contain('Hello from Component A!') + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('comp-a h2 {\n color: #f00;\n}') + done() + }) + }) +}) diff --git a/test/basic.js b/test/basic.js new file mode 100644 index 000000000..5668239b4 --- /dev/null +++ b/test/basic.js @@ -0,0 +1,105 @@ +process.env.VUE_LOADER_TEST = true + +const { expect } = require('chai') +const { + genId, + test, + mockRender, + initStylesForAllSubComponents +} = require('./shared') + +const normalizeNewline = require('normalize-newline') + +describe('basic usage', () => { + it('basic', done => { + test({ + entry: 'basic.vue' + }, (window, module, rawModule) => { + const vnode = mockRender(module, { + msg: 'hi' + }) + + //

{{msg}}

+ expect(vnode.tag).to.equal('h2') + expect(vnode.data.staticClass).to.equal('red') + expect(vnode.children[0].text).to.equal('hi') + + expect(module.data().msg).to.contain('Hello from Component A!') + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('comp-a h2 {\n color: #f00;\n}') + done() + }) + }) + + it('pre-processors', done => { + test({ + entry: 'pre.vue' + }, (window, module) => { + const vnode = mockRender(module) + // div + // h1 This is the app + // comp-a + // comp-b + expect(vnode.children[0].tag).to.equal('h1') + expect(vnode.children[1].tag).to.equal('comp-a') + expect(vnode.children[2].tag).to.equal('comp-b') + + expect(module.data().msg).to.contain('Hello from coffee!') + const style = window.document.querySelector('style').textContent + expect(style).to.contain('body {\n font: 100% Helvetica, sans-serif;\n color: #999;\n}') + done() + }) + }) + + it('style import', done => { + test({ + entry: 'style-import.vue' + }, (window) => { + const styles = window.document.querySelectorAll('style') + expect(styles[0].textContent).to.contain('h1 { color: red;\n}') + // import with scoped + const id = 'data-v-' + genId('style-import.vue') + expect(styles[1].textContent).to.contain('h1[' + id + '] { color: green;\n}') + done() + }) + }) + + it('style import for a same file twice', done => { + test({ + entry: 'style-import-twice.vue' + }, (window, module) => { + initStylesForAllSubComponents(module) + const styles = window.document.querySelectorAll('style') + expect(styles.length).to.equal(3) + expect(styles[0].textContent).to.contain('h1 { color: red;\n}') + // import with scoped + const id = 'data-v-' + genId('style-import-twice.vue') + expect(styles[1].textContent).to.contain('h1[' + id + '] { color: green;\n}') + const id2 = 'data-v-' + genId('style-import-twice-sub.vue') + expect(styles[2].textContent).to.contain('h1[' + id2 + '] { color: green;\n}') + done() + }) + }) + + it('template import', done => { + test({ + entry: 'template-import.vue' + }, (window, module) => { + const vnode = mockRender(module) + // '

hello

' + expect(vnode.children[0].tag).to.equal('h1') + expect(vnode.children[0].children[0].text).to.equal('hello') + done() + }) + }) + + it('script import', done => { + test({ + entry: 'script-import.vue' + }, (window, module) => { + expect(module.data().msg).to.contain('Hello from Component A!') + done() + }) + }) +}) diff --git a/test/custom.js b/test/custom.js new file mode 100644 index 000000000..9056dbfea --- /dev/null +++ b/test/custom.js @@ -0,0 +1,156 @@ +process.env.VUE_LOADER_TEST = true + +const { expect } = require('chai') +const { + mfs, + bundle, + test, + mockRender +} = require('./shared') + +const normalizeNewline = require('normalize-newline') +const webpack = require('webpack') +const ExtractTextPlugin = require('extract-text-webpack-plugin') + +describe('custom block features', () => { + it('extract custom blocks to a separate file', done => { + bundle({ + entry: 'custom-language.vue', + vue: { + loaders: { + 'documentation': ExtractTextPlugin.extract('raw-loader') + } + }, + plugins: [ + new ExtractTextPlugin('doc.md') + ] + }, (code, warnings) => { + let unitTest = mfs.readFileSync('/doc.md').toString() + unitTest = normalizeNewline(unitTest) + expect(unitTest).to.contain('This is example documentation for a component.') + done() + }) + }) + + it('add custom blocks to the webpack output', done => { + bundle({ + entry: 'custom-language.vue', + vue: { + loaders: { + 'unit-test': 'babel-loader' + } + } + }, (code, warnings) => { + expect(code).to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') + done() + }) + }) + + it('custom blocks work with src imports', done => { + bundle({ + entry: 'custom-import.vue', + vue: { + loaders: { + 'unit-test': 'babel-loader' + } + } + }, (code, warnings) => { + expect(code).to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') + done() + }) + }) + + it('passes Component to custom block loaders', done => { + const mockLoaderPath = require.resolve('./mock-loaders/docs') + test({ + entry: 'custom-language.vue', + vue: { + loaders: { + 'documentation': mockLoaderPath + } + } + }, (window, module) => { + expect(module.__docs).to.contain('This is example documentation for a component.') + done() + }) + }) + + it('custom blocks can be ignored', done => { + bundle({ + entry: 'custom-language.vue' + }, (code, warnings) => { + expect(code).not.to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') + done() + }) + }) + + it('custom blocks with ES module default export', done => { + test({ + entry: 'custom-blocks.vue', + vue: { + loaders: { + esm: require.resolve('./mock-loaders/identity') + } + } + }, (window, module) => { + // option added by custom block code + expect(module.foo).to.equal(1) + done() + }) + }) + + it('passes attributes as options to the loader', done => { + bundle({ + entry: 'custom-options.vue', + vue: { + loaders: { + 'unit-test': 'babel-loader!skeleton-loader' + } + }, + plugins: [ + new webpack.LoaderOptionsPlugin({ + options: { + skeletonLoader: { + procedure: (content, sourceMap, callback, options) => { + expect(options.foo).to.equal('bar') + expect(options.opt2).to.equal('value2') + + // Return the content to output. + return content + } + } + } + }) + ] + }, (code, warnings) => { + expect(code).to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') + done() + }) + }) + + it('pre/post loaders for custom blocks', done => { + test({ + entry: 'custom-blocks.vue', + vue: { + preLoaders: { + i18n: require.resolve('./mock-loaders/yaml') + }, + loaders: { + i18n: require.resolve('./mock-loaders/i18n'), + blog: 'marked' + }, + postLoaders: { + blog: require.resolve('./mock-loaders/blog') + } + } + }, (window, module) => { + const vnode = mockRender(module, { + msg: JSON.parse(module.__i18n).en.hello, + blog: module.__blog + }) + expect(vnode.children[0].children[0].text).to.equal('hello world') + expect(vnode.children[2].data.domProps.innerHTML).to.equal('

foo

') + done() + }) + }) +}) diff --git a/test/script.js b/test/script.js new file mode 100644 index 000000000..c0644669e --- /dev/null +++ b/test/script.js @@ -0,0 +1,34 @@ +process.env.VUE_LOADER_TEST = true + +const { expect } = require('chai') +const { + test, + mockRender +} = require('./shared') + +describe('script block features', () => { + it('allows to export extended constructor', done => { + test({ + entry: 'extend.vue' + }, (window, Module) => { + // extend.vue should export Vue constructor + const vnode = mockRender(Module.options, { + msg: 'success' + }) + expect(vnode.tag).to.equal('div') + expect(vnode.children[0].text).to.equal('success') + expect(new Module().msg === 'success') + done() + }) + }) + + it('named exports', done => { + test({ + entry: 'named-exports.vue' + }, (window, _, rawModule) => { + expect(rawModule.default.name).to.equal('named-exports') + expect(rawModule.foo()).to.equal(1) + done() + }) + }) +}) diff --git a/test/shared.js b/test/shared.js new file mode 100644 index 000000000..8790f0272 --- /dev/null +++ b/test/shared.js @@ -0,0 +1,139 @@ +const path = require('path') +const jsdom = require('jsdom') +const webpack = require('webpack') +const merge = require('webpack-merge') +const MemoryFS = require('memory-fs') +const { expect } = require('chai') +const hash = require('hash-sum') +const Vue = require('vue') + +const mfs = new MemoryFS() +const loaderPath = path.resolve(__dirname, '../index.js') +const globalConfig = { + // mode: 'development', + devtool: false, + output: { + path: '/', + filename: 'test.build.js' + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: loaderPath + } + ] + }, + plugins: [ + new webpack.optimize.ModuleConcatenationPlugin() + ] +} + +function genId (file) { + return hash(path.join('test', 'fixtures', file)) +} + +function bundle (options, cb, wontThrowError) { + let config = merge({}, globalConfig, options) + + if (config.vue) { + const vueOptions = options.vue + delete config.vue + const vueIndex = config.module.rules.findIndex(r => r.test.test('.vue')) + const vueRule = config.module.rules[vueIndex] + config.module.rules[vueIndex] = Object.assign({}, vueRule, { options: vueOptions }) + } + + if (/\.vue$/.test(config.entry)) { + const vueFile = config.entry + config = merge(config, { + entry: require.resolve('./fixtures/entry'), + resolve: { + alias: { + '~target': path.resolve(__dirname, './fixtures', vueFile) + } + } + }) + } + + if (options.modify) { + delete config.modify + options.modify(config) + } + + const webpackCompiler = webpack(config) + webpackCompiler.outputFileSystem = mfs + webpackCompiler.run((err, stats) => { + const errors = stats.compilation.errors + if (!wontThrowError) { + expect(err).to.be.null + if (errors && errors.length) { + errors.forEach(error => { + console.error(error.message) + }) + } + expect(errors).to.be.empty + } + cb(mfs.readFileSync('/test.build.js').toString(), stats, err) + }) +} + +function test (options, assert, wontThrowError) { + bundle(options, (code, stats, err) => { + jsdom.env({ + html: '', + src: [code], + done: (errors, window) => { + if (errors) { + console.log(errors[0].data.error.stack) + if (!wontThrowError) { + expect(errors).to.be.null + } + } + const module = interopDefault(window.vueModule) + const instance = {} + if (module && module.beforeCreate) { + module.beforeCreate.forEach(hook => hook.call(instance)) + } + assert(window, module, window.vueModule, instance, errors, { stats, err }) + } + }) + }, wontThrowError) +} + +function mockRender (options, data = {}) { + const vm = new Vue(Object.assign({}, options, { data () { return data } })) + vm.$mount() + return vm._vnode +} + +function interopDefault (module) { + return module + ? module.default ? module.default : module + : module +} + +function initStylesForAllSubComponents (module) { + if (module.components) { + for (const name in module.components) { + const sub = module.components[name] + const instance = {} + if (sub && sub.beforeCreate) { + sub.beforeCreate.forEach(hook => hook.call(instance)) + } + initStylesForAllSubComponents(sub) + } + } +} + +module.exports = { + mfs, + loaderPath, + globalConfig, + genId, + bundle, + test, + mockRender, + interopDefault, + initStylesForAllSubComponents +} diff --git a/test/ssr.js b/test/ssr.js new file mode 100644 index 000000000..e3c950a7f --- /dev/null +++ b/test/ssr.js @@ -0,0 +1,78 @@ +process.env.VUE_LOADER_TEST = true + +const { expect } = require('chai') + +const { + bundle, + interopDefault, + globalConfig +} = require('./shared') + +const SSR = require('vue-server-renderer') + +describe('SSR', () => { + it('css-modules in SSR', done => { + bundle({ + entry: 'css-modules.vue', + target: 'node', + output: Object.assign({}, globalConfig.output, { + libraryTarget: 'commonjs2' + }) + }, (code, warnings) => { + // http://stackoverflow.com/questions/17581830/load-node-js-module-from-string-in-memory + function requireFromString (src, filename) { + const Module = module.constructor + const m = new Module() + m._compile(src, filename) + return m.exports + } + + const output = interopDefault(requireFromString(code, './test.build.js')) + const mockInstance = {} + + output.beforeCreate.forEach(hook => hook.call(mockInstance)) + expect(mockInstance.style.red).to.exist + + done() + }) + }) + + it('SSR style and moduleId extraction', done => { + bundle({ + target: 'node', + entry: './test/fixtures/ssr-style.js', + output: { + path: '/', + filename: 'test.build.js', + libraryTarget: 'commonjs2' + }, + externals: ['vue'] + }, code => { + const renderer = SSR.createBundleRenderer(code, { + basedir: __dirname, + runInNewContext: 'once' + }) + const context = { + _registeredComponents: new Set() + } + renderer.renderToString(context, (err, res) => { + if (err) return done(err) + expect(res).to.contain('data-server-rendered') + expect(res).to.contain('

Hello

') + expect(res).to.contain('Hello from Component A!') + expect(res).to.contain('
functional
') + // from main component + expect(context.styles).to.contain('h1 { color: green;') + // from imported child component + expect(context.styles).to.contain('comp-a h2 {\n color: #f00;') + // from imported css file + expect(context.styles).to.contain('h1 { color: red;') + // from imported functional component + expect(context.styles).to.contain('.foo { color: red;') + // collect component identifiers during render + expect(Array.from(context._registeredComponents).length).to.equal(3) + done() + }) + }) + }) +}) diff --git a/test/style.js b/test/style.js new file mode 100644 index 000000000..c3a0c5cd0 --- /dev/null +++ b/test/style.js @@ -0,0 +1,221 @@ +process.env.VUE_LOADER_TEST = true + +const fs = require('fs') +const path = require('path') +const { expect } = require('chai') +const { + genId, + test, + mockRender +} = require('./shared') + +const normalizeNewline = require('normalize-newline') + +describe('style block features', () => { + it('scoped style', done => { + test({ + entry: 'scoped-css.vue' + }, (window, module) => { + const id = 'data-v-' + genId('scoped-css.vue') + expect(module._scopeId).to.equal(id) + + const vnode = mockRender(module, { + ok: true + }) + //
+ //

hi

+ //

hi

+ // + //

+ //
+ expect(vnode.children[0].tag).to.equal('div') + expect(vnode.children[1].text).to.equal(' ') + expect(vnode.children[2].tag).to.equal('p') + expect(vnode.children[2].data.staticClass).to.equal('abc def') + expect(vnode.children[4].tag).to.equal('p') + expect(vnode.children[4].data.staticClass).to.equal('test') + + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain(`.test[${id}] {\n color: yellow;\n}`) + expect(style).to.contain(`.test[${id}]:after {\n content: \'bye!\';\n}`) + expect(style).to.contain(`h1[${id}] {\n color: green;\n}`) + // scoped keyframes + expect(style).to.contain(`.anim[${id}] {\n animation: color-${id} 5s infinite, other 5s;`) + expect(style).to.contain(`.anim-2[${id}] {\n animation-name: color-${id}`) + expect(style).to.contain(`.anim-3[${id}] {\n animation: 5s color-${id} infinite, 5s other;`) + expect(style).to.contain(`@keyframes color-${id} {`) + expect(style).to.contain(`@-webkit-keyframes color-${id} {`) + + expect(style).to.contain(`.anim-multiple[${id}] {\n animation: color-${id} 5s infinite,opacity-${id} 2s;`) + expect(style).to.contain(`.anim-multiple-2[${id}] {\n animation-name: color-${id},opacity-${id};`) + expect(style).to.contain(`@keyframes opacity-${id} {`) + expect(style).to.contain(`@-webkit-keyframes opacity-${id} {`) + // >>> combinator + expect(style).to.contain(`.foo p[${id}] .bar {\n color: red;\n}`) + done() + }) + }) + + it('media-query', done => { + test({ + entry: 'media-query.vue' + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + const id = 'data-v-' + genId('media-query.vue') + expect(style).to.contain('@media print {\n.foo[' + id + '] {\n color: #000;\n}\n}') + done() + }) + }) + + it('supports-query', done => { + test({ + entry: 'supports-query.vue' + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + const id = 'data-v-' + genId('supports-query.vue') + expect(style).to.contain('@supports ( color: #000 ) {\n.foo[' + id + '] {\n color: #000;\n}\n}') + done() + }) + }) + + it('postcss options', done => { + test({ + entry: 'postcss.vue', + vue: { + postcss: { + options: { + parser: require('sugarss') + } + } + } + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') + done() + }) + }) + + it('load postcss config file', done => { + fs.writeFileSync('.postcssrc', JSON.stringify({ parser: 'sugarss' })) + test({ + entry: 'postcss.vue' + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') + fs.unlinkSync('.postcssrc') + done() + }) + }) + + it('load cascading postcss config file', done => { + fs.writeFileSync('.postcssrc', JSON.stringify({ parser: 'sugarss' })) + test({ + entry: 'sub/postcss-cascade.vue', + vue: { postcss: { cascade: true }} + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('display: -webkit-box') + fs.unlinkSync('.postcssrc') + done() + }) + }) + + it('load postcss config file by path', done => { + fs.writeFileSync('test/.postcssrc', JSON.stringify({ parser: 'sugarss' })) + test({ + entry: 'postcss.vue', + vue: { + postcss: { + config: { + path: path.resolve('test') + } + } + } + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') + fs.unlinkSync('test/.postcssrc') + done() + }) + }) + + it('load postcss config file with js syntax error', done => { + fs.writeFileSync('.postcssrc.js', 'module.exports = { hello }') + test({ + entry: 'basic.vue' + }, (window, module, vueModule, instance, jsdomErr, webpackInfo) => { + const { stats: { compilation: { warnings, errors }}, err } = webpackInfo + expect(jsdomErr).to.be.null + expect(err).to.be.null + expect(errors).to.be.empty + expect(warnings.length).to.equal(1) + expect(warnings[0]).to.match(/Error loading PostCSS config\:/) + fs.unlinkSync('.postcssrc.js') + done() + }, true) + }) + + it('postcss lang', done => { + test({ + entry: 'postcss-lang.vue' + }, (window) => { + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') + done() + }) + }) + + it('css-modules', done => { + function testWithIdent (localIdentName, regexToMatch, cb) { + test({ + entry: 'css-modules.vue', + vue: { + cssModules: localIdentName && { + localIdentName: localIdentName + } + } + }, (window, module, raw, instance) => { + // get local class name + const className = instance.style.red + expect(className).to.match(regexToMatch) + + // class name in style + let style = [].slice.call(window.document.querySelectorAll('style')).map((style) => { + return style.textContent + }).join('\n') + style = normalizeNewline(style) + expect(style).to.contain('.' + className + ' {\n color: red;\n}') + + // animation name + const match = style.match(/@keyframes\s+(\S+)\s+{/) + expect(match).to.have.length(2) + const animationName = match[1] + expect(animationName).to.not.equal('fade') + expect(style).to.contain('animation: ' + animationName + ' 1s;') + + // default module + pre-processor + scoped + const anotherClassName = instance.$style.red + expect(anotherClassName).to.match(regexToMatch).and.not.equal(className) + const id = 'data-v-' + genId('css-modules.vue') + expect(style).to.contain('.' + anotherClassName + '[' + id + ']') + + cb() + }) + } + // default localIdentName + testWithIdent(undefined, /^red_\w{8}/, () => { + // specified localIdentName + const ident = '[path][name]---[local]---[hash:base64:5]' + const regex = /css-modules---red---\w{5}/ + testWithIdent(ident, regex, done) + }) + }) +}) diff --git a/test/template.js b/test/template.js new file mode 100644 index 000000000..a4a70f037 --- /dev/null +++ b/test/template.js @@ -0,0 +1,251 @@ +process.env.VUE_LOADER_TEST = true + +const path = require('path') +const { expect } = require('chai') +const { + test, + mockRender +} = require('./shared') + +const normalizeNewline = require('normalize-newline') + +describe('template block features', () => { + it('template with comments', done => { + test({ + entry: 'template-comment.vue' + }, (window, module, rawModule) => { + expect(module.comments).to.equal(true) + const vnode = mockRender(module, { + msg: 'hi' + }) + expect(vnode.tag).to.equal('div') + expect(vnode.children.length).to.equal(2) + expect(vnode.children[0].data.staticClass).to.equal('red') + expect(vnode.children[0].children[0].text).to.equal('hi') + expect(vnode.children[1].isComment).to.true + expect(vnode.children[1].text).to.equal(' comment here ') + done() + }) + }) + + it('transpile ES2015 features in template', done => { + test({ + entry: 'es2015.vue' + }, (window, module) => { + const vnode = mockRender(module, { + a: 'hello', + b: true + }) + //
+ expect(vnode.tag).to.equal('div') + expect(vnode.data.class['test-hello']).to.equal(true) + expect(vnode.data.class['b']).to.equal(true) + done() + }) + }) + + it('translates relative URLs and respects resolve alias', done => { + test({ + entry: 'resolve.vue', + resolve: { + alias: { + fixtures: path.resolve(__dirname, './fixtures') + } + }, + module: { + rules: [ + { test: /\.png$/, loader: 'file-loader?name=[name].[hash:6].[ext]' } + ] + } + }, (window, module) => { + const vnode = mockRender(module) + //
+ // + // + //
+ expect(vnode.children[0].tag).to.equal('img') + expect(vnode.children[0].data.attrs.src).to.equal('logo.c9e00e.png') + expect(vnode.children[2].tag).to.equal('img') + expect(vnode.children[2].data.attrs.src).to.equal('logo.c9e00e.png') + + const style = window.document.querySelector('style').textContent + expect(style).to.contain('html { background-image: url(logo.c9e00e.png);\n}') + expect(style).to.contain('body { background-image: url(logo.c9e00e.png);\n}') + done() + }) + }) + + it('transformToRequire option', done => { + test({ + entry: 'transform.vue', + module: { + rules: [ + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader' + } + ] + } + }, (window, module) => { + function includeDataURL (s) { + return !!s.match(/\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*/i) + } + const vnode = mockRender(module) + // img tag + expect(includeDataURL(vnode.children[0].data.attrs.src)).to.equal(true) + // image tag (SVG) + expect(includeDataURL(vnode.children[2].children[0].data.attrs['xlink:href'])).to.equal(true) + const style = window.document.querySelector('style').textContent + + const dataURL = vnode.children[0].data.attrs.src + + // image tag with srcset + expect(vnode.children[4].data.attrs.srcset).to.equal(dataURL) + expect(vnode.children[6].data.attrs.srcset).to.equal(dataURL + ' 2x') + // image tag with multiline srcset + expect(vnode.children[8].data.attrs.srcset).to.equal(dataURL + ', ' + dataURL + ' 2x') + expect(vnode.children[10].data.attrs.srcset).to.equal(dataURL + ' 2x, ' + dataURL) + expect(vnode.children[12].data.attrs.srcset).to.equal(dataURL + ' 2x, ' + dataURL + ' 3x') + expect(vnode.children[14].data.attrs.srcset).to.equal(dataURL + ', ' + dataURL + ' 2x, ' + dataURL + ' 3x') + expect(vnode.children[16].data.attrs.srcset).to.equal(dataURL + ' 2x, ' + dataURL + ' 3x') + + // style + expect(includeDataURL(style)).to.equal(true) + done() + }) + }) + + it('should allow adding custom html loaders', done => { + test({ + entry: 'markdown.vue', + vue: { + loaders: { + html: 'marked' + } + } + }, (window, module) => { + const vnode = mockRender(module, { + msg: 'hi' + }) + //

{{msg}}

+ expect(vnode.tag).to.equal('h2') + expect(vnode.data.attrs.id).to.equal('-msg-') + expect(vnode.children[0].text).to.equal('hi') + done() + }) + }) + + it('custom compiler modules', done => { + test({ + entry: 'custom-module.vue', + vue: { + compilerModules: [ + { + postTransformNode: el => { + if (el.staticStyle) { + el.staticStyle = `$processStyle(${el.staticStyle})` + } + if (el.styleBinding) { + el.styleBinding = `$processStyle(${el.styleBinding})` + } + } + } + ] + } + }, (window, module) => { + const results = [] + // var vnode = + mockRender( + Object.assign(module, { methods: { $processStyle: style => results.push(style) }}), + { transform: 'translateX(10px)' } + ) + expect(results).to.deep.equal([ + { 'flex-direction': 'row' }, + { 'transform': 'translateX(10px)' } + ]) + done() + }) + }) + + it('custom compiler directives', done => { + test({ + entry: 'custom-directive.vue', + vue: { + compilerDirectives: { + i18n (el, dir) { + if (dir.name === 'i18n' && dir.value) { + el.i18n = dir.value + if (!el.props) { + el.props = [] + } + el.props.push({ name: 'textContent', value: `_s(${JSON.stringify(dir.value)})` }) + } + } + } + } + }, (window, module) => { + const vnode = mockRender(module) + expect(vnode.data.domProps.textContent).to.equal('keypath') + done() + }) + }) + + it('functional component with styles', done => { + test({ + entry: 'functional-style.vue' + }, (window, module, rawModule) => { + expect(module.functional).to.equal(true) + const vnode = mockRender(module) + //
hi
+ expect(vnode.tag).to.equal('div') + expect(vnode.data.class).to.equal('foo') + expect(vnode.children[0].text).to.equal('functional') + + let style = window.document.querySelector('style').textContent + style = normalizeNewline(style) + expect(style).to.contain('.foo { color: red;\n}') + done() + }) + }) + + it('functional template', done => { + test({ + entry: 'functional-root.vue', + vue: { + preserveWhitespace: false + } + }, (window, module) => { + expect(module.components.Functional._compiled).to.equal(true) + expect(module.components.Functional.functional).to.equal(true) + expect(module.components.Functional.staticRenderFns).to.exist + expect(module.components.Functional.render).to.be.a('function') + + const vnode = mockRender(module, { + fn () { + done() + } + }).children[0] + + // Basic vnode + expect(vnode.children[0].data.staticClass).to.equal('red') + expect(vnode.children[0].children[0].text).to.equal('hello') + // Default slot vnode + expect(vnode.children[1].tag).to.equal('span') + expect(vnode.children[1].children[0].text).to.equal('hello') + // Named slot vnode + expect(vnode.children[2].tag).to.equal('div') + expect(vnode.children[2].children[0].text).to.equal('Second slot') + // // Scoped slot vnode + expect(vnode.children[3].text).to.equal('hello') + // // Static content vnode + expect(vnode.children[4].tag).to.equal('div') + expect(vnode.children[4].children[0].text).to.equal('Some ') + expect(vnode.children[4].children[1].tag).to.equal('span') + expect(vnode.children[4].children[1].children[0].text).to.equal('text') + // // v-if vnode + expect(vnode.children[5].text).to.equal('') + + vnode.children[6].data.on.click() + }) + }) +}) diff --git a/test/test.js b/test/test.js deleted file mode 100644 index aff24e89d..000000000 --- a/test/test.js +++ /dev/null @@ -1,1161 +0,0 @@ -process.env.VUE_LOADER_TEST = true - -const fs = require('fs') -const path = require('path') -const jsdom = require('jsdom') -const webpack = require('webpack') -const merge = require('webpack-merge') -const MemoryFS = require('memory-fs') -const expect = require('chai').expect -const hash = require('hash-sum') -const Vue = require('vue') -const SSR = require('vue-server-renderer') -// var compiler = require('../lib/template-compiler') -const normalizeNewline = require('normalize-newline') -const ExtractTextPlugin = require('extract-text-webpack-plugin') -const SourceMapConsumer = require('source-map').SourceMapConsumer - -const mfs = new MemoryFS() -const loaderPath = path.resolve(__dirname, '../index.js') -const globalConfig = { - // mode: 'development', - devtool: false, - output: { - path: '/', - filename: 'test.build.js' - }, - module: { - rules: [ - { - test: /\.vue$/, - loader: loaderPath - } - ] - }, - plugins: [ - new webpack.optimize.ModuleConcatenationPlugin() - ] -} - -function genId (file) { - return hash(path.join('test', 'fixtures', file)) -} - -function bundle (options, cb, wontThrowError) { - let config = merge({}, globalConfig, options) - - if (config.vue) { - const vueOptions = options.vue - delete config.vue - const vueIndex = config.module.rules.findIndex(r => r.test.test('.vue')) - const vueRule = config.module.rules[vueIndex] - config.module.rules[vueIndex] = Object.assign({}, vueRule, { options: vueOptions }) - } - - if (/\.vue$/.test(config.entry)) { - const vueFile = config.entry - config = merge(config, { - entry: require.resolve('./fixtures/entry'), - resolve: { - alias: { - '~target': path.resolve(__dirname, './fixtures', vueFile) - } - } - }) - } - - if (options.modify) { - delete config.modify - options.modify(config) - } - - const webpackCompiler = webpack(config) - webpackCompiler.outputFileSystem = mfs - webpackCompiler.run((err, stats) => { - const errors = stats.compilation.errors - if (!wontThrowError) { - expect(err).to.be.null - if (errors && errors.length) { - errors.forEach(error => { - console.error(error.message) - }) - } - expect(errors).to.be.empty - } - cb(mfs.readFileSync('/test.build.js').toString(), stats, err) - }) -} - -function test (options, assert, wontThrowError) { - bundle(options, (code, stats, err) => { - jsdom.env({ - html: '', - src: [code], - done: (errors, window) => { - if (errors) { - console.log(errors[0].data.error.stack) - if (!wontThrowError) { - expect(errors).to.be.null - } - } - const module = interopDefault(window.vueModule) - const instance = {} - if (module && module.beforeCreate) { - module.beforeCreate.forEach(hook => hook.call(instance)) - } - assert(window, module, window.vueModule, instance, errors, { stats, err }) - } - }) - }, wontThrowError) -} - -function mockRender (options, data = {}) { - const vm = new Vue(Object.assign({}, options, { data () { return data } })) - vm.$mount() - return vm._vnode -} - -function interopDefault (module) { - return module - ? module.default ? module.default : module - : module -} - -function initStylesForAllSubComponents (module) { - if (module.components) { - for (const name in module.components) { - const sub = module.components[name] - const instance = {} - if (sub && sub.beforeCreate) { - sub.beforeCreate.forEach(hook => hook.call(instance)) - } - initStylesForAllSubComponents(sub) - } - } -} - -describe('vue-loader', () => { - it('basic', done => { - test({ - entry: 'basic.vue' - }, (window, module, rawModule) => { - const vnode = mockRender(module, { - msg: 'hi' - }) - - //

{{msg}}

- expect(vnode.tag).to.equal('h2') - expect(vnode.data.staticClass).to.equal('red') - expect(vnode.children[0].text).to.equal('hi') - - expect(module.data().msg).to.contain('Hello from Component A!') - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('comp-a h2 {\n color: #f00;\n}') - done() - }) - }) - - it('expose filename', done => { - test({ - entry: 'basic.vue' - }, (window, module, rawModule) => { - expect(module.__file).to.equal(path.normalize('test/fixtures/basic.vue')) - done() - }) - }) - - it('pre-processors', done => { - test({ - entry: 'pre.vue' - }, (window, module) => { - const vnode = mockRender(module) - // div - // h1 This is the app - // comp-a - // comp-b - expect(vnode.children[0].tag).to.equal('h1') - expect(vnode.children[1].tag).to.equal('comp-a') - expect(vnode.children[2].tag).to.equal('comp-b') - - expect(module.data().msg).to.contain('Hello from coffee!') - const style = window.document.querySelector('style').textContent - expect(style).to.contain('body {\n font: 100% Helvetica, sans-serif;\n color: #999;\n}') - done() - }) - }) - - it('pre-processors with extract css', done => { - test({ - entry: 'pre.vue', - vue: { - extractCSS: true - }, - plugins: [ - new ExtractTextPlugin('test.output.css') - ] - }, (window, module) => { - const vnode = mockRender(module) - - expect(vnode.children[0].tag).to.equal('h1') - expect(vnode.children[1].tag).to.equal('comp-a') - expect(vnode.children[2].tag).to.equal('comp-b') - - expect(module.data().msg).to.contain('Hello from coffee!') - - let css = mfs.readFileSync('/test.output.css').toString() - css = normalizeNewline(css) - expect(css).to.contain('body {\n font: 100% Helvetica, sans-serif;\n color: #999;\n}') - - done() - }) - }) - - it('scoped style', done => { - test({ - entry: 'scoped-css.vue' - }, (window, module) => { - const id = 'data-v-' + genId('scoped-css.vue') - expect(module._scopeId).to.equal(id) - - const vnode = mockRender(module, { - ok: true - }) - //
- //

hi

- //

hi

- // - //

- //
- expect(vnode.children[0].tag).to.equal('div') - expect(vnode.children[1].text).to.equal(' ') - expect(vnode.children[2].tag).to.equal('p') - expect(vnode.children[2].data.staticClass).to.equal('abc def') - expect(vnode.children[4].tag).to.equal('p') - expect(vnode.children[4].data.staticClass).to.equal('test') - - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain(`.test[${id}] {\n color: yellow;\n}`) - expect(style).to.contain(`.test[${id}]:after {\n content: \'bye!\';\n}`) - expect(style).to.contain(`h1[${id}] {\n color: green;\n}`) - // scoped keyframes - expect(style).to.contain(`.anim[${id}] {\n animation: color-${id} 5s infinite, other 5s;`) - expect(style).to.contain(`.anim-2[${id}] {\n animation-name: color-${id}`) - expect(style).to.contain(`.anim-3[${id}] {\n animation: 5s color-${id} infinite, 5s other;`) - expect(style).to.contain(`@keyframes color-${id} {`) - expect(style).to.contain(`@-webkit-keyframes color-${id} {`) - - expect(style).to.contain(`.anim-multiple[${id}] {\n animation: color-${id} 5s infinite,opacity-${id} 2s;`) - expect(style).to.contain(`.anim-multiple-2[${id}] {\n animation-name: color-${id},opacity-${id};`) - expect(style).to.contain(`@keyframes opacity-${id} {`) - expect(style).to.contain(`@-webkit-keyframes opacity-${id} {`) - // >>> combinator - expect(style).to.contain(`.foo p[${id}] .bar {\n color: red;\n}`) - done() - }) - }) - - it('style import', done => { - test({ - entry: 'style-import.vue' - }, (window) => { - const styles = window.document.querySelectorAll('style') - expect(styles[0].textContent).to.contain('h1 { color: red;\n}') - // import with scoped - const id = 'data-v-' + genId('style-import.vue') - expect(styles[1].textContent).to.contain('h1[' + id + '] { color: green;\n}') - done() - }) - }) - - it('style import for a same file twice', done => { - test({ - entry: 'style-import-twice.vue' - }, (window, module) => { - initStylesForAllSubComponents(module) - const styles = window.document.querySelectorAll('style') - expect(styles.length).to.equal(3) - expect(styles[0].textContent).to.contain('h1 { color: red;\n}') - // import with scoped - const id = 'data-v-' + genId('style-import-twice.vue') - expect(styles[1].textContent).to.contain('h1[' + id + '] { color: green;\n}') - const id2 = 'data-v-' + genId('style-import-twice-sub.vue') - expect(styles[2].textContent).to.contain('h1[' + id2 + '] { color: green;\n}') - done() - }) - }) - - it('template import', done => { - test({ - entry: 'template-import.vue' - }, (window, module) => { - const vnode = mockRender(module) - // '

hello

' - expect(vnode.children[0].tag).to.equal('h1') - expect(vnode.children[0].children[0].text).to.equal('hello') - done() - }) - }) - - it('script import', done => { - test({ - entry: 'script-import.vue' - }, (window, module) => { - expect(module.data().msg).to.contain('Hello from Component A!') - done() - }) - }) - - it('source map', done => { - bundle({ - entry: 'basic.vue', - devtool: '#source-map' - }, (code, warnings) => { - const map = mfs.readFileSync('/test.build.js.map', 'utf-8') - const smc = new SourceMapConsumer(JSON.parse(map)) - let line - let col - const targetRE = /^\s+msg: 'Hello from Component A!'/ - code.split(/\r?\n/g).some((l, i) => { - if (targetRE.test(l)) { - line = i + 1 - col = 0 - return true - } - }) - const pos = smc.originalPositionFor({ - line: line, - column: col - }) - expect(pos.source.indexOf('basic.vue') > -1) - expect(pos.line).to.equal(9) - done() - }) - }) - - it('media-query', done => { - test({ - entry: 'media-query.vue' - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - const id = 'data-v-' + genId('media-query.vue') - expect(style).to.contain('@media print {\n.foo[' + id + '] {\n color: #000;\n}\n}') - done() - }) - }) - - it('supports-query', done => { - test({ - entry: 'supports-query.vue' - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - const id = 'data-v-' + genId('supports-query.vue') - expect(style).to.contain('@supports ( color: #000 ) {\n.foo[' + id + '] {\n color: #000;\n}\n}') - done() - }) - }) - - it('extract CSS', done => { - bundle({ - entry: 'extract-css.vue', - vue: { - loaders: { - css: ExtractTextPlugin.extract('css-loader'), - stylus: ExtractTextPlugin.extract('css-loader?sourceMap!stylus-loader') - } - }, - plugins: [ - new ExtractTextPlugin('test.output.css') - ] - }, (code, warnings) => { - let css = mfs.readFileSync('/test.output.css').toString() - css = normalizeNewline(css) - expect(css).to.contain('h1 {\n color: #f00;\n}') - expect(css).to.contain('h2 {\n color: green;\n}') - done() - }) - }) - - it('extract CSS using option', done => { - bundle({ - entry: 'extract-css.vue', - vue: { - extractCSS: true - }, - plugins: [ - new ExtractTextPlugin('test.output.css') - ] - }, (code, warnings) => { - let css = mfs.readFileSync('/test.output.css').toString() - css = normalizeNewline(css) - expect(css).to.contain('h1 {\n color: #f00;\n}') - expect(css).to.contain('h2 {\n color: green;\n}') - done() - }) - }) - - it('extract CSS using option (passing plugin instance)', done => { - const plugin = new ExtractTextPlugin('test.output.css') - bundle({ - entry: 'extract-css.vue', - vue: { - extractCSS: plugin - }, - plugins: [ - plugin - ] - }, (code, warnings) => { - let css = mfs.readFileSync('/test.output.css').toString() - css = normalizeNewline(css) - expect(css).to.contain('h1 {\n color: #f00;\n}') - expect(css).to.contain('h2 {\n color: green;\n}') - done() - }) - }) - - it('translates relative URLs and respects resolve alias', done => { - test({ - entry: 'resolve.vue', - resolve: { - alias: { - fixtures: path.resolve(__dirname, './fixtures') - } - }, - module: { - rules: [ - { test: /\.png$/, loader: 'file-loader?name=[name].[hash:6].[ext]' } - ] - } - }, (window, module) => { - const vnode = mockRender(module) - //
- // - // - //
- expect(vnode.children[0].tag).to.equal('img') - expect(vnode.children[0].data.attrs.src).to.equal('logo.c9e00e.png') - expect(vnode.children[2].tag).to.equal('img') - expect(vnode.children[2].data.attrs.src).to.equal('logo.c9e00e.png') - - const style = window.document.querySelector('style').textContent - expect(style).to.contain('html { background-image: url(logo.c9e00e.png);\n}') - expect(style).to.contain('body { background-image: url(logo.c9e00e.png);\n}') - done() - }) - }) - - it('transformToRequire option', done => { - test({ - entry: 'transform.vue', - module: { - rules: [ - { - test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, - loader: 'url-loader' - } - ] - } - }, (window, module) => { - function includeDataURL (s) { - return !!s.match(/\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*/i) - } - const vnode = mockRender(module) - // img tag - expect(includeDataURL(vnode.children[0].data.attrs.src)).to.equal(true) - // image tag (SVG) - expect(includeDataURL(vnode.children[2].children[0].data.attrs['xlink:href'])).to.equal(true) - const style = window.document.querySelector('style').textContent - - const dataURL = vnode.children[0].data.attrs.src - - // image tag with srcset - expect(vnode.children[4].data.attrs.srcset).to.equal(dataURL) - expect(vnode.children[6].data.attrs.srcset).to.equal(dataURL + ' 2x') - // image tag with multiline srcset - expect(vnode.children[8].data.attrs.srcset).to.equal(dataURL + ', ' + dataURL + ' 2x') - expect(vnode.children[10].data.attrs.srcset).to.equal(dataURL + ' 2x, ' + dataURL) - expect(vnode.children[12].data.attrs.srcset).to.equal(dataURL + ' 2x, ' + dataURL + ' 3x') - expect(vnode.children[14].data.attrs.srcset).to.equal(dataURL + ', ' + dataURL + ' 2x, ' + dataURL + ' 3x') - expect(vnode.children[16].data.attrs.srcset).to.equal(dataURL + ' 2x, ' + dataURL + ' 3x') - - // style - expect(includeDataURL(style)).to.equal(true) - done() - }) - }) - - it('postcss options', done => { - test({ - entry: 'postcss.vue', - vue: { - postcss: { - options: { - parser: require('sugarss') - } - } - } - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') - done() - }) - }) - - it('load postcss config file', done => { - fs.writeFileSync('.postcssrc', JSON.stringify({ parser: 'sugarss' })) - test({ - entry: 'postcss.vue' - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') - fs.unlinkSync('.postcssrc') - done() - }) - }) - - it('load cascading postcss config file', done => { - fs.writeFileSync('.postcssrc', JSON.stringify({ parser: 'sugarss' })) - test({ - entry: 'sub/postcss-cascade.vue', - vue: { postcss: { cascade: true }} - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('display: -webkit-box') - fs.unlinkSync('.postcssrc') - done() - }) - }) - - it('load postcss config file by path', done => { - fs.writeFileSync('test/.postcssrc', JSON.stringify({ parser: 'sugarss' })) - test({ - entry: 'postcss.vue', - vue: { - postcss: { - config: { - path: path.resolve('test') - } - } - } - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') - fs.unlinkSync('test/.postcssrc') - done() - }) - }) - - it('load postcss config file with js syntax error', done => { - fs.writeFileSync('.postcssrc.js', 'module.exports = { hello }') - test({ - entry: 'basic.vue' - }, (window, module, vueModule, instance, jsdomErr, webpackInfo) => { - const { stats: { compilation: { warnings, errors }}, err } = webpackInfo - expect(jsdomErr).to.be.null - expect(err).to.be.null - expect(errors).to.be.empty - expect(warnings.length).to.equal(1) - expect(warnings[0]).to.match(/Error loading PostCSS config\:/) - fs.unlinkSync('.postcssrc.js') - done() - }, true) - }) - - it('postcss lang', done => { - test({ - entry: 'postcss-lang.vue' - }, (window) => { - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('h1 {\n color: red;\n font-size: 14px\n}') - done() - }) - }) - - it('transpile ES2015 features in template', done => { - test({ - entry: 'es2015.vue' - }, (window, module) => { - const vnode = mockRender(module, { - a: 'hello', - b: true - }) - //
- expect(vnode.tag).to.equal('div') - expect(vnode.data.class['test-hello']).to.equal(true) - expect(vnode.data.class['b']).to.equal(true) - done() - }) - }) - - it('allows to export extended constructor', done => { - test({ - entry: 'extend.vue' - }, (window, Module) => { - // extend.vue should export Vue constructor - const vnode = mockRender(Module.options, { - msg: 'success' - }) - expect(vnode.tag).to.equal('div') - expect(vnode.children[0].text).to.equal('success') - expect(new Module().msg === 'success') - done() - }) - }) - - it('css-modules', done => { - function testWithIdent (localIdentName, regexToMatch, cb) { - test({ - entry: 'css-modules.vue', - vue: { - cssModules: localIdentName && { - localIdentName: localIdentName - } - } - }, (window, module, raw, instance) => { - // get local class name - const className = instance.style.red - expect(className).to.match(regexToMatch) - - // class name in style - let style = [].slice.call(window.document.querySelectorAll('style')).map((style) => { - return style.textContent - }).join('\n') - style = normalizeNewline(style) - expect(style).to.contain('.' + className + ' {\n color: red;\n}') - - // animation name - const match = style.match(/@keyframes\s+(\S+)\s+{/) - expect(match).to.have.length(2) - const animationName = match[1] - expect(animationName).to.not.equal('fade') - expect(style).to.contain('animation: ' + animationName + ' 1s;') - - // default module + pre-processor + scoped - const anotherClassName = instance.$style.red - expect(anotherClassName).to.match(regexToMatch).and.not.equal(className) - const id = 'data-v-' + genId('css-modules.vue') - expect(style).to.contain('.' + anotherClassName + '[' + id + ']') - - cb() - }) - } - // default localIdentName - testWithIdent(undefined, /^red_\w{8}/, () => { - // specified localIdentName - const ident = '[path][name]---[local]---[hash:base64:5]' - const regex = /css-modules---red---\w{5}/ - testWithIdent(ident, regex, done) - }) - }) - - it('css-modules in SSR', done => { - bundle({ - entry: 'css-modules.vue', - target: 'node', - output: Object.assign({}, globalConfig.output, { - libraryTarget: 'commonjs2' - }) - }, (code, warnings) => { - // http://stackoverflow.com/questions/17581830/load-node-js-module-from-string-in-memory - function requireFromString (src, filename) { - const Module = module.constructor - const m = new Module() - m._compile(src, filename) - return m.exports - } - - const output = interopDefault(requireFromString(code, './test.build.js')) - const mockInstance = {} - - output.beforeCreate.forEach(hook => hook.call(mockInstance)) - expect(mockInstance.style.red).to.exist - - done() - }) - }) - - it('should allow adding custom html loaders', done => { - test({ - entry: 'markdown.vue', - vue: { - loaders: { - html: 'marked' - } - } - }, (window, module) => { - const vnode = mockRender(module, { - msg: 'hi' - }) - //

{{msg}}

- expect(vnode.tag).to.equal('h2') - expect(vnode.data.attrs.id).to.equal('-msg-') - expect(vnode.children[0].text).to.equal('hi') - done() - }) - }) - - it('support chaining with other loaders', done => { - const mockLoaderPath = require.resolve('./mock-loaders/js') - test({ - entry: 'basic.vue', - modify: config => { - config.module.rules[0].loader = loaderPath + '!' + mockLoaderPath - } - }, (window, module) => { - expect(module.data().msg).to.equal('Changed!') - done() - }) - }) - - it('SSR style and moduleId extraction', done => { - bundle({ - target: 'node', - entry: './test/fixtures/ssr-style.js', - output: { - path: '/', - filename: 'test.build.js', - libraryTarget: 'commonjs2' - }, - externals: ['vue'] - }, code => { - const renderer = SSR.createBundleRenderer(code, { - basedir: __dirname, - runInNewContext: 'once' - }) - const context = { - _registeredComponents: new Set() - } - renderer.renderToString(context, (err, res) => { - if (err) return done(err) - expect(res).to.contain('data-server-rendered') - expect(res).to.contain('

Hello

') - expect(res).to.contain('Hello from Component A!') - expect(res).to.contain('
functional
') - // from main component - expect(context.styles).to.contain('h1 { color: green;') - // from imported child component - expect(context.styles).to.contain('comp-a h2 {\n color: #f00;') - // from imported css file - expect(context.styles).to.contain('h1 { color: red;') - // from imported functional component - expect(context.styles).to.contain('.foo { color: red;') - // collect component identifiers during render - expect(Array.from(context._registeredComponents).length).to.equal(3) - done() - }) - }) - }) - - it('extract custom blocks to a separate file', done => { - bundle({ - entry: 'custom-language.vue', - vue: { - loaders: { - 'documentation': ExtractTextPlugin.extract('raw-loader') - } - }, - plugins: [ - new ExtractTextPlugin('doc.md') - ] - }, (code, warnings) => { - let unitTest = mfs.readFileSync('/doc.md').toString() - unitTest = normalizeNewline(unitTest) - expect(unitTest).to.contain('This is example documentation for a component.') - done() - }) - }) - - it('add custom blocks to the webpack output', done => { - bundle({ - entry: 'custom-language.vue', - vue: { - loaders: { - 'unit-test': 'babel-loader' - } - } - }, (code, warnings) => { - expect(code).to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') - done() - }) - }) - - it('custom blocks work with src imports', done => { - bundle({ - entry: 'custom-import.vue', - vue: { - loaders: { - 'unit-test': 'babel-loader' - } - } - }, (code, warnings) => { - expect(code).to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') - done() - }) - }) - - it('passes Component to custom block loaders', done => { - const mockLoaderPath = require.resolve('./mock-loaders/docs') - test({ - entry: 'custom-language.vue', - vue: { - loaders: { - 'documentation': mockLoaderPath - } - } - }, (window, module) => { - expect(module.__docs).to.contain('This is example documentation for a component.') - done() - }) - }) - - it('custom blocks can be ignored', done => { - bundle({ - entry: 'custom-language.vue' - }, (code, warnings) => { - expect(code).not.to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') - done() - }) - }) - - it('custom blocks with ES module default export', done => { - test({ - entry: 'custom-blocks.vue', - vue: { - loaders: { - esm: require.resolve('./mock-loaders/identity') - } - } - }, (window, module) => { - // option added by custom block code - expect(module.foo).to.equal(1) - done() - }) - }) - - it('passes attributes as options to the loader', done => { - bundle({ - entry: 'custom-options.vue', - vue: { - loaders: { - 'unit-test': 'babel-loader!skeleton-loader' - } - }, - plugins: [ - new webpack.LoaderOptionsPlugin({ - options: { - skeletonLoader: { - procedure: (content, sourceMap, callback, options) => { - expect(options.foo).to.equal('bar') - expect(options.opt2).to.equal('value2') - - // Return the content to output. - return content - } - } - } - }) - ] - }, (code, warnings) => { - expect(code).to.contain('describe(\'example\', function () {\n it(\'basic\', function (done) {\n done();\n });\n})') - done() - }) - }) - - it('pre/post loaders', done => { - test({ - entry: 'basic.vue', - vue: { - preLoaders: { - js: require.resolve('./mock-loaders/js'), - css: require.resolve('./mock-loaders/css') - }, - postLoaders: { - html: require.resolve('./mock-loaders/html') - } - } - }, (window, module) => { - const vnode = mockRender(module, { - msg: 'hi' - }) - //

{{msg}}

- expect(vnode.tag).to.equal('h2') - expect(vnode.data.staticClass).to.equal('green') - expect(vnode.children[0].text).to.equal('hi') - - expect(module.data().msg).to.contain('Changed!') - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('comp-a h2 {\n color: #00f;\n}') - done() - }) - }) - - it('pre/post loaders for custom blocks', done => { - test({ - entry: 'custom-blocks.vue', - vue: { - preLoaders: { - i18n: require.resolve('./mock-loaders/yaml') - }, - loaders: { - i18n: require.resolve('./mock-loaders/i18n'), - blog: 'marked' - }, - postLoaders: { - blog: require.resolve('./mock-loaders/blog') - } - } - }, (window, module) => { - const vnode = mockRender(module, { - msg: JSON.parse(module.__i18n).en.hello, - blog: module.__blog - }) - expect(vnode.children[0].children[0].text).to.equal('hello world') - expect(vnode.children[2].data.domProps.innerHTML).to.equal('

foo

') - done() - }) - }) - - it('custom compiler modules', done => { - test({ - entry: 'custom-module.vue', - vue: { - compilerModules: [ - { - postTransformNode: el => { - if (el.staticStyle) { - el.staticStyle = `$processStyle(${el.staticStyle})` - } - if (el.styleBinding) { - el.styleBinding = `$processStyle(${el.styleBinding})` - } - } - } - ] - } - }, (window, module) => { - const results = [] - // var vnode = - mockRender( - Object.assign(module, { methods: { $processStyle: style => results.push(style) }}), - { transform: 'translateX(10px)' } - ) - expect(results).to.deep.equal([ - { 'flex-direction': 'row' }, - { 'transform': 'translateX(10px)' } - ]) - done() - }) - }) - - it('custom compiler directives', done => { - test({ - entry: 'custom-directive.vue', - vue: { - compilerDirectives: { - i18n (el, dir) { - if (dir.name === 'i18n' && dir.value) { - el.i18n = dir.value - if (!el.props) { - el.props = [] - } - el.props.push({ name: 'textContent', value: `_s(${JSON.stringify(dir.value)})` }) - } - } - } - } - }, (window, module) => { - const vnode = mockRender(module) - expect(vnode.data.domProps.textContent).to.equal('keypath') - done() - }) - }) - - it('functional component with styles', done => { - test({ - entry: 'functional-style.vue' - }, (window, module, rawModule) => { - expect(module.functional).to.equal(true) - const vnode = mockRender(module) - //
hi
- expect(vnode.tag).to.equal('div') - expect(vnode.data.class).to.equal('foo') - expect(vnode.children[0].text).to.equal('functional') - - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('.foo { color: red;\n}') - done() - }) - }) - - it('template with comments', done => { - test({ - entry: 'template-comment.vue' - }, (window, module, rawModule) => { - expect(module.comments).to.equal(true) - const vnode = mockRender(module, { - msg: 'hi' - }) - expect(vnode.tag).to.equal('div') - expect(vnode.children.length).to.equal(2) - expect(vnode.children[0].data.staticClass).to.equal('red') - expect(vnode.children[0].children[0].text).to.equal('hi') - expect(vnode.children[1].isComment).to.true - expect(vnode.children[1].text).to.equal(' comment here ') - done() - }) - }) - - it('functional template', done => { - test({ - entry: 'functional-root.vue', - vue: { - preserveWhitespace: false - } - }, (window, module) => { - expect(module.components.Functional._compiled).to.equal(true) - expect(module.components.Functional.functional).to.equal(true) - expect(module.components.Functional.staticRenderFns).to.exist - expect(module.components.Functional.render).to.be.a('function') - - const vnode = mockRender(module, { - fn () { - done() - } - }).children[0] - - // Basic vnode - expect(vnode.children[0].data.staticClass).to.equal('red') - expect(vnode.children[0].children[0].text).to.equal('hello') - // Default slot vnode - expect(vnode.children[1].tag).to.equal('span') - expect(vnode.children[1].children[0].text).to.equal('hello') - // Named slot vnode - expect(vnode.children[2].tag).to.equal('div') - expect(vnode.children[2].children[0].text).to.equal('Second slot') - // // Scoped slot vnode - expect(vnode.children[3].text).to.equal('hello') - // // Static content vnode - expect(vnode.children[4].tag).to.equal('div') - expect(vnode.children[4].children[0].text).to.equal('Some ') - expect(vnode.children[4].children[1].tag).to.equal('span') - expect(vnode.children[4].children[1].children[0].text).to.equal('text') - // // v-if vnode - expect(vnode.children[5].text).to.equal('') - - vnode.children[6].data.on.click() - }) - }) - - it('named exports', done => { - test({ - entry: 'named-exports.vue' - }, (window, _, rawModule) => { - expect(rawModule.default.name).to.equal('named-exports') - expect(rawModule.foo()).to.equal(1) - done() - }) - }) - - it('multiple rule definitions', done => { - test({ - modify: config => { - // remove default rule - config.module.rules.shift() - }, - entry: './test/fixtures/multiple-rules.js', - module: { - rules: [ - { - test: /\.vue$/, - oneOf: [ - { - include: /-1\.vue$/, - loader: loaderPath, - options: { - postcss: [ - css => { - css.walkDecls('font-size', decl => { - decl.value = `${parseInt(decl.value, 10) * 2}px` - }) - } - ], - compilerModules: [{ - postTransformNode: el => { - el.staticClass = '"multiple-rule-1"' - } - }] - } - }, - { - include: /-2\.vue$/, - loader: loaderPath, - options: { - postcss: [ - css => { - css.walkDecls('font-size', decl => { - decl.value = `${parseInt(decl.value, 10) / 2}px` - }) - } - ], - compilerModules: [{ - postTransformNode: el => { - el.staticClass = '"multiple-rule-2"' - } - }] - } - } - ] - } - ] - } - }, (window, module) => { - const vnode1 = mockRender(window.rules[0]) - const vnode2 = mockRender(window.rules[1]) - expect(vnode1.data.staticClass).to.equal('multiple-rule-1') - expect(vnode2.data.staticClass).to.equal('multiple-rule-2') - const styles = window.document.querySelectorAll('style') - const expr = /\.multiple-rule-\d\s*\{\s*font-size:\s*([.0-9]+)px;/ - for (let i = 0, l = styles.length; i < l; i++) { - const content = styles[i].textContent - if (expr.test(content)) { - expect(parseFloat(RegExp.$1)).to.be.equal(14) - } - } - done() - }) - }) - - it('thread mode', done => { - test({ - entry: 'basic.vue', - vue: { - threadMode: true - } - }, (window, module, rawModule) => { - const vnode = mockRender(module, { - msg: 'hi' - }) - - //

{{msg}}

- expect(vnode.tag).to.equal('h2') - expect(vnode.data.staticClass).to.equal('red') - expect(vnode.children[0].text).to.equal('hi') - - expect(module.data().msg).to.contain('Hello from Component A!') - let style = window.document.querySelector('style').textContent - style = normalizeNewline(style) - expect(style).to.contain('comp-a h2 {\n color: #f00;\n}') - done() - }) - }) -})