├── .circleci └── config.yml ├── .gitignore ├── README.md ├── lerna.json ├── netlify.toml ├── package.json ├── packages └── @birdseye │ ├── app │ ├── .browserslistrc │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .postcssrc.js │ ├── README.md │ ├── babel.config.js │ ├── dummy-birdseye-loader.js │ ├── jest.config.js │ ├── package.json │ ├── prettier.config.js │ ├── public │ │ └── index.html │ ├── regconfig.json │ ├── src │ │ ├── App.vue │ │ ├── Preview.vue │ │ ├── augment.d.ts │ │ ├── components │ │ │ ├── BaseInput.vue │ │ │ ├── BaseInputText.vue │ │ │ ├── BaseSelect.vue │ │ │ ├── ButtonPlusMinus.vue │ │ │ ├── InputArray.vue │ │ │ ├── InputBoolean.vue │ │ │ ├── InputEmpty.vue │ │ │ ├── InputNumber.vue │ │ │ ├── InputObject.vue │ │ │ ├── InputProperty.vue │ │ │ ├── InputPropertyObject.vue │ │ │ ├── InputPropertyPrimitive.vue │ │ │ ├── InputString.vue │ │ │ ├── NavSide.vue │ │ │ ├── PanelPattern.vue │ │ │ └── PanelPatternGroup.vue │ │ ├── main.ts │ │ ├── router.ts │ │ ├── store.ts │ │ ├── types │ │ │ ├── shims-tsx.d.ts │ │ │ ├── shims-vue.d.ts │ │ │ ├── vue-transition-components.d.ts │ │ │ └── vue-web-component-wrapper.d.ts │ │ └── utils.ts │ ├── tests │ │ ├── dummy │ │ │ ├── augment.d.ts │ │ │ ├── catalogs │ │ │ │ ├── BaseInput.catalog.ts │ │ │ │ ├── BaseInputText.catalog.ts │ │ │ │ ├── BaseSelect.catalog.ts │ │ │ │ ├── ButtonPlusMinus.catalog.ts │ │ │ │ ├── PanelPattern.catalog.ts │ │ │ │ ├── ScopedSlot.catalog.ts │ │ │ │ └── ScopedSlot.vue │ │ │ ├── components │ │ │ │ ├── Fill.vue │ │ │ │ ├── Foo.vue │ │ │ │ └── Test.vue │ │ │ ├── css.d.ts │ │ │ ├── main.ts │ │ │ └── style.css │ │ ├── unit │ │ │ ├── .eslintrc.js │ │ │ ├── components │ │ │ │ ├── BaseInput.spec.ts │ │ │ │ ├── BaseInputText.spec.ts │ │ │ │ ├── BaseSelect.spec.ts │ │ │ │ ├── ButtonPlusMinus.spec.ts │ │ │ │ ├── InputArray.spec.ts │ │ │ │ ├── InputBoolean.spec.ts │ │ │ │ ├── InputNumber.spec.ts │ │ │ │ ├── InputObject.spec.ts │ │ │ │ ├── InputProperty.spec.ts │ │ │ │ ├── InputPropertyObject.spec.ts │ │ │ │ ├── InputPropertyPrimitive.spec.ts │ │ │ │ ├── InputString.spec.ts │ │ │ │ ├── NavSide.spec.ts │ │ │ │ ├── PanelPattern.spec.ts │ │ │ │ ├── PanelPatternGroup.spec.ts │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── BaseInput.spec.ts.snap │ │ │ │ │ ├── NavSide.spec.ts.snap │ │ │ │ │ ├── PanelPattern.spec.ts.snap │ │ │ │ │ └── PanelPatternGroup.spec.ts.snap │ │ │ ├── setup.ts │ │ │ └── store.spec.ts │ │ └── visual │ │ │ └── capture.js │ ├── tsconfig.json │ └── vue.config.js │ ├── core │ ├── .eslintrc.yml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── build.js │ ├── jest.config.js │ ├── package.json │ ├── prettier.config.js │ ├── src │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── meta.ts │ │ └── vue-shims.d.ts │ ├── test │ │ ├── .eslintrc.yml │ │ ├── meta.spec.ts │ │ └── setup.ts │ ├── tsconfig.json │ ├── tsconfig.main.json │ └── tsconfig.test.json │ ├── snapshot │ ├── .eslintrc.yml │ ├── .gitignore │ ├── .prettierignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── prettier.config.js │ ├── src │ │ ├── index.ts │ │ ├── page-context.ts │ │ ├── plugin.ts │ │ └── tsconfig.json │ ├── test │ │ ├── .eslintrc.yml │ │ ├── __image_snapshots__ │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-1-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-10-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-11-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-12-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-2-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-3-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-4-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-5-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-6-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-7-snap.png │ │ │ ├── index-spec-ts-snapshot-outputs-images-to-the-default-location-8-snap.png │ │ │ └── index-spec-ts-snapshot-outputs-images-to-the-default-location-9-snap.png │ │ ├── fixture │ │ │ ├── augment.d.ts │ │ │ ├── catalogs │ │ │ │ ├── Active.catalog.ts │ │ │ │ ├── Animation.catalog.ts │ │ │ │ ├── Fill.catalog.ts │ │ │ │ ├── Fixed.catalog.ts │ │ │ │ ├── Foo.catalog.ts │ │ │ │ └── Hover.catalog.ts │ │ │ ├── components │ │ │ │ ├── Active.vue │ │ │ │ ├── Animation.vue │ │ │ │ ├── Fill.vue │ │ │ │ ├── Fixed.vue │ │ │ │ ├── Foo.vue │ │ │ │ └── Hover.vue │ │ │ ├── main.ts │ │ │ ├── shims.d.ts │ │ │ └── style.css │ │ ├── index.spec.ts │ │ └── jest-setup.ts │ ├── tsconfig.json │ └── tsconfig.test.json │ └── vue │ ├── .eslintrc.yml │ ├── .gitignore │ ├── .prettierignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── prettier.config.js │ ├── src │ ├── catalog.ts │ ├── extract-props.ts │ ├── index.ts │ ├── instrument.ts │ ├── tsconfig.json │ └── webpack-loader.ts │ ├── test │ ├── .eslintrc.yml │ ├── extract-props.spec.ts │ ├── instrument.spec.ts │ ├── webpack-loader.spec.ts │ └── wrap.spec.ts │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── webpack-loader.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/working/repo 5 | docker: 6 | - image: circleci/node:12-browsers 7 | 8 | jobs: 9 | install: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | 14 | # Download and cache dependencies 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "yarn.lock" }} 18 | # fallback to using the latest cache if no exact match is found 19 | - v1-dependencies- 20 | 21 | - run: yarn global add lerna 22 | - run: yarn bootstrap 23 | 24 | - save_cache: 25 | paths: 26 | - node_modules 27 | key: v1-dependencies-{{ checksum "yarn.lock" }} 28 | 29 | - persist_to_workspace: 30 | root: ~/working 31 | paths: 32 | - repo 33 | 34 | test: 35 | <<: *defaults 36 | steps: 37 | - attach_workspace: 38 | at: ~/working 39 | - run: yarn test 40 | 41 | visual-test: 42 | <<: *defaults 43 | steps: 44 | - attach_workspace: 45 | at: ~/working 46 | - run: yarn test:visual 47 | 48 | workflows: 49 | version: 2 50 | build_and_test: 51 | jobs: 52 | - install 53 | - test: 54 | requires: 55 | - install 56 | - visual-test: 57 | requires: 58 | - install 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Birdseye 2 | 3 | **This project is under work in progress. Some features may not be implemented or have bugs. APIs would be changed near the future** 4 | 5 | Next generation component catalog. 6 | 7 | ## Concept 8 | 9 | - No need to write source code for each component guide. 10 | - Essentially both build tool and view library agnostic. 11 | - Use Web Components instead of iframe to encapsulate styles for better dev experience. 12 | 13 | ## Getting Started 14 | 15 | Birdseye currently supports the following view libraries. Please refer the guide for a lib that you are using. 16 | 17 | - Vue.js 18 | 19 | ### Vue.js with Vue CLI v3 20 | 21 | Install `@birdseye/app` and `@birdseye/vue` in your Vue CLI project: 22 | 23 | ```bash 24 | $ npm i -D @birdseye/app @birdseye/vue 25 | ``` 26 | 27 | #### Writing catalog by code 28 | 29 | Create catalog file: 30 | 31 | ```js 32 | // birdseye/catalogs/MyButton.catalog.js 33 | import { catalogFor } from '@birdseye/vue' 34 | import MyButton from '@/components/MyButton.vue' 35 | 36 | export default catalogFor(MyButton, 'MyButton') 37 | .add('primary', { 38 | props: { 39 | primary: true 40 | }, 41 | slots: { 42 | default: 'Button Text' 43 | } 44 | }) 45 | .add('hovered state', { 46 | data: { 47 | hover: true 48 | }, 49 | slots: { 50 | default: 'Hovered' 51 | } 52 | }) 53 | ``` 54 | 55 | Make `birdseye/preview.js` and bootstrap component catalog: 56 | 57 | ```js 58 | // birdseye/preview.js 59 | import birdseye from '@birdseye/app' 60 | 61 | // Load all your catalogs 62 | const load = ctx => ctx.keys().map(x => ctx(x).default) 63 | const catalogs = load(require.context('./catalogs/', true, /\.catalog\.js$/)) 64 | 65 | // Mount component catalog 66 | birdseye('#app', catalogs) 67 | ``` 68 | 69 | Serve the component catalog by running vue-cli-service: 70 | 71 | ```bash 72 | $ npm run serve -- birdseye/preview.js 73 | ``` 74 | 75 | #### Tweaking preview container style 76 | 77 | When you want to modify preview container style (such as padding, background-color etc.), specify `containerStyle` option in your catalog: 78 | 79 | ```js 80 | import { catalogFor } from '@birdseye/vue' 81 | import MyButton from '@/components/MyButton.vue' 82 | 83 | export default catalogFor(MyButton, 'MyButton').add('white button', { 84 | props: { 85 | white: true 86 | }, 87 | slots: { 88 | default: 'Button Text' 89 | }, 90 | 91 | // Make background color black 92 | containerStyle: { 93 | backgroundColor: 'black' 94 | } 95 | }) 96 | ``` 97 | 98 | The above example makes the preview background color black. You can specify any CSS properties in `containerStyle` option. 99 | 100 | #### Injecting slots and scoped slots 101 | 102 | You can inject slots and scoped slots by using `slots` option of `add` function. Pass a template string for a slot: 103 | 104 | ```js 105 | import { catalogFor } from '@birdseye/vue' 106 | import MyButton from '@/components/MyButton.vue' 107 | 108 | export default catalogFor(MyButton, 'MyButton') 109 | .add('primary', { 110 | props: { 111 | primary: true 112 | }, 113 | slots: { 114 | // Directly pass template string for the slot 115 | default: 'Button Text' 116 | } 117 | }) 118 | ``` 119 | 120 | Pass a function for a scoped slot. The first argument is injected scoped slot props. You can get `$createElement` helper via `this` context: 121 | 122 | ```js 123 | import { catalogFor } from '@birdseye/vue' 124 | import MyButton from '@/components/MyButton.vue' 125 | 126 | export default catalogFor(MyButton, 'MyButton') 127 | .add('primary', { 128 | props: { 129 | primary: true 130 | }, 131 | slots: { 132 | // Pass scoped slot function 133 | default(props) { 134 | const h = this.$createElement 135 | return h('span', ['Button Text ', props.message]) 136 | } 137 | } 138 | }) 139 | ``` 140 | 141 | #### Wrapping catalog component with another element 142 | 143 | You can use `mapRender` option to modify rendered element structure. `mapRender` should be a function that receives [`createElement` function](https://vuejs.org/v2/guide/render-function.html#createElement-Arguments) and original VNode object as arguments respectively. 144 | 145 | The following example maps the catalog component with `VApp` component of [Vuetify](https://vuetifyjs.com). 146 | 147 | ```js 148 | import { catalogFor } from '@birdseye/vue' 149 | import { VApp } from 'vuetify/lib' 150 | import MyButton from '@/components/MyButton.vue' 151 | 152 | export default catalogFor(MyButton, { 153 | name: 'MyButton', 154 | 155 | // Define map function to render 156 | mapRender: (h, vnode) => { 157 | return h(VApp, [vnode]) 158 | } 159 | }).add('white button', { 160 | props: { 161 | white: true 162 | }, 163 | slots: { 164 | default: 'Button Text' 165 | } 166 | }) 167 | ``` 168 | 169 | #### Writing catalog in SFC 170 | 171 | Update the webpack config in `vue.config.js`: 172 | 173 | ```js 174 | module.exports = { 175 | chainWebpack: config => { 176 | if (process.env.NODE_ENV !== 'production') { 177 | // Process custom block with @birdseye/vue/webpack-loader 178 | // prettier-ignore 179 | config.module 180 | .rule('birdseye-vue') 181 | .resourceQuery(/blockType=birdseye/) 182 | .use('birdseye-vue-loader') 183 | .loader('@birdseye/vue/webpack-loader') 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | You write rendering patterns of a component in `` custom block. In each `patterns` item, you can specify `props` and `data` value which will be passed to the component. For example, in the following code, component guide will have two patterns for this component (named `Test Component`). The first pattern (`Pattern Name 1`) shows the component having `"First pattern"` as `foo` value and `123` as `bar` value. 190 | 191 | ```vue 192 | 199 | 200 | 216 | 217 | 218 | { 219 | "name": "Test Component", 220 | "patterns": [ 221 | { 222 | "name": "Pattern Name 1", 223 | "props": { 224 | "foo": "First pattern" 225 | }, 226 | "data": { 227 | "bar": 123 228 | } 229 | }, 230 | { 231 | "name": "Pattern Name 2", 232 | "props": { 233 | "foo": "Second pattern" 234 | }, 235 | "data": { 236 | "bar": 456 237 | } 238 | } 239 | ] 240 | } 241 | 242 | ``` 243 | 244 | Finally, make `birdseye/preview.js` and bootstrap component catalog: 245 | 246 | ```js 247 | import birdseye from '@birdseye/app' 248 | import { instrument } from '@birdseye/vue' 249 | 250 | // Load all your component 251 | const load = ctx => ctx.keys().map(x => ctx(x).default) 252 | const components = load(require.context('../src/components', true, /\.vue$/)) 253 | 254 | // Mount component catalog 255 | birdseye('#app', instrument(components)) 256 | ``` 257 | 258 | You can serve the component catalog by running vue-cli-service: 259 | 260 | ```bash 261 | $ npm run serve -- birdseye/preview.js 262 | ``` 263 | 264 | ## Visual Regression Testing 265 | 266 | If you want to visual regression test your component catalog, use `@birdseye/snapshot` package. See [docs](packages/@birdseye/snapshot/README.md) of `@birdseye/snapshot` 267 | 268 | ## License 269 | 270 | MIT 271 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": { 3 | "publish": { 4 | "npmClient": "npm" 5 | } 6 | }, 7 | "npmClient": "yarn", 8 | "useWorkspaces": true, 9 | "version": "0.9.3" 10 | } 11 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "packages/@birdseye/app/dist" 3 | command = "yarn build && yarn catalog" 4 | environment = { NODE_ENV = "development" } 5 | ignore = "git log -1 --pretty=%B | grep dependabot" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "birdseye", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/@birdseye/*" 6 | ], 7 | "scripts": { 8 | "bootstrap": "lerna bootstrap && yarn build", 9 | "test": "lerna run test", 10 | "test:visual": "lerna run test:visual", 11 | "build": "lerna run build", 12 | "catalog": "lerna run catalog" 13 | }, 14 | "devDependencies": { 15 | "lerna": "^3.2.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/@birdseye/app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 -------------------------------------------------------------------------------- /packages/@birdseye/app/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib -------------------------------------------------------------------------------- /packages/@birdseye/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | jsx: true, 5 | }, 6 | extends: 'ktsn-vue', 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /coverage 5 | /.reg 6 | /snapshots 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | -------------------------------------------------------------------------------- /packages/@birdseye/app/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/@birdseye/app/README.md: -------------------------------------------------------------------------------- 1 | # @birdseye/app 2 | 3 | [![@birdseye/app Dev Token](https://badge.devtoken.rocks/@birdseye/app)](https://devtoken.rocks/package/@birdseye/app) 4 | 5 | ## Project setup 6 | ``` 7 | npm install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | npm run build 18 | ``` 19 | 20 | ### Lints and fixes files 21 | ``` 22 | npm run lint 23 | ``` 24 | 25 | ### Run your unit tests 26 | ``` 27 | npm run test:unit 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/@birdseye/app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /packages/@birdseye/app/dummy-birdseye-loader.js: -------------------------------------------------------------------------------- 1 | // temporary mock custom block because 2 | // vue-loader unexpectedly load it even though 3 | // there are no corresponding loader for it. 4 | // See https://github.com/vuejs/vue-loader/issues/1394 5 | module.exports = () => '' 6 | -------------------------------------------------------------------------------- /packages/@birdseye/app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | setupFiles: ['/tests/unit/setup.ts'], 4 | collectCoverageFrom: ['src/**/*.{ts,tsx,vue}', '!**/*.d.ts'], 5 | } 6 | -------------------------------------------------------------------------------- /packages/@birdseye/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@birdseye/app", 3 | "version": "0.9.3", 4 | "author": "katashin", 5 | "description": "Application of Birdseye", 6 | "keywords": [ 7 | "Birdseye", 8 | "app", 9 | "component", 10 | "styleguide" 11 | ], 12 | "main": "dist/app.common.js", 13 | "files": [ 14 | "dist" 15 | ], 16 | "homepage": "https://github.com/ktsn/birdseye", 17 | "bugs": "https://github.com/ktsn/birdseye/issues", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/ktsn/birdseye.git" 21 | }, 22 | "license": "MIT", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "scripts": { 27 | "serve": "vue-cli-service serve tests/dummy/main.ts", 28 | "catalog": "vue-cli-service build tests/dummy/main.ts", 29 | "build": "vue-cli-service build --target lib --name app src/main.ts", 30 | "lint": "vue-cli-service lint --no-fix", 31 | "lint:fix": "vue-cli-service lint", 32 | "test": "yarn lint && yarn test:unit", 33 | "test:unit": "vue-cli-service test:unit", 34 | "test:watch": "yarn test:unit --watch", 35 | "test:visual": "node tests/visual/capture.js && reg-suit run", 36 | "prepare": "yarn build", 37 | "prepublishOnly": "yarn test" 38 | }, 39 | "dependencies": { 40 | "@birdseye/core": "^0.9.0", 41 | "@vue/web-component-wrapper": "^1.2.0", 42 | "k-css": "^3.0.0", 43 | "vue-lazy-components-option": "^0.2.0", 44 | "vue-router": "^2.0.0 || ^3.0.0", 45 | "vue-transition-components": "^0.2.2" 46 | }, 47 | "peerDependencies": { 48 | "vue": "^2.0.0" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.0.0", 52 | "@birdseye/vue": "^0.9.3", 53 | "@types/jest": "^26.0.0", 54 | "@types/webpack-env": "^1.13.7", 55 | "@vue/cli-plugin-babel": "^4.0.2", 56 | "@vue/cli-plugin-eslint": "^4.0.2", 57 | "@vue/cli-plugin-typescript": "^4.0.2", 58 | "@vue/cli-plugin-unit-jest": "^4.0.2", 59 | "@vue/cli-service": "^4.0.2", 60 | "@vue/eslint-config-typescript": "^7.0.0", 61 | "@vue/test-utils": "^1.0.0-beta.24", 62 | "babel-core": "7.0.0-bridge.0", 63 | "babel-jest": "^26.0.1", 64 | "core-js": "^3.5.0", 65 | "eslint": "^7.0.0", 66 | "eslint-config-ktsn-vue": "^2.0.0", 67 | "eslint-loader": "^4.0.0", 68 | "jest": "^26.0.1", 69 | "lerna": "^3.2.1", 70 | "prettier": "2.2.1", 71 | "prettier-config-ktsn": "^1.0.0", 72 | "reg-keygen-git-hash-plugin": "^0.10.3", 73 | "reg-notify-github-plugin": "^0.10.7", 74 | "reg-publish-s3-plugin": "^0.10.7", 75 | "reg-suit": "^0.10.0", 76 | "rimraf": "^3.0.0", 77 | "ts-jest": "^26.0.0", 78 | "typescript": "^4.0.2", 79 | "vue": "^2.6.7", 80 | "vue-template-compiler": "^2.6.7", 81 | "webpack": "^4.17.1" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/@birdseye/app/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('prettier-config-ktsn') 2 | -------------------------------------------------------------------------------- /packages/@birdseye/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Birdseye App 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/@birdseye/app/regconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": { 3 | "workingDir": ".reg", 4 | "actualDir": "snapshots", 5 | "thresholdRate": 0, 6 | "addIgnore": true, 7 | "ximgdiff": { 8 | "invocationType": "client" 9 | } 10 | }, 11 | "plugins": { 12 | "reg-keygen-git-hash-plugin": true, 13 | "reg-notify-github-plugin": { 14 | "clientId": "MzQxsjQzNrW00E/KLEopTq1M1Te1MDKwMDLVzy4pzgMA" 15 | }, 16 | "reg-publish-s3-plugin": { 17 | "bucketName": "birdseye-app-snapshots" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 96 | 97 | 98 | 99 | 112 | 113 | 165 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/Preview.vue: -------------------------------------------------------------------------------- 1 | 63 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/augment.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | declare module 'vue/types/vue' { 4 | interface Vue { 5 | $_birdseye_experimental: boolean 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/BaseInput.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 127 | 128 | 138 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/BaseInputText.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 32 | 61 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/BaseSelect.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/ButtonPlusMinus.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | 63 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputArray.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 68 | 69 | 78 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputBoolean.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputEmpty.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputNumber.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputObject.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 67 | 68 | 77 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputProperty.vue: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputPropertyObject.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 67 | 68 | 92 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputPropertyPrimitive.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 54 | 55 | 74 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/InputString.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/NavSide.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 95 | 96 | 153 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/PanelPattern.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 54 | 55 | 68 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/components/PanelPatternGroup.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 67 | 68 | 94 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import wrap from '@vue/web-component-wrapper' 3 | import { Catalog } from '@birdseye/core' 4 | import LazyComponents from 'vue-lazy-components-option' 5 | import router from './router' 6 | import AppStore from './store' 7 | import App from './App.vue' 8 | 9 | export interface BirdseyePlugin { 10 | (catalogs: Catalog[]): void 11 | } 12 | 13 | export interface BirdseyeOptions { 14 | experimental?: boolean 15 | plugins?: BirdseyePlugin[] 16 | } 17 | 18 | Vue.use(LazyComponents) 19 | 20 | const appTagName = 'birdseye-app' 21 | const Root = Vue.extend({ router }) 22 | 23 | Vue.config.ignoredElements = [appTagName] 24 | window.customElements.define(appTagName, wrap(Root, App)) 25 | 26 | export default function birdseye( 27 | el: string | Element, 28 | catalogs: Catalog[], 29 | options: BirdseyeOptions = {} 30 | ): void { 31 | Vue.prototype.$_birdseye_experimental = !!options.experimental 32 | 33 | const app = document.createElement(appTagName) as any 34 | 35 | const content = document.createElement('div') 36 | app.appendChild(content) 37 | 38 | const wrapper = typeof el === 'string' ? document.querySelector(el) : el 39 | wrapper!.appendChild(app) 40 | 41 | const store = new AppStore({ 42 | declarations: catalogs.map((c) => c.toDeclaration()), 43 | fullscreen: !!router.currentRoute.query.fullscreen, 44 | }) 45 | 46 | app.store = store 47 | 48 | new Root({ 49 | el: content, 50 | render: (h) => 51 | // Preview.vue 52 | h('router-view', { 53 | attrs: { 54 | store, 55 | }, 56 | }), 57 | }) 58 | 59 | // Execute plugins 60 | if (options.plugins) { 61 | options.plugins.forEach((f) => f(catalogs)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Preview from './Preview.vue' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | routes: [ 9 | { 10 | name: 'preview', 11 | path: '/:meta/:pattern', 12 | component: Preview, 13 | props: true, 14 | }, 15 | ], 16 | }) 17 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/store.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | ComponentDeclaration, 4 | ComponentPattern, 5 | ComponentMeta, 6 | ComponentDataType, 7 | } from '@birdseye/core' 8 | import { dedupe } from './utils' 9 | 10 | class Store { 11 | private vm: Vue 12 | 13 | constructor(initialState: S) { 14 | this.vm = new Vue({ 15 | data: initialState, 16 | }) 17 | } 18 | 19 | get state(): S { 20 | return this.vm.$data as S 21 | } 22 | 23 | set>( 24 | target: T, 25 | key: K, 26 | value: T[K] 27 | ): void { 28 | this.vm.$set(target as any, key as any, value) 29 | } 30 | } 31 | 32 | interface AppState { 33 | declarations: ComponentDeclaration[] 34 | fullscreen: boolean 35 | } 36 | 37 | export interface QualifiedData { 38 | type: ComponentDataType[] 39 | name: string 40 | value: any 41 | } 42 | 43 | /** 44 | * Light-weight store to handle changing pattern values 45 | */ 46 | export default class AppStore extends Store { 47 | getMeta(metaName: string): ComponentMeta | undefined { 48 | const decl = this.state.declarations.find((d) => d.meta.name === metaName) 49 | return decl && decl.meta 50 | } 51 | 52 | getPattern( 53 | metaName: string, 54 | patternName: string 55 | ): ComponentPattern | undefined { 56 | const meta = this.getMeta(metaName) 57 | 58 | if (!meta) return 59 | 60 | return meta.patterns.find((p) => p.name === patternName) 61 | } 62 | 63 | getQualifiedProps(metaName: string, patternName?: string): QualifiedData[] { 64 | return this.genericQualifiedData(metaName, patternName, 'props') 65 | } 66 | 67 | getQualifiedData(metaName: string, patternName?: string): QualifiedData[] { 68 | return this.genericQualifiedData(metaName, patternName, 'data') 69 | } 70 | 71 | fullscreen(): boolean { 72 | return this.state.fullscreen 73 | } 74 | 75 | updatePropValue( 76 | meta: string, 77 | pattern: string, 78 | key: string, 79 | value: any 80 | ): void { 81 | const p = this.getPattern(meta, pattern) 82 | if (!p) return 83 | 84 | this.set(p.props, key, value) 85 | } 86 | 87 | updateDataValue( 88 | meta: string, 89 | pattern: string, 90 | key: string, 91 | value: any 92 | ): void { 93 | const p = this.getPattern(meta, pattern) 94 | if (!p) return 95 | 96 | this.set(p.data, key, value) 97 | } 98 | 99 | private genericQualifiedData( 100 | metaName: string, 101 | patternName: string | undefined, 102 | type: 'props' | 'data' 103 | ): QualifiedData[] { 104 | const meta = this.getMeta(metaName) 105 | if (!meta) return [] 106 | 107 | const pattern = patternName && this.getPattern(metaName, patternName) 108 | const names = dedupe([ 109 | ...Object.keys(meta[type]), 110 | ...(pattern ? Object.keys(pattern[type]) : []), 111 | ]) 112 | 113 | return names.map((name) => { 114 | const info = meta[type][name] 115 | const value = pattern ? pattern[type][name] : undefined 116 | return { 117 | type: info ? info.type : [], 118 | name, 119 | value, 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | /* eslint-disable @typescript-eslint/no-empty-interface */ 6 | interface Element extends VNode {} 7 | interface ElementClass extends Vue {} 8 | interface IntrinsicElements { 9 | [elem: string]: any 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/types/vue-transition-components.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-transition-components' { 2 | import { VueConstructor } from 'vue' 3 | export const TransitionDisclosure: VueConstructor 4 | } 5 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/types/vue-web-component-wrapper.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@vue/web-component-wrapper' { 2 | export default function wrap(...args: any[]): any 3 | } 4 | -------------------------------------------------------------------------------- /packages/@birdseye/app/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDataType } from '@birdseye/core' 2 | 3 | export function dedupe(list: string[]): string[] { 4 | const set: Record = {} 5 | return list.reduce((acc, item) => { 6 | if (set[item]) { 7 | return acc 8 | } 9 | set[item] = true 10 | return acc.concat(item) 11 | }, []) 12 | } 13 | 14 | export function emptyValue(type: string): any { 15 | const map: Record = { 16 | string: '', 17 | number: 0, 18 | boolean: false, 19 | array: [], 20 | object: {}, 21 | null: null, 22 | } 23 | return map[type] 24 | } 25 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/augment.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | declare module 'vue/types/options' { 4 | interface ComponentOptions { 5 | shadowRoot?: Element 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/BaseInput.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import BaseInput from '@/components/BaseInput.vue' 3 | 4 | export default catalogFor(BaseInput, { 5 | name: 'BaseInput', 6 | rootOptions: { 7 | shadowRoot: document.head, 8 | }, 9 | }) 10 | .add('String', { 11 | props: { 12 | value: 'string value', 13 | }, 14 | }) 15 | .add('Number', { 16 | props: { 17 | value: 123, 18 | }, 19 | }) 20 | .add('Boolean', { 21 | props: { 22 | value: true, 23 | }, 24 | }) 25 | .add('Array', { 26 | props: { 27 | value: ['foo', 42, true], 28 | removeTypes: true, 29 | }, 30 | }) 31 | .add('Nested Array', { 32 | props: { 33 | value: ['foo', [1, 2, 3], ['bar', 'baz']], 34 | removeTypes: true, 35 | }, 36 | }) 37 | .add('Object', { 38 | props: { 39 | value: { 40 | a: 'foo', 41 | b: 42, 42 | c: true, 43 | }, 44 | }, 45 | }) 46 | .add('Nested Object', { 47 | props: { 48 | value: { 49 | a: 'foo', 50 | b: { 51 | x: 1, 52 | y: 2, 53 | z: 3, 54 | }, 55 | c: { 56 | test1: 'bar', 57 | test2: 'baz', 58 | }, 59 | }, 60 | removeTypes: true, 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/BaseInputText.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import BaseInputText from '@/components/BaseInputText.vue' 3 | 4 | export default catalogFor(BaseInputText, { 5 | name: 'BaseInputText', 6 | rootOptions: { 7 | shadowRoot: document.head, 8 | }, 9 | }) 10 | .add('with value', { 11 | props: { 12 | value: 'text value', 13 | }, 14 | }) 15 | .add('number type', { 16 | props: { 17 | type: 'number', 18 | value: 42, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/BaseSelect.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import BaseSelect from '@/components/BaseSelect.vue' 3 | 4 | export default catalogFor(BaseSelect, { 5 | name: 'BaseSelect', 6 | rootOptions: { 7 | shadowRoot: document.head, 8 | }, 9 | }).add('default', { 10 | props: { 11 | value: 'bar', 12 | }, 13 | slots: { 14 | default: ` 15 | 16 | 17 | 18 | `, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/ButtonPlusMinus.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import ButtonPlusMinus from '@/components/ButtonPlusMinus.vue' 3 | 4 | export default catalogFor(ButtonPlusMinus, { 5 | name: 'ButtonPlusMinus', 6 | rootOptions: { 7 | shadowRoot: document.head, 8 | }, 9 | }) 10 | .add('plus (default)', { 11 | props: { 12 | type: 'plus', 13 | }, 14 | }) 15 | .add('minus', { 16 | props: { 17 | type: 'minus', 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/PanelPattern.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import PanelPattern from '@/components/PanelPattern.vue' 3 | 4 | export default catalogFor(PanelPattern, { 5 | name: 'PanelPattern', 6 | rootOptions: { 7 | shadowRoot: document.head, 8 | }, 9 | }).add('Normal', { 10 | props: { 11 | props: { 12 | type: ['string'], 13 | name: 'foo', 14 | value: 'foo value', 15 | }, 16 | data: [ 17 | { 18 | type: ['string', 'number'], 19 | name: 'bar', 20 | value: 123, 21 | }, 22 | { 23 | type: ['boolean'], 24 | name: 'baz', 25 | value: true, 26 | }, 27 | ], 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/ScopedSlot.catalog.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { catalogFor } from '@birdseye/vue' 3 | import ScopedSlot from './ScopedSlot.vue' 4 | 5 | const Another = Vue.extend({ 6 | props: { 7 | message: { 8 | type: String, 9 | required: true, 10 | }, 11 | }, 12 | 13 | render(h: Function) { 14 | return h('strong', { style: 'font-weight: bold; color: #ff0000;' }, [ 15 | this.message, 16 | ]) 17 | }, 18 | }) 19 | 20 | export default catalogFor(ScopedSlot, 'ScopedSlot') 21 | .add('inject scoped slot', { 22 | slots: { 23 | default(props: any) { 24 | return [this.$createElement('div', ['message: ', props.message])] 25 | }, 26 | }, 27 | }) 28 | .add('use another component for slot', { 29 | slots: { 30 | default(props: any) { 31 | const h = this.$createElement 32 | return [ 33 | h('div', ['Top']), 34 | h(Another, { props: { message: props.message } }), 35 | h('div', ['Bottom']), 36 | ] 37 | }, 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/catalogs/ScopedSlot.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/components/Fill.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 24 | name: Fill preview area 25 | patterns: 26 | - name: style 27 | containerStyle: 28 | backgroundColor: '#aaa' 29 | height: 100% 30 | 31 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/components/Foo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 24 | 31 | 32 | 33 | { 34 | "name": "Foo component", 35 | "patterns": [ 36 | { 37 | "name": "Normal", 38 | "props": { 39 | "foo": "foo value" 40 | }, 41 | "data": { 42 | "bar": "bar value" 43 | } 44 | }, 45 | { 46 | "name": "Bar number", 47 | "props": { 48 | "foo": "string" 49 | }, 50 | "data": { 51 | "bar": 12345 52 | } 53 | } 54 | ] 55 | } 56 | 57 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/components/Test.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/css.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const _default: any 3 | export default _default 4 | } 5 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import birdseye from '@/main' 3 | import { createInstrument } from '@birdseye/vue' 4 | // @ts-ignore 5 | import { snapshotPlugin } from '../../../snapshot/lib/plugin' // Avoid circular dependencies 6 | import style from './style.css' 7 | 8 | const load = (ctx: any) => ctx.keys().map((x: any) => ctx(x).default) 9 | const components = load(require.context('./components', true, /\.vue$/)) 10 | const catalogs = load(require.context('./catalogs', true, /\.catalog\.ts$/)) 11 | 12 | // For debug 13 | style.__inject__(document.head) 14 | const instrument = createInstrument(Vue, { 15 | shadowRoot: document.head, 16 | }) 17 | 18 | birdseye('#app', catalogs.concat(instrument(components)), { 19 | experimental: true, 20 | plugins: [snapshotPlugin], 21 | }) 22 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/dummy/style.css: -------------------------------------------------------------------------------- 1 | @import '~k-css/k.css'; 2 | 3 | body { 4 | margin: 0; 5 | font-size: 14px; 6 | color: #333; 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | rules: { 6 | 'import/no-extraneous-dependencies': 'off', 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/BaseInput.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount, Wrapper } from '@vue/test-utils' 2 | import BaseInput from '@/components/BaseInput.vue' 3 | import BaseSelect from '@/components/BaseSelect.vue' 4 | import { ComponentDataType } from '@birdseye/core' 5 | 6 | describe('BaseInput', () => { 7 | describe('rendering', () => { 8 | it('removes input', () => { 9 | const wrapper = shallowMount(BaseInput, { 10 | propsData: { 11 | value: 'foo', 12 | removeInput: true, 13 | }, 14 | }) 15 | expect(wrapper.html()).toMatchSnapshot() 16 | }) 17 | 18 | it('removes type selector', () => { 19 | const wrapper = shallowMount(BaseInput, { 20 | propsData: { 21 | value: 'foo', 22 | removeTypes: true, 23 | }, 24 | }) 25 | expect(wrapper.html()).toMatchSnapshot() 26 | }) 27 | }) 28 | 29 | describe('supported types', () => { 30 | it('string', () => { 31 | const wrapper = shallowMount(BaseInput, { 32 | propsData: { 33 | value: 'string value', 34 | }, 35 | }) 36 | expect(wrapper.html()).toMatchSnapshot() 37 | }) 38 | 39 | it('number', () => { 40 | const wrapper = shallowMount(BaseInput, { 41 | propsData: { 42 | value: 123, 43 | }, 44 | }) 45 | expect(wrapper.html()).toMatchSnapshot() 46 | }) 47 | 48 | it('boolean', () => { 49 | const wrapper = shallowMount(BaseInput, { 50 | propsData: { 51 | value: true, 52 | }, 53 | }) 54 | expect(wrapper.html()).toMatchSnapshot() 55 | }) 56 | 57 | it('array', () => { 58 | const wrapper = shallowMount(BaseInput, { 59 | propsData: { 60 | value: ['foo', 42, true], 61 | }, 62 | }) 63 | expect(wrapper.html()).toMatchSnapshot() 64 | }) 65 | 66 | it('object', () => { 67 | const wrapper = shallowMount(BaseInput, { 68 | propsData: { 69 | value: { 70 | a: 'foo', 71 | b: 42, 72 | c: true, 73 | }, 74 | }, 75 | }) 76 | expect(wrapper.html()).toMatchSnapshot() 77 | }) 78 | 79 | it('null', () => { 80 | const wrapper = shallowMount(BaseInput, { 81 | propsData: { 82 | value: null, 83 | }, 84 | }) 85 | expect(wrapper.html()).toMatchSnapshot() 86 | }) 87 | 88 | it('undefined', () => { 89 | const wrapper = shallowMount(BaseInput, { 90 | propsData: { 91 | value: undefined, 92 | }, 93 | }) 94 | expect(wrapper.html()).toMatchSnapshot() 95 | }) 96 | }) 97 | 98 | describe('events', () => { 99 | const Test = { 100 | name: 'Test', 101 | render(h: Function): any { 102 | return h() 103 | }, 104 | } 105 | 106 | it('ports input event from the field', () => { 107 | const wrapper = shallowMount(BaseInput, { 108 | propsData: { 109 | value: 'str', 110 | availableTypes: ['string'], 111 | }, 112 | stubs: { 113 | InputString: Test, 114 | }, 115 | }) 116 | 117 | wrapper.findComponent(Test).vm.$emit('input', 'updated') 118 | expect(wrapper.emitted('input')![0][0]).toBe('updated') 119 | }) 120 | }) 121 | 122 | describe('will emit input event with empty value when the type is changed', () => { 123 | function changeType(wrapper: Wrapper, type: ComponentDataType): void { 124 | const select = wrapper.findComponent(BaseSelect) 125 | select.vm.$emit('change', type) 126 | } 127 | 128 | let wrapper: Wrapper 129 | beforeEach(() => { 130 | wrapper = shallowMount(BaseInput, { 131 | propsData: { 132 | value: 'str', 133 | }, 134 | }) 135 | }) 136 | 137 | it('string', () => { 138 | changeType(wrapper, 'string') 139 | expect(wrapper.emitted('input')![0][0]).toBe('') 140 | }) 141 | 142 | it('number', () => { 143 | changeType(wrapper, 'number') 144 | expect(wrapper.emitted('input')![0][0]).toBe(0) 145 | }) 146 | 147 | it('boolean', () => { 148 | changeType(wrapper, 'boolean') 149 | expect(wrapper.emitted('input')![0][0]).toBe(false) 150 | }) 151 | 152 | it('array', () => { 153 | changeType(wrapper, 'array') 154 | expect(wrapper.emitted('input')![0][0]).toEqual([]) 155 | }) 156 | 157 | it('object', () => { 158 | changeType(wrapper, 'object') 159 | expect(wrapper.emitted('input')![0][0]).toEqual({}) 160 | }) 161 | 162 | it('null', () => { 163 | changeType(wrapper, 'null') 164 | expect(wrapper.emitted('input')![0][0]).toEqual(null) 165 | }) 166 | 167 | it('undefined', () => { 168 | changeType(wrapper, 'undefined') 169 | expect(wrapper.emitted('input')![0][0]).toEqual(undefined) 170 | }) 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/BaseInputText.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import BaseInputText from '@/components/BaseInputText.vue' 3 | 4 | describe('BaseInputText', () => { 5 | it('ports value', () => { 6 | const wrapper = shallowMount(BaseInputText, { 7 | propsData: { 8 | value: 'test', 9 | }, 10 | }) 11 | const input = wrapper.find('input').element as HTMLInputElement 12 | expect(input.value).toBe('test') 13 | }) 14 | 15 | it('propagates input event', () => { 16 | const wrapper = shallowMount(BaseInputText, { 17 | propsData: { 18 | value: 'test', 19 | }, 20 | }) 21 | const input = wrapper.find('input') 22 | ;(input.element as HTMLInputElement).value = 'updated' 23 | input.trigger('input') 24 | expect(wrapper.emitted('input')![0][0]).toBe('updated') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/BaseSelect.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import BaseSelect from '@/components/BaseSelect.vue' 3 | 4 | describe('BaseSelect', () => { 5 | it('ports value', () => { 6 | const wrapper = shallowMount(BaseSelect, { 7 | propsData: { 8 | value: 'bar', 9 | }, 10 | slots: { 11 | default: ` 12 | 13 | 14 | 15 | `, 16 | }, 17 | }) 18 | const select = wrapper.find('select').element as HTMLSelectElement 19 | expect(select.value).toBe('bar') 20 | }) 21 | 22 | it('propagates change event', () => { 23 | const wrapper = shallowMount(BaseSelect, { 24 | propsData: { 25 | value: 'bar', 26 | }, 27 | slots: { 28 | default: ` 29 | 30 | 31 | 32 | `, 33 | }, 34 | }) 35 | const option = wrapper.find('#foo').element as HTMLOptionElement 36 | option.selected = true 37 | wrapper.find('select').trigger('change') 38 | expect(wrapper.emitted('change')![0][0]).toBe('foo') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/ButtonPlusMinus.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import ButtonPlusMinus from '@/components/ButtonPlusMinus.vue' 3 | 4 | describe('ButtonPlusMinus', () => { 5 | it('propagates click event', () => { 6 | const wrapper = shallowMount(ButtonPlusMinus) 7 | wrapper.find('button').trigger('click') 8 | expect(wrapper.emitted('click')!.length).toBe(1) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputArray.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import InputArray from '@/components/InputArray.vue' 3 | 4 | describe('InputArray', () => { 5 | const InputProperty = { 6 | name: 'InputProperty', 7 | props: ['value'], 8 | render(this: any, h: Function) { 9 | return h('input', { 10 | attrs: { 11 | 'data-value': this.value, 12 | }, 13 | }) 14 | }, 15 | } 16 | 17 | it('applies array item value for each input', () => { 18 | const wrapper = shallowMount(InputArray, { 19 | propsData: { 20 | value: ['foo', 1, true], 21 | }, 22 | stubs: { 23 | InputProperty, 24 | }, 25 | }) 26 | 27 | const inputs = wrapper.findAllComponents(InputProperty) 28 | expect(inputs.at(0).props().value).toBe('foo') 29 | expect(inputs.at(1).props().value).toBe(1) 30 | expect(inputs.at(2).props().value).toBe(true) 31 | }) 32 | 33 | it('emits input event when an item is added', () => { 34 | const wrapper = shallowMount(InputArray, { 35 | propsData: { 36 | value: ['foo'], 37 | }, 38 | }) 39 | 40 | wrapper.find('[aria-label="Add"]').vm.$emit('click') 41 | expect(wrapper.emitted('input')![0][0]).toEqual(['foo', undefined]) 42 | }) 43 | 44 | it('emits input event when an item is removed', () => { 45 | const wrapper = shallowMount(InputArray, { 46 | propsData: { 47 | value: ['foo', 'bar', 'baz'], 48 | }, 49 | stubs: { 50 | InputProperty, 51 | }, 52 | }) 53 | 54 | wrapper.findAllComponents(InputProperty).at(1).vm.$emit('remove') 55 | expect(wrapper.emitted('input')![0][0]).toEqual(['foo', 'baz']) 56 | }) 57 | 58 | it('emits input event when an item is updated', () => { 59 | const wrapper = shallowMount(InputArray, { 60 | propsData: { 61 | value: ['foo', 'bar', 'baz'], 62 | }, 63 | stubs: { 64 | InputProperty, 65 | }, 66 | }) 67 | 68 | const bar = wrapper.findAllComponents(InputProperty).at(1) 69 | bar.vm.$emit('input', 'updated') 70 | expect(wrapper.emitted('input')![0][0]).toEqual(['foo', 'updated', 'baz']) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputBoolean.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { shallowMount } from '@vue/test-utils' 3 | import InputBoolean from '@/components/InputBoolean.vue' 4 | 5 | describe('InputBoolean', () => { 6 | it('applies input prop to actual element checked', async () => { 7 | const wrapper = shallowMount(InputBoolean, { 8 | propsData: { 9 | value: true, 10 | }, 11 | }) 12 | const input = wrapper.find('input').element as HTMLInputElement 13 | expect(input.checked).toBe(true) 14 | 15 | wrapper.setProps({ 16 | value: false, 17 | }) 18 | await Vue.nextTick() 19 | expect(input.checked).toBe(false) 20 | }) 21 | 22 | it('emit input event when checkbox is changed', () => { 23 | const wrapper = shallowMount(InputBoolean, { 24 | propsData: { 25 | value: true, 26 | }, 27 | }) 28 | const input = wrapper.find('input') 29 | ;(input.element as HTMLInputElement).checked = false 30 | input.trigger('change') 31 | 32 | expect(wrapper.emitted('input')![0][0]).toBe(false) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputNumber.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import InputNumber from '@/components/InputNumber.vue' 3 | import BaseInputText from '@/components/BaseInputText.vue' 4 | 5 | describe('InputNumber', () => { 6 | it('applies input prop to actual element', () => { 7 | const wrapper = shallowMount(InputNumber, { 8 | propsData: { 9 | value: 123, 10 | }, 11 | }) 12 | const input = wrapper.findComponent(BaseInputText) 13 | expect(input.props().value).toBe('123') 14 | }) 15 | 16 | it('ports input event with converting value to number', () => { 17 | const wrapper = shallowMount(InputNumber, { 18 | propsData: { 19 | value: 123, 20 | }, 21 | }) 22 | const input = wrapper.findComponent(BaseInputText) 23 | input.vm.$emit('input', 456) 24 | 25 | expect(wrapper.emitted('input')![0][0]).toBe(456) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputObject.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import InputObject from '@/components/InputObject.vue' 3 | 4 | describe('InputObject', () => { 5 | const InputProperty = { 6 | name: 'InputProperty', 7 | props: ['name', 'value'], 8 | render(this: any, h: Function) { 9 | return h('input', { 10 | attrs: { 11 | 'data-name': this.name, 12 | 'data-value': this.value, 13 | }, 14 | }) 15 | }, 16 | } 17 | 18 | it('applies array item value for each input', () => { 19 | const wrapper = shallowMount(InputObject, { 20 | propsData: { 21 | value: { 22 | a: 'foo', 23 | b: 1, 24 | c: true, 25 | }, 26 | }, 27 | stubs: { 28 | InputProperty, 29 | }, 30 | }) 31 | 32 | const inputs = wrapper.findAllComponents(InputProperty) 33 | expect(inputs.at(0).props().name).toBe('a') 34 | expect(inputs.at(1).props().name).toBe('b') 35 | expect(inputs.at(2).props().name).toBe('c') 36 | expect(inputs.at(0).props().value).toBe('foo') 37 | expect(inputs.at(1).props().value).toBe(1) 38 | expect(inputs.at(2).props().value).toBe(true) 39 | }) 40 | 41 | it('emits input event when an item is added', () => { 42 | const wrapper = shallowMount(InputObject, { 43 | propsData: { 44 | value: { 45 | a: 'foo', 46 | }, 47 | }, 48 | }) 49 | 50 | wrapper.find('[aria-label="Add"]').vm.$emit('click') 51 | expect(wrapper.emitted('input')![0][0]).toEqual({ 52 | a: 'foo', 53 | '': undefined, 54 | }) 55 | }) 56 | 57 | it('emits input event when an item is removed', () => { 58 | const wrapper = shallowMount(InputObject, { 59 | propsData: { 60 | value: { 61 | a: 'foo', 62 | b: 'bar', 63 | c: 'baz', 64 | }, 65 | }, 66 | stubs: { 67 | InputProperty, 68 | }, 69 | }) 70 | 71 | wrapper.findAllComponents(InputProperty).at(1).vm.$emit('remove') 72 | expect(wrapper.emitted('input')![0][0]).toEqual({ 73 | a: 'foo', 74 | c: 'baz', 75 | }) 76 | }) 77 | 78 | it('emits input event when an item is updated', () => { 79 | const wrapper = shallowMount(InputObject, { 80 | propsData: { 81 | value: { 82 | a: 'foo', 83 | b: 'bar', 84 | c: 'baz', 85 | }, 86 | }, 87 | stubs: { 88 | InputProperty, 89 | }, 90 | }) 91 | 92 | const bar = wrapper.findAllComponents(InputProperty).at(1) 93 | bar.vm.$emit('input', 'updated') 94 | expect(wrapper.emitted('input')![0][0]).toEqual({ 95 | a: 'foo', 96 | b: 'updated', 97 | c: 'baz', 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputProperty.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import InputProperty from '@/components/InputProperty.vue' 3 | import InputPropertyPrimitive from '@/components/InputPropertyPrimitive.vue' 4 | import InputPropertyObject from '@/components/InputPropertyObject.vue' 5 | 6 | describe('InputProperty', () => { 7 | it('treats null as primitive', () => { 8 | const wrapper = shallowMount(InputProperty, { 9 | context: { 10 | attrs: { 11 | name: 'propname', 12 | value: null, 13 | }, 14 | }, 15 | }) 16 | expect(wrapper.findComponent(InputPropertyPrimitive).exists()).toBe(true) 17 | }) 18 | 19 | it('treats array as object', () => { 20 | const wrapper = shallowMount(InputProperty, { 21 | context: { 22 | attrs: { 23 | name: 'propname', 24 | value: [], 25 | }, 26 | }, 27 | }) 28 | expect(wrapper.findComponent(InputPropertyObject).exists()).toBe(true) 29 | }) 30 | 31 | it('treats object as object', () => { 32 | const wrapper = shallowMount(InputProperty, { 33 | context: { 34 | attrs: { 35 | name: 'propname', 36 | value: {}, 37 | }, 38 | }, 39 | }) 40 | expect(wrapper.findComponent(InputPropertyObject).exists()).toBe(true) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputPropertyObject.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount, Wrapper } from '@vue/test-utils' 2 | import InputPropertyObject from '@/components/InputPropertyObject.vue' 3 | import BaseInput from '@/components/BaseInput.vue' 4 | 5 | describe('InputPropertyObject', () => { 6 | function findTypeInput(wrapper: Wrapper) { 7 | return wrapper 8 | .findAllComponents(BaseInput) 9 | .wrappers.find((w) => !w.props().removeType)! 10 | } 11 | 12 | function findValueInput(wrapper: Wrapper) { 13 | return wrapper 14 | .findAllComponents(BaseInput) 15 | .wrappers.find((w) => !w.props().removeInput)! 16 | } 17 | 18 | it('ports value to the form component', () => { 19 | const wrapper = shallowMount(InputPropertyObject, { 20 | propsData: { 21 | name: 'propname', 22 | value: ['foo', 'bar'], 23 | }, 24 | }) 25 | wrapper.findAllComponents(BaseInput).wrappers.forEach((input) => { 26 | expect(input.props().value).toEqual(['foo', 'bar']) 27 | }) 28 | }) 29 | 30 | it('ports available types to the form component', () => { 31 | const wrapper = shallowMount(InputPropertyObject, { 32 | propsData: { 33 | name: 'propname', 34 | value: ['test'], 35 | availableTypes: ['array', 'object'], 36 | }, 37 | }) 38 | const input = findTypeInput(wrapper) 39 | expect(input.props().availableTypes).toEqual(['array', 'object']) 40 | }) 41 | 42 | it('listens input events from the form component', () => { 43 | const wrapper = shallowMount(InputPropertyObject, { 44 | propsData: { 45 | name: 'propname', 46 | value: ['foo'], 47 | }, 48 | }) 49 | const type = findTypeInput(wrapper) 50 | type.vm.$emit('input', {}) 51 | 52 | const value = findValueInput(wrapper) 53 | value.vm.$emit('input', ['bar']) 54 | 55 | expect(wrapper.emitted('input')![0][0]).toEqual({}) 56 | expect(wrapper.emitted('input')![1][0]).toEqual(['bar']) 57 | }) 58 | 59 | it('listens remove events', () => { 60 | const wrapper = shallowMount(InputPropertyObject, { 61 | propsData: { 62 | name: 'propname', 63 | value: ['foo'], 64 | }, 65 | }) 66 | wrapper.find('[aria-label="Remove"]').vm.$emit('click') 67 | expect(wrapper.emitted('remove')!.length).toBe(1) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputPropertyPrimitive.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import InputPropertyPrimitive from '@/components/InputPropertyPrimitive.vue' 3 | import BaseInput from '@/components/BaseInput.vue' 4 | 5 | describe('InputPropertyPrimitive', () => { 6 | it('ports value to the form component', () => { 7 | const wrapper = shallowMount(InputPropertyPrimitive, { 8 | propsData: { 9 | name: 'propname', 10 | value: 'foo', 11 | }, 12 | }) 13 | const input = wrapper.findComponent(BaseInput) 14 | expect(input.props().value).toBe('foo') 15 | }) 16 | 17 | it('ports available types to the form component', () => { 18 | const wrapper = shallowMount(InputPropertyPrimitive, { 19 | propsData: { 20 | name: 'propname', 21 | value: 'test', 22 | availableTypes: ['string', 'number'], 23 | }, 24 | }) 25 | const input = wrapper.findComponent(BaseInput) 26 | expect(input.props().availableTypes).toEqual(['string', 'number']) 27 | }) 28 | 29 | it('listens input events from the form component', () => { 30 | const wrapper = shallowMount(InputPropertyPrimitive, { 31 | propsData: { 32 | name: 'propname', 33 | value: 'foo', 34 | }, 35 | }) 36 | const input = wrapper.findComponent(BaseInput) 37 | input.vm.$emit('input', 'bar') 38 | expect(wrapper.emitted('input')![0][0]).toBe('bar') 39 | }) 40 | 41 | it('listens remove events', () => { 42 | const wrapper = shallowMount(InputPropertyPrimitive, { 43 | propsData: { 44 | name: 'propname', 45 | value: 'foo', 46 | }, 47 | }) 48 | wrapper.find('[aria-label="Remove"]').vm.$emit('click') 49 | expect(wrapper.emitted('remove')!.length).toBe(1) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/InputString.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import InputString from '@/components/InputString.vue' 3 | import BaseInputText from '@/components/BaseInputText.vue' 4 | 5 | describe('InputString', () => { 6 | it('applies input prop to actual element', () => { 7 | const wrapper = shallowMount(InputString, { 8 | propsData: { 9 | value: 'prop value', 10 | }, 11 | }) 12 | const input = wrapper.findComponent(BaseInputText) 13 | expect(input.props().value).toBe('prop value') 14 | }) 15 | 16 | it('ports input event', () => { 17 | const wrapper = shallowMount(InputString, { 18 | propsData: { 19 | value: 'prop value', 20 | }, 21 | }) 22 | const input = wrapper.findComponent(BaseInputText) 23 | input.vm.$emit('input', 'updated') 24 | 25 | expect(wrapper.emitted('input')![0][0]).toBe('updated') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/NavSide.spec.ts: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import { mount, createLocalVue } from '@vue/test-utils' 3 | import NavSide from '@/components/NavSide.vue' 4 | 5 | describe('NavSide', () => { 6 | const nav = [ 7 | { 8 | name: 'Foo', 9 | patterns: [ 10 | { 11 | name: 'Pattern 1', 12 | }, 13 | { 14 | name: 'Pattern 2', 15 | }, 16 | ], 17 | }, 18 | { 19 | name: 'Bar', 20 | patterns: [ 21 | { 22 | name: 'Pattern 1', 23 | }, 24 | ], 25 | }, 26 | ] 27 | 28 | let localVue: any 29 | 30 | beforeEach(() => { 31 | const router = new Router({ 32 | routes: [ 33 | { 34 | name: 'preview', 35 | path: '/:meta/:pattern?', 36 | component: {}, 37 | props: true, 38 | }, 39 | ], 40 | }) 41 | localVue = createLocalVue().extend({ router }) 42 | localVue.use(Router) 43 | }) 44 | 45 | it('renders declarations', () => { 46 | const wrapper = mount(NavSide, { 47 | localVue, 48 | propsData: { 49 | nav, 50 | }, 51 | }) 52 | 53 | expect(wrapper.html()).toMatchSnapshot() 54 | }) 55 | 56 | it('highlight current component', () => { 57 | const wrapper = mount(NavSide, { 58 | localVue, 59 | propsData: { 60 | nav, 61 | meta: 'Foo', 62 | }, 63 | }) 64 | 65 | expect(wrapper.html()).toMatchSnapshot() 66 | }) 67 | 68 | it('highlight current pattern', () => { 69 | const wrapper = mount(NavSide, { 70 | localVue, 71 | propsData: { 72 | nav, 73 | meta: 'Foo', 74 | pattern: 'Pattern 1', 75 | }, 76 | }) 77 | 78 | expect(wrapper.html()).toMatchSnapshot() 79 | }) 80 | 81 | it('avoid including children dom when patterns are empty', () => { 82 | const wrapper = mount(NavSide, { 83 | localVue, 84 | propsData: { 85 | nav: [ 86 | { 87 | name: 'Empty', 88 | patterns: [], 89 | }, 90 | { 91 | name: 'Empty Selected', 92 | patterns: [], 93 | }, 94 | ], 95 | meta: 'Empty Selected', 96 | }, 97 | }) 98 | 99 | expect(wrapper.html()).toMatchSnapshot() 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/PanelPattern.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import PanelPattern from '@/components/PanelPattern.vue' 3 | 4 | describe('PanelPattern', () => { 5 | const StubGroup = { 6 | name: 'PanelPatternGroup', 7 | props: ['title', 'data'], 8 | 9 | render(this: any, h: Function): any { 10 | return h('div', [ 11 | h('p', [`title: ${this.title}`]), 12 | h('p', [`data: ${JSON.stringify(this.data)}`]), 13 | ]) 14 | }, 15 | } 16 | 17 | it('renders for props and data', () => { 18 | const wrapper = shallowMount(PanelPattern, { 19 | propsData: { 20 | props: [ 21 | { 22 | type: ['string'], 23 | name: 'foo', 24 | value: 'string value', 25 | }, 26 | ], 27 | data: [ 28 | { 29 | type: [], 30 | name: 'bar', 31 | value: true, 32 | }, 33 | ], 34 | }, 35 | stubs: { 36 | PanelPatternGroup: StubGroup, 37 | }, 38 | }) 39 | expect(wrapper.html()).toMatchSnapshot() 40 | }) 41 | 42 | it('propagates events from props', () => { 43 | const wrapper = shallowMount(PanelPattern, { 44 | propsData: { 45 | props: [ 46 | { 47 | type: ['string', 'number'], 48 | name: 'foo', 49 | value: 'str', 50 | }, 51 | ], 52 | data: [], 53 | }, 54 | stubs: { 55 | PanelPatternGroup: StubGroup, 56 | }, 57 | }) 58 | const group = wrapper.findAllComponents(StubGroup).at(0) 59 | group.vm.$emit('input', { 60 | name: 'foo', 61 | value: 'test', 62 | }) 63 | expect(wrapper.emitted('input-prop')![0][0]).toEqual({ 64 | name: 'foo', 65 | value: 'test', 66 | }) 67 | }) 68 | 69 | it('propagates events from data', () => { 70 | const wrapper = shallowMount(PanelPattern, { 71 | propsData: { 72 | props: [], 73 | data: [ 74 | { 75 | type: [], 76 | name: 'foo', 77 | value: 'str', 78 | }, 79 | ], 80 | }, 81 | stubs: { 82 | PanelPatternGroup: StubGroup, 83 | }, 84 | }) 85 | const group = wrapper.findAllComponents(StubGroup).at(1) 86 | group.vm.$emit('input', { 87 | name: 'foo', 88 | value: 'test', 89 | }) 90 | expect(wrapper.emitted('input-data')![0][0]).toEqual({ 91 | name: 'foo', 92 | value: 'test', 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/PanelPatternGroup.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import PanelPatternGroup from '@/components/PanelPatternGroup.vue' 3 | 4 | describe('PanelPatternGroup', () => { 5 | const dummyData = [ 6 | { 7 | type: ['string'], 8 | name: 'foo', 9 | value: 'string value', 10 | }, 11 | { 12 | type: ['number'], 13 | name: 'bar', 14 | value: 123, 15 | }, 16 | { 17 | type: ['string', 'number'], 18 | name: 'baz', 19 | value: 'test', 20 | }, 21 | ] 22 | 23 | const StubInputProperty = { 24 | name: 'InputProperty', 25 | render(h: Function) { 26 | return h() 27 | }, 28 | } 29 | 30 | it('renders title and data', () => { 31 | const wrapper = shallowMount(PanelPatternGroup, { 32 | propsData: { 33 | title: 'title string', 34 | data: dummyData, 35 | }, 36 | }) 37 | expect(wrapper.html()).toMatchSnapshot() 38 | }) 39 | 40 | it('renders no data when data is empty', () => { 41 | const wrapper = shallowMount(PanelPatternGroup, { 42 | propsData: { 43 | title: 'no data', 44 | data: [], 45 | }, 46 | }) 47 | expect(wrapper.html()).toMatchSnapshot() 48 | }) 49 | 50 | it('propagates input events', () => { 51 | const wrapper = shallowMount(PanelPatternGroup, { 52 | propsData: { 53 | title: 'title', 54 | data: dummyData, 55 | }, 56 | stubs: { 57 | InputProperty: StubInputProperty, 58 | }, 59 | }) 60 | 61 | const input = wrapper.findAllComponents(StubInputProperty).at(1) 62 | input.vm.$emit('input', 456) 63 | expect(wrapper.emitted('input')![0][0]).toEqual({ 64 | name: 'bar', 65 | value: 456, 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/__snapshots__/BaseInput.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BaseInput rendering removes input 1`] = ` 4 |
5 | 6 | 7 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 29 |
30 | `; 31 | 32 | exports[`BaseInput rendering removes type selector 1`] = ` 33 |
34 | 35 | 36 |
37 | `; 38 | 39 | exports[`BaseInput supported types array 1`] = ` 40 |
41 | 42 | 43 | 46 | 49 | 52 | 55 | 58 | 61 | 64 | 65 |
66 | `; 67 | 68 | exports[`BaseInput supported types boolean 1`] = ` 69 |
70 | 71 | 72 | 75 | 78 | 81 | 84 | 87 | 90 | 93 | 94 |
95 | `; 96 | 97 | exports[`BaseInput supported types null 1`] = ` 98 |
99 | 100 | 101 | 104 | 107 | 110 | 113 | 116 | 119 | 122 | 123 |
124 | `; 125 | 126 | exports[`BaseInput supported types number 1`] = ` 127 |
128 | 129 | 130 | 133 | 136 | 139 | 142 | 145 | 148 | 151 | 152 |
153 | `; 154 | 155 | exports[`BaseInput supported types object 1`] = ` 156 |
157 | 158 | 159 | 162 | 165 | 168 | 171 | 174 | 177 | 180 | 181 |
182 | `; 183 | 184 | exports[`BaseInput supported types string 1`] = ` 185 |
186 | 187 | 188 | 191 | 194 | 197 | 200 | 203 | 206 | 209 | 210 |
211 | `; 212 | 213 | exports[`BaseInput supported types undefined 1`] = ` 214 |
215 | 216 | 217 | 220 | 223 | 226 | 229 | 232 | 235 | 238 | 239 |
240 | `; 241 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/__snapshots__/NavSide.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NavSide avoid including children dom when patterns are empty 1`] = ` 4 | 20 | `; 21 | 22 | exports[`NavSide highlight current component 1`] = ` 23 | 54 | `; 55 | 56 | exports[`NavSide highlight current pattern 1`] = ` 57 | 88 | `; 89 | 90 | exports[`NavSide renders declarations 1`] = ` 91 | 122 | `; 123 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/__snapshots__/PanelPattern.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PanelPattern renders for props and data 1`] = ` 4 |
5 |
6 |

