├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .release.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── build.js ├── utils │ ├── index.js │ ├── log.js │ └── write.js ├── webpack.config.base.js ├── webpack.config.browser.js ├── webpack.config.dev.js └── webpack.config.dll.js ├── circle.yml ├── package.json ├── src ├── Button.vue ├── Card │ ├── Card.vue │ ├── CardActions.js │ ├── CardHorizontalBlock.js │ ├── CardMedia.js │ ├── CardPrimary.js │ ├── CardSubtitle.js │ ├── CardSupportingText.js │ └── CardTitle.js ├── Checkbox.vue ├── Dialog │ ├── Dialog.vue │ └── DialogHeaderTitle.js ├── Drawer │ ├── Drawer.vue │ ├── DrawerHeader.vue │ ├── DrawerNav.js │ └── DrawerNavItem.vue ├── Fab.vue ├── List │ ├── List.vue │ ├── ListDivider.vue │ └── ListItem.vue ├── Toolbar │ ├── Toolbar.vue │ ├── ToolbarRow.js │ └── ToolbarSection.js ├── index.js ├── ripple.js └── utils │ └── index.js ├── test ├── .eslintrc ├── helpers │ ├── Test.vue │ ├── index.js │ ├── style.css │ ├── utils.js │ └── wait-for-update.js ├── index.js ├── karma.conf.js ├── specs │ ├── Button.spec.js │ ├── Card.spec.js │ ├── Checkbox.spec.js │ ├── Dialog.spec.js │ ├── Drawer.spec.js │ ├── Fab.spec.js │ ├── List.spec.js │ ├── Toolbar.spec.js │ ├── VueMdc.spec.js │ └── ripple.spec.js └── visual.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | globals: { 4 | HTMLElement: true, 5 | }, 6 | extends: 'posva', 7 | // add your custom rules here 8 | 'rules': { 9 | // allow async-await 10 | 'generator-star-spacing': 0, 11 | // allow debugger during development 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 13 | 'comma-dangle': ['error', 'always-multiline'], 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/coverage 5 | dist 6 | yarn-error.log 7 | reports 8 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": { 3 | "tagName": "v%s", 4 | "commitMessage": "🔖 %s" 5 | }, 6 | "github": { 7 | "release": true, 8 | "releaseName": "Release %s", 9 | "tokenRef": "GITHUB_TOKEN" 10 | }, 11 | "npm": { 12 | "publish": true 13 | }, 14 | "changelogCommand": "git log --pretty=format:'- %s (%h)' [REV_RANGE]" 15 | } 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/posva/vue-mdc). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Keep the same style** - eslint will automatically be ran before committing 11 | 12 | - **Tip** to pass lint tests easier use the `npm run lint:fix` command 13 | 14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 15 | 16 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create feature branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure your commits message means something 25 | 26 | 27 | ## Running Tests 28 | 29 | Launch visual tests and watch the components at the same time 30 | 31 | ``` bash 32 | $ npm run dev 33 | ``` 34 | 35 | 36 | **Happy coding**! 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Eduardo San Martin Morote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **If you're looking for vue-mdl, check the [mdl branch](https://github.com/posva/vue-mdc/tree/mdl). To learn about what's happening, check [#139](https://github.com/posva/vue-mdc/issues/139)** 2 | 3 | --- 4 | 5 |

6 | 7 |
8 | 9 | last version 10 | 11 | 12 | Circle CI 13 | 14 | 15 | Codecov Coverage 16 | 17 | 18 | Discord 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

