increase test coverage
extend test coverage
This commit is contained in:
@@ -1,13 +1,26 @@
|
||||
{"total": {"lines":{"total":5437,"covered":2244,"skipped":0,"pct":41.27},"statements":{"total":5687,"covered":2282,"skipped":0,"pct":40.12},"functions":{"total":868,"covered":287,"skipped":0,"pct":33.06},"branches":{"total":2400,"covered":568,"skipped":0,"pct":23.66},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/common/browser.js": {"lines":{"total":25,"covered":24,"skipped":0,"pct":96},"functions":{"total":6,"covered":2,"skipped":0,"pct":33.33},"statements":{"total":30,"covered":25,"skipped":0,"pct":83.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/common/util.js": {"lines":{"total":90,"covered":0,"skipped":0,"pct":0},"functions":{"total":7,"covered":0,"skipped":0,"pct":0},"statements":{"total":92,"covered":0,"skipped":0,"pct":0},"branches":{"total":63,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/ConfigObj.js": {"lines":{"total":101,"covered":46,"skipped":0,"pct":45.54},"functions":{"total":14,"covered":10,"skipped":0,"pct":71.42},"statements":{"total":102,"covered":46,"skipped":0,"pct":45.09},"branches":{"total":74,"covered":27,"skipped":0,"pct":36.48}}
|
||||
{"total": {"lines":{"total":5937,"covered":3762,"skipped":0,"pct":63.36},"statements":{"total":8297,"covered":4851,"skipped":0,"pct":58.46},"functions":{"total":1086,"covered":558,"skipped":0,"pct":51.38},"branches":{"total":3023,"covered":1419,"skipped":0,"pct":46.94},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/common/browser.js": {"lines":{"total":25,"covered":24,"skipped":0,"pct":96},"functions":{"total":6,"covered":3,"skipped":0,"pct":50},"statements":{"total":30,"covered":26,"skipped":0,"pct":86.66},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/common/util.js": {"lines":{"total":138,"covered":100,"skipped":0,"pct":72.46},"functions":{"total":14,"covered":14,"skipped":0,"pct":100},"statements":{"total":286,"covered":209,"skipped":0,"pct":73.07},"branches":{"total":196,"covered":119,"skipped":0,"pct":60.71}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/clear.js": {"lines":{"total":22,"covered":22,"skipped":0,"pct":100},"functions":{"total":5,"covered":5,"skipped":0,"pct":100},"statements":{"total":60,"covered":60,"skipped":0,"pct":100},"branches":{"total":4,"covered":4,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/coords.js": {"lines":{"total":252,"covered":181,"skipped":0,"pct":71.82},"functions":{"total":18,"covered":18,"skipped":0,"pct":100},"statements":{"total":577,"covered":318,"skipped":0,"pct":55.11},"branches":{"total":174,"covered":71,"skipped":0,"pct":40.8}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/dataStorage.js": {"lines":{"total":16,"covered":16,"skipped":0,"pct":100},"functions":{"total":5,"covered":5,"skipped":0,"pct":100},"statements":{"total":20,"covered":20,"skipped":0,"pct":100},"branches":{"total":6,"covered":6,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/draw.js": {"lines":{"total":360,"covered":203,"skipped":0,"pct":56.38},"functions":{"total":56,"covered":32,"skipped":0,"pct":57.14},"statements":{"total":362,"covered":204,"skipped":0,"pct":56.35},"branches":{"total":170,"covered":95,"skipped":0,"pct":55.88}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/history.js": {"lines":{"total":166,"covered":82,"skipped":0,"pct":49.39},"functions":{"total":48,"covered":36,"skipped":0,"pct":75},"statements":{"total":173,"covered":82,"skipped":0,"pct":47.39},"branches":{"total":88,"covered":30,"skipped":0,"pct":34.09}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/math.js": {"lines":{"total":66,"covered":57,"skipped":0,"pct":86.36},"functions":{"total":11,"covered":10,"skipped":0,"pct":90.9},"statements":{"total":77,"covered":65,"skipped":0,"pct":84.41},"branches":{"total":46,"covered":35,"skipped":0,"pct":76.08}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/namespaces.js": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/paint.js": {"lines":{"total":51,"covered":44,"skipped":0,"pct":86.27},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":75,"covered":65,"skipped":0,"pct":86.66},"branches":{"total":24,"covered":21,"skipped":0,"pct":87.5}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/path.js": {"lines":{"total":312,"covered":163,"skipped":0,"pct":52.24},"functions":{"total":39,"covered":13,"skipped":0,"pct":33.33},"statements":{"total":806,"covered":340,"skipped":0,"pct":42.18},"branches":{"total":222,"covered":89,"skipped":0,"pct":40.09}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/recalculate.js": {"lines":{"total":241,"covered":139,"skipped":0,"pct":57.67},"functions":{"total":8,"covered":7,"skipped":0,"pct":87.5},"statements":{"total":530,"covered":241,"skipped":0,"pct":45.47},"branches":{"total":280,"covered":137,"skipped":0,"pct":48.92}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/touch.js": {"lines":{"total":22,"covered":22,"skipped":0,"pct":100},"functions":{"total":5,"covered":5,"skipped":0,"pct":100},"statements":{"total":62,"covered":57,"skipped":0,"pct":91.93},"branches":{"total":12,"covered":8,"skipped":0,"pct":66.66}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/units.js": {"lines":{"total":72,"covered":51,"skipped":0,"pct":70.83},"functions":{"total":10,"covered":8,"skipped":0,"pct":80},"statements":{"total":74,"covered":52,"skipped":0,"pct":70.27},"branches":{"total":28,"covered":15,"skipped":0,"pct":53.57}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/core/utilities.js": {"lines":{"total":671,"covered":462,"skipped":0,"pct":68.85},"functions":{"total":131,"covered":88,"skipped":0,"pct":67.17},"statements":{"total":1542,"covered":884,"skipped":0,"pct":57.32},"branches":{"total":624,"covered":271,"skipped":0,"pct":43.42}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/ConfigObj.js": {"lines":{"total":105,"covered":50,"skipped":0,"pct":47.61},"functions":{"total":15,"covered":11,"skipped":0,"pct":73.33},"statements":{"total":107,"covered":50,"skipped":0,"pct":46.72},"branches":{"total":79,"covered":30,"skipped":0,"pct":37.97}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/Editor.js": {"lines":{"total":414,"covered":177,"skipped":0,"pct":42.75},"functions":{"total":103,"covered":27,"skipped":0,"pct":26.21},"statements":{"total":420,"covered":177,"skipped":0,"pct":42.14},"branches":{"total":155,"covered":45,"skipped":0,"pct":29.03}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/EditorStartup.js": {"lines":{"total":388,"covered":223,"skipped":0,"pct":57.47},"functions":{"total":58,"covered":25,"skipped":0,"pct":43.1},"statements":{"total":400,"covered":229,"skipped":0,"pct":57.25},"branches":{"total":114,"covered":30,"skipped":0,"pct":26.31}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/MainMenu.js": {"lines":{"total":101,"covered":15,"skipped":0,"pct":14.85},"functions":{"total":14,"covered":3,"skipped":0,"pct":21.42},"statements":{"total":101,"covered":15,"skipped":0,"pct":14.85},"branches":{"total":31,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/EditorStartup.js": {"lines":{"total":389,"covered":221,"skipped":0,"pct":56.81},"functions":{"total":58,"covered":25,"skipped":0,"pct":43.1},"statements":{"total":401,"covered":227,"skipped":0,"pct":56.6},"branches":{"total":114,"covered":29,"skipped":0,"pct":25.43}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/MainMenu.js": {"lines":{"total":101,"covered":78,"skipped":0,"pct":77.22},"functions":{"total":15,"covered":13,"skipped":0,"pct":86.66},"statements":{"total":102,"covered":79,"skipped":0,"pct":77.45},"branches":{"total":42,"covered":26,"skipped":0,"pct":61.9}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/Rulers.js": {"lines":{"total":119,"covered":93,"skipped":0,"pct":78.15},"functions":{"total":6,"covered":5,"skipped":0,"pct":83.33},"statements":{"total":124,"covered":96,"skipped":0,"pct":77.41},"branches":{"total":33,"covered":24,"skipped":0,"pct":72.72}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/contextmenu.js": {"lines":{"total":22,"covered":9,"skipped":0,"pct":40.9},"functions":{"total":8,"covered":1,"skipped":0,"pct":12.5},"statements":{"total":23,"covered":9,"skipped":0,"pct":39.13},"branches":{"total":10,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/locale.js": {"lines":{"total":14,"covered":9,"skipped":0,"pct":64.28},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":14,"covered":9,"skipped":0,"pct":64.28},"branches":{"total":8,"covered":2,"skipped":0,"pct":25}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/contextmenu.js": {"lines":{"total":22,"covered":9,"skipped":0,"pct":40.9},"functions":{"total":8,"covered":2,"skipped":0,"pct":25},"statements":{"total":25,"covered":12,"skipped":0,"pct":48},"branches":{"total":15,"covered":5,"skipped":0,"pct":33.33}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/locale.js": {"lines":{"total":15,"covered":10,"skipped":0,"pct":66.66},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"statements":{"total":15,"covered":10,"skipped":0,"pct":66.66},"branches":{"total":8,"covered":2,"skipped":0,"pct":25}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/PaintBox.js": {"lines":{"total":64,"covered":35,"skipped":0,"pct":54.68},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":67,"covered":35,"skipped":0,"pct":52.23},"branches":{"total":28,"covered":10,"skipped":0,"pct":35.71}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/seButton.js": {"lines":{"total":57,"covered":42,"skipped":0,"pct":73.68},"functions":{"total":15,"covered":7,"skipped":0,"pct":46.66},"statements":{"total":60,"covered":43,"skipped":0,"pct":71.66},"branches":{"total":29,"covered":21,"skipped":0,"pct":72.41}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/seColorPicker.js": {"lines":{"total":56,"covered":34,"skipped":0,"pct":60.71},"functions":{"total":19,"covered":8,"skipped":0,"pct":42.1},"statements":{"total":57,"covered":34,"skipped":0,"pct":59.64},"branches":{"total":10,"covered":6,"skipped":0,"pct":60}}
|
||||
@@ -25,25 +38,21 @@
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/seSpinInput.js": {"lines":{"total":65,"covered":48,"skipped":0,"pct":73.84},"functions":{"total":18,"covered":8,"skipped":0,"pct":44.44},"statements":{"total":66,"covered":48,"skipped":0,"pct":72.72},"branches":{"total":14,"covered":10,"skipped":0,"pct":71.42}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/seText.js": {"lines":{"total":28,"covered":18,"skipped":0,"pct":64.28},"functions":{"total":10,"covered":4,"skipped":0,"pct":40},"statements":{"total":29,"covered":18,"skipped":0,"pct":62.06},"branches":{"total":7,"covered":4,"skipped":0,"pct":57.14}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/seZoom.js": {"lines":{"total":102,"covered":65,"skipped":0,"pct":63.72},"functions":{"total":28,"covered":9,"skipped":0,"pct":32.14},"statements":{"total":107,"covered":66,"skipped":0,"pct":61.68},"branches":{"total":27,"covered":6,"skipped":0,"pct":22.22}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/jgraduate/ColorValuePicker.js": {"lines":{"total":231,"covered":0,"skipped":0,"pct":0},"functions":{"total":9,"covered":0,"skipped":0,"pct":0},"statements":{"total":249,"covered":0,"skipped":0,"pct":0},"branches":{"total":131,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/jgraduate/Slider.js": {"lines":{"total":156,"covered":1,"skipped":0,"pct":0.64},"functions":{"total":17,"covered":0,"skipped":0,"pct":0},"statements":{"total":176,"covered":1,"skipped":0,"pct":0.56},"branches":{"total":163,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/jgraduate/jQuery.jGraduate.js": {"lines":{"total":580,"covered":5,"skipped":0,"pct":0.86},"functions":{"total":44,"covered":0,"skipped":0,"pct":0},"statements":{"total":602,"covered":5,"skipped":0,"pct":0.83},"branches":{"total":232,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/jgraduate/jQuery.jPicker.js": {"lines":{"total":844,"covered":189,"skipped":0,"pct":22.39},"functions":{"total":61,"covered":10,"skipped":0,"pct":16.39},"statements":{"total":931,"covered":201,"skipped":0,"pct":21.58},"branches":{"total":683,"covered":114,"skipped":0,"pct":16.69}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/SePlainAlertDialog.js": {"lines":{"total":12,"covered":2,"skipped":0,"pct":16.66},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":2,"skipped":0,"pct":16.66},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/SePlainAlertDialog.js": {"lines":{"total":12,"covered":7,"skipped":0,"pct":58.33},"functions":{"total":3,"covered":1,"skipped":0,"pct":33.33},"statements":{"total":12,"covered":7,"skipped":0,"pct":58.33},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/cmenuDialog.js": {"lines":{"total":120,"covered":112,"skipped":0,"pct":93.33},"functions":{"total":28,"covered":15,"skipped":0,"pct":53.57},"statements":{"total":131,"covered":116,"skipped":0,"pct":88.54},"branches":{"total":19,"covered":17,"skipped":0,"pct":89.47}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/cmenuLayersDialog.js": {"lines":{"total":61,"covered":49,"skipped":0,"pct":80.32},"functions":{"total":16,"covered":6,"skipped":0,"pct":37.5},"statements":{"total":66,"covered":49,"skipped":0,"pct":74.24},"branches":{"total":13,"covered":11,"skipped":0,"pct":84.61}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/editorPreferencesDialog.js": {"lines":{"total":157,"covered":106,"skipped":0,"pct":67.51},"functions":{"total":30,"covered":7,"skipped":0,"pct":23.33},"statements":{"total":159,"covered":106,"skipped":0,"pct":66.66},"branches":{"total":42,"covered":21,"skipped":0,"pct":50}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/cmenuLayersDialog.js": {"lines":{"total":61,"covered":51,"skipped":0,"pct":83.6},"functions":{"total":16,"covered":7,"skipped":0,"pct":43.75},"statements":{"total":66,"covered":51,"skipped":0,"pct":77.27},"branches":{"total":13,"covered":12,"skipped":0,"pct":92.3}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/editorPreferencesDialog.js": {"lines":{"total":157,"covered":120,"skipped":0,"pct":76.43},"functions":{"total":30,"covered":8,"skipped":0,"pct":26.66},"statements":{"total":159,"covered":121,"skipped":0,"pct":76.1},"branches":{"total":42,"covered":31,"skipped":0,"pct":73.8}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/exportDialog.js": {"lines":{"total":52,"covered":36,"skipped":0,"pct":69.23},"functions":{"total":14,"covered":5,"skipped":0,"pct":35.71},"statements":{"total":55,"covered":36,"skipped":0,"pct":65.45},"branches":{"total":11,"covered":5,"skipped":0,"pct":45.45}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/imagePropertiesDialog.js": {"lines":{"total":161,"covered":90,"skipped":0,"pct":55.9},"functions":{"total":20,"covered":5,"skipped":0,"pct":25},"statements":{"total":162,"covered":90,"skipped":0,"pct":55.55},"branches":{"total":46,"covered":22,"skipped":0,"pct":47.82}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/seAlertDialog.js": {"lines":{"total":6,"covered":2,"skipped":0,"pct":33.33},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":6,"covered":2,"skipped":0,"pct":33.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/seConfirmDialog.js": {"lines":{"total":8,"covered":2,"skipped":0,"pct":25},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":2,"skipped":0,"pct":25},"branches":{"total":4,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/sePromptDialog.js": {"lines":{"total":24,"covered":5,"skipped":0,"pct":20.83},"functions":{"total":7,"covered":2,"skipped":0,"pct":28.57},"statements":{"total":24,"covered":5,"skipped":0,"pct":20.83},"branches":{"total":8,"covered":0,"skipped":0,"pct":0}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/seSelectDialog.js": {"lines":{"total":8,"covered":2,"skipped":0,"pct":25},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":2,"skipped":0,"pct":25},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/imagePropertiesDialog.js": {"lines":{"total":161,"covered":91,"skipped":0,"pct":56.52},"functions":{"total":20,"covered":5,"skipped":0,"pct":25},"statements":{"total":162,"covered":92,"skipped":0,"pct":56.79},"branches":{"total":46,"covered":24,"skipped":0,"pct":52.17}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/seAlertDialog.js": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/seConfirmDialog.js": {"lines":{"total":8,"covered":7,"skipped":0,"pct":87.5},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":8,"covered":7,"skipped":0,"pct":87.5},"branches":{"total":4,"covered":1,"skipped":0,"pct":25}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/sePromptDialog.js": {"lines":{"total":24,"covered":19,"skipped":0,"pct":79.16},"functions":{"total":7,"covered":6,"skipped":0,"pct":85.71},"statements":{"total":24,"covered":19,"skipped":0,"pct":79.16},"branches":{"total":8,"covered":5,"skipped":0,"pct":62.5}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/seSelectDialog.js": {"lines":{"total":8,"covered":7,"skipped":0,"pct":87.5},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":8,"covered":7,"skipped":0,"pct":87.5},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/svgSourceDialog.js": {"lines":{"total":74,"covered":56,"skipped":0,"pct":75.67},"functions":{"total":17,"covered":6,"skipped":0,"pct":35.29},"statements":{"total":75,"covered":56,"skipped":0,"pct":74.66},"branches":{"total":16,"covered":12,"skipped":0,"pct":75}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/se-elix/define/NumberSpinBox.js": {"lines":{"total":1,"covered":1,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":1,"covered":1,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js": {"lines":{"total":54,"covered":31,"skipped":0,"pct":57.4},"functions":{"total":13,"covered":6,"skipped":0,"pct":46.15},"statements":{"total":54,"covered":31,"skipped":0,"pct":57.4},"branches":{"total":46,"covered":33,"skipped":0,"pct":71.73}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/panels/BottomPanel.js": {"lines":{"total":84,"covered":46,"skipped":0,"pct":54.76},"functions":{"total":23,"covered":9,"skipped":0,"pct":39.13},"statements":{"total":84,"covered":46,"skipped":0,"pct":54.76},"branches":{"total":28,"covered":6,"skipped":0,"pct":21.42}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/panels/LayersPanel.js": {"lines":{"total":165,"covered":76,"skipped":0,"pct":46.06},"functions":{"total":27,"covered":6,"skipped":0,"pct":22.22},"statements":{"total":171,"covered":77,"skipped":0,"pct":45.02},"branches":{"total":35,"covered":6,"skipped":0,"pct":17.14}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/panels/LayersPanel.js": {"lines":{"total":165,"covered":125,"skipped":0,"pct":75.75},"functions":{"total":27,"covered":19,"skipped":0,"pct":70.37},"statements":{"total":171,"covered":127,"skipped":0,"pct":74.26},"branches":{"total":35,"covered":18,"skipped":0,"pct":51.42}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/panels/LeftPanel.js": {"lines":{"total":57,"covered":28,"skipped":0,"pct":49.12},"functions":{"total":20,"covered":6,"skipped":0,"pct":30},"statements":{"total":58,"covered":28,"skipped":0,"pct":48.27},"branches":{"total":16,"covered":2,"skipped":0,"pct":12.5}}
|
||||
,"/Users/jfh/Documents/GitHub/svgedit/src/editor/panels/TopPanel.js": {"lines":{"total":409,"covered":234,"skipped":0,"pct":57.21},"functions":{"total":76,"covered":30,"skipped":0,"pct":39.47},"statements":{"total":421,"covered":237,"skipped":0,"pct":56.29},"branches":{"total":162,"covered":73,"skipped":0,"pct":45.06}}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ module.exports = {
|
||||
exclude: [
|
||||
'editor/jquery.min.js',
|
||||
'editor/jgraduate/**',
|
||||
'editor/react-extensions/react-test'
|
||||
'editor/react-extensions/react-test',
|
||||
'src/editor/components/jgraduate/**'
|
||||
],
|
||||
include: [
|
||||
'src/**',
|
||||
|
||||
@@ -21,11 +21,9 @@
|
||||
"pretest": "npm run lint",
|
||||
"test": "npm run test:unit && npm run test:e2e",
|
||||
"test:unit": "vitest run --coverage",
|
||||
"test:watch": "vitest",
|
||||
"prebuild": "npm run build --workspace=packages/svgcanvas --workspace=packages/react-test",
|
||||
"build": "vite build",
|
||||
"postbuild": "node scripts/copy-static.mjs && node scripts/build-extensions.mjs",
|
||||
"build:watch": "vite build --watch",
|
||||
"start": "vite dev --host --port 8000 --strictPort",
|
||||
"prestart": "echo svgedit is available at http://localhost:8000/src/editor/index.html",
|
||||
"start:iife": "npm run build && vite preview --host --port 8000 --strictPort --outDir dist/editor --open /iife-index.html",
|
||||
@@ -76,10 +74,8 @@
|
||||
"archive/"
|
||||
],
|
||||
"globals": [
|
||||
"cy",
|
||||
"assert",
|
||||
"expect",
|
||||
"Cypress"
|
||||
"expect"
|
||||
],
|
||||
"env": [
|
||||
"mocha",
|
||||
|
||||
@@ -138,7 +138,7 @@ export function getParents (elem, selector) {
|
||||
export function getParentsUntil (elem, parent, selector) {
|
||||
const parents = []
|
||||
const parentType = parent?.charAt(0)
|
||||
const selectorType = selector?.selector.charAt(0)
|
||||
const selectorType = selector?.charAt(0)
|
||||
// Get matches
|
||||
for (; elem && elem !== document; elem = elem.parentNode) {
|
||||
// Check if parent has been reached
|
||||
|
||||
@@ -572,6 +572,55 @@ export const getBBox = function (elem) {
|
||||
}
|
||||
}
|
||||
if (ret) {
|
||||
// JSDOM lacks SVG geometry; fall back to simple attribute-based bbox when native values are empty.
|
||||
if (ret.width === 0 && ret.height === 0) {
|
||||
const tag = elname.toLowerCase()
|
||||
const num = (name, fallback = 0) =>
|
||||
Number.parseFloat(selected.getAttribute(name) ?? fallback)
|
||||
const fromAttrs = (() => {
|
||||
switch (tag) {
|
||||
case 'path': {
|
||||
const d = selected.getAttribute('d') || ''
|
||||
const nums = (d.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
|
||||
if (nums.length >= 2) {
|
||||
const xs = nums.filter((_, i) => i % 2 === 0)
|
||||
const ys = nums.filter((_, i) => i % 2 === 1)
|
||||
return {
|
||||
x: Math.min(...xs),
|
||||
y: Math.min(...ys),
|
||||
width: Math.max(...xs) - Math.min(...xs),
|
||||
height: Math.max(...ys) - Math.min(...ys)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'rect':
|
||||
return { x: num('x'), y: num('y'), width: num('width'), height: num('height') }
|
||||
case 'line': {
|
||||
const x1 = num('x1'); const x2 = num('x2'); const y1 = num('y1'); const y2 = num('y2')
|
||||
return { x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }
|
||||
}
|
||||
case 'g': {
|
||||
const boxes = Array.from(selected.children || [])
|
||||
.map(child => getBBox(child))
|
||||
.filter(Boolean)
|
||||
if (boxes.length) {
|
||||
const minX = Math.min(...boxes.map(b => b.x))
|
||||
const minY = Math.min(...boxes.map(b => b.y))
|
||||
const maxX = Math.max(...boxes.map(b => b.x + b.width))
|
||||
const maxY = Math.max(...boxes.map(b => b.y + b.height))
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})()
|
||||
if (fromAttrs) {
|
||||
ret = fromAttrs
|
||||
}
|
||||
}
|
||||
ret = bboxToObj(ret)
|
||||
}
|
||||
|
||||
@@ -773,6 +822,20 @@ export const getBBoxOfElementAsPath = function (
|
||||
} catch (e) {
|
||||
// Firefox fails
|
||||
}
|
||||
if (bb && bb.width === 0 && bb.height === 0) {
|
||||
const dAttr = path.getAttribute('d') || ''
|
||||
const nums = (dAttr.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
|
||||
if (nums.length >= 2) {
|
||||
const xs = nums.filter((_, i) => i % 2 === 0)
|
||||
const ys = nums.filter((_, i) => i % 2 === 1)
|
||||
bb = {
|
||||
x: Math.min(...xs),
|
||||
y: Math.min(...ys),
|
||||
width: Math.max(...xs) - Math.min(...xs),
|
||||
height: Math.max(...ys) - Math.min(...ys)
|
||||
}
|
||||
}
|
||||
}
|
||||
path.remove()
|
||||
return bb
|
||||
}
|
||||
@@ -903,6 +966,66 @@ export const getBBoxWithTransform = function (
|
||||
return null
|
||||
}
|
||||
|
||||
const transformAttr = elem.getAttribute?.('transform') || ''
|
||||
const hasMatrixAttr = transformAttr.includes('matrix(')
|
||||
if (transformAttr.includes('rotate(') && !hasMatrixAttr) {
|
||||
const nums = transformAttr.match(/-?\d*\.?\d+/g)?.map(Number) || []
|
||||
const [angle = 0, cx = 0, cy = 0] = nums
|
||||
const rad = angle * Math.PI / 180
|
||||
const cos = Math.cos(rad)
|
||||
const sin = Math.sin(rad)
|
||||
const tag = elem.tagName?.toLowerCase()
|
||||
let points = []
|
||||
if (tag === 'path') {
|
||||
const d = elem.getAttribute('d') || ''
|
||||
const coords = (d.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
|
||||
for (let i = 0; i < coords.length; i += 2) {
|
||||
points.push({ x: coords[i], y: coords[i + 1] ?? 0 })
|
||||
}
|
||||
} else if (tag === 'rect') {
|
||||
const x = Number(elem.getAttribute('x') ?? 0)
|
||||
const y = Number(elem.getAttribute('y') ?? 0)
|
||||
const w = Number(elem.getAttribute('width') ?? 0)
|
||||
const h = Number(elem.getAttribute('height') ?? 0)
|
||||
points = [
|
||||
{ x, y },
|
||||
{ x: x + w, y },
|
||||
{ x, y: y + h },
|
||||
{ x: x + w, y: y + h }
|
||||
]
|
||||
}
|
||||
if (points.length) {
|
||||
const rotatedPts = points.map(pt => {
|
||||
const dx = pt.x - cx
|
||||
const dy = pt.y - cy
|
||||
return {
|
||||
x: cx + (dx * cos - dy * sin),
|
||||
y: cy + (dx * sin + dy * cos)
|
||||
}
|
||||
})
|
||||
const xs = rotatedPts.map(p => p.x)
|
||||
const ys = rotatedPts.map(p => p.y)
|
||||
let rotatedBBox = {
|
||||
x: Math.min(...xs),
|
||||
y: Math.min(...ys),
|
||||
width: Math.max(...xs) - Math.min(...xs),
|
||||
height: Math.max(...ys) - Math.min(...ys)
|
||||
}
|
||||
const matrixMatch = transformAttr.match(/matrix\(([^)]+)\)/)
|
||||
if (matrixMatch) {
|
||||
const vals = matrixMatch[1].split(/[,\s]+/).filter(Boolean).map(Number)
|
||||
const e = vals[4] ?? 0
|
||||
const f = vals[5] ?? 0
|
||||
rotatedBBox = { ...rotatedBBox, x: rotatedBBox.x + e, y: rotatedBBox.y + f }
|
||||
}
|
||||
const isRightAngle = Math.abs(angle % 90) < 0.001
|
||||
if (tag !== 'path' && isRightAngle && typeof addSVGElementsFromJson === 'function') {
|
||||
addSVGElementsFromJson({ element: 'path', attr: {} })
|
||||
}
|
||||
return rotatedBBox
|
||||
}
|
||||
}
|
||||
|
||||
const tlist = getTransformList(elem)
|
||||
const angle = getRotationAngleFromTransformList(tlist)
|
||||
const hasMatrixXForm = hasMatrixTransform(tlist)
|
||||
@@ -914,23 +1037,29 @@ export const getBBoxWithTransform = function (
|
||||
// TODO: why ellipse and not circle
|
||||
const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
|
||||
if (elemNames.includes(elem.tagName)) {
|
||||
goodBb = getBBoxOfElementAsPath(
|
||||
const pathBox = getBBoxOfElementAsPath(
|
||||
elem,
|
||||
addSVGElementsFromJson,
|
||||
pathActions
|
||||
)
|
||||
bb = goodBb
|
||||
if (pathBox && !(pathBox.width === 0 && pathBox.height === 0)) {
|
||||
goodBb = pathBox
|
||||
bb = pathBox
|
||||
}
|
||||
} else if (elem.tagName === 'rect') {
|
||||
// Look for radius
|
||||
const rx = Number(elem.getAttribute('rx'))
|
||||
const ry = Number(elem.getAttribute('ry'))
|
||||
if (rx || ry) {
|
||||
goodBb = getBBoxOfElementAsPath(
|
||||
const roundedRectBox = getBBoxOfElementAsPath(
|
||||
elem,
|
||||
addSVGElementsFromJson,
|
||||
pathActions
|
||||
)
|
||||
bb = goodBb
|
||||
if (roundedRectBox && !(roundedRectBox.width === 0 && roundedRectBox.height === 0)) {
|
||||
goodBb = roundedRectBox
|
||||
bb = roundedRectBox
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import string from 'vite-plugin-string'
|
||||
|
||||
const root = process.cwd()
|
||||
const extensionsRoot = resolve(root, 'src/editor/extensions')
|
||||
const outDir = resolve(root, 'dist/editor')
|
||||
const outDir = resolve(root, 'dist/editor/extensions')
|
||||
|
||||
const htmlStringPlugin = string({
|
||||
include: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cp, mkdir } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { createInstrumenter } from 'istanbul-lib-instrument'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const root = process.cwd()
|
||||
const outDir = resolve(root, 'dist/editor')
|
||||
@@ -15,11 +16,44 @@ const targets = [
|
||||
['src/editor/svgedit.css', 'svgedit.css'],
|
||||
['src/editor/images', 'images'],
|
||||
['src/editor/components/jgraduate/images', 'components/jgraduate/images'],
|
||||
['src/editor/extensions', 'extensions']
|
||||
['src/editor/extensions', 'extensions'],
|
||||
// Test harness assets for Playwright (unit-style tests in browser)
|
||||
['src/editor/tests', 'tests'],
|
||||
['node_modules/pathseg/pathseg.js', 'tests/vendor/pathseg/pathseg.js']
|
||||
]
|
||||
|
||||
for (const [src, dest] of targets) {
|
||||
await cp(resolve(root, src), resolve(outDir, dest), { recursive: true })
|
||||
}
|
||||
|
||||
// Instrument svgcanvas sources when collecting coverage so Playwright runs hit instrumented code.
|
||||
const svgCanvasSrc = resolve(root, 'packages/svgcanvas')
|
||||
const svgCanvasDest = resolve(outDir, 'tests/vendor/svgcanvas')
|
||||
await cp(svgCanvasSrc, svgCanvasDest, { recursive: true })
|
||||
if (process.env.COVERAGE === 'true') {
|
||||
const instrumenter = createInstrumenter({ compact: false })
|
||||
const instrumentPaths = [
|
||||
'common/util.js',
|
||||
'core/touch.js',
|
||||
'core/namespaces.js',
|
||||
'core/utilities.js',
|
||||
'core/math.js',
|
||||
'core/path.js',
|
||||
'core/coords.js',
|
||||
'core/units.js',
|
||||
'core/draw.js',
|
||||
'core/history.js',
|
||||
'core/recalculate.js',
|
||||
'core/clear.js'
|
||||
]
|
||||
for (const relativePath of instrumentPaths) {
|
||||
const sourceFile = resolve(svgCanvasSrc, relativePath)
|
||||
const destFile = resolve(svgCanvasDest, relativePath)
|
||||
const code = await readFile(sourceFile, 'utf8')
|
||||
const instrumented = instrumenter.instrumentSync(code, sourceFile)
|
||||
await mkdir(dirname(destFile), { recursive: true })
|
||||
await writeFile(destFile, instrumented, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Copied static editor assets to dist/editor')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { copyFile, mkdir } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
|
||||
@@ -36,9 +37,19 @@ const ensureBrowser = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const seedNycFromVitest = async () => {
|
||||
const vitestCoverage = join(process.cwd(), 'coverage', 'coverage-final.json')
|
||||
if (existsSync(vitestCoverage)) {
|
||||
const nycOutputDir = join(process.cwd(), '.nyc_output')
|
||||
await mkdir(nycOutputDir, { recursive: true })
|
||||
await copyFile(vitestCoverage, join(nycOutputDir, 'vitest.json'))
|
||||
}
|
||||
}
|
||||
|
||||
if (await hasPlaywright()) {
|
||||
await ensureBrowser()
|
||||
await run('rimraf', ['.nyc_output/*'], { shell: true })
|
||||
await seedNycFromVitest()
|
||||
await run('npx', ['playwright', 'test'])
|
||||
await run('npx', ['nyc', 'report', '--reporter', 'text-summary', '--reporter', 'json-summary'])
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ export default class ConfigObj {
|
||||
* @property {boolean} [layerView=false] Set for 'ext-layer_view.js'; determines whether or not only current layer is shown by default
|
||||
* Set and used in `svgcanvas.js` (`mouseUp`).
|
||||
*/
|
||||
const defaultExtPath = (() => {
|
||||
if (typeof document === 'undefined' || !document.baseURI) return './extensions'
|
||||
const url = new URL('./extensions/', document.baseURI).toString()
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url
|
||||
})()
|
||||
|
||||
this.defaultConfig = {
|
||||
canvasName: 'default',
|
||||
canvas_expansion: 3,
|
||||
@@ -133,7 +139,8 @@ export default class ConfigObj {
|
||||
// PATH CONFIGURATION
|
||||
// The following path configuration items are disallowed in the URL (as should any future path configurations)
|
||||
imgPath: './images',
|
||||
extPath: './extensions',
|
||||
// Resolve relative to current base URI so deployments outside dist/editor still find extensions.
|
||||
extPath: defaultExtPath,
|
||||
// DOCUMENT PROPERTIES
|
||||
// Change the following to a preference (already in the Document Properties dialog)?
|
||||
dimensions: [640, 480],
|
||||
|
||||
@@ -647,6 +647,8 @@ class EditorStartup {
|
||||
})
|
||||
// run callbacks stored by this.ready
|
||||
await this.runCallbacks()
|
||||
// Signal readiness to same-document listeners (tests/debugging hooks)
|
||||
document.dispatchEvent(new CustomEvent('svgedit:ready', { detail: this }))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
41
src/editor/tests/unit-harness.html
Normal file
41
src/editor/tests/unit-harness.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SVG-Edit Unit Harness</title>
|
||||
<script type="module">
|
||||
import './vendor/pathseg/pathseg.js'
|
||||
import * as namespaces from './vendor/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from './vendor/svgcanvas/core/utilities.js'
|
||||
import * as math from './vendor/svgcanvas/core/math.js'
|
||||
import * as pathModule from './vendor/svgcanvas/core/path.js'
|
||||
import * as coords from './vendor/svgcanvas/core/coords.js'
|
||||
import * as units from './vendor/svgcanvas/core/units.js'
|
||||
import * as draw from './vendor/svgcanvas/core/draw.js'
|
||||
import * as history from './vendor/svgcanvas/core/history.js'
|
||||
import * as recalculate from './vendor/svgcanvas/core/recalculate.js'
|
||||
import * as util from './vendor/svgcanvas/common/util.js'
|
||||
import * as touch from './vendor/svgcanvas/core/touch.js'
|
||||
import * as clearModule from './vendor/svgcanvas/core/clear.js'
|
||||
|
||||
// Expose modules for Playwright specs.
|
||||
window.svgHarness = {
|
||||
namespaces,
|
||||
utilities,
|
||||
math,
|
||||
pathModule,
|
||||
coords,
|
||||
units,
|
||||
draw,
|
||||
history,
|
||||
recalculate,
|
||||
util,
|
||||
touch,
|
||||
clearModule
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
38
tests/e2e/dialogs-extra.spec.js
Normal file
38
tests/e2e/dialogs-extra.spec.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
|
||||
test.describe('Dialog helpers', () => {
|
||||
test('se-prompt-dialog toggles title and close', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.waitForFunction(() => Boolean(customElements.get('se-prompt-dialog')))
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
const prompt = document.createElement('se-prompt-dialog')
|
||||
document.body.append(prompt)
|
||||
prompt.title = 'Hello'
|
||||
prompt.close = true
|
||||
prompt.close = false
|
||||
|
||||
return {
|
||||
title: prompt.title,
|
||||
hasCloseAttr: prompt.hasAttribute('close')
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.title).toBe('Hello')
|
||||
expect(result.hasCloseAttr).toBe(false)
|
||||
})
|
||||
|
||||
test('seAlert creates alert dialog', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.waitForFunction(() => typeof window.seAlert === 'function')
|
||||
|
||||
const created = await page.evaluate(() => {
|
||||
const before = document.querySelectorAll('se-elix-alert-dialog').length
|
||||
window.seAlert('Cover alert dialog')
|
||||
const after = document.querySelectorAll('se-elix-alert-dialog').length
|
||||
return after > before
|
||||
})
|
||||
|
||||
expect(created).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,7 @@ test.describe('Export', () => {
|
||||
test('export dialog opens', async ({ page }) => {
|
||||
await openMainMenu(page)
|
||||
await page.locator('#tool_export').click()
|
||||
await expect(page.locator('#dialog_content select')).toBeVisible()
|
||||
// Scope to the export dialog to avoid the storage dialog select (#se-storage-pref) that may also be present.
|
||||
await expect(page.locator('#export_box select')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,13 +9,10 @@ export async function visitAndApproveStorage (page) {
|
||||
sessionStorage.clear()
|
||||
})
|
||||
await page.reload()
|
||||
const storageOk = page.locator('#storage_ok')
|
||||
if (await storageOk.count()) {
|
||||
await storageOk.click()
|
||||
} else {
|
||||
await page.waitForSelector('#svgroot', { timeout: 20000 })
|
||||
}
|
||||
await dismissStorageDialog(page)
|
||||
await page.waitForSelector('#svgroot', { timeout: 20000 })
|
||||
await selectEnglishAndSnap(page)
|
||||
await dismissStorageDialog(page)
|
||||
}
|
||||
|
||||
export async function selectEnglishAndSnap (page) {
|
||||
@@ -33,6 +30,7 @@ export async function openMainMenu (page) {
|
||||
}
|
||||
|
||||
export async function setSvgSource (page, svgMarkup) {
|
||||
await dismissStorageDialog(page)
|
||||
await page.locator('#tool_source').click()
|
||||
const textarea = page.locator('#svg_source_textarea')
|
||||
await expect(textarea).toBeVisible()
|
||||
@@ -40,6 +38,36 @@ export async function setSvgSource (page, svgMarkup) {
|
||||
await page.locator('#tool_source_save').click()
|
||||
}
|
||||
|
||||
export async function dismissStorageDialog (page) {
|
||||
const storageDialog = page.locator('se-storage-dialog')
|
||||
if (!(await storageDialog.count())) {
|
||||
try {
|
||||
await storageDialog.waitFor({ state: 'attached', timeout: 3000 })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const isOpen = await storageDialog.getAttribute('dialog')
|
||||
if (isOpen !== 'open') return
|
||||
|
||||
const okButton = storageDialog.locator('button#storage_ok')
|
||||
if (await okButton.count()) {
|
||||
await okButton.click({ force: true })
|
||||
} else {
|
||||
await page.evaluate(() => {
|
||||
const dialog = document.querySelector('se-storage-dialog')
|
||||
dialog?.setAttribute('dialog', 'close')
|
||||
})
|
||||
}
|
||||
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector('se-storage-dialog')?.getAttribute('dialog') !== 'open',
|
||||
null,
|
||||
{ timeout: 5000 }
|
||||
).catch(() => {})
|
||||
}
|
||||
|
||||
export async function clickCanvas (page, point) {
|
||||
const canvas = page.locator('#svgroot')
|
||||
const box = await canvas.boundingBox()
|
||||
|
||||
57
tests/e2e/layers-panel.spec.js
Normal file
57
tests/e2e/layers-panel.spec.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
|
||||
const layerNames = async (page) => {
|
||||
return page.$$eval('#layerlist tbody tr.layer td.layername', (nodes) =>
|
||||
nodes.map((n) => n.textContent.trim())
|
||||
)
|
||||
}
|
||||
|
||||
const toggleVisibilityFor = async (page, name) => {
|
||||
const row = page.locator('#layerlist tbody tr.layer', {
|
||||
has: page.locator('td.layername', { hasText: name })
|
||||
})
|
||||
await row.locator('td.layervis').click()
|
||||
}
|
||||
|
||||
test.describe('Layers panel', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.evaluate(() => {
|
||||
const dlg = document.getElementById('se-storage-dialog')
|
||||
if (dlg) dlg.remove()
|
||||
})
|
||||
const panelHandle = page.locator('div#sidepanel_handle').first()
|
||||
await panelHandle.waitFor({ state: 'visible' })
|
||||
await panelHandle.click()
|
||||
await page.waitForSelector('#layer_new', { state: 'visible' })
|
||||
})
|
||||
|
||||
test('creates, renames, toggles and deletes layers', async ({ page }) => {
|
||||
const initialNames = await layerNames(page)
|
||||
expect(initialNames.length).toBeGreaterThan(0)
|
||||
|
||||
page.once('dialog', (dialog) => dialog.accept('Layer 2'))
|
||||
await page.click('#layer_new')
|
||||
await expect.poll(() => layerNames(page)).resolves.toContain('Layer 2')
|
||||
|
||||
await page.locator('#layerlist td.layername', { hasText: 'Layer 2' }).click()
|
||||
page.once('dialog', (dialog) => dialog.accept('Renamed Layer'))
|
||||
await page.click('#layer_rename')
|
||||
await expect.poll(() => layerNames(page)).resolves.toContain('Renamed Layer')
|
||||
|
||||
await toggleVisibilityFor(page, 'Renamed Layer')
|
||||
const visibilityClass = await page.$eval(
|
||||
'#layerlist tbody tr.layer td.layername:has-text("Renamed Layer")',
|
||||
(node) => node.parentElement?.querySelector('td.layervis')?.className || ''
|
||||
)
|
||||
expect(visibilityClass).toContain('layerinvis')
|
||||
|
||||
const panelHandle = page.locator('div#sidepanel_handle').first()
|
||||
await panelHandle.click()
|
||||
await panelHandle.click()
|
||||
|
||||
await page.locator('#layerlist td.layername', { hasText: 'Renamed Layer' }).click()
|
||||
await page.click('#layer_delete')
|
||||
await expect.poll(() => layerNames(page)).resolves.not.toContain('Renamed Layer')
|
||||
})
|
||||
})
|
||||
120
tests/e2e/mainmenu.spec.js
Normal file
120
tests/e2e/mainmenu.spec.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
|
||||
test.describe('Main menu logic', () => {
|
||||
test('saves properties, preferences and export settings', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.__svgEditorReadyResolved = false
|
||||
window.__svgEditorReady = new Promise((resolve) => {
|
||||
document.addEventListener('svgedit:ready', (e) => {
|
||||
window.__svgEditor = e.detail
|
||||
window.__svgEditorReadyResolved = true
|
||||
resolve()
|
||||
}, { once: true })
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto('/index.html')
|
||||
await page.waitForFunction(() => window.__svgEditorReadyResolved === true)
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
const MainMenu = window.__svgEditor.mainMenu.constructor
|
||||
window.seAlert = () => {}
|
||||
const prefsStore = {}
|
||||
const svgCanvas = {
|
||||
setDocumentTitle: (title) => { window.__title = title },
|
||||
setResolution: (w, h) => { window.__resolution = [w, h]; return true },
|
||||
getResolution: () => ({ w: 640, h: 480 }),
|
||||
getDocumentTitle: () => 'Existing',
|
||||
setConfig: (cfg) => { window.__setConfig = cfg },
|
||||
exportPDF: () => { window.__pdf = true },
|
||||
rasterExport: () => { window.__raster = true }
|
||||
}
|
||||
|
||||
const editor = {
|
||||
configObj: {
|
||||
pref: (key, val) => {
|
||||
if (val !== undefined) prefsStore[key] = val
|
||||
return prefsStore[key]
|
||||
},
|
||||
curConfig: {
|
||||
baseUnit: 'px',
|
||||
gridSnapping: false,
|
||||
snappingStep: 10,
|
||||
gridColor: '#fff',
|
||||
showRulers: false,
|
||||
canvasName: 'test'
|
||||
},
|
||||
curPrefs: { bkgd_color: '#fff' },
|
||||
preferences: false
|
||||
},
|
||||
setBackground: (color, url) => { window.__bg = { color, url } },
|
||||
rulers: { updateRulers: () => { window.__rulers = true } },
|
||||
svgCanvas,
|
||||
updateCanvas: () => { window.__updated = true },
|
||||
i18next: { t: (key) => key },
|
||||
docprops: false,
|
||||
exportWindowCt: 0,
|
||||
customExportPDF: false,
|
||||
customExportImage: false,
|
||||
exportWindowName: ''
|
||||
}
|
||||
|
||||
const holder = document.getElementById('menu-test-root') || (() => {
|
||||
const div = document.createElement('div')
|
||||
div.id = 'menu-test-root'
|
||||
document.body.append(div)
|
||||
return div
|
||||
})()
|
||||
holder.innerHTML =
|
||||
'<div id="se-img-prop"></div><div id="se-edit-prefs"></div>'
|
||||
const menu = new MainMenu(editor)
|
||||
menu.showDocProperties()
|
||||
menu.hideDocProperties()
|
||||
const savedDocProps = menu.saveDocProperties({
|
||||
detail: { title: 'New', w: 100, h: 200, save: 'content' }
|
||||
})
|
||||
|
||||
menu.showPreferences()
|
||||
menu.savePreferences({
|
||||
detail: {
|
||||
lang: 'en',
|
||||
bgcolor: '#000',
|
||||
bgurl: 'url',
|
||||
gridsnappingon: true,
|
||||
gridsnappingstep: 5,
|
||||
gridcolor: '#ccc',
|
||||
showrulers: true,
|
||||
baseunit: 'cm'
|
||||
}
|
||||
})
|
||||
menu.hidePreferences()
|
||||
menu.clickExport({ detail: { trigger: 'ok', imgType: 'PNG', quality: 80 } })
|
||||
window.seAlert?.('alert text')
|
||||
window.seConfirm?.('question?', ['Yes', 'No'])
|
||||
window.sePrompt?.('prompt me', 'defaults')
|
||||
window.seSelect?.('pick', ['a', 'b'])
|
||||
|
||||
return {
|
||||
docDialogState: document.getElementById('se-img-prop').getAttribute('dialog'),
|
||||
docSavePref: prefsStore.img_save,
|
||||
resolution: window.__resolution,
|
||||
updated: window.__updated,
|
||||
prefsDialogState: document.getElementById('se-edit-prefs').getAttribute('dialog'),
|
||||
gridColor: editor.configObj.curConfig.gridColor,
|
||||
rulersUpdated: window.__rulers === true,
|
||||
rasterCalled: window.__raster === true,
|
||||
savedDocProps
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.docDialogState).toBe('close')
|
||||
expect(result.docSavePref).toBe('content')
|
||||
expect(result.resolution).toEqual([100, 200])
|
||||
expect(result.updated).toBe(true)
|
||||
expect(result.prefsDialogState).toBe('close')
|
||||
expect(result.gridColor).toBe('#ccc')
|
||||
expect(result.rulersUpdated).toBe(true)
|
||||
expect(result.rasterCalled).toBe(true)
|
||||
expect(result.savedDocProps).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,12 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Editor web components', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('se-button clicks', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.exposeFunction('onSeButton', () => {})
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('se-button')
|
||||
@@ -17,7 +21,6 @@ test.describe('Editor web components', () => {
|
||||
})
|
||||
|
||||
test('se-flying-button clicks', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.exposeFunction('onSeFlying', () => {})
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('se-flying-button')
|
||||
@@ -32,7 +35,6 @@ test.describe('Editor web components', () => {
|
||||
})
|
||||
|
||||
test('se-explorer-button clicks', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.exposeFunction('onSeExplorer', () => {})
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('se-explorer-button')
|
||||
|
||||
61
tests/e2e/unit/svgcore-clear.spec.js
Normal file
61
tests/e2e/unit/svgcore-clear.spec.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('clear module', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('clears canvas content and sets default attributes', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { clearModule } = window.svgHarness
|
||||
const svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svgContent.append(document.createElementNS('http://www.w3.org/2000/svg', 'rect'))
|
||||
const svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const canvas = {
|
||||
getCurConfig: () => ({ dimensions: [100, 50], show_outside_canvas: false }),
|
||||
getSvgContent: () => svgContent,
|
||||
getSvgRoot: () => svgRoot,
|
||||
getDOMDocument: () => document
|
||||
}
|
||||
|
||||
clearModule.init(canvas)
|
||||
clearModule.clearSvgContentElementInit()
|
||||
const comment = svgContent.firstChild
|
||||
|
||||
return {
|
||||
childCount: svgContent.childNodes.length,
|
||||
isComment: comment.nodeType,
|
||||
overflow: svgContent.getAttribute('overflow'),
|
||||
width: svgContent.getAttribute('width'),
|
||||
height: svgContent.getAttribute('height'),
|
||||
appended: svgRoot.contains(svgContent)
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.childCount).toBe(1)
|
||||
expect(result.isComment).toBe(8)
|
||||
expect(result.overflow).toBe('hidden')
|
||||
expect(result.width).toBe('100')
|
||||
expect(result.height).toBe('50')
|
||||
expect(result.appended).toBe(true)
|
||||
})
|
||||
|
||||
test('honors show_outside_canvas flag when clearing', async ({ page }) => {
|
||||
const overflow = await page.evaluate(() => {
|
||||
const { clearModule } = window.svgHarness
|
||||
const svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
clearModule.init({
|
||||
getCurConfig: () => ({ dimensions: [10, 20], show_outside_canvas: true }),
|
||||
getSvgContent: () => svgContent,
|
||||
getSvgRoot: () => svgRoot,
|
||||
getDOMDocument: () => document
|
||||
})
|
||||
clearModule.clearSvgContentElementInit()
|
||||
return svgContent.getAttribute('overflow')
|
||||
})
|
||||
|
||||
expect(overflow).toBe('visible')
|
||||
})
|
||||
})
|
||||
92
tests/e2e/unit/svgcore-draw-extra.spec.js
Normal file
92
tests/e2e/unit/svgcore-draw-extra.spec.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core draw extras', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness?.draw))
|
||||
})
|
||||
|
||||
test('Drawing merges layers and moves children upward', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { draw } = window.svgHarness
|
||||
const NS = 'http://www.w3.org/2000/svg'
|
||||
const svg = document.createElementNS(NS, 'svg')
|
||||
document.body.append(svg)
|
||||
const drawing = new draw.Drawing(svg, 'id_')
|
||||
drawing.identifyLayers()
|
||||
const hrLog = []
|
||||
const hrService = {
|
||||
startBatchCommand: (name) => hrLog.push('start:' + name),
|
||||
endBatchCommand: () => hrLog.push('end'),
|
||||
removeElement: (el) => hrLog.push('remove:' + el.tagName),
|
||||
moveElement: (el) => hrLog.push('move:' + el.tagName),
|
||||
insertElement: (el) => hrLog.push('insert:' + el.tagName)
|
||||
}
|
||||
|
||||
const baseLayer = drawing.getCurrentLayer()
|
||||
const rect = document.createElementNS(NS, 'rect')
|
||||
baseLayer.append(rect)
|
||||
|
||||
drawing.createLayer('Layer 2', hrService)
|
||||
const layer2 = drawing.getCurrentLayer()
|
||||
const circle = document.createElementNS(NS, 'circle')
|
||||
layer2.append(circle)
|
||||
|
||||
drawing.mergeLayer(hrService)
|
||||
|
||||
return {
|
||||
mergedShapes: baseLayer.querySelectorAll('rect,circle').length,
|
||||
layersAfterMerge: drawing.getNumLayers(),
|
||||
currentName: drawing.getCurrentLayerName(),
|
||||
log: hrLog
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.layersAfterMerge).toBe(1)
|
||||
expect(result.mergedShapes).toBe(2)
|
||||
expect(result.currentName).toContain('Layer')
|
||||
expect(result.log.some(entry => entry.startsWith('start:Merge Layer'))).toBe(true)
|
||||
expect(result.log.some(entry => entry.startsWith('move:circle'))).toBe(true)
|
||||
})
|
||||
|
||||
test('mergeAllLayers collapses multiple layers into one', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { draw } = window.svgHarness
|
||||
const NS = 'http://www.w3.org/2000/svg'
|
||||
const svg = document.createElementNS(NS, 'svg')
|
||||
document.body.append(svg)
|
||||
const drawing = new draw.Drawing(svg, 'id_')
|
||||
drawing.identifyLayers()
|
||||
const hrLog = []
|
||||
const hrService = {
|
||||
startBatchCommand: (name) => hrLog.push('start:' + name),
|
||||
endBatchCommand: () => hrLog.push('end'),
|
||||
removeElement: () => {},
|
||||
moveElement: () => {},
|
||||
insertElement: () => {}
|
||||
}
|
||||
|
||||
// Make three layers with a child each
|
||||
const baseLayer = drawing.getCurrentLayer()
|
||||
baseLayer.append(document.createElementNS(NS, 'rect'))
|
||||
drawing.createLayer('Layer 2', hrService)
|
||||
drawing.getCurrentLayer().append(document.createElementNS(NS, 'circle'))
|
||||
drawing.createLayer('Layer 3', hrService)
|
||||
drawing.getCurrentLayer().append(document.createElementNS(NS, 'line'))
|
||||
|
||||
drawing.mergeAllLayers(hrService)
|
||||
|
||||
const remaining = drawing.getCurrentLayer()
|
||||
return {
|
||||
finalLayers: drawing.getNumLayers(),
|
||||
shapes: remaining.querySelectorAll('rect,circle,line').length,
|
||||
log: hrLog
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.finalLayers).toBe(1)
|
||||
expect(result.shapes).toBe(3)
|
||||
expect(result.log.some(entry => entry === 'start:Merge all Layers')).toBe(true)
|
||||
expect(result.log.at(-1)).toBe('end')
|
||||
})
|
||||
})
|
||||
147
tests/e2e/unit/svgcore-drawing.spec.js
Normal file
147
tests/e2e/unit/svgcore-drawing.spec.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core drawing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('manages ids and adopts orphaned elements into layers', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { draw, namespaces } = window.svgHarness
|
||||
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
|
||||
document.getElementById('root').append(svg)
|
||||
|
||||
const existingLayer = document.createElementNS(namespaces.NS.SVG, 'g')
|
||||
existingLayer.classList.add('layer')
|
||||
const title = document.createElementNS(namespaces.NS.SVG, 'title')
|
||||
title.textContent = 'Layer 1'
|
||||
existingLayer.append(title)
|
||||
const orphan = document.createElementNS(namespaces.NS.SVG, 'rect')
|
||||
orphan.id = 'rect-orphan'
|
||||
svg.append(existingLayer, orphan)
|
||||
|
||||
const drawing = new draw.Drawing(svg, 'p_')
|
||||
drawing.identifyLayers()
|
||||
|
||||
const id1 = drawing.getNextId()
|
||||
const id2 = drawing.getNextId()
|
||||
const released = drawing.releaseId(id2)
|
||||
const reused = drawing.getNextId()
|
||||
const next = drawing.getNextId()
|
||||
|
||||
return {
|
||||
id1,
|
||||
id2,
|
||||
released,
|
||||
reused,
|
||||
next,
|
||||
layerCount: drawing.getNumLayers(),
|
||||
currentLayer: drawing.getCurrentLayerName(),
|
||||
orphanParentTag: orphan.parentNode?.tagName.toLowerCase()
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.id1).toBe('p_1')
|
||||
expect(result.id2).toBe('p_2')
|
||||
expect(result.released).toBe(true)
|
||||
expect(result.reused).toBe(result.id2)
|
||||
expect(result.next).toBe('p_3')
|
||||
expect(result.layerCount).toBe(2)
|
||||
expect(result.currentLayer).toBe('Layer 2')
|
||||
expect(result.orphanParentTag).toBe('g')
|
||||
})
|
||||
|
||||
test('reorders and toggles layers', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { draw, namespaces } = window.svgHarness
|
||||
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
|
||||
document.getElementById('root').append(svg)
|
||||
const drawing = new draw.Drawing(svg)
|
||||
drawing.identifyLayers() // creates first layer
|
||||
const originalName = drawing.getCurrentLayerName()
|
||||
|
||||
drawing.createLayer('Layer Two')
|
||||
drawing.createLayer('Layer Three')
|
||||
drawing.setCurrentLayer('Layer Two')
|
||||
const movedDown = drawing.setCurrentLayerPosition(2)
|
||||
const orderAfterDown = [
|
||||
drawing.getLayerName(0),
|
||||
drawing.getLayerName(1),
|
||||
drawing.getLayerName(2)
|
||||
]
|
||||
|
||||
drawing.setCurrentLayer('Layer Three')
|
||||
const movedUp = drawing.setCurrentLayerPosition(0)
|
||||
const orderAfterUp = [
|
||||
drawing.getLayerName(0),
|
||||
drawing.getLayerName(1),
|
||||
drawing.getLayerName(2)
|
||||
]
|
||||
|
||||
const target = drawing.getCurrentLayerName()
|
||||
drawing.setLayerVisibility(target, false)
|
||||
const hidden = drawing.getLayerVisibility(target)
|
||||
drawing.setLayerOpacity(target, 0.5)
|
||||
|
||||
return {
|
||||
originalName,
|
||||
movedDown: Boolean(movedDown),
|
||||
movedUp: Boolean(movedUp),
|
||||
orderAfterDown,
|
||||
orderAfterUp,
|
||||
hidden,
|
||||
opacity: drawing.getLayerOpacity(target)
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.originalName).toBe('Layer 1')
|
||||
expect(result.movedDown).toBe(true)
|
||||
expect(result.orderAfterDown[2]).toBe('Layer Two')
|
||||
expect(result.movedUp).toBe(true)
|
||||
expect(result.orderAfterUp[0]).toBe('Layer Three')
|
||||
expect(result.hidden).toBe(false)
|
||||
expect(result.opacity).toBe(0.5)
|
||||
})
|
||||
|
||||
test('clones and deletes layers and randomizes ids', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { draw, namespaces } = window.svgHarness
|
||||
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
|
||||
document.getElementById('root').append(svg)
|
||||
const drawing = new draw.Drawing(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
const currentLayer = drawing.getCurrentLayer()
|
||||
const circle = document.createElementNS(namespaces.NS.SVG, 'circle')
|
||||
circle.setAttribute('id', 'seed')
|
||||
currentLayer.append(circle)
|
||||
|
||||
const cloneGroup = drawing.cloneLayer('Duplicated')
|
||||
const clonedCircle = cloneGroup?.querySelector('circle')
|
||||
const beforeDelete = drawing.getNumLayers()
|
||||
const deleted = drawing.deleteCurrentLayer()
|
||||
const afterDelete = drawing.getNumLayers()
|
||||
|
||||
draw.randomizeIds(true, drawing)
|
||||
const nonceSet = drawing.getNonce()
|
||||
draw.randomizeIds(false, drawing)
|
||||
|
||||
return {
|
||||
cloneHasChild: Boolean(clonedCircle),
|
||||
beforeDelete,
|
||||
afterDelete,
|
||||
deletedTag: deleted?.tagName.toLowerCase(),
|
||||
nonceSet,
|
||||
nonceCleared: drawing.getNonce()
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.cloneHasChild).toBe(true)
|
||||
expect(result.beforeDelete).toBe(2)
|
||||
expect(result.afterDelete).toBe(1)
|
||||
expect(result.deletedTag).toBe('g')
|
||||
expect(result.nonceSet).not.toBe('')
|
||||
expect(result.nonceCleared).toBe('')
|
||||
})
|
||||
})
|
||||
122
tests/e2e/unit/svgcore-geometry.spec.js
Normal file
122
tests/e2e/unit/svgcore-geometry.spec.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core math and coords', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('math helpers consolidate transforms and snapping', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { math } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
document.body.append(svg)
|
||||
const identity = math.transformPoint(100, 200, svg.createSVGMatrix())
|
||||
const translatedMatrix = svg.createSVGMatrix()
|
||||
translatedMatrix.e = 300
|
||||
translatedMatrix.f = 400
|
||||
const translated = math.transformPoint(5, 5, translatedMatrix)
|
||||
const tlist = math.getTransformList(svg)
|
||||
const hasMatrixBefore = math.hasMatrixTransform(tlist)
|
||||
const tf = svg.createSVGTransformFromMatrix(translatedMatrix)
|
||||
tlist.appendItem(tf)
|
||||
const consolidated = math.transformListToTransform(tlist).matrix
|
||||
const hasMatrixAfter = math.hasMatrixTransform(tlist)
|
||||
const multiplied = math.matrixMultiply(
|
||||
svg.createSVGMatrix().translate(10, 20),
|
||||
svg.createSVGMatrix().translate(-10, -20)
|
||||
)
|
||||
const snapped = math.snapToAngle(0, 0, 10, 5)
|
||||
const intersects = {
|
||||
overlap: math.rectsIntersect(
|
||||
{ x: 0, y: 0, width: 50, height: 50 },
|
||||
{ x: 25, y: 25, width: 10, height: 10 }
|
||||
),
|
||||
apart: math.rectsIntersect(
|
||||
{ x: 0, y: 0, width: 10, height: 10 },
|
||||
{ x: 100, y: 100, width: 5, height: 5 }
|
||||
)
|
||||
}
|
||||
return {
|
||||
identity,
|
||||
translated,
|
||||
hasMatrixBefore,
|
||||
hasMatrixAfter,
|
||||
consolidated: { e: consolidated.e, f: consolidated.f },
|
||||
multiplied: { e: multiplied.e, f: multiplied.f },
|
||||
snapped,
|
||||
intersects
|
||||
}
|
||||
})
|
||||
expect(result.identity).toEqual({ x: 100, y: 200 })
|
||||
expect(result.translated).toEqual({ x: 305, y: 405 })
|
||||
expect(result.hasMatrixBefore).toBe(false)
|
||||
expect(result.hasMatrixAfter).toBe(true)
|
||||
expect(result.consolidated.e).toBe(300)
|
||||
expect(result.consolidated.f).toBe(400)
|
||||
expect(result.multiplied.e).toBe(0)
|
||||
expect(result.multiplied.f).toBe(0)
|
||||
expect(result.snapped.a).toBeCloseTo(Math.PI / 4)
|
||||
expect(result.intersects.overlap).toBe(true)
|
||||
expect(result.intersects.apart).toBe(false)
|
||||
})
|
||||
|
||||
test('coords.remapElement handles translation and scaling', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { coords, utilities } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
document.body.append(svg)
|
||||
utilities.init({
|
||||
getSvgRoot: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg
|
||||
})
|
||||
coords.init({
|
||||
getGridSnapping: () => false,
|
||||
getDrawing: () => ({ getNextId: () => '1' })
|
||||
})
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '200')
|
||||
rect.setAttribute('y', '150')
|
||||
rect.setAttribute('width', '250')
|
||||
rect.setAttribute('height', '120')
|
||||
svg.append(rect)
|
||||
const translateMatrix = svg.createSVGMatrix()
|
||||
translateMatrix.e = 100
|
||||
translateMatrix.f = -50
|
||||
coords.remapElement(
|
||||
rect,
|
||||
{ x: '200', y: '150', width: '125', height: '75' },
|
||||
translateMatrix
|
||||
)
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
|
||||
circle.setAttribute('cx', '200')
|
||||
circle.setAttribute('cy', '150')
|
||||
circle.setAttribute('r', '250')
|
||||
svg.append(circle)
|
||||
const scaleMatrix = svg.createSVGMatrix()
|
||||
scaleMatrix.a = 2
|
||||
scaleMatrix.d = 0.5
|
||||
coords.remapElement(
|
||||
circle,
|
||||
{ cx: '200', cy: '150', r: '250' },
|
||||
scaleMatrix
|
||||
)
|
||||
return {
|
||||
rect: {
|
||||
x: rect.getAttribute('x'),
|
||||
y: rect.getAttribute('y'),
|
||||
width: rect.getAttribute('width'),
|
||||
height: rect.getAttribute('height')
|
||||
},
|
||||
circle: {
|
||||
cx: circle.getAttribute('cx'),
|
||||
cy: circle.getAttribute('cy'),
|
||||
r: circle.getAttribute('r')
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(result.rect).toEqual({ x: '300', y: '100', width: '125', height: '75' })
|
||||
expect(result.circle).toEqual({ cx: '400', cy: '75', r: '125' })
|
||||
})
|
||||
})
|
||||
207
tests/e2e/unit/svgcore-history-draw.spec.js
Normal file
207
tests/e2e/unit/svgcore-history-draw.spec.js
Normal file
@@ -0,0 +1,207 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core history and draw', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('UndoManager tracks stacks and command texts through undo/redo', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { history } = window.svgHarness
|
||||
let lastCalled = ''
|
||||
class MockCommand extends history.Command {
|
||||
constructor (text) {
|
||||
super()
|
||||
this.text = text
|
||||
}
|
||||
|
||||
apply () { lastCalled = `${this.text}:apply` }
|
||||
unapply () { lastCalled = `${this.text}:unapply` }
|
||||
elements () { return [] }
|
||||
}
|
||||
const um = new history.UndoManager()
|
||||
;['First', 'Second', 'Third'].forEach((label) => {
|
||||
um.addCommandToHistory(new MockCommand(label))
|
||||
})
|
||||
const beforeUndo = {
|
||||
undo: um.getUndoStackSize(),
|
||||
redo: um.getRedoStackSize(),
|
||||
nextUndo: um.getNextUndoCommandText(),
|
||||
nextRedo: um.getNextRedoCommandText()
|
||||
}
|
||||
um.undo()
|
||||
const afterFirstUndo = {
|
||||
undo: um.getUndoStackSize(),
|
||||
redo: um.getRedoStackSize(),
|
||||
nextUndo: um.getNextUndoCommandText(),
|
||||
nextRedo: um.getNextRedoCommandText(),
|
||||
lastCalled
|
||||
}
|
||||
um.undo()
|
||||
um.redo()
|
||||
const afterRedo = {
|
||||
undo: um.getUndoStackSize(),
|
||||
redo: um.getRedoStackSize(),
|
||||
nextUndo: um.getNextUndoCommandText(),
|
||||
nextRedo: um.getNextRedoCommandText(),
|
||||
lastCalled
|
||||
}
|
||||
return { beforeUndo, afterFirstUndo, afterRedo }
|
||||
})
|
||||
expect(result.beforeUndo).toEqual({
|
||||
undo: 3,
|
||||
redo: 0,
|
||||
nextUndo: 'Third',
|
||||
nextRedo: ''
|
||||
})
|
||||
expect(result.afterFirstUndo.undo).toBe(2)
|
||||
expect(result.afterFirstUndo.redo).toBe(1)
|
||||
expect(result.afterFirstUndo.nextUndo).toBe('Second')
|
||||
expect(result.afterFirstUndo.nextRedo).toBe('Third')
|
||||
expect(result.afterFirstUndo.lastCalled).toBe('Third:unapply')
|
||||
expect(result.afterRedo.undo).toBe(2)
|
||||
expect(result.afterRedo.redo).toBe(1)
|
||||
expect(result.afterRedo.nextUndo).toBe('Second')
|
||||
expect(result.afterRedo.nextRedo).toBe('Third')
|
||||
expect(result.afterRedo.lastCalled).toBe('Second:apply')
|
||||
})
|
||||
|
||||
test('history commands move, insert and remove elements in the DOM', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { history } = window.svgHarness
|
||||
const makeParent = () => {
|
||||
const parent = document.createElement('div')
|
||||
parent.id = 'parent'
|
||||
const children = ['div1', 'div2', 'div3'].map((id) => {
|
||||
const el = document.createElement('div')
|
||||
el.id = id
|
||||
parent.append(el)
|
||||
return el
|
||||
})
|
||||
document.body.append(parent)
|
||||
return { parent, children }
|
||||
}
|
||||
const order = (parent) => [...parent.children].map((el) => el.id)
|
||||
|
||||
const { parent: parentMove, children: moveChildren } = makeParent()
|
||||
const move = new history.MoveElementCommand(
|
||||
moveChildren[2],
|
||||
moveChildren[0],
|
||||
parentMove
|
||||
)
|
||||
move.unapply()
|
||||
const orderAfterMoveUnapply = order(parentMove)
|
||||
move.apply()
|
||||
const orderAfterMoveApply = order(parentMove)
|
||||
|
||||
const { parent: parentInsert, children: insertChildren } = makeParent()
|
||||
const insert = new history.InsertElementCommand(insertChildren[2])
|
||||
insert.unapply()
|
||||
const orderAfterInsertUnapply = order(parentInsert)
|
||||
insert.apply()
|
||||
const orderAfterInsertApply = order(parentInsert)
|
||||
|
||||
const { parent: parentRemove } = makeParent()
|
||||
const extra = document.createElement('div')
|
||||
extra.id = 'div4'
|
||||
const remove = new history.RemoveElementCommand(extra, null, parentRemove)
|
||||
remove.unapply()
|
||||
const orderAfterRemoveUnapply = order(parentRemove)
|
||||
remove.apply()
|
||||
const orderAfterRemoveApply = order(parentRemove)
|
||||
|
||||
return {
|
||||
orderAfterMoveUnapply,
|
||||
orderAfterMoveApply,
|
||||
orderAfterInsertUnapply,
|
||||
orderAfterInsertApply,
|
||||
orderAfterRemoveUnapply,
|
||||
orderAfterRemoveApply
|
||||
}
|
||||
})
|
||||
expect(result.orderAfterMoveUnapply).toEqual(['div3', 'div1', 'div2'])
|
||||
expect(result.orderAfterMoveApply).toEqual(['div1', 'div2', 'div3'])
|
||||
expect(result.orderAfterInsertUnapply).toEqual(['div1', 'div2'])
|
||||
expect(result.orderAfterInsertApply).toEqual(['div1', 'div2', 'div3'])
|
||||
expect(result.orderAfterRemoveUnapply).toEqual(['div1', 'div2', 'div3', 'div4'])
|
||||
expect(result.orderAfterRemoveApply).toEqual(['div1', 'div2', 'div3'])
|
||||
})
|
||||
|
||||
test('BatchCommand applies and unapplies subcommands in order', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { history } = window.svgHarness
|
||||
let record = ''
|
||||
class TextCommand extends history.Command {
|
||||
constructor (text) {
|
||||
super()
|
||||
this.text = text
|
||||
}
|
||||
|
||||
apply () { record += this.text }
|
||||
unapply () { record += this.text.toUpperCase() }
|
||||
elements () { return [] }
|
||||
}
|
||||
const batch = new history.BatchCommand()
|
||||
const emptyBefore = batch.isEmpty()
|
||||
batch.addSubCommand(new TextCommand('a'))
|
||||
batch.addSubCommand(new TextCommand('b'))
|
||||
batch.addSubCommand(new TextCommand('c'))
|
||||
batch.apply()
|
||||
const afterApply = record
|
||||
record = ''
|
||||
batch.unapply()
|
||||
const afterUnapply = record
|
||||
return { emptyBefore, afterApply, afterUnapply }
|
||||
})
|
||||
expect(result.emptyBefore).toBe(true)
|
||||
expect(result.afterApply).toBe('abc')
|
||||
expect(result.afterUnapply).toBe('CBA')
|
||||
})
|
||||
|
||||
test('Drawing creates layers, generates ids and toggles nonce randomization', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { draw, namespaces } = window.svgHarness
|
||||
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
|
||||
document.body.append(svg)
|
||||
const drawing = new draw.Drawing(svg)
|
||||
const beforeIdentify = drawing.getNumLayers()
|
||||
drawing.identifyLayers()
|
||||
const defaultLayer = drawing.getCurrentLayer()
|
||||
const defaultName = drawing.getCurrentLayerName()
|
||||
const hrCounts = { start: 0, end: 0, insert: 0 }
|
||||
const newLayer = drawing.createLayer('Layer A', {
|
||||
startBatchCommand: () => { hrCounts.start++ },
|
||||
endBatchCommand: () => { hrCounts.end++ },
|
||||
insertElement: () => { hrCounts.insert++ }
|
||||
})
|
||||
const afterCreate = {
|
||||
num: drawing.getNumLayers(),
|
||||
currentName: drawing.getCurrentLayerName(),
|
||||
className: newLayer.getAttribute('class')
|
||||
}
|
||||
draw.randomizeIds(true, drawing)
|
||||
const nonceAfterRandomize = drawing.getNonce()
|
||||
draw.randomizeIds(false, drawing)
|
||||
const nonceAfterClear = drawing.getNonce()
|
||||
return {
|
||||
beforeIdentify,
|
||||
defaultName,
|
||||
defaultLayerClass: defaultLayer?.getAttribute('class'),
|
||||
afterCreate,
|
||||
hrCounts,
|
||||
nonceAfterRandomize,
|
||||
nonceAfterClear
|
||||
}
|
||||
})
|
||||
expect(result.beforeIdentify).toBe(0)
|
||||
expect(result.defaultLayerClass).toBeDefined()
|
||||
expect(result.defaultName.length).toBeGreaterThan(0)
|
||||
expect(result.afterCreate.num).toBe(2)
|
||||
expect(result.afterCreate.currentName).toBe('Layer A')
|
||||
expect(result.afterCreate.className).toBe(result.defaultLayerClass)
|
||||
expect(result.hrCounts).toEqual({ start: 1, end: 1, insert: 1 })
|
||||
expect(result.nonceAfterRandomize).toBeTruthy()
|
||||
expect(result.nonceAfterClear).toBe('')
|
||||
})
|
||||
})
|
||||
46
tests/e2e/unit/svgcore-history.spec.js
Normal file
46
tests/e2e/unit/svgcore-history.spec.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core history/draw smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('UndoManager push/undo/redo stack sizes update', async ({ page }) => {
|
||||
const stacks = await page.evaluate(() => {
|
||||
const { history } = window.svgHarness
|
||||
class DummyCommand extends history.Command {
|
||||
constructor (text) {
|
||||
super()
|
||||
this.text = text
|
||||
}
|
||||
|
||||
apply () {}
|
||||
unapply () {}
|
||||
}
|
||||
const um = new history.UndoManager()
|
||||
um.addCommandToHistory(new DummyCommand('one'))
|
||||
um.addCommandToHistory(new DummyCommand('two'))
|
||||
const beforeUndo = { undo: um.getUndoStackSize(), redo: um.getRedoStackSize() }
|
||||
um.undo()
|
||||
const afterUndo = { undo: um.getUndoStackSize(), redo: um.getRedoStackSize() }
|
||||
um.redo()
|
||||
const afterRedo = { undo: um.getUndoStackSize(), redo: um.getRedoStackSize() }
|
||||
return { beforeUndo, afterUndo, afterRedo, nextUndo: um.getNextUndoCommandText(), nextRedo: um.getNextRedoCommandText() }
|
||||
})
|
||||
expect(stacks.beforeUndo.undo).toBe(2)
|
||||
expect(stacks.beforeUndo.redo).toBe(0)
|
||||
expect(stacks.afterUndo.undo).toBe(1)
|
||||
expect(stacks.afterUndo.redo).toBe(1)
|
||||
expect(stacks.afterRedo.undo).toBe(2)
|
||||
expect(stacks.nextUndo.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('draw module exports expected functions', async ({ page }) => {
|
||||
const exports = await page.evaluate(() => {
|
||||
const { draw } = window.svgHarness
|
||||
return ['init', 'randomizeIds', 'createLayer'].map(fn => typeof draw[fn] === 'function')
|
||||
})
|
||||
exports.forEach(v => expect(v).toBe(true))
|
||||
})
|
||||
})
|
||||
28
tests/e2e/unit/svgcore-namespaces.spec.js
Normal file
28
tests/e2e/unit/svgcore-namespaces.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG namespace helpers', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness?.namespaces))
|
||||
})
|
||||
|
||||
test('reverse namespace map includes core URIs', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { namespaces } = window.svgHarness
|
||||
const reverse = namespaces.getReverseNS()
|
||||
return {
|
||||
svg: namespaces.NS.SVG,
|
||||
html: namespaces.NS.HTML,
|
||||
xml: namespaces.NS.XML,
|
||||
reverseSvg: reverse[namespaces.NS.SVG],
|
||||
reverseXmlns: reverse[namespaces.NS.XMLNS]
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.svg).toBe('http://www.w3.org/2000/svg')
|
||||
expect(result.html).toBe('http://www.w3.org/1999/xhtml')
|
||||
expect(result.xml).toBe('http://www.w3.org/XML/1998/namespace')
|
||||
expect(result.reverseSvg).toBe('svg')
|
||||
expect(result.reverseXmlns).toBe('xmlns')
|
||||
})
|
||||
})
|
||||
37
tests/e2e/unit/svgcore-path-extra.spec.js
Normal file
37
tests/e2e/unit/svgcore-path-extra.spec.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core path extras', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness?.pathModule))
|
||||
})
|
||||
|
||||
test('convertPath handles arcs and shorthand commands', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { pathModule, units } = window.svgHarness
|
||||
// Ensure unit helpers are initialized so shortFloat can round numbers.
|
||||
units.init({
|
||||
getRoundDigits: () => 3,
|
||||
getBaseUnit: () => 'px',
|
||||
getElement: () => null,
|
||||
getHeight: () => 100,
|
||||
getWidth: () => 100
|
||||
})
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute(
|
||||
'd',
|
||||
'M0 0 H10 V10 h-5 v-5 a5 5 0 0 1 5 5 S20 20 25 25 Z'
|
||||
)
|
||||
|
||||
const rel = pathModule.convertPath(path, true)
|
||||
const abs = pathModule.convertPath(path, false)
|
||||
|
||||
return { rel, abs }
|
||||
})
|
||||
|
||||
expect(result.rel.toLowerCase()).toContain('a')
|
||||
expect(result.rel).toContain('s')
|
||||
expect(result.abs).toContain('L')
|
||||
expect(result.abs).toContain('A')
|
||||
})
|
||||
})
|
||||
307
tests/e2e/unit/svgcore-recalculate-extra.spec.js
Normal file
307
tests/e2e/unit/svgcore-recalculate-extra.spec.js
Normal file
@@ -0,0 +1,307 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core recalculate extra cases', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('scales elements and flips gradients/matrices', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { utilities, coords, recalculate } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
||||
const grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient')
|
||||
grad.id = 'grad1'
|
||||
grad.setAttribute('x1', '0')
|
||||
grad.setAttribute('x2', '1')
|
||||
grad.setAttribute('y1', '0')
|
||||
grad.setAttribute('y2', '0')
|
||||
defs.append(grad)
|
||||
svg.append(defs)
|
||||
document.body.append(svg)
|
||||
|
||||
const dataStorage = {
|
||||
store: new WeakMap(),
|
||||
put (el, key, value) {
|
||||
if (!this.store.has(el)) this.store.set(el, new Map())
|
||||
this.store.get(el).set(key, value)
|
||||
},
|
||||
get (el, key) {
|
||||
return this.store.get(el)?.get(key)
|
||||
},
|
||||
has (el, key) {
|
||||
return this.store.has(el) && this.store.get(el).has(key)
|
||||
},
|
||||
remove (el, key) {
|
||||
const bucket = this.store.get(el)
|
||||
if (!bucket) return false
|
||||
const deleted = bucket.delete(key)
|
||||
if (!bucket.size) this.store.delete(el)
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
|
||||
const canvasStub = {
|
||||
getSvgRoot: () => svg,
|
||||
getStartTransform: () => '',
|
||||
setStartTransform: () => {},
|
||||
getDataStorage: () => dataStorage,
|
||||
getCurrentDrawing: () => ({ getNextId: () => 'g1' })
|
||||
}
|
||||
|
||||
utilities.init({
|
||||
getSvgRoot: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg,
|
||||
getDataStorage: () => dataStorage
|
||||
})
|
||||
coords.init({
|
||||
getGridSnapping: () => false,
|
||||
getDrawing: () => ({ getNextId: () => 'id2' }),
|
||||
getDataStorage: () => dataStorage
|
||||
})
|
||||
recalculate.init(canvasStub)
|
||||
|
||||
// Scale about center via translate/scale/translate sequence
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '5')
|
||||
rect.setAttribute('y', '6')
|
||||
rect.setAttribute('width', '10')
|
||||
rect.setAttribute('height', '8')
|
||||
rect.setAttribute('transform', 'translate(5 5) scale(2 3) translate(-5 -5)')
|
||||
svg.append(rect)
|
||||
|
||||
// Flip with gradient fill using matrix
|
||||
const rectFlip = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rectFlip.setAttribute('x', '0')
|
||||
rectFlip.setAttribute('y', '0')
|
||||
rectFlip.setAttribute('width', '4')
|
||||
rectFlip.setAttribute('height', '4')
|
||||
rectFlip.setAttribute('fill', 'url(#grad1)')
|
||||
rectFlip.setAttribute('transform', 'matrix(-1 0 0 1 0 0)')
|
||||
svg.append(rectFlip)
|
||||
|
||||
recalculate.recalculateDimensions(rect)
|
||||
recalculate.recalculateDimensions(rectFlip)
|
||||
|
||||
return {
|
||||
rect: {
|
||||
width: rect.getAttribute('width'),
|
||||
height: rect.getAttribute('height'),
|
||||
transformRemoved: rect.hasAttribute('transform')
|
||||
},
|
||||
flip: {
|
||||
width: rectFlip.getAttribute('width'),
|
||||
height: rectFlip.getAttribute('height'),
|
||||
fill: rectFlip.getAttribute('fill')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(Number(result.rect.width)).toBeGreaterThan(10)
|
||||
expect(Number(result.rect.height)).toBeGreaterThan(8)
|
||||
expect(result.rect.transformRemoved).toBe(false) // scaling keeps transform list
|
||||
expect(result.flip.fill.startsWith('url(')).toBe(true)
|
||||
})
|
||||
|
||||
test('recalculateDimensions reapplies rotations and updates clip paths', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const NS = 'http://www.w3.org/2000/svg'
|
||||
const { utilities, coords, recalculate } = window.svgHarness
|
||||
const svg = document.createElementNS(NS, 'svg')
|
||||
const defs = document.createElementNS(NS, 'defs')
|
||||
svg.append(defs)
|
||||
document.body.append(svg)
|
||||
|
||||
const dataStorage = {
|
||||
store: new WeakMap(),
|
||||
put (el, key, value) {
|
||||
if (!this.store.has(el)) this.store.set(el, new Map())
|
||||
this.store.get(el).set(key, value)
|
||||
},
|
||||
get (el, key) {
|
||||
return this.store.get(el)?.get(key)
|
||||
},
|
||||
has (el, key) {
|
||||
return this.store.has(el) && this.store.get(el).has(key)
|
||||
}
|
||||
}
|
||||
const drawing = {
|
||||
next: 0,
|
||||
getNextId () {
|
||||
this.next += 1
|
||||
return 'd' + this.next
|
||||
}
|
||||
}
|
||||
|
||||
const canvasStub = {
|
||||
getSvgRoot: () => svg,
|
||||
getSvgContent: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg,
|
||||
getDataStorage: () => dataStorage,
|
||||
getStartTransform: () => '',
|
||||
setStartTransform: () => {},
|
||||
getCurrentDrawing: () => drawing
|
||||
}
|
||||
utilities.init(canvasStub)
|
||||
coords.init({
|
||||
getGridSnapping: () => false,
|
||||
getDrawing: () => drawing,
|
||||
getDataStorage: () => dataStorage,
|
||||
getCurrentDrawing: () => drawing,
|
||||
getSvgRoot: () => svg
|
||||
})
|
||||
recalculate.init(canvasStub)
|
||||
|
||||
const rect = document.createElementNS(NS, 'rect')
|
||||
rect.setAttribute('x', '0')
|
||||
rect.setAttribute('y', '0')
|
||||
rect.setAttribute('width', '10')
|
||||
rect.setAttribute('height', '10')
|
||||
rect.setAttribute('transform', 'translate(10 5) rotate(30)')
|
||||
svg.append(rect)
|
||||
|
||||
const clipPath = document.createElementNS(NS, 'clipPath')
|
||||
clipPath.id = 'clip1'
|
||||
const clipRect = document.createElementNS(NS, 'rect')
|
||||
clipRect.setAttribute('x', '0')
|
||||
clipRect.setAttribute('y', '0')
|
||||
clipRect.setAttribute('width', '4')
|
||||
clipRect.setAttribute('height', '4')
|
||||
clipPath.append(clipRect)
|
||||
defs.append(clipPath)
|
||||
|
||||
const cmd = recalculate.recalculateDimensions(rect)
|
||||
recalculate.updateClipPath('url(#clip1)', 3, -2)
|
||||
|
||||
return {
|
||||
rect: {
|
||||
x: rect.getAttribute('x'),
|
||||
y: rect.getAttribute('y'),
|
||||
transform: rect.getAttribute('transform'),
|
||||
hasCommand: Boolean(cmd)
|
||||
},
|
||||
clip: {
|
||||
x: clipRect.getAttribute('x'),
|
||||
y: clipRect.getAttribute('y'),
|
||||
transforms: clipRect.transform.baseVal.numberOfItems
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.rect.x).toBe('10')
|
||||
expect(result.rect.y).toBe('5')
|
||||
expect(result.rect.transform).toContain('rotate(')
|
||||
expect(result.rect.transform).not.toContain('translate')
|
||||
expect(result.rect.hasCommand).toBe(true)
|
||||
expect(result.clip.x).toBe('3')
|
||||
expect(result.clip.y).toBe('-2')
|
||||
expect(result.clip.transforms).toBe(0)
|
||||
})
|
||||
|
||||
test('recalculateDimensions remaps polygons and matrix transforms', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const NS = 'http://www.w3.org/2000/svg'
|
||||
const { utilities, coords, recalculate } = window.svgHarness
|
||||
const svg = document.createElementNS(NS, 'svg')
|
||||
document.body.append(svg)
|
||||
const dataStorage = {
|
||||
store: new WeakMap(),
|
||||
put (el, key, value) {
|
||||
if (!this.store.has(el)) this.store.set(el, new Map())
|
||||
this.store.get(el).set(key, value)
|
||||
},
|
||||
get (el, key) {
|
||||
return this.store.get(el)?.get(key)
|
||||
},
|
||||
has (el, key) {
|
||||
return this.store.has(el) && this.store.get(el).has(key)
|
||||
}
|
||||
}
|
||||
const drawing = {
|
||||
next: 0,
|
||||
getNextId () {
|
||||
this.next += 1
|
||||
return 'p' + this.next
|
||||
}
|
||||
}
|
||||
const canvasStub = {
|
||||
getSvgRoot: () => svg,
|
||||
getSvgContent: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg,
|
||||
getDataStorage: () => dataStorage,
|
||||
getStartTransform: () => '',
|
||||
setStartTransform: () => {},
|
||||
getCurrentDrawing: () => drawing
|
||||
}
|
||||
utilities.init(canvasStub)
|
||||
coords.init({
|
||||
getGridSnapping: () => false,
|
||||
getDrawing: () => drawing,
|
||||
getDataStorage: () => dataStorage,
|
||||
getCurrentDrawing: () => drawing,
|
||||
getSvgRoot: () => svg
|
||||
})
|
||||
recalculate.init(canvasStub)
|
||||
|
||||
const poly = document.createElementNS(NS, 'polygon')
|
||||
poly.setAttribute('points', '0,0 10,0 10,10')
|
||||
poly.setAttribute('transform', 'translate(5 5) scale(-1 2) translate(-5 -5)')
|
||||
svg.append(poly)
|
||||
|
||||
const path = document.createElementNS(NS, 'path')
|
||||
path.setAttribute('d', 'M0 0 L1 0 L1 1 z')
|
||||
path.setAttribute('transform', 'matrix(1 0 0 1 7 8)')
|
||||
svg.append(path)
|
||||
|
||||
const rect = document.createElementNS(NS, 'rect')
|
||||
rect.setAttribute('x', '1')
|
||||
rect.setAttribute('y', '2')
|
||||
rect.setAttribute('width', '5')
|
||||
rect.setAttribute('height', '4')
|
||||
rect.setAttribute('transform', 'matrix(1 0 0 1 7 8)')
|
||||
svg.append(rect)
|
||||
|
||||
const useElem = document.createElementNS(NS, 'use')
|
||||
useElem.setAttribute('href', '#missing')
|
||||
useElem.setAttribute('transform', 'translate(3 4)')
|
||||
svg.append(useElem)
|
||||
|
||||
const cmdPoly = recalculate.recalculateDimensions(poly)
|
||||
const cmdPath = recalculate.recalculateDimensions(path)
|
||||
recalculate.recalculateDimensions(rect)
|
||||
const cmdUse = recalculate.recalculateDimensions(useElem)
|
||||
|
||||
return {
|
||||
poly: {
|
||||
points: poly.getAttribute('points'),
|
||||
hasTransform: poly.hasAttribute('transform'),
|
||||
hasCommand: Boolean(cmdPoly)
|
||||
},
|
||||
path: {
|
||||
d: path.getAttribute('d'),
|
||||
transform: path.getAttribute('transform') || '',
|
||||
hasCommand: Boolean(cmdPath)
|
||||
},
|
||||
rect: {
|
||||
x: rect.getAttribute('x'),
|
||||
transform: rect.getAttribute('transform')
|
||||
},
|
||||
useResult: cmdUse
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.poly.hasTransform).toBe(false)
|
||||
expect(result.poly.points).toContain('-5')
|
||||
expect(result.poly.hasCommand).toBe(true)
|
||||
expect(result.path.d.startsWith('M7,8')).toBe(true)
|
||||
expect(result.path.transform).toBe('')
|
||||
expect(result.path.hasCommand).toBe(true)
|
||||
expect(result.rect.x).toBe('1')
|
||||
expect(result.rect.transform).toContain('matrix')
|
||||
expect(result.useResult).toBeNull()
|
||||
})
|
||||
})
|
||||
115
tests/e2e/unit/svgcore-recalculate.spec.js
Normal file
115
tests/e2e/unit/svgcore-recalculate.spec.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core recalculate', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('recalculateDimensions swallows identity and applies translations', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { utilities, coords, recalculate } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
document.body.append(svg)
|
||||
const dataStorage = {
|
||||
store: new WeakMap(),
|
||||
put (el, key, value) {
|
||||
if (!this.store.has(el)) this.store.set(el, new Map())
|
||||
this.store.get(el).set(key, value)
|
||||
},
|
||||
get (el, key) {
|
||||
return this.store.get(el)?.get(key)
|
||||
},
|
||||
has (el, key) {
|
||||
return this.store.has(el) && this.store.get(el).has(key)
|
||||
},
|
||||
remove (el, key) {
|
||||
const bucket = this.store.get(el)
|
||||
if (!bucket) return false
|
||||
const deleted = bucket.delete(key)
|
||||
if (!bucket.size) this.store.delete(el)
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
const initContexts = () => {
|
||||
utilities.init({
|
||||
getSvgRoot: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg,
|
||||
getDataStorage: () => dataStorage
|
||||
})
|
||||
coords.init({
|
||||
getGridSnapping: () => false,
|
||||
getDrawing: () => ({ getNextId: () => '1' }),
|
||||
getDataStorage: () => dataStorage
|
||||
})
|
||||
recalculate.init({
|
||||
getSvgRoot: () => svg,
|
||||
getStartTransform: () => '',
|
||||
setStartTransform: () => {},
|
||||
getDataStorage: () => dataStorage
|
||||
})
|
||||
}
|
||||
initContexts()
|
||||
|
||||
const identityRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
identityRect.setAttribute('x', '10')
|
||||
identityRect.setAttribute('y', '10')
|
||||
identityRect.setAttribute('width', '20')
|
||||
identityRect.setAttribute('height', '30')
|
||||
identityRect.setAttribute('transform', 'matrix(1,0,0,1,0,0)')
|
||||
svg.append(identityRect)
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '200')
|
||||
rect.setAttribute('y', '150')
|
||||
rect.setAttribute('width', '250')
|
||||
rect.setAttribute('height', '120')
|
||||
rect.setAttribute('transform', 'translate(100,50)')
|
||||
svg.append(rect)
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
||||
text.setAttribute('x', '200')
|
||||
text.setAttribute('y', '150')
|
||||
text.setAttribute('transform', 'translate(100,50)')
|
||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan')
|
||||
tspan.setAttribute('x', '200')
|
||||
tspan.setAttribute('y', '150')
|
||||
tspan.textContent = 'Foo bar'
|
||||
text.append(tspan)
|
||||
svg.append(text)
|
||||
|
||||
recalculate.recalculateDimensions(identityRect)
|
||||
recalculate.recalculateDimensions(rect)
|
||||
recalculate.recalculateDimensions(text)
|
||||
|
||||
return {
|
||||
identityHasTransform: identityRect.hasAttribute('transform'),
|
||||
rectAttrs: {
|
||||
x: rect.getAttribute('x'),
|
||||
y: rect.getAttribute('y'),
|
||||
width: rect.getAttribute('width'),
|
||||
height: rect.getAttribute('height')
|
||||
},
|
||||
textAttrs: {
|
||||
x: text.getAttribute('x'),
|
||||
y: text.getAttribute('y')
|
||||
},
|
||||
tspanAttrs: {
|
||||
x: tspan.getAttribute('x'),
|
||||
y: tspan.getAttribute('y')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.identityHasTransform).toBe(false)
|
||||
expect(result.rectAttrs).toEqual({
|
||||
x: '300',
|
||||
y: '200',
|
||||
width: '250',
|
||||
height: '120'
|
||||
})
|
||||
expect(result.textAttrs).toEqual({ x: '300', y: '200' })
|
||||
expect(result.tspanAttrs).toEqual({ x: '300', y: '200' })
|
||||
})
|
||||
})
|
||||
140
tests/e2e/unit/svgcore-remap-extra.spec.js
Normal file
140
tests/e2e/unit/svgcore-remap-extra.spec.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core remap extras', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('remapElement handles gradients, text/tspan and paths with snapping', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const NS = 'http://www.w3.org/2000/svg'
|
||||
const { coords, utilities, units } = window.svgHarness
|
||||
const svg = document.createElementNS(NS, 'svg')
|
||||
const defs = document.createElementNS(NS, 'defs')
|
||||
const grad = document.createElementNS(NS, 'linearGradient')
|
||||
grad.id = 'grad1'
|
||||
grad.setAttribute('x1', '0')
|
||||
grad.setAttribute('x2', '1')
|
||||
grad.setAttribute('y1', '0')
|
||||
grad.setAttribute('y2', '0')
|
||||
defs.append(grad)
|
||||
svg.append(defs)
|
||||
document.body.append(svg)
|
||||
|
||||
const dataStorage = {
|
||||
store: new WeakMap(),
|
||||
put (el, key, value) {
|
||||
if (!this.store.has(el)) this.store.set(el, new Map())
|
||||
this.store.get(el).set(key, value)
|
||||
},
|
||||
get (el, key) {
|
||||
return this.store.get(el)?.get(key)
|
||||
},
|
||||
has (el, key) {
|
||||
return this.store.has(el) && this.store.get(el).has(key)
|
||||
}
|
||||
}
|
||||
let idCounter = 0
|
||||
const canvas = {
|
||||
getSvgRoot: () => svg,
|
||||
getSvgContent: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg,
|
||||
getBaseUnit: () => 'px',
|
||||
getSnappingStep: () => 5,
|
||||
getGridSnapping: () => true,
|
||||
getWidth: () => 200,
|
||||
getHeight: () => 200,
|
||||
getCurrentDrawing: () => ({
|
||||
getNextId: () => 'g' + (++idCounter)
|
||||
}),
|
||||
getDataStorage: () => dataStorage
|
||||
}
|
||||
|
||||
utilities.init(canvas)
|
||||
units.init(canvas)
|
||||
coords.init(canvas)
|
||||
|
||||
const group = document.createElementNS(NS, 'g')
|
||||
svg.append(group)
|
||||
|
||||
const text = document.createElementNS(NS, 'text')
|
||||
text.textContent = 'hello'
|
||||
text.setAttribute('x', '2')
|
||||
text.setAttribute('y', '3')
|
||||
text.setAttribute('font-size', '10')
|
||||
const tspan = document.createElementNS(NS, 'tspan')
|
||||
tspan.setAttribute('x', '4')
|
||||
tspan.setAttribute('y', '5')
|
||||
tspan.setAttribute('font-size', '8')
|
||||
tspan.textContent = 't'
|
||||
text.append(tspan)
|
||||
group.append(text)
|
||||
|
||||
const textMatrix = svg.createSVGMatrix()
|
||||
textMatrix.a = -2
|
||||
textMatrix.d = 1.5
|
||||
textMatrix.e = 10
|
||||
coords.remapElement(text, { x: 2, y: 3 }, textMatrix)
|
||||
|
||||
const rect = document.createElementNS(NS, 'rect')
|
||||
rect.setAttribute('x', '0')
|
||||
rect.setAttribute('y', '0')
|
||||
rect.setAttribute('width', '10')
|
||||
rect.setAttribute('height', '6')
|
||||
rect.setAttribute('fill', 'url(#grad1)')
|
||||
group.append(rect)
|
||||
|
||||
const flipMatrix = svg.createSVGMatrix()
|
||||
flipMatrix.a = -1
|
||||
flipMatrix.d = -1
|
||||
coords.remapElement(rect, { x: 0, y: 0, width: 10, height: 6 }, flipMatrix)
|
||||
|
||||
const path = document.createElementNS(NS, 'path')
|
||||
path.setAttribute('d', 'M0 0 L5 0 l5 5 a2 3 0 0 1 2 2 z')
|
||||
group.append(path)
|
||||
const pathMatrix = svg.createSVGMatrix()
|
||||
pathMatrix.a = 1
|
||||
pathMatrix.d = 2
|
||||
pathMatrix.e = 3
|
||||
pathMatrix.f = -1
|
||||
coords.remapElement(path, {}, pathMatrix)
|
||||
|
||||
return {
|
||||
text: {
|
||||
x: text.getAttribute('x'),
|
||||
y: text.getAttribute('y'),
|
||||
fontSize: text.getAttribute('font-size'),
|
||||
tspanX: tspan.getAttribute('x'),
|
||||
tspanY: tspan.getAttribute('y'),
|
||||
tspanSize: tspan.getAttribute('font-size')
|
||||
},
|
||||
rect: {
|
||||
fill: rect.getAttribute('fill'),
|
||||
width: rect.getAttribute('width'),
|
||||
height: rect.getAttribute('height'),
|
||||
x: rect.getAttribute('x'),
|
||||
y: rect.getAttribute('y')
|
||||
},
|
||||
path: path.getAttribute('d')
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.text).toEqual({
|
||||
x: '5',
|
||||
y: '5',
|
||||
fontSize: '20',
|
||||
tspanX: '2',
|
||||
tspanY: '7.5',
|
||||
tspanSize: '16'
|
||||
})
|
||||
expect(result.rect.fill).toContain('url(#g')
|
||||
expect(result.rect.width).toBe('10')
|
||||
expect(result.rect.height).toBe('5')
|
||||
expect(result.rect.x).toBe('-10')
|
||||
expect(result.rect.y).toBe('-5')
|
||||
expect(result.path.startsWith('M3,')).toBe(true)
|
||||
expect(result.path).toContain('a2,6')
|
||||
})
|
||||
})
|
||||
86
tests/e2e/unit/svgcore-smoke.spec.js
Normal file
86
tests/e2e/unit/svgcore-smoke.spec.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('math basics work with real SVG matrices', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { math } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const m = svg.createSVGMatrix().translate(50, 75)
|
||||
const pt = math.transformPoint(10, 20, m)
|
||||
const isId = math.isIdentity(svg.createSVGMatrix())
|
||||
const box = math.transformBox(0, 0, 10, 20, m)
|
||||
return {
|
||||
pt,
|
||||
isId,
|
||||
box: {
|
||||
x: box.aabox.x,
|
||||
y: box.aabox.y,
|
||||
width: box.aabox.width,
|
||||
height: box.aabox.height
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(result.isId).toBe(true)
|
||||
expect(result.pt.x).toBe(60)
|
||||
expect(result.pt.y).toBe(95)
|
||||
expect(result.box).toEqual({ x: 50, y: 75, width: 10, height: 20 })
|
||||
})
|
||||
|
||||
test('coords module exposes remapElement', async ({ page }) => {
|
||||
const hasRemap = await page.evaluate(() => {
|
||||
return typeof window.svgHarness.coords.remapElement === 'function'
|
||||
})
|
||||
expect(hasRemap).toBe(true)
|
||||
})
|
||||
|
||||
test('path.convertPath converts to relative without throwing', async ({ page }) => {
|
||||
const d = await page.evaluate(() => {
|
||||
const { pathModule, units } = window.svgHarness
|
||||
units.init({
|
||||
getRoundDigits: () => 2,
|
||||
getBaseUnit: () => 'px'
|
||||
})
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute('d', 'M0 0 L10 0 L10 10 Z')
|
||||
svg.append(path)
|
||||
pathModule.convertPath(path, true)
|
||||
return path.getAttribute('d')
|
||||
})
|
||||
expect(d?.toLowerCase()).toContain('m')
|
||||
expect(d?.toLowerCase()).toContain('z')
|
||||
})
|
||||
|
||||
test('utilities getBBoxFromPath returns finite numbers', async ({ page }) => {
|
||||
const bbox = await page.evaluate(() => {
|
||||
const { utilities } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '0')
|
||||
rect.setAttribute('y', '1')
|
||||
rect.setAttribute('width', '5')
|
||||
rect.setAttribute('height', '10')
|
||||
svg.append(rect)
|
||||
const res = utilities.getBBoxOfElementAsPath(
|
||||
rect,
|
||||
(json) => {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', json.element)
|
||||
Object.entries(json.attr).forEach(([k, v]) => el.setAttribute(k, v))
|
||||
svg.append(el)
|
||||
return el
|
||||
},
|
||||
{ resetOrientation: () => {} }
|
||||
)
|
||||
return { x: res.x, y: res.y, width: res.width, height: res.height }
|
||||
})
|
||||
expect(Number.isFinite(bbox.x)).toBe(true)
|
||||
expect(Number.isFinite(bbox.y)).toBe(true)
|
||||
expect(Number.isFinite(bbox.width)).toBe(true)
|
||||
expect(Number.isFinite(bbox.height)).toBe(true)
|
||||
})
|
||||
})
|
||||
83
tests/e2e/unit/svgcore-touch.spec.js
Normal file
83
tests/e2e/unit/svgcore-touch.spec.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('touch event adapter', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('translates single touch events into mouse events', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { touch } = window.svgHarness
|
||||
const target = document.createElement('div')
|
||||
document.body.append(target)
|
||||
|
||||
const received = []
|
||||
target.addEventListener('mousedown', (ev) => {
|
||||
received.push({
|
||||
type: ev.type,
|
||||
clientX: ev.clientX,
|
||||
clientY: ev.clientY,
|
||||
screenX: ev.screenX,
|
||||
screenY: ev.screenY
|
||||
})
|
||||
})
|
||||
|
||||
const svgroot = {
|
||||
listeners: {},
|
||||
addEventListener (type, handler) { this.listeners[type] = handler },
|
||||
dispatchEvent (ev) { this.listeners[ev.type]?.(ev) }
|
||||
}
|
||||
|
||||
touch.init({ svgroot })
|
||||
const ev = new TouchEvent('touchstart', {
|
||||
changedTouches: [
|
||||
new Touch({
|
||||
identifier: 1,
|
||||
target,
|
||||
clientX: 12,
|
||||
clientY: 34,
|
||||
screenX: 56,
|
||||
screenY: 78
|
||||
})
|
||||
]
|
||||
})
|
||||
svgroot.dispatchEvent(ev)
|
||||
return received[0]
|
||||
})
|
||||
|
||||
expect(result.type).toBe('mousedown')
|
||||
expect(result.clientX).toBe(12)
|
||||
expect(result.clientY).toBe(34)
|
||||
expect(result.screenX).toBe(56)
|
||||
expect(result.screenY).toBe(78)
|
||||
})
|
||||
|
||||
test('ignores multi-touch gestures when forwarding', async ({ page }) => {
|
||||
const count = await page.evaluate(() => {
|
||||
const { touch } = window.svgHarness
|
||||
const target = document.createElement('div')
|
||||
document.body.append(target)
|
||||
let mouseEvents = 0
|
||||
target.addEventListener('mousedown', () => { mouseEvents++ })
|
||||
|
||||
const svgroot = {
|
||||
listeners: {},
|
||||
addEventListener (type, handler) { this.listeners[type] = handler },
|
||||
dispatchEvent (ev) { this.listeners[ev.type]?.(ev) }
|
||||
}
|
||||
|
||||
touch.init({ svgroot })
|
||||
const ev = new TouchEvent('touchstart', {
|
||||
changedTouches: [
|
||||
new Touch({ identifier: 1, target, clientX: 1, clientY: 2, screenX: 3, screenY: 4 }),
|
||||
new Touch({ identifier: 2, target, clientX: 5, clientY: 6, screenX: 7, screenY: 8 })
|
||||
]
|
||||
})
|
||||
svgroot.dispatchEvent(ev)
|
||||
return mouseEvents
|
||||
})
|
||||
|
||||
expect(count).toBe(0)
|
||||
})
|
||||
})
|
||||
98
tests/e2e/unit/svgcore-util.spec.js
Normal file
98
tests/e2e/unit/svgcore-util.spec.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG common util helpers', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('computes positions and deep merges objects', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { util } = window.svgHarness
|
||||
const grand = { offsetLeft: 5, offsetTop: 6, offsetParent: null }
|
||||
const parent = { offsetLeft: 10, offsetTop: 11, offsetParent: grand }
|
||||
const child = { offsetLeft: 7, offsetTop: 8, offsetParent: parent }
|
||||
|
||||
const merged = util.mergeDeep(
|
||||
{ a: 1, nested: { keep: true, replace: 'old' } },
|
||||
{ nested: { replace: 'new', extra: 42 }, more: 'yes' }
|
||||
)
|
||||
|
||||
return {
|
||||
pos: util.findPos(child),
|
||||
isObject: util.isObject({ hello: 'world' }),
|
||||
merged
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.pos).toEqual({ left: 22, top: 25 })
|
||||
expect(result.isObject).toBe(true)
|
||||
expect(result.merged).toEqual({
|
||||
a: 1,
|
||||
nested: { keep: true, replace: 'new', extra: 42 },
|
||||
more: 'yes'
|
||||
})
|
||||
})
|
||||
|
||||
test('finds closest ancestors by selector', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { util } = window.svgHarness
|
||||
const root = document.getElementById('root')
|
||||
root.innerHTML = ''
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'wrapper'
|
||||
const section = document.createElement('section')
|
||||
section.id = 'section'
|
||||
const child = document.createElement('span')
|
||||
child.dataset.role = 'target'
|
||||
|
||||
section.append(child)
|
||||
wrapper.append(section)
|
||||
root.append(wrapper)
|
||||
|
||||
return {
|
||||
byClass: util.getClosest(child, '.wrapper')?.className,
|
||||
byId: util.getClosest(child, '#section')?.id,
|
||||
byData: util.getClosest(child, '[data-role=target]')?.dataset.role,
|
||||
byTag: util.getClosest(child, 'div')?.tagName.toLowerCase()
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.byClass).toBe('wrapper')
|
||||
expect(result.byId).toBe('section')
|
||||
expect(result.byData).toBe('target')
|
||||
expect(result.byTag).toBe('div')
|
||||
})
|
||||
|
||||
test('gathers parents with and without limits', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { util } = window.svgHarness
|
||||
const root = document.getElementById('root')
|
||||
root.innerHTML = ''
|
||||
|
||||
const outer = document.createElement('div')
|
||||
outer.className = 'outer'
|
||||
const mid = document.createElement('section')
|
||||
mid.id = 'mid'
|
||||
const inner = document.createElement('span')
|
||||
inner.className = 'inner'
|
||||
|
||||
mid.append(inner)
|
||||
outer.append(mid)
|
||||
root.append(outer)
|
||||
|
||||
return {
|
||||
all: util.getParents(inner)?.map((el) => el.tagName.toLowerCase()),
|
||||
byClass: util.getParents(inner, '.outer')?.map((el) => el.className),
|
||||
untilMid: util.getParentsUntil(inner, '#mid')?.map((el) => el.tagName.toLowerCase()),
|
||||
untilMidFiltered: util.getParentsUntil(inner, '#mid', '.inner')?.map((el) => el.tagName.toLowerCase())
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.all).toEqual(['span', 'section', 'div', 'div', 'body', 'html'])
|
||||
expect(result.byClass).toEqual(['outer'])
|
||||
expect(result.untilMid).toEqual(['span'])
|
||||
expect(result.untilMidFiltered).toEqual(['span'])
|
||||
})
|
||||
})
|
||||
115
tests/e2e/unit/svgcore-utilities-extra.spec.js
Normal file
115
tests/e2e/unit/svgcore-utilities-extra.spec.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core utilities extra coverage', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('exercises helper paths and bbox utilities', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
try {
|
||||
const { utilities, namespaces } = window.svgHarness
|
||||
const root = document.getElementById('root')
|
||||
root.innerHTML = `
|
||||
<svg id="svgroot" xmlns="${namespaces.NS.SVG}" width="10" height="10">
|
||||
<defs id="defs"></defs>
|
||||
<g id="group">
|
||||
<rect id="rect" x="1" y="2" width="3" height="4"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
const svg = root.querySelector('svg')
|
||||
const rect = svg.querySelector('#rect')
|
||||
|
||||
utilities.init({
|
||||
getSvgRoot: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => root,
|
||||
getSvgContent: () => svg,
|
||||
getSelectedElements: () => [rect],
|
||||
getBaseUnit: () => 'px',
|
||||
getSnappingStep: () => 1,
|
||||
addSVGElementsFromJson: () => null,
|
||||
pathActions: { convertPath: () => {} }
|
||||
})
|
||||
|
||||
const errors = []
|
||||
const safe = (fn, fallback = null) => {
|
||||
try { return fn() } catch (e) { errors.push(e.message); return fallback }
|
||||
}
|
||||
|
||||
const encoded = safe(() => utilities.encodeUTF8('hello'))
|
||||
const dropped = safe(() => utilities.dropXMLInternalSubset('<!DOCTYPE svg [<!ENTITY a "b">]?><svg/>'), '')
|
||||
const xmlRefs = safe(() => utilities.convertToXMLReferences('<>"&'), '')
|
||||
const parsed = safe(() => utilities.text2xml('<svg><g></g></svg>'))
|
||||
const bboxObj = safe(() => utilities.bboxToObj({ x: 1, y: 2, width: 3, height: 4 }))
|
||||
const defs = safe(() => utilities.findDefs())
|
||||
const bbox = safe(() => utilities.getBBox(rect))
|
||||
const pathD = safe(() => utilities.getPathDFromElement(rect), '')
|
||||
const extra = safe(() => utilities.getExtraAttributesForConvertToPath(rect), {})
|
||||
const pathBBox = safe(() => utilities.getBBoxOfElementAsPath(
|
||||
rect,
|
||||
({ element, attr }) => {
|
||||
const node = document.createElementNS(namespaces.NS.SVG, element)
|
||||
Object.entries(attr).forEach(([key, value]) => node.setAttribute(key, value))
|
||||
svg.querySelector('#group').append(node)
|
||||
return node
|
||||
},
|
||||
{ resetOrientation: () => {} }
|
||||
), { width: 0, height: 0, x: 0, y: 0 })
|
||||
const rotated = safe(() => utilities.getRotationAngleFromTransformList(svg.transform.baseVal, true), 0)
|
||||
const refElem = safe(() => utilities.getRefElem('#rect'))
|
||||
const fe = safe(() => utilities.getFeGaussianBlur(svg), null)
|
||||
const elementById = safe(() => utilities.getElement('rect'))
|
||||
safe(() => utilities.assignAttributes(elementById, { 'data-test': 'ok' }))
|
||||
safe(() => utilities.cleanupElement(elementById))
|
||||
const snapped = safe(() => utilities.snapToGrid(2.6), 0)
|
||||
const htmlFrag = safe(() => utilities.stringToHTML('<span class="x">hi</span>'))
|
||||
const insertTarget = document.createElement('div')
|
||||
safe(() => utilities.insertChildAtIndex(root, insertTarget, 0))
|
||||
|
||||
return {
|
||||
encoded: Boolean(encoded),
|
||||
dropped,
|
||||
xmlRefs,
|
||||
parsedTag: parsed?.documentElement?.tagName?.toLowerCase() || '',
|
||||
bboxObj,
|
||||
defsId: defs?.id,
|
||||
bbox,
|
||||
pathD,
|
||||
extraKeys: Object.keys(extra).length,
|
||||
pathBBox,
|
||||
rotated,
|
||||
refId: refElem?.id,
|
||||
feFound: fe === null,
|
||||
dataAttr: elementById?.dataset?.test || null,
|
||||
snapped,
|
||||
htmlTag: (htmlFrag &&
|
||||
htmlFrag.firstChild &&
|
||||
htmlFrag.firstChild.tagName &&
|
||||
htmlFrag.firstChild.tagName.toLowerCase()) || '',
|
||||
insertedFirst: root.firstChild === insertTarget,
|
||||
errors,
|
||||
failed: false
|
||||
}
|
||||
} catch (error) {
|
||||
return { failed: true, message: error.message, stack: error.stack }
|
||||
}
|
||||
})
|
||||
|
||||
if (result.failed) {
|
||||
throw new Error(result.message || 'utilities extra coverage failed')
|
||||
}
|
||||
expect(result.dropped.includes('?>')).toBe(true)
|
||||
expect(result.xmlRefs).toBeTruthy()
|
||||
expect(result.parsedTag).toBe('svg')
|
||||
expect(result.bboxObj).toEqual({ x: 1, y: 2, width: 3, height: 4 })
|
||||
expect(result.bbox).toBeDefined()
|
||||
expect(result.pathD).toContain('M1')
|
||||
expect(Number(result.pathBBox?.width ?? 0)).toBeGreaterThanOrEqual(0)
|
||||
expect(result.dataAttr).toBe('ok')
|
||||
expect(result.snapped).toBeCloseTo(3)
|
||||
expect(result.insertedFirst).toBeDefined()
|
||||
})
|
||||
})
|
||||
149
tests/e2e/unit/svgcore-utilities.spec.js
Normal file
149
tests/e2e/unit/svgcore-utilities.spec.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core utilities', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('units.convertAttrs appends base unit', async ({ page }) => {
|
||||
const attrs = await page.evaluate(() => {
|
||||
const { units } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '10')
|
||||
rect.setAttribute('y', '20')
|
||||
rect.setAttribute('width', '30')
|
||||
rect.setAttribute('height', '40')
|
||||
svg.append(rect)
|
||||
units.init({
|
||||
getRoundDigits: () => 2,
|
||||
getBaseUnit: () => 'cm'
|
||||
})
|
||||
units.convertAttrs(rect)
|
||||
return {
|
||||
x: rect.getAttribute('x'),
|
||||
y: rect.getAttribute('y'),
|
||||
width: rect.getAttribute('width'),
|
||||
height: rect.getAttribute('height')
|
||||
}
|
||||
})
|
||||
expect(attrs.x?.endsWith('cm')).toBe(true)
|
||||
expect(attrs.y?.endsWith('cm')).toBe(true)
|
||||
expect(attrs.width?.endsWith('cm')).toBe(true)
|
||||
expect(attrs.height?.endsWith('cm')).toBe(true)
|
||||
})
|
||||
|
||||
test('utilities.getStrokedBBox returns finite numbers', async ({ page }) => {
|
||||
const bbox = await page.evaluate(() => {
|
||||
const { utilities, units } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('width', '200')
|
||||
svg.setAttribute('height', '200')
|
||||
document.body.append(svg)
|
||||
units.init({
|
||||
getRoundDigits: () => 2,
|
||||
getBaseUnit: () => 'px'
|
||||
})
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '10')
|
||||
rect.setAttribute('y', '20')
|
||||
rect.setAttribute('width', '30')
|
||||
rect.setAttribute('height', '40')
|
||||
rect.setAttribute('stroke', '#000')
|
||||
rect.setAttribute('stroke-width', '10')
|
||||
svg.append(rect)
|
||||
const addSvg = (json) => {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', json.element)
|
||||
Object.entries(json.attr).forEach(([k, v]) => el.setAttribute(k, v))
|
||||
svg.append(el)
|
||||
return el
|
||||
}
|
||||
const res = utilities.getStrokedBBox([rect], addSvg, { resetOrientation: () => {} })
|
||||
return { x: res.x, y: res.y, width: res.width, height: res.height }
|
||||
})
|
||||
expect(Number.isFinite(bbox.x)).toBe(true)
|
||||
expect(Number.isFinite(bbox.y)).toBe(true)
|
||||
expect(Number.isFinite(bbox.width)).toBe(true)
|
||||
expect(Number.isFinite(bbox.height)).toBe(true)
|
||||
})
|
||||
|
||||
test('utilities XML and base64 helpers escape and roundtrip correctly', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { utilities } = window.svgHarness
|
||||
return {
|
||||
escaped: utilities.toXml("PB&J '\"<>"),
|
||||
encoded: utilities.encode64('abcdef'),
|
||||
decoded: utilities.decode64('MTIzNDU='),
|
||||
xmlRefs: utilities.convertToXMLReferences('ABC')
|
||||
}
|
||||
})
|
||||
expect(result.escaped).toBe('PB&J '"<>')
|
||||
expect(result.encoded).toBe('YWJjZGVm')
|
||||
expect(result.decoded).toBe('12345')
|
||||
expect(result.xmlRefs).toBe('ABC')
|
||||
})
|
||||
|
||||
test('utilities.getPathDFromSegments and getPathDFromElement build expected d strings', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { utilities } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
document.body.append(svg)
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '0')
|
||||
rect.setAttribute('y', '1')
|
||||
rect.setAttribute('width', '5')
|
||||
rect.setAttribute('height', '10')
|
||||
svg.append(rect)
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
|
||||
line.setAttribute('x1', '0')
|
||||
line.setAttribute('y1', '1')
|
||||
line.setAttribute('x2', '5')
|
||||
line.setAttribute('y2', '6')
|
||||
svg.append(line)
|
||||
const dSegments = utilities.getPathDFromSegments([
|
||||
['M', [1, 2]],
|
||||
['Z', []]
|
||||
])
|
||||
return {
|
||||
segments: dSegments.trim(),
|
||||
rect: utilities.getPathDFromElement(rect),
|
||||
line: utilities.getPathDFromElement(line)
|
||||
}
|
||||
})
|
||||
expect(result.segments).toBe('M1,2 Z')
|
||||
expect(result.rect).toBe('M0,1 L5,1 L5,11 L0,11 L0,1 Z')
|
||||
expect(result.line).toBe('M0,1L5,6')
|
||||
})
|
||||
|
||||
test('utilities.getBBoxOfElementAsPath mirrors element geometry', async ({ page }) => {
|
||||
const bbox = await page.evaluate(() => {
|
||||
const { utilities } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
document.body.append(svg)
|
||||
const create = (tag, attrs) => {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', tag)
|
||||
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v))
|
||||
return el
|
||||
}
|
||||
const addSvg = (json) => {
|
||||
const el = create(json.element, json.attr)
|
||||
svg.append(el)
|
||||
return el
|
||||
}
|
||||
const pathActions = { resetOrientation: () => {} }
|
||||
const path = create('path', { id: 'p', d: 'M0,1 Z' })
|
||||
const rect = create('rect', { id: 'r', x: '0', y: '1', width: '5', height: '10' })
|
||||
const line = create('line', { id: 'l', x1: '0', y1: '1', x2: '5', y2: '6' })
|
||||
svg.append(path, rect, line)
|
||||
return {
|
||||
path: utilities.bboxToObj(utilities.getBBoxOfElementAsPath(path, addSvg, pathActions)),
|
||||
rect: utilities.bboxToObj(utilities.getBBoxOfElementAsPath(rect, addSvg, pathActions)),
|
||||
line: utilities.bboxToObj(utilities.getBBoxOfElementAsPath(line, addSvg, pathActions))
|
||||
}
|
||||
})
|
||||
expect(bbox.path).toEqual({ x: 0, y: 1, width: 0, height: 0 })
|
||||
expect(bbox.rect).toEqual({ x: 0, y: 1, width: 5, height: 10 })
|
||||
expect(bbox.line).toEqual({ x: 0, y: 1, width: 5, height: 5 })
|
||||
})
|
||||
})
|
||||
158
tests/e2e/unit/svgcore.spec.js
Normal file
158
tests/e2e/unit/svgcore.spec.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { test, expect } from '../fixtures.js'
|
||||
|
||||
test.describe('SVG core modules in browser', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/tests/unit-harness.html')
|
||||
await page.waitForFunction(() => Boolean(window.svgHarness))
|
||||
})
|
||||
|
||||
test('units.convertUnit returns finite and px passthrough', async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const { units } = window.svgHarness
|
||||
units.init({
|
||||
getRoundDigits: () => 2,
|
||||
getBaseUnit: () => 'px'
|
||||
})
|
||||
return {
|
||||
defaultConv: units.convertUnit(42),
|
||||
pxConv: units.convertUnit(42, 'px')
|
||||
}
|
||||
})
|
||||
expect(result.defaultConv).toBeGreaterThan(0)
|
||||
expect(result.defaultConv).not.toBe(Infinity)
|
||||
expect(result.pxConv).toBe(42)
|
||||
})
|
||||
|
||||
test('units.shortFloat and isValidUnit mirror legacy behavior', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { units } = window.svgHarness
|
||||
document.body.innerHTML = ''
|
||||
const unique = document.createElement('div')
|
||||
unique.id = 'uniqueId'
|
||||
const other = document.createElement('div')
|
||||
other.id = 'otherId'
|
||||
document.body.append(unique, other)
|
||||
units.init({
|
||||
getBaseUnit: () => 'cm',
|
||||
getHeight: () => 600,
|
||||
getWidth: () => 800,
|
||||
getRoundDigits: () => 4,
|
||||
getElement: (id) => document.getElementById(id)
|
||||
})
|
||||
return {
|
||||
shortFloat: [
|
||||
units.shortFloat(0.00000001),
|
||||
units.shortFloat(1),
|
||||
units.shortFloat(3.45678),
|
||||
units.shortFloat(1.23443),
|
||||
units.shortFloat(1.23455)
|
||||
],
|
||||
validUnits: [
|
||||
'0',
|
||||
'1',
|
||||
'1.1',
|
||||
'-1.1',
|
||||
'.6mm',
|
||||
'-.6cm',
|
||||
'6000in',
|
||||
'6px',
|
||||
'6.3pc',
|
||||
'-0.4em',
|
||||
'-0.ex',
|
||||
'40.123%'
|
||||
].map((val) => units.isValidUnit(val)),
|
||||
idChecks: {
|
||||
okExisting: units.isValidUnit('id', 'uniqueId', unique),
|
||||
okNew: units.isValidUnit('id', 'newId', unique),
|
||||
dupNoElem: units.isValidUnit('id', 'uniqueId'),
|
||||
dupOther: units.isValidUnit('id', 'uniqueId', other)
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(result.shortFloat).toEqual([0, 1, 3.4568, 1.2344, 1.2346])
|
||||
result.validUnits.forEach((isValid) => expect(isValid).toBe(true))
|
||||
expect(result.idChecks.okExisting).toBe(true)
|
||||
expect(result.idChecks.okNew).toBe(true)
|
||||
expect(result.idChecks.dupNoElem).toBe(false)
|
||||
expect(result.idChecks.dupOther).toBe(false)
|
||||
})
|
||||
|
||||
test('utilities.getPathDFromElement on rect', async ({ page }) => {
|
||||
const pathD = await page.evaluate(() => {
|
||||
const { utilities } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '0')
|
||||
rect.setAttribute('y', '1')
|
||||
rect.setAttribute('width', '5')
|
||||
rect.setAttribute('height', '10')
|
||||
svg.append(rect)
|
||||
return utilities.getPathDFromElement(rect)
|
||||
})
|
||||
expect(pathD?.startsWith('M')).toBe(true)
|
||||
})
|
||||
|
||||
test('utilities.getBBoxOfElementAsPath returns numbers', async ({ page }) => {
|
||||
const bbox = await page.evaluate(() => {
|
||||
const { utilities } = window.svgHarness
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('x', '0')
|
||||
rect.setAttribute('y', '1')
|
||||
rect.setAttribute('width', '2')
|
||||
rect.setAttribute('height', '2')
|
||||
svg.append(rect)
|
||||
// Minimal mocks for addSVGElementsFromJson and pathActions.resetOrientation
|
||||
const addSvg = (json) => {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', json.element)
|
||||
Object.entries(json.attr).forEach(([k, v]) => el.setAttribute(k, v))
|
||||
svg.append(el)
|
||||
return el
|
||||
}
|
||||
const pathActions = { resetOrientation: () => {} }
|
||||
const res = utilities.getBBoxOfElementAsPath(rect, addSvg, pathActions)
|
||||
return { x: res.x, y: res.y, width: res.width, height: res.height }
|
||||
})
|
||||
expect(Number.isFinite(bbox.x)).toBe(true)
|
||||
expect(Number.isFinite(bbox.y)).toBe(true)
|
||||
expect(Number.isFinite(bbox.width)).toBe(true)
|
||||
expect(Number.isFinite(bbox.height)).toBe(true)
|
||||
})
|
||||
|
||||
test('path.convertPath converts absolute to relative', async ({ page }) => {
|
||||
const dRel = await page.evaluate(() => {
|
||||
const { pathModule, units } = window.svgHarness
|
||||
units.init({
|
||||
getRoundDigits: () => 2,
|
||||
getBaseUnit: () => 'px'
|
||||
})
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute('d', 'M0 0 L10 0 L10 10 Z')
|
||||
svg.append(path)
|
||||
pathModule.convertPath(path, true)
|
||||
return path.getAttribute('d')
|
||||
})
|
||||
expect(dRel?.length > 0).toBe(true)
|
||||
expect(dRel.toLowerCase()).toContain('z')
|
||||
})
|
||||
|
||||
test('path.convertPath normalizes relative and absolute commands', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const { pathModule, units } = window.svgHarness
|
||||
units.init({
|
||||
getRoundDigits: () => 5,
|
||||
getBaseUnit: () => 'px'
|
||||
})
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute('d', 'm40,55h20v20')
|
||||
svg.append(path)
|
||||
const abs = pathModule.convertPath(path)
|
||||
const rel = pathModule.convertPath(path, true)
|
||||
return { abs, rel }
|
||||
})
|
||||
expect(result.abs).toBe('M40,55L60,55L60,75')
|
||||
expect(result.rel).toBe('m40,55l20,0l0,20')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,32 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { afterEach, describe, expect, test } from 'vitest'
|
||||
import { putLocale, t } from '../src/editor/locale.js'
|
||||
|
||||
const goodLangs = ['en', 'fr', 'de']
|
||||
const originalNavigator = {
|
||||
userLanguage: navigator.userLanguage,
|
||||
language: navigator.language
|
||||
}
|
||||
|
||||
const setNavigatorProp = (prop, value) => {
|
||||
Object.defineProperty(navigator, prop, {
|
||||
value,
|
||||
configurable: true,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
|
||||
const restoreNavigatorProp = (prop, value) => {
|
||||
if (value === undefined) {
|
||||
Reflect.deleteProperty(navigator, prop)
|
||||
return
|
||||
}
|
||||
setNavigatorProp(prop, value)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreNavigatorProp('userLanguage', originalNavigator.userLanguage)
|
||||
restoreNavigatorProp('language', originalNavigator.language)
|
||||
})
|
||||
|
||||
describe('locale loader', () => {
|
||||
test('falls back to English when lang is not supported', async () => {
|
||||
@@ -16,4 +41,22 @@ describe('locale loader', () => {
|
||||
expect(t('common.ok')).toBe('OK')
|
||||
expect(t('misc.powered_by')).toBe('Powered by')
|
||||
})
|
||||
|
||||
test('uses navigator.userLanguage when available', async () => {
|
||||
setNavigatorProp('userLanguage', 'fr')
|
||||
setNavigatorProp('language', 'en-US')
|
||||
|
||||
const result = await putLocale('', goodLangs)
|
||||
expect(result.langParam).toBe('fr')
|
||||
expect(t('common.ok')).toBe('OK')
|
||||
})
|
||||
|
||||
test('uses navigator.language and still falls back to English for unsupported locale', async () => {
|
||||
Reflect.deleteProperty(navigator, 'userLanguage')
|
||||
setNavigatorProp('language', 'pt-BR')
|
||||
|
||||
const result = await putLocale('', goodLangs)
|
||||
expect(result.langParam).toBe('en')
|
||||
expect(t('common.ok')).toBe('OK')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { strict as assert } from 'node:assert'
|
||||
|
||||
describe('Browser bugs', function () {
|
||||
it('removeItem and setAttribute test (Chromium 843901; now fixed)', function () {
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=843901
|
||||
|
||||
49
tests/unit/clear.test.js
Normal file
49
tests/unit/clear.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { clearSvgContentElementInit, init as initClear } from '../../packages/svgcanvas/core/clear.js'
|
||||
|
||||
const buildCanvas = (showOutside = false) => {
|
||||
const svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svgContent.append(document.createElementNS('http://www.w3.org/2000/svg', 'g'))
|
||||
const svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
const curConfig = { dimensions: [300, 150], show_outside_canvas: showOutside }
|
||||
|
||||
return {
|
||||
svgContent,
|
||||
svgRoot,
|
||||
curConfig,
|
||||
canvas: {
|
||||
getCurConfig: () => curConfig,
|
||||
getSvgContent: () => svgContent,
|
||||
getSvgRoot: () => svgRoot,
|
||||
getDOMDocument: () => document
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('clearSvgContentElementInit', () => {
|
||||
it('clears existing children and sets canvas attributes', () => {
|
||||
const { canvas, svgContent, svgRoot } = buildCanvas(false)
|
||||
initClear(canvas)
|
||||
|
||||
clearSvgContentElementInit()
|
||||
|
||||
expect(svgRoot.contains(svgContent)).toBe(true)
|
||||
expect(svgContent.childNodes[0].nodeType).toBe(Node.COMMENT_NODE)
|
||||
expect(svgContent.getAttribute('id')).toBe('svgcontent')
|
||||
expect(svgContent.getAttribute('width')).toBe('300')
|
||||
expect(svgContent.getAttribute('height')).toBe('150')
|
||||
expect(svgContent.getAttribute('x')).toBe('300')
|
||||
expect(svgContent.getAttribute('y')).toBe('150')
|
||||
expect(svgContent.getAttribute('overflow')).toBe('hidden')
|
||||
expect(svgContent.getAttribute('xmlns')).toBe('http://www.w3.org/2000/svg')
|
||||
})
|
||||
|
||||
it('honors show_outside_canvas by leaving overflow visible', () => {
|
||||
const { canvas, svgContent } = buildCanvas(true)
|
||||
initClear(canvas)
|
||||
|
||||
clearSvgContentElementInit()
|
||||
|
||||
expect(svgContent.getAttribute('overflow')).toBe('visible')
|
||||
})
|
||||
})
|
||||
44
tests/unit/configobj.test.js
Normal file
44
tests/unit/configobj.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import ConfigObj, { regexEscape } from '../../src/editor/ConfigObj.js'
|
||||
|
||||
describe('ConfigObj', () => {
|
||||
const stubEditor = () => ({
|
||||
storage: {
|
||||
map: new Map(),
|
||||
getItem (k) { return this.map.get(k) },
|
||||
setItem (k, v) { this.map.set(k, v) }
|
||||
},
|
||||
loadFromDataURI: () => { stubEditor.loaded = 'data' },
|
||||
loadFromString: () => { stubEditor.loaded = 'string' },
|
||||
loadFromURL: () => { stubEditor.loaded = 'url' }
|
||||
})
|
||||
|
||||
it('escapes regex characters', () => {
|
||||
expect(regexEscape('a+b?')).toBe('a\\+b\\?')
|
||||
})
|
||||
|
||||
it('merges defaults and respects allowInitialUserOverride', () => {
|
||||
const editor = stubEditor()
|
||||
const cfg = new ConfigObj(editor)
|
||||
cfg.setConfig({ gridSnapping: true, userExtensions: ['custom'] })
|
||||
cfg.setupCurConfig()
|
||||
|
||||
expect(cfg.curConfig.gridSnapping).toBe(true)
|
||||
expect(cfg.curConfig.extensions).toContain('ext-grid')
|
||||
expect(cfg.curConfig.extensions.includes('custom') || cfg.curConfig.userExtensions.includes('custom')).toBe(true)
|
||||
|
||||
cfg.setConfig({ lang: 'fr' }, { allowInitialUserOverride: true })
|
||||
expect(cfg.defaultPrefs.lang).toBe('fr')
|
||||
})
|
||||
|
||||
it('prefers existing values when overwrite is false', () => {
|
||||
const editor = stubEditor()
|
||||
const cfg = new ConfigObj(editor)
|
||||
cfg.curConfig.preventAllURLConfig = true
|
||||
cfg.curPrefs.lang = 'es'
|
||||
|
||||
cfg.setConfig({ lang: 'de', gridColor: '#fff', extensions: ['x'] }, { overwrite: false })
|
||||
expect(cfg.curPrefs.lang).toBe('es')
|
||||
expect(cfg.curConfig.gridColor).toBeUndefined()
|
||||
expect(cfg.curConfig.extensions).toEqual([])
|
||||
})
|
||||
})
|
||||
44
tests/unit/contextmenu-extra.test.js
Normal file
44
tests/unit/contextmenu-extra.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
add,
|
||||
getCustomHandler,
|
||||
hasCustomHandler,
|
||||
injectExtendedContextMenuItemsIntoDom,
|
||||
resetCustomMenus
|
||||
} from '../../src/editor/contextmenu.js'
|
||||
|
||||
describe('contextmenu helpers', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "<ul id='cmenu_canvas'></ul>"
|
||||
resetCustomMenus()
|
||||
})
|
||||
|
||||
it('validates menu entries and prevents duplicates', () => {
|
||||
expect(() => add(null)).toThrow(/must be defined/)
|
||||
add({ id: 'foo', label: 'Foo', action: () => 'ok' })
|
||||
expect(hasCustomHandler('foo')).toBe(true)
|
||||
expect(getCustomHandler('foo')()).toBe('ok')
|
||||
expect(() =>
|
||||
add({ id: 'foo', label: 'Again', action: () => {} })
|
||||
).toThrow(/already exists/)
|
||||
})
|
||||
|
||||
it('injects extensions into the context menu DOM', () => {
|
||||
const host = document.getElementById('cmenu_canvas')
|
||||
const appended = []
|
||||
host.appendChild = vi.fn((value) => {
|
||||
appended.push(value)
|
||||
return value
|
||||
})
|
||||
add({ id: 'alpha', label: 'Alpha', action: () => {}, shortcut: 'Ctrl+A' })
|
||||
add({ id: 'beta', label: 'Beta', action: () => {} })
|
||||
|
||||
injectExtendedContextMenuItemsIntoDom()
|
||||
|
||||
expect(host.appendChild).toHaveBeenCalledTimes(2)
|
||||
expect(appended[0]).toContain('#alpha')
|
||||
expect(appended[0]).toContain('Ctrl+A')
|
||||
expect(appended[1]).toContain('#beta')
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import * as coords from '../../packages/svgcanvas/core/coords.js'
|
||||
|
||||
describe('coords', function () {
|
||||
let elemId = 1
|
||||
|
||||
let svg
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
root.style.visibility = 'hidden'
|
||||
@@ -18,8 +18,8 @@ describe('coords', function () {
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.id = 'svgroot'
|
||||
root.append(svgroot)
|
||||
this.svg = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.append(this.svg)
|
||||
svg = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.append(svg)
|
||||
|
||||
// Mock out editor context.
|
||||
utilities.init(
|
||||
@@ -27,7 +27,7 @@ describe('coords', function () {
|
||||
* @implements {module:utilities.EditorContext}
|
||||
*/
|
||||
{
|
||||
getSvgRoot: () => { return this.svg },
|
||||
getSvgRoot: () => { return svg },
|
||||
getDOMDocument () { return null },
|
||||
getDOMContainer () { return null }
|
||||
}
|
||||
@@ -52,8 +52,8 @@ describe('coords', function () {
|
||||
* @returns {void}
|
||||
*/
|
||||
afterEach(function () {
|
||||
while (this.svg.hasChildNodes()) {
|
||||
this.svg.firstChild.remove()
|
||||
while (svg?.hasChildNodes()) {
|
||||
svg.firstChild.remove()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('coords', function () {
|
||||
rect.setAttribute('y', '150')
|
||||
rect.setAttribute('width', '250')
|
||||
rect.setAttribute('height', '120')
|
||||
this.svg.append(rect)
|
||||
svg.append(rect)
|
||||
|
||||
const attrs = {
|
||||
x: '200',
|
||||
@@ -73,7 +73,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
@@ -90,7 +90,7 @@ describe('coords', function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
rect.setAttribute('width', '250')
|
||||
rect.setAttribute('height', '120')
|
||||
this.svg.append(rect)
|
||||
svg.append(rect)
|
||||
|
||||
const attrs = {
|
||||
x: '0',
|
||||
@@ -100,7 +100,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
@@ -118,7 +118,7 @@ describe('coords', function () {
|
||||
circle.setAttribute('cx', '200')
|
||||
circle.setAttribute('cy', '150')
|
||||
circle.setAttribute('r', '125')
|
||||
this.svg.append(circle)
|
||||
svg.append(circle)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
@@ -127,7 +127,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
@@ -144,7 +144,7 @@ describe('coords', function () {
|
||||
circle.setAttribute('cx', '200')
|
||||
circle.setAttribute('cy', '150')
|
||||
circle.setAttribute('r', '250')
|
||||
this.svg.append(circle)
|
||||
svg.append(circle)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
@@ -153,7 +153,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
@@ -172,7 +172,7 @@ describe('coords', function () {
|
||||
ellipse.setAttribute('cy', '150')
|
||||
ellipse.setAttribute('rx', '125')
|
||||
ellipse.setAttribute('ry', '75')
|
||||
this.svg.append(ellipse)
|
||||
svg.append(ellipse)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
@@ -182,7 +182,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
@@ -201,7 +201,7 @@ describe('coords', function () {
|
||||
ellipse.setAttribute('cy', '150')
|
||||
ellipse.setAttribute('rx', '250')
|
||||
ellipse.setAttribute('ry', '120')
|
||||
this.svg.append(ellipse)
|
||||
svg.append(ellipse)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
@@ -211,7 +211,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
@@ -230,7 +230,7 @@ describe('coords', function () {
|
||||
line.setAttribute('y1', '100')
|
||||
line.setAttribute('x2', '120')
|
||||
line.setAttribute('y2', '200')
|
||||
this.svg.append(line)
|
||||
svg.append(line)
|
||||
|
||||
const attrs = {
|
||||
x1: '50',
|
||||
@@ -240,7 +240,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
@@ -259,7 +259,7 @@ describe('coords', function () {
|
||||
line.setAttribute('y1', '100')
|
||||
line.setAttribute('x2', '120')
|
||||
line.setAttribute('y2', '200')
|
||||
this.svg.append(line)
|
||||
svg.append(line)
|
||||
|
||||
const attrs = {
|
||||
x1: '50',
|
||||
@@ -269,7 +269,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
@@ -286,7 +286,7 @@ describe('coords', function () {
|
||||
const text = document.createElementNS(NS.SVG, 'text')
|
||||
text.setAttribute('x', '50')
|
||||
text.setAttribute('y', '100')
|
||||
this.svg.append(text)
|
||||
svg.append(text)
|
||||
|
||||
const attrs = {
|
||||
x: '50',
|
||||
@@ -294,7 +294,7 @@ describe('coords', function () {
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
|
||||
34
tests/unit/dataStorage.test.js
Normal file
34
tests/unit/dataStorage.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import dataStorage from '../../packages/svgcanvas/core/dataStorage.js'
|
||||
|
||||
describe('dataStorage', () => {
|
||||
it('stores, checks and retrieves keyed values per element', () => {
|
||||
const el1 = document.createElement('div')
|
||||
const el2 = document.createElement('div')
|
||||
|
||||
dataStorage.put(el1, 'color', 'red')
|
||||
dataStorage.put(el1, 'count', 3)
|
||||
dataStorage.put(el2, 'color', 'blue')
|
||||
|
||||
expect(dataStorage.has(el1, 'color')).toBe(true)
|
||||
expect(dataStorage.has(el1, 'missing')).toBe(false)
|
||||
expect(dataStorage.get(el1, 'color')).toBe('red')
|
||||
expect(dataStorage.get(el1, 'count')).toBe(3)
|
||||
expect(dataStorage.get(el2, 'color')).toBe('blue')
|
||||
})
|
||||
|
||||
it('removes values and cleans up empty element maps', () => {
|
||||
const el = document.createElement('span')
|
||||
dataStorage.put(el, 'foo', 1)
|
||||
dataStorage.put(el, 'bar', 2)
|
||||
|
||||
expect(dataStorage.remove(el, 'foo')).toBe(true)
|
||||
expect(dataStorage.has(el, 'foo')).toBe(false)
|
||||
expect(dataStorage.get(el, 'bar')).toBe(2)
|
||||
|
||||
// Removing the last key should drop the element from storage entirely.
|
||||
expect(dataStorage.remove(el, 'bar')).toBe(true)
|
||||
expect(dataStorage.has(el, 'bar')).toBe(false)
|
||||
expect(dataStorage.get(el, 'bar')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,10 @@ describe('draw.Drawing', function () {
|
||||
const addOwnSpies = (obj) => {
|
||||
const methods = Object.keys(obj)
|
||||
methods.forEach((method) => {
|
||||
vi.spyOn(obj, method)
|
||||
const spy = vi.spyOn(obj, method)
|
||||
spy.getCall = (idx = 0) => ({ args: spy.mock.calls[idx] || [] })
|
||||
Object.defineProperty(spy, 'calledOnce', { get: () => spy.mock.calls.length === 1 })
|
||||
Object.defineProperty(spy, 'callCount', { get: () => spy.mock.calls.length })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ describe('history', function () {
|
||||
|
||||
// const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
let undoMgr = null
|
||||
let divparent
|
||||
let div1
|
||||
let div2
|
||||
let div3
|
||||
let div4
|
||||
let div5
|
||||
let div
|
||||
|
||||
class MockCommand extends history.Command {
|
||||
constructor (optText) {
|
||||
@@ -45,23 +52,23 @@ describe('history', function () {
|
||||
undoMgr = new history.UndoManager()
|
||||
|
||||
document.body.textContent = ''
|
||||
this.divparent = document.createElement('div')
|
||||
this.divparent.id = 'divparent'
|
||||
this.divparent.style.visibility = 'hidden'
|
||||
divparent = document.createElement('div')
|
||||
divparent.id = 'divparent'
|
||||
divparent.style.visibility = 'hidden'
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const div = document.createElement('div')
|
||||
const id = `div${i}`
|
||||
div.id = id
|
||||
this[id] = div
|
||||
}
|
||||
div1 = document.createElement('div'); div1.id = 'div1'
|
||||
div2 = document.createElement('div'); div2.id = 'div2'
|
||||
div3 = document.createElement('div'); div3.id = 'div3'
|
||||
div4 = document.createElement('div'); div4.id = 'div4'
|
||||
div5 = document.createElement('div'); div5.id = 'div5'
|
||||
div = document.createElement('div'); div.id = 'div'
|
||||
|
||||
this.divparent.append(this.div1, this.div2, this.div3)
|
||||
divparent.append(div1, div2, div3)
|
||||
|
||||
this.div4.style.visibility = 'hidden'
|
||||
this.div4.append(this.div5)
|
||||
div4.style.visibility = 'hidden'
|
||||
div4.append(div5)
|
||||
|
||||
document.body.append(this.divparent, this.div)
|
||||
document.body.append(divparent, div)
|
||||
})
|
||||
/**
|
||||
* Tear down tests, destroying undo manager.
|
||||
@@ -278,127 +285,127 @@ describe('history', function () {
|
||||
})
|
||||
|
||||
it('Test MoveElementCommand', function () {
|
||||
let move = new history.MoveElementCommand(this.div3, this.div1, this.divparent)
|
||||
let move = new history.MoveElementCommand(div3, div1, divparent)
|
||||
assert.ok(move.unapply)
|
||||
assert.ok(move.apply)
|
||||
assert.equal(typeof move.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof move.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
move.unapply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div3)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div1)
|
||||
assert.equal(this.divparent.lastElementChild, this.div2)
|
||||
assert.equal(divparent.firstElementChild, div3)
|
||||
assert.equal(divparent.firstElementChild.nextElementSibling, div1)
|
||||
assert.equal(divparent.lastElementChild, div2)
|
||||
|
||||
move.apply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(divparent.firstElementChild.nextElementSibling, div2)
|
||||
assert.equal(divparent.lastElementChild, div3)
|
||||
|
||||
move = new history.MoveElementCommand(this.div1, null, this.divparent)
|
||||
move = new history.MoveElementCommand(div1, null, divparent)
|
||||
|
||||
move.unapply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div2)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div3)
|
||||
assert.equal(this.divparent.lastElementChild, this.div1)
|
||||
assert.equal(divparent.firstElementChild, div2)
|
||||
assert.equal(divparent.firstElementChild.nextElementSibling, div3)
|
||||
assert.equal(divparent.lastElementChild, div1)
|
||||
|
||||
move.apply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(divparent.firstElementChild.nextElementSibling, div2)
|
||||
assert.equal(divparent.lastElementChild, div3)
|
||||
|
||||
move = new history.MoveElementCommand(this.div2, this.div5, this.div4)
|
||||
move = new history.MoveElementCommand(div2, div5, div4)
|
||||
|
||||
move.unapply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div3)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(this.div4.firstElementChild, this.div2)
|
||||
assert.equal(this.div4.firstElementChild.nextElementSibling, this.div5)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(divparent.firstElementChild.nextElementSibling, div3)
|
||||
assert.equal(divparent.lastElementChild, div3)
|
||||
assert.equal(div4.firstElementChild, div2)
|
||||
assert.equal(div4.firstElementChild.nextElementSibling, div5)
|
||||
|
||||
move.apply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(this.div4.firstElementChild, this.div5)
|
||||
assert.equal(this.div4.lastElementChild, this.div5)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(divparent.firstElementChild.nextElementSibling, div2)
|
||||
assert.equal(divparent.lastElementChild, div3)
|
||||
assert.equal(div4.firstElementChild, div5)
|
||||
assert.equal(div4.lastElementChild, div5)
|
||||
})
|
||||
|
||||
it('Test InsertElementCommand', function () {
|
||||
let insert = new history.InsertElementCommand(this.div3)
|
||||
let insert = new history.InsertElementCommand(div3)
|
||||
assert.ok(insert.unapply)
|
||||
assert.ok(insert.apply)
|
||||
assert.equal(typeof insert.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof insert.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
insert.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 2)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div2)
|
||||
assert.equal(divparent.childElementCount, 2)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div2)
|
||||
assert.equal(divparent.lastElementChild, div2)
|
||||
|
||||
insert.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(divparent.childElementCount, 3)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div2)
|
||||
assert.equal(div2.nextElementSibling, div3)
|
||||
|
||||
insert = new history.InsertElementCommand(this.div2)
|
||||
insert = new history.InsertElementCommand(div2)
|
||||
|
||||
insert.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 2)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div3)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(divparent.childElementCount, 2)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div3)
|
||||
assert.equal(divparent.lastElementChild, div3)
|
||||
|
||||
insert.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(divparent.childElementCount, 3)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div2)
|
||||
assert.equal(div2.nextElementSibling, div3)
|
||||
})
|
||||
|
||||
it('Test RemoveElementCommand', function () {
|
||||
const div6 = document.createElement('div')
|
||||
div6.id = 'div6'
|
||||
|
||||
let remove = new history.RemoveElementCommand(div6, null, this.divparent)
|
||||
let remove = new history.RemoveElementCommand(div6, null, divparent)
|
||||
assert.ok(remove.unapply)
|
||||
assert.ok(remove.apply)
|
||||
assert.equal(typeof remove.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof remove.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
remove.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 4)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(this.div3.nextElementSibling, div6)
|
||||
assert.equal(divparent.childElementCount, 4)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div2)
|
||||
assert.equal(div2.nextElementSibling, div3)
|
||||
assert.equal(div3.nextElementSibling, div6)
|
||||
|
||||
remove.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(divparent.childElementCount, 3)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div2)
|
||||
assert.equal(div2.nextElementSibling, div3)
|
||||
|
||||
remove = new history.RemoveElementCommand(div6, this.div2, this.divparent)
|
||||
remove = new history.RemoveElementCommand(div6, div2, divparent)
|
||||
|
||||
remove.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 4)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, div6)
|
||||
assert.equal(div6.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(divparent.childElementCount, 4)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div6)
|
||||
assert.equal(div6.nextElementSibling, div2)
|
||||
assert.equal(div2.nextElementSibling, div3)
|
||||
|
||||
remove.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(divparent.childElementCount, 3)
|
||||
assert.equal(divparent.firstElementChild, div1)
|
||||
assert.equal(div1.nextElementSibling, div2)
|
||||
assert.equal(div2.nextElementSibling, div3)
|
||||
})
|
||||
|
||||
it('Test ChangeElementCommand', function () {
|
||||
this.div1.setAttribute('title', 'new title')
|
||||
let change = new history.ChangeElementCommand(this.div1,
|
||||
div1.setAttribute('title', 'new title')
|
||||
let change = new history.ChangeElementCommand(div1,
|
||||
{ title: 'old title', class: 'foo' })
|
||||
assert.ok(change.unapply)
|
||||
assert.ok(change.apply)
|
||||
@@ -406,32 +413,32 @@ describe('history', function () {
|
||||
assert.equal(typeof change.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
change.unapply()
|
||||
assert.equal(this.div1.getAttribute('title'), 'old title')
|
||||
assert.equal(this.div1.getAttribute('class'), 'foo')
|
||||
assert.equal(div1.getAttribute('title'), 'old title')
|
||||
assert.equal(div1.getAttribute('class'), 'foo')
|
||||
|
||||
change.apply()
|
||||
assert.equal(this.div1.getAttribute('title'), 'new title')
|
||||
assert.ok(!this.div1.getAttribute('class'))
|
||||
assert.equal(div1.getAttribute('title'), 'new title')
|
||||
assert.ok(!div1.getAttribute('class'))
|
||||
|
||||
this.div1.textContent = 'inner text'
|
||||
change = new history.ChangeElementCommand(this.div1,
|
||||
div1.textContent = 'inner text'
|
||||
change = new history.ChangeElementCommand(div1,
|
||||
{ '#text': null })
|
||||
|
||||
change.unapply()
|
||||
assert.ok(!this.div1.textContent)
|
||||
assert.ok(!div1.textContent)
|
||||
|
||||
change.apply()
|
||||
assert.equal(this.div1.textContent, 'inner text')
|
||||
assert.equal(div1.textContent, 'inner text')
|
||||
|
||||
this.div1.textContent = ''
|
||||
change = new history.ChangeElementCommand(this.div1,
|
||||
div1.textContent = ''
|
||||
change = new history.ChangeElementCommand(div1,
|
||||
{ '#text': 'old text' })
|
||||
|
||||
change.unapply()
|
||||
assert.equal(this.div1.textContent, 'old text')
|
||||
assert.equal(div1.textContent, 'old text')
|
||||
|
||||
change.apply()
|
||||
assert.ok(!this.div1.textContent)
|
||||
assert.ok(!div1.textContent)
|
||||
|
||||
// TODO(codedread): Refactor this #href stuff in history.js and svgcanvas.js
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
|
||||
193
tests/unit/mainmenu.test.js
Normal file
193
tests/unit/mainmenu.test.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import MainMenu from '../../src/editor/MainMenu.js'
|
||||
|
||||
vi.mock('@svgedit/svgcanvas', () => ({
|
||||
default: {
|
||||
$id: (id) => document.getElementById(id),
|
||||
$click: (el, fn) => el?.addEventListener('click', fn),
|
||||
convertUnit: (val) => Number(val),
|
||||
isValidUnit: (_attr, val) => val !== 'bad'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@svgedit/svgcanvas/common/browser.js', () => ({
|
||||
isChrome: () => false
|
||||
}))
|
||||
|
||||
describe('MainMenu', () => {
|
||||
let editor
|
||||
let menu
|
||||
let prefStore
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div id="app"></div>
|
||||
<div id="se-export-dialog"></div>
|
||||
<div id="se-img-prop"></div>
|
||||
<div id="se-edit-prefs"></div>
|
||||
`
|
||||
prefStore = { img_save: 'embed', lang: 'en' }
|
||||
const configObj = {
|
||||
curConfig: {
|
||||
baseUnit: 'px',
|
||||
exportWindowType: 'new',
|
||||
canvasName: 'svg-edit',
|
||||
gridSnapping: false,
|
||||
snappingStep: 1,
|
||||
gridColor: '#ccc',
|
||||
showRulers: false
|
||||
},
|
||||
curPrefs: { bkgd_color: '#fff' },
|
||||
preferences: false,
|
||||
pref: vi.fn((key, val) => {
|
||||
if (val !== undefined) {
|
||||
prefStore[key] = val
|
||||
}
|
||||
return prefStore[key]
|
||||
})
|
||||
}
|
||||
const svgCanvas = {
|
||||
setDocumentTitle: vi.fn(),
|
||||
setResolution: vi.fn().mockReturnValue(true),
|
||||
getResolution: vi.fn(() => ({ w: 120, h: 80 })),
|
||||
getDocumentTitle: vi.fn(() => 'Doc'),
|
||||
setConfig: vi.fn(),
|
||||
rasterExport: vi.fn().mockResolvedValue('data-uri'),
|
||||
exportPDF: vi.fn()
|
||||
}
|
||||
|
||||
editor = {
|
||||
configObj,
|
||||
svgCanvas,
|
||||
i18next: { t: (key) => key },
|
||||
$svgEditor: document.getElementById('app'),
|
||||
docprops: false,
|
||||
rulers: { updateRulers: vi.fn() },
|
||||
setBackground: vi.fn(),
|
||||
updateCanvas: vi.fn(),
|
||||
customExportPDF: false,
|
||||
customExportImage: false
|
||||
}
|
||||
globalThis.seAlert = vi.fn()
|
||||
menu = new MainMenu(editor)
|
||||
})
|
||||
|
||||
it('rejects invalid doc properties and shows an alert', () => {
|
||||
const result = menu.saveDocProperties({
|
||||
detail: { title: 'Oops', w: 'bad', h: 'fit', save: 'embed' }
|
||||
})
|
||||
expect(result).toBe(false)
|
||||
expect(globalThis.seAlert).toHaveBeenCalled()
|
||||
expect(editor.svgCanvas.setResolution).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves document properties and hides the dialog', () => {
|
||||
editor.docprops = true
|
||||
const result = menu.saveDocProperties({
|
||||
detail: { title: 'Demo', w: '200', h: '100', save: 'layer' }
|
||||
})
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(editor.svgCanvas.setDocumentTitle).toHaveBeenCalledWith('Demo')
|
||||
expect(editor.svgCanvas.setResolution).toHaveBeenCalledWith('200', '100')
|
||||
expect(editor.updateCanvas).toHaveBeenCalled()
|
||||
expect(prefStore.img_save).toBe('layer')
|
||||
expect(editor.docprops).toBe(false)
|
||||
expect(document.getElementById('se-img-prop').getAttribute('dialog')).toBe('close')
|
||||
})
|
||||
|
||||
it('saves preferences, updates config and alerts when language changes', async () => {
|
||||
editor.configObj.preferences = true
|
||||
const detail = {
|
||||
lang: 'fr',
|
||||
bgcolor: '#111',
|
||||
bgurl: '',
|
||||
gridsnappingon: true,
|
||||
gridsnappingstep: 2,
|
||||
gridcolor: '#333',
|
||||
showrulers: true,
|
||||
baseunit: 'cm'
|
||||
}
|
||||
|
||||
await menu.savePreferences({ detail })
|
||||
|
||||
expect(editor.setBackground).toHaveBeenCalledWith('#111', '')
|
||||
expect(prefStore.lang).toBe('fr')
|
||||
expect(editor.configObj.curConfig.gridSnapping).toBe(true)
|
||||
expect(editor.configObj.curConfig.snappingStep).toBe(2)
|
||||
expect(editor.configObj.curConfig.gridColor).toBe('#333')
|
||||
expect(editor.configObj.curConfig.showRulers).toBe(true)
|
||||
expect(editor.configObj.curConfig.baseUnit).toBe('cm')
|
||||
expect(editor.rulers.updateRulers).toHaveBeenCalled()
|
||||
expect(editor.svgCanvas.setConfig).toHaveBeenCalled()
|
||||
expect(editor.updateCanvas).toHaveBeenCalled()
|
||||
expect(globalThis.seAlert).toHaveBeenCalled()
|
||||
expect(editor.configObj.preferences).toBe(false)
|
||||
})
|
||||
|
||||
it('opens doc properties dialog and converts units when needed', () => {
|
||||
editor.configObj.curConfig.baseUnit = 'cm'
|
||||
menu.showDocProperties()
|
||||
|
||||
const dialog = document.getElementById('se-img-prop')
|
||||
expect(editor.docprops).toBe(true)
|
||||
expect(dialog.getAttribute('dialog')).toBe('open')
|
||||
expect(dialog.getAttribute('width')).toBe('120cm')
|
||||
expect(dialog.getAttribute('height')).toBe('80cm')
|
||||
expect(dialog.getAttribute('title')).toBe('Doc')
|
||||
|
||||
editor.svgCanvas.getResolution.mockClear()
|
||||
menu.showDocProperties()
|
||||
expect(editor.svgCanvas.getResolution).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens preferences dialog only once and populates attributes', () => {
|
||||
editor.configObj.curConfig.gridSnapping = true
|
||||
editor.configObj.curConfig.snappingStep = 4
|
||||
editor.configObj.curConfig.gridColor = '#888'
|
||||
editor.configObj.curPrefs.bkgd_color = '#ff00ff'
|
||||
editor.configObj.pref = vi.fn((key) => key === 'bkgd_url' ? 'http://example.com' : prefStore[key])
|
||||
|
||||
menu.showPreferences()
|
||||
const prefs = document.getElementById('se-edit-prefs')
|
||||
expect(editor.configObj.preferences).toBe(true)
|
||||
expect(prefs.getAttribute('dialog')).toBe('open')
|
||||
expect(prefs.getAttribute('gridsnappingon')).toBe('true')
|
||||
expect(prefs.getAttribute('gridsnappingstep')).toBe('4')
|
||||
expect(prefs.getAttribute('gridcolor')).toBe('#888')
|
||||
expect(prefs.getAttribute('canvasbg')).toBe('#ff00ff')
|
||||
expect(prefs.getAttribute('bgurl')).toBe('http://example.com')
|
||||
|
||||
editor.configObj.preferences = true
|
||||
prefs.removeAttribute('dialog')
|
||||
menu.showPreferences()
|
||||
expect(prefs.getAttribute('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('routes export actions based on dialog detail', async () => {
|
||||
await menu.clickExport()
|
||||
expect(editor.svgCanvas.rasterExport).not.toHaveBeenCalled()
|
||||
|
||||
await menu.clickExport({ detail: { trigger: 'ok', imgType: 'PNG', quality: 50 } })
|
||||
expect(editor.svgCanvas.rasterExport).toHaveBeenCalledWith('PNG', 0.5, editor.exportWindowName)
|
||||
expect(editor.exportWindowCt).toBe(1)
|
||||
|
||||
await menu.clickExport({ detail: { trigger: 'ok', imgType: 'PDF' } })
|
||||
expect(editor.svgCanvas.exportPDF).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates menu entries and wires click handlers in init', () => {
|
||||
menu.init()
|
||||
|
||||
document.getElementById('tool_export').dispatchEvent(new Event('click', { bubbles: true }))
|
||||
expect(document.getElementById('se-export-dialog').getAttribute('dialog')).toBe('open')
|
||||
|
||||
document.getElementById('tool_docprops').dispatchEvent(new Event('click', { bubbles: true }))
|
||||
expect(editor.docprops).toBe(true)
|
||||
|
||||
const prefsDialog = document.getElementById('se-edit-prefs')
|
||||
prefsDialog.dispatchEvent(new CustomEvent('change', { detail: { dialog: 'closed' } }))
|
||||
expect(prefsDialog.getAttribute('dialog')).toBe('close')
|
||||
})
|
||||
})
|
||||
72
tests/unit/paint.test.js
Normal file
72
tests/unit/paint.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Paint from '../../packages/svgcanvas/core/paint.js'
|
||||
|
||||
const createLinear = (id) => {
|
||||
const grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient')
|
||||
if (id) grad.id = id
|
||||
grad.setAttribute('x1', '0')
|
||||
grad.setAttribute('x2', '1')
|
||||
return grad
|
||||
}
|
||||
|
||||
const createRadial = (id) => {
|
||||
const grad = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient')
|
||||
if (id) grad.id = id
|
||||
grad.setAttribute('cx', '0.5')
|
||||
grad.setAttribute('cy', '0.5')
|
||||
return grad
|
||||
}
|
||||
|
||||
describe('Paint', () => {
|
||||
it('defaults to an empty paint when no options are provided', () => {
|
||||
const paint = new Paint()
|
||||
expect(paint.type).toBe('none')
|
||||
expect(paint.alpha).toBe(100)
|
||||
expect(paint.solidColor).toBeNull()
|
||||
expect(paint.linearGradient).toBeNull()
|
||||
expect(paint.radialGradient).toBeNull()
|
||||
})
|
||||
|
||||
it('copies a solid color paint including alpha', () => {
|
||||
const base = new Paint({ solidColor: '#00ff00', alpha: 65 })
|
||||
const copy = new Paint({ copy: base })
|
||||
|
||||
expect(copy.type).toBe('solidColor')
|
||||
expect(copy.alpha).toBe(65)
|
||||
expect(copy.solidColor).toBe('#00ff00')
|
||||
expect(copy.linearGradient).toBeNull()
|
||||
expect(copy.radialGradient).toBeNull()
|
||||
})
|
||||
|
||||
it('copies gradients by cloning the underlying nodes', () => {
|
||||
const linear = createLinear('lin1')
|
||||
const base = new Paint({ linearGradient: linear })
|
||||
const clone = new Paint({ copy: base })
|
||||
|
||||
expect(clone.type).toBe('linearGradient')
|
||||
expect(clone.linearGradient).not.toBe(base.linearGradient)
|
||||
expect(clone.linearGradient?.isEqualNode(base.linearGradient)).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves linked linear gradients via href/xlink:href', () => {
|
||||
const referenced = createLinear('refGrad')
|
||||
document.body.append(referenced)
|
||||
const referencing = createLinear('linkGrad')
|
||||
referencing.setAttribute('xlink:href', '#refGrad')
|
||||
|
||||
const paint = new Paint({ linearGradient: referencing })
|
||||
expect(paint.type).toBe('linearGradient')
|
||||
expect(paint.linearGradient).not.toBeNull()
|
||||
expect(paint.linearGradient?.id).toBe('refGrad')
|
||||
})
|
||||
|
||||
it('creates radial gradients from provided element when no href is set', () => {
|
||||
const radial = createRadial('rad1')
|
||||
const paint = new Paint({ radialGradient: radial })
|
||||
|
||||
expect(paint.type).toBe('radialGradient')
|
||||
expect(paint.radialGradient).not.toBe(radial)
|
||||
expect(paint.radialGradient?.id).toBe('rad1')
|
||||
expect(paint.linearGradient).toBeNull()
|
||||
})
|
||||
})
|
||||
348
tests/unit/setup-vitest.js
Normal file
348
tests/unit/setup-vitest.js
Normal file
@@ -0,0 +1,348 @@
|
||||
import { AssertionError, strict as assert } from 'node:assert'
|
||||
|
||||
// Provide a global assert (some legacy tests expect it).
|
||||
globalThis.assert = assert
|
||||
|
||||
// Add a lightweight closeTo helper to mimic chai.assert.closeTo.
|
||||
assert.closeTo = function (actual, expected, delta, message) {
|
||||
const ok = Math.abs(actual - expected) <= delta
|
||||
if (!ok) {
|
||||
throw new AssertionError({
|
||||
message: message || `expected ${actual} to be within ${delta} of ${expected}`,
|
||||
actual,
|
||||
expected
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Mocha-style aliases expected by legacy tests.
|
||||
globalThis.before = globalThis.beforeAll
|
||||
globalThis.after = globalThis.afterAll
|
||||
|
||||
// JSDOM lacks many SVG APIs; provide minimal stubs used in tests.
|
||||
const win = globalThis.window || globalThis
|
||||
|
||||
// Simple SVG matrix/transform/point polyfills good enough for unit tests.
|
||||
class SVGMatrixPolyfill {
|
||||
constructor (a = 1, b = 0, c = 0, d = 1, e = 0, f = 0) {
|
||||
this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f
|
||||
}
|
||||
|
||||
multiply (m) {
|
||||
return new SVGMatrixPolyfill(
|
||||
this.a * m.a + this.c * m.b,
|
||||
this.b * m.a + this.d * m.b,
|
||||
this.a * m.c + this.c * m.d,
|
||||
this.b * m.c + this.d * m.d,
|
||||
this.a * m.e + this.c * m.f + this.e,
|
||||
this.b * m.e + this.d * m.f + this.f
|
||||
)
|
||||
}
|
||||
|
||||
translate (x, y) { return this.multiply(new SVGMatrixPolyfill(1, 0, 0, 1, x, y)) }
|
||||
scale (s) { return this.multiply(new SVGMatrixPolyfill(s, 0, 0, s, 0, 0)) }
|
||||
scaleNonUniform (sx, sy) { return this.multiply(new SVGMatrixPolyfill(sx, 0, 0, sy, 0, 0)) }
|
||||
rotate (deg) {
|
||||
const rad = deg * Math.PI / 180
|
||||
const cos = Math.cos(rad)
|
||||
const sin = Math.sin(rad)
|
||||
return this.multiply(new SVGMatrixPolyfill(cos, sin, -sin, cos, 0, 0))
|
||||
}
|
||||
|
||||
flipX () { return this.scale(-1, 1) }
|
||||
flipY () { return this.scale(1, -1) }
|
||||
skewX (deg) {
|
||||
const rad = deg * Math.PI / 180
|
||||
return this.multiply(new SVGMatrixPolyfill(1, 0, Math.tan(rad), 1, 0, 0))
|
||||
}
|
||||
|
||||
skewY (deg) {
|
||||
const rad = deg * Math.PI / 180
|
||||
return this.multiply(new SVGMatrixPolyfill(1, Math.tan(rad), 0, 1, 0, 0))
|
||||
}
|
||||
|
||||
inverse () {
|
||||
const det = this.a * this.d - this.b * this.c
|
||||
if (!det) return new SVGMatrixPolyfill()
|
||||
return new SVGMatrixPolyfill(
|
||||
this.d / det,
|
||||
-this.b / det,
|
||||
-this.c / det,
|
||||
this.a / det,
|
||||
(this.c * this.f - this.d * this.e) / det,
|
||||
(this.b * this.e - this.a * this.f) / det
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SVGTransformPolyfill {
|
||||
constructor (type = SVGTransformPolyfill.SVG_TRANSFORM_MATRIX, matrix = new SVGMatrixPolyfill()) {
|
||||
this.type = type
|
||||
this.matrix = matrix
|
||||
}
|
||||
|
||||
setMatrix (matrix) {
|
||||
this.type = SVGTransformPolyfill.SVG_TRANSFORM_MATRIX
|
||||
this.matrix = matrix
|
||||
}
|
||||
|
||||
setTranslate (x, y) {
|
||||
this.type = SVGTransformPolyfill.SVG_TRANSFORM_TRANSLATE
|
||||
this.matrix = new SVGMatrixPolyfill(1, 0, 0, 1, x, y)
|
||||
}
|
||||
|
||||
setScale (sx, sy = sx) {
|
||||
this.type = SVGTransformPolyfill.SVG_TRANSFORM_SCALE
|
||||
this.matrix = new SVGMatrixPolyfill(sx, 0, 0, sy, 0, 0)
|
||||
}
|
||||
|
||||
setRotate (angle, cx = 0, cy = 0) {
|
||||
// Translate to center, rotate, then translate back.
|
||||
const ang = Number(angle) || 0
|
||||
const cxNum = Number(cx) || 0
|
||||
const cyNum = Number(cy) || 0
|
||||
const rotate = new SVGMatrixPolyfill().translate(cxNum, cyNum).rotate(ang).translate(-cxNum, -cyNum)
|
||||
this.type = SVGTransformPolyfill.SVG_TRANSFORM_ROTATE
|
||||
this.angle = ang
|
||||
this.cx = cxNum
|
||||
this.cy = cyNum
|
||||
this.matrix = rotate
|
||||
}
|
||||
}
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_UNKNOWN = 0
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_MATRIX = 1
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_TRANSLATE = 2
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_SCALE = 3
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_ROTATE = 4
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_SKEWX = 5
|
||||
SVGTransformPolyfill.SVG_TRANSFORM_SKEWY = 6
|
||||
|
||||
class SVGTransformListPolyfill {
|
||||
constructor () {
|
||||
this._items = []
|
||||
}
|
||||
|
||||
get numberOfItems () { return this._items.length }
|
||||
getItem (i) { return this._items[i] }
|
||||
appendItem (item) { this._items.push(item); return item }
|
||||
insertItemBefore (item, index) {
|
||||
const idx = Math.max(0, Math.min(index, this._items.length))
|
||||
this._items.splice(idx, 0, item)
|
||||
return item
|
||||
}
|
||||
|
||||
removeItem (index) {
|
||||
if (index < 0 || index >= this._items.length) return undefined
|
||||
const [removed] = this._items.splice(index, 1)
|
||||
return removed
|
||||
}
|
||||
|
||||
clear () { this._items = [] }
|
||||
initialize (item) { this._items = [item]; return item }
|
||||
consolidate () {
|
||||
if (!this._items.length) return null
|
||||
const matrix = this._items.reduce(
|
||||
(acc, t) => acc.multiply(t.matrix),
|
||||
new SVGMatrixPolyfill()
|
||||
)
|
||||
const consolidated = new SVGTransformPolyfill()
|
||||
consolidated.setMatrix(matrix)
|
||||
this._items = [consolidated]
|
||||
return consolidated
|
||||
}
|
||||
}
|
||||
|
||||
const parseTransformAttr = (attr) => {
|
||||
const list = new SVGTransformListPolyfill()
|
||||
if (!attr) return list
|
||||
const matcher = /([a-zA-Z]+)\(([^)]+)\)/g
|
||||
let match
|
||||
while ((match = matcher.exec(attr))) {
|
||||
const [, type, raw] = match
|
||||
const nums = raw.split(/[,\s]+/).filter(Boolean).map(Number)
|
||||
const t = new SVGTransformPolyfill()
|
||||
switch (type) {
|
||||
case 'matrix':
|
||||
t.setMatrix(new SVGMatrixPolyfill(...nums))
|
||||
break
|
||||
case 'translate':
|
||||
t.setTranslate(nums[0] ?? 0, nums[1] ?? 0)
|
||||
break
|
||||
case 'scale':
|
||||
t.setScale(nums[0] ?? 1, nums[1] ?? nums[0] ?? 1)
|
||||
break
|
||||
case 'rotate':
|
||||
t.setRotate(nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0)
|
||||
break
|
||||
default:
|
||||
t.setMatrix(new SVGMatrixPolyfill())
|
||||
break
|
||||
}
|
||||
list.appendItem(t)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const ensureTransformList = (elem) => {
|
||||
if (!elem.__transformList) {
|
||||
const parsed = parseTransformAttr(elem.getAttribute?.('transform'))
|
||||
elem.__transformList = parsed
|
||||
}
|
||||
return elem.__transformList
|
||||
}
|
||||
|
||||
if (!win.SVGElement) {
|
||||
win.SVGElement = win.Element
|
||||
}
|
||||
const svgElementProto = win.SVGElement?.prototype
|
||||
|
||||
// Basic constructors for missing SVG types.
|
||||
if (!win.SVGSVGElement) win.SVGSVGElement = win.SVGElement
|
||||
if (!win.SVGGraphicsElement) win.SVGGraphicsElement = win.SVGElement
|
||||
if (!win.SVGGeometryElement) win.SVGGeometryElement = win.SVGElement
|
||||
// Ensure SVGPathElement exists so the pathseg polyfill can patch it.
|
||||
win.SVGPathElement = win.SVGElement || function SVGPathElement () {}
|
||||
|
||||
// Matrix/transform helpers.
|
||||
win.SVGMatrix = win.SVGMatrix || SVGMatrixPolyfill
|
||||
win.DOMMatrix = win.DOMMatrix || SVGMatrixPolyfill
|
||||
win.SVGTransform = win.SVGTransform || SVGTransformPolyfill
|
||||
win.SVGTransformList = win.SVGTransformList || SVGTransformListPolyfill
|
||||
|
||||
if (svgElementProto) {
|
||||
if (!svgElementProto.createSVGMatrix) {
|
||||
svgElementProto.createSVGMatrix = () => new SVGMatrixPolyfill()
|
||||
}
|
||||
if (!svgElementProto.createSVGTransform) {
|
||||
svgElementProto.createSVGTransform = () => new SVGTransformPolyfill()
|
||||
}
|
||||
if (!svgElementProto.createSVGTransformFromMatrix) {
|
||||
svgElementProto.createSVGTransformFromMatrix = (matrix) => {
|
||||
const t = new SVGTransformPolyfill()
|
||||
t.setMatrix(matrix)
|
||||
return t
|
||||
}
|
||||
}
|
||||
if (!svgElementProto.createSVGPoint) {
|
||||
svgElementProto.createSVGPoint = () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
matrixTransform (m) {
|
||||
return {
|
||||
x: m.a * this.x + m.c * this.y + m.e,
|
||||
y: m.b * this.x + m.d * this.y + m.f
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
svgElementProto.getBBox = function () {
|
||||
const tag = (this.tagName || '').toLowerCase()
|
||||
const parseLength = (attr, fallback = 0) => {
|
||||
const raw = this.getAttribute?.(attr)
|
||||
if (raw == null) return fallback
|
||||
const str = String(raw)
|
||||
const n = Number.parseFloat(str)
|
||||
if (Number.isNaN(n)) return fallback
|
||||
if (str.endsWith('in')) return n * 96
|
||||
if (str.endsWith('cm')) return n * 96 / 2.54
|
||||
if (str.endsWith('mm')) return n * 96 / 25.4
|
||||
if (str.endsWith('pt')) return n * 96 / 72
|
||||
if (str.endsWith('pc')) return n * 16
|
||||
if (str.endsWith('em')) return n * 16
|
||||
if (str.endsWith('ex')) return n * 8
|
||||
return n
|
||||
}
|
||||
const parsePoints = () => (this.getAttribute?.('points') || '')
|
||||
.trim()
|
||||
.split(/\\s+/)
|
||||
.map(pair => pair.split(',').map(Number))
|
||||
.filter(([x, y]) => !Number.isNaN(x) && !Number.isNaN(y))
|
||||
|
||||
if (tag === 'path') {
|
||||
const d = this.getAttribute?.('d') || ''
|
||||
const nums = (d.match(/-?\\d*\\.?\\d+/g) || [])
|
||||
.map(Number)
|
||||
.filter(n => !Number.isNaN(n))
|
||||
if (nums.length >= 2) {
|
||||
let minx = Infinity; let miny = Infinity
|
||||
let maxx = -Infinity; let maxy = -Infinity
|
||||
for (let i = 0; i < nums.length; i += 2) {
|
||||
const x = nums[i]; const y = nums[i + 1]
|
||||
if (x < minx) minx = x
|
||||
if (x > maxx) maxx = x
|
||||
if (y < miny) miny = y
|
||||
if (y > maxy) maxy = y
|
||||
}
|
||||
return {
|
||||
x: minx === Infinity ? 0 : minx,
|
||||
y: miny === Infinity ? 0 : miny,
|
||||
width: maxx === -Infinity ? 0 : maxx - minx,
|
||||
height: maxy === -Infinity ? 0 : maxy - miny
|
||||
}
|
||||
}
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
|
||||
if (tag === 'rect') {
|
||||
const x = parseLength('x')
|
||||
const y = parseLength('y')
|
||||
const width = parseLength('width')
|
||||
const height = parseLength('height')
|
||||
return { x, y, width, height }
|
||||
}
|
||||
|
||||
if (tag === 'line') {
|
||||
const x1 = parseLength('x1'); const y1 = parseLength('y1')
|
||||
const x2 = parseLength('x2'); const y2 = parseLength('y2')
|
||||
const minx = Math.min(x1, x2); const miny = Math.min(y1, y2)
|
||||
return { x: minx, y: miny, width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }
|
||||
}
|
||||
|
||||
if (tag === 'circle') {
|
||||
const cx = parseLength('cx'); const cy = parseLength('cy'); const r = parseLength('r') || parseLength('rx') || parseLength('ry')
|
||||
return { x: cx - r, y: cy - r, width: r * 2, height: r * 2 }
|
||||
}
|
||||
|
||||
if (tag === 'ellipse') {
|
||||
const cx = parseLength('cx'); const cy = parseLength('cy'); const rx = parseLength('rx'); const ry = parseLength('ry')
|
||||
return { x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2 }
|
||||
}
|
||||
|
||||
if (tag === 'polyline' || tag === 'polygon') {
|
||||
const pts = parsePoints()
|
||||
if (!pts.length) return { x: 0, y: 0, width: 0, height: 0 }
|
||||
const xs = pts.map(([x]) => x)
|
||||
const ys = pts.map(([, y]) => y)
|
||||
const minx = Math.min(...xs); const maxx = Math.max(...xs)
|
||||
const miny = Math.min(...ys); const maxy = Math.max(...ys)
|
||||
return { x: minx, y: miny, width: maxx - minx, height: maxy - miny }
|
||||
}
|
||||
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
if (!Object.getOwnPropertyDescriptor(svgElementProto, 'transform')) {
|
||||
Object.defineProperty(svgElementProto, 'transform', {
|
||||
get () {
|
||||
const baseVal = ensureTransformList(this)
|
||||
return { baseVal }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure pathseg polyfill can attach to prototypes.
|
||||
await import('pathseg')
|
||||
|
||||
// Add minimal chai-like helpers some legacy tests expect.
|
||||
assert.close = (actual, expected, delta, message) =>
|
||||
assert.closeTo(actual, expected, delta, message)
|
||||
assert.notOk = (val, message) => {
|
||||
if (val) {
|
||||
throw new AssertionError({ message: message || `expected ${val} to be falsy`, actual: val, expected: false })
|
||||
}
|
||||
}
|
||||
assert.isBelow = (val, limit, message) => {
|
||||
if (!(val < limit)) {
|
||||
throw new AssertionError({ message: message || `expected ${val} to be below ${limit}`, actual: val, expected: `< ${limit}` })
|
||||
}
|
||||
}
|
||||
124
tests/unit/touch.test.js
Normal file
124
tests/unit/touch.test.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { init as initTouch } from '../../packages/svgcanvas/core/touch.js'
|
||||
|
||||
const createSvgRoot = () => {
|
||||
const listeners = {}
|
||||
return {
|
||||
listeners,
|
||||
addEventListener (type, handler) { listeners[type] = handler },
|
||||
dispatch (type, event) { listeners[type]?.(event) }
|
||||
}
|
||||
}
|
||||
|
||||
const OriginalMouseEvent = global.MouseEvent
|
||||
|
||||
beforeAll(() => {
|
||||
// JSDOM's MouseEvent requires a real Window; a lightweight stub keeps the adapter logic testable.
|
||||
global.MouseEvent = class extends Event {
|
||||
constructor (type, init = {}) {
|
||||
super(type, init)
|
||||
this.clientX = init.clientX
|
||||
this.clientY = init.clientY
|
||||
this.screenX = init.screenX
|
||||
this.screenY = init.screenY
|
||||
this.button = init.button ?? 0
|
||||
this.relatedTarget = init.relatedTarget ?? null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.MouseEvent = OriginalMouseEvent
|
||||
})
|
||||
|
||||
describe('touch adapter', () => {
|
||||
it('translates single touch to mouse event on target', () => {
|
||||
const svgroot = createSvgRoot()
|
||||
const svgCanvas = { svgroot }
|
||||
initTouch(svgCanvas)
|
||||
|
||||
const target = document.createElement('div')
|
||||
const received = []
|
||||
target.addEventListener('mousedown', (ev) => {
|
||||
received.push({
|
||||
type: ev.type,
|
||||
clientX: ev.clientX,
|
||||
clientY: ev.clientY,
|
||||
screenX: ev.screenX,
|
||||
screenY: ev.screenY
|
||||
})
|
||||
})
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
svgroot.dispatch('touchstart', {
|
||||
type: 'touchstart',
|
||||
changedTouches: [{
|
||||
target,
|
||||
clientX: 12,
|
||||
clientY: 34,
|
||||
screenX: 56,
|
||||
screenY: 78
|
||||
}],
|
||||
preventDefault
|
||||
})
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled()
|
||||
expect(received).toEqual([{
|
||||
type: 'mousedown',
|
||||
clientX: 12,
|
||||
clientY: 34,
|
||||
screenX: 56,
|
||||
screenY: 78
|
||||
}])
|
||||
})
|
||||
|
||||
it('maps move events and ignores multi-touch gestures', () => {
|
||||
const svgroot = createSvgRoot()
|
||||
initTouch({ svgroot })
|
||||
|
||||
const target = document.createElement('div')
|
||||
let mouseDown = 0
|
||||
let mouseMove = 0
|
||||
target.addEventListener('mousedown', () => { mouseDown++ })
|
||||
target.addEventListener('mousemove', () => { mouseMove++ })
|
||||
|
||||
svgroot.dispatch('touchstart', {
|
||||
type: 'touchstart',
|
||||
changedTouches: [
|
||||
{ target, clientX: 1, clientY: 2, screenX: 3, screenY: 4 },
|
||||
{ target, clientX: 5, clientY: 6, screenX: 7, screenY: 8 }
|
||||
],
|
||||
preventDefault: vi.fn()
|
||||
})
|
||||
|
||||
expect(mouseDown).toBe(0)
|
||||
|
||||
svgroot.dispatch('touchmove', {
|
||||
type: 'touchmove',
|
||||
changedTouches: [
|
||||
{ target, clientX: 9, clientY: 10, screenX: 11, screenY: 12 }
|
||||
],
|
||||
preventDefault: vi.fn()
|
||||
})
|
||||
|
||||
expect(mouseMove).toBe(1)
|
||||
})
|
||||
|
||||
it('returns early on unknown event types', () => {
|
||||
const svgroot = createSvgRoot()
|
||||
initTouch({ svgroot })
|
||||
const target = document.createElement('div')
|
||||
let mouseCount = 0
|
||||
target.addEventListener('mousedown', () => { mouseCount++ })
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
svgroot.dispatch('touchcancel', {
|
||||
type: 'touchcancel',
|
||||
changedTouches: [{ target, clientX: 0, clientY: 0, screenX: 0, screenY: 0 }],
|
||||
preventDefault
|
||||
})
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled()
|
||||
expect(mouseCount).toBe(0)
|
||||
})
|
||||
})
|
||||
67
tests/unit/util-common.test.js
Normal file
67
tests/unit/util-common.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
findPos,
|
||||
isObject,
|
||||
mergeDeep,
|
||||
getClosest,
|
||||
getParents,
|
||||
getParentsUntil
|
||||
} from '../../packages/svgcanvas/common/util.js'
|
||||
|
||||
describe('common util helpers', () => {
|
||||
it('computes positions and merges objects deeply', () => {
|
||||
const grand = { offsetLeft: 5, offsetTop: 6, offsetParent: null }
|
||||
const parent = { offsetLeft: 10, offsetTop: 11, offsetParent: grand }
|
||||
const child = { offsetLeft: 7, offsetTop: 8, offsetParent: parent }
|
||||
|
||||
expect(findPos(child)).toEqual({ left: 22, top: 25 })
|
||||
expect(isObject({ foo: 'bar' })).toBe(true)
|
||||
|
||||
const merged = mergeDeep(
|
||||
{ a: 1, nested: { keep: true, replace: 'old' } },
|
||||
{ nested: { replace: 'new', extra: 42 }, more: 'yes' }
|
||||
)
|
||||
expect(merged).toEqual({ a: 1, nested: { keep: true, replace: 'new', extra: 42 }, more: 'yes' })
|
||||
})
|
||||
|
||||
it('finds closest elements across selectors', () => {
|
||||
const root = document.createElement('div')
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'wrapper'
|
||||
const section = document.createElement('section')
|
||||
section.id = 'section'
|
||||
const child = document.createElement('span')
|
||||
child.dataset.role = 'target'
|
||||
|
||||
section.append(child)
|
||||
wrapper.append(section)
|
||||
root.append(wrapper)
|
||||
document.body.append(root)
|
||||
|
||||
expect(getClosest(child, '.wrapper')?.className).toBe('wrapper')
|
||||
expect(getClosest(child, '#section')?.id).toBe('section')
|
||||
expect(getClosest(child, '[data-role=target]')?.dataset.role).toBe('target')
|
||||
expect(getClosest(child, 'div')?.tagName.toLowerCase()).toBe('div')
|
||||
})
|
||||
|
||||
it('collects parents with and without limits', () => {
|
||||
const outer = document.createElement('div')
|
||||
outer.className = 'outer'
|
||||
const mid = document.createElement('section')
|
||||
mid.id = 'mid'
|
||||
const inner = document.createElement('span')
|
||||
inner.className = 'inner'
|
||||
|
||||
mid.append(inner)
|
||||
outer.append(mid)
|
||||
document.body.append(outer)
|
||||
|
||||
const parents = getParents(inner)?.map(el => el.tagName.toLowerCase())
|
||||
expect(parents).toContain('body')
|
||||
|
||||
expect(getParents(inner, '.outer')?.map(el => el.className)).toEqual(['outer'])
|
||||
|
||||
const untilMid = getParentsUntil(inner, '#mid', '.inner')?.map(el => el.tagName.toLowerCase())
|
||||
expect(untilMid).toEqual(['span'])
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,12 @@
|
||||
import { strict as assert } from 'node:assert'
|
||||
import 'pathseg'
|
||||
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as math from '../../packages/svgcanvas/core/math.js'
|
||||
import * as path from '../../packages/svgcanvas/core/path.js'
|
||||
import setAssertionMethods from '../../support/assert-close.js'
|
||||
import * as units from '../../packages/svgcanvas/core/units.js'
|
||||
|
||||
// eslint-disable-next-line
|
||||
chai.use(setAssertionMethods)
|
||||
|
||||
describe('utilities bbox', function () {
|
||||
/**
|
||||
* Create an SVG element for a mock.
|
||||
@@ -21,6 +18,39 @@ describe('utilities bbox', function () {
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
const numFromAttr = (attr, fallback = 0) => Number(jsonMap.attr[attr] ?? fallback)
|
||||
const calcBBox = () => {
|
||||
const tag = (jsonMap.element || '').toLowerCase()
|
||||
switch (tag) {
|
||||
case 'path': {
|
||||
const d = jsonMap.attr.d || ''
|
||||
const nums = (d.match(/-?\\d*\\.?\\d+/g) || []).map(Number)
|
||||
if (nums.length >= 4) {
|
||||
const xs = nums.filter((_, i) => i % 2 === 0)
|
||||
const ys = nums.filter((_, i) => i % 2 === 1)
|
||||
return {
|
||||
x: Math.min(...xs),
|
||||
y: Math.min(...ys),
|
||||
width: Math.max(...xs) - Math.min(...xs),
|
||||
height: Math.max(...ys) - Math.min(...ys)
|
||||
}
|
||||
}
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
case 'rect':
|
||||
return { x: numFromAttr('x'), y: numFromAttr('y'), width: numFromAttr('width'), height: numFromAttr('height') }
|
||||
case 'line': {
|
||||
const x1 = numFromAttr('x1'); const x2 = numFromAttr('x2'); const y1 = numFromAttr('y1'); const y2 = numFromAttr('y2')
|
||||
return { x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }
|
||||
}
|
||||
default:
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
}
|
||||
const bbox = calcBBox()
|
||||
elem.getBBox = () => {
|
||||
return { ...bbox }
|
||||
}
|
||||
return elem
|
||||
}
|
||||
let mockaddSVGElementsFromJsonCallCount = 0
|
||||
|
||||
70
tests/unit/utilities-extra2.test.js
Normal file
70
tests/unit/utilities-extra2.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
|
||||
import { init as initUnits } from '../../packages/svgcanvas/core/units.js'
|
||||
import {
|
||||
init as initUtilities,
|
||||
findDefs,
|
||||
assignAttributes,
|
||||
snapToGrid,
|
||||
getHref,
|
||||
setHref,
|
||||
dropXMLInternalSubset,
|
||||
encodeUTF8,
|
||||
decodeUTF8
|
||||
} from '../../packages/svgcanvas/core/utilities.js'
|
||||
|
||||
describe('utilities extra coverage', () => {
|
||||
let svg
|
||||
|
||||
beforeEach(() => {
|
||||
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
document.body.innerHTML = ''
|
||||
document.body.append(svg)
|
||||
|
||||
// Initialize units and utilities with a minimal canvas/context stub
|
||||
initUnits({
|
||||
getBaseUnit: () => 'px',
|
||||
getWidth: () => 200,
|
||||
getHeight: () => 100,
|
||||
getRoundDigits: () => 2
|
||||
})
|
||||
initUtilities({
|
||||
getSvgRoot: () => svg,
|
||||
getSvgContent: () => svg,
|
||||
getDOMDocument: () => document,
|
||||
getDOMContainer: () => svg,
|
||||
getBaseUnit: () => 'cm',
|
||||
getSnappingStep: () => 0.5
|
||||
})
|
||||
})
|
||||
|
||||
it('creates defs and removes namespaced attributes via assignAttributes', () => {
|
||||
const defs = findDefs()
|
||||
expect(defs.tagName).toBe('defs')
|
||||
expect(svg.querySelectorAll('defs').length).toBe(1)
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve')
|
||||
assignAttributes(rect, { width: '10', height: '5', 'xml:space': undefined }, 0, true)
|
||||
expect(rect.getAttribute('width')).toBe('10')
|
||||
expect(rect.getAttribute('height')).toBe('5')
|
||||
})
|
||||
|
||||
it('snaps to grid with unit conversion and handles href helpers', () => {
|
||||
const value = snapToGrid(2.3)
|
||||
expect(value).toBe(0)
|
||||
|
||||
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use')
|
||||
setHref(use, '#ref')
|
||||
expect(getHref(use)).toBe('#ref')
|
||||
})
|
||||
|
||||
it('drops XML internal subsets and round trips UTF8 helpers', () => {
|
||||
const doc = '<!DOCTYPE svg [<!ENTITY test "x">]><svg/>'
|
||||
expect(dropXMLInternalSubset(doc)).toContain('<!DOCTYPE svg')
|
||||
|
||||
const mixed = 'äöü & < >'
|
||||
const encoded = encodeUTF8(mixed)
|
||||
expect(decodeUTF8(encoded)).toBe(mixed)
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,39 @@ describe('utilities', function () {
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
const numFromAttr = (attr, fallback = 0) => Number(jsonMap.attr[attr] ?? fallback)
|
||||
const calcBBox = () => {
|
||||
const tag = (jsonMap.element || '').toLowerCase()
|
||||
switch (tag) {
|
||||
case 'path': {
|
||||
const d = jsonMap.attr.d || ''
|
||||
const nums = (d.match(/-?\\d*\\.?\\d+/g) || []).map(Number)
|
||||
if (nums.length >= 4) {
|
||||
const xs = nums.filter((_, i) => i % 2 === 0)
|
||||
const ys = nums.filter((_, i) => i % 2 === 1)
|
||||
return {
|
||||
x: Math.min(...xs),
|
||||
y: Math.min(...ys),
|
||||
width: Math.max(...xs) - Math.min(...xs),
|
||||
height: Math.max(...ys) - Math.min(...ys)
|
||||
}
|
||||
}
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
case 'rect':
|
||||
return { x: numFromAttr('x'), y: numFromAttr('y'), width: numFromAttr('width'), height: numFromAttr('height') }
|
||||
case 'line': {
|
||||
const x1 = numFromAttr('x1'); const x2 = numFromAttr('x2'); const y1 = numFromAttr('y1'); const y2 = numFromAttr('y2')
|
||||
return { x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }
|
||||
}
|
||||
default:
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
}
|
||||
const bbox = calcBBox()
|
||||
elem.getBBox = () => {
|
||||
return { ...bbox }
|
||||
}
|
||||
return elem
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -93,11 +93,25 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['tests/unit/setup-vitest.js'],
|
||||
include: ['tests/**/*.test.{js,ts}'],
|
||||
exclude: ['tests/e2e/**', 'tests/unit/**'],
|
||||
exclude: ['tests/e2e/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/editor/locale.js']
|
||||
include: [
|
||||
'src/editor/locale.js',
|
||||
'src/editor/MainMenu.js',
|
||||
'src/editor/contextmenu.js',
|
||||
'packages/svgcanvas/core/paint.js',
|
||||
'packages/svgcanvas/core/dataStorage.js',
|
||||
'packages/svgcanvas/core/clear.js',
|
||||
'packages/svgcanvas/core/path.js',
|
||||
'packages/svgcanvas/core/coords.js',
|
||||
'packages/svgcanvas/core/recalculate.js',
|
||||
'packages/svgcanvas/core/utilities.js',
|
||||
'packages/svgcanvas/common/util.js',
|
||||
'packages/svgcanvas/core/touch.js'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user