title: props

7 |

data: [{"type":["string"],"name":"foo","value":"string value"}]

8 |
9 |
10 |

title: data

11 |

data: [{"type":[],"name":"bar","value":true}]

12 |
13 |
14 | `; 15 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/components/__snapshots__/PanelPatternGroup.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PanelPatternGroup renders no data when data is empty 1`] = ` 4 |
5 | no data 6 | 7 |

No Data

8 |
9 | `; 10 | 11 | exports[`PanelPatternGroup renders title and data 1`] = ` 12 |
13 | title string 14 | 15 |
    16 |
  • 17 | 18 |
  • 19 |
  • 20 | 21 |
  • 22 |
  • 23 | 24 |
  • 25 |
26 |
27 | `; 28 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/setup.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import LazyComponents from 'vue-lazy-components-option' 3 | import { config } from '@vue/test-utils' 4 | 5 | Vue.use(LazyComponents) 6 | 7 | Vue.prototype.$_birdseye_experimental = true 8 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/unit/store.spec.ts: -------------------------------------------------------------------------------- 1 | import AppStore from '@/store' 2 | 3 | describe('AppStore', () => { 4 | describe('qualified values', () => { 5 | let store: AppStore 6 | beforeEach(() => { 7 | store = new AppStore({ 8 | fullscreen: false, 9 | declarations: [ 10 | { 11 | Wrapper: {} as any, 12 | meta: { 13 | name: 'foo', 14 | props: { 15 | a: { 16 | type: ['string'], 17 | }, 18 | b: { 19 | type: ['string', 'number'], 20 | }, 21 | }, 22 | data: { 23 | c: { 24 | type: ['boolean'], 25 | }, 26 | }, 27 | patterns: [ 28 | { 29 | name: 'pattern 1', 30 | props: { 31 | a: 'test1', 32 | b: 123, 33 | }, 34 | data: { 35 | c: true, 36 | }, 37 | slots: {}, 38 | containerStyle: {}, 39 | plugins: {}, 40 | }, 41 | { 42 | name: 'pattern 2', 43 | props: { 44 | a: 'test2', 45 | b: '123', 46 | }, 47 | data: { 48 | c: false, 49 | }, 50 | slots: {}, 51 | containerStyle: {}, 52 | plugins: {}, 53 | }, 54 | ], 55 | }, 56 | }, 57 | { 58 | Wrapper: {} as any, 59 | meta: { 60 | name: 'bar', 61 | props: {}, 62 | data: { 63 | test1: { 64 | type: ['string'], 65 | }, 66 | }, 67 | patterns: [ 68 | { 69 | name: 'pattern 3', 70 | props: {}, 71 | data: { 72 | test2: 'test value', 73 | }, 74 | slots: {}, 75 | containerStyle: {}, 76 | plugins: {}, 77 | }, 78 | ], 79 | }, 80 | }, 81 | ], 82 | }) 83 | }) 84 | 85 | it('qualifies props', () => { 86 | const props = store.getQualifiedProps('foo', 'pattern 1') 87 | expect(props).toEqual([ 88 | { 89 | type: ['string'], 90 | name: 'a', 91 | value: 'test1', 92 | }, 93 | { 94 | type: ['string', 'number'], 95 | name: 'b', 96 | value: 123, 97 | }, 98 | ]) 99 | }) 100 | 101 | it('qualifies data', () => { 102 | const data = store.getQualifiedData('foo', 'pattern 2') 103 | expect(data).toEqual([ 104 | { 105 | type: ['boolean'], 106 | name: 'c', 107 | value: false, 108 | }, 109 | ]) 110 | }) 111 | 112 | it('qualifies props without pattern', () => { 113 | const props = store.getQualifiedProps('foo') 114 | expect(props).toEqual([ 115 | { 116 | type: ['string'], 117 | name: 'a', 118 | value: undefined, 119 | }, 120 | { 121 | type: ['string', 'number'], 122 | name: 'b', 123 | value: undefined, 124 | }, 125 | ]) 126 | }) 127 | 128 | it('qualifies data without pattern', () => { 129 | const data = store.getQualifiedData('foo') 130 | expect(data).toEqual([ 131 | { 132 | type: ['boolean'], 133 | name: 'c', 134 | value: undefined, 135 | }, 136 | ]) 137 | }) 138 | 139 | it('merges meta and pattern properties', () => { 140 | const data = store.getQualifiedData('bar', 'pattern 3') 141 | expect(data).toEqual([ 142 | { 143 | type: ['string'], 144 | name: 'test1', 145 | value: undefined, 146 | }, 147 | { 148 | type: [], 149 | name: 'test2', 150 | value: 'test value', 151 | }, 152 | ]) 153 | }) 154 | }) 155 | 156 | describe('others', () => { 157 | let store: AppStore 158 | beforeEach(() => { 159 | store = new AppStore({ 160 | fullscreen: false, 161 | declarations: [ 162 | { 163 | Wrapper: {} as any, 164 | meta: { 165 | name: 'foo', 166 | props: {}, 167 | data: {}, 168 | patterns: [ 169 | { 170 | name: 'foo pattern 1', 171 | props: { 172 | a: 'test value', 173 | }, 174 | data: { 175 | b: 123, 176 | c: true, 177 | }, 178 | slots: {}, 179 | containerStyle: { 180 | padding: '0', 181 | }, 182 | plugins: {}, 183 | }, 184 | { 185 | name: 'foo pattern 2', 186 | props: {}, 187 | data: { 188 | b: 456, 189 | }, 190 | slots: {}, 191 | containerStyle: { 192 | backgroundColor: 'black', 193 | }, 194 | plugins: {}, 195 | }, 196 | ], 197 | }, 198 | }, 199 | ], 200 | }) 201 | }) 202 | 203 | it('gets meta', () => { 204 | const m = store.getMeta('foo') 205 | expect(m).toBe(store.state.declarations[0].meta) 206 | }) 207 | 208 | it('gets a pattern', () => { 209 | const p = store.getPattern('foo', 'foo pattern 2') 210 | expect(p).toBe(store.state.declarations[0].meta.patterns[1]) 211 | }) 212 | 213 | it('updates a prop value', () => { 214 | store.updatePropValue('foo', 'foo pattern 1', 'a', 'updated') 215 | expect(store.state.declarations[0].meta.patterns[0].props.a).toBe( 216 | 'updated' 217 | ) 218 | }) 219 | 220 | it('updates a data value', () => { 221 | store.updateDataValue('foo', 'foo pattern 2', 'b', 1000) 222 | expect(store.state.declarations[0].meta.patterns[1].data.b).toBe(1000) 223 | }) 224 | }) 225 | }) 226 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tests/visual/capture.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { spawn } = require('child_process') 3 | const { snapshot } = require('../../../snapshot') // Avoid circular dependencies 4 | const rimraf = require('rimraf') 5 | 6 | function wait(n) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, n) 9 | }) 10 | } 11 | 12 | const snapshotDir = path.resolve(__dirname, '../../snapshots') 13 | 14 | ;(async () => { 15 | rimraf.sync(snapshotDir) 16 | 17 | const cp = spawn('yarn serve', { 18 | cwd: path.resolve(__dirname, '../../'), 19 | shell: true, 20 | stdio: 'ignore', 21 | }) 22 | 23 | await wait(3000) 24 | 25 | await snapshot({ 26 | url: 'http://localhost:8080', 27 | snapshotDir, 28 | }) 29 | 30 | cp.kill() 31 | })() 32 | -------------------------------------------------------------------------------- /packages/@birdseye/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "types": ["node", "jest", "webpack-env"], 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "lib": ["es2015", "dom", "dom.iterable", "scripthost"] 18 | }, 19 | "include": [ 20 | "src/**/*.ts", 21 | "src/**/*.tsx", 22 | "src/**/*.vue", 23 | "tests/**/*.ts", 24 | "tests/**/*.tsx" 25 | ], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/@birdseye/app/vue.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json') 2 | 3 | const externals = Object.keys(pkg.dependencies).concat('vue') 4 | 5 | process.env.VUE_CLI_CSS_SHADOW_MODE = true 6 | 7 | module.exports = { 8 | css: { 9 | loaderOptions: { 10 | postcss: { 11 | config: { 12 | path: __dirname, 13 | }, 14 | }, 15 | }, 16 | }, 17 | 18 | configureWebpack: { 19 | externals: (context, request, callback) => { 20 | if ( 21 | process.env.NODE_ENV === 'production' && 22 | externals.includes(request) 23 | ) { 24 | return callback(null, 'commonjs ' + request) 25 | } 26 | callback() 27 | }, 28 | }, 29 | 30 | chainWebpack: (config) => { 31 | // prettier-ignore 32 | config.module 33 | .rule('vue') 34 | .use('vue-loader') 35 | .loader('vue-loader') 36 | .tap(options => { 37 | options.shadowMode = true 38 | return options 39 | }) 40 | 41 | const birdseyeLoader = 42 | process.env.NODE_ENV !== 'production' 43 | ? '@birdseye/vue/webpack-loader' 44 | : './dummy-birdseye-loader.js' 45 | 46 | // prettier-ignore 47 | config.module 48 | .rule('birdseye-vue') 49 | .resourceQuery(/blockType=birdseye/) 50 | .use('birdseye-vue-loader') 51 | .loader(birdseyeLoader) 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /packages/@birdseye/core/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: ktsn-vue 4 | rules: 5 | typescript/no-type-alias: off -------------------------------------------------------------------------------- /packages/@birdseye/core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log* 3 | 4 | /dist/ 5 | /dist-play/ 6 | /.tmp/ 7 | /.rpt2_cache/ -------------------------------------------------------------------------------- /packages/@birdseye/core/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 katashin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/@birdseye/core/README.md: -------------------------------------------------------------------------------- 1 | # @birdseye/core 2 | 3 | Core modules of Birdseye. 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /packages/@birdseye/core/build.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json') 2 | const { rollup } = require('rollup') 3 | const vue = require('rollup-plugin-vue') 4 | const css = require('rollup-plugin-css-only') 5 | const ts = require('rollup-plugin-typescript2') 6 | 7 | const banner = `/*! 8 | * ${pkg.name} v${pkg.version} 9 | * ${pkg.homepage} 10 | * 11 | * @license 12 | * Copyright (c) 2018 ${pkg.author} 13 | * Released under the MIT license 14 | */` 15 | 16 | function capitalize(name) { 17 | const camelized = name.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()) 18 | return camelized[0].toUpperCase() + camelized.slice(1) 19 | } 20 | 21 | const base = { 22 | input: 'src/index.ts', 23 | output: { 24 | banner, 25 | exports: 'named', 26 | name: capitalize(pkg.name), 27 | globals: { 28 | vue: 'Vue' 29 | } 30 | }, 31 | 32 | external: ['vue'], 33 | plugins: [ 34 | vue({ 35 | css: false 36 | }), 37 | 38 | ts({ 39 | tsconfig: './tsconfig.main.json', 40 | clean: true, 41 | typescript: require('typescript') 42 | }), 43 | 44 | css({ 45 | output: 'dist/core.css' 46 | }) 47 | ] 48 | } 49 | 50 | async function build(type) { 51 | const start = Date.now() 52 | 53 | const bundle = await rollup(base) 54 | 55 | const file = `dist/core.${type}.js` 56 | await bundle.write( 57 | Object.assign({}, base.output, { 58 | file, 59 | format: type 60 | }) 61 | ) 62 | 63 | const end = Date.now() 64 | 65 | console.log(`\u001b[32mcreated ${file} in ${(end - start) / 1000}s\u001b[0m`) 66 | } 67 | 68 | build('cjs') 69 | .then(() => build('es')) 70 | .catch(err => { 71 | console.error(String(err)) 72 | process.exit(1) 73 | }) 74 | -------------------------------------------------------------------------------- /packages/@birdseye/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.[jt]s$': 'ts-jest', 4 | '^.+\\.vue$': 'vue-jest' 5 | }, 6 | setupFiles: ['/test/setup.ts'], 7 | testURL: 'http://localhost', 8 | testRegex: '/test/.+\\.spec\\.(js|ts)$', 9 | moduleNameMapper: { 10 | '^@/(.+)$': '/src/$1', 11 | '^vue$': 'vue/dist/vue.runtime.common.js' 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'json', 'vue'], 14 | collectCoverageFrom: ['src/**/*.{ts,vue}'], 15 | globals: { 16 | 'ts-jest': { 17 | tsConfig: 'tsconfig.test.json' 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/@birdseye/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@birdseye/core", 3 | "version": "0.9.0", 4 | "author": "katashin", 5 | "description": "Core modules of Birdseye", 6 | "keywords": [ 7 | "Birdseye", 8 | "core" 9 | ], 10 | "license": "MIT", 11 | "main": "dist/core.cjs.js", 12 | "module": "dist/core.es.js", 13 | "types": "dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "homepage": "https://github.com/ktsn/birdseye", 18 | "bugs": "https://github.com/ktsn/birdseye/issues", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/ktsn/birdseye.git" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "scripts": { 27 | "prepare": "yarn clean && yarn build", 28 | "prepublishOnly": "yarn test", 29 | "clean": "rm -rf dist", 30 | "play": "poi --open --config play.config.js", 31 | "build": "node build.js", 32 | "build:play": "poi build --config play.config.js", 33 | "lint": "eslint --ext js,ts,vue src test", 34 | "lint:fix": "yarn lint --fix", 35 | "test": "yarn lint && yarn test:unit", 36 | "test:unit": "jest", 37 | "test:watch": "jest --watch" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.0.0", 41 | "@types/jest": "^26.0.0", 42 | "@vue/test-utils": "^1.0.0-beta.24", 43 | "babel-core": "7.0.0-bridge.0", 44 | "eslint": "^7.0.0", 45 | "eslint-config-ktsn-vue": "^2.0.0", 46 | "jest": "^26.0.1", 47 | "postcss": "^8.0.5", 48 | "prettier": "2.2.1", 49 | "prettier-config-ktsn": "^1.0.0", 50 | "rollup": "^2.0.0", 51 | "rollup-plugin-css-only": "^3.0.0", 52 | "rollup-plugin-typescript2": "^0.29.0", 53 | "rollup-plugin-vue": "^5.0.1", 54 | "ts-jest": "^26.0.0", 55 | "typescript": "^4.0.2", 56 | "vue": "^2.6.7", 57 | "vue-jest": "^3.0.5", 58 | "vue-template-compiler": "^2.6.7" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/@birdseye/core/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('prettier-config-ktsn') 2 | -------------------------------------------------------------------------------- /packages/@birdseye/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces' 2 | export { normalizeMeta } from './meta' 3 | -------------------------------------------------------------------------------- /packages/@birdseye/core/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor, VNode } from 'vue' 2 | 3 | export interface Catalog { 4 | toDeclaration(): ComponentDeclaration 5 | } 6 | 7 | export interface ComponentDeclaration { 8 | Wrapper: VueConstructor 9 | meta: ComponentMeta 10 | } 11 | 12 | export interface ComponentMeta { 13 | name: string 14 | props: Record 15 | data: Record 16 | patterns: ComponentPattern[] 17 | } 18 | 19 | export type ComponentDataType = 20 | | 'string' 21 | | 'number' 22 | | 'boolean' 23 | | 'array' 24 | | 'object' 25 | | 'null' 26 | | 'undefined' 27 | 28 | export interface ComponentDataInfo { 29 | type: ComponentDataType[] 30 | defaultValue?: any 31 | } 32 | 33 | export interface ComponentPattern { 34 | name: string 35 | props: Record 36 | data: Record 37 | slots: Record VNode[] | undefined> 38 | containerStyle: Partial 39 | plugins: PluginOptions 40 | } 41 | 42 | // eslint-disable-next-line 43 | export interface PluginOptions { 44 | // For augmentation 45 | } 46 | -------------------------------------------------------------------------------- /packages/@birdseye/core/src/meta.ts: -------------------------------------------------------------------------------- 1 | import { ComponentMeta, ComponentPattern } from './interfaces' 2 | 3 | export function normalizeMeta(meta: any): ComponentMeta { 4 | return { 5 | name: meta.name || '', 6 | props: meta.props || {}, 7 | data: meta.data || {}, 8 | patterns: meta.patterns ? meta.patterns.map(normalizePattern) : [], 9 | } 10 | } 11 | 12 | function normalizePattern(pattern: any): ComponentPattern { 13 | return { 14 | name: pattern.name || '', 15 | props: pattern.props || {}, 16 | data: pattern.data || {}, 17 | slots: pattern.slots || {}, 18 | containerStyle: pattern.containerStyle || {}, 19 | plugins: pattern.plugins || {}, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/@birdseye/core/src/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /packages/@birdseye/core/test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | jest: true 4 | -------------------------------------------------------------------------------- /packages/@birdseye/core/test/meta.spec.ts: -------------------------------------------------------------------------------- 1 | import { normalizeMeta } from '../src/meta' 2 | 3 | describe('Meta', () => { 4 | it('normalizes meta', () => { 5 | const meta = normalizeMeta({}) 6 | 7 | expect(meta.name).toBe('') 8 | expect(meta.props).toEqual({}) 9 | expect(meta.data).toEqual({}) 10 | expect(meta.patterns).toEqual([]) 11 | }) 12 | 13 | it('normalizes pattern', () => { 14 | const meta = normalizeMeta({ 15 | patterns: [{}], 16 | }) 17 | 18 | const { patterns } = meta 19 | const p = patterns[0] 20 | 21 | expect(patterns.length).toBe(1) 22 | expect(p.name).toBe('') 23 | expect(p.props).toEqual({}) 24 | expect(p.data).toEqual({}) 25 | expect(p.slots).toEqual({}) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/@birdseye/core/test/setup.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /packages/@birdseye/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2015", 8 | "dom" 9 | ], 10 | "strict": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["src/*"] 14 | } 15 | }, 16 | "include": [ 17 | "src/**/*.ts", 18 | "src/**/*.vue", 19 | "test/**/*.ts", 20 | "test/**/*.vue" 21 | ] 22 | } -------------------------------------------------------------------------------- /packages/@birdseye/core/tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts", 9 | "src/**/*.vue" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/@birdseye/core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true 6 | } 7 | } -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: ktsn-typescript -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | /birdseye/snapshots -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/.prettierignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | *.json -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 katashin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/README.md: -------------------------------------------------------------------------------- 1 | # @birdseye/snapshot 2 | 3 | Taking snapshots for Birdseye catalog. 4 | 5 | ## Install 6 | 7 | ```sh 8 | $ npm install --save-dev @birdseye/snapshot 9 | ``` 10 | 11 | ## Usage 12 | 13 | Before running capturing process, you need to pass `snapshotPlugin` in `plugins` option of `birdseye` function. 14 | 15 | ```js 16 | import birdseye from '@birdseye/app' 17 | 18 | // Import snapshot plugin 19 | import { snapshotPlugin } from '@birdseye/snapshot/lib/plugin' 20 | 21 | const load = (ctx: any) => ctx.keys().map((x: any) => ctx(x).default) 22 | const catalogs = load(require.context('./catalogs', true, /\.catalog\.ts$/)) 23 | 24 | birdseye('#app', catalogs, { 25 | // Pass the plugin to birdseye function 26 | plugins: [snapshotPlugin] 27 | }) 28 | 29 | ``` 30 | 31 | Next, write `birdseye/capture.js` like below: 32 | 33 | ```js 34 | const path = require('path') 35 | const { spawn } = require('child_process') 36 | const { snapshot } = require('@birdseye/snapshot') 37 | 38 | function wait(n) { 39 | return new Promise(resolve => { 40 | setTimeout(resolve, n) 41 | }) 42 | } 43 | 44 | ;(async () => { 45 | // Run catalog server 46 | const cp = spawn('npm run serve -- birdseye/preview.js', { 47 | cwd: path.resolve(__dirname, '../'), 48 | shell: true, 49 | stdio: 'ignore' 50 | }) 51 | 52 | // Wait until server is ready 53 | await wait(3000) 54 | 55 | // Get snapshots for all component catalogs 56 | await snapshot({ 57 | url: 'http://localhost:8080' 58 | }) 59 | 60 | // Kill the server process 61 | cp.kill() 62 | })() 63 | ``` 64 | 65 | Then run the script with following command: 66 | 67 | ```sh 68 | $ node birdseye/capture.js 69 | ``` 70 | 71 | It will store snapshot images in `birdseye/snapshots` for all component catalogs. You can run visual regression test with the snapshots. 72 | 73 | ### Snapshot Options 74 | 75 | You can specify snapshot options into your catalog to tweak capture behavior. 76 | 77 | ```js 78 | import { catalogFor } from '@birdseye/vue' 79 | import MyButton from '@/components/MyButton.vue' 80 | 81 | export default catalogFor(MyButton, 'MyButton') 82 | .add('primary', { 83 | props: { 84 | primary: true 85 | }, 86 | 87 | slots: { 88 | default: 'Button Text' 89 | }, 90 | 91 | plugins: { 92 | snapshot: { 93 | // Specify snapshot options here 94 | delay: 1000 95 | } 96 | } 97 | }) 98 | ``` 99 | 100 | All options should be into `plugins.snapshot` for each catalog settings. 101 | 102 | Available snapshot options are below: 103 | 104 | - `skip` Set `true` if you want to skip capturing for the catalog. (default `false`) 105 | - `target` CSS selector for the element that will be captured. (default: the root element of the preview) 106 | - `delay` A delay (ms) before taking snapshot. 107 | - `disableCssAnimation` Disable CSS animations and transitions if `true`. (default `true`) 108 | - `capture` A function to define interactions (e.g. `click`, `hover` etc. the an element) before capture. See [Triggering Interaction before Capture](#triggering-interaction-before-capture) for details. 109 | 110 | ### Triggering Interaction before Capture 111 | 112 | There are cases that you want to manipulate a rendered catalog before capturing it. For example, capturing a hover style of a button, a focused style of a text field, etc. 113 | 114 | You can trigger such manipulations with `capture` option: 115 | 116 | ```js 117 | import { catalogFor } from '@birdseye/vue' 118 | import MyButton from '@/components/MyButton.vue' 119 | 120 | export default catalogFor(MyButton, 'MyButton') 121 | .add('primary', { 122 | props: { 123 | primary: true 124 | }, 125 | 126 | slots: { 127 | default: 'Button Text' 128 | }, 129 | 130 | plugins: { 131 | snapshot: { 132 | capture: async (page, capture) => { 133 | // Capture the regular style of the button. 134 | await capture() 135 | 136 | // Trigger a hover for the button. Specify the target elemenet with a CSS selector. 137 | // The below triggers a hover for an element with `my-button` class. 138 | await page.hover('.my-button') 139 | 140 | // Capture the button while it is hovered. 141 | await capture() 142 | } 143 | } 144 | } 145 | }) 146 | ``` 147 | 148 | `capture` option is a function receiving two arguments - a page context and a capture function. The page context has methods to trigger manipulations for an element in the page. They are just aliases of [Puppeteer's ElementHandle methods](https://github.com/puppeteer/puppeteer/blob/v5.3.0/docs/api.md#class-elementhandle) except receiving the selector for the element as the first argument. Available methods are below: 149 | 150 | - click 151 | - focus 152 | - hover 153 | - press 154 | - select 155 | - tap 156 | - type 157 | 158 | The original method arguments are supposed to placed after the second argument. For example, if you write `el.click({ button: 'right' })` with Puppeteer, the equivalent is `page.click('.selector', { button: 'right' })`. 159 | 160 | In addition, [`page.mouse`](https://github.com/puppeteer/puppeteer/blob/v5.3.0/docs/api.md#class-mouse) and [`page.keyboard`](https://github.com/puppeteer/puppeteer/blob/v5.3.0/docs/api.md#class-keyboard) are also exposed under the page context with the same name and the same interface of functions. 161 | 162 | ### Visual Regression Testing with [reg-suit](https://github.com/reg-viz/reg-suit) 163 | 164 | [reg-suit](https://github.com/reg-viz/reg-suit) is a visual regression testing tool which compares snapshot images, stores snapshots on cloud storage (S3, GCS), etc. This section describes how to set up visual regiression testing with reg-suit and @birdseye/snapshot with storing snapshot images on S3. You may also want to read [the example repository of reg-suit](https://github.com/reg-viz/reg-puppeteer-demo). 165 | 166 | Before using reg-suit, setup your AWS credentials. Set environment variables: 167 | 168 | ```sh 169 | export AWS_ACCESS_KEY_ID= 170 | export AWS_SECRET_ACCESS_KEY= 171 | ``` 172 | 173 | Or create a file at `~/.aws/credentials`: 174 | 175 | ```sh 176 | [default] 177 | aws_access_key_id = 178 | aws_secret_access_key = 179 | ``` 180 | 181 | Install reg-suit and execute initialization command. The reg-suit CLI tool asks you several questions for set up: 182 | 183 | ```sh 184 | $ npm install -D reg-suit 185 | $ npx reg-suit init 186 | ``` 187 | 188 | After finishing reg-suit set up, run the following command for visual regression test: 189 | 190 | ```sh 191 | $ node birdseye/capture.js && reg-suit run 192 | ``` 193 | 194 | ## License 195 | 196 | MIT 197 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@birdseye/snapshot", 3 | "version": "0.9.3", 4 | "author": "katashin", 5 | "description": "Taking snapshots for Birdseye catalog", 6 | "keywords": [ 7 | "Birdseye", 8 | "snapshot", 9 | "testing", 10 | "visual regression" 11 | ], 12 | "license": "MIT", 13 | "main": "lib/index.js", 14 | "typings": "lib/index.d.ts", 15 | "files": [ 16 | "lib" 17 | ], 18 | "homepage": "https://github.com/ktsn/birdseye", 19 | "bugs": "https://github.com/ktsn/birdseye/issues", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/ktsn/birdseye.git" 23 | }, 24 | "scripts": { 25 | "prepublishOnly": "npm run clean && npm run test && npm run build", 26 | "clean": "rm -rf lib", 27 | "serve": "vue-cli-service serve test/fixture/main.ts", 28 | "build": "tsc -p src", 29 | "dev": "jest --watch", 30 | "lint": "eslint --ext js,ts src test", 31 | "lint:fix": "eslint --fix --ext js,ts src test", 32 | "test": "npm run lint && npm run test:unit", 33 | "test:unit": "jest" 34 | }, 35 | "jest": { 36 | "transform": { 37 | "^.+\\.ts$": "ts-jest" 38 | }, 39 | "testRegex": "/test/.+\\.spec\\.(js|ts)$", 40 | "moduleFileExtensions": [ 41 | "ts", 42 | "js", 43 | "json" 44 | ], 45 | "setupFilesAfterEnv": [ 46 | "/test/jest-setup.ts" 47 | ], 48 | "globals": { 49 | "ts-jest": { 50 | "tsConfig": "tsconfig.test.json" 51 | } 52 | } 53 | }, 54 | "devDependencies": { 55 | "@birdseye/app": "^0.9.3", 56 | "@types/jest": "^26.0.0", 57 | "@types/jest-image-snapshot": "^4.1.0", 58 | "@types/mkdirp": "^1.0.0", 59 | "@types/puppeteer": "^5.4.0", 60 | "@types/rimraf": "^3.0.0", 61 | "@vue/cli-plugin-typescript": "^4.1.1", 62 | "@vue/cli-service": "^4.1.1", 63 | "eslint": "^7.0.0", 64 | "eslint-config-ktsn-typescript": "^2.0.0", 65 | "jest": "^26.0.1", 66 | "jest-image-snapshot": "^4.0.0", 67 | "prettier": "2.2.1", 68 | "prettier-config-ktsn": "^1.0.0", 69 | "rimraf": "^3.0.0", 70 | "ts-jest": "^26.0.0", 71 | "typescript": "^4.0.2" 72 | }, 73 | "dependencies": { 74 | "@birdseye/core": "^0.9.0", 75 | "@birdseye/vue": "^0.9.3", 76 | "capture-all": "^0.7.1", 77 | "mkdirp": "^1.0.3", 78 | "puppeteer": "^5.2.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('prettier-config-ktsn') 2 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as mkdirp from 'mkdirp' 4 | import * as puppeteer from 'puppeteer' 5 | import { createCaptureStream } from 'capture-all' 6 | import { CatalogRoute } from './plugin' 7 | import { runCapture } from './page-context' 8 | 9 | export interface SnapshotOptions { 10 | url: string 11 | snapshotDir?: string 12 | viewport?: { 13 | width: number 14 | height: number 15 | } 16 | } 17 | 18 | const previewSelector = '#__birdseye_preview__' 19 | 20 | function fillOptionDefaults( 21 | options: SnapshotOptions 22 | ): Required { 23 | return { 24 | snapshotDir: 'birdseye/snapshots', 25 | viewport: { 26 | width: 800, 27 | height: 600, 28 | }, 29 | ...options, 30 | } 31 | } 32 | 33 | export async function snapshot(options: SnapshotOptions): Promise { 34 | const opts = fillOptionDefaults(options) 35 | 36 | const browser = await puppeteer.launch() 37 | const page = await browser.newPage() 38 | await page.goto(opts.url) 39 | 40 | // Get all snapshot options from catalogs. 41 | const routes: CatalogRoute[] = await page.evaluate(() => { 42 | return window.__birdseye_routes__ 43 | }) 44 | 45 | await browser.close() 46 | 47 | return new Promise((resolve, reject) => { 48 | const stream = createCaptureStream( 49 | routes.map((route, i) => { 50 | const snapshot = route.snapshot ?? {} 51 | // capture option becomes '{} | undefined' as Function is not serializable. 52 | const hasCapture = !!snapshot.capture 53 | 54 | return { 55 | url: opts.url + '#' + route.path + '?fullscreen=1', 56 | target: snapshot.target ?? previewSelector, 57 | viewport: opts.viewport, 58 | delay: snapshot.delay, 59 | disableCssAnimation: snapshot.disableCssAnimation, 60 | capture: hasCapture 61 | ? (page, capture) => runCapture(page, capture, i) 62 | : undefined, 63 | } 64 | }) 65 | ) 66 | 67 | mkdirp.sync(opts.snapshotDir) 68 | 69 | stream.on('data', (result) => { 70 | const hash = decodeURIComponent(result.url.split('#')[1]) 71 | const normalized = hash 72 | .slice(1) 73 | .replace(/\?fullscreen=1$/, '') 74 | .replace(/[^0-9a-zA-Z]/g, '_') 75 | const dest = path.join( 76 | opts.snapshotDir, 77 | normalized + '_' + (result.index + 1) + '.png' 78 | ) 79 | 80 | fs.writeFile(dest, result.image, (error) => { 81 | if (error) { 82 | stream.destroy() 83 | reject(error) 84 | } 85 | }) 86 | }) 87 | 88 | stream.on('error', reject) 89 | stream.on('end', resolve) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/src/page-context.ts: -------------------------------------------------------------------------------- 1 | import { Page, ElementHandle, Mouse, Keyboard } from 'puppeteer' 2 | 3 | type WithSelector any> = ( 4 | selector: string, 5 | ...args: Parameters 6 | ) => ReturnType | null 7 | 8 | type ExposedElementHandleKeys = typeof elementHandleKeys[number] 9 | type ExposedMouseKeys = typeof mouseKeys[number] 10 | type ExposedKeyboardKeys = typeof keyboardKeys[number] 11 | 12 | export type ExposedElementHandle = { 13 | [K in ExposedElementHandleKeys]: WithSelector 14 | } 15 | 16 | export interface PageContext extends ExposedElementHandle { 17 | readonly mouse: Pick 18 | readonly keyboard: Pick 19 | } 20 | 21 | const exposedKeyPrefix = '__birdseye_expose_' 22 | const exposedMouseKeyPrefix = exposedKeyPrefix + 'mouse_' 23 | const exposedKeyboardKeyPrefix = exposedKeyPrefix + 'keyboard_' 24 | const exposedCaptureKey = exposedKeyPrefix + 'capture' 25 | 26 | const elementHandleKeys = [ 27 | 'click', 28 | 'focus', 29 | 'hover', 30 | 'press', 31 | 'select', 32 | 'tap', 33 | 'type', 34 | ] as const 35 | 36 | const mouseKeys = ['click', 'down', 'move', 'up'] as const 37 | 38 | const keyboardKeys = ['down', 'up', 'press', 'sendCharacter', 'type'] as const 39 | 40 | async function exposePageContext(page: Page): Promise { 41 | await Promise.all([ 42 | ...elementHandleKeys.map((key) => { 43 | return page.exposeFunction( 44 | exposedKeyPrefix + key, 45 | async (selector: string, ...args: any[]) => { 46 | const el = await page.$(selector) 47 | if (!el) { 48 | return null 49 | } 50 | return (el[key] as any)(...args) 51 | } 52 | ) 53 | }), 54 | 55 | ...mouseKeys.map((key) => { 56 | return page.exposeFunction( 57 | exposedMouseKeyPrefix + key, 58 | async (...args: any[]) => { 59 | return (page.mouse[key] as any)(...args) 60 | } 61 | ) 62 | }), 63 | 64 | ...keyboardKeys.map((key) => { 65 | return page.exposeFunction( 66 | exposedKeyboardKeyPrefix + key, 67 | (...args: any[]) => { 68 | return (page.keyboard[key] as any)(...args) 69 | } 70 | ) 71 | }), 72 | ]) 73 | } 74 | 75 | interface ExposeContext { 76 | routeIndex: number 77 | exposedKeyPrefix: string 78 | exposedMouseKeyPrefix: string 79 | exposedKeyboardKeyPrefix: string 80 | exposedCaptureKey: string 81 | elementHandleKeys: string[] 82 | mouseKeys: string[] 83 | keyboardKeys: string[] 84 | } 85 | 86 | export async function runCapture( 87 | page: Page, 88 | capture: () => Promise, 89 | routeIndex: number 90 | ): Promise { 91 | // Exposed function must be unique as the one exposed by another catalog can remain 92 | // because there is no page transition between catalog as they are hashed routes. 93 | await exposePageContext(page) 94 | await page.exposeFunction(exposedCaptureKey, capture) 95 | 96 | const exposeContext: ExposeContext = { 97 | routeIndex, 98 | exposedKeyPrefix, 99 | exposedMouseKeyPrefix, 100 | exposedKeyboardKeyPrefix, 101 | exposedCaptureKey, 102 | elementHandleKeys: (elementHandleKeys as unknown) as string[], 103 | mouseKeys: (mouseKeys as unknown) as string[], 104 | keyboardKeys: (keyboardKeys as unknown) as string[], 105 | } 106 | 107 | await page.evaluate( 108 | ({ 109 | routeIndex, 110 | exposedKeyPrefix, 111 | exposedMouseKeyPrefix, 112 | exposedKeyboardKeyPrefix, 113 | exposedCaptureKey, 114 | elementHandleKeys, 115 | mouseKeys, 116 | keyboardKeys, 117 | }: ExposeContext) => { 118 | const captureOption = 119 | window.__birdseye_routes__[routeIndex]?.snapshot?.capture 120 | if (!captureOption) { 121 | return 122 | } 123 | 124 | const pageContext = { 125 | mouse: {}, 126 | keyboard: {}, 127 | } as PageContext 128 | 129 | elementHandleKeys.forEach((key) => { 130 | Object.defineProperty(pageContext, key, { 131 | get: () => (window as any)[exposedKeyPrefix + key], 132 | }) 133 | }) 134 | 135 | mouseKeys.forEach((key) => { 136 | Object.defineProperty(pageContext.mouse, key, { 137 | get: () => (window as any)[exposedMouseKeyPrefix + key], 138 | }) 139 | }) 140 | 141 | keyboardKeys.forEach((key) => { 142 | Object.defineProperty(pageContext.keyboard, key, { 143 | get: () => (window as any)[exposedKeyboardKeyPrefix + key], 144 | }) 145 | }) 146 | 147 | const captureInPage = (window as any)[exposedCaptureKey] 148 | return captureOption(pageContext, captureInPage) 149 | }, 150 | exposeContext as any 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Catalog } from '@birdseye/core' 2 | import { PageContext } from './page-context' 3 | 4 | export interface SnapshotOptions { 5 | skip?: boolean 6 | target?: string 7 | delay?: number 8 | disableCssAnimation?: boolean 9 | capture?: ( 10 | page: PageContext, 11 | capture: (target?: string) => Promise 12 | ) => Promise 13 | } 14 | 15 | export interface CatalogRoute { 16 | path: string 17 | snapshot?: SnapshotOptions 18 | } 19 | 20 | export function snapshotPlugin(catalogs: Catalog[]): void { 21 | const routes = catalogs.reduce((acc, catalog) => { 22 | const meta = catalog.toDeclaration().meta 23 | return acc.concat( 24 | meta.patterns 25 | .filter((pattern) => { 26 | return !pattern.plugins.snapshot?.skip 27 | }) 28 | .map((pattern) => { 29 | return { 30 | path: `/${encodeURIComponent(meta.name)}/${encodeURIComponent( 31 | pattern.name 32 | )}`, 33 | snapshot: pattern.plugins.snapshot, 34 | } 35 | }) 36 | ) 37 | }, []) 38 | 39 | window.__birdseye_routes__ = routes 40 | } 41 | 42 | declare global { 43 | interface Window { 44 | __birdseye_routes__: CatalogRoute[] 45 | } 46 | } 47 | 48 | declare module '@birdseye/core/dist/interfaces' { 49 | interface PluginOptions { 50 | snapshot?: SnapshotOptions 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../lib", 5 | "declaration": true 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | jest: true -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-1-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-10-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-10-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-11-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-11-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-12-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-12-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-2-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-3-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-4-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-5-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-5-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-6-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-6-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-7-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-7-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-8-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-8-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-9-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktsn/birdseye/2a1b778c4dd2a8cf153ab3ba9bb5ee1c8fe0a6be/packages/@birdseye/snapshot/test/__image_snapshots__/index-spec-ts-snapshot-outputs-images-to-the-default-location-9-snap.png -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/augment.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | declare module 'vue/types/options' { 4 | interface ComponentOptions { 5 | shadowRoot?: Element 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/catalogs/Active.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import Active from '../components/Active.vue' 3 | 4 | export default catalogFor(Active, 'Active').add('active', { 5 | plugins: { 6 | snapshot: { 7 | capture: async (page, capture) => { 8 | await capture() 9 | 10 | const rect = document.querySelector('button')!.getBoundingClientRect() 11 | const pos = { 12 | x: window.scrollX + rect.left, 13 | y: window.scrollY + rect.top, 14 | } 15 | 16 | // Mouse 17 | await page.mouse.move(pos.x, pos.y) 18 | await page.mouse.down() 19 | await capture() 20 | await page.mouse.up() 21 | 22 | // Keyboard 23 | await page.focus('input') 24 | await page.keyboard.down('x') 25 | await page.keyboard.down('y') 26 | await page.keyboard.down('z') 27 | await capture() 28 | await page.keyboard.up('z') 29 | }, 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/catalogs/Animation.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import Animation from '../components/Animation.vue' 3 | 4 | export default catalogFor(Animation, 'Animation') 5 | .add('Normal', { 6 | plugins: { 7 | snapshot: { 8 | delay: 1000, 9 | }, 10 | }, 11 | }) 12 | .add('Blink', { 13 | props: { 14 | blink: true, 15 | }, 16 | plugins: { 17 | snapshot: { 18 | skip: true, 19 | }, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/catalogs/Fill.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import Fill from '../components/Fill.vue' 3 | 4 | export default catalogFor(Fill, 'Fill preview area').add('style', { 5 | containerStyle: { 6 | backgroundColor: '#aaa', 7 | height: '100%', 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/catalogs/Fixed.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import Fixed from '../components/Fixed.vue' 3 | 4 | export default catalogFor(Fixed, 'Fixed').add('fixed', { 5 | plugins: { 6 | snapshot: { 7 | target: '.fixed', 8 | }, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/catalogs/Foo.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import Foo from '../components/Foo.vue' 3 | 4 | export default catalogFor(Foo, 'Foo component') 5 | .add('Normal', { 6 | props: { 7 | foo: 'foo value', 8 | }, 9 | data: { 10 | bar: 'bar value', 11 | }, 12 | }) 13 | .add('Bar number', { 14 | props: { 15 | foo: 'string', 16 | }, 17 | data: { 18 | bar: 12345, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/catalogs/Hover.catalog.ts: -------------------------------------------------------------------------------- 1 | import { catalogFor } from '@birdseye/vue' 2 | import Hover from '../components/Hover.vue' 3 | 4 | export default catalogFor(Hover, 'Hover') 5 | .add('hover', { 6 | slots: { 7 | default: 'Hover', 8 | }, 9 | plugins: { 10 | snapshot: { 11 | capture: async (page, capture) => { 12 | await capture() 13 | await page.hover('[data-test-id=button]') 14 | await capture() 15 | }, 16 | }, 17 | }, 18 | }) 19 | .add('another hover', { 20 | slots: { 21 | default: 'Another Hover', 22 | }, 23 | plugins: { 24 | snapshot: { 25 | capture: async (page, capture) => { 26 | await capture() 27 | await page.hover('[data-test-id=button]') 28 | await capture() 29 | }, 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/components/Active.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/components/Animation.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/components/Fill.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/components/Fixed.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/components/Foo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/components/Hover.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/main.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import birdseye from '@birdseye/app' 3 | import { snapshotPlugin } from '../../src/plugin' 4 | import './style.css' 5 | 6 | const load = (ctx: any) => ctx.keys().map((x: any) => ctx(x).default) 7 | const catalogs = load(require.context('./catalogs', true, /\.catalog\.ts$/)) 8 | 9 | birdseye('#app', catalogs, { 10 | plugins: [snapshotPlugin], 11 | }) 12 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const _default: any 3 | export default _default 4 | } 5 | 6 | declare module '*.vue' { 7 | import Vue from 'vue' 8 | const _default: typeof Vue 9 | export default _default 10 | } 11 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/fixture/style.css: -------------------------------------------------------------------------------- 1 | @import '~k-css/k.css'; 2 | 3 | body { 4 | margin: 0; 5 | font-size: 14px; 6 | color: #333; 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as rimraf from 'rimraf' 3 | import * as fs from 'fs' 4 | import { spawn } from 'child_process' 5 | import { snapshot } from '../src' 6 | 7 | const url = 'http://localhost:50000' 8 | 9 | describe('Snapshot', () => { 10 | function runCatalogServer(): () => void { 11 | const cp = spawn( 12 | 'yarn vue-cli-service serve --port 50000 test/fixture/main.ts', 13 | { 14 | cwd: path.resolve(__dirname, '../'), 15 | shell: true, 16 | stdio: 'ignore', 17 | } 18 | ) 19 | 20 | return () => { 21 | cp.kill() 22 | } 23 | } 24 | 25 | function wait(n: number): Promise { 26 | return new Promise((resolve) => { 27 | setTimeout(resolve, n) 28 | }) 29 | } 30 | 31 | const outDir = 'birdseye/snapshots' 32 | 33 | let killServer: () => void 34 | beforeAll(async () => { 35 | rimraf.sync(outDir) 36 | killServer = runCatalogServer() 37 | await wait(3000) 38 | }) 39 | 40 | afterAll(() => { 41 | killServer() 42 | }) 43 | 44 | it('outputs images to the default location', async () => { 45 | await snapshot({ 46 | url, 47 | }) 48 | 49 | const files = await fs.promises.readdir(outDir) 50 | files.sort().forEach((file) => { 51 | // Use sync version to make sure the order is not changed 52 | const image = fs.readFileSync(path.join(outDir, file)) 53 | expect(image).toMatchImageSnapshot({ 54 | failureThreshold: 0.01, 55 | failureThresholdType: 'percent', 56 | }) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/test/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import { toMatchImageSnapshot } from 'jest-image-snapshot' 2 | 3 | jest.setTimeout(30000) 4 | 5 | expect.extend({ 6 | toMatchImageSnapshot, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": ["esnext", "dom"], 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "stripInternal": true 12 | }, 13 | "include": ["src/**/*.ts", "test/**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/@birdseye/snapshot/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: ktsn-typescript -------------------------------------------------------------------------------- /packages/@birdseye/vue/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/.prettierignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | *.json -------------------------------------------------------------------------------- /packages/@birdseye/vue/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 katashin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/README.md: -------------------------------------------------------------------------------- 1 | # @birdseye/vue 2 | 3 | Vue.js integration of Birdseye. 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@birdseye/vue", 3 | "version": "0.9.3", 4 | "author": "katashin", 5 | "description": "Vue.js integration of Birdseye", 6 | "keywords": [ 7 | "Birdseye", 8 | "Vue.js", 9 | "component", 10 | "styleguide" 11 | ], 12 | "license": "MIT", 13 | "main": "lib/index.js", 14 | "typings": "lib/index.d.ts", 15 | "files": [ 16 | "lib", 17 | "webpack-loader.js" 18 | ], 19 | "homepage": "https://github.com/ktsn/birdseye", 20 | "bugs": "https://github.com/ktsn/birdseye/issues", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/ktsn/birdseye.git" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "scripts": { 29 | "prepare": "yarn clean && yarn build", 30 | "prepublishOnly": "yarn test", 31 | "clean": "rm -rf lib", 32 | "build": "tsc -p src && tsc src/webpack-loader.ts --outDir lib --module commonjs --target es2018", 33 | "dev": "yarn build -w", 34 | "lint": "eslint --ext js,ts src test", 35 | "lint:fix": "eslint --fix --ext js,ts src test", 36 | "test": "yarn lint && yarn test:unit", 37 | "test:unit": "jest", 38 | "test:watch": "jest --watch" 39 | }, 40 | "jest": { 41 | "transform": { 42 | "^.+\\.ts$": "ts-jest" 43 | }, 44 | "testURL": "http://localhost", 45 | "testRegex": "/test/.+\\.spec\\.(js|ts)$", 46 | "moduleFileExtensions": [ 47 | "ts", 48 | "js", 49 | "json" 50 | ], 51 | "moduleNameMapper": { 52 | "^vue$": "vue/dist/vue.common.js" 53 | }, 54 | "globals": { 55 | "ts-jest": { 56 | "tsConfig": "tsconfig.test.json" 57 | } 58 | } 59 | }, 60 | "devDependencies": { 61 | "@types/jest": "^26.0.0", 62 | "@types/js-yaml": "^3.11.2", 63 | "@types/loader-utils": "^2.0.0", 64 | "@types/node": "^14.0.1", 65 | "@types/webpack": "^4.4.11", 66 | "@vue/composition-api": "^1.0.0-rc.1", 67 | "@vue/test-utils": "^1.0.0-beta.29", 68 | "eslint": "^7.0.0", 69 | "eslint-config-ktsn-typescript": "^2.0.0", 70 | "jest": "^26.0.1", 71 | "prettier": "2.2.1", 72 | "prettier-config-ktsn": "^1.0.0", 73 | "ts-jest": "^26.0.0", 74 | "typescript": "^4.0.2", 75 | "vue": "^2.6.7", 76 | "vue-template-compiler": "^2.6.7", 77 | "webpack": "^4.17.1" 78 | }, 79 | "dependencies": { 80 | "@birdseye/core": "^0.9.0", 81 | "js-yaml": "^3.12.0", 82 | "loader-utils": "^2.0.0" 83 | }, 84 | "peerDependencies": { 85 | "vue-template-compiler": "^2.0.0" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('prettier-config-ktsn') 2 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/src/catalog.ts: -------------------------------------------------------------------------------- 1 | import Vue, { Component, VNode, ComponentOptions, CreateElement } from 'vue' 2 | import { compileToFunctions } from 'vue-template-compiler' 3 | import { 4 | Catalog as BaseCatalog, 5 | ComponentPattern, 6 | PluginOptions, 7 | } from '@birdseye/core' 8 | import { createInstrument } from './instrument' 9 | import extractProps from './extract-props' 10 | 11 | export interface Catalog extends BaseCatalog { 12 | add(name: string, options?: CatalogPatternOptions): Catalog 13 | } 14 | 15 | export interface CatalogOptions { 16 | name: string 17 | rootVue?: typeof Vue 18 | rootOptions?: ComponentOptions 19 | mapRender?: (this: Vue, h: CreateElement, wrapped: VNode) => VNode 20 | } 21 | 22 | export type Slot = string | ((this: Vue, props: any) => VNode[] | undefined) 23 | 24 | export interface CatalogPatternOptions { 25 | props?: Record 26 | data?: Record 27 | slots?: Record 28 | containerStyle?: Partial 29 | plugins?: PluginOptions 30 | } 31 | 32 | export function catalogFor( 33 | Comp: Component, 34 | nameOrOptions: string | CatalogOptions 35 | ): Catalog { 36 | const name = 37 | typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions.name 38 | const options = typeof nameOrOptions === 'string' ? { name } : nameOrOptions 39 | 40 | const { wrap } = createInstrument( 41 | options.rootVue || Vue, 42 | options.rootOptions || {}, 43 | options.mapRender 44 | ) 45 | 46 | const Wrapper = wrap(Comp) 47 | const compOptions = typeof Comp === 'function' ? (Comp as any).options : Comp 48 | const props = extractProps(compOptions.props) 49 | 50 | function catalog(patterns: ComponentPattern[]): Catalog { 51 | return { 52 | add(patternName, options = {}) { 53 | const rawSlots = options.slots || {} 54 | 55 | const slots = Object.keys(rawSlots).reduce< 56 | Record VNode[] | undefined> 57 | >((acc, key) => { 58 | const raw = rawSlots[key] 59 | acc[key] = 60 | typeof raw === 'function' 61 | ? raw.bind(getRenderProxy()) 62 | : (_props) => { 63 | return compileSlot(raw) 64 | } 65 | return acc 66 | }, {}) 67 | 68 | return catalog( 69 | patterns.concat({ 70 | name: patternName, 71 | props: options.props || {}, 72 | data: options.data || {}, 73 | slots, 74 | containerStyle: options.containerStyle || {}, 75 | plugins: options.plugins || {}, 76 | }) 77 | ) 78 | }, 79 | 80 | toDeclaration() { 81 | return { 82 | Wrapper, 83 | meta: { 84 | name, 85 | props, 86 | data: {}, 87 | patterns, 88 | }, 89 | } 90 | }, 91 | } 92 | } 93 | 94 | return catalog([]) 95 | } 96 | 97 | function compileSlot(slot: string): VNode[] { 98 | const compiled = compileToFunctions(` 99 |
${slot}
100 | `) 101 | 102 | const vnode = compiled.render.call(getRenderProxy(compiled.staticRenderFns)) 103 | return vnode.children! 104 | } 105 | 106 | function getRenderProxy(staticRenderFns?: (() => VNode)[]): Vue { 107 | const ctx: any = new Vue({ 108 | staticRenderFns, 109 | }) 110 | return ctx._renderProxy 111 | } 112 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/src/extract-props.ts: -------------------------------------------------------------------------------- 1 | import { PropOptions } from 'vue' 2 | import { ComponentDataInfo, ComponentDataType } from '@birdseye/core' 3 | 4 | type Prop = PropOptions | (new () => any) | (new () => any)[] | null | true 5 | 6 | type PropsDefinition = string[] | Record 7 | 8 | export default function extractProps( 9 | props: PropsDefinition | null | undefined 10 | ): Record { 11 | if (!props) { 12 | return {} 13 | } 14 | 15 | const res: Record = {} 16 | if (Array.isArray(props)) { 17 | props.forEach((name) => { 18 | res[name] = { type: [] } 19 | }) 20 | return res 21 | } 22 | 23 | Object.keys(props).forEach((name) => { 24 | const def = props[name] 25 | if (def && typeof def === 'object' && !Array.isArray(def)) { 26 | res[name] = { 27 | type: toTypeStrings(def.type, !!def.required), 28 | } 29 | 30 | if ('default' in def && typeof def.default !== 'function') { 31 | res[name].defaultValue = def.default 32 | } 33 | } else { 34 | res[name] = { type: toTypeStrings(def, false) } 35 | } 36 | }) 37 | 38 | return res 39 | } 40 | 41 | function toTypeStrings(type: any, required: boolean): ComponentDataType[] { 42 | if (!Array.isArray(type)) { 43 | return toTypeStrings([type], required) 44 | } 45 | 46 | const res = type 47 | .map((t): ComponentDataType | null => { 48 | if (t === String) { 49 | return 'string' 50 | } 51 | 52 | if (t === Number) { 53 | return 'number' 54 | } 55 | 56 | if (t === Boolean) { 57 | return 'boolean' 58 | } 59 | 60 | if (t === Array) { 61 | return 'array' 62 | } 63 | 64 | if (typeof t === 'function' && t !== Function) { 65 | return 'object' 66 | } 67 | 68 | return null 69 | }) 70 | .filter(nonNull) 71 | 72 | return !required && res.length > 0 ? res.concat(['null', 'undefined']) : res 73 | } 74 | 75 | function nonNull(val: T): val is NonNullable { 76 | return val != null 77 | } 78 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | import Vue, { Component, VueConstructor, ComponentOptions } from 'vue' 2 | import { Catalog } from '@birdseye/core' 3 | import { createInstrument as create } from './instrument' 4 | 5 | function isNative(Ctor: any): boolean { 6 | return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) 7 | } 8 | 9 | const hasSymbol = 10 | typeof Symbol !== 'undefined' && 11 | isNative(Symbol) && 12 | typeof Reflect !== 'undefined' && 13 | isNative(Reflect.ownKeys) 14 | 15 | export { catalogFor } from './catalog' 16 | 17 | export function createInstrument( 18 | Vue: VueConstructor, 19 | rootOptions: ComponentOptions = {} 20 | ) { 21 | const { instrument: _instrument } = create(Vue, rootOptions) 22 | 23 | return function instrument( 24 | Components: (Component | { default: Component })[] 25 | ): Catalog[] { 26 | return Components.map((c: any) => { 27 | if (c.__esModule || (hasSymbol && c[Symbol.toStringTag] === 'Module')) { 28 | c = c.default 29 | } 30 | return { toDeclaration: () => _instrument(c) } 31 | }) 32 | } 33 | } 34 | 35 | export const instrument = createInstrument(Vue) 36 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/src/instrument.ts: -------------------------------------------------------------------------------- 1 | import Vue, { 2 | Component, 3 | VueConstructor, 4 | VNode, 5 | ComponentOptions, 6 | CreateElement, 7 | } from 'vue' 8 | import { 9 | normalizeMeta, 10 | ComponentDeclaration, 11 | ComponentDataInfo, 12 | ComponentDataType, 13 | } from '@birdseye/core' 14 | 15 | export function createInstrument( 16 | Vue: VueConstructor, 17 | rootOptions: ComponentOptions = {}, 18 | mapRender?: (this: Vue, h: CreateElement, mapped: VNode) => VNode 19 | ) { 20 | // We need to mount an individual root so that the users can inject 21 | // some object to the internal root. 22 | const Root = Vue.extend({ 23 | data() { 24 | return { 25 | props: {} as Record, 26 | data: {} as Record, 27 | slots: {} as Record< 28 | string, 29 | ((props: any) => VNode[] | undefined) | undefined 30 | >, 31 | id: null as number | null, 32 | 33 | defaultData: null as Record | null, 34 | } 35 | }, 36 | 37 | methods: { 38 | applyData(newData: Record): void { 39 | const child = this.$refs.child as Vue | undefined 40 | if (child && this.defaultData) { 41 | const defaultData = this.defaultData 42 | 43 | // Avoid printing error when $data has computed property which comes from @vue/composition-api 44 | ignoreWarning(() => { 45 | Object.keys(child.$data).forEach((key) => { 46 | child.$data[key] = 47 | key in newData ? newData[key] : defaultData[key] 48 | }) 49 | 50 | // There is a case that the field defined in a catalog does not exist on the component yet. 51 | // e.g. On a component initialization with @vue/composition-api 52 | Object.keys(newData).forEach((key) => { 53 | ;(child as any)[key] = newData[key] 54 | }) 55 | }) 56 | } 57 | }, 58 | 59 | updateComponent(component: Component | null, id: number | null): void { 60 | const vm: any = this 61 | vm.component = component 62 | this.id = id 63 | this.defaultData = null 64 | this.$forceUpdate() 65 | }, 66 | }, 67 | 68 | watch: { 69 | data: 'applyData', 70 | }, 71 | 72 | created() { 73 | const vm: any = this 74 | vm.component = null 75 | }, 76 | 77 | updated() { 78 | const child = this.$refs.child as Vue | undefined 79 | if (child && !this.defaultData) { 80 | this.defaultData = child.$data 81 | } 82 | this.applyData(this.data) 83 | }, 84 | 85 | mounted() { 86 | const child = this.$refs.child as Vue | undefined 87 | if (child && !this.defaultData) { 88 | this.defaultData = child.$data 89 | } 90 | this.applyData(this.data) 91 | }, 92 | 93 | render(h): VNode { 94 | const vm: any = this 95 | if (!vm.component) { 96 | return h() 97 | } 98 | 99 | const wrapped = h(vm.component, { 100 | key: String(this.id), 101 | props: this.props, 102 | attrs: this.props, 103 | ref: 'child', 104 | scopedSlots: this.slots, 105 | }) 106 | 107 | return mapRender ? mapRender.call(this, h, wrapped) : wrapped 108 | }, 109 | }) 110 | 111 | // We need to immediately mount root instance to let devtools detect it 112 | // To make sure devtools to detect root instance, we need to create placeholder 113 | // element and attach root instance to it. 114 | const root = new Root(rootOptions).$mount() 115 | const placeholder: any = document.createComment('Birdseye placeholder') 116 | placeholder.__vue__ = root 117 | document.body.appendChild(placeholder) 118 | 119 | return { 120 | instrument, 121 | wrap, 122 | } 123 | 124 | function instrument(Component: Component): ComponentDeclaration { 125 | const options = 126 | typeof Component === 'function' ? (Component as any).options : Component 127 | 128 | const rawMeta = options.__birdseye || {} 129 | const meta = normalizeMeta({ 130 | name: rawMeta.name || options.name, 131 | props: rawMeta.props, 132 | data: rawMeta.data, 133 | patterns: rawMeta.patterns, 134 | }) 135 | 136 | const Wrapper = wrap(Component, meta.props) 137 | 138 | return { 139 | Wrapper, 140 | meta, 141 | } 142 | } 143 | 144 | function wrap( 145 | Component: Component, 146 | metaProps: Record = {} 147 | ): VueConstructor { 148 | let maxId = 0 149 | 150 | return Vue.extend({ 151 | name: 'ComponentWrapper', 152 | 153 | props: { 154 | props: { 155 | type: Object, 156 | required: true, 157 | }, 158 | 159 | data: { 160 | type: Object, 161 | required: true, 162 | }, 163 | }, 164 | 165 | computed: { 166 | filledProps(): Record { 167 | const filled = { ...this.props } 168 | Object.keys(metaProps).forEach((key) => { 169 | if (filled[key] !== undefined) { 170 | return 171 | } 172 | const meta = metaProps[key] 173 | filled[key] = 174 | 'defaultValue' in meta 175 | ? meta.defaultValue 176 | : inferValueFromType(meta.type) 177 | }) 178 | return filled 179 | }, 180 | 181 | // We need to clone data to correctly track some dependent value is changed 182 | clonedData(): Record { 183 | return { ...this.data } 184 | }, 185 | }, 186 | 187 | watch: { 188 | filledProps(newProps: Record): void { 189 | root.props = newProps 190 | }, 191 | 192 | clonedData(newData: Record): void { 193 | root.data = newData 194 | }, 195 | }, 196 | 197 | mounted() { 198 | root.updateComponent(Component, ++maxId) 199 | root.props = this.filledProps 200 | root.data = this.clonedData 201 | 202 | const wrapper = this.$refs.wrapper as Element 203 | wrapper.appendChild(root.$el) 204 | }, 205 | 206 | beforeDestroy() { 207 | root.updateComponent(null, null) 208 | }, 209 | 210 | render(h): VNode { 211 | root.slots = this.$scopedSlots 212 | return h('div', { 213 | ref: 'wrapper', 214 | style: { 215 | height: '100%', 216 | }, 217 | }) 218 | }, 219 | }) 220 | } 221 | } 222 | 223 | function inferValueFromType( 224 | type: ComponentDataType | ComponentDataType[] 225 | ): any { 226 | if (Array.isArray(type)) { 227 | return inferValueFromType(type[0]) 228 | } 229 | 230 | switch (type) { 231 | case 'string': 232 | return '' 233 | case 'number': 234 | return 0 235 | case 'boolean': 236 | return false 237 | case 'object': 238 | return {} 239 | case 'array': 240 | return [] 241 | default: 242 | return undefined 243 | } 244 | } 245 | 246 | function ignoreWarning(fn: () => void): void { 247 | const originalWarn = Vue.config.warnHandler 248 | Vue.config.warnHandler = () => {} 249 | fn() 250 | Vue.config.warnHandler = originalWarn 251 | } 252 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../lib", 5 | "declaration": true 6 | }, 7 | "include": [ 8 | "**/*.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/@birdseye/vue/src/webpack-loader.ts: -------------------------------------------------------------------------------- 1 | import { loader } from 'webpack' 2 | import * as loaderUtils from 'loader-utils' 3 | import * as yaml from 'js-yaml' 4 | 5 | const vueBirdseyeLoader: loader.Loader = function (source, map) { 6 | const options = this.resourceQuery 7 | ? loaderUtils.parseQuery(this.resourceQuery) 8 | : {} 9 | 10 | try { 11 | let meta: string | object 12 | if (options.lang === 'yaml' || options.lang === 'yml') { 13 | meta = yaml.safeLoad(String(source)) ?? {} 14 | } else { 15 | meta = JSON.parse(String(source)) ?? {} 16 | } 17 | 18 | const extractPropsReq = loaderUtils.stringifyRequest( 19 | this, 20 | '@birdseye/vue/lib/extract-props' 21 | ) 22 | 23 | this.callback( 24 | null, 25 | [ 26 | `import extractProps from ${extractPropsReq}`, 27 | 'export default function(Component) {', 28 | ' var props = extractProps(Component.options.props)', 29 | ` Component.options.__birdseye = ${JSON.stringify(meta)}`, 30 | ' Component.options.__birdseye.props = props', 31 | '}', 32 | ].join('\n'), 33 | map 34 | ) 35 | } catch (err) { 36 | this.callback(err) 37 | } 38 | } 39 | 40 | export default vueBirdseyeLoader 41 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | jest: true -------------------------------------------------------------------------------- /packages/@birdseye/vue/test/extract-props.spec.ts: -------------------------------------------------------------------------------- 1 | import extractProps from '../src/extract-props' 2 | 3 | describe('Extract props', () => { 4 | it('returns empty object if falsy value is passed', () => { 5 | const props = extractProps(null) 6 | expect(props).toEqual({}) 7 | }) 8 | 9 | it('extracts array syntax props', () => { 10 | const props = extractProps(['foo', 'bar']) 11 | expect(props).toEqual({ 12 | foo: { type: [] }, 13 | bar: { type: [] }, 14 | }) 15 | }) 16 | 17 | it('extracts simple object syntax props', () => { 18 | const props = extractProps({ 19 | foo: true, 20 | bar: null, 21 | }) 22 | expect(props).toEqual({ 23 | foo: { type: [] }, 24 | bar: { type: [] }, 25 | }) 26 | }) 27 | 28 | it('extracts props types', () => { 29 | const props = extractProps({ 30 | foo: String, 31 | bar: { 32 | type: Number, 33 | }, 34 | }) 35 | expect(props).toEqual({ 36 | foo: { type: ['string', 'null', 'undefined'] }, 37 | bar: { type: ['number', 'null', 'undefined'] }, 38 | }) 39 | }) 40 | 41 | it('extracts required type', () => { 42 | const props = extractProps({ 43 | foo: { 44 | type: String, 45 | required: true, 46 | }, 47 | }) 48 | expect(props).toEqual({ 49 | foo: { type: ['string'] }, 50 | }) 51 | }) 52 | 53 | it('extracts default value', () => { 54 | const props = extractProps({ 55 | foo: { 56 | default: 'test', 57 | }, 58 | }) 59 | expect(props).toEqual({ 60 | foo: { 61 | type: [], 62 | defaultValue: 'test', 63 | }, 64 | }) 65 | }) 66 | 67 | it('extracts null / undefined as default', () => { 68 | const props = extractProps({ 69 | foo: { 70 | default: null, 71 | }, 72 | bar: { 73 | default: undefined, 74 | }, 75 | }) 76 | expect(props).toEqual({ 77 | foo: { 78 | type: [], 79 | defaultValue: null, 80 | }, 81 | bar: { 82 | type: [], 83 | defaultValue: undefined, 84 | }, 85 | }) 86 | }) 87 | 88 | it('does not extract default value from function', () => { 89 | const props = extractProps({ 90 | foo: { 91 | default: () => ['foo', 'bar'], 92 | }, 93 | }) 94 | expect(props).toEqual({ 95 | foo: { 96 | type: [], 97 | // since it needs component instance as `this`, 98 | // we should not choose function style default value. 99 | // it will be provided by Vue.js side in any cases. 100 | defaultValue: undefined, 101 | }, 102 | }) 103 | }) 104 | 105 | describe('types', () => { 106 | it('string', () => { 107 | const props = extractProps({ 108 | foo: String, 109 | }) 110 | expect(props.foo.type).toEqual(['string', 'null', 'undefined']) 111 | }) 112 | 113 | it('number', () => { 114 | const props = extractProps({ 115 | foo: Number, 116 | }) 117 | expect(props.foo.type).toEqual(['number', 'null', 'undefined']) 118 | }) 119 | 120 | it('boolean', () => { 121 | const props = extractProps({ 122 | foo: Boolean, 123 | }) 124 | expect(props.foo.type).toEqual(['boolean', 'null', 'undefined']) 125 | }) 126 | 127 | it('array', () => { 128 | const props = extractProps({ 129 | foo: Array, 130 | }) 131 | expect(props.foo.type).toEqual(['array', 'null', 'undefined']) 132 | }) 133 | 134 | it('object', () => { 135 | const props = extractProps({ 136 | foo: Object, 137 | }) 138 | expect(props.foo.type).toEqual(['object', 'null', 'undefined']) 139 | }) 140 | 141 | it('function', () => { 142 | const props = extractProps({ 143 | foo: Function, 144 | }) 145 | expect(props.foo.type).toEqual([]) 146 | }) 147 | 148 | it('other object', () => { 149 | class Test {} 150 | const props = extractProps({ 151 | foo: Test, 152 | }) 153 | expect(props.foo.type).toEqual(['object', 'null', 'undefined']) 154 | }) 155 | 156 | it('union', () => { 157 | const props = extractProps({ 158 | foo: [String, Number], 159 | }) 160 | expect(props.foo.type).toEqual(['string', 'number', 'null', 'undefined']) 161 | }) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/test/instrument.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { createInstrument } from '../src/instrument' 3 | 4 | describe('Instrument', () => { 5 | let Dummy: any 6 | 7 | const { instrument } = createInstrument(Vue) 8 | 9 | beforeEach(() => { 10 | Dummy = { 11 | render(h: Function) { 12 | return h() 13 | }, 14 | } 15 | }) 16 | 17 | it('uses component name if available', () => { 18 | Dummy.name = 'Dummy' 19 | const result = instrument(Dummy) 20 | expect(result.meta.name).toBe('Dummy') 21 | }) 22 | 23 | it('extracts meta data', () => { 24 | Dummy.__birdseye = { 25 | name: 'Dummy component', 26 | patterns: [ 27 | { 28 | name: 'Normal pattern', 29 | props: { 30 | test: 'foo', 31 | }, 32 | data: { 33 | test2: 'bar', 34 | }, 35 | slots: {}, 36 | containerStyle: {}, 37 | plugins: {}, 38 | }, 39 | ], 40 | } 41 | const result = instrument(Dummy) 42 | expect(result.meta.name).toBe(Dummy.__birdseye.name) 43 | expect(result.meta.patterns).toEqual(Dummy.__birdseye.patterns) 44 | }) 45 | 46 | it('extracts from constructor', () => { 47 | const Ctor: any = Vue.extend(Dummy) 48 | Ctor.options.__birdseye = { 49 | name: 'Dummy component', 50 | patterns: [ 51 | { 52 | name: 'Normal pattern', 53 | props: { 54 | test: 'foo', 55 | }, 56 | data: { 57 | test2: 'bar', 58 | }, 59 | slots: {}, 60 | containerStyle: {}, 61 | plugins: {}, 62 | }, 63 | ], 64 | } 65 | const result = instrument(Ctor) 66 | expect(result.meta.name).toBe(Ctor.options.__birdseye.name) 67 | expect(result.meta.patterns).toEqual(Ctor.options.__birdseye.patterns) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/test/webpack-loader.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import loader from '../src/webpack-loader' 3 | 4 | function test( 5 | content: string, 6 | lang: string | null, 7 | cb: (err: Error, result: string) => void 8 | ): void { 9 | loader.call( 10 | { 11 | context: path.resolve(__dirname, '../'), 12 | callback: cb, 13 | resourceQuery: lang ? '?lang=' + lang : '', 14 | }, 15 | content 16 | ) 17 | } 18 | 19 | describe('webpack loader', () => { 20 | it('injects birdseye content', (done) => { 21 | test( 22 | `{ 23 | "name": "Test" 24 | }`, 25 | null, 26 | (_err, result) => { 27 | expect(result).toMatchInlineSnapshot(` 28 | "import extractProps from \\"@birdseye/vue/lib/extract-props\\" 29 | export default function(Component) { 30 | var props = extractProps(Component.options.props) 31 | Component.options.__birdseye = {\\"name\\":\\"Test\\"} 32 | Component.options.__birdseye.props = props 33 | }" 34 | `) 35 | done() 36 | } 37 | ) 38 | }) 39 | 40 | it('emit parse error', () => { 41 | test(`{ "name": "Test", }`, null, (err) => { 42 | expect(err.name).toBe('SyntaxError') 43 | }) 44 | }) 45 | 46 | it('loads yaml data', () => { 47 | test(`name: Test`, 'yaml', (_err, result) => { 48 | expect(result).toMatchInlineSnapshot(` 49 | "import extractProps from \\"@birdseye/vue/lib/extract-props\\" 50 | export default function(Component) { 51 | var props = extractProps(Component.options.props) 52 | Component.options.__birdseye = {\\"name\\":\\"Test\\"} 53 | Component.options.__birdseye.props = props 54 | }" 55 | `) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/test/wrap.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | import CompositionApi, { 3 | defineComponent, 4 | ref, 5 | computed, 6 | } from '@vue/composition-api' 7 | import { shallowMount, createLocalVue } from '@vue/test-utils' 8 | import { ComponentDataType } from '@birdseye/core' 9 | import { createInstrument } from '../src/instrument' 10 | 11 | describe('Wrap', () => { 12 | const { wrap } = createInstrument(Vue) 13 | 14 | const Dummy = Vue.extend({ 15 | name: 'Dummy', 16 | 17 | props: { 18 | foo: { 19 | type: String, 20 | required: true, 21 | }, 22 | 23 | bar: { 24 | type: Number, 25 | default: 0, 26 | }, 27 | }, 28 | 29 | data() { 30 | return { 31 | baz: 'baz', 32 | } 33 | }, 34 | 35 | render(h): VNode { 36 | const el = (id: string, content: any) => { 37 | return h('div', { attrs: { id } }, [content]) 38 | } 39 | 40 | return h('div', [ 41 | // props, data 42 | el('foo', this.foo), 43 | el('bar', this.bar), 44 | el('baz', this.baz), 45 | el('qux', this.$attrs.qux), 46 | 47 | // slots 48 | el('default-slot', this.$scopedSlots.default?.({ message: 'Hello' })), 49 | el('named-slot', this.$scopedSlots.named?.({ message: 'Hello' })), 50 | ]) 51 | }, 52 | }) 53 | 54 | const Wrapper = wrap(Dummy) 55 | 56 | it('applies initial props and data', async () => { 57 | const wrapper = shallowMount(Wrapper, { 58 | propsData: { 59 | props: { 60 | foo: 'test', 61 | bar: 123, 62 | }, 63 | data: { 64 | baz: 'baz data', 65 | }, 66 | }, 67 | }) 68 | 69 | await wrapper.vm.$nextTick() 70 | 71 | expect(wrapper.find('#foo').text()).toBe('test') 72 | expect(wrapper.find('#bar').text()).toBe('123') 73 | expect(wrapper.find('#baz').text()).toBe('baz data') 74 | }) 75 | 76 | it('applies props not specified on props option', async () => { 77 | const wrapper = shallowMount(Wrapper, { 78 | propsData: { 79 | props: { 80 | foo: 'test', 81 | qux: 456, 82 | }, 83 | data: {}, 84 | }, 85 | }) 86 | 87 | await wrapper.vm.$nextTick() 88 | 89 | expect(wrapper.find('#qux').text()).toBe('456') 90 | }) 91 | 92 | it('applies initial slots', async () => { 93 | const wrapper = shallowMount(Wrapper, { 94 | propsData: { 95 | props: { 96 | foo: '', 97 | }, 98 | data: {}, 99 | }, 100 | scopedSlots: { 101 | default: '
default slot
', 102 | named: '
named slot
', 103 | }, 104 | }) 105 | 106 | await wrapper.vm.$nextTick() 107 | 108 | expect(wrapper.find('#default-slot').text()).toBe('default slot') 109 | expect(wrapper.find('#named-slot').text()).toBe('named slot') 110 | }) 111 | 112 | it('applies initial scoped slots', async () => { 113 | const wrapper = shallowMount(Wrapper, { 114 | propsData: { 115 | props: { 116 | foo: '', 117 | }, 118 | data: {}, 119 | }, 120 | scopedSlots: { 121 | default(props: any): any { 122 | const h = this.$createElement 123 | return h('div', ['default: ', props.message]) 124 | }, 125 | named(props: any): any { 126 | const h = this.$createElement 127 | return h('div', ['named: ', props.message]) 128 | }, 129 | }, 130 | }) 131 | 132 | await wrapper.vm.$nextTick() 133 | 134 | expect(wrapper.find('#default-slot').text()).toBe('default: Hello') 135 | expect(wrapper.find('#named-slot').text()).toBe('named: Hello') 136 | }) 137 | 138 | it('updates props', async () => { 139 | const wrapper = shallowMount(Wrapper, { 140 | propsData: { 141 | props: { 142 | foo: 'test', 143 | bar: 123, 144 | }, 145 | data: { 146 | baz: 'baz data', 147 | }, 148 | }, 149 | }) 150 | 151 | wrapper.setProps({ 152 | props: { 153 | foo: 'updated', 154 | bar: 123, 155 | }, 156 | }) 157 | 158 | await wrapper.vm.$nextTick() 159 | 160 | expect(wrapper.find('#foo').text()).toBe('updated') 161 | }) 162 | 163 | it('updates data', async () => { 164 | const wrapper = shallowMount(Wrapper, { 165 | propsData: { 166 | props: { 167 | foo: 'test', 168 | bar: 123, 169 | }, 170 | data: { 171 | baz: 'baz data', 172 | }, 173 | }, 174 | }) 175 | 176 | wrapper.setProps({ 177 | data: { 178 | baz: 'baz updated', 179 | }, 180 | }) 181 | 182 | await wrapper.vm.$nextTick() 183 | 184 | expect(wrapper.find('#baz').text()).toBe('baz updated') 185 | }) 186 | 187 | it('removes props', async () => { 188 | const wrapper = shallowMount(Wrapper, { 189 | propsData: { 190 | props: { 191 | foo: 'test', 192 | bar: 42, 193 | }, 194 | data: {}, 195 | }, 196 | }) 197 | 198 | wrapper.setProps({ 199 | props: { 200 | foo: 'test', 201 | }, 202 | }) 203 | 204 | await wrapper.vm.$nextTick() 205 | 206 | expect(wrapper.find('#bar').text()).toBe('0') 207 | }) 208 | 209 | it('removes data with undefined', async () => { 210 | const wrapper = shallowMount(Wrapper, { 211 | propsData: { 212 | props: { 213 | foo: 'test', 214 | bar: 42, 215 | }, 216 | data: { 217 | baz: 'baz', 218 | }, 219 | }, 220 | }) 221 | 222 | wrapper.setProps({ 223 | data: { 224 | baz: undefined, 225 | }, 226 | }) 227 | 228 | await wrapper.vm.$nextTick() 229 | 230 | expect(wrapper.find('#baz').text()).toBe('') 231 | }) 232 | 233 | it('uses props value for default data', async () => { 234 | const Test = Vue.extend({ 235 | props: ['foo'], 236 | data() { 237 | return { 238 | bar: this.foo, 239 | } 240 | }, 241 | render(h): any { 242 | return h('div', ['data - ' + this.bar]) 243 | }, 244 | }) 245 | 246 | const wrapper = shallowMount(wrap(Test), { 247 | propsData: { 248 | props: { 249 | foo: 'first', 250 | }, 251 | data: {}, 252 | }, 253 | }) 254 | 255 | await wrapper.vm.$nextTick() 256 | 257 | expect(wrapper.text()).toBe('data - first') 258 | }) 259 | 260 | it('does not remove data when not specified', async () => { 261 | const wrapper = shallowMount(Wrapper, { 262 | propsData: { 263 | props: { 264 | foo: 'test', 265 | bar: 42, 266 | }, 267 | data: { 268 | baz: 'baz', 269 | }, 270 | }, 271 | }) 272 | 273 | wrapper.setProps({ 274 | data: {}, 275 | }) 276 | 277 | await wrapper.vm.$nextTick() 278 | 279 | expect(wrapper.find('#baz').text()).toBe('baz') 280 | }) 281 | 282 | it('can be injected Vue constructor', async () => { 283 | const localVue = createLocalVue() 284 | localVue.prototype.$test = 'injected' 285 | 286 | const Test = { 287 | render(h: Function): any { 288 | return h('div', (this as any).$test) 289 | }, 290 | } 291 | 292 | const { wrap } = createInstrument(localVue) 293 | const Wrapper = wrap(Test) 294 | 295 | const wrapper = shallowMount(Wrapper, { 296 | localVue, 297 | propsData: { 298 | props: {}, 299 | data: {}, 300 | }, 301 | }) 302 | 303 | await wrapper.vm.$nextTick() 304 | 305 | expect(wrapper.text()).toBe('injected') 306 | }) 307 | 308 | it('can be injected root constructor options', async () => { 309 | const { wrap } = createInstrument(Vue, { 310 | test: 'injected', 311 | } as any) 312 | 313 | const Test = { 314 | render(h: Function): any { 315 | return h('div', (this as any).$root.$options.test) 316 | }, 317 | } 318 | 319 | const Wrapper = wrap(Test) 320 | 321 | const wrapper = shallowMount(Wrapper, { 322 | propsData: { 323 | props: {}, 324 | data: {}, 325 | }, 326 | }) 327 | 328 | await wrapper.vm.$nextTick() 329 | 330 | expect(wrapper.text()).toBe('injected') 331 | }) 332 | 333 | it('allows to map render function', async () => { 334 | const { wrap } = createInstrument(Vue, {}, (h, vnode) => { 335 | return h('div', { attrs: { 'data-test': 'wrapper' } }, [vnode]) 336 | }) 337 | 338 | const Test = Vue.extend({ 339 | props: ['foo'], 340 | 341 | data() { 342 | return { 343 | bar: 123, 344 | } 345 | }, 346 | 347 | render(h): VNode { 348 | return h('div', [`foo: ${this.foo}, bar: ${this.bar}`]) 349 | }, 350 | }) 351 | 352 | const Wrapper = wrap(Test) 353 | 354 | const wrapper = shallowMount(Wrapper, { 355 | propsData: { 356 | props: { 357 | foo: 'Test', 358 | }, 359 | data: { 360 | bar: 456, 361 | }, 362 | }, 363 | }) 364 | 365 | await wrapper.vm.$nextTick() 366 | 367 | expect(wrapper.html()).toMatchInlineSnapshot(` 368 | "
369 |
370 |
foo: Test, bar: 456
371 |
372 |
" 373 | `) 374 | }) 375 | 376 | describe('props default', () => { 377 | async function test( 378 | meta: { type: ComponentDataType[]; defaultValue?: any }, 379 | prop: any, 380 | expected: any 381 | ) { 382 | const Test = Vue.extend({ 383 | props: ['__test__'], 384 | 385 | render(h) { 386 | const rendered = 387 | typeof this.__test__ === 'object' 388 | ? JSON.stringify(this.__test__) 389 | : this.__test__ 390 | return h('div', rendered) 391 | }, 392 | }) 393 | 394 | const Wrapper = wrap(Test, { 395 | __test__: meta, 396 | }) 397 | 398 | const wrapper = shallowMount(Wrapper, { 399 | propsData: { 400 | props: 401 | prop === undefined 402 | ? {} 403 | : { 404 | __test__: prop, 405 | }, 406 | data: {}, 407 | }, 408 | }) 409 | 410 | await wrapper.vm.$nextTick() 411 | 412 | expect(wrapper.text()).toBe(expected) 413 | } 414 | 415 | it('fills props value from meta default value', () => { 416 | return test({ type: ['string'], defaultValue: 'test' }, undefined, 'test') 417 | }) 418 | 419 | it('fills props value from meta type', () => { 420 | return test({ type: ['number'] }, undefined, '0') 421 | }) 422 | 423 | it('does not auto fills with null value', () => { 424 | return test({ type: ['string'] }, null, 'null') 425 | }) 426 | 427 | it('does not auto fills props value when default is specified even if it is null or undefined', async () => { 428 | await test({ type: ['array'], defaultValue: null }, undefined, 'null') 429 | await test({ type: ['array'], defaultValue: undefined }, undefined, '') 430 | }) 431 | 432 | it('does not overwrite specified props with default value', () => { 433 | return test({ type: ['string'], defaultValue: 'test' }, 'foo', 'foo') 434 | }) 435 | }) 436 | 437 | describe('lifecycle', () => { 438 | const mounted = jest.fn() 439 | const destroyed = jest.fn() 440 | 441 | const LifecycleTest = Vue.extend({ 442 | props: { 443 | message: String, 444 | }, 445 | 446 | data() { 447 | return { 448 | updateCount: 0, 449 | } 450 | }, 451 | 452 | mounted, 453 | destroyed, 454 | 455 | render(h): any { 456 | return h('div', [this.message]) 457 | }, 458 | }) 459 | 460 | const LifecycleWrapper = wrap(LifecycleTest) 461 | 462 | it('re-mount if wrapper instance is different', async () => { 463 | shallowMount(LifecycleWrapper, { 464 | propsData: { 465 | props: { 466 | message: 'initial', 467 | }, 468 | data: {}, 469 | }, 470 | }) 471 | await Vue.nextTick() 472 | 473 | shallowMount(LifecycleWrapper, { 474 | propsData: { 475 | props: { 476 | message: 'another', 477 | }, 478 | data: {}, 479 | }, 480 | }) 481 | await Vue.nextTick() 482 | 483 | expect(mounted).toHaveBeenCalledTimes(2) 484 | expect(destroyed).toHaveBeenCalledTimes(1) 485 | }) 486 | }) 487 | 488 | describe('composition api', () => { 489 | Vue.use(CompositionApi) 490 | 491 | const Composition = defineComponent({ 492 | setup() { 493 | const count = ref(1) 494 | const double = computed(() => count.value * 2) 495 | 496 | return { 497 | count, 498 | double, 499 | } 500 | }, 501 | 502 | template: ` 503 |
504 |
{{ count }}
505 |
{{ double }}
506 |
507 | `, 508 | }) 509 | 510 | const Wrapper = wrap(Composition) 511 | 512 | it('handles initial data with composition api', async () => { 513 | const wrapper = shallowMount(Wrapper, { 514 | propsData: { 515 | props: {}, 516 | data: { 517 | count: 2, 518 | }, 519 | }, 520 | }) 521 | await Vue.nextTick() 522 | 523 | expect(wrapper.find('[data-test-id=count]').text()).toBe('2') 524 | expect(wrapper.find('[data-test-id=double]').text()).toBe('4') 525 | }) 526 | 527 | it('handles data changes with composition api', async () => { 528 | const wrapper = shallowMount(Wrapper, { 529 | propsData: { 530 | props: {}, 531 | data: {}, 532 | }, 533 | }) 534 | await Vue.nextTick() 535 | 536 | expect(wrapper.find('[data-test-id=count]').text()).toBe('1') 537 | expect(wrapper.find('[data-test-id=double]').text()).toBe('2') 538 | 539 | await wrapper.setProps({ 540 | data: { 541 | count: 2, 542 | }, 543 | }) 544 | 545 | expect(wrapper.find('[data-test-id=count]').text()).toBe('2') 546 | expect(wrapper.find('[data-test-id=double]').text()).toBe('4') 547 | }) 548 | }) 549 | }) 550 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "es2015", 9 | "dom" 10 | ], 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "esModuleInterop": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts", 18 | "test/**/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "sourceMap": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/@birdseye/vue/webpack-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/webpack-loader').default 2 | --------------------------------------------------------------------------------