31 | 32 | --- 33 | 34 | # Vue Material Components web 35 | 36 | > Material Components Web for Vue.js 37 | 38 | This is the adaptation of [Material Components web](https://github.com/material-components/material-components-web) for Vue.js. Keeping them fast, easy to use and, SSR ready. 39 | 40 | The docs will be the demo, until then you can [check the test folder](test/specs) and the [Material Components web demo](http://material-components-web.appspot.com/) 41 | 42 | ## Installation 43 | 44 | ```bash 45 | $ npm install --save vue-mdc 46 | # or 47 | $ yarn add vue-mdc 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### Bundler (Webpack, Rollup) 53 | 54 | ```js 55 | import Vue from 'vue' 56 | import VueMdc from 'vue-mdc' 57 | // You need a specific loader for CSS files like https://github.com/webpack/css-loader 58 | import 'vue-mdc/dist/vue-mdc.css' 59 | 60 | Vue.use(VueMdc) 61 | ``` 62 | 63 | ### Browser 64 | 65 | ```html 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | ### Docs 77 | 78 | WIP 😁 79 | 80 | ## Development 81 | 82 | ### Launch visual tests 83 | 84 | ```bash 85 | npm run dev 86 | ``` 87 | 88 | ### Launch Karma with coverage 89 | 90 | ```bash 91 | npm run dev:coverage 92 | ``` 93 | 94 | ### Build 95 | 96 | Bundle the js and css of to the `dist` folder: 97 | 98 | ```bash 99 | npm run build 100 | ``` 101 | 102 | 103 | ## Publishing 104 | 105 | The `prepublish` hook will ensure dist files are created before publishing. This 106 | way you don't need to commit them in your repository. 107 | 108 | ```bash 109 | # Bump the version first 110 | # It'll also commit it and create a tag 111 | npm version 112 | # Push the bumped package and tags 113 | git push --follow-tags 114 | # Ship it 🚀 115 | npm publish 116 | ``` 117 | 118 | ## License 119 | 120 | [MIT](http://opensource.org/licenses/MIT) 121 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp') 2 | const rollup = require('rollup').rollup 3 | const vue = require('rollup-plugin-vue') 4 | const buble = require('rollup-plugin-buble') 5 | const replace = require('rollup-plugin-replace') 6 | const cjs = require('rollup-plugin-commonjs') 7 | const node = require('rollup-plugin-node-resolve') 8 | const scss = require('rollup-plugin-scss') 9 | const uglify = require('uglify-js') 10 | 11 | // Make sure dist dir exists 12 | mkdirp('dist') 13 | 14 | const { 15 | logError, 16 | write, 17 | banner, 18 | name, 19 | moduleName, 20 | version, 21 | } = require('./utils') 22 | 23 | function rollupBundle ({ env, entry }) { 24 | return rollup({ 25 | input: entry || 'src/index.js', 26 | plugins: [ 27 | scss({ output: false }), 28 | node({ 29 | extensions: ['.js', '.jsx', '.vue'], 30 | }), 31 | cjs(), 32 | vue({ compileTemplate: true, css: false }), 33 | replace(Object.assign({ 34 | __VERSION__: version, 35 | }, env)), 36 | buble({ 37 | objectAssign: 'Object.assign', 38 | }), 39 | ], 40 | }) 41 | } 42 | 43 | const bundleOptions = { 44 | banner, 45 | exports: 'named', 46 | format: 'umd', 47 | name: moduleName, 48 | } 49 | 50 | function createBundle ({ name, env, entry, format }) { 51 | return rollupBundle({ 52 | env, 53 | entry, 54 | }).then(function (bundle) { 55 | const options = Object.assign({}, bundleOptions) 56 | if (format) options.format = format 57 | return bundle.generate(options).then(({ code }) => { 58 | if (/min$/.test(name)) { 59 | const minified = uglify.minify(code, { 60 | output: { 61 | preamble: banner, 62 | ascii_only: true, // eslint-disable-line camelcase 63 | }, 64 | }).code 65 | return write(`dist/${name}.js`, minified) 66 | } else { 67 | return write(`dist/${name}.js`, code) 68 | } 69 | }) 70 | }).catch(logError) 71 | } 72 | 73 | // Browser bundle (can be used with script) 74 | createBundle({ 75 | name: `${name}`, 76 | env: { 77 | 'process.env.NODE_ENV': '"development"', 78 | }, 79 | }) 80 | 81 | // Commonjs bundle (preserves process.env.NODE_ENV) so 82 | // the user can replace it in dev and prod mode 83 | createBundle({ 84 | name: `${name}.esm`, 85 | env: {}, 86 | format: 'es', 87 | }) 88 | 89 | createBundle({ 90 | name: `${name}.common`, 91 | env: {}, 92 | format: 'cjs', 93 | }) 94 | 95 | // Minified version for browser 96 | createBundle({ 97 | name: `${name}.min`, 98 | env: { 99 | 'process.env.NODE_ENV': '"production"', 100 | }, 101 | }) 102 | -------------------------------------------------------------------------------- /build/utils/index.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | const { join } = require('path') 3 | 4 | const { 5 | red, 6 | logError, 7 | } = require('./log') 8 | 9 | const uppercamelcase = require('uppercamelcase') 10 | 11 | exports.write = require('./write') 12 | 13 | const { 14 | author, 15 | name, 16 | version, 17 | dllPlugin, 18 | } = require('../../package.json') 19 | 20 | const authorName = author.replace(/\s+<.*/, '') 21 | const minExt = process.env.NODE_ENV === 'production' ? '.min' : '' 22 | 23 | exports.author = authorName 24 | exports.version = version 25 | exports.dllName = dllPlugin.name 26 | exports.name = name 27 | exports.moduleName = uppercamelcase(name) 28 | exports.filename = name + minExt 29 | exports.banner = `/*! 30 | * ${name} v${version} 31 | * (c) ${new Date().getFullYear()} ${authorName} 32 | * Released under the MIT License. 33 | */ 34 | ` 35 | 36 | // log.js 37 | exports.red = red 38 | exports.logError = logError 39 | 40 | // It'd be better to add a sass property to the vue-loader options 41 | // but it simply don't work 42 | const sassOptions = { 43 | includePaths: [ 44 | join(__dirname, '../../node_modules'), 45 | ], 46 | } 47 | 48 | // don't extract css in test mode 49 | const nullLoader = /(common|esm)/.test(process.env.NODE_ENV) ? 'null-loader!' : '' 50 | exports.vueLoaders = 51 | process.env.BABEL_ENV === 'test' ? { 52 | scss: `css-loader!sass-loader?${JSON.stringify(sassOptions)}`, 53 | } : { 54 | scss: ExtractTextPlugin.extract( 55 | `${nullLoader}css-loader!sass-loader?${JSON.stringify(sassOptions)}` 56 | ), 57 | } 58 | -------------------------------------------------------------------------------- /build/utils/log.js: -------------------------------------------------------------------------------- 1 | function logError (e) { 2 | console.log(e) 3 | } 4 | 5 | function blue (str) { 6 | return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m` 7 | } 8 | 9 | function green (str) { 10 | return `\x1b[1m\x1b[32m${str}\x1b[39m\x1b[22m` 11 | } 12 | 13 | function red (str) { 14 | return `\x1b[1m\x1b[31m${str}\x1b[39m\x1b[22m` 15 | } 16 | 17 | function yellow (str) { 18 | return `\x1b[1m\x1b[33m${str}\x1b[39m\x1b[22m` 19 | } 20 | 21 | module.exports = { 22 | blue, 23 | green, 24 | red, 25 | yellow, 26 | logError, 27 | } 28 | -------------------------------------------------------------------------------- /build/utils/write.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const { blue } = require('./log.js') 4 | 5 | function write (dest, code) { 6 | return new Promise(function (resolve, reject) { 7 | fs.writeFile(dest, code, function (err) { 8 | if (err) return reject(err) 9 | console.log(blue(dest) + ' ' + getSize(code)) 10 | resolve(code) 11 | }) 12 | }) 13 | } 14 | 15 | function getSize (code) { 16 | return (code.length / 1024).toFixed(2) + 'kb' 17 | } 18 | 19 | module.exports = write 20 | -------------------------------------------------------------------------------- /build/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | const { resolve } = require('path') 4 | 5 | const { 6 | banner, 7 | filename, 8 | version, 9 | vueLoaders, 10 | } = require('./utils') 11 | 12 | const plugins = [ 13 | new webpack.DefinePlugin({ 14 | '__VERSION__': JSON.stringify(version), 15 | 'process.env.NODE_ENV': '"test"', 16 | }), 17 | new webpack.BannerPlugin({ banner, raw: true, entryOnly: true }), 18 | new ExtractTextPlugin({ 19 | filename: `${filename}.css`, 20 | // Don't extract css in test mode 21 | disable: /^(common|test|esm)$/.test(process.env.NODE_ENV), 22 | }), 23 | ] 24 | 25 | module.exports = { 26 | output: { 27 | pathinfo: true, 28 | path: resolve(__dirname, '../dist'), 29 | filename: `${filename}.common.js`, 30 | }, 31 | entry: './src/index.js', 32 | resolve: { 33 | extensions: ['.js', '.vue', '.jsx', 'css'], 34 | alias: { 35 | 'src': resolve(__dirname, '../src'), 36 | }, 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /.jsx?$/, 42 | use: 'babel-loader', 43 | include: [ 44 | resolve(__dirname, '../node_modules/@material'), 45 | resolve(__dirname, '../src'), 46 | resolve(__dirname, '../test'), 47 | ], 48 | }, 49 | { 50 | test: /\.vue$/, 51 | loader: 'vue-loader', 52 | options: { 53 | loaders: vueLoaders, 54 | }, 55 | }, 56 | ], 57 | }, 58 | plugins, 59 | } 60 | -------------------------------------------------------------------------------- /build/webpack.config.browser.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 3 | const base = require('./webpack.config.base') 4 | const { resolve } = require('path') 5 | const { 6 | filename, 7 | moduleName, 8 | vueLoaders, 9 | } = require('./utils') 10 | 11 | module.exports = merge(base, { 12 | output: { 13 | pathinfo: false, 14 | filename: `${filename}.js`, 15 | library: moduleName, 16 | libraryTarget: 'umd', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /.scss$/, 22 | use: vueLoaders.scss, 23 | include: [ 24 | resolve(__dirname, '../node_modules/@material'), 25 | resolve(__dirname, '../src'), 26 | ], 27 | }, 28 | ], 29 | }, 30 | plugins: [ 31 | new BundleAnalyzerPlugin({ 32 | analyzerMode: 'static', 33 | openAnalyzer: false, 34 | reportFilename: resolve(__dirname, `../reports/${process.env.NODE_ENV}.html`), 35 | }), 36 | ], 37 | }) 38 | -------------------------------------------------------------------------------- /build/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 6 | const DashboardPlugin = require('webpack-dashboard/plugin') 7 | const base = require('./webpack.config.base') 8 | const { resolve, join } = require('path') 9 | const { existsSync } = require('fs') 10 | const { 11 | dllName, 12 | logError, 13 | red, 14 | vueLoaders, 15 | } = require('./utils') 16 | 17 | const rootDir = resolve(__dirname, '../test') 18 | const buildPath = resolve(rootDir, 'dist') 19 | 20 | if (!existsSync(join(buildPath, dllName) + '.dll.js')) { 21 | logError(red('The DLL manifest is missing. Please run `npm run build:dll` (Quit this with `q`)')) 22 | process.exit(0) 23 | } 24 | 25 | const dllManifest = require( 26 | join(buildPath, dllName) + '.json' 27 | ) 28 | 29 | module.exports = merge(base, { 30 | entry: { 31 | tests: resolve(rootDir, 'visual.js'), 32 | }, 33 | output: { 34 | path: buildPath, 35 | filename: '[name].js', 36 | chunkFilename: '[id].js', 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /.scss$/, 42 | loader: vueLoaders.scss, 43 | include: [ 44 | resolve(__dirname, '../node_modules/@material'), 45 | resolve(__dirname, '../src'), 46 | ], 47 | }, 48 | ], 49 | }, 50 | plugins: [ 51 | new webpack.DllReferencePlugin({ 52 | context: join(__dirname, '..'), 53 | manifest: dllManifest, 54 | }), 55 | new HtmlWebpackPlugin({ 56 | chunkSortMode: 'dependency', 57 | }), 58 | new AddAssetHtmlPlugin({ 59 | filepath: require.resolve( 60 | join(buildPath, dllName) + '.dll.js' 61 | ), 62 | }), 63 | new webpack.optimize.CommonsChunkPlugin({ 64 | name: 'vendor', 65 | minChunks (module, count) { 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf(join(__dirname, '../node_modules/')) === 0 70 | ) 71 | }, 72 | }), 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'manifest', 75 | chunks: ['vendor'], 76 | }), 77 | new DashboardPlugin(), 78 | new BundleAnalyzerPlugin({ 79 | analyzerMode: 'static', 80 | openAnalyzer: false, 81 | reportFilename: resolve(__dirname, `../reports/${process.env.NODE_ENV}.html`), 82 | }), 83 | ], 84 | devtool: '#eval-source-map', 85 | devServer: { 86 | inline: true, 87 | stats: { 88 | colors: true, 89 | chunks: false, 90 | cached: false, 91 | }, 92 | contentBase: buildPath, 93 | }, 94 | performance: { 95 | hints: false, 96 | }, 97 | }) 98 | -------------------------------------------------------------------------------- /build/webpack.config.dll.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path') 2 | const webpack = require('webpack') 3 | const pkg = require('../package.json') 4 | 5 | const rootDir = resolve(__dirname, '../test') 6 | const buildPath = resolve(rootDir, 'dist') 7 | 8 | const entry = {} 9 | entry[pkg.dllPlugin.name] = pkg.dllPlugin.include 10 | 11 | module.exports = { 12 | devtool: '#source-map', 13 | entry, 14 | output: { 15 | path: buildPath, 16 | filename: '[name].dll.js', 17 | library: '[name]', 18 | }, 19 | plugins: [ 20 | new webpack.DllPlugin({ 21 | name: '[name]', 22 | path: join(buildPath, '[name].json'), 23 | }), 24 | ], 25 | performance: { 26 | hints: false, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - gh-pages 5 | machine: 6 | node: 7 | version: 6 8 | environment: 9 | PATH: "${PATH}:${HOME}/.yarn/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 10 | dependencies: 11 | pre: 12 | - | 13 | if [[ ! -e ~/.yarn/bin/yarn ]]; then 14 | curl -o- -L https://yarnpkg.com/install.sh | bash -s 15 | fi 16 | cache_directories: 17 | - ~/.yarn 18 | - ~/.yarn-cache 19 | override: 20 | - yarn install 21 | test: 22 | pre: 23 | - npm rebuild node-sass 24 | - yarn build:dll 25 | override: 26 | - yarn test 27 | post: 28 | - bash <(curl -s https://codecov.io/bash) 29 | - cat test/coverage/lcov.info | ./node_modules/.bin/codacy-coverage 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-mdc", 3 | "version": "0.0.10", 4 | "description": "Material Components Web for Vue.js", 5 | "author": "Eduardo San Martin Morote ", 6 | "main": "dist/vue-mdc.common.js", 7 | "module": "dist/vue-mdc.esm.js", 8 | "browser": "dist/vue-mdc.js", 9 | "unpkg": "dist/vue-mdc.js", 10 | "style": "dist/vue-mdc.css", 11 | "files": [ 12 | "dist", 13 | "src" 14 | ], 15 | "scripts": { 16 | "prebuild": "rimraf dist", 17 | "build": "yon run build:browser && yon run build:browser:min && yon run build:esm", 18 | "build:esm": "cross-env NODE_ENV=esm node build/build.js", 19 | "build:browser:base": "webpack --config build/webpack.config.browser.js --progress --hide-modules", 20 | "build:browser": "cross-env NODE_ENV=browser yon run build:browser:base", 21 | "build:browser:min": "cross-env NODE_ENV=production yon build:browser:base -- -p", 22 | "build:dll": "webpack --progress --config build/webpack.config.dll.js", 23 | "lint": "eslint --ext js --ext jsx --ext vue src test/**/*.spec.js test/*.js build", 24 | "lint:fix": "yon run lint:js -- --fix", 25 | "lint:staged": "lint-staged", 26 | "pretest": "true || yon run lint", 27 | "test": "cross-env BABEL_ENV=test karma start test/karma.conf.js --single-run", 28 | "dev": "webpack-dashboard -- webpack-dev-server --config build/webpack.config.dev.js --open", 29 | "dev:coverage": "cross-env BABEL_ENV=test karma start test/karma.conf.js", 30 | "prepublish": "! in-publish || yon run build" 31 | }, 32 | "lint-staged": { 33 | "*.{vue,jsx,js}": [ 34 | "true || eslint --fix" 35 | ] 36 | }, 37 | "pre-commit": "lint:staged", 38 | "devDependencies": { 39 | "add-asset-html-webpack-plugin": "^2.1.2", 40 | "babel-core": "^6.26.0", 41 | "babel-eslint": "^8.0.1", 42 | "babel-helper-vue-jsx-merge-props": "^2.0.2", 43 | "babel-loader": "^7.1.2", 44 | "babel-plugin-istanbul": "^4.1.5", 45 | "babel-plugin-syntax-jsx": "^6.18.0", 46 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 47 | "babel-plugin-transform-vue-jsx": "^3.5.0", 48 | "babel-preset-env": "^1.6.0", 49 | "camelcase": "^4.1.0", 50 | "chai": ">= 3.5.0 && < 4.0.0", 51 | "chai-dom": "^1.5.0", 52 | "codacy-coverage": "^2.0.3", 53 | "cross-env": "^5.0.5", 54 | "css-loader": "^0.28.7", 55 | "eslint": "^4.8.0", 56 | "eslint-config-posva": "^1.3.1", 57 | "extract-text-webpack-plugin": "^3.0.0", 58 | "function-bind": "^1.1.1", 59 | "html-webpack-plugin": "^2.30.1", 60 | "in-publish": "^2.0.0", 61 | "karma": "^1.7.1", 62 | "karma-chai-dom": "^1.1.0", 63 | "karma-chrome-launcher": "^2.2.0", 64 | "karma-coverage": "^1.1.1", 65 | "karma-mocha": "^1.3.0", 66 | "karma-sinon-chai": "^1.3.2", 67 | "karma-sourcemap-loader": "^0.3.7", 68 | "karma-spec-reporter": "^0.0.31", 69 | "karma-webpack": "^2.0.4", 70 | "lint-staged": "^6.0.0", 71 | "material-components-web": "^0.24.0", 72 | "mocha": "^3.5.3", 73 | "mocha-css": "^1.0.1", 74 | "node-sass": "^4.5.3", 75 | "null-loader": "^0.1.1", 76 | "pre-commit": "^1.2.2", 77 | "rimraf": "^2.6.2", 78 | "rollup": "^0.52.0", 79 | "rollup-plugin-buble": "^0.18.0", 80 | "rollup-plugin-commonjs": "^8.2.1", 81 | "rollup-plugin-node-resolve": "^3.0.0", 82 | "rollup-plugin-replace": "^2.0.0", 83 | "rollup-plugin-scss": "^0.4.0", 84 | "rollup-plugin-vue": "^3.0.0", 85 | "sass-loader": "^6.0.6", 86 | "sinon": "^4.0.0", 87 | "sinon-chai": "^2.14.0", 88 | "style-loader": "^0.19.0", 89 | "uglify-js": "^3.1.3", 90 | "uppercamelcase": "^3.0.0", 91 | "vue": "^2.4.4", 92 | "vue-loader": "^13.0.5", 93 | "vue-template-compiler": "^2.4.4", 94 | "webpack": "^3.6.0", 95 | "webpack-bundle-analyzer": "^2.9.0", 96 | "webpack-dashboard": "^1.0.0-5", 97 | "webpack-dev-server": "^2.9.1", 98 | "webpack-merge": "^4.1.0", 99 | "yarn-or-npm": "^2.0.4" 100 | }, 101 | "dllPlugin": { 102 | "name": "visualTestingDeps", 103 | "include": [ 104 | "mocha/mocha.js", 105 | "style-loader!css-loader!mocha-css", 106 | "html-entities", 107 | "vue/dist/vue.js", 108 | "chai", 109 | "chai-dom", 110 | "sinon", 111 | "sinon-chai", 112 | "diff", 113 | "core-js/library", 114 | "url", 115 | "sockjs-client", 116 | "vue-style-loader/lib/addStylesClient.js", 117 | "events", 118 | "ansi-html", 119 | "text-encoding" 120 | ] 121 | }, 122 | "babel": { 123 | "presets": [ 124 | [ 125 | "env", 126 | { 127 | "targets": { 128 | "browsers": [ 129 | "last 2 versions", 130 | "ie >= 11" 131 | ] 132 | } 133 | } 134 | ] 135 | ], 136 | "plugins": [ 137 | "transform-vue-jsx", 138 | "transform-object-rest-spread" 139 | ], 140 | "env": { 141 | "test": { 142 | "plugins": [ 143 | "istanbul" 144 | ] 145 | } 146 | } 147 | }, 148 | "repository": { 149 | "type": "git", 150 | "url": "git+https://github.com/posva/vue-mdc.git" 151 | }, 152 | "bugs": { 153 | "url": "https://github.com/posva/vue-mdc/issues" 154 | }, 155 | "homepage": "https://github.com/posva/vue-mdc#readme", 156 | "license": { 157 | "type": "MIT", 158 | "url": "http://www.opensource.org/licenses/mit-license.php" 159 | }, 160 | "dependencies": { 161 | "decamelize": "^1.2.0" 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Button.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /src/Card/Card.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 74 | 75 | 77 | 78 | 84 | -------------------------------------------------------------------------------- /src/Card/CardActions.js: -------------------------------------------------------------------------------- 1 | import { propsToClasses } from '../utils' 2 | 3 | export default { 4 | functional: true, 5 | 6 | props: { 7 | vertical: Boolean, 8 | }, 9 | 10 | render (h, { data, children, props }) { 11 | const staticClass = propsToClasses('mdc-card__actions', props) 12 | data.staticClass = data.staticClass 13 | ? `${data.staticClass} ${staticClass}` 14 | : staticClass 15 | children.forEach(child => { 16 | if (child.tag === 'button') { 17 | child.data.staticClass += ' mdc-card__action' 18 | } 19 | }) 20 | return h('section', { 21 | ...data, 22 | }, children) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /src/Card/CardHorizontalBlock.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | render (h, { data, children }) { 5 | const staticClass = 'mdc-card__horizontal-block' 6 | data.staticClass = data.staticClass 7 | ? `${data.staticClass} ${staticClass}` 8 | : staticClass 9 | return h('div', { 10 | ...data, 11 | }, children) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/Card/CardMedia.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | props: { 5 | media: String, 6 | }, 7 | 8 | render (h, { data, children, props }) { 9 | const staticClass = 'mdc-card__media' 10 | data.staticClass = data.staticClass 11 | ? `${data.staticClass} ${staticClass}` 12 | : staticClass 13 | data.style = data.style || {} 14 | data.style['background-image'] = data.style['background-image'] || `url(${props.media})` 15 | 16 | return h('section', { 17 | ...data, 18 | }, children) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/Card/CardPrimary.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | render (h, { data, children }) { 5 | const staticClass = 'mdc-card__primary' 6 | data.staticClass = data.staticClass 7 | ? `${data.staticClass} ${staticClass}` 8 | : staticClass 9 | return h('section', { 10 | ...data, 11 | }, children) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/Card/CardSubtitle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | render (h, { data, children }) { 5 | const staticClass = 'mdc-card__subtitle' 6 | data.staticClass = data.staticClass 7 | ? `${data.staticClass} ${staticClass}` 8 | : staticClass 9 | return h('h2', { 10 | ...data, 11 | }, children) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/Card/CardSupportingText.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | render (h, { data, children }) { 5 | const staticClass = 'mdc-card__supporting-text' 6 | data.staticClass = data.staticClass 7 | ? `${data.staticClass} ${staticClass}` 8 | : staticClass 9 | return h('section', { 10 | ...data, 11 | }, children) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/Card/CardTitle.js: -------------------------------------------------------------------------------- 1 | import { propsToClasses } from '../utils' 2 | 3 | export default { 4 | functional: true, 5 | 6 | props: { 7 | large: Boolean, 8 | }, 9 | 10 | render (h, { data, children, props, parent }) { 11 | const staticClass = propsToClasses('mdc-card__title', props) 12 | data.staticClass = data.staticClass 13 | ? `${data.staticClass} ${staticClass}` 14 | : staticClass 15 | return h('h1', { 16 | ...data, 17 | }, children) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 55 | 56 | 58 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 108 | 109 | 111 | -------------------------------------------------------------------------------- /src/Dialog/DialogHeaderTitle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | render (h, { data, children }) { 5 | const staticClass = 'mdc-dialog__header__title' 6 | data.staticClass = data.staticClass 7 | ? `${data.staticClass} ${staticClass}` 8 | : staticClass 9 | return h('h2', { 10 | ...data, 11 | }, children) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/Drawer/Drawer.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 113 | 114 | 116 | 117 | 122 | -------------------------------------------------------------------------------- /src/Drawer/DrawerHeader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | -------------------------------------------------------------------------------- /src/Drawer/DrawerNav.js: -------------------------------------------------------------------------------- 1 | import List from '../List/List' 2 | 3 | export default { 4 | functional: true, 5 | 6 | props: { 7 | tag: { 8 | type: String, 9 | default: 'nav', 10 | }, 11 | }, 12 | 13 | render (h, { children, data, props }) { 14 | return h(List, { 15 | ...data, 16 | props, 17 | }, children) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/Drawer/DrawerNavItem.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /src/Fab.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 43 | -------------------------------------------------------------------------------- /src/List/List.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 32 | -------------------------------------------------------------------------------- /src/List/ListDivider.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/List/ListItem.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /src/Toolbar/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /src/Toolbar/ToolbarRow.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | render (h, { children, data }) { 5 | const staticClass = 'mdc-toolbar__row' 6 | data.staticClass = data.staticClass 7 | ? `${data.staticClass} ${staticClass}` 8 | : staticClass 9 | return h('div', { 10 | ...data, 11 | }, children) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/Toolbar/ToolbarSection.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | props: { 5 | align: { 6 | default: 'start', 7 | }, 8 | shrink: Boolean, 9 | }, 10 | 11 | render (h, { 12 | children, 13 | props: { 14 | align, 15 | shrink, 16 | }, 17 | data, 18 | }) { 19 | let staticClass = 'mdc-toolbar__section' 20 | if (align !== 'center') { 21 | staticClass += ` mdc-toolbar__section--align-${align}` 22 | } 23 | if (shrink) staticClass += ' mdc-toolbar__section--shrink-to-fit' 24 | data.staticClass = data.staticClass 25 | ? `${data.staticClass} ${staticClass}` 26 | : staticClass 27 | return h('section', { 28 | ...data, 29 | }, children) 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Button from './Button' 2 | 3 | import Card from './Card/Card' 4 | import CardActions from './Card/CardActions' 5 | import CardHorizontalBlock from './Card/CardHorizontalBlock' 6 | import CardMedia from './Card/CardMedia' 7 | import CardPrimary from './Card/CardPrimary' 8 | import CardSubtitle from './Card/CardSubtitle' 9 | import CardSupportingText from './Card/CardSupportingText' 10 | import CardTitle from './Card/CardTitle' 11 | 12 | import Checkbox from './Checkbox' 13 | 14 | import Dialog from './Dialog/Dialog' 15 | import DialogHeaderTitle from './Dialog/DialogHeaderTitle' 16 | 17 | import Drawer from './Drawer/Drawer' 18 | import DrawerHeader from './Drawer/DrawerHeader' 19 | import DrawerNav from './Drawer/DrawerNav' 20 | import DrawerNavItem from './Drawer/DrawerNavItem' 21 | 22 | import Fab from './Fab' 23 | 24 | import List from './List/List' 25 | import ListDivider from './List/ListDivider' 26 | import ListItem from './List/ListItem' 27 | 28 | import Toolbar from './Toolbar/Toolbar' 29 | import ToolbarRow from './Toolbar/ToolbarRow' 30 | import ToolbarSection from './Toolbar/ToolbarSection' 31 | 32 | import ripple from './ripple' 33 | 34 | const components = { 35 | Button, 36 | 37 | Card, 38 | CardActions, 39 | CardHorizontalBlock, 40 | CardMedia, 41 | CardPrimary, 42 | CardSubtitle, 43 | CardSupportingText, 44 | CardTitle, 45 | 46 | Checkbox, 47 | 48 | Dialog, 49 | DialogHeaderTitle, 50 | 51 | Drawer, 52 | DrawerHeader, 53 | DrawerNav, 54 | DrawerNavItem, 55 | 56 | Fab, 57 | 58 | List, 59 | ListDivider, 60 | ListItem, 61 | 62 | Toolbar, 63 | ToolbarRow, 64 | ToolbarSection, 65 | } 66 | 67 | function plugin (Vue, opts = { prefix: 'Mdc' }) { 68 | const { prefix } = opts 69 | const compNames = Object.keys(components) 70 | for (let i = 0; i < compNames.length; i++) { 71 | const name = compNames[i] 72 | Vue.component(`${prefix}${name}`, components[name]) 73 | } 74 | Vue.directive('ripple', ripple) 75 | } 76 | 77 | export default plugin 78 | // Export all components too 79 | const version = '__VERSION__' 80 | export { 81 | version, 82 | 83 | // components 84 | Button, 85 | 86 | Card, 87 | CardActions, 88 | CardHorizontalBlock, 89 | CardMedia, 90 | CardPrimary, 91 | CardSubtitle, 92 | CardSupportingText, 93 | CardTitle, 94 | 95 | Checkbox, 96 | 97 | Dialog, 98 | DialogHeaderTitle, 99 | 100 | Drawer, 101 | DrawerHeader, 102 | DrawerNav, 103 | DrawerNavItem, 104 | 105 | Fab, 106 | 107 | List, 108 | ListDivider, 109 | ListItem, 110 | 111 | Toolbar, 112 | ToolbarRow, 113 | ToolbarSection, 114 | 115 | // directives 116 | ripple, 117 | } 118 | -------------------------------------------------------------------------------- /src/ripple.js: -------------------------------------------------------------------------------- 1 | import MDCRippleFoundation from '@material/ripple/foundation' 2 | import { supportsCssVariables, getMatchesProperty } from '@material/ripple/util' 3 | import '@material/ripple/mdc-ripple.scss' 4 | 5 | const surfaceClass = 'mdc-ripple-surface' 6 | 7 | let MATCHES 8 | function createAdapter (el) { 9 | MATCHES = MATCHES || getMatchesProperty(HTMLElement.prototype) 10 | return { 11 | browserSupportsCssVars: () => supportsCssVariables(window), 12 | isSurfaceActive: /* istanbul ignore next */ () => el[MATCHES](':active'), 13 | addClass: (className) => el.classList.add(className), 14 | removeClass: (className) => el.classList.remove(className), 15 | registerInteractionHandler: (evtType, handler) => el.addEventListener(evtType, handler), 16 | deregisterInteractionHandler: (evtType, handler) => el.removeEventListener(evtType, handler), 17 | registerResizeHandler: (handler) => window.addEventListener('resize', handler), 18 | deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), 19 | updateCssVariable: (varName, value) => el.style.setProperty(varName, value), 20 | computeBoundingRect: () => el.getBoundingClientRect(), 21 | getWindowPageOffset: /* istanbul ignore next */ () => ({ x: window.pageXOffset, y: window.pageYOffset }), 22 | } 23 | } 24 | 25 | export default { 26 | bind (el, binding) { 27 | if (!binding.modifiers.custom) { 28 | el.classList.add(surfaceClass) 29 | } 30 | const unbounded = binding.modifiers.unbounded 31 | el.mdcRipple_ = new MDCRippleFoundation( 32 | Object.assign(createAdapter(el), { 33 | isUnbounded: () => unbounded, 34 | }) 35 | ) 36 | el.mdcRipple_.init() 37 | }, 38 | 39 | inserted (el, binding, { componentInstance }) { 40 | // TODO add test case 41 | // I have been unable to reproduce the bug outside 42 | // of vue-mdc docs, so it may have to deal with SSR 43 | if (componentInstance) { 44 | componentInstance.$on('hook:updated', () => { 45 | el.mdcRipple_.destroy() 46 | binding.def.bind(el, binding) 47 | }) 48 | } 49 | }, 50 | 51 | componentUpdated (el, binding, vnode, oldVnode) { 52 | // Always recreate for functional components 53 | if (vnode.functionalContext) { 54 | el.mdcRipple_.destroy() 55 | binding.def.bind(el, binding) 56 | } 57 | }, 58 | 59 | unbind (el, binding) { 60 | // istanbul ignore else 61 | if (el.mdcRipple_) { 62 | el.mdcRipple_.destroy() 63 | el.classList.remove(surfaceClass) 64 | delete el.mdcRipple_ 65 | } 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import decamelize from 'decamelize' 2 | 3 | export function propsToClasses (base, props) { 4 | return Object.keys(props).reduce((classes, key) => { 5 | if (props[key]) classes += ` ${base}--${decamelize(key, '-')}` 6 | return classes 7 | }, base) 8 | } 9 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true, 8 | "should": true, 9 | "requestAnimationFrame": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers/Test.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import camelcase from 'camelcase' 2 | import { createVM, Vue } from './utils' 3 | import { nextTick } from './wait-for-update' 4 | 5 | export function dataPropagationTest (Component) { 6 | return function () { 7 | const spy = sinon.spy() 8 | const vm = createVM(this, function (h) { 9 | return ( 10 | Hello 11 | ) 12 | }) 13 | spy.should.have.not.been.called 14 | vm.$('.custom').should.exist 15 | vm.$('.custom').click() 16 | if (Component.functional) spy.should.have.been.calledOnce 17 | } 18 | } 19 | 20 | export function attrTest (it, base, Component, attr) { 21 | const attrs = Array.isArray(attr) ? attr : [attr] 22 | 23 | attrs.forEach(attr => { 24 | it(attr, function (done) { 25 | const vm = createVM(this, function (h) { 26 | const opts = { 27 | props: { 28 | [camelcase(attr)]: this.active, 29 | }, 30 | } 31 | return ( 32 | {attr} 33 | ) 34 | }, { 35 | data: { active: true }, 36 | }) 37 | vm.$(`.${base}`).should.have.class(`${base}--${attr}`) 38 | vm.active = false 39 | nextTick().then(() => { 40 | vm.$(`.${base}`).should.not.have.class(`${base}--${attr}`) 41 | vm.active = true 42 | }).then(done) 43 | }) 44 | }) 45 | } 46 | export function foundationDetroyTest (Component) { 47 | return function (done) { 48 | const vm = createVM(this, function (h) { 49 | return ( 50 |
{ this.show && }
51 | ) 52 | }, { 53 | data: { show: true }, 54 | }) 55 | const foundation = vm.$refs.comp.foundation 56 | sinon.spy(foundation, 'destroy') 57 | foundation.destroy.should.have.not.been.called 58 | vm.show = false 59 | nextTick().then(() => { 60 | foundation.destroy.should.have.been.called.once 61 | foundation.destroy.restore() 62 | }).then(done) 63 | } 64 | } 65 | 66 | export { 67 | createVM, 68 | Vue, 69 | nextTick, 70 | } 71 | -------------------------------------------------------------------------------- /test/helpers/style.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import 'https://fonts.googleapis.com/icon?family=Material+Icons'; 3 | 4 | #mocha .test-dom-container { 5 | border: 1px solid #eee; 6 | border-bottom-color: #ddd; 7 | border-radius: 3px; 8 | clear: left; 9 | display: block; 10 | margin: 5px; 11 | max-width: calc(100% - 42px); 12 | padding: 15px; 13 | position: relative; 14 | } 15 | 16 | /* Allow overflow */ 17 | #mocha li.test { 18 | overflow: visible; 19 | } 20 | 21 | .test-dom-container { 22 | font: initial; 23 | } 24 | 25 | #mocha .test-dom-container--hidden { 26 | padding: 0; 27 | border: 0; 28 | margin: 0; 29 | } 30 | 31 | .test-dom-container__toggle { 32 | background-color: #edeeee; 33 | border-radius: 2px; 34 | color: #202129; 35 | cursor: pointer; 36 | display: block; 37 | left: -28px; 38 | padding: 0 3px; 39 | position: absolute; 40 | text-align: center; 41 | top: -24px; 42 | 43 | /* width - container border - spacing */ 44 | user-select: none; 45 | width: 15px; 46 | } 47 | 48 | .test-dom-container__toggle:hover { 49 | background-color: #e1e2e2; 50 | } 51 | 52 | .test-dom-container__toggle:active { 53 | background-color: #d5d6d6; 54 | } 55 | 56 | .test-dom-container--hidden .test-dom-container__toggle { 57 | /* Add the 1px from border to both */ 58 | top: -18px; 59 | 60 | /* width - spacing */ 61 | left: -22px; 62 | } 63 | 64 | .test-dom-container--hidden .test-dom-container__content { 65 | display: none; 66 | visibility: hidden; 67 | } 68 | 69 | .mdc-card--theme-dark .mdc-card__primary, 70 | .mdc-card--theme-dark .mdc-card__actions { 71 | background: rgba(0, 0, 0, .4); 72 | } 73 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import Test from './Test.vue' 3 | 4 | const isKarma = !!window.__karma__ 5 | 6 | export function createVM (context, template, opts = {}) { 7 | return isKarma 8 | ? createKarmaTest(context, template, opts) 9 | : createVisualTest(context, template, opts) 10 | } 11 | 12 | const emptyNodes = document.querySelectorAll('nonexistant') 13 | Vue.prototype.$$ = function $$ (selector) { 14 | const els = document.querySelectorAll(selector) 15 | const vmEls = this.$el.querySelectorAll(selector) 16 | const fn = vmEls.length 17 | ? el => vmEls.find(el) 18 | : el => this.$el === el 19 | const found = Array.from(els).filter(fn) 20 | return found.length 21 | ? found 22 | : emptyNodes 23 | } 24 | 25 | Vue.prototype.$ = function $ (selector) { 26 | const els = document.querySelectorAll(selector) 27 | const vmEl = this.$el.querySelector(selector) 28 | const fn = vmEl 29 | ? el => el === vmEl 30 | : el => el === this.$el 31 | // Allow should chaining for tests 32 | return Array.from(els).find(fn) || emptyNodes 33 | } 34 | 35 | export function createKarmaTest (context, template, opts) { 36 | const el = document.createElement('div') 37 | document.getElementById('tests').appendChild(el) 38 | const render = typeof template === 'string' 39 | ? { template: `
${template}
` } 40 | : { render: template } 41 | return new Vue({ 42 | el, 43 | name: 'Test', 44 | ...render, 45 | ...opts, 46 | }) 47 | } 48 | 49 | export function createVisualTest (context, template, opts) { 50 | let vm 51 | if (typeof template === 'string') { 52 | opts.components = opts.components || {} 53 | // Let the user define a test component 54 | if (!opts.components.Test) { 55 | opts.components.Test = Test 56 | } 57 | vm = new Vue({ 58 | name: 'TestContainer', 59 | el: context.DOMElement, 60 | template: `${template}`, 61 | ...opts, 62 | }) 63 | } else { 64 | // TODO allow redefinition of Test component 65 | vm = new Vue({ 66 | name: 'TestContainer', 67 | el: context.DOMElement, 68 | render (h) { 69 | return h(Test, { 70 | attrs: { 71 | id: context.DOMElement.id, 72 | }, 73 | // render the passed component with this scope 74 | }, [template.call(this, h)]) 75 | }, 76 | ...opts, 77 | }) 78 | } 79 | 80 | context.DOMElement.vm = vm 81 | return vm 82 | } 83 | 84 | export function register (name, component) { 85 | Vue.component(name, component) 86 | } 87 | 88 | export { isKarma, Vue } 89 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | 3 | // Testing helper 4 | // nextTick().then(() => { 5 | // 6 | // Automatically waits for nextTick 7 | // }).then(() => { 8 | // return a promise or value to skip the wait 9 | // }) 10 | function nextTick () { 11 | const jobs = [] 12 | let done 13 | 14 | const chainer = { 15 | then (cb) { 16 | jobs.push(cb) 17 | return chainer 18 | }, 19 | } 20 | 21 | function shift (...args) { 22 | const job = jobs.shift() 23 | let result 24 | try { 25 | result = job(...args) 26 | } catch (e) { 27 | jobs.length = 0 28 | done(e) 29 | } 30 | 31 | // wait for nextTick 32 | if (result !== undefined) { 33 | if (result.then) { 34 | result.then(shift) 35 | } else { 36 | shift(result) 37 | } 38 | } else if (jobs.length) { 39 | requestAnimationFrame(() => Vue.nextTick(shift)) 40 | } 41 | } 42 | 43 | // First time 44 | Vue.nextTick(() => { 45 | done = jobs[jobs.length - 1] 46 | if (done.toString().slice(0, 14) !== 'function (err)') { 47 | throw new Error('waitForUpdate chain is missing .then(done)') 48 | } 49 | shift() 50 | }) 51 | 52 | return chainer 53 | } 54 | 55 | exports.nextTick = nextTick 56 | exports.delay = time => new Promise(resolve => setTimeout(resolve, time)) 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // require all src files for coverage. 2 | // you can also change this to match only the subset of files that 3 | // you want coverage for. 4 | const srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) 5 | srcContext.keys().forEach(srcContext) 6 | 7 | // Use a div to insert elements 8 | before(function () { 9 | const el = document.createElement('DIV') 10 | el.id = 'tests' 11 | document.body.appendChild(el) 12 | }) 13 | 14 | // Remove every test html scenario 15 | afterEach(function () { 16 | const el = document.getElementById('tests') 17 | for (let i = 0; i < el.children.length; ++i) { 18 | el.removeChild(el.children[i]) 19 | } 20 | }) 21 | 22 | const specsContext = require.context('./specs', true) 23 | specsContext.keys().forEach(specsContext) 24 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const baseConfig = require('../build/webpack.config.dev.js') 3 | 4 | const webpackConfig = merge(baseConfig, { 5 | // use inline sourcemap for karma-sourcemap-loader 6 | devtool: '#inline-source-map', 7 | }) 8 | 9 | webpackConfig.plugins = [] 10 | 11 | const vueRule = webpackConfig.module.rules.find(rule => rule.loader === 'vue-loader') 12 | vueRule.options = vueRule.options || {} 13 | vueRule.options.loaders = vueRule.options.loaders || {} 14 | vueRule.options.loaders.js = 'babel-loader' 15 | 16 | // no need for app entry during tests 17 | delete webpackConfig.entry 18 | 19 | module.exports = function (config) { 20 | config.set({ 21 | // to run in additional browsers: 22 | // 1. install corresponding karma launcher 23 | // http://karma-runner.github.io/0.13/config/browsers.html 24 | // 2. add it to the `browsers` array below. 25 | browsers: ['Chrome'], 26 | frameworks: ['mocha', 'chai-dom', 'sinon-chai'], 27 | reporters: ['spec', 'coverage'], 28 | files: ['./index.js'], 29 | preprocessors: { 30 | './index.js': ['webpack', 'sourcemap'], 31 | }, 32 | webpack: webpackConfig, 33 | webpackMiddleware: { 34 | noInfo: true, 35 | }, 36 | coverageReporter: { 37 | dir: './coverage', 38 | reporters: [ 39 | { type: 'lcov', subdir: '.' }, 40 | { type: 'text-summary' }, 41 | ], 42 | }, 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /test/specs/Button.spec.js: -------------------------------------------------------------------------------- 1 | import Button from 'src/Button.vue' 2 | import { 3 | createVM, 4 | dataPropagationTest, 5 | attrTest, 6 | } from '../helpers' 7 | 8 | describe('Button.vue', function () { 9 | it('renders an upgraded button', function () { 10 | const vm = createVM(this, ` 11 | 12 | `, { 13 | components: { Button }, 14 | }) 15 | vm.$('button').should.have.text('Click me') 16 | vm.$('button').should.have.class('mdc-button') 17 | }) 18 | 19 | it('keeps original tag data', dataPropagationTest(Button)) 20 | 21 | describe('attrs', function () { 22 | attrTest(it, 'mdc-button', Button, [ 23 | 'dense', 24 | 'raised', 25 | 'compact', 26 | 'primary', 27 | 'accent', 28 | 'theme-dark', 29 | ]) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/specs/Card.spec.js: -------------------------------------------------------------------------------- 1 | import MdcCard from 'src/Card/Card.vue' 2 | import MdcButton from 'src/Button.vue' 3 | import MdcCardActions from 'src/Card/CardActions.js' 4 | import MdcCardHorizontalBlock from 'src/Card/CardHorizontalBlock.js' 5 | import MdcCardMedia from 'src/Card/CardMedia.js' 6 | import MdcCardPrimary from 'src/Card/CardPrimary.js' 7 | import MdcCardTitle from 'src/Card/CardTitle.js' 8 | import MdcCardSubtitle from 'src/Card/CardSubtitle.js' 9 | import MdcCardSupportingText from 'src/Card/CardSupportingText.js' 10 | import { 11 | createVM, 12 | nextTick, 13 | dataPropagationTest, 14 | attrTest, 15 | } from '../helpers' 16 | const largeImg = 'http://material-components-web.appspot.com/images/16-9.jpg' 17 | const squareImg = 'http://material-components-web.appspot.com/images/1-1.jpg' 18 | 19 | describe('Card.vue', function () { 20 | it('can have a title', function () { 21 | const vm = createVM(this, ` 22 | 23 | Title 24 | 25 | 26 | `, { 27 | components: { MdcCard, MdcCardTitle }, 28 | }) 29 | vm.$('.title .mdc-card__title').should.have.text('Title') 30 | vm.$('.no-title .mdc-card__title').should.not.exist 31 | }) 32 | 33 | it('can have a title as a prop', function (done) { 34 | const vm = createVM(this, ` 35 | 36 | `, { 37 | data: { title: 'Title' }, 38 | components: { MdcCard }, 39 | }) 40 | vm.$('.prop .mdc-card__title').should.have.text('Title') 41 | // It's large by default 42 | vm.$('.prop .mdc-card__title').should.have.class('mdc-card__title--large') 43 | vm.title = '' 44 | nextTick().then(() => { 45 | vm.$('.prop .mdc-card__title').should.not.exist 46 | }).then(done) 47 | }) 48 | 49 | it('can have a title and subtitle', function () { 50 | const vm = createVM(this, ` 51 | 52 | Title 53 | subtitle 54 | 55 | `, { 56 | components: { MdcCard, MdcCardTitle, MdcCardSubtitle }, 57 | }) 58 | vm.$('.multi .mdc-card__title').should.have.text('Title') 59 | vm.$('.multi .mdc-card__subtitle').should.have.text('subtitle') 60 | }) 61 | 62 | it('can have a subtitle as a prop', function (done) { 63 | const vm = createVM(this, ` 64 | 65 | `, { 66 | data: { sub: 'sub' }, 67 | components: { MdcCard }, 68 | }) 69 | vm.$('.prop .mdc-card__subtitle').should.have.text('sub') 70 | vm.sub = '' 71 | nextTick().then(() => { 72 | vm.$('.prop .mdc-card__sub').should.not.exist 73 | }).then(done) 74 | }) 75 | 76 | it('keeps original data in Title', dataPropagationTest(MdcCardTitle)) 77 | it('keeps original data in Subtitle', dataPropagationTest(MdcCardSubtitle)) 78 | it('keeps original data in Primary', dataPropagationTest(MdcCardPrimary)) 79 | it('keeps original data in HorizontalBlock', dataPropagationTest(MdcCardHorizontalBlock)) 80 | it('keeps original data in SupportingText', dataPropagationTest(MdcCardSupportingText)) 81 | it('keeps original data in Actions', dataPropagationTest(MdcCardActions)) 82 | 83 | it('has a media section before the title', function (done) { 84 | const vm = createVM(this, ` 85 | 87 | 88 | `, { 89 | data: { media: largeImg }, 90 | components: { MdcCard }, 91 | }) 92 | vm.$('.mdc-card__media + .mdc-card__primary').should.exist 93 | vm.$('.mdc-card__primary + .mdc-card__media').should.not.exist 94 | const style = vm.$('.mdc-card__media').style 95 | style.height.should.equal('12.313rem') 96 | style.backgroundImage.should.equal(`url("${largeImg}")`) 97 | vm.media = '' 98 | nextTick().then(() => { 99 | vm.$('.mdc-card__media').should.not.exist 100 | }).then(done) 101 | }) 102 | 103 | it('can reorder elements', function () { 104 | const vm = createVM(this, ` 105 | 106 | 107 | 108 | `, { 109 | components: { MdcCard, MdcCardMedia }, 110 | }) 111 | vm.$('.mdc-card__media + .mdc-card__primary').should.not.exist 112 | vm.$('.mdc-card__primary + .mdc-card__media').should.exist 113 | }) 114 | 115 | it('has a supporting text', function (done) { 116 | const vm = createVM(this, ` 117 | 118 | 119 | `, { 120 | data: { text: 'Hello there' }, 121 | components: { MdcCard, MdcCardMedia }, 122 | }) 123 | vm.$('.mdc-card__supporting-text').should.exist 124 | vm.$('.mdc-card__supporting-text').should.have.text('Hello there') 125 | vm.text = '' 126 | nextTick().then(() => { 127 | vm.$('.mdc-card__supporting-text').should.not.exist 128 | }).then(done) 129 | }) 130 | 131 | it('has a slot in supporting text', function (done) { 132 | const vm = createVM(this, ` 133 | 134 | A slot 135 | 136 | `, { 137 | data: { show: true }, 138 | components: { MdcCard, MdcCardSupportingText }, 139 | }) 140 | vm.$('.mdc-card__supporting-text').should.exist 141 | vm.$('.mdc-card__supporting-text').should.have.text('A slot') 142 | vm.show = false 143 | nextTick().then(() => { 144 | vm.$('.mdc-card__supporting-text').should.not.exist 145 | }).then(done) 146 | }) 147 | 148 | it('has an actions slot', function (done) { 149 | const vm = createVM(this, ` 150 | 151 |

