├── .env ├── .env.sample ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── babel.config.js ├── docker-compose.yml ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── server.js ├── src ├── App.vue ├── __mocks__ │ └── socket.js ├── assets │ └── logo.png ├── components │ ├── VOperationsTimeline.vue │ ├── VOperationsTimelineItem.vue │ ├── VOperationsTimelineItemDetail.vue │ └── VOperationsTimelineItemDetailValue.vue ├── env.js ├── main.js ├── router.js ├── socket.js ├── store │ ├── __mocks__ │ │ └── index.js │ ├── actions.js │ ├── index.js │ ├── mutations.js │ └── state.js └── views │ └── TheOperationsLog.vue └── tests └── unit ├── .eslintrc.js ├── App.spec.js ├── TheOperationsLog.spec.js ├── VOperationsTimeline.spec.js ├── VOperationsTimelineItem.spec.js ├── VOperationsTimelineItemDetail.spec.js ├── VOperationsTimelineItemDetailValue.spec.js ├── __snapshots__ ├── App.spec.js.snap ├── TheOperationsLog.spec.js.snap ├── VOperationsTimeline.spec.js.snap ├── VOperationsTimelineItem.spec.js.snap ├── VOperationsTimelineItemDetail.spec.js.snap └── VOperationsTimelineItemDetailValue.spec.js.snap ├── actions.spec.js ├── fixtures └── socketServer.js ├── mutations.spec.js └── socket.spec.js /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_SOCKET_SERVER="localhost:3000" 2 | MYSQL_HOST="0.0.0.0" 3 | MYSQL_PORT="3306" 4 | MYSQL_USER="root" 5 | MYSQL_PASSWORD="root" -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | VUE_APP_SOCKET_SERVER="localhost:3000" 2 | MYSQL_HOST="0.0.0.0" 3 | MYSQL_PORT="3306" 4 | MYSQL_USER="root" 5 | MYSQL_PASSWORD="root" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/base', 8 | 'plugin:vue/essential', 9 | 'plugin:vue/strongly-recommended', 10 | 'plugin:vue/recommended', 11 | 'eslint:recommended' 12 | ], 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | 'vue/html-self-closing': 'off', 17 | }, 18 | parserOptions: { 19 | parser: 'babel-eslint' 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /mysql 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql-events-ui 2 | 3 | A implementation from [rodrigogs](https://github.com/rodrigogs) [mysql-events](https://github.com/rodrigogs/mysql-events) project 4 | 5 | ## Install 6 | 7 | 1. You must have a mysql database running, you can create your own or just run `docker-compose up -d` 8 | 9 | 2. Configure the environment variables creating a .env based on .env.sample 10 | 11 | 3. Install the dependencies 12 | 13 | ```sh 14 | npm install 15 | ``` 16 | 17 | 4. Run express server to listen from database events 18 | 19 | ```sh 20 | npm start 21 | ``` 22 | 23 | 5. Start client application 24 | 25 | ```sh 26 | npm run serve 27 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mysql: 4 | image: mysql:5.7 5 | restart: always 6 | environment: 7 | MYSQL_ROOT_PASSWORD: root 8 | ports: 9 | - 3306:3306 10 | command: ["mysqld", "--log-bin=mysql-bin", "--server-id=1"] -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.(jsx|js)?$': 'babel-jest' 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1' 15 | }, 16 | snapshotSerializers: [ 17 | 'jest-serializer-vue' 18 | ], 19 | testMatch: [ 20 | '/tests/unit/**/*.spec.(js|jsx|ts|tsx)|/**/__tests__/*.(js|jsx|ts|tsx)' 21 | ] 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-events-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node server.js", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "test:unit": "vue-cli-service test:unit --watch" 11 | }, 12 | "dependencies": { 13 | "@rodrigogs/mysql-events": "^0.3.0", 14 | "date-fns": "^1.29.0", 15 | "dotenv": "^6.0.0", 16 | "express": "^4.16.3", 17 | "mysql": "^2.15.0", 18 | "socket.io": "^2.1.1", 19 | "socket.io-client": "^2.1.1", 20 | "uuid": "^3.3.2", 21 | "vue": "^2.5.16", 22 | "vue-router": "^3.0.1", 23 | "vue-timeago": "^4.0.2", 24 | "vue2vis": "0.0.13", 25 | "vuex": "^3.0.1" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^3.0.0-beta.15", 29 | "@vue/cli-plugin-eslint": "^3.0.0-beta.15", 30 | "@vue/cli-plugin-unit-jest": "^3.0.0-beta.15", 31 | "@vue/cli-service": "^3.0.0-beta.15", 32 | "@vue/test-utils": "^1.0.0-beta.16", 33 | "babel-core": "7.0.0-bridge.0", 34 | "babel-jest": "^23.0.1", 35 | "flush-promises": "^1.0.0", 36 | "vue-template-compiler": "^2.5.16" 37 | }, 38 | "browserslist": [ 39 | "> 1%", 40 | "last 2 versions", 41 | "not ie <= 8" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuroski/mysql-events-ui/be72173e634eab3cd5b51028249ad146c78b928f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | mysql-events-ui 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const app = require('express')() 2 | const http = require('http').Server(app) 3 | const io = require('socket.io')(http) 4 | const mysql = require('mysql') 5 | const MySQLEvents = require('@rodrigogs/mysql-events') 6 | require('dotenv').load() 7 | 8 | io.on('connection', (socket) => { 9 | console.log('a user connected') 10 | console.log(socket) 11 | }) 12 | 13 | http.listen(3000, () => { 14 | console.log('listening on *:3000') 15 | }) 16 | 17 | const program = async () => { 18 | const connection = mysql.createConnection({ 19 | host: process.env.MYSQL_HOST, 20 | port: process.env.MYSQL_PORT, 21 | user: process.env.MYSQL_USER, 22 | password: process.env.MYSQL_PASSWORD, 23 | }) 24 | 25 | const instance = new MySQLEvents(connection, { 26 | startAtEnd: true, 27 | excludedSchemas: { 28 | mysql: true, 29 | }, 30 | }) 31 | 32 | await instance.start() 33 | 34 | instance.addTrigger({ 35 | name: 'OPERATIONS', 36 | expression: '*', 37 | statement: MySQLEvents.STATEMENTS.ALL, 38 | callback: (event) => { 39 | console.log(event) 40 | io.emit('operationReceived', event) 41 | }, 42 | }) 43 | 44 | instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error) 45 | instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error) 46 | } 47 | 48 | program() 49 | .then(() => console.log('Waiting for database vents...')) 50 | .catch(console.error) -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/__mocks__/socket.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default { 4 | connect: jest.fn() 5 | } -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuroski/mysql-events-ui/be72173e634eab3cd5b51028249ad146c78b928f/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/VOperationsTimeline.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 37 | 38 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/VOperationsTimelineItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 69 | 70 | 129 | 130 | -------------------------------------------------------------------------------- /src/components/VOperationsTimelineItemDetail.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 83 | 84 | 106 | -------------------------------------------------------------------------------- /src/components/VOperationsTimelineItemDetailValue.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 53 | 54 | 75 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | export default { 2 | socketServer: process.env.VUE_APP_SOCKET_SERVER || 'localhost:3000' 3 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueTimeago from 'vue-timeago' 3 | import { Timeline } from 'vue2vis' 4 | import App from './App.vue' 5 | import router from './router' 6 | import store from './store' 7 | import 'vue2vis/dist/vue2vis.css' 8 | 9 | Vue.config.productionTip = false 10 | Vue.use(VueTimeago) 11 | Vue.component('timeline', Timeline) 12 | 13 | new Vue({ 14 | router, 15 | store, 16 | render: h => h(App) 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import TheOperationsLog from './views/TheOperationsLog.vue' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'operationsLog', 12 | component: TheOperationsLog 13 | }, 14 | ] 15 | }) 16 | -------------------------------------------------------------------------------- /src/socket.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client' 2 | import env from '@/env' 3 | import store from '@/store' 4 | 5 | let instance 6 | 7 | export default { 8 | instance: () => instance, 9 | connect() { 10 | if (!instance) instance = io(env.socketServer) 11 | instance.on('connect', this.onConnect) 12 | instance.on('operationReceived', this.onOperationReceived) 13 | return instance 14 | }, 15 | onConnect() { 16 | console.log('user connected') 17 | }, 18 | onOperationReceived(data) { 19 | return { 20 | INSERT: () => store.dispatch('INSERT', data), 21 | UPDATE: () => store.dispatch('UPDATE', data), 22 | DELETE: () => store.dispatch('DELETE', data), 23 | }[data.type]() 24 | }, 25 | } -------------------------------------------------------------------------------- /src/store/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default { 4 | dispatch: jest.fn() 5 | } -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | INSERT: ({ commit }, payload) => { 3 | commit('ADD_INSERT_OPERATION', payload) 4 | }, 5 | UPDATE: ({ commit }, payload) => { 6 | commit('ADD_UPDATE_OPERATION', payload) 7 | }, 8 | DELETE: ({ commit }, payload) => { 9 | commit('ADD_DELETE_OPERATION', payload) 10 | }, 11 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import state from '@/store/state' 4 | import mutations from '@/store/mutations' 5 | import actions from '@/store/actions' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | state, 11 | mutations, 12 | actions, 13 | }) 14 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ADD_INSERT_OPERATION: (state, payload) => { 3 | state.operations.push({...payload}) 4 | }, 5 | ADD_UPDATE_OPERATION: (state, payload) => { 6 | state.operations.push({...payload}) 7 | }, 8 | ADD_DELETE_OPERATION: (state, payload) => { 9 | state.operations.push({...payload}) 10 | }, 11 | } -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | operations: [] 3 | } 4 | -------------------------------------------------------------------------------- /src/views/TheOperationsLog.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 55 | 56 | 65 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | }, 5 | rules: { 6 | 'import/no-extraneous-dependencies': 'off' 7 | } 8 | } -------------------------------------------------------------------------------- /tests/unit/App.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('@/socket') 2 | import { shallowMount } from '@vue/test-utils' 3 | import socket from '@/socket' 4 | import App from '@/App' 5 | 6 | describe('App', () => { 7 | const build = () => { 8 | const wrapper = shallowMount(App, { 9 | stubs: ['router-view'] 10 | }) 11 | 12 | return { 13 | wrapper, 14 | } 15 | } 16 | 17 | it('renders the component correctly', () => { 18 | // arranje 19 | const { wrapper } = build() 20 | 21 | // assert 22 | expect(wrapper.html()).toMatchSnapshot() 23 | }) 24 | 25 | it('starts socket service', () => { 26 | // arranje 27 | build() 28 | 29 | // assert 30 | expect(socket.connect).toHaveBeenCalled() 31 | }) 32 | }) -------------------------------------------------------------------------------- /tests/unit/TheOperationsLog.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, createLocalVue } from '@vue/test-utils' 2 | import Vuex from 'vuex' 3 | import TheOperationsLog from '@/views/TheOperationsLog' 4 | import VOperationsTimeline from '@/components/VOperationsTimeline' 5 | import state from '@/store/state' 6 | 7 | const localVue = createLocalVue() 8 | localVue.use(Vuex) 9 | 10 | describe('TheOperationsLog', () => { 11 | const build = () => { 12 | const wrapper = shallowMount(TheOperationsLog, { 13 | store: new Vuex.Store({ state }), 14 | localVue, 15 | }) 16 | 17 | return { 18 | wrapper, 19 | operationsTimeline: () => wrapper.find(VOperationsTimeline) 20 | } 21 | } 22 | 23 | it('renders the component correctly', () => { 24 | // arranje 25 | const { wrapper } = build() 26 | 27 | // assert 28 | expect(wrapper.html()).toMatchSnapshot() 29 | }) 30 | 31 | it('renders main components', () => { 32 | // arranje 33 | const { operationsTimeline } = build() 34 | 35 | // assert 36 | expect(operationsTimeline().exists()).toBe(true) 37 | expect(operationsTimeline().props().operations).toBe(state.operations) 38 | }) 39 | }) -------------------------------------------------------------------------------- /tests/unit/VOperationsTimeline.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import VOperationsTimeline from '@/components/VOperationsTimeline' 3 | import VOperationsTimelineItem from '@/components/VOperationsTimelineItem' 4 | import socketFixture from './fixtures/socketServer' 5 | 6 | describe('VOperationsTimeline', () => { 7 | let props 8 | 9 | const build = () => { 10 | const wrapper = shallowMount(VOperationsTimeline, { 11 | propsData: props 12 | }) 13 | 14 | return { 15 | wrapper, 16 | operationsTimelineItems: () => wrapper.findAll(VOperationsTimelineItem), 17 | noOperations: () => wrapper.find('.no-operations'), 18 | } 19 | } 20 | 21 | beforeEach(() => { 22 | props = { 23 | operations: [] 24 | } 25 | }) 26 | 27 | it('renders the component correctly', () => { 28 | // arranje 29 | const { wrapper } = build() 30 | 31 | // assert 32 | expect(wrapper.html()).toMatchSnapshot() 33 | }) 34 | 35 | it('renders a list of timeline items', () => { 36 | // arranje 37 | props.operations = [ 38 | socketFixture.response.INSERT, 39 | socketFixture.response.UPDATE, 40 | socketFixture.response.DELETE, 41 | ] 42 | const { operationsTimelineItems, noOperations } = build() 43 | const firstItem = operationsTimelineItems().at(0) 44 | 45 | // assert 46 | expect(operationsTimelineItems().length).toBe(props.operations.length) 47 | expect(firstItem.exists()).toBe(true) 48 | expect(firstItem.props().operation).toBe(props.operations[0]) 49 | expect(noOperations().exists()).toBe(false) 50 | }) 51 | 52 | it('updates the timeline list items', () => { 53 | // arranje 54 | const expectedOperations = [ 55 | socketFixture.response.INSERT, 56 | socketFixture.response.UPDATE, 57 | ] 58 | 59 | props.operations = [ 60 | socketFixture.response.INSERT, 61 | socketFixture.response.UPDATE, 62 | socketFixture.response.DELETE, 63 | ] 64 | const { wrapper, operationsTimelineItems } = build() 65 | expect(operationsTimelineItems().length).toBe(props.operations.length) 66 | 67 | // act 68 | wrapper.setProps({ 69 | operations: expectedOperations 70 | }) 71 | 72 | // assert 73 | expect(operationsTimelineItems().length).toBe(expectedOperations.length) 74 | }) 75 | 76 | it('shows empty timeline when no operations are passed', () => { 77 | // arranje 78 | const { operationsTimelineItems, noOperations } = build() 79 | 80 | // assert 81 | expect(operationsTimelineItems().exists()).toBe(false) 82 | expect(noOperations().exists()).toBe(true) 83 | expect(noOperations().isVisible()).toBe(true) 84 | }) 85 | }) -------------------------------------------------------------------------------- /tests/unit/VOperationsTimelineItem.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, shallowMount, createLocalVue } from '@vue/test-utils' 2 | import toNow from 'date-fns/distance_in_words_to_now' 3 | import VueTimeago from 'vue-timeago' 4 | import VOperationsTimelineItem from '@/components/VOperationsTimelineItem' 5 | import VOperationsTimelineItemDetail from '@/components/VOperationsTimelineItemDetail' 6 | import socketFixture from './fixtures/socketServer' 7 | 8 | const localVue = createLocalVue() 9 | localVue.use(VueTimeago) 10 | 11 | describe('VOperationsTimelineItem', () => { 12 | let props 13 | 14 | const build = () => { 15 | const options = { 16 | propsData: props, 17 | localVue, 18 | } 19 | 20 | const wrapper = shallowMount(VOperationsTimelineItem, options) 21 | const wrapperMounted = mount(VOperationsTimelineItem, options) 22 | 23 | return { 24 | wrapper, 25 | header: () => wrapper.find('.timeline-item__header'), 26 | content: () => wrapper.find('.timeline-item__content'), 27 | type: () => wrapper.find('.type'), 28 | date: () => wrapperMounted.find('.date'), 29 | detail: () => wrapper.find(VOperationsTimelineItemDetail), 30 | } 31 | } 32 | 33 | beforeEach(() => { 34 | props = { 35 | operation: {} 36 | } 37 | }) 38 | 39 | it('renders the component correctly', () => { 40 | // arranje 41 | const { wrapper } = build() 42 | 43 | // assert 44 | expect(wrapper.html()).toMatchSnapshot() 45 | }) 46 | 47 | it('renders operation information', () => { 48 | // arranje 49 | props.operation = socketFixture.response.INSERT 50 | const { type, content, date, detail } = build() 51 | 52 | // assert 53 | expect(type().exists()).toBe(true) 54 | expect(type().text()).toContain(props.operation.type) 55 | 56 | expect(content().exists()).toBe(true) 57 | expect(content().text()).toContain(props.operation.schema) 58 | 59 | expect(content().exists()).toBe(true) 60 | expect(content().text()).toContain(props.operation.table) 61 | 62 | expect(date().exists()).toBe(true) 63 | expect(date().text()).toContain(toNow(props.operation.timestamp)) 64 | 65 | expect(detail().isVisible()).toBe(false) 66 | }) 67 | 68 | it('changes item class based on operation type', () => { 69 | // arranje 70 | props.operation = socketFixture.response.INSERT 71 | const { wrapper, header } = build() 72 | 73 | // assert INSERT 74 | expect(header().classes()).toContain('timeline-item__header--insert') 75 | expect(header().classes()).not.toContain('timeline-item__header--update') 76 | expect(header().classes()).not.toContain('timeline-item__header--delete') 77 | 78 | // assert UPDATE 79 | wrapper.setProps({ 80 | operation: socketFixture.response.UPDATE, 81 | }) 82 | expect(header().classes()).not.toContain('timeline-item__header--insert') 83 | expect(header().classes()).toContain('timeline-item__header--update') 84 | expect(header().classes()).not.toContain('timeline-item__header--delete') 85 | 86 | // assert DELETE 87 | wrapper.setProps({ 88 | operation: socketFixture.response.DELETE, 89 | }) 90 | expect(header().classes()).not.toContain('timeline-item__header--insert') 91 | expect(header().classes()).not.toContain('timeline-item__header--update') 92 | expect(header().classes()).toContain('timeline-item__header--delete') 93 | }) 94 | 95 | it('shows opeartion details', () => { 96 | // arranje 97 | const { content, detail } = build() 98 | 99 | // act 100 | expect(detail().isVisible()).toBe(false) 101 | content().trigger('click') 102 | 103 | // assert 104 | expect(detail().isVisible()).toBe(true) 105 | }) 106 | 107 | it('passes valid props to details component', () => { 108 | // arranje 109 | props.operation = socketFixture.response.INSERT 110 | const { detail } = build() 111 | 112 | // assert 113 | expect(detail().props().type).toBe(props.operation.type) 114 | expect(detail().props().rows).toBe(props.operation.affectedRows) 115 | }) 116 | }) -------------------------------------------------------------------------------- /tests/unit/VOperationsTimelineItemDetail.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import VOperationsTimelineItemDetail from '@/components/VOperationsTimelineItemDetail' 3 | import VOperationsTimelineItemDetailValue from '@/components/VOperationsTimelineItemDetailValue' 4 | import socketFixture from './fixtures/socketServer' 5 | 6 | describe('VOperationsTimelineItem', () => { 7 | let props 8 | 9 | const build = () => { 10 | const wrapper = shallowMount(VOperationsTimelineItemDetail, { 11 | propsData: props, 12 | }) 13 | 14 | return { 15 | wrapper, 16 | content: () => wrapper.find('.timeline-item-detail__content'), 17 | values: () => wrapper.findAll(VOperationsTimelineItemDetailValue), 18 | } 19 | } 20 | 21 | beforeEach(() => { 22 | props = { 23 | type: '', 24 | rows: [], 25 | } 26 | }) 27 | 28 | it('renders the component correctly', () => { 29 | // arranje 30 | const { wrapper } = build() 31 | 32 | // assert 33 | expect(wrapper.html()).toMatchSnapshot() 34 | }) 35 | 36 | it('renders correct INSERT operation details', () => { 37 | // arranje 38 | props.type = socketFixture.response.INSERT.type 39 | props.rows = socketFixture.response.INSERT.affectedRows 40 | const { values } = build() 41 | const firstValue = values().at(0) 42 | 43 | // assert 44 | expect(firstValue.props().prefix).toBe('+') 45 | expect(firstValue.props().name).toBe('id') 46 | expect(firstValue.props().value).toBe(4) 47 | expect(firstValue.props().highlight).toBe(false) 48 | expect(firstValue.props().add).toBe(true) 49 | }) 50 | 51 | it('renders correct UPDATE operation details', () => { 52 | // arranje 53 | props.type = socketFixture.response.UPDATE.type 54 | props.rows = socketFixture.response.UPDATE.affectedRows 55 | const { values } = build() 56 | const firstValue = values().at(0) 57 | const secondValue = values().at(1) 58 | 59 | // assert 60 | expect(firstValue.props().prefix).toBe('-') 61 | expect(firstValue.props().name).toBe('id') 62 | expect(firstValue.props().value).toBe(4) 63 | expect(firstValue.props().highlight).toBe(false) 64 | expect(firstValue.props().remove).toBe(true) 65 | 66 | expect(secondValue.props().prefix).toBe('+') 67 | expect(secondValue.props().name).toBe('id') 68 | expect(secondValue.props().value).toBe(5) 69 | expect(secondValue.props().highlight).toBe(true) 70 | expect(secondValue.props().add).toBe(true) 71 | }) 72 | 73 | it('renders correct DELETE operation details', () => { 74 | // arranje 75 | props.type = socketFixture.response.DELETE.type 76 | props.rows = socketFixture.response.DELETE.affectedRows 77 | const { values } = build() 78 | const firstValue = values().at(0) 79 | 80 | // assert 81 | expect(firstValue.props().prefix).toBe('-') 82 | expect(firstValue.props().name).toBe('id') 83 | expect(firstValue.props().value).toBe(5) 84 | expect(firstValue.props().highlight).toBe(false) 85 | expect(firstValue.props().remove).toBe(true) 86 | }) 87 | }) -------------------------------------------------------------------------------- /tests/unit/VOperationsTimelineItemDetailValue.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import VOperationsTimelineItemDetailValue from '@/components/VOperationsTimelineItemDetailValue' 3 | import socketFixture from './fixtures/socketServer' 4 | 5 | describe('VOperationsTimelineItem', () => { 6 | let props 7 | 8 | const build = () => { 9 | const wrapper = shallowMount(VOperationsTimelineItemDetailValue, { 10 | propsData: props, 11 | }) 12 | 13 | return { 14 | wrapper, 15 | } 16 | } 17 | 18 | beforeEach(() => { 19 | props = { 20 | name: 'id', 21 | value: 4, 22 | } 23 | }) 24 | 25 | it('renders the component correctly', () => { 26 | // arranje 27 | const { wrapper } = build() 28 | 29 | // assert 30 | expect(wrapper.html()).toMatchSnapshot() 31 | }) 32 | }) -------------------------------------------------------------------------------- /tests/unit/__snapshots__/App.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App renders the component correctly 1`] = ` 4 |
5 | 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/TheOperationsLog.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TheOperationsLog renders the component correctly 1`] = ` 4 |
5 | 7 | 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/VOperationsTimeline.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VOperationsTimeline renders the component correctly 1`] = ` 4 |
5 |
6 | 7 | timer_off 8 | 9 | No operations on the moment 10 |
11 |
12 | `; 13 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/VOperationsTimelineItem.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VOperationsTimelineItem renders the component correctly 1`] = ` 4 |
5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 |
: 15 | 16 |
17 | 18 | unfold_more 19 | 20 |
21 | 22 |
23 | `; 24 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/VOperationsTimelineItemDetail.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VOperationsTimelineItem renders the component correctly 1`] = ` 4 |
5 |
6 | 7 | 8 |
9 |
10 | `; 11 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/VOperationsTimelineItemDetailValue.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VOperationsTimelineItem renders the component correctly 1`] = ` 4 |
5 | 6 | id: 7 | 4 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /tests/unit/actions.spec.js: -------------------------------------------------------------------------------- 1 | import actions from '@/store/actions' 2 | import socketFixture from './fixtures/socketServer' 3 | 4 | describe('vuex actions', () => { 5 | let commit 6 | 7 | beforeEach(() => { 8 | commit = jest.fn() 9 | }) 10 | 11 | it('process INSERT operation', () => { 12 | // arranje 13 | const expectedPayload = socketFixture.response.INSERT 14 | 15 | // act 16 | actions.INSERT({ commit }, expectedPayload) 17 | 18 | // assert 19 | expect(commit).toHaveBeenCalledWith('ADD_INSERT_OPERATION', expectedPayload) 20 | }) 21 | 22 | it('process UPDATE operation', () => { 23 | // arranje 24 | const expectedPayload = socketFixture.response.UPDATE 25 | 26 | // act 27 | actions.UPDATE({ commit }, expectedPayload) 28 | 29 | // assert 30 | expect(commit).toHaveBeenCalledWith('ADD_UPDATE_OPERATION', expectedPayload) 31 | }) 32 | 33 | it('process DELETE operation', () => { 34 | // arranje 35 | const expectedPayload = socketFixture.response.DELETE 36 | 37 | // act 38 | actions.DELETE({ commit }, expectedPayload) 39 | 40 | // assert 41 | expect(commit).toHaveBeenCalledWith('ADD_DELETE_OPERATION', expectedPayload) 42 | }) 43 | }) -------------------------------------------------------------------------------- /tests/unit/fixtures/socketServer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | response: { 3 | INSERT: { 4 | type: 'INSERT', 5 | schema: 'mydb', 6 | table: 'mytable', 7 | affectedRows: [ 8 | { 9 | after: { 10 | id: 4 11 | } 12 | } 13 | ], 14 | affectedColumns: [ 15 | 'id' 16 | ], 17 | timestamp: 1530985873000, 18 | nextPosition: 2199 19 | }, 20 | UPDATE: { 21 | type: 'UPDATE', 22 | schema: 'mydb', 23 | table: 'mytable', 24 | affectedRows: [ 25 | { 26 | after: { 27 | id: 5 28 | }, 29 | before: { 30 | id: 4 31 | } 32 | } 33 | ], 34 | affectedColumns: [ 35 | 'id' 36 | ], 37 | timestamp: 1530985930000, 38 | nextPosition: 2463 39 | }, 40 | DELETE: { 41 | type: 'DELETE', 42 | schema: 'mydb', 43 | table: 'mytable', 44 | affectedRows: [ 45 | { 46 | before: { 47 | id: 5 48 | } 49 | } 50 | ], 51 | affectedColumns: [ 52 | 'id' 53 | ], 54 | timestamp: 1530985970000, 55 | nextPosition: 2721 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /tests/unit/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import state from '@/store/state' 2 | import mutations from '@/store/mutations' 3 | import socketFixture from './fixtures/socketServer' 4 | 5 | describe('store mutations', () => { 6 | let storeState 7 | 8 | beforeEach(() => { 9 | storeState = JSON.parse(JSON.stringify(state)) 10 | }) 11 | 12 | it('adds INSERT operation', () => { 13 | // arranje 14 | const expectedPayload = socketFixture.response.INSERT 15 | 16 | // act 17 | mutations.ADD_INSERT_OPERATION(storeState, expectedPayload) 18 | 19 | // assert 20 | expect(storeState.operations).toHaveLength(1) 21 | expect(storeState.operations[0]).toEqual(expectedPayload) 22 | expect(storeState.operations[0]).not.toBe(expectedPayload) 23 | }) 24 | 25 | it('adds UPDATE operation', () => { 26 | // arranje 27 | const expectedPayload = socketFixture.response.UPDATE 28 | 29 | // act 30 | mutations.ADD_UPDATE_OPERATION(storeState, expectedPayload) 31 | 32 | // assert 33 | expect(storeState.operations).toHaveLength(1) 34 | expect(storeState.operations[0]).toEqual(expectedPayload) 35 | expect(storeState.operations[0]).not.toBe(expectedPayload) 36 | }) 37 | 38 | it('adds DELETE operation', () => { 39 | // arranje 40 | const expectedPayload = socketFixture.response.DELETE 41 | 42 | // act 43 | mutations.ADD_DELETE_OPERATION(storeState, expectedPayload) 44 | 45 | // assert 46 | expect(storeState.operations).toHaveLength(1) 47 | expect(storeState.operations[0]).toEqual(expectedPayload) 48 | expect(storeState.operations[0]).not.toBe(expectedPayload) 49 | }) 50 | }) -------------------------------------------------------------------------------- /tests/unit/socket.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('@/store') 2 | jest.mock('socket.io-client', () => jest.fn(() => ({ 3 | on: jest.fn() 4 | }))) 5 | import io from 'socket.io-client' 6 | import flushPromises from 'flush-promises' 7 | import store from '@/store' 8 | import socket from '@/socket' 9 | import env from '@/env' 10 | import socketFixture from './fixtures/socketServer' 11 | 12 | describe('socket service', () => { 13 | 14 | afterEach(() => { 15 | jest.resetModules() 16 | jest.resetAllMocks() 17 | }) 18 | 19 | it('connects to the socket server if not instanciated', async () => { 20 | // arranje 21 | expect(socket.instance()).not.toBeDefined() 22 | 23 | // act 24 | const instance = socket.connect() 25 | await flushPromises() 26 | 27 | // assert 28 | expect(io).toHaveBeenCalledWith(env.socketServer) 29 | expect(instance.on).toHaveBeenCalledWith('connect', socket.onConnect) 30 | expect(instance.on).toHaveBeenCalledWith('operationReceived', socket.onOperationReceived) 31 | expect(socket.instance()).toBeDefined() 32 | }) 33 | 34 | it('calls store insert action when INSERT operation is received', () => { 35 | // arranje 36 | const expectedPayload = socketFixture.response.INSERT 37 | 38 | // act 39 | socket.onOperationReceived(expectedPayload) 40 | 41 | // assert 42 | expect(store.dispatch).toHaveBeenCalled() 43 | expect(store.dispatch).toHaveBeenCalledTimes(1) 44 | expect(store.dispatch.mock.calls[0][0]).toBe('INSERT') 45 | expect(store.dispatch.mock.calls[0][1]).toBe(expectedPayload) 46 | }) 47 | 48 | it('calls store update action when UPDATE operation is received', () => { 49 | // arranje 50 | const expectedPayload = socketFixture.response.UPDATE 51 | 52 | // act 53 | socket.onOperationReceived(expectedPayload) 54 | 55 | // assert 56 | expect(store.dispatch).toHaveBeenCalled() 57 | expect(store.dispatch).toHaveBeenCalledTimes(1) 58 | expect(store.dispatch.mock.calls[0][0]).toBe('UPDATE') 59 | expect(store.dispatch.mock.calls[0][1]).toBe(expectedPayload) 60 | }) 61 | 62 | it('calls store delete action when DELETE operation is received', () => { 63 | // arranje 64 | const expectedPayload = socketFixture.response.DELETE 65 | 66 | // act 67 | socket.onOperationReceived(expectedPayload) 68 | 69 | // assert 70 | expect(store.dispatch).toHaveBeenCalled() 71 | expect(store.dispatch).toHaveBeenCalledTimes(1) 72 | expect(store.dispatch.mock.calls[0][0]).toBe('DELETE') 73 | expect(store.dispatch.mock.calls[0][1]).toBe(expectedPayload) 74 | }) 75 | }) --------------------------------------------------------------------------------