w-flow-vue 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/AGENT.md +223 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-flow-vue.umd.js +15 -0
- package/dist/w-flow-vue.umd.js.map +1 -0
- package/docs/components_WFlowVue.vue.html +1214 -0
- package/docs/examples/app.html +62 -0
- package/docs/examples/app.umd.js +20 -0
- package/docs/examples/app.umd.js.map +1 -0
- package/docs/examples/ex-AppBasic.html +440 -0
- package/docs/examples/ex-AppConnectivity.html +131 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/global.html +1919 -0
- package/docs/index.html +84 -0
- package/docs/js_defaults.mjs.html +105 -0
- package/docs/js_edge-path.mjs.html +237 -0
- package/docs/js_geometry.mjs.html +298 -0
- package/docs/js_graph.mjs.html +103 -0
- package/docs/js_step-routing.mjs.html +346 -0
- package/docs/module-WFlowVue.html +2790 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/jest.config.js +20 -0
- package/package.json +80 -0
- package/public/index.html +38 -0
- package/script.txt +22 -0
- package/src/App.vue +326 -0
- package/src/AppBasic.vue +125 -0
- package/src/AppConnectivity.vue +186 -0
- package/src/components/WFlowVue.vue +1142 -0
- package/src/components/canvas/BackgroundLayer.vue +78 -0
- package/src/components/canvas/FlowCanvas.vue +64 -0
- package/src/components/canvas/SelectionBox.vue +36 -0
- package/src/components/canvas/ViewportTransform.vue +35 -0
- package/src/components/edges/ConnectionLine.vue +65 -0
- package/src/components/edges/EdgeMarkerDefs.vue +76 -0
- package/src/components/edges/EdgeRenderer.vue +120 -0
- package/src/components/edges/EdgeWrapper.vue +379 -0
- package/src/components/nodes/DefaultNode.vue +276 -0
- package/src/components/nodes/Handle.vue +101 -0
- package/src/components/nodes/InputNode.vue +47 -0
- package/src/components/nodes/NodeBody.vue +103 -0
- package/src/components/nodes/NodeFace.vue +128 -0
- package/src/components/nodes/NodeRenderer.vue +95 -0
- package/src/components/nodes/NodeWrapper.vue +475 -0
- package/src/components/nodes/OutputNode.vue +47 -0
- package/src/components/ui/ConnSettingsForm.vue +158 -0
- package/src/components/ui/Controls.vue +83 -0
- package/src/components/ui/NodeSettingsForm.vue +185 -0
- package/src/js/defaults.mjs +33 -0
- package/src/js/edge-path.mjs +165 -0
- package/src/js/geometry.mjs +226 -0
- package/src/js/graph.mjs +31 -0
- package/src/js/step-routing.mjs +274 -0
- package/src/main.js +22 -0
- package/test/WFlowVue-features.test.mjs +760 -0
- package/test/WFlowVue.test.mjs +421 -0
- package/test/components-canvas.test.mjs +102 -0
- package/test/components-edge.test.mjs +147 -0
- package/test/components-node.test.mjs +174 -0
- package/test/components-ui.test.mjs +69 -0
- package/test/defaults.test.mjs +86 -0
- package/test/edge-path.test.mjs +102 -0
- package/test/generate-routing-snapshots.mjs +77 -0
- package/test/generate-visual-baselines.mjs +206 -0
- package/test/geometry.test.mjs +236 -0
- package/test/graph.test.mjs +72 -0
- package/test/jsons/routing-snapshots.json +24994 -0
- package/test/pics/_check2.png +0 -0
- package/test/pics/_check3.png +0 -0
- package/test/pics/_check4.png +0 -0
- package/test/pics/_check5.png +0 -0
- package/test/pics/_v1.png +0 -0
- package/test/pics/_v2.png +0 -0
- package/test/pics/_v3.png +0 -0
- package/test/pics/_v4.png +0 -0
- package/test/pics/_v5.png +0 -0
- package/test/pics/_v6.png +0 -0
- package/test/pics/_v7.png +0 -0
- package/test/pics/vb-edge-hovered.png +0 -0
- package/test/pics/vb-edges-normal.png +0 -0
- package/test/pics/vb-locked-edge-hovered.png +0 -0
- package/test/pics/vb-locked-node-hovered.png +0 -0
- package/test/pics/vb-locked-node-selected.png +0 -0
- package/test/pics/vb-locked-overview.png +0 -0
- package/test/pics/vb-node-1.png +0 -0
- package/test/pics/vb-node-10.png +0 -0
- package/test/pics/vb-node-11.png +0 -0
- package/test/pics/vb-node-12.png +0 -0
- package/test/pics/vb-node-2.png +0 -0
- package/test/pics/vb-node-3.png +0 -0
- package/test/pics/vb-node-4.png +0 -0
- package/test/pics/vb-node-5.png +0 -0
- package/test/pics/vb-node-6.png +0 -0
- package/test/pics/vb-node-7.png +0 -0
- package/test/pics/vb-node-8.png +0 -0
- package/test/pics/vb-node-9.png +0 -0
- package/test/pics/vb-node-hovered.png +0 -0
- package/test/pics/vb-node-selected.png +0 -0
- package/test/pics/vb-overview.png +0 -0
- package/test/step-routing-connectivity.test.mjs +78 -0
- package/test/step-routing.test.mjs +88 -0
- package/test/visual-regression.test.mjs +274 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +4 -0
- package/toolg/gDistApp.mjs +34 -0
- package/toolg/gDistRollupComps.mjs +22 -0
- package/toolg/gDocExams.mjs +47 -0
- package/toolg/gExtractHtml.mjs +179 -0
- package/toolg/modifyReadme.mjs +4 -0
- package/vue.config.js +9 -0
- package/vue2/344/271/213foreignObject/345/205/247/346/270/262/346/237/223/345/225/217/351/241/214/350/210/207/344/277/256/346/255/243.md +151 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing snapshot regression test.
|
|
3
|
+
*
|
|
4
|
+
* 576 combinations: 4 source handles × 4 target handles × 36 angles (10° steps).
|
|
5
|
+
* Each case verifies calculateStepPoints output matches the saved snapshot exactly.
|
|
6
|
+
* If a code change alters any routing result, this test will catch it.
|
|
7
|
+
*
|
|
8
|
+
* To regenerate snapshots after intentional changes:
|
|
9
|
+
* node --experimental-vm-modules test/generate-routing-snapshots.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { calculateStepPoints, clearStepCache } from '../src/js/step-routing'
|
|
12
|
+
import snapshots from './jsons/routing-snapshots.json'
|
|
13
|
+
|
|
14
|
+
const NODE_W = 100
|
|
15
|
+
const NODE_H = 40
|
|
16
|
+
const BUF = 24
|
|
17
|
+
|
|
18
|
+
describe('routing snapshot regression', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
clearStepCache()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('snapshot file has 576 cases', () => {
|
|
24
|
+
expect(snapshots.length).toBe(576)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe.each(
|
|
28
|
+
snapshots.map((c, i) => [
|
|
29
|
+
`[${i}] ${c.srcPos}→${c.tgtPos} ${c.deg}°`,
|
|
30
|
+
c,
|
|
31
|
+
])
|
|
32
|
+
)('%s', (_label, c) => {
|
|
33
|
+
test('matches snapshot', () => {
|
|
34
|
+
let nodes = [
|
|
35
|
+
{ id: 'A', position: c.aPos, width: NODE_W, height: NODE_H },
|
|
36
|
+
{ id: 'B', position: c.bPos, width: NODE_W, height: NODE_H },
|
|
37
|
+
]
|
|
38
|
+
let pts = calculateStepPoints(
|
|
39
|
+
c.sourceXY.x, c.sourceXY.y, c.srcPos,
|
|
40
|
+
c.targetXY.x, c.targetXY.y, c.tgtPos,
|
|
41
|
+
BUF, nodes, {}, 'A', 'B'
|
|
42
|
+
)
|
|
43
|
+
expect(pts).toEqual(c.pts)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('no NaN in points', () => {
|
|
47
|
+
let nodes = [
|
|
48
|
+
{ id: 'A', position: c.aPos, width: NODE_W, height: NODE_H },
|
|
49
|
+
{ id: 'B', position: c.bPos, width: NODE_W, height: NODE_H },
|
|
50
|
+
]
|
|
51
|
+
let pts = calculateStepPoints(
|
|
52
|
+
c.sourceXY.x, c.sourceXY.y, c.srcPos,
|
|
53
|
+
c.targetXY.x, c.targetXY.y, c.tgtPos,
|
|
54
|
+
BUF, nodes, {}, 'A', 'B'
|
|
55
|
+
)
|
|
56
|
+
pts.forEach((p, j) => {
|
|
57
|
+
expect(isNaN(p.x)).toBe(false)
|
|
58
|
+
expect(isNaN(p.y)).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('first/last points match source/target', () => {
|
|
63
|
+
let nodes = [
|
|
64
|
+
{ id: 'A', position: c.aPos, width: NODE_W, height: NODE_H },
|
|
65
|
+
{ id: 'B', position: c.bPos, width: NODE_W, height: NODE_H },
|
|
66
|
+
]
|
|
67
|
+
let pts = calculateStepPoints(
|
|
68
|
+
c.sourceXY.x, c.sourceXY.y, c.srcPos,
|
|
69
|
+
c.targetXY.x, c.targetXY.y, c.tgtPos,
|
|
70
|
+
BUF, nodes, {}, 'A', 'B'
|
|
71
|
+
)
|
|
72
|
+
expect(pts[0].x).toBe(c.sourceXY.x)
|
|
73
|
+
expect(pts[0].y).toBe(c.sourceXY.y)
|
|
74
|
+
expect(pts[pts.length - 1].x).toBe(c.targetXY.x)
|
|
75
|
+
expect(pts[pts.length - 1].y).toBe(c.targetXY.y)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { calculateStepPoints, clearStepCache } from '../src/js/step-routing'
|
|
2
|
+
|
|
3
|
+
describe('step-routing', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
clearStepCache()
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
describe('calculateStepPoints', () => {
|
|
9
|
+
test('returns array of points', () => {
|
|
10
|
+
const pts = calculateStepPoints(100, 50, 'bottom', 300, 250, 'top', 20)
|
|
11
|
+
expect(Array.isArray(pts)).toBe(true)
|
|
12
|
+
expect(pts.length).toBeGreaterThanOrEqual(2)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('first point matches source', () => {
|
|
16
|
+
const pts = calculateStepPoints(100, 50, 'bottom', 300, 250, 'top', 20)
|
|
17
|
+
expect(pts[0].x).toBe(100)
|
|
18
|
+
expect(pts[0].y).toBe(50)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('last point matches target', () => {
|
|
22
|
+
const pts = calculateStepPoints(100, 50, 'bottom', 300, 250, 'top', 20)
|
|
23
|
+
const last = pts[pts.length - 1]
|
|
24
|
+
expect(last.x).toBe(300)
|
|
25
|
+
expect(last.y).toBe(250)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('all points have x and y', () => {
|
|
29
|
+
const pts = calculateStepPoints(0, 0, 'right', 200, 100, 'left', 20)
|
|
30
|
+
pts.forEach(p => {
|
|
31
|
+
expect(typeof p.x).toBe('number')
|
|
32
|
+
expect(typeof p.y).toBe('number')
|
|
33
|
+
expect(isNaN(p.x)).toBe(false)
|
|
34
|
+
expect(isNaN(p.y)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('works with all position combinations', () => {
|
|
39
|
+
const positions = ['top', 'bottom', 'left', 'right']
|
|
40
|
+
positions.forEach(sp => {
|
|
41
|
+
positions.forEach(tp => {
|
|
42
|
+
const pts = calculateStepPoints(0, 0, sp, 200, 200, tp, 20)
|
|
43
|
+
expect(pts.length).toBeGreaterThanOrEqual(2)
|
|
44
|
+
expect(pts[0]).toEqual({ x: 0, y: 0 })
|
|
45
|
+
expect(pts[pts.length - 1]).toEqual({ x: 200, y: 200 })
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('avoids node obstacles', () => {
|
|
51
|
+
const nodes = [
|
|
52
|
+
{ id: '1', position: { x: 0, y: 0 }, width: 100, height: 40 },
|
|
53
|
+
{ id: '2', position: { x: 200, y: 200 }, width: 100, height: 40 },
|
|
54
|
+
{ id: 'block', position: { x: 100, y: 80 }, width: 100, height: 100 },
|
|
55
|
+
]
|
|
56
|
+
const ni = {}
|
|
57
|
+
const pts = calculateStepPoints(
|
|
58
|
+
50, 40, 'bottom', 250, 200, 'top', 20,
|
|
59
|
+
nodes, ni, '1', '2'
|
|
60
|
+
)
|
|
61
|
+
expect(pts.length).toBeGreaterThanOrEqual(2)
|
|
62
|
+
expect(pts[0]).toEqual({ x: 50, y: 40 })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('handles same source and target position', () => {
|
|
66
|
+
const pts = calculateStepPoints(100, 100, 'bottom', 100, 300, 'bottom', 20)
|
|
67
|
+
expect(pts.length).toBeGreaterThanOrEqual(2)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('handles close source and target', () => {
|
|
71
|
+
const pts = calculateStepPoints(100, 100, 'bottom', 105, 105, 'top', 20)
|
|
72
|
+
expect(pts.length).toBeGreaterThanOrEqual(2)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('clearStepCache', () => {
|
|
77
|
+
test('does not throw', () => {
|
|
78
|
+
expect(() => clearStepCache()).not.toThrow()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('can be called multiple times', () => {
|
|
82
|
+
clearStepCache()
|
|
83
|
+
clearStepCache()
|
|
84
|
+
clearStepCache()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
})
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual regression test using Playwright screenshots.
|
|
3
|
+
* Compares current rendering against baseline images pixel-by-pixel.
|
|
4
|
+
*
|
|
5
|
+
* Requires: npm run serve (http://localhost:8080) running
|
|
6
|
+
* Run: node test/visual-regression.test.mjs
|
|
7
|
+
*
|
|
8
|
+
* To regenerate baselines after intentional visual changes:
|
|
9
|
+
* node test/generate-visual-baselines.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { chromium } from 'playwright'
|
|
12
|
+
import fs from 'fs'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import { fileURLToPath } from 'url'
|
|
15
|
+
import { PNG } from 'pngjs'
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const baselineDir = path.join(__dirname, 'pics')
|
|
19
|
+
|
|
20
|
+
function comparePNG(baselinePath, currentBuffer, threshold = 0.01) {
|
|
21
|
+
if (!fs.existsSync(baselinePath)) {
|
|
22
|
+
return { match: false, reason: 'Baseline not found: ' + baselinePath }
|
|
23
|
+
}
|
|
24
|
+
const baseline = PNG.sync.read(fs.readFileSync(baselinePath))
|
|
25
|
+
const current = PNG.sync.read(currentBuffer)
|
|
26
|
+
if (baseline.width !== current.width || baseline.height !== current.height) {
|
|
27
|
+
return { match: false, reason: `Size: ${baseline.width}x${baseline.height} vs ${current.width}x${current.height}` }
|
|
28
|
+
}
|
|
29
|
+
let diffPixels = 0
|
|
30
|
+
const total = baseline.width * baseline.height
|
|
31
|
+
for (let i = 0; i < baseline.data.length; i += 4) {
|
|
32
|
+
let d = Math.abs(baseline.data[i] - current.data[i]) + Math.abs(baseline.data[i + 1] - current.data[i + 1]) + Math.abs(baseline.data[i + 2] - current.data[i + 2])
|
|
33
|
+
if (d > 30) diffPixels++
|
|
34
|
+
}
|
|
35
|
+
let diffRatio = diffPixels / total
|
|
36
|
+
return { match: diffRatio <= threshold, diffRatio, diffPixels, total }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let passed = 0, failed = 0, errors = []
|
|
40
|
+
|
|
41
|
+
async function assert(name, result) {
|
|
42
|
+
if (result.match) {
|
|
43
|
+
passed++
|
|
44
|
+
console.log(` ✓ ${name}`)
|
|
45
|
+
} else {
|
|
46
|
+
failed++
|
|
47
|
+
let msg = result.reason || `${(result.diffRatio * 100).toFixed(2)}% diff (${result.diffPixels}/${result.total})`
|
|
48
|
+
errors.push(`${name}: ${msg}`)
|
|
49
|
+
console.log(` ✗ ${name} — ${msg}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const VW = 1280
|
|
54
|
+
const VH = 900
|
|
55
|
+
|
|
56
|
+
/** Clip a region around a bounding box with padding, clamped to viewport */
|
|
57
|
+
function clipAround(box, pad) {
|
|
58
|
+
let x = Math.max(0, Math.floor(box.x - pad))
|
|
59
|
+
let y = Math.max(0, Math.floor(box.y - pad))
|
|
60
|
+
let w = Math.min(Math.ceil(box.width + pad * 2), VW - x)
|
|
61
|
+
let h = Math.min(Math.ceil(box.height + pad * 2), VH - y)
|
|
62
|
+
if (w < 1 || h < 1) return null
|
|
63
|
+
return { x, y, width: w, height: h }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Center viewport on a node by its data id */
|
|
67
|
+
function centerOnNode(page, nodeId) {
|
|
68
|
+
return page.evaluate((id) => {
|
|
69
|
+
let vm
|
|
70
|
+
for (let el of document.querySelectorAll('*')) {
|
|
71
|
+
if (el.__vue__ && el.__vue__.setViewport) {
|
|
72
|
+
vm = el.__vue__
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!vm) return false
|
|
77
|
+
let n = vm.nodes.find(n => n.id === id)
|
|
78
|
+
if (!n) return false
|
|
79
|
+
let cw = vm.widthInp
|
|
80
|
+
let ch = vm.heightInp
|
|
81
|
+
let w = n.width || 100
|
|
82
|
+
let h = n.height || 40
|
|
83
|
+
vm.setViewport({ x: cw / 2 - (n.position.x + w / 2), y: ch / 2 - (n.position.y + h / 2), zoom: 1 })
|
|
84
|
+
return true
|
|
85
|
+
}, nodeId)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Reset viewport to origin */
|
|
89
|
+
function resetViewport(page) {
|
|
90
|
+
return page.evaluate(() => {
|
|
91
|
+
for (let el of document.querySelectorAll('*')) {
|
|
92
|
+
if (el.__vue__ && el.__vue__.setViewport) {
|
|
93
|
+
el.__vue__.setViewport({ x: 0, y: 0, zoom: 1 })
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** fitView to show all nodes */
|
|
101
|
+
function fitView(page) {
|
|
102
|
+
return page.evaluate(() => {
|
|
103
|
+
for (let el of document.querySelectorAll('*')) {
|
|
104
|
+
if (el.__vue__ && el.__vue__.fitView) {
|
|
105
|
+
el.__vue__.fitView(30)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Get the canvas container clip region */
|
|
113
|
+
function getCanvasClip(page) {
|
|
114
|
+
return page.evaluate(() => {
|
|
115
|
+
let el = document.querySelector('.vue-flow__viewport')
|
|
116
|
+
if (!el) return null
|
|
117
|
+
let container = el.closest('[style*="height"]')
|
|
118
|
+
if (!container) return null
|
|
119
|
+
let rect = container.getBoundingClientRect()
|
|
120
|
+
return { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.ceil(rect.width), height: Math.ceil(rect.height) }
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function run() {
|
|
125
|
+
const browser = await chromium.launch()
|
|
126
|
+
const page = await browser.newPage({ viewport: { width: VW, height: VH } })
|
|
127
|
+
await page.goto('http://localhost:8080')
|
|
128
|
+
await page.waitForTimeout(2000)
|
|
129
|
+
|
|
130
|
+
console.log('Visual Regression Tests\n')
|
|
131
|
+
const pad = 60
|
|
132
|
+
|
|
133
|
+
// 1. Overview — fitView to show all nodes
|
|
134
|
+
await fitView(page)
|
|
135
|
+
await page.waitForTimeout(500)
|
|
136
|
+
let canvasClip = await getCanvasClip(page)
|
|
137
|
+
let buf = await page.screenshot({ clip: canvasClip })
|
|
138
|
+
await assert('overview', comparePNG(path.join(baselineDir, 'vb-overview.png'), buf))
|
|
139
|
+
|
|
140
|
+
// 2. Each node — center on node, clip with padding (includes surrounding edges)
|
|
141
|
+
let nodes = await page.$$('.vue-flow__node')
|
|
142
|
+
for (let node of nodes) {
|
|
143
|
+
let id = await node.getAttribute('data-id')
|
|
144
|
+
if (!id) continue
|
|
145
|
+
let bp = path.join(baselineDir, `vb-node-${id}.png`)
|
|
146
|
+
if (!fs.existsSync(bp)) continue
|
|
147
|
+
await centerOnNode(page, id)
|
|
148
|
+
await page.waitForTimeout(300)
|
|
149
|
+
let box = await node.boundingBox()
|
|
150
|
+
if (!box) continue
|
|
151
|
+
let clip = clipAround(box, pad)
|
|
152
|
+
if (!clip) continue
|
|
153
|
+
buf = await page.screenshot({ clip })
|
|
154
|
+
await assert(`node-${id}`, comparePNG(bp, buf))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Node hovered — resize handles + settings icon
|
|
158
|
+
let firstNode = nodes[0]
|
|
159
|
+
let firstId = firstNode ? await firstNode.getAttribute('data-id') : null
|
|
160
|
+
if (firstNode && firstId) {
|
|
161
|
+
await centerOnNode(page, firstId)
|
|
162
|
+
await page.waitForTimeout(300)
|
|
163
|
+
let box = await firstNode.boundingBox()
|
|
164
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
|
165
|
+
await page.waitForTimeout(500)
|
|
166
|
+
box = await firstNode.boundingBox()
|
|
167
|
+
buf = await page.screenshot({ clip: clipAround(box, pad) })
|
|
168
|
+
await assert('node-hovered', comparePNG(path.join(baselineDir, 'vb-node-hovered.png'), buf))
|
|
169
|
+
await page.mouse.move(0, 0)
|
|
170
|
+
await page.waitForTimeout(300)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4. Node selected
|
|
174
|
+
if (firstNode && firstId) {
|
|
175
|
+
await centerOnNode(page, firstId)
|
|
176
|
+
await page.waitForTimeout(300)
|
|
177
|
+
await firstNode.click()
|
|
178
|
+
await page.waitForTimeout(300)
|
|
179
|
+
let box = await firstNode.boundingBox()
|
|
180
|
+
buf = await page.screenshot({ clip: clipAround(box, pad) })
|
|
181
|
+
await assert('node-selected', comparePNG(path.join(baselineDir, 'vb-node-selected.png'), buf))
|
|
182
|
+
await page.mouse.click(10, 10)
|
|
183
|
+
await page.waitForTimeout(300)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Edge hovered — settings icon
|
|
187
|
+
await resetViewport(page)
|
|
188
|
+
await page.waitForTimeout(300)
|
|
189
|
+
let edges = await page.$$('.vue-flow__edge-interaction')
|
|
190
|
+
if (edges.length > 0) {
|
|
191
|
+
let box = await edges[0].boundingBox()
|
|
192
|
+
if (box) {
|
|
193
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
|
194
|
+
await page.waitForTimeout(500)
|
|
195
|
+
box = await edges[0].boundingBox()
|
|
196
|
+
buf = await page.screenshot({ clip: clipAround(box, 40) })
|
|
197
|
+
await assert('edge-hovered', comparePNG(path.join(baselineDir, 'vb-edge-hovered.png'), buf))
|
|
198
|
+
await page.mouse.move(0, 0)
|
|
199
|
+
await page.waitForTimeout(300)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 6. Edges normal
|
|
204
|
+
await fitView(page)
|
|
205
|
+
await page.waitForTimeout(500)
|
|
206
|
+
buf = await page.screenshot({ clip: canvasClip })
|
|
207
|
+
await assert('edges-normal', comparePNG(path.join(baselineDir, 'vb-edges-normal.png'), buf))
|
|
208
|
+
|
|
209
|
+
// ===== LOCKED STATE =====
|
|
210
|
+
console.log('')
|
|
211
|
+
let lockBtn = await page.$('.vue-flow__controls button[title="Lock"]')
|
|
212
|
+
if (lockBtn) {
|
|
213
|
+
await lockBtn.click()
|
|
214
|
+
await page.waitForTimeout(500)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 7. Locked overview
|
|
218
|
+
buf = await page.screenshot({ clip: canvasClip })
|
|
219
|
+
await assert('locked-overview', comparePNG(path.join(baselineDir, 'vb-locked-overview.png'), buf))
|
|
220
|
+
|
|
221
|
+
// 8. Locked node hovered — no resize handles, no settings icon, no handles
|
|
222
|
+
if (firstNode && firstId) {
|
|
223
|
+
await centerOnNode(page, firstId)
|
|
224
|
+
await page.waitForTimeout(300)
|
|
225
|
+
let box = await firstNode.boundingBox()
|
|
226
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
|
227
|
+
await page.waitForTimeout(500)
|
|
228
|
+
box = await firstNode.boundingBox()
|
|
229
|
+
buf = await page.screenshot({ clip: clipAround(box, pad) })
|
|
230
|
+
await assert('locked-node-hovered', comparePNG(path.join(baselineDir, 'vb-locked-node-hovered.png'), buf))
|
|
231
|
+
await page.mouse.move(0, 0)
|
|
232
|
+
await page.waitForTimeout(300)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 9. Locked node selected
|
|
236
|
+
if (firstNode && firstId) {
|
|
237
|
+
await centerOnNode(page, firstId)
|
|
238
|
+
await page.waitForTimeout(300)
|
|
239
|
+
await firstNode.click()
|
|
240
|
+
await page.waitForTimeout(300)
|
|
241
|
+
let box = await firstNode.boundingBox()
|
|
242
|
+
buf = await page.screenshot({ clip: clipAround(box, pad) })
|
|
243
|
+
await assert('locked-node-selected', comparePNG(path.join(baselineDir, 'vb-locked-node-selected.png'), buf))
|
|
244
|
+
await page.mouse.click(10, 10)
|
|
245
|
+
await page.waitForTimeout(300)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 10. Locked edge hovered — no settings icon
|
|
249
|
+
await resetViewport(page)
|
|
250
|
+
await page.waitForTimeout(300)
|
|
251
|
+
if (edges.length > 0) {
|
|
252
|
+
let box = await edges[0].boundingBox()
|
|
253
|
+
if (box) {
|
|
254
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
|
255
|
+
await page.waitForTimeout(500)
|
|
256
|
+
box = await edges[0].boundingBox()
|
|
257
|
+
buf = await page.screenshot({ clip: clipAround(box, 40) })
|
|
258
|
+
await assert('locked-edge-hovered', comparePNG(path.join(baselineDir, 'vb-locked-edge-hovered.png'), buf))
|
|
259
|
+
await page.mouse.move(0, 0)
|
|
260
|
+
await page.waitForTimeout(300)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`)
|
|
265
|
+
if (errors.length > 0) {
|
|
266
|
+
console.log('\nFailed:')
|
|
267
|
+
errors.forEach(e => console.log(' ' + e))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await browser.close()
|
|
271
|
+
process.exit(failed > 0 ? 1 : 0)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
run().catch(e => { console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import rollupVueToHtml from 'w-package-tools/src/rollupVueToHtml.mjs'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let opt = {
|
|
5
|
+
title: `w-flow-vue`,
|
|
6
|
+
head: `
|
|
7
|
+
|
|
8
|
+
<!-- rollupVueToHtml已自動添加@babel/polyfill與vue -->
|
|
9
|
+
|
|
10
|
+
<!-- fontawesome -->
|
|
11
|
+
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.2/css/all.min.css" rel="stylesheet">
|
|
12
|
+
|
|
13
|
+
<!-- mdi, 各組件使用mdi/js故不需引用 -->
|
|
14
|
+
<link _href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
|
|
15
|
+
|
|
16
|
+
<!-- google, 各組件使用mdi/js故不需引用 -->
|
|
17
|
+
<link _href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
|
|
18
|
+
<link _href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet">
|
|
19
|
+
|
|
20
|
+
<!-- lodash -->
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
|
|
22
|
+
|
|
23
|
+
<!-- wsemi -->
|
|
24
|
+
<script src="https://cdn.jsdelivr.net/npm/wsemi/dist/wsemi.umd.min.js"></script>
|
|
25
|
+
|
|
26
|
+
`,
|
|
27
|
+
newVue: ``,
|
|
28
|
+
globals: {
|
|
29
|
+
},
|
|
30
|
+
external: [
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
rollupVueToHtml('./src/App.vue', './docs/examples/app.html', opt)
|
|
34
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import rollupFiles from 'w-package-tools/src/rollupFiles.mjs'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let fdSrc = './src/components/'
|
|
5
|
+
let fdTar = './dist'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
rollupFiles({
|
|
9
|
+
fns: 'WFlowVue.vue',
|
|
10
|
+
fdSrc,
|
|
11
|
+
fdTar,
|
|
12
|
+
format: 'umd',
|
|
13
|
+
//nameDistType: 'kebabCase',
|
|
14
|
+
hookNameDist: () => {
|
|
15
|
+
return 'w-flow-vue'
|
|
16
|
+
},
|
|
17
|
+
globals: {
|
|
18
|
+
},
|
|
19
|
+
external: [
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import _ from 'lodash-es'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import getFiles from 'w-package-tools/src/getFiles.mjs'
|
|
4
|
+
import getPks from 'w-package-tools/src/getPks.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
let fdSrc = './test-html/'
|
|
8
|
+
let fdTar = './docs/examples/'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
//把example裡面cdn更換, 再複製到docs的example內, 作為日後發佈為靜態網站
|
|
13
|
+
|
|
14
|
+
//pks
|
|
15
|
+
let pks = getPks()
|
|
16
|
+
|
|
17
|
+
//cdn
|
|
18
|
+
let cdn = `<script src="https://cdn.jsdelivr.net/npm/w-flow-vue@${pks.version}/dist/w-flow-vue.umd.js"></script>`
|
|
19
|
+
|
|
20
|
+
//mkdirSync
|
|
21
|
+
if (!fs.existsSync(fdTar)) {
|
|
22
|
+
fs.mkdirSync(fdTar)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//getFiles
|
|
26
|
+
let ltfs = getFiles(fdSrc)
|
|
27
|
+
|
|
28
|
+
_.each(ltfs, function(v) {
|
|
29
|
+
|
|
30
|
+
//fn
|
|
31
|
+
let fn = fdSrc + v
|
|
32
|
+
|
|
33
|
+
//c
|
|
34
|
+
let c = fs.readFileSync(fn, 'utf8')
|
|
35
|
+
|
|
36
|
+
//replace
|
|
37
|
+
let r = `<script src="../dist/w-flow-vue.umd.js"></script>`
|
|
38
|
+
c = c.replace(r, cdn)
|
|
39
|
+
|
|
40
|
+
//write
|
|
41
|
+
//console.log(c)
|
|
42
|
+
fs.writeFileSync(fdTar + v, c, 'utf8')
|
|
43
|
+
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
main()
|