) {
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 |
5 | -
8 |
9 |
10 |
11 |
12 | -
15 |
16 |
17 |
18 |
19 |
20 | `;
21 |
22 | exports[`NavSide highlight current component 1`] = `
23 |
24 | -
27 |
28 |
38 |
39 |
40 | -
43 |
44 |
51 |
52 |
53 |
54 | `;
55 |
56 | exports[`NavSide highlight current pattern 1`] = `
57 |
58 | -
61 |
62 |
72 |
73 |
74 | -
77 |
78 |
85 |
86 |
87 |
88 | `;
89 |
90 | exports[`NavSide renders declarations 1`] = `
91 |
92 | -
95 |
96 |
106 |
107 |
108 | -
111 |
112 |
119 |
120 |
121 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
25 |
--------------------------------------------------------------------------------
/packages/@birdseye/snapshot/test/fixture/components/Animation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
--------------------------------------------------------------------------------
/packages/@birdseye/snapshot/test/fixture/components/Fill.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This area should fill entire preview space
4 |
5 |
6 |
7 |
22 |
--------------------------------------------------------------------------------
/packages/@birdseye/snapshot/test/fixture/components/Fixed.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is the base text
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/packages/@birdseye/snapshot/test/fixture/components/Foo.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ foo + ' ' + bar }}
3 |
4 |
5 |
23 |
24 |
31 |
--------------------------------------------------------------------------------
/packages/@birdseye/snapshot/test/fixture/components/Hover.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
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 |
--------------------------------------------------------------------------------