Actions

152 | Actions 153 |
154 | `, { 155 | data: { show: true }, 156 | components: { MdcCard, MdcButton }, 157 | }) 158 | vm.$('.mdc-card__actions').should.exist 159 | vm.$('.mdc-card__actions p').should.have.text('Actions') 160 | vm.$('.mdc-card__actions button').should.have.class('mdc-card__action') 161 | vm.$('.mdc-card__actions p').should.not.have.class('mdc-card__action') 162 | vm.show = false 163 | nextTick().then(() => { 164 | vm.$('.mdc-card__actions').should.not.exist 165 | vm.show = true 166 | }).then(done) 167 | }) 168 | 169 | it('can apply a dark theme', function () { 170 | const vm = createVM(this, ` 171 | 172 | Action 173 | 174 | `, { 175 | components: { MdcCard, MdcButton }, 176 | }) 177 | vm.$('.mdc-card').should.have.class('mdc-card--theme-dark') 178 | }) 179 | 180 | it('horizontal block', function () { 181 | const vm = createVM(this, ` 182 | 183 | 184 | 185 | Title Here 186 | Subtitle Here 187 | 188 | 189 | 190 | Action 1 191 | Action 2 192 | 193 | `, { 194 | components: { 195 | MdcCard, 196 | MdcButton, 197 | MdcCardTitle, 198 | MdcCardSubtitle, 199 | MdcCardPrimary, 200 | MdcCardHorizontalBlock, 201 | }, 202 | }) 203 | vm.$('.mdc-card__horizontal-block .mdc-card__primary .mdc-card__title').should.exist 204 | vm.$('.mdc-card__horizontal-block .mdc-card__primary .mdc-card__subtitle').should.exist 205 | }) 206 | 207 | attrTest(it, 'mdc-card__actions', MdcCardActions, 'vertical') 208 | }) 209 | -------------------------------------------------------------------------------- /test/specs/Checkbox.spec.js: -------------------------------------------------------------------------------- 1 | import MdcCheckbox from 'src/Checkbox.vue' 2 | import { createVM } from '../helpers/utils.js' 3 | import { nextTick } from '../helpers/wait-for-update.js' 4 | 5 | describe('Checkbox.vue', function () { 6 | it('renders checkbox', function () { 7 | const vm = createVM(this, function (h) { 8 | return ( 9 | 10 | ) 11 | }, { 12 | components: { MdcCheckbox }, 13 | }) 14 | vm.$('input').should.have.attr('type', 'checkbox') 15 | }) 16 | 17 | it('works with v-model and bool', function (done) { 18 | const vm = createVM(this, ``, { 19 | data: { 20 | checked: true, 21 | }, 22 | components: { MdcCheckbox }, 23 | }) 24 | vm.checked.should.be.true 25 | vm.$('input').should.be.checked 26 | vm.checked = false 27 | nextTick().then(() => { 28 | vm.checked.should.be.false 29 | vm.$('input').should.not.be.checked 30 | }).then(done) 31 | }) 32 | 33 | it('works with v-model and arrays', function (done) { 34 | const vm = createVM(this, ` 35 | 36 | 37 | `, { 38 | data: { 39 | opts: ['one'], 40 | }, 41 | components: { MdcCheckbox }, 42 | }) 43 | vm.opts.should.eql(['one']) 44 | vm.$('.one').should.be.checked 45 | vm.$('.two').should.not.be.checked 46 | vm.$('.one input').should.have.value('one') 47 | vm.opts = ['one', 'two'] 48 | nextTick().then(() => { 49 | vm.opts.should.eql(['one', 'two']) 50 | vm.$('.one').should.be.checked 51 | vm.$('.two').should.be.checked 52 | vm.$('.one input').click() 53 | }).then(() => { 54 | vm.opts.should.eql(['two']) 55 | vm.$('.one').should.be.not.checked 56 | vm.$('.two').should.be.checked 57 | }).then(done) 58 | }) 59 | 60 | it('can have classic attrs: id, disabled', function () { 61 | const vm = createVM(this, ` 62 | 63 | `, { 64 | components: { MdcCheckbox }, 65 | }) 66 | vm.$('input').should.have.attr('disabled') 67 | vm.$('input').should.have.id('foo') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/specs/Dialog.spec.js: -------------------------------------------------------------------------------- 1 | import Dialog from 'src/Dialog/Dialog' 2 | import DialogHeaderTitle from 'src/Dialog/DialogHeaderTitle' 3 | import { 4 | createVM, 5 | dataPropagationTest, 6 | foundationDetroyTest, 7 | nextTick, 8 | } from '../helpers' 9 | 10 | describe('Dialog', function () { 11 | describe('DialogHeaderTitle', function () { 12 | it('keeps original tag data', dataPropagationTest(DialogHeaderTitle)) 13 | }) 14 | 15 | it('keeps original tag data', dataPropagationTest(Dialog)) 16 | it('calls foundation destroy', foundationDetroyTest(Dialog)) 17 | 18 | it('can be scrollable', function (done) { 19 | const vm = createVM(this, function (h) { 20 | return ( 21 |
22 | 23 | 24 |
25 | ) 26 | }, { 27 | data: { 28 | scrollable: true, 29 | }, 30 | }) 31 | 32 | vm.$('.mdc-dialog__body') 33 | .should.exist.and.have.class('mdc-dialog__body--scrollable') 34 | vm.scrollable = false 35 | nextTick().then(() => { 36 | vm.$('.mdc-dialog__body') 37 | .should.exist.and.not.have.class('mdc-dialog__body--scrollable') 38 | }).then(done) 39 | }) 40 | 41 | it('has a title', function () { 42 | const vm = createVM(this, function (h) { 43 | return ( 44 |
45 | 46 | 47 |
48 | ) 49 | }) 50 | 51 | vm.$('.mdc-dialog__header').should.exist.and.have.text('Title') 52 | }) 53 | 54 | it('accept button is focused by default', function () { 55 | const vm = createVM(this, function (h) { 56 | return ( 57 |
58 | 59 | 60 |
61 | ) 62 | }) 63 | 64 | vm.$('.mdc-dialog__footer__button--accept') 65 | .should.exist 66 | .and.have.text('OK') 67 | .and.be.focused 68 | }) 69 | 70 | it('can be toggled', function (done) { 71 | const vm = createVM(this, function (h) { 72 | return ( 73 |
74 | 75 | 76 |
77 | ) 78 | }) 79 | vm.$refs.dialog.$el.should.have.class('mdc-dialog') 80 | vm.$refs.dialog.$el.should.be.hidden 81 | vm.$refs.dialog.open() 82 | nextTick().then(() => { 83 | vm.$refs.dialog.$el.should.not.be.hidden 84 | vm.$refs.dialog.close() 85 | }).then(() => { 86 | vm.$refs.dialog.$el.should.be.hidden 87 | }).then(done) 88 | }) 89 | 90 | it('displays its content', function () { 91 | const vm = createVM(this, function (h) { 92 | return ( 93 |
94 | 95 | Dialog content 96 | 97 | 98 |
99 | ) 100 | }) 101 | vm.$('.mdc-dialog__body').should.exist.and.have.text('Dialog content') 102 | }) 103 | 104 | it('is visible if content changes', function (done) { 105 | const vm = createVM(this, function (h) { 106 | return ( 107 |
108 | 109 |
110 | n: {this.n} 111 |
112 | 113 | 114 |
115 | 116 |
117 | ) 118 | }, { 119 | data: { 120 | n: 0, 121 | }, 122 | }) 123 | vm.$('.content').should.exist.and.have.text('n: 0') 124 | vm.$refs.dialog.$el.should.be.hidden 125 | vm.$refs.dialog.open() 126 | nextTick().then(() => { 127 | vm.$refs.dialog.$el.should.not.be.hidden 128 | vm.n = 20 129 | }).then(() => { 130 | vm.$refs.dialog.$el.should.not.be.hidden 131 | vm.$('.content').should.exist.and.have.text('n: 20') 132 | }).then(done) 133 | }) 134 | 135 | ;['accept', 'cancel'].map(type => { 136 | it(`displays a ${type} button`, function (done) { 137 | const vm = createVM(this, function (h) { 138 | const data = { 139 | props: { 140 | [`${type}Text`]: this.text, 141 | }, 142 | } 143 | return ( 144 |
145 | 146 | Dialog content 147 | 148 | 149 |
150 | ) 151 | }, { 152 | data: { 153 | text: 'Yes', 154 | }, 155 | }) 156 | vm.$(`.mdc-dialog__footer__button--${type}`) 157 | .should.exist.and.have.text('Yes') 158 | vm.text = undefined 159 | nextTick().then(() => { 160 | if (type === 'cancel') { 161 | vm.$(`.mdc-dialog__footer__button--${type}`).should.not.exist 162 | } else if (type === 'accept') { 163 | // default text for accept 164 | vm.$(`.mdc-dialog__footer__button--${type}`).should.exist 165 | } 166 | vm.text = 'Yes' 167 | }).then(done) 168 | }) 169 | }) 170 | 171 | it('has a ripple in buttons', function () { 172 | const vm = createVM(this, function (h) { 173 | return ( 174 |
175 | 176 | 177 |
178 | ) 179 | }, { 180 | data: { 181 | n: 0, 182 | }, 183 | }) 184 | vm.$('.mdc-dialog__footer__button--accept').should.exist.and.have.class('mdc-ripple-surface') 185 | }) 186 | 187 | describe('Events', function () { 188 | it('accepted', function (done) { 189 | const spy = sinon.spy() 190 | const vm = createVM(this, function (h) { 191 | return ( 192 |
193 | 194 | Dialog content 195 | 196 | 197 |
198 | ) 199 | }, { 200 | data: { 201 | text: 'Yes', 202 | }, 203 | }) 204 | vm.$refs.dialog.open() 205 | vm.text = 'ok' 206 | nextTick().then(() => { 207 | spy.should.have.not.been.called 208 | vm.$('.mdc-dialog__footer__button--accept').click() 209 | }).then(() => { 210 | spy.should.have.been.called 211 | }).then(done) 212 | }) 213 | 214 | it('canceled', function (done) { 215 | const spy = sinon.spy() 216 | const vm = createVM(this, function (h) { 217 | return ( 218 |
219 | 220 | Dialog content 221 | 222 | 223 |
224 | ) 225 | }, { 226 | data: { 227 | text: 'No', 228 | }, 229 | }) 230 | vm.$refs.dialog.open() 231 | vm.text = 'nope' 232 | nextTick().then(() => { 233 | spy.should.have.not.been.called 234 | vm.$('.mdc-dialog__footer__button--cancel').click() 235 | }).then(() => { 236 | spy.should.have.been.called 237 | }).then(done) 238 | }) 239 | }) 240 | 241 | it('can override the header', function () { 242 | const vm = createVM(this, function (h) { 243 | return ( 244 |
245 | 246 | Custom 247 | Dialog content 248 | 249 | 250 |
251 | ) 252 | }) 253 | vm.$('.mdc-dialog__header__title').should.exist.and.have.text('Custom') 254 | }) 255 | 256 | it('should not have an id by default', function () { 257 | const vm = createVM(this, function (h) { 258 | return ( 259 |
260 | 261 | Dialog content 262 | 263 | 264 |
265 | ) 266 | }) 267 | vm.$('.mdc-dialog__header__title').should.not.have.attr('id') 268 | vm.$('.mdc-dialog__body').should.not.have.attr('id') 269 | vm.$refs.dialog.$el.should.not.have.attr('id') 270 | }) 271 | 272 | it('should propagate id to aria labels', function (done) { 273 | const vm = createVM(this, function (h) { 274 | return ( 275 |
276 | 277 | Dialog content 278 | 279 | 280 |
281 | ) 282 | }, { 283 | data: { 284 | id: 'my-dialog', 285 | }, 286 | }) 287 | vm.$('.mdc-dialog__header__title').should.have.id('my-dialog__label') 288 | vm.$('.mdc-dialog__body').should.have.id('my-dialog__description') 289 | vm.$refs.dialog.$el.should.have.id('my-dialog') 290 | vm.$refs.dialog.$el.should.have.attr('aria-labelledby', 'my-dialog__label') 291 | vm.$refs.dialog.$el.should.have.attr('aria-describedby', 'my-dialog__description') 292 | vm.id = false 293 | nextTick().then(() => { 294 | vm.$('.mdc-dialog__header__title').should.not.have.attr('id') 295 | vm.$('.mdc-dialog__body').should.not.have.attr('id') 296 | vm.$refs.dialog.$el.should.not.have.attr('id') 297 | }).then(done) 298 | }) 299 | }) 300 | -------------------------------------------------------------------------------- /test/specs/Drawer.spec.js: -------------------------------------------------------------------------------- 1 | import Drawer from 'src/Drawer/Drawer' 2 | import DrawerNav from 'src/Drawer/DrawerNav' 3 | import DrawerNavItem from 'src/Drawer/DrawerNavItem' 4 | import DrawerHeader from 'src/Drawer/DrawerHeader' 5 | import ListDivider from 'src/List/ListDivider' 6 | import { 7 | createVM, 8 | dataPropagationTest, 9 | foundationDetroyTest, 10 | nextTick, 11 | } from '../helpers' 12 | 13 | describe('Drawer', function () { 14 | it('keeps original tag data', dataPropagationTest(Drawer)) 15 | 16 | it('is temporary by default', function () { 17 | const vm = createVM(this, h => ( 18 | 19 | 20 | )) 21 | vm.$refs.drawer.$el.should.have.class('mdc-temporary-drawer') 22 | }) 23 | 24 | it('calls foundation destroy', foundationDetroyTest(Drawer)) 25 | 26 | describe('temporary', function () { 27 | describe('no content', function () { 28 | beforeEach(function () { 29 | this.vm = createVM(this, function (h) { 30 | return ( 31 |
32 | 33 | 34 | 35 |
36 | ) 37 | }) 38 | }) 39 | 40 | afterEach(function () { 41 | this.vm.$refs.drawer.close() 42 | }) 43 | 44 | it('makes the body unscrollable while open', function () { 45 | const scrollBlock = 'mdc-dialog-scroll-lock' 46 | document.body.should.not.have.class(scrollBlock) 47 | this.vm.$refs.drawer.open() 48 | document.body.should.have.class(scrollBlock) 49 | this.vm.$refs.drawer.close() 50 | document.body.should.not.have.class(scrollBlock) 51 | }) 52 | 53 | it('can be closed', function (done) { 54 | this.vm.$refs.drawer.$el.should.not.have.class('mdc-temporary-drawer--open') 55 | this.vm.$refs.drawer.close() 56 | nextTick().then(() => { 57 | this.vm.$refs.drawer.$el.should.not.have.class('mdc-temporary-drawer--open') 58 | }).then(done) 59 | }) 60 | 61 | it('closes when clicking outside, function ()', function () { 62 | this.vm.$refs.drawer.open() 63 | this.vm.$refs.drawer.$el.should.have.class('mdc-temporary-drawer--open') 64 | this.vm.$('.mdc-temporary-drawer').click() 65 | this.vm.$refs.drawer.$el.should.not.have.class('mdc-temporary-drawer--open') 66 | }) 67 | 68 | it('can be opened', function (done) { 69 | this.vm.$refs.drawer.$el.should.not.have.class('mdc-temporary-drawer--open') 70 | this.vm.$refs.drawer.open() 71 | nextTick().then(() => { 72 | this.vm.$refs.drawer.$el.should.have.class('mdc-temporary-drawer--open') 73 | // should do nothing 74 | this.vm.$refs.drawer.open() 75 | }).then(() => { 76 | this.vm.$refs.drawer.$el.should.have.class('mdc-temporary-drawer--open') 77 | this.vm.$refs.drawer.close() 78 | }).then(done) 79 | }) 80 | }) 81 | 82 | describe('with a nav', function () { 83 | beforeEach(function () { 84 | this.vm = createVM(this, function (h) { 85 | const select = e => { 86 | this.selected = e.target.textContent 87 | } 88 | const menu1 = [ 89 | { icon: 'inbox', text: 'Inbox' }, 90 | { icon: 'star', text: 'Star' }, 91 | { icon: 'send', text: 'Sent Mail' }, 92 | { icon: 'drafts', text: 'Drafts' }, 93 | ] 94 | const menu2 = [ 95 | { icon: 'email', text: 'All Mail' }, 96 | { icon: 'delete', text: 'Trash' }, 97 | { icon: 'report', text: 'Spam' }, 98 | ] 99 | const isSelected = id => id === this.selected 100 | 101 | return ( 102 |
103 | 104 | 107 | Header Here 108 | 109 | 110 | 111 | {menu1.map(({ icon, text }) => ( 112 | 116 | {text} 118 | 119 | ))} 120 | 121 | 122 | 123 | 124 | 125 | {menu2.map(({ icon, text }) => ( 126 | 130 | {text} 132 | 133 | ))} 134 | 135 | 136 | 137 |
138 | ) 139 | }, { 140 | data: { 141 | selected: 'inboxInbox', 142 | }, 143 | }) 144 | }) 145 | 146 | it('holds a header and nav', function () { 147 | this.vm.$('.mdc-temporary-drawer__header').should.exist 148 | this.vm.$('.mdc-temporary-drawer__content').should.match('.mdc-list-group') 149 | this.vm.$('.mdc-list-group .mdc-list').should.contain('.mdc-list-item') 150 | this.vm.$('.mdc-list-item').should.have.text('inboxInbox') 151 | this.vm.$('.mdc-list-item').should.have.class('mdc-temporary-drawer--selected') 152 | }) 153 | }) 154 | }) 155 | 156 | describe('DrawerNav', function () { 157 | it('is a nav by default', function () { 158 | const vm = createVM(this, h => ( 159 | 160 | )) 161 | vm.$('.mdc-list').should.exist.and.match('nav') 162 | }) 163 | it('can render a custom tag', function () { 164 | const vm = createVM(this, h => ( 165 | 166 | )) 167 | vm.$('.mdc-list').should.exist.and.match('div') 168 | }) 169 | }) 170 | 171 | describe('DrawerNavItem', function () { 172 | it('is an a by default', function () { 173 | const vm = createVM(this, h => ( 174 | Hello 175 | )) 176 | vm.$('.mdc-list-item').should.exist.and.match('a') 177 | }) 178 | it('can render a custom tag', function () { 179 | const vm = createVM(this, h => ( 180 | Hello 181 | )) 182 | vm.$('.mdc-list-item').should.exist.and.match('div') 183 | }) 184 | }) 185 | }) 186 | -------------------------------------------------------------------------------- /test/specs/Fab.spec.js: -------------------------------------------------------------------------------- 1 | import Fab from 'src/Fab' 2 | import { 3 | createVM, 4 | dataPropagationTest, 5 | attrTest, 6 | } from '../helpers' 7 | 8 | describe('Fab', function () { 9 | it('keeps original tag data', dataPropagationTest(Fab)) 10 | 11 | it('renders a button by default', function () { 12 | const vm = createVM(this, function (h) { 13 | return ( 14 | 15 | ) 16 | }) 17 | vm.$('.mdc-fab') 18 | .should.have.match('button') 19 | .and.have.class('material-icons') 20 | }) 21 | 22 | it('contains an icon', function () { 23 | const vm = createVM(this, function (h) { 24 | return ( 25 | 26 | ) 27 | }) 28 | vm.$('.mdc-fab__icon').should.have.match('span') 29 | }) 30 | 31 | it('contains custom content', function () { 32 | const vm = createVM(this, function (h) { 33 | return ( 34 | 💃🏻 35 | ) 36 | }) 37 | vm.$('.mdc-fab').should.not.have.class('material-icons') 38 | vm.$('.mdc-fab__icon').should.not.exist 39 | }) 40 | 41 | describe('attrs', function () { 42 | attrTest(it, 'mdc-fab', Fab, [ 43 | 'mini', 44 | 'plain', 45 | ]) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/specs/List.spec.js: -------------------------------------------------------------------------------- 1 | import List from 'src/List/List' 2 | import ListItem from 'src/List/ListItem' 3 | import ListDivider from 'src/List/ListDivider' 4 | import ripple from 'src/ripple' 5 | import { 6 | createVM, 7 | dataPropagationTest, 8 | attrTest, 9 | } from '../helpers' 10 | 11 | describe('List', function () { 12 | it('renders an upgraded list', function () { 13 | const vm = createVM(this, h => ( 14 | 15 | Item 1 16 | Item 2 17 | 18 | ), { 19 | }) 20 | vm.$refs.list.should.have.class('mdc-list') 21 | vm.$refs.list.should.match('ul') 22 | }) 23 | 24 | it('can render a custom tag', function () { 25 | const vm = createVM(this, h => ( 26 | Hello 27 | )) 28 | vm.$refs.list.should.have.class('mdc-list') 29 | vm.$refs.list.should.match('div') 30 | }) 31 | 32 | it('keeps original tag data', dataPropagationTest(List)) 33 | 34 | describe('attrs', function () { 35 | attrTest(it, 'mdc-list', List, [ 36 | 'dense', 37 | 'two-lines', 38 | ]) 39 | }) 40 | 41 | describe('ListItem', function () { 42 | it('renders an upgraded list item', function () { 43 | const vm = createVM(this, h => ( 44 |
45 | 46 | Item 1 47 | 48 | 50 | 51 | Two-line item 52 | Secondary text 53 | 54 | 55 | 56 |
57 | ), { directives: { ripple }}) 58 | vm.$refs.item.should.have.class('mdc-list-item') 59 | vm.$refs.item.should.match('li') 60 | }) 61 | 62 | it('can render a custom tag', function () { 63 | const vm = createVM(this, h => ( 64 | Hello 65 | )) 66 | vm.$refs.item.should.have.class('mdc-list-item') 67 | vm.$refs.item.should.match('a') 68 | }) 69 | 70 | it('keeps original tag data', dataPropagationTest(ListItem)) 71 | }) 72 | 73 | describe('ListDivider', function () { 74 | it('keeps original tag data', dataPropagationTest(ListDivider)) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/specs/Toolbar.spec.js: -------------------------------------------------------------------------------- 1 | import Toolbar from 'src/Toolbar/Toolbar' 2 | import ToolbarRow from 'src/Toolbar/ToolbarRow' 3 | import ToolbarSection from 'src/Toolbar/ToolbarSection' 4 | import { 5 | createVM, 6 | dataPropagationTest, 7 | nextTick, 8 | } from '../helpers' 9 | 10 | describe('Toolbar', function () { 11 | it('keeps original tag data', dataPropagationTest(Toolbar)) 12 | 13 | it('has default icon', function () { 14 | const vm = createVM(this, function (h) { 15 | return ( 16 | 17 | 18 | ) 19 | }) 20 | vm.$refs.toolbar.$el.should.have.class('mdc-toolbar') 21 | vm.$('.mdc-toolbar__menu').should.have.text('menu') 22 | }) 23 | 24 | it('can set the title', function () { 25 | const vm = createVM(this, function (h) { 26 | return ( 27 | 28 | 29 | ) 30 | }) 31 | vm.$('.mdc-toolbar__title').should.have.text('My Title') 32 | }) 33 | 34 | it('can set the icon', function () { 35 | const vm = createVM(this, function (h) { 36 | return ( 37 | 38 | 39 | ) 40 | }) 41 | vm.$('.mdc-toolbar__menu').should.have.text('star') 42 | }) 43 | 44 | it('emits when clicking the menu', function (done) { 45 | const spy = sinon.spy() 46 | const vm = createVM(this, function (h) { 47 | return ( 48 | 49 | 50 | ) 51 | }) 52 | spy.should.have.not.been.called 53 | vm.$('.mdc-toolbar__menu').click() 54 | nextTick().then(() => { 55 | spy.should.have.been.called 56 | }).then(done) 57 | }) 58 | 59 | it('aligns start by default', function () { 60 | const vm = createVM(this, function (h) { 61 | return ( 62 | 63 | 64 | ) 65 | }) 66 | vm.$('.mdc-toolbar__section') 67 | .should.have.class('mdc-toolbar__section--align-start') 68 | .and.not.have.class('mdc-toolbar__section--align-end') 69 | }) 70 | 71 | it('can accomodate more sections') 72 | 73 | describe('ToolbarSection', function () { 74 | it('keeps original tag data', dataPropagationTest(ToolbarSection)) 75 | 76 | it('aligns start by default', function () { 77 | const vm = createVM(this, function (h) { 78 | return ( 79 | Content 80 | ) 81 | }) 82 | vm.$('.mdc-toolbar__section') 83 | .should.have.class('mdc-toolbar__section--align-start') 84 | .and.have.text('Content') 85 | .and.not.have.class('mdc-toolbar__section--align-end') 86 | }) 87 | 88 | it('can align center', function () { 89 | const vm = createVM(this, function (h) { 90 | return ( 91 | 92 | 93 | ) 94 | }) 95 | vm.$('.mdc-toolbar__section') 96 | .should.not.have.class('mdc-toolbar__section--align-start') 97 | .and.not.have.class('mdc-toolbar__section--align-end') 98 | }) 99 | 100 | it('can align end', function () { 101 | const vm = createVM(this, function (h) { 102 | return ( 103 | 104 | 105 | ) 106 | }) 107 | vm.$('.mdc-toolbar__section') 108 | .should.have.class('mdc-toolbar__section--align-end') 109 | .and.not.have.class('mdc-toolbar__section--align-start') 110 | }) 111 | 112 | it('shrinks', function () { 113 | const vm = createVM(this, function (h) { 114 | return ( 115 | 116 | 117 | ) 118 | }) 119 | vm.$('.mdc-toolbar__section') 120 | .should.have.class('mdc-toolbar__section--shrink-to-fit') 121 | }) 122 | }) 123 | 124 | describe('ToolbarRow', function () { 125 | it('keeps original tag data', dataPropagationTest(ToolbarRow)) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /test/specs/VueMdc.spec.js: -------------------------------------------------------------------------------- 1 | import VueMdc from 'src' 2 | import * as all from 'src' 3 | import { Vue } from '../helpers/utils' 4 | 5 | function comp (Vue, name) { 6 | return function () { 7 | should.exist(Vue.component(name)) 8 | } 9 | } 10 | 11 | function dir (Vue, name) { 12 | return function () { 13 | should.exist(Vue.directive(name)) 14 | } 15 | } 16 | 17 | const components = [ 18 | 'Button', 19 | 20 | 'Card', 21 | 'CardActions', 22 | 'CardHorizontalBlock', 23 | 'CardMedia', 24 | 'CardPrimary', 25 | 'CardSubtitle', 26 | 'CardSupportingText', 27 | 'CardTitle', 28 | 29 | 'Checkbox', 30 | 31 | 'Dialog', 32 | 'DialogHeaderTitle', 33 | 34 | 'Drawer', 35 | 'DrawerHeader', 36 | 'DrawerNav', 37 | 'DrawerNavItem', 38 | 39 | 'Fab', 40 | 41 | 'List', 42 | 'ListDivider', 43 | 'ListItem', 44 | 45 | 'Toolbar', 46 | 'ToolbarRow', 47 | 'ToolbarSection', 48 | ] 49 | 50 | describe('VueMdc', function () { 51 | it('exporting all components', function () { 52 | Object.keys(all).should.eql( 53 | ['version', ...components, 'ripple', 'default'] 54 | ) 55 | }) 56 | 57 | describe('Register components', function () { 58 | const Vue1 = Vue.extend() 59 | components.forEach(name => { 60 | Vue1.use(VueMdc) 61 | it(`registers Mdc${name}`, comp(Vue1, `Mdc${name}`)) 62 | }) 63 | 64 | it('registers v-ripple', dir(Vue1, 'ripple')) 65 | }) 66 | 67 | describe('Prefixes', function () { 68 | const Vue2 = Vue.extend() 69 | before(function () { 70 | VueMdc.installed = false 71 | Vue2.use(VueMdc, { 72 | prefix: 'Ui', 73 | }) 74 | }) 75 | components.forEach(name => { 76 | it(`registers Ui${name}`, comp(Vue2, `Ui${name}`)) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/specs/ripple.spec.js: -------------------------------------------------------------------------------- 1 | import ripple from 'src/ripple.js' 2 | import { createVM } from '../helpers/utils.js' 3 | import { nextTick } from '../helpers/wait-for-update.js' 4 | 5 | describe('Ripple', function () { 6 | it('upgrades a div', function (done) { 7 | const vm = createVM(this, function (h) { 8 | return ( 9 |
Simple
10 | ) 11 | }, { 12 | directives: { ripple }, 13 | }) 14 | nextTick().then(() => { 15 | const ripple = vm.$('.my-ripple') 16 | ripple.should.have.class('mdc-ripple-surface') 17 | }).then(() => { 18 | const ripple = vm.$('.my-ripple') 19 | ripple.should.have.class('mdc-ripple-upgraded') 20 | }).then(done) 21 | }) 22 | 23 | it('can use custom class', function (done) { 24 | const vm = createVM( 25 | this, 26 | `
Custom
`, 27 | { directives: { ripple }} 28 | ) 29 | nextTick().then(() => { 30 | const ripple = vm.$('.my-ripple') 31 | ripple.should.not.have.class('mdc-ripple-surface') 32 | }).then(() => { 33 | const ripple = vm.$('.my-ripple') 34 | ripple.should.have.class('mdc-ripple-upgraded') 35 | }).then(done) 36 | }) 37 | 38 | it('unbounds events and clases', function (done) { 39 | const vm = createVM(this, function (h) { 40 | const opts = { 41 | directives: [], 42 | } 43 | if (this.show) { 44 | opts.directives.push({ 45 | name: 'ripple', 46 | }) 47 | } 48 | return ( 49 |
Unbind
50 | ) 51 | }, { 52 | data: { 53 | show: true, 54 | }, 55 | directives: { ripple }, 56 | }) 57 | vm.show = false 58 | nextTick().then(() => { 59 | const ripple = vm.$('.my-ripple') 60 | ripple.should.not.have.class('mdc-ripple-upgraded') 61 | ripple.should.not.have.class('mdc-ripple-surface') 62 | }).then(done) 63 | }) 64 | 65 | it('works with regular elements', function (done) { 66 | const vm = createVM( 67 | this, 68 | `
A div
`, 69 | { 70 | data: { 71 | text: 'foo', 72 | }, 73 | directives: { ripple }, 74 | } 75 | ) 76 | const oldEl = vm.$('.my-ripple') 77 | vm.text = 'bar' 78 | nextTick().then(() => { 79 | // we need to wait an extra tick 80 | }).then(() => { 81 | const ripple = vm.$('.my-ripple') 82 | oldEl.should.not.equal(ripple) 83 | ripple.should.have.class('mdc-ripple-upgraded') 84 | ripple.should.have.class('mdc-ripple-surface') 85 | }).then(done) 86 | }) 87 | 88 | it('works with regular components', function (done) { 89 | const vm = createVM( 90 | this, 91 | `A foo`, 92 | { 93 | data: { 94 | text: 'foo', 95 | }, 96 | directives: { ripple }, 97 | components: { 98 | Foo: { 99 | template: '
', 100 | }, 101 | }, 102 | } 103 | ) 104 | const oldEl = vm.$('.my-ripple') 105 | vm.text = 'bar' 106 | nextTick().then(() => { 107 | // we need to wait an extra tick 108 | }).then(() => { 109 | const ripple = vm.$('.my-ripple') 110 | oldEl.should.not.equal(ripple) 111 | ripple.should.have.class('mdc-ripple-upgraded') 112 | ripple.should.have.class('mdc-ripple-surface') 113 | }).then(done) 114 | }) 115 | 116 | it('works with functional components', function (done) { 117 | const vm = createVM( 118 | this, 119 | `{{text}}`, 120 | { 121 | data: { 122 | text: 'foo', 123 | }, 124 | directives: { ripple }, 125 | components: { 126 | Foo: { 127 | functional: true, 128 | render: (h, { data, children }) => h('div', data, children), 129 | }, 130 | }, 131 | } 132 | ) 133 | const oldEl = vm.$('.my-ripple') 134 | vm.text = 'bar' 135 | nextTick().then(() => { 136 | // we need to wait an extra tick 137 | }).then(() => { 138 | const ripple = vm.$('.my-ripple') 139 | // The same object is reused 140 | oldEl.should.equal(ripple) 141 | ripple.should.have.class('mdc-ripple-upgraded') 142 | ripple.should.have.class('mdc-ripple-surface') 143 | }).then(done) 144 | }) 145 | 146 | it('works when the node is reused', function (done) { 147 | const vm = createVM( 148 | this, 149 | `
{{text}}
`, 150 | { 151 | data: { 152 | text: 'foo', 153 | }, 154 | directives: { ripple }, 155 | } 156 | ) 157 | const oldEl = vm.$('.my-ripple') 158 | vm.text = 'bar' 159 | nextTick().then(() => { 160 | // we need to wait an extra tick 161 | }).then(() => { 162 | const ripple = vm.$('.my-ripple') 163 | // The same object is reused 164 | oldEl.should.equal(ripple) 165 | ripple.should.have.class('mdc-ripple-upgraded') 166 | ripple.should.have.class('mdc-ripple-surface') 167 | }).then(done) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /test/visual.js: -------------------------------------------------------------------------------- 1 | /* global requestAnimationFrame */ 2 | import 'style-loader!css-loader!mocha-css' 3 | import 'style-loader!css-loader!./helpers/style.css' 4 | 5 | // create a div where mocha can add its stuff 6 | const mochaDiv = document.createElement('DIV') 7 | mochaDiv.id = 'mocha' 8 | document.body.appendChild(mochaDiv) 9 | 10 | import 'mocha/mocha.js' 11 | import sinon from 'sinon' 12 | import chai from 'chai' 13 | window.mocha.setup({ 14 | ui: 'bdd', 15 | slow: 750, 16 | timeout: 5000, 17 | globals: [ 18 | '__VUE_DEVTOOLS_INSTANCE_MAP__', 19 | 'script', 20 | 'inject', 21 | 'originalOpenFunction', 22 | // some chrome plugins things 23 | 'pb_blacklist', 24 | 'pb_whitelist', 25 | 'pb_isRunning', 26 | ], 27 | }) 28 | window.sinon = sinon 29 | chai.use(require('chai-dom')) 30 | chai.use(require('sinon-chai')) 31 | window.should = chai.should() 32 | 33 | let vms = [] 34 | let testId = 0 35 | 36 | beforeEach(function () { 37 | this.DOMElement = document.createElement('DIV') 38 | this.DOMElement.id = `test-${++testId}` 39 | document.body.appendChild(this.DOMElement) 40 | }) 41 | 42 | afterEach(function () { 43 | const testReportElements = document.getElementsByClassName('test') 44 | const lastReportElement = testReportElements[testReportElements.length - 1] 45 | 46 | if (!lastReportElement) return 47 | const el = document.getElementById(this.DOMElement.id) 48 | if (el) lastReportElement.appendChild(el) 49 | // Save the vm to hide it later 50 | if (this.DOMElement.vm) vms.push(this.DOMElement.vm) 51 | }) 52 | 53 | // Hide all tests at the end to prevent some weird bugs 54 | before(function () { 55 | vms = [] 56 | testId = 0 57 | }) 58 | after(function () { 59 | requestAnimationFrame(function () { 60 | setTimeout(function () { 61 | vms.forEach(vm => { 62 | // Hide if test passed 63 | if (vm.$el.parentElement && 64 | !vm.$el.parentElement.classList.contains('fail')) { 65 | vm.$children[0].visible = false 66 | } 67 | }) 68 | }, 100) 69 | }) 70 | }) 71 | 72 | const specsContext = require.context('./specs', true) 73 | specsContext.keys().forEach(specsContext) 74 | 75 | window.mocha.checkLeaks() 76 | window.mocha.run() 77 | --------------------------------------------------------------------------------