diff --git a/data/4-svgedit-ui/LICENSE.txt b/data/4-svgedit-ui/LICENSE.txt new file mode 100644 index 0000000..7778dc1 --- /dev/null +++ b/data/4-svgedit-ui/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2009-2022 by SVG-edit authors (see AUTHORS file) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/data/4-svgedit-ui/clipboard.cy.js b/data/4-svgedit-ui/clipboard.cy.js new file mode 100644 index 0000000..acccc88 --- /dev/null +++ b/data/4-svgedit-ui/clipboard.cy.js @@ -0,0 +1,63 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('UI - Clipboard', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('Editor - Copy and paste', () => { + cy.get('#tool_source').click({ force: true }) + + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.get('#testCircle').should('exist') + cy.get('#svg_1').should('not.exist') + cy.get('#svg_2').should('not.exist') + + // Copy. + cy.get('#testCircle').click({ force: true }).rightclick({ force: true }) + cy.get('#cmenu_canvas a[href="#copy"]').click({ force: true }) + + // Paste. + // Scrollbars fail to recenter in Cypress test. Works fine in reality. + // Thus forcing click is needed since workspace is mostly offscreen. + cy.get('#svgroot').rightclick({ force: true }) + cy.get('#cmenu_canvas a[href="#paste"]').click({ force: true }) + cy.get('#testCircle').should('exist') + cy.get('#svg_1').should('exist') + cy.get('#svg_2').should('not.exist') + + // Cut. + cy.get('#testCircle').click({ force: true }).rightclick({ force: true }) + cy.get('#cmenu_canvas a[href="#cut"]').click({ force: true }) + cy.get('#testCircle').should('not.exist') + cy.get('#svg_1').should('exist') + cy.get('#svg_2').should('not.exist') + + // Paste. + // Scrollbars fail to recenter in Cypress test. Works fine in reality. + // Thus forcing click is needed since workspace is mostly offscreen. + cy.get('#svgroot').rightclick({ force: true }) + cy.get('#cmenu_canvas a[href="#paste"]').click({ force: true }) + cy.get('#testCircle').should('not.exist') + cy.get('#svg_1').should('exist') + cy.get('#svg_2').should('exist') + + // Delete. + cy.get('#svg_2').click({ force: true }).rightclick({ force: true }) + cy.get('#cmenu_canvas a[href="#delete"]').click({ force: true }) + cy.get('#svg_1').click({ force: true }).rightclick({ force: true }) + cy.get('#cmenu_canvas a[href="#delete"]').click({ force: true }) + cy.get('#svg_1').should('not.exist') + cy.get('#svg_2').should('not.exist') + }) +}) diff --git a/data/4-svgedit-ui/control-points.cy.js b/data/4-svgedit-ui/control-points.cy.js new file mode 100644 index 0000000..ecee75a --- /dev/null +++ b/data/4-svgedit-ui/control-points.cy.js @@ -0,0 +1,34 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('UI - Control Points', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('Editor - No parameters: Drag control point of arc path', () => { + const randomOffset = () => 2 + Math.round(10 + Math.random() * 40) + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + + cy.get('#svg_1').click({ force: true }).click({ force: true }) + + cy.get('#pathpointgrip_0').trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', randomOffset(), randomOffset(), { force: true }) + .trigger('mouseup', { force: true }) + cy.get('#pathpointgrip_1').trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', randomOffset(), randomOffset(), { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#svg_1[d]').should('not.contain', 'NaN') + }) +}) diff --git a/data/4-svgedit-ui/export.cy.js b/data/4-svgedit-ui/export.cy.js new file mode 100644 index 0000000..128a5f8 --- /dev/null +++ b/data/4-svgedit-ui/export.cy.js @@ -0,0 +1,20 @@ +import { + visitAndApproveStorage, openMainMenu +} from '../../support/ui-test-helper.js' + +describe('UI - Export tests', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('Editor - No parameters: Has export button', () => { + openMainMenu() + cy.get('#tool_export') + }) + + it('Editor - No parameters: Export button clicking; dialog opens', () => { + openMainMenu() + cy.get('#tool_export').click({ force: true }) + cy.get('#dialog_content select') + }) +}) diff --git a/data/4-svgedit-ui/issue-359.cy.js b/data/4-svgedit-ui/issue-359.cy.js new file mode 100644 index 0000000..666694d --- /dev/null +++ b/data/4-svgedit-ui/issue-359.cy.js @@ -0,0 +1,26 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/359 +describe('Fix issue 359', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('can undo without throwing', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + + `, { parseSpecialCharSequences: false, force: true }) + cy.get('#tool_source_save').click({ force: true }) + cy.get('#tool_undo').click({ force: true }) + cy.get('#tool_redo').click({ force: true }) // test also redo to make the test more comprehensive + // if the undo throws an error to the console, the test will fail + }) +}) diff --git a/data/4-svgedit-ui/issue-407.cy.js b/data/4-svgedit-ui/issue-407.cy.js new file mode 100644 index 0000000..21800ef --- /dev/null +++ b/data/4-svgedit-ui/issue-407.cy.js @@ -0,0 +1,35 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/407 +describe('Fix issue 407', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + it('can enter edit on text child', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + + hello + + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.get('#svg_1').click({ force: true }).dblclick({ force: true }) + cy.get('#a_text').should('exist') + /** @todo: need to understand the reason why this test now fails */ + // cy.get('#a_text') + // .trigger('mousedown', { which: 1, force: true }) + // .trigger('mouseup', { force: true }) + // .dblclick({ force: true }) + // svgedit use the #text text field to capture the text + // cy.get('#text').type('1234', {force: true}) + // cy.get('#a_text').should('have.text', 'he1234llo') + }) +}) diff --git a/data/4-svgedit-ui/issue-408.cy.js b/data/4-svgedit-ui/issue-408.cy.js new file mode 100644 index 0000000..94639ea --- /dev/null +++ b/data/4-svgedit-ui/issue-408.cy.js @@ -0,0 +1,29 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/408 +describe('Fix issue 408', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('should not throw when showing/saving svg content', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + + + + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.get('#svg_6').click({ force: true }).dblclick({ force: true }) // change context + cy.get('#tool_source').click({ force: true }) // reopen tool_source + cy.get('#tool_source_save').should('exist') // The save button should be here if it does not throw + }) +}) diff --git a/data/4-svgedit-ui/issue-423.cy.js b/data/4-svgedit-ui/issue-423.cy.js new file mode 100644 index 0000000..a2cf7b1 --- /dev/null +++ b/data/4-svgedit-ui/issue-423.cy.js @@ -0,0 +1,33 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/423 +describe('Fix issue 423', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('should not throw when undoing the move', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + + + + + + + `, { parseSpecialCharSequences: false, force: true }) + cy.get('#tool_source_save').click({ force: true }) + cy.get('#TANK1') + .trigger('mousedown', { force: true }) + .trigger('mousemove', 50, 0, { force: true }) + .trigger('mouseup', { force: true }) + cy.get('#tool_undo').click({ force: true }) + }) +}) diff --git a/data/4-svgedit-ui/issue-660.cy.js b/data/4-svgedit-ui/issue-660.cy.js new file mode 100644 index 0000000..f4b8eaa --- /dev/null +++ b/data/4-svgedit-ui/issue-660.cy.js @@ -0,0 +1,35 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/660 +describe('Fix issue 660', function () { + beforeEach(() => { + visitAndApproveStorage() + cy.viewport(512, 512) + }) + /** @todo: reenable this test when we understand why it is passing locally but not on ci */ + it.skip('can resize text', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + hello + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.get('#a_text').should('exist') + cy.get('#a_text') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mouseup', { force: true }) + cy.get('#selectorGrip_resize_s') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', { clientX: 0, clientY: 600 }) + .trigger('mouseup', { force: true }) + // svgedit use the #text text field to capture the text + cy.get('#a_text').should('have.attr', 'transform') + .and('equal', 'matrix(1 0 0 4.54639 0 -540.825)') // Chrome 96 is matrix(1 0 0 4.17431 0 -325.367) + }) +}) diff --git a/data/4-svgedit-ui/issue-699.cy.js b/data/4-svgedit-ui/issue-699.cy.js new file mode 100644 index 0000000..98dd8fa --- /dev/null +++ b/data/4-svgedit-ui/issue-699.cy.js @@ -0,0 +1,29 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/699 +describe('Fix issue 699', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('should not throw error when undoing and redoing convert to path for a rectangle', function () { + cy.get('#tool_rect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 150, 150, { force: true }) + .trigger('mousemove', 250, 200, { force: true }) + .trigger('mouseup', { force: true }) + cy.get('#tool_topath') // Check if undo redo is correct for tool_topath with tool_rect + .click({ force: true }) + cy.get('#tool_undo') + .click({ force: true }) + cy.get('#tool_redo') + .click({ force: true }) + cy.get('#tool_undo') // Do twice just to make sure + .click({ force: true }) + cy.get('#tool_redo') + .click({ force: true }) + }) +}) diff --git a/data/4-svgedit-ui/issue-726.cy.js b/data/4-svgedit-ui/issue-726.cy.js new file mode 100644 index 0000000..5e57bd6 --- /dev/null +++ b/data/4-svgedit-ui/issue-726.cy.js @@ -0,0 +1,41 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/726 +describe('Fix issue 726', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('Send forward and send backward should move one layer at a time', function () { + cy.get('#tool_rect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 250, 250, { force: true }) + .trigger('mousemove', 350, 350, { force: true }) + .trigger('mouseup', { force: true }) + cy.wait(300) + cy.get('#tool_rect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 10, 0, { force: true }) + .trigger('mousemove', 100, 100, { force: true }) + .trigger('mouseup', { force: true }) + cy.wait(300) + cy.get('#tool_rect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 10, 10, { force: true }) + .trigger('mousemove', 100, 100, { force: true }) + .trigger('mouseup', { force: true }) + cy.wait(300) + cy.get('#svg_3') + .rightclick(0, 0, { force: true }) + cy.get('a:contains("Send Backward")').click({ force: true }) + cy.get('#svg_2').should(($div) => { + const id = $div[0].previousElementSibling.id + assert.equal(id, 'svg_3') + }) + }) +}) diff --git a/data/4-svgedit-ui/issue-752.cy.js b/data/4-svgedit-ui/issue-752.cy.js new file mode 100644 index 0000000..585d9e7 --- /dev/null +++ b/data/4-svgedit-ui/issue-752.cy.js @@ -0,0 +1,39 @@ +import { + visitAndApproveStorage +} from '../../../support/ui-test-helper.js' + +// See https://github.com/SVG-Edit/svgedit/issues/752 +describe('Fix issue 752', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('Moving an unsnapped shape will not cause selector box misalignment', function () { + cy.get('#tool_rect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 12, 12, { force: true }) + .trigger('mousemove', 99, 99, { force: true }) + .trigger('mouseup', { force: true }) + cy.wait(300) + cy.get('#svg_1') + .click({ force: true }) + cy.get('#tool_editor_prefs') + .click({ force: true }) + cy.get('#grid_snapping_step') + .then(elem => { + elem.val('35') + }) + cy.wait(300) + cy.get('#grid_snapping_on') + .click({ force: true }) + cy.get('#tool_prefs_save') + .click({ force: true }) + cy.get('#svg_1') + .trigger('mousedown', 20, 20, { force: true }) + .trigger('mousemove', 203, 205, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#selectedBox0').should('have.attr', 'd', 'M192,194 L284,194 284,286 192,286z') + }) +}) diff --git a/data/4-svgedit-ui/scenario.cy.js b/data/4-svgedit-ui/scenario.cy.js new file mode 100644 index 0000000..609da62 --- /dev/null +++ b/data/4-svgedit-ui/scenario.cy.js @@ -0,0 +1,209 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use text tools of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text', function () { + cy.get('#tool_text') + .click({ force: true }) + cy.get('#svgroot') + .trigger('mousedown', { clientX: 400, clientY: 400, force: true }) + .trigger('mouseup', { force: true }) + // svgedit use the #text text field to capture the text + cy.get('#text').type('AB', { force: true }) + // force text position for snapshot tests being consistent on CI/Interactive + cy.get('#selected_x').shadow().find('elix-number-spin-box').eq(0).shadow().find('#inner').eq(0).type('{selectall}200', { force: true }) + cy.get('#selected_y').shadow().find('elix-number-spin-box').eq(0).shadow().find('#inner').eq(0).type('{selectall}200', { force: true }) + cy.svgSnapshot() + // cy.get('#svg_1').should('exist') + }) + it('check tool_clone', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_clone') + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_italic', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_italic') + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_bold', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_bold') + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_change_x_y_coordinate', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 25; n++) { + cy.get('#selected_x').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#selected_y').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_text_change_font_size', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#font_size').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_text_change_stroke_width', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#stroke_width').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_change_stoke_fill_color', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#stroke_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(51).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.get('#fill_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#fill_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(3).click({ force: true }) + cy.get('#fill_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_change_blur', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#blur').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_text_change_opacity', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#opacity').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_text_align_to_page', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true }) + cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_change_class', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#elem_class').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('svg_2_class{enter}', { force: true }) + cy.get('#svg_2') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_text_change_id', function () { + cy.get('#svg_2').click({ force: true }).click({ force: true }) + cy.get('#elem_id').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('_id{enter}', { force: true }) + cy.get('#svg_2_id') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_text_delete', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_delete').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_change_font_family', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_font_family').shadow().find('select').select('Serif', { force: true }) + cy.svgSnapshot() + }) + it('check tool_text_decoration_underline', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_text_decoration_underline') + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_decoration_linethrough', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_text_decoration_linethrough') + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_text_decoration_overline', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_text_decoration_overline') + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_letter_spacing', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#tool_letter_spacing').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_word_spacing', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 15; n++) { + cy.get('#tool_word_spacing').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_text_length', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 20; n++) { + cy.get('#tool_text_length').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_length_adjust', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_length_adjust').shadow().find('select').select(1, { force: true }) + cy.svgSnapshot() + }) + it('check tool_text_change_rotation', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 6; n++) { + cy.get('#angle').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.get('#svg_1').should('have.attr', 'transform') + .and('match', /rotate\(30/) + // snapshot removed below for inconsistency between local and CI tests. + // cy.svgSnapshot() + }) +}) diff --git a/data/4-svgedit-ui/scenario1.cy.js b/data/4-svgedit-ui/scenario1.cy.js new file mode 100644 index 0000000..dcdc94c --- /dev/null +++ b/data/4-svgedit-ui/scenario1.cy.js @@ -0,0 +1,60 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('check tool shape and image of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_shape', function () { + cy.get('#tool_shapelib').shadow().find('.overall').eq(0).click({ force: true }) + cy.get('[data-shape="heart"]').click({ force: true }) + cy.get('#svgroot') + .trigger('mousemove', { clientX: 400, clientY: 400, force: true }) + .trigger('mousedown', { clientX: 400, clientY: 400, force: true }) + .trigger('mousemove', { clientX: 20, clientY: 20, force: true }) + .trigger('mouseup', { force: true }) + cy.get('#selectorGrip_rotate') + .trigger('mousedown', { force: true }) + .trigger('mousemove', { clientX: 20, clientY: 20, force: true }) + .trigger('mouseup', { force: true }) + // issue with snapshot not being consistent on CI/Interactive + // cy.svgSnapshot() + // so we use typical DOM tests to validate + cy.get('#svg_1').should('have.attr', 'd') + + // cy.get('#a_text').should('have.attr', 'transform') + // .and('equal', 'matrix(1 0 0 4.54639 0 -540.825)') // Chrome 96 is matrix(1 0 0 4.17431 0 -325.367) + }) + it('check tool_image', function () { + cy.get('#tool_image').click({ force: true }) + cy.get('#svgroot') + .trigger('mousedown', { clientX: 100, clientY: 100, force: true }) + .trigger('mousemove', { clientX: 120, clientY: 120, force: true }) + .trigger('mouseup', { force: true }) + // eslint-disable-next-line promise/catch-or-return + cy.window() + // eslint-disable-next-line promise/always-return + .then(($win) => { + cy.stub($win, 'prompt').returns('./images/logo.svg') + cy.contains('OK') + }) + // issue with snapshot not being consistent on CI/Interactive + // cy.svgSnapshot() + // so we use typical DOM tests to validate + cy.get('#svg_2').should('have.attr', 'xlink:href').and('equal', './images/logo.svg') + }) +}) diff --git a/data/4-svgedit-ui/scenario2.cy.js b/data/4-svgedit-ui/scenario2.cy.js new file mode 100644 index 0000000..78832de --- /dev/null +++ b/data/4-svgedit-ui/scenario2.cy.js @@ -0,0 +1,120 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use ellipse and circle of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_circle', function () { + cy.get('#tool_circle') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 200, 200, { force: true }) + .trigger('mousemove', 300, 200, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_fhellipse', function () { + cy.get('#tool_fhellipse') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 400, 200, { force: true }).wait(100) + .trigger('mousemove', { force: true, pageX: 400, pageY: 200 }).wait(100) + .trigger('mousemove', { force: true, pageX: 400, pageY: 300 }).wait(100) + .trigger('mousemove', { force: true, pageX: 300, pageY: 400 }).wait(100) + .trigger('mousemove', { force: true, pageX: 200, pageY: 200 }).wait(100) + .trigger('mouseup', 200, 100, { force: true }) + cy.svgSnapshot() + }) + it('check tool_ellipse', function () { + cy.get('#tool_ellipse').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 100, 300, { force: true }) + .trigger('mousemove', 200, 200, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_circle_change_fill_color', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#js-se-palette').find('.square').eq(8) + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_circle_change_opacity', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#opacity').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_ellipse_change_rotation', function () { + cy.get('#svg_3').click({ force: true }) + for (let n = 0; n < 5; n++) { + cy.get('#angle').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_ellipse_change_blur', function () { + cy.get('#svg_3').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#blur').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_ellipse_change_cx_cy_coordinate', function () { + cy.get('#svg_3').click({ force: true }) + for (let n = 0; n < 20; n++) { + cy.get('#ellipse_cx').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 20; n++) { + cy.get('#ellipse_cy').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_ellipse_change_rx_ry_radius', function () { + cy.get('#svg_3').click({ force: true }) + for (let n = 0; n < 20; n++) { + cy.get('#ellipse_rx').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 20; n++) { + cy.get('#ellipse_ry').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_ellipse_bring_to_back', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_move_bottom').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_ellipse_bring_to_front', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_move_top').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_ellipse_clone', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_clone').click({ force: true }) + cy.svgSnapshot() + }) +}) diff --git a/data/4-svgedit-ui/scenario3.cy.js b/data/4-svgedit-ui/scenario3.cy.js new file mode 100644 index 0000000..8923007 --- /dev/null +++ b/data/4-svgedit-ui/scenario3.cy.js @@ -0,0 +1,96 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use path tools of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_path', function () { + cy.get('#tool_path') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_path_change_node_xy', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#svg_1').dblclick({ force: true }) + for (let n = 0; n < 25; n++) { + cy.get('#path_node_x').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#path_node_y').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_path_change_seg_type', function () { + // cy.get('#svg_1').click({ force: true }) + cy.get('#svg_1').dblclick({ force: true }) + cy.get('#seg_type').shadow().find('select').select('6', { force: true }).should('have.value', '6') + cy.get('#ctrlpointgrip_3c1') + .trigger('mousedown', { force: true }) + .trigger('mousemove', 130, 175, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_path_change_clone_node', function () { + // cy.get('#svg_1').click({ force: true }) + cy.get('#svg_1').dblclick({ force: true }) + cy.get('#tool_node_clone').click({ force: true }) + cy.get('#pathpointgrip_4') + .trigger('mousedown', { force: true }) + .trigger('mousemove', 130, 175, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_path_openclose', function () { + cy.get('#tool_select').click({ force: true }) + cy.get('#svg_1').click({ force: true }) + cy.get('#svg_1').dblclick({ force: true }) + cy.get('#tool_openclose_path').click({ force: true }) + cy.svgSnapshot() + }) + /* it('check tool_path_add_subpath', function () { + cy.get('#tool_add_subpath').click({ force: true }); + cy.get('#svgcontent') + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }); + cy.get('#tool_select').click({ force: true }); + cy.svgSnapshot(); + }); */ +}) diff --git a/data/4-svgedit-ui/scenario4.cy.js b/data/4-svgedit-ui/scenario4.cy.js new file mode 100644 index 0000000..357bb7e --- /dev/null +++ b/data/4-svgedit-ui/scenario4.cy.js @@ -0,0 +1,160 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use rect/square tools of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_rect', function () { + cy.get('#tool_rect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 150, 150, { force: true }) + .trigger('mousemove', 250, 200, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_fhrect', function () { + cy.get('#tool_fhrect') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 200, 80, { force: true }) + .trigger('mousemove', 320, 80, { force: true }) + .trigger('mousemove', 320, 180, { force: true }) + .trigger('mousemove', 200, 180, { force: true }) + .trigger('mousemove', 200, 80, { force: true }) + .trigger('mouseup', 200, 80, { force: true }) + cy.svgSnapshot() + }) + it('check tool_square', function () { + cy.get('#tool_square').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mousemove', 125, 200, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_rect_change_fill_color', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#js-se-palette').find('.square').eq(8) + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_rect_change_rotation', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 5; n++) { + cy.get('#angle').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_rect_change_blur', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#blur').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_rect_change_opacity', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#opacity').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_fhrect_change_x_y_coordinate', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 25; n++) { + cy.get('#selected_x').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#selected_y').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_fhrect_change_width_height', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 25; n++) { + cy.get('#rect_width').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#rect_height').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_square_clone', function () { + cy.get('#svg_3').click({ force: true }) + cy.get('#tool_clone').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_square_bring_to_back', function () { + cy.get('#svg_3').click({ force: true }) + cy.get('#tool_move_bottom').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_square_bring_to_front', function () { + cy.get('#svg_3').click({ force: true }) + cy.get('#tool_move_top').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_square_change_corner_radius', function () { + cy.get('#svg_4').click({ force: true }) + for (let n = 0; n < 25; n++) { + cy.get('#rect_rx').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_rect_change_to_path', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_topath').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_rect_delete', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_delete').click({ force: true }) + cy.get('#svg_3').click({ force: true }) + cy.get('#tool_delete').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_rect_change_class', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#elem_class').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('svg_2_class{enter}', { force: true }) + cy.get('#svg_2') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_rect_change_id', function () { + cy.get('#svg_2').click({ force: true }).click({ force: true }) + cy.get('#elem_id').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('_id{enter}', { force: true }) + cy.get('#svg_2_id') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) +}) diff --git a/data/4-svgedit-ui/scenario5.cy.js b/data/4-svgedit-ui/scenario5.cy.js new file mode 100644 index 0000000..ff39df0 --- /dev/null +++ b/data/4-svgedit-ui/scenario5.cy.js @@ -0,0 +1,149 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use line tools of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_line', function () { + cy.get('#tool_line') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousemove', 200, 200, { force: true }) + .trigger('mousedown', 200, 200, { force: true }) + .trigger('mousemove', 250, 250, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_line_change_class', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#elem_class').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('svg_1_class{enter}', { force: true }) + cy.get('#svg_1') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_1_class') + }) + }) + it('check tool_line_change_id', function () { + cy.get('#svg_1').click({ force: true }).click({ force: true }) + cy.get('#elem_id').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('_id{enter}', { force: true }) + cy.get('#svg_1_id') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_1_class') + }) + }) + it('check tool_line_change_rotation', function () { + cy.get('#svg_1_id').click({ force: true }) + for (let n = 0; n < 5; n++) { + cy.get('#angle').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_line_change_blur', function () { + cy.get('#svg_1_id').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#blur').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_line_change_opacity', function () { + cy.get('#svg_1_id').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#opacity').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_line_delete', function () { + cy.get('#svg_1_id').click({ force: true }) + cy.get('#tool_delete').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_line_clone', function () { + cy.get('#tool_line') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousemove', 200, 200, { force: true }) + .trigger('mousedown', 200, 200, { force: true }) + .trigger('mousemove', 250, 250, { force: true }) + .trigger('mouseup', { force: true }) + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_clone').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_line_bring_to_back', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_move_bottom').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_line_bring_to_front', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#tool_move_top').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_line_change_x_y_coordinate', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 25; n++) { + cy.get('#line_x1').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#line_y1').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#line_x2').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + for (let n = 0; n < 25; n++) { + cy.get('#line_y2').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_line_change_stroke_width', function () { + cy.get('#svg_2').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#stroke_width').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_line_change_stoke_color', function () { + cy.get('#svg_3').click({ force: true }) + cy.get('#stroke_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(9).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_line_align_to_page', function () { + cy.get('#svg_3').click({ force: true }) + cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true }) + cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) +}) diff --git a/data/4-svgedit-ui/scenario6.cy.js b/data/4-svgedit-ui/scenario6.cy.js new file mode 100644 index 0000000..dbcd9a6 --- /dev/null +++ b/data/4-svgedit-ui/scenario6.cy.js @@ -0,0 +1,146 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use polygon tools of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon', function () { + cy.get('#tool_polygon') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 325, 250, { force: true }) + .trigger('mousemove', 325, 345, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon_clone', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_clone').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon_change_class', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#elem_class').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('svg_2_class{enter}', { force: true }) + cy.get('#svg_2') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_polygon_change_id', function () { + cy.get('#svg_2').click({ force: true }).click({ force: true }) + cy.get('#elem_id').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('_id{enter}', { force: true }) + cy.get('#svg_2_id') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_polygon_change_rotation', function () { + cy.get('#svg_2_id').click({ force: true }) + for (let n = 0; n < 5; n++) { + cy.get('#angle').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_polygon_change_blur', function () { + cy.get('#svg_2_id').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#blur').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_polygon_change_opacity', function () { + cy.get('#svg_2_id').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#opacity').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_polygon_bring_to_back', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_move_bottom').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon_bring_to_front', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_move_top').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon_delete', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_delete').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon_align_to_page', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true }) + cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) + /* it('check tool_polygon_change_x_y_coordinate', function () { + cy.get('#svg_1').click({ force: true }); + for(let n = 0; n < 25; n ++){ + cy.get('#selected_x').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }); + } + for(let n = 0; n < 25; n ++){ + cy.get('#selected_y').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }); + } + cy.svgSnapshot(); + }); */ + it('check tool_polygon_change_stroke_width', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#stroke_width').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_polygon_change_stoke_fill_color', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#stroke_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(51).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.get('#fill_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#fill_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(3).click({ force: true }) + cy.get('#fill_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_polygon_change_sides', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#polySides').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) +}) diff --git a/data/4-svgedit-ui/scenario7.cy.js b/data/4-svgedit-ui/scenario7.cy.js new file mode 100644 index 0000000..a7f982a --- /dev/null +++ b/data/4-svgedit-ui/scenario7.cy.js @@ -0,0 +1,134 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('use star tools of svg-edit', function () { + before(() => { + visitAndApproveStorage() + }) + + it('check tool_source_set', function () { + cy.get('#tool_source').click({ force: true }) + cy.get('#svg_source_textarea') + .type('{selectall}', { force: true }) + .type(` + + Layer 1 + + `, { force: true, parseSpecialCharSequences: false }) + cy.get('#tool_source_save').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star', function () { + cy.get('#tool_star') + .click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 300, 150, { force: true }) + .trigger('mousemove', 300, 250, { force: true }) + .trigger('mouseup', { force: true }) + cy.svgSnapshot() + }) + it('check tool_star_clone', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_clone').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star_change_class', function () { + cy.get('#svg_2').click({ force: true }) + cy.get('#elem_class').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('svg_2_class{enter}', { force: true }) + cy.get('#svg_2') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_star_change_id', function () { + cy.get('#svg_2').click({ force: true }).click({ force: true }) + cy.get('#elem_id').shadow().find('elix-input').eq(0).shadow().find('#inner').eq(0) + .type('_id{enter}', { force: true }) + cy.get('#svg_2_id') + .should('satisfy', ($el) => { + const classList = Array.from($el[0].classList) + return classList.includes('svg_2_class') + }) + }) + it('check tool_star_change_rotation', function () { + cy.get('#svg_2_id').click({ force: true }) + for (let n = 0; n < 5; n++) { + cy.get('#angle').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_star_change_blur', function () { + cy.get('#svg_2_id').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#blur').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_star_change_opacity', function () { + cy.get('#svg_2_id').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#opacity').shadow().find('elix-number-spin-box').eq(0).shadow().find('#downButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_star_bring_to_back', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_move_bottom').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star_bring_to_front', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_move_top').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star_delete', function () { + cy.get('#svg_2_id').click({ force: true }) + cy.get('#tool_delete').click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star_align_to_page', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true }) + cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star_change_stroke_width', function () { + cy.get('#svg_1').click({ force: true }) + for (let n = 0; n < 10; n++) { + cy.get('#stroke_width').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + } + cy.svgSnapshot() + }) + it('check tool_star_change_stoke_fill_color', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#stroke_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(51).click({ force: true }) + cy.get('#stroke_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.get('#fill_color').shadow().find('#picker').eq(0).click({ force: true }) + cy.get('#fill_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('.QuickColor').eq(3).click({ force: true }) + cy.get('#fill_color').shadow().find('#color_picker').eq(0) + .find('#jGraduate_colPick').eq(0).find('#jPicker-table').eq(0) + .find('#Ok').eq(0).click({ force: true }) + cy.svgSnapshot() + }) + it('check tool_star_change_sides', function () { + cy.get('#svg_1').click({ force: true }) + cy.get('#starNumPoints').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }) + cy.svgSnapshot() + }) +}) diff --git a/data/4-svgedit-ui/tool-selection.cy.js b/data/4-svgedit-ui/tool-selection.cy.js new file mode 100644 index 0000000..98510bf --- /dev/null +++ b/data/4-svgedit-ui/tool-selection.cy.js @@ -0,0 +1,17 @@ +import { + visitAndApproveStorage +} from '../../support/ui-test-helper.js' + +describe('UI - Tool selection', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('should set rectangle selection by click', function () { + cy.get('#tools_rect') + .should('not.have.attr', 'pressed') + cy.get('#tools_rect') + .trigger('click', { force: true }) + .should('have.attr', 'pressed') + }) +}) diff --git a/data/5-svgedit-unit/browser-bugs/removeItem-setAttribute.cy.js b/data/5-svgedit-unit/browser-bugs/removeItem-setAttribute.cy.js new file mode 100644 index 0000000..0a81639 --- /dev/null +++ b/data/5-svgedit-unit/browser-bugs/removeItem-setAttribute.cy.js @@ -0,0 +1,10 @@ +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 + const elem = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + elem.setAttribute('transform', 'matrix(1,0,0,1,0,0)') + elem.transform.baseVal.removeItem(0) + elem.removeAttribute('transform') + assert.equal(elem.hasAttribute('transform'), false) + }) +}) diff --git a/data/5-svgedit-unit/contextmenu.cy.js b/data/5-svgedit-unit/contextmenu.cy.js new file mode 100644 index 0000000..056d6d7 --- /dev/null +++ b/data/5-svgedit-unit/contextmenu.cy.js @@ -0,0 +1,58 @@ +import * as contextmenu from '../../../src/editor/contextmenu.js' + +describe('contextmenu', function () { + /** + * Tear down tests, resetting custom menus. + * @returns {void} + */ + afterEach(() => { + contextmenu.resetCustomMenus() + }) + + it('Test svgedit.contextmenu package', function () { + assert.ok(contextmenu, 'contextmenu registered correctly') + assert.ok(contextmenu.add, 'add registered correctly') + assert.ok(contextmenu.hasCustomHandler, 'contextmenu hasCustomHandler registered correctly') + assert.ok(contextmenu.getCustomHandler, 'contextmenu getCustomHandler registered correctly') + }) + + it('Test svgedit.contextmenu does not add invalid menu item', function () { + assert.throws( + () => contextmenu.add({ id: 'justanid' }), + null, null, + 'menu item with just an id is invalid' + ) + + assert.throws( + () => contextmenu.add({ id: 'idandlabel', label: 'anicelabel' }), + null, null, + 'menu item with just an id and label is invalid' + ) + + assert.throws( + () => contextmenu.add({ id: 'idandlabel', label: 'anicelabel', action: 'notafunction' }), + null, null, + 'menu item with action that is not a function is invalid' + ) + }) + + it('Test svgedit.contextmenu adds valid menu item', function () { + const validItem = { id: 'valid', label: 'anicelabel', action () { /* empty fn */ } } + contextmenu.add(validItem) + + assert.ok(contextmenu.hasCustomHandler('valid'), 'Valid menu item is added.') + assert.equal(contextmenu.getCustomHandler('valid'), validItem.action, 'Valid menu action is added.') + }) + + it('Test svgedit.contextmenu rejects valid duplicate menu item id', function () { + const validItem1 = { id: 'valid', label: 'anicelabel', action () { /* empty fn */ } } + const validItem2 = { id: 'valid', label: 'anicelabel', action () { /* empty fn */ } } + contextmenu.add(validItem1) + + assert.throws( + () => contextmenu.add(validItem2), + null, null, + 'duplicate menu item is rejected.' + ) + }) +}) diff --git a/data/5-svgedit-unit/coords.cy.js b/data/5-svgedit-unit/coords.cy.js new file mode 100644 index 0000000..2a8f7fc --- /dev/null +++ b/data/5-svgedit-unit/coords.cy.js @@ -0,0 +1,307 @@ +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as utilities from '../../../packages/svgcanvas/core/utilities.js' +import * as coords from '../../../packages/svgcanvas/core/coords.js' + +describe('coords', function () { + let elemId = 1 + + const root = document.createElement('div') + root.id = 'root' + root.style.visibility = 'hidden' + document.body.append(root) + + /** + * Set up tests with mock data. + * @returns {void} + */ + beforeEach(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) + + // Mock out editor context. + utilities.init( + /** + * @implements {module:utilities.EditorContext} + */ + { + getSvgRoot: () => { return this.svg }, + getDOMDocument () { return null }, + getDOMContainer () { return null } + } + ) + coords.init( + /** + * @implements {module:coords.EditorContext} + */ + { + getGridSnapping () { return false }, + getDrawing () { + return { + getNextId () { return String(elemId++) } + } + } + } + ) + }) + + /** + * Tear down tests, removing elements. + * @returns {void} + */ + afterEach(function () { + while (this.svg.hasChildNodes()) { + this.svg.firstChild.remove() + } + }) + + it('Test remapElement(translate) for rect', function () { + const rect = document.createElementNS(NS.SVG, 'rect') + rect.setAttribute('x', '200') + rect.setAttribute('y', '150') + rect.setAttribute('width', '250') + rect.setAttribute('height', '120') + this.svg.append(rect) + + const attrs = { + x: '200', + y: '150', + width: '125', + height: '75' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 100; m.f = -50 + + coords.remapElement(rect, attrs, m) + + assert.equal(rect.getAttribute('x'), '300') + assert.equal(rect.getAttribute('y'), '100') + assert.equal(rect.getAttribute('width'), '125') + assert.equal(rect.getAttribute('height'), '75') + }) + + it('Test remapElement(scale) for rect', function () { + const rect = document.createElementNS(NS.SVG, 'rect') + rect.setAttribute('width', '250') + rect.setAttribute('height', '120') + this.svg.append(rect) + + const attrs = { + x: '0', + y: '0', + width: '250', + height: '120' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 2; m.b = 0 + m.c = 0; m.d = 0.5 + m.e = 0; m.f = 0 + + coords.remapElement(rect, attrs, m) + + assert.equal(rect.getAttribute('x'), '0') + assert.equal(rect.getAttribute('y'), '0') + assert.equal(rect.getAttribute('width'), '500') + assert.equal(rect.getAttribute('height'), '60') + }) + + it('Test remapElement(translate) for circle', function () { + const circle = document.createElementNS(NS.SVG, 'circle') + circle.setAttribute('cx', '200') + circle.setAttribute('cy', '150') + circle.setAttribute('r', '125') + this.svg.append(circle) + + const attrs = { + cx: '200', + cy: '150', + r: '125' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 100; m.f = -50 + + coords.remapElement(circle, attrs, m) + + assert.equal(circle.getAttribute('cx'), '300') + assert.equal(circle.getAttribute('cy'), '100') + assert.equal(circle.getAttribute('r'), '125') + }) + + it('Test remapElement(scale) for circle', function () { + const circle = document.createElementNS(NS.SVG, 'circle') + circle.setAttribute('cx', '200') + circle.setAttribute('cy', '150') + circle.setAttribute('r', '250') + this.svg.append(circle) + + const attrs = { + cx: '200', + cy: '150', + r: '250' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 2; m.b = 0 + m.c = 0; m.d = 0.5 + m.e = 0; m.f = 0 + + coords.remapElement(circle, attrs, m) + + assert.equal(circle.getAttribute('cx'), '400') + assert.equal(circle.getAttribute('cy'), '75') + // Radius is the minimum that fits in the new bounding box. + assert.equal(circle.getAttribute('r'), '125') + }) + + it('Test remapElement(translate) for ellipse', function () { + const ellipse = document.createElementNS(NS.SVG, 'ellipse') + ellipse.setAttribute('cx', '200') + ellipse.setAttribute('cy', '150') + ellipse.setAttribute('rx', '125') + ellipse.setAttribute('ry', '75') + this.svg.append(ellipse) + + const attrs = { + cx: '200', + cy: '150', + rx: '125', + ry: '75' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 100; m.f = -50 + + coords.remapElement(ellipse, attrs, m) + + assert.equal(ellipse.getAttribute('cx'), '300') + assert.equal(ellipse.getAttribute('cy'), '100') + assert.equal(ellipse.getAttribute('rx'), '125') + assert.equal(ellipse.getAttribute('ry'), '75') + }) + + it('Test remapElement(scale) for ellipse', function () { + const ellipse = document.createElementNS(NS.SVG, 'ellipse') + ellipse.setAttribute('cx', '200') + ellipse.setAttribute('cy', '150') + ellipse.setAttribute('rx', '250') + ellipse.setAttribute('ry', '120') + this.svg.append(ellipse) + + const attrs = { + cx: '200', + cy: '150', + rx: '250', + ry: '120' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 2; m.b = 0 + m.c = 0; m.d = 0.5 + m.e = 0; m.f = 0 + + coords.remapElement(ellipse, attrs, m) + + assert.equal(ellipse.getAttribute('cx'), '400') + assert.equal(ellipse.getAttribute('cy'), '75') + assert.equal(ellipse.getAttribute('rx'), '500') + assert.equal(ellipse.getAttribute('ry'), '60') + }) + + it('Test remapElement(translate) for line', function () { + const line = document.createElementNS(NS.SVG, 'line') + line.setAttribute('x1', '50') + line.setAttribute('y1', '100') + line.setAttribute('x2', '120') + line.setAttribute('y2', '200') + this.svg.append(line) + + const attrs = { + x1: '50', + y1: '100', + x2: '120', + y2: '200' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 100; m.f = -50 + + coords.remapElement(line, attrs, m) + + assert.equal(line.getAttribute('x1'), '150') + assert.equal(line.getAttribute('y1'), '50') + assert.equal(line.getAttribute('x2'), '220') + assert.equal(line.getAttribute('y2'), '150') + }) + + it('Test remapElement(scale) for line', function () { + const line = document.createElementNS(NS.SVG, 'line') + line.setAttribute('x1', '50') + line.setAttribute('y1', '100') + line.setAttribute('x2', '120') + line.setAttribute('y2', '200') + this.svg.append(line) + + const attrs = { + x1: '50', + y1: '100', + x2: '120', + y2: '200' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 2; m.b = 0 + m.c = 0; m.d = 0.5 + m.e = 0; m.f = 0 + + coords.remapElement(line, attrs, m) + + assert.equal(line.getAttribute('x1'), '100') + assert.equal(line.getAttribute('y1'), '50') + assert.equal(line.getAttribute('x2'), '240') + assert.equal(line.getAttribute('y2'), '100') + }) + + it('Test remapElement(translate) for text', function () { + const text = document.createElementNS(NS.SVG, 'text') + text.setAttribute('x', '50') + text.setAttribute('y', '100') + this.svg.append(text) + + const attrs = { + x: '50', + y: '100' + } + + // Create a translate. + const m = this.svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 100; m.f = -50 + + coords.remapElement(text, attrs, m) + + assert.equal(text.getAttribute('x'), '150') + assert.equal(text.getAttribute('y'), '50') + }) +}) diff --git a/data/5-svgedit-unit/draw.cy.js b/data/5-svgedit-unit/draw.cy.js new file mode 100644 index 0000000..af49b76 --- /dev/null +++ b/data/5-svgedit-unit/draw.cy.js @@ -0,0 +1,787 @@ +import 'pathseg' +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as draw from '../../../packages/svgcanvas/core/draw.js' +import * as units from '../../../packages/svgcanvas/core/units.js' + +describe('draw.Drawing', function () { + const addOwnSpies = (obj) => { + const methods = Object.keys(obj) + methods.forEach((method) => { + cy.spy(obj, method) + }) + } + + const LAYER_CLASS = draw.Layer.CLASS_NAME + const NONCE = 'foo' + const LAYER1 = 'Layer 1' + const LAYER2 = 'Layer 2' + const LAYER3 = 'Layer 3' + const PATH_ATTR = { + // clone will convert relative to absolute, so the test for equality fails. + // d: 'm7.38867,57.38867c0,-27.62431 22.37569,-50 50,-50c27.62431,0 50,22.37569 50,50c0,27.62431 -22.37569,50 -50,50c-27.62431,0 -50,-22.37569 -50,-50z', + d: 'M7.389,57.389C7.389,29.764 29.764,7.389 57.389,7.389C85.013,7.389 107.389,29.764 107.389,57.389C107.389,85.013 85.013,107.389 57.389,107.389C29.764,107.389 7.389,85.013 7.389,57.389z', + transform: 'rotate(45 57.388671875000036,57.388671874999986) ', + 'stroke-width': '5', + stroke: '#660000', + fill: '#ff0000' + } + + units.init( + /** + * @implements {module:units.ElementContainer} + */ + { + // used by units.shortFloat - call path: cloneLayer -> copyElem -> convertPath -> pathDSegment -> shortFloat + getRoundDigits () { return 3 } + } + ) + + // Simplifying from svgcanvas.js usage + const idprefix = 'svg_' + + const getCurrentDrawing = function () { + return currentDrawing_ + } + const setCurrentGroup = () => { /* empty fn */ } + draw.init( + /** + * @implements {module:draw.DrawCanvasInit} + */ + { + getCurrentDrawing, + setCurrentGroup + } + ) + + /** + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function createSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element) + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value) + }) + return elem + } + + const setupSVGWith3Layers = function (svgElem) { + const layer1 = document.createElementNS(NS.SVG, 'g') + const layer1Title = document.createElementNS(NS.SVG, 'title') + layer1Title.append(LAYER1) + layer1.append(layer1Title) + svgElem.append(layer1) + + const layer2 = document.createElementNS(NS.SVG, 'g') + const layer2Title = document.createElementNS(NS.SVG, 'title') + layer2Title.append(LAYER2) + layer2.append(layer2Title) + svgElem.append(layer2) + + const layer3 = document.createElementNS(NS.SVG, 'g') + const layer3Title = document.createElementNS(NS.SVG, 'title') + layer3Title.append(LAYER3) + layer3.append(layer3Title) + svgElem.append(layer3) + + return [layer1, layer2, layer3] + } + + const createSomeElementsInGroup = function (group) { + group.append( + createSVGElement({ + element: 'path', + attr: PATH_ATTR + }), + // createSVGElement({ + // element: 'path', + // attr: {d: 'M0,1L2,3'} + // }), + createSVGElement({ + element: 'rect', + attr: { x: '0', y: '1', width: '5', height: '10' } + }), + createSVGElement({ + element: 'line', + attr: { x1: '0', y1: '1', x2: '5', y2: '6' } + }) + ) + + const g = createSVGElement({ + element: 'g', + attr: {} + }) + g.append(createSVGElement({ + element: 'rect', + attr: { x: '0', y: '1', width: '5', height: '10' } + })) + group.append(g) + return 4 + } + + const cleanupSVG = function (svgElem) { + while (svgElem.firstChild) { svgElem.firstChild.remove() } + } + + let sandbox; let currentDrawing_; let svg; let svgN + beforeEach(() => { + sandbox = document.createElement('div') + sandbox.id = 'sandbox' + sandbox.style.visibility = 'hidden' + + svg = document.createElementNS(NS.SVG, 'svg') + // Firefox throws exception in getBBox() when svg is not attached to DOM. + sandbox.append(svg) + + // Set up with nonce. + svgN = document.createElementNS(NS.SVG, 'svg') + svgN.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE) + svgN.setAttributeNS(NS.SE, 'se:nonce', NONCE) + + const svgContent = document.createElementNS(NS.SVG, 'svg') + currentDrawing_ = new draw.Drawing(svgContent, idprefix) + }) + + it('Test draw module', function () { + assert.ok(draw) + assert.equal(typeof draw, typeof {}) + + assert.ok(draw.Drawing) + assert.equal(typeof draw.Drawing, typeof function () { /* empty fn */ }) + }) + + it('Test document creation', function () { + let doc + try { + doc = new draw.Drawing() + assert.ok(false, 'Created drawing without a valid element') + } catch (e) { + assert.ok(true) + } + + try { + doc = new draw.Drawing(svg) + assert.ok(doc) + assert.equal(typeof doc, typeof {}) + } catch (e) { + assert.ok(false, 'Could not create document from valid element: ' + e) + } + }) + + it('Test nonce', function () { + let doc = new draw.Drawing(svg) + assert.equal(doc.getNonce(), '') + + doc = new draw.Drawing(svgN) + assert.equal(doc.getNonce(), NONCE) + assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE) + + doc.clearNonce() + assert.ok(!doc.getNonce()) + assert.ok(!doc.getSvgElem().getAttributeNS(NS.SE, 'se:nonce')) + + doc.setNonce(NONCE) + assert.equal(doc.getNonce(), NONCE) + assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE) + }) + + it('Test getId() and getNextId() without nonce', function () { + const elem2 = document.createElementNS(NS.SVG, 'circle') + elem2.id = 'svg_2' + svg.append(elem2) + + const doc = new draw.Drawing(svg) + + assert.equal(doc.getId(), 'svg_0') + + assert.equal(doc.getNextId(), 'svg_1') + assert.equal(doc.getId(), 'svg_1') + + assert.equal(doc.getNextId(), 'svg_3') + assert.equal(doc.getId(), 'svg_3') + + assert.equal(doc.getNextId(), 'svg_4') + assert.equal(doc.getId(), 'svg_4') + // clean out svg document + cleanupSVG(svg) + }) + + it('Test getId() and getNextId() with prefix without nonce', function () { + const prefix = 'Bar-' + const doc = new draw.Drawing(svg, prefix) + + assert.equal(doc.getId(), prefix + '0') + + assert.equal(doc.getNextId(), prefix + '1') + assert.equal(doc.getId(), prefix + '1') + + assert.equal(doc.getNextId(), prefix + '2') + assert.equal(doc.getId(), prefix + '2') + + assert.equal(doc.getNextId(), prefix + '3') + assert.equal(doc.getId(), prefix + '3') + + cleanupSVG(svg) + }) + + it('Test getId() and getNextId() with nonce', function () { + const prefix = 'svg_' + NONCE + + const elem2 = document.createElementNS(NS.SVG, 'circle') + elem2.id = prefix + '_2' + svgN.append(elem2) + + const doc = new draw.Drawing(svgN) + + assert.equal(doc.getId(), prefix + '_0') + + assert.equal(doc.getNextId(), prefix + '_1') + assert.equal(doc.getId(), prefix + '_1') + + assert.equal(doc.getNextId(), prefix + '_3') + assert.equal(doc.getId(), prefix + '_3') + + assert.equal(doc.getNextId(), prefix + '_4') + assert.equal(doc.getId(), prefix + '_4') + + cleanupSVG(svgN) + }) + + it('Test getId() and getNextId() with prefix with nonce', function () { + const PREFIX = 'Bar-' + const doc = new draw.Drawing(svgN, PREFIX) + + const prefix = PREFIX + NONCE + '_' + assert.equal(doc.getId(), prefix + '0') + + assert.equal(doc.getNextId(), prefix + '1') + assert.equal(doc.getId(), prefix + '1') + + assert.equal(doc.getNextId(), prefix + '2') + assert.equal(doc.getId(), prefix + '2') + + assert.equal(doc.getNextId(), prefix + '3') + assert.equal(doc.getId(), prefix + '3') + + cleanupSVG(svgN) + }) + + it('Test releaseId()', function () { + const doc = new draw.Drawing(svg) + + const firstId = doc.getNextId() + /* const secondId = */ doc.getNextId() + + const result = doc.releaseId(firstId) + assert.ok(result) + assert.equal(doc.getNextId(), firstId) + assert.equal(doc.getNextId(), 'svg_3') + + assert.ok(!doc.releaseId('bad-id')) + assert.ok(doc.releaseId(firstId)) + assert.ok(!doc.releaseId(firstId)) + + cleanupSVG(svg) + }) + + it('Test getNumLayers', function () { + const drawing = new draw.Drawing(svg) + assert.equal(typeof drawing.getNumLayers, typeof function () { /* empty fn */ }) + assert.equal(drawing.getNumLayers(), 0) + + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.equal(drawing.getNumLayers(), 3) + + cleanupSVG(svg) + }) + + it('Test hasLayer', function () { + setupSVGWith3Layers(svg) + const drawing = new draw.Drawing(svg) + drawing.identifyLayers() + + assert.equal(typeof drawing.hasLayer, typeof function () { /* empty fn */ }) + assert.ok(!drawing.hasLayer('invalid-layer')) + + assert.ok(drawing.hasLayer(LAYER3)) + assert.ok(drawing.hasLayer(LAYER2)) + assert.ok(drawing.hasLayer(LAYER1)) + + cleanupSVG(svg) + }) + + it('Test identifyLayers() with empty document', function () { + const drawing = new draw.Drawing(svg) + assert.equal(drawing.getCurrentLayer(), null) + // By default, an empty document gets an empty group created. + drawing.identifyLayers() + + // Check that element now has one child node + assert.ok(drawing.getSvgElem().hasChildNodes()) + assert.equal(drawing.getSvgElem().childNodes.length, 1) + + // Check that all_layers are correctly set up. + assert.equal(drawing.getNumLayers(), 1) + const emptyLayer = drawing.all_layers[0] + assert.ok(emptyLayer) + const layerGroup = emptyLayer.getGroup() + assert.equal(layerGroup, drawing.getSvgElem().firstChild) + assert.equal(layerGroup.tagName, 'g') + assert.equal(layerGroup.getAttribute('class'), LAYER_CLASS) + assert.ok(layerGroup.hasChildNodes()) + assert.equal(layerGroup.childNodes.length, 1) + const firstChild = layerGroup.childNodes.item(0) + assert.equal(firstChild.tagName, 'title') + + cleanupSVG(svg) + }) + + it('Test identifyLayers() with some layers', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + + assert.equal(svg.childNodes.length, 3) + + drawing.identifyLayers() + + assert.equal(drawing.getNumLayers(), 3) + assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0)) + assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1)) + assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2)) + + assert.equal(drawing.all_layers[0].getGroup().getAttribute('class'), LAYER_CLASS) + assert.equal(drawing.all_layers[1].getGroup().getAttribute('class'), LAYER_CLASS) + assert.equal(drawing.all_layers[2].getGroup().getAttribute('class'), LAYER_CLASS) + + cleanupSVG(svg) + }) + + it('Test identifyLayers() with some layers and orphans', function () { + setupSVGWith3Layers(svg) + + const orphan1 = document.createElementNS(NS.SVG, 'rect') + const orphan2 = document.createElementNS(NS.SVG, 'rect') + svg.append(orphan1, orphan2) + + assert.equal(svg.childNodes.length, 5) + + const drawing = new draw.Drawing(svg) + drawing.identifyLayers() + + assert.equal(drawing.getNumLayers(), 4) + assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0)) + assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1)) + assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2)) + assert.equal(drawing.all_layers[3].getGroup(), svg.childNodes.item(3)) + + assert.equal(drawing.all_layers[0].getGroup().getAttribute('class'), LAYER_CLASS) + assert.equal(drawing.all_layers[1].getGroup().getAttribute('class'), LAYER_CLASS) + assert.equal(drawing.all_layers[2].getGroup().getAttribute('class'), LAYER_CLASS) + assert.equal(drawing.all_layers[3].getGroup().getAttribute('class'), LAYER_CLASS) + + const layer4 = drawing.all_layers[3].getGroup() + assert.equal(layer4.tagName, 'g') + assert.equal(layer4.childNodes.length, 3) + assert.equal(layer4.childNodes.item(1), orphan1) + assert.equal(layer4.childNodes.item(2), orphan2) + + cleanupSVG(svg) + }) + + it('Test getLayerName()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + + drawing.identifyLayers() + + assert.equal(drawing.getNumLayers(), 3) + assert.equal(drawing.getLayerName(0), LAYER1) + assert.equal(drawing.getLayerName(1), LAYER2) + assert.equal(drawing.getLayerName(2), LAYER3) + + cleanupSVG(svg) + }) + + it('Test getCurrentLayer()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.getCurrentLayer) + assert.equal(typeof drawing.getCurrentLayer, typeof function () { /* empty fn */ }) + assert.ok(drawing.getCurrentLayer()) + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup()) + + cleanupSVG(svg) + }) + + it('Test setCurrentLayer() and getCurrentLayerName()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.setCurrentLayer) + assert.equal(typeof drawing.setCurrentLayer, typeof function () { /* empty fn */ }) + + drawing.setCurrentLayer(LAYER2) + assert.equal(drawing.getCurrentLayerName(), LAYER2) + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup()) + + drawing.setCurrentLayer(LAYER3) + assert.equal(drawing.getCurrentLayerName(), LAYER3) + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup()) + + cleanupSVG(svg) + }) + + it('Test setCurrentLayerName()', function () { + const mockHrService = { + changeElement () { + // empty + } + } + addOwnSpies(mockHrService) + + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.setCurrentLayerName) + assert.equal(typeof drawing.setCurrentLayerName, typeof function () { /* empty fn */ }) + + const oldName = drawing.getCurrentLayerName() + const newName = 'New Name' + assert.ok(drawing.layer_map[oldName]) + assert.equal(drawing.layer_map[newName], undefined) // newName shouldn't exist. + const result = drawing.setCurrentLayerName(newName, mockHrService) + assert.equal(result, newName) + assert.equal(drawing.getCurrentLayerName(), newName) + // Was the map updated? + assert.equal(drawing.layer_map[oldName], undefined) + assert.equal(drawing.layer_map[newName], drawing.current_layer) + // Was mockHrService called? + assert.ok(mockHrService.changeElement.calledOnce) + assert.equal(oldName, mockHrService.changeElement.getCall(0).args[1]['#text']) + assert.equal(newName, mockHrService.changeElement.getCall(0).args[0].textContent) + + cleanupSVG(svg) + }) + + it('Test createLayer()', function () { + const mockHrService = { + startBatchCommand () { /* empty fn */ }, + endBatchCommand () { /* empty fn */ }, + insertElement () { /* empty fn */ } + } + addOwnSpies(mockHrService) + + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.createLayer) + assert.equal(typeof drawing.createLayer, typeof function () { /* empty fn */ }) + + const NEW_LAYER_NAME = 'Layer A' + const layerG = drawing.createLayer(NEW_LAYER_NAME, mockHrService) + assert.equal(drawing.getNumLayers(), 4) + assert.equal(layerG, drawing.getCurrentLayer()) + assert.equal(layerG.getAttribute('class'), LAYER_CLASS) + assert.equal(NEW_LAYER_NAME, drawing.getCurrentLayerName()) + assert.equal(NEW_LAYER_NAME, drawing.getLayerName(3)) + + assert.equal(layerG, mockHrService.insertElement.getCall(0).args[0]) + assert.ok(mockHrService.startBatchCommand.calledOnce) + assert.ok(mockHrService.endBatchCommand.calledOnce) + + cleanupSVG(svg) + }) + + it('Test mergeLayer()', function () { + const mockHrService = { + startBatchCommand () { /* empty fn */ }, + endBatchCommand () { /* empty fn */ }, + moveElement () { /* empty fn */ }, + removeElement () { /* empty fn */ } + } + addOwnSpies(mockHrService) + + const drawing = new draw.Drawing(svg) + const layers = setupSVGWith3Layers(svg) + const elementCount = createSomeElementsInGroup(layers[2]) + 1 // +1 for title element + assert.equal(layers[1].childElementCount, 1) + assert.equal(layers[2].childElementCount, elementCount) + drawing.identifyLayers() + assert.equal(drawing.getCurrentLayer(), layers[2]) + + assert.ok(drawing.mergeLayer) + assert.equal(typeof drawing.mergeLayer, typeof function () { /* empty fn */ }) + + drawing.mergeLayer(mockHrService) + + assert.equal(drawing.getNumLayers(), 2) + assert.equal(svg.childElementCount, 2) + assert.equal(drawing.getCurrentLayer(), layers[1]) + assert.equal(layers[1].childElementCount, elementCount) + + // check history record + assert.ok(mockHrService.startBatchCommand.calledOnce) + assert.ok(mockHrService.endBatchCommand.calledOnce) + assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge Layer') + assert.equal(mockHrService.moveElement.callCount, elementCount - 1) // -1 because the title was not moved. + assert.equal(mockHrService.removeElement.callCount, 2) // remove group and title. + + cleanupSVG(svg) + }) + + it('Test mergeLayer() when no previous layer to merge', function () { + const mockHrService = { + startBatchCommand () { /* empty fn */ }, + endBatchCommand () { /* empty fn */ }, + moveElement () { /* empty fn */ }, + removeElement () { /* empty fn */ } + } + addOwnSpies(mockHrService) + + const drawing = new draw.Drawing(svg) + const layers = setupSVGWith3Layers(svg) + drawing.identifyLayers() + drawing.setCurrentLayer(LAYER1) + assert.equal(drawing.getCurrentLayer(), layers[0]) + + drawing.mergeLayer(mockHrService) + + assert.equal(drawing.getNumLayers(), 3) + assert.equal(svg.childElementCount, 3) + assert.equal(drawing.getCurrentLayer(), layers[0]) + assert.equal(layers[0].childElementCount, 1) + assert.equal(layers[1].childElementCount, 1) + assert.equal(layers[2].childElementCount, 1) + + // check history record + assert.equal(mockHrService.startBatchCommand.callCount, 0) + assert.equal(mockHrService.endBatchCommand.callCount, 0) + assert.equal(mockHrService.moveElement.callCount, 0) + assert.equal(mockHrService.removeElement.callCount, 0) + + cleanupSVG(svg) + }) + + it('Test mergeAllLayers()', function () { + const mockHrService = { + startBatchCommand () { /* empty fn */ }, + endBatchCommand () { /* empty fn */ }, + moveElement () { /* empty fn */ }, + removeElement () { /* empty fn */ } + } + addOwnSpies(mockHrService) + + const drawing = new draw.Drawing(svg) + const layers = setupSVGWith3Layers(svg) + const elementCount = createSomeElementsInGroup(layers[0]) + 1 // +1 for title element + createSomeElementsInGroup(layers[1]) + createSomeElementsInGroup(layers[2]) + assert.equal(layers[0].childElementCount, elementCount) + assert.equal(layers[1].childElementCount, elementCount) + assert.equal(layers[2].childElementCount, elementCount) + drawing.identifyLayers() + + assert.ok(drawing.mergeAllLayers) + assert.equal(typeof drawing.mergeAllLayers, typeof function () { /* empty fn */ }) + + drawing.mergeAllLayers(mockHrService) + + assert.equal(drawing.getNumLayers(), 1) + assert.equal(svg.childElementCount, 1) + assert.equal(drawing.getCurrentLayer(), layers[0]) + assert.equal(layers[0].childElementCount, elementCount * 3 - 2) // -2 because two titles were deleted. + + // check history record + assert.equal(mockHrService.startBatchCommand.callCount, 3) // mergeAllLayers + 2 * mergeLayer + assert.equal(mockHrService.endBatchCommand.callCount, 3) + assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge all Layers') + assert.equal(mockHrService.startBatchCommand.getCall(1).args[0], 'Merge Layer') + assert.equal(mockHrService.startBatchCommand.getCall(2).args[0], 'Merge Layer') + // moveElement count is times 3 instead of 2, because one layer's elements were moved twice. + // moveElement count is minus 3 because the three titles were not moved. + assert.equal(mockHrService.moveElement.callCount, elementCount * 3 - 3) + assert.equal(mockHrService.removeElement.callCount, 2 * 2) // remove group and title twice. + + cleanupSVG(svg) + }) + + it('Test cloneLayer()', function () { + const mockHrService = { + startBatchCommand () { /* empty fn */ }, + endBatchCommand () { /* empty fn */ }, + insertElement () { /* empty fn */ } + } + addOwnSpies(mockHrService) + + const drawing = new draw.Drawing(svg) + const layers = setupSVGWith3Layers(svg) + const layer3 = layers[2] + const elementCount = createSomeElementsInGroup(layer3) + 1 // +1 for title element + assert.equal(layer3.childElementCount, elementCount) + drawing.identifyLayers() + + assert.ok(drawing.cloneLayer) + assert.equal(typeof drawing.cloneLayer, typeof function () { /* empty fn */ }) + + const clone = drawing.cloneLayer('clone', mockHrService) + + assert.equal(drawing.getNumLayers(), 4) + assert.equal(svg.childElementCount, 4) + assert.equal(drawing.getCurrentLayer(), clone) + assert.equal(clone.childElementCount, elementCount) + + // check history record + assert.ok(mockHrService.startBatchCommand.calledOnce) // mergeAllLayers + 2 * mergeLayer + assert.ok(mockHrService.endBatchCommand.calledOnce) + assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Duplicate Layer') + assert.equal(mockHrService.insertElement.callCount, 1) + assert.equal(mockHrService.insertElement.getCall(0).args[0], clone) + + // check that path is cloned properly + assert.equal(clone.childNodes.length, elementCount) + const path = clone.childNodes[1] + assert.equal(path.id, 'svg_1') + assert.equal(path.getAttribute('d'), PATH_ATTR.d) + assert.equal(path.getAttribute('transform'), PATH_ATTR.transform) + assert.equal(path.getAttribute('fill'), PATH_ATTR.fill) + assert.equal(path.getAttribute('stroke'), PATH_ATTR.stroke) + assert.equal(path.getAttribute('stroke-width'), PATH_ATTR['stroke-width']) + + // check that g is cloned properly + const g = clone.childNodes[4] + assert.equal(g.childNodes.length, 1) + assert.equal(g.id, 'svg_4') + + cleanupSVG(svg) + }) + + it('Test getLayerVisibility()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.getLayerVisibility) + assert.equal(typeof drawing.getLayerVisibility, typeof function () { /* empty fn */ }) + assert.ok(drawing.getLayerVisibility(LAYER1)) + assert.ok(drawing.getLayerVisibility(LAYER2)) + assert.ok(drawing.getLayerVisibility(LAYER3)) + + cleanupSVG(svg) + }) + + it('Test setLayerVisibility()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.setLayerVisibility) + assert.equal(typeof drawing.setLayerVisibility, typeof function () { /* empty fn */ }) + + drawing.setLayerVisibility(LAYER3, false) + drawing.setLayerVisibility(LAYER2, true) + drawing.setLayerVisibility(LAYER1, false) + + assert.ok(!drawing.getLayerVisibility(LAYER1)) + assert.ok(drawing.getLayerVisibility(LAYER2)) + assert.ok(!drawing.getLayerVisibility(LAYER3)) + + drawing.setLayerVisibility(LAYER3, 'test-string') + assert.ok(!drawing.getLayerVisibility(LAYER3)) + + cleanupSVG(svg) + }) + + it('Test getLayerOpacity()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.getLayerOpacity) + assert.equal(typeof drawing.getLayerOpacity, typeof function () { /* empty fn */ }) + assert.strictEqual(drawing.getLayerOpacity(LAYER1), 1.0) + assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0) + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0) + + cleanupSVG(svg) + }) + + it('Test setLayerOpacity()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + assert.ok(drawing.setLayerOpacity) + assert.equal(typeof drawing.setLayerOpacity, typeof function () { /* empty fn */ }) + + drawing.setLayerOpacity(LAYER1, 0.4) + drawing.setLayerOpacity(LAYER2, 'invalid-string') + drawing.setLayerOpacity(LAYER3, -1.4) + + assert.strictEqual(drawing.getLayerOpacity(LAYER1), 0.4) + assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0) + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0) + + drawing.setLayerOpacity(LAYER3, 100) + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0) + + cleanupSVG(svg) + }) + + it('Test deleteCurrentLayer()', function () { + const drawing = new draw.Drawing(svg) + setupSVGWith3Layers(svg) + drawing.identifyLayers() + + drawing.setCurrentLayer(LAYER2) + + const curLayer = drawing.getCurrentLayer() + assert.equal(curLayer, drawing.all_layers[1].getGroup()) + const deletedLayer = drawing.deleteCurrentLayer() + + assert.equal(curLayer, deletedLayer) + assert.equal(drawing.getNumLayers(), 2) + assert.equal(LAYER1, drawing.all_layers[0].getName()) + assert.equal(LAYER3, drawing.all_layers[1].getName()) + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup()) + }) + + it('Test svgedit.draw.randomizeIds()', function () { + // Confirm in LET_DOCUMENT_DECIDE mode that the document decides + // if there is a nonce. + let drawing = new draw.Drawing(svgN.cloneNode(true)) + assert.ok(drawing.getNonce()) + + drawing = new draw.Drawing(svg.cloneNode(true)) + assert.ok(!drawing.getNonce()) + + // Confirm that a nonce is set once we're in ALWAYS_RANDOMIZE mode. + draw.randomizeIds(true, drawing) + assert.ok(drawing.getNonce()) + + // Confirm new drawings in ALWAYS_RANDOMIZE mode have a nonce. + drawing = new draw.Drawing(svg.cloneNode(true)) + assert.ok(drawing.getNonce()) + + drawing.clearNonce() + assert.ok(!drawing.getNonce()) + + // Confirm new drawings in NEVER_RANDOMIZE mode do not have a nonce + // but that their se:nonce attribute is left alone. + draw.randomizeIds(false, drawing) + assert.ok(!drawing.getNonce()) + assert.ok(drawing.getSvgElem().getAttributeNS(NS.SE, 'nonce')) + + drawing = new draw.Drawing(svg.cloneNode(true)) + assert.ok(!drawing.getNonce()) + + drawing = new draw.Drawing(svgN.cloneNode(true)) + assert.ok(!drawing.getNonce()) + }) +}) diff --git a/data/5-svgedit-unit/history.cy.js b/data/5-svgedit-unit/history.cy.js new file mode 100644 index 0000000..bbbad8b --- /dev/null +++ b/data/5-svgedit-unit/history.cy.js @@ -0,0 +1,520 @@ +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as utilities from '../../../packages/svgcanvas/core/utilities.js' +import * as history from '../../../packages/svgcanvas/core/history.js' + +describe('history', function () { + // TODO(codedread): Write tests for handling history events. + + utilities.mock({ + getHref () { return '#foo' }, + setHref () { /* empty fn */ }, + getRotationAngle () { return 0 } + }) + + // const svg = document.createElementNS(NS.SVG, 'svg'); + let undoMgr = null + + class MockCommand extends history.Command { + constructor (optText) { + super() + this.text = optText + } + + apply (handler) { + super.apply(handler, () => { /* empty fn */ }) + } + + unapply (handler) { + super.unapply(handler, () => { /* empty fn */ }) + } + + elements () { return [] } + } + + /* + class MockHistoryEventHandler { + handleHistoryEvent (eventType, command) {} + } + */ + + /** + * Set up tests (with undo manager). + * @returns {void} + */ + beforeEach(function () { + undoMgr = new history.UndoManager() + + document.body.textContent = '' + this.divparent = document.createElement('div') + this.divparent.id = 'divparent' + this.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 + } + + this.divparent.append(this.div1, this.div2, this.div3) + + this.div4.style.visibility = 'hidden' + this.div4.append(this.div5) + + document.body.append(this.divparent, this.div) + }) + /** + * Tear down tests, destroying undo manager. + * @returns {void} + */ + afterEach(() => { + undoMgr = null + }) + + it('Test svgedit.history package', function () { + assert.ok(history) + assert.ok(history.MoveElementCommand) + assert.ok(history.InsertElementCommand) + assert.ok(history.ChangeElementCommand) + assert.ok(history.RemoveElementCommand) + assert.ok(history.BatchCommand) + assert.ok(history.UndoManager) + assert.equal(typeof history.MoveElementCommand, typeof function () { /* empty fn */ }) + assert.equal(typeof history.InsertElementCommand, typeof function () { /* empty fn */ }) + assert.equal(typeof history.ChangeElementCommand, typeof function () { /* empty fn */ }) + assert.equal(typeof history.RemoveElementCommand, typeof function () { /* empty fn */ }) + assert.equal(typeof history.BatchCommand, typeof function () { /* empty fn */ }) + assert.equal(typeof history.UndoManager, typeof function () { /* empty fn */ }) + }) + + it('Test UndoManager methods', function () { + assert.ok(undoMgr) + assert.ok(undoMgr.addCommandToHistory) + assert.ok(undoMgr.getUndoStackSize) + assert.ok(undoMgr.getRedoStackSize) + assert.ok(undoMgr.resetUndoStack) + assert.ok(undoMgr.getNextUndoCommandText) + assert.ok(undoMgr.getNextRedoCommandText) + + assert.equal(typeof undoMgr, typeof {}) + assert.equal(typeof undoMgr.addCommandToHistory, typeof function () { /* empty fn */ }) + assert.equal(typeof undoMgr.getUndoStackSize, typeof function () { /* empty fn */ }) + assert.equal(typeof undoMgr.getRedoStackSize, typeof function () { /* empty fn */ }) + assert.equal(typeof undoMgr.resetUndoStack, typeof function () { /* empty fn */ }) + assert.equal(typeof undoMgr.getNextUndoCommandText, typeof function () { /* empty fn */ }) + assert.equal(typeof undoMgr.getNextRedoCommandText, typeof function () { /* empty fn */ }) + }) + + it('Test UndoManager.addCommandToHistory() function', function () { + assert.equal(undoMgr.getUndoStackSize(), 0) + undoMgr.addCommandToHistory(new MockCommand()) + assert.equal(undoMgr.getUndoStackSize(), 1) + undoMgr.addCommandToHistory(new MockCommand()) + assert.equal(undoMgr.getUndoStackSize(), 2) + }) + + it('Test UndoManager.getUndoStackSize() and getRedoStackSize() functions', function () { + undoMgr.addCommandToHistory(new MockCommand()) + undoMgr.addCommandToHistory(new MockCommand()) + undoMgr.addCommandToHistory(new MockCommand()) + + assert.equal(undoMgr.getUndoStackSize(), 3) + assert.equal(undoMgr.getRedoStackSize(), 0) + + undoMgr.undo() + assert.equal(undoMgr.getUndoStackSize(), 2) + assert.equal(undoMgr.getRedoStackSize(), 1) + + undoMgr.undo() + assert.equal(undoMgr.getUndoStackSize(), 1) + assert.equal(undoMgr.getRedoStackSize(), 2) + + undoMgr.undo() + assert.equal(undoMgr.getUndoStackSize(), 0) + assert.equal(undoMgr.getRedoStackSize(), 3) + + undoMgr.undo() + assert.equal(undoMgr.getUndoStackSize(), 0) + assert.equal(undoMgr.getRedoStackSize(), 3) + + undoMgr.redo() + assert.equal(undoMgr.getUndoStackSize(), 1) + assert.equal(undoMgr.getRedoStackSize(), 2) + + undoMgr.redo() + assert.equal(undoMgr.getUndoStackSize(), 2) + assert.equal(undoMgr.getRedoStackSize(), 1) + + undoMgr.redo() + assert.equal(undoMgr.getUndoStackSize(), 3) + assert.equal(undoMgr.getRedoStackSize(), 0) + + undoMgr.redo() + assert.equal(undoMgr.getUndoStackSize(), 3) + assert.equal(undoMgr.getRedoStackSize(), 0) + }) + + it('Test UndoManager.resetUndoStackSize() function', function () { + undoMgr.addCommandToHistory(new MockCommand()) + undoMgr.addCommandToHistory(new MockCommand()) + undoMgr.addCommandToHistory(new MockCommand()) + undoMgr.undo() + + assert.equal(undoMgr.getUndoStackSize(), 2) + assert.equal(undoMgr.getRedoStackSize(), 1) + + undoMgr.resetUndoStack() + + assert.equal(undoMgr.getUndoStackSize(), 0) + assert.equal(undoMgr.getRedoStackSize(), 0) + }) + + it('Test UndoManager.getNextUndoCommandText() function', function () { + assert.equal(undoMgr.getNextUndoCommandText(), '') + + undoMgr.addCommandToHistory(new MockCommand('First')) + undoMgr.addCommandToHistory(new MockCommand('Second')) + undoMgr.addCommandToHistory(new MockCommand('Third')) + + assert.equal(undoMgr.getNextUndoCommandText(), 'Third') + + undoMgr.undo() + assert.equal(undoMgr.getNextUndoCommandText(), 'Second') + + undoMgr.undo() + assert.equal(undoMgr.getNextUndoCommandText(), 'First') + + undoMgr.undo() + assert.equal(undoMgr.getNextUndoCommandText(), '') + + undoMgr.redo() + assert.equal(undoMgr.getNextUndoCommandText(), 'First') + + undoMgr.redo() + assert.equal(undoMgr.getNextUndoCommandText(), 'Second') + + undoMgr.redo() + assert.equal(undoMgr.getNextUndoCommandText(), 'Third') + + undoMgr.redo() + assert.equal(undoMgr.getNextUndoCommandText(), 'Third') + }) + + it('Test UndoManager.getNextRedoCommandText() function', function () { + assert.equal(undoMgr.getNextRedoCommandText(), '') + + undoMgr.addCommandToHistory(new MockCommand('First')) + undoMgr.addCommandToHistory(new MockCommand('Second')) + undoMgr.addCommandToHistory(new MockCommand('Third')) + + assert.equal(undoMgr.getNextRedoCommandText(), '') + + undoMgr.undo() + assert.equal(undoMgr.getNextRedoCommandText(), 'Third') + + undoMgr.undo() + assert.equal(undoMgr.getNextRedoCommandText(), 'Second') + + undoMgr.undo() + assert.equal(undoMgr.getNextRedoCommandText(), 'First') + + undoMgr.redo() + assert.equal(undoMgr.getNextRedoCommandText(), 'Second') + + undoMgr.redo() + assert.equal(undoMgr.getNextRedoCommandText(), 'Third') + + undoMgr.redo() + assert.equal(undoMgr.getNextRedoCommandText(), '') + }) + + it('Test UndoManager.undo() and redo() functions', function () { + let lastCalled = null + const cmd1 = new MockCommand() + const cmd2 = new MockCommand() + const cmd3 = new MockCommand() + cmd1.apply = function () { lastCalled = 'cmd1.apply' } + cmd2.apply = function () { lastCalled = 'cmd2.apply' } + cmd3.apply = function () { lastCalled = 'cmd3.apply' } + cmd1.unapply = function () { lastCalled = 'cmd1.unapply' } + cmd2.unapply = function () { lastCalled = 'cmd2.unapply' } + cmd3.unapply = function () { lastCalled = 'cmd3.unapply' } + + undoMgr.addCommandToHistory(cmd1) + undoMgr.addCommandToHistory(cmd2) + undoMgr.addCommandToHistory(cmd3) + + assert.ok(!lastCalled) + + undoMgr.undo() + assert.equal(lastCalled, 'cmd3.unapply') + + undoMgr.redo() + assert.equal(lastCalled, 'cmd3.apply') + + undoMgr.undo() + undoMgr.undo() + assert.equal(lastCalled, 'cmd2.unapply') + + undoMgr.undo() + assert.equal(lastCalled, 'cmd1.unapply') + lastCalled = null + + undoMgr.undo() + assert.ok(!lastCalled) + + undoMgr.redo() + assert.equal(lastCalled, 'cmd1.apply') + + undoMgr.redo() + assert.equal(lastCalled, 'cmd2.apply') + + undoMgr.redo() + assert.equal(lastCalled, 'cmd3.apply') + lastCalled = null + + undoMgr.redo() + assert.ok(!lastCalled) + }) + + it('Test MoveElementCommand', function () { + let move = new history.MoveElementCommand(this.div3, this.div1, this.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) + + move.apply() + assert.equal(this.divparent.firstElementChild, this.div1) + assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2) + assert.equal(this.divparent.lastElementChild, this.div3) + + move = new history.MoveElementCommand(this.div1, null, this.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) + + move.apply() + assert.equal(this.divparent.firstElementChild, this.div1) + assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2) + assert.equal(this.divparent.lastElementChild, this.div3) + + move = new history.MoveElementCommand(this.div2, this.div5, this.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) + + 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) + }) + + it('Test InsertElementCommand', function () { + let insert = new history.InsertElementCommand(this.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) + + 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) + + insert = new history.InsertElementCommand(this.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) + + 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) + }) + + it('Test RemoveElementCommand', function () { + const div6 = document.createElement('div') + div6.id = 'div6' + + let remove = new history.RemoveElementCommand(div6, null, this.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) + + 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) + + remove = new history.RemoveElementCommand(div6, this.div2, this.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) + + 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) + }) + + it('Test ChangeElementCommand', function () { + this.div1.setAttribute('title', 'new title') + let change = new history.ChangeElementCommand(this.div1, + { title: 'old title', class: 'foo' }) + assert.ok(change.unapply) + assert.ok(change.apply) + assert.equal(typeof change.unapply, typeof function () { /* empty fn */ }) + 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') + + change.apply() + assert.equal(this.div1.getAttribute('title'), 'new title') + assert.ok(!this.div1.getAttribute('class')) + + this.div1.textContent = 'inner text' + change = new history.ChangeElementCommand(this.div1, + { '#text': null }) + + change.unapply() + assert.ok(!this.div1.textContent) + + change.apply() + assert.equal(this.div1.textContent, 'inner text') + + this.div1.textContent = '' + change = new history.ChangeElementCommand(this.div1, + { '#text': 'old text' }) + + change.unapply() + assert.equal(this.div1.textContent, 'old text') + + change.apply() + assert.ok(!this.div1.textContent) + + // TODO(codedread): Refactor this #href stuff in history.js and svgcanvas.js + const rect = document.createElementNS(NS.SVG, 'rect') + let justCalled = null + let gethrefvalue = null + let sethrefvalue = null + utilities.mock({ + getHref (elem) { + assert.equal(elem, rect) + justCalled = 'getHref' + return gethrefvalue + }, + setHref (elem, val) { + assert.equal(elem, rect) + assert.equal(val, sethrefvalue) + justCalled = 'setHref' + }, + getRotationAngle () { return 0 } + }) + + gethrefvalue = '#newhref' + change = new history.ChangeElementCommand(rect, + { '#href': '#oldhref' }) + assert.equal(justCalled, 'getHref') + + justCalled = null + sethrefvalue = '#oldhref' + change.unapply() + assert.equal(justCalled, 'setHref') + + justCalled = null + sethrefvalue = '#newhref' + change.apply() + assert.equal(justCalled, 'setHref') + + const line = document.createElementNS(NS.SVG, 'line') + line.setAttribute('class', 'newClass') + change = new history.ChangeElementCommand(line, { class: 'oldClass' }) + + assert.ok(change.unapply) + assert.ok(change.apply) + assert.equal(typeof change.unapply, typeof function () { /* empty fn */ }) + assert.equal(typeof change.apply, typeof function () { /* empty fn */ }) + + change.unapply() + assert.equal(line.getAttribute('class'), 'oldClass') + + change.apply() + assert.equal(line.getAttribute('class'), 'newClass') + }) + + it('Test BatchCommand', function () { + let concatResult = '' + MockCommand.prototype.apply = function () { concatResult += this.text } + + const batch = new history.BatchCommand() + assert.ok(batch.unapply) + assert.ok(batch.apply) + assert.ok(batch.addSubCommand) + assert.ok(batch.isEmpty) + assert.equal(typeof batch.unapply, 'function') + assert.equal(typeof batch.apply, 'function') + assert.equal(typeof batch.addSubCommand, 'function') + assert.equal(typeof batch.isEmpty, 'function') + + assert.ok(batch.isEmpty()) + + batch.addSubCommand(new MockCommand('a')) + assert.ok(!batch.isEmpty()) + batch.addSubCommand(new MockCommand('b')) + batch.addSubCommand(new MockCommand('c')) + + assert.ok(!concatResult) + batch.apply() + assert.equal(concatResult, 'abc') + + MockCommand.prototype.apply = function () { /* empty fn */ } + MockCommand.prototype.unapply = function () { concatResult += this.text } + concatResult = '' + assert.ok(!concatResult) + batch.unapply() + assert.equal(concatResult, 'cba') + + MockCommand.prototype.unapply = function () { /* empty fn */ } + }) +}) diff --git a/data/5-svgedit-unit/math.cy.js b/data/5-svgedit-unit/math.cy.js new file mode 100644 index 0000000..a3a4ebc --- /dev/null +++ b/data/5-svgedit-unit/math.cy.js @@ -0,0 +1,106 @@ +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as math from '../../../packages/svgcanvas/core/math.js' + +describe('math', function () { + const svg = document.createElementNS(NS.SVG, 'svg') + + it('Test svgedit.math package', function () { + assert.ok(math) + assert.ok(math.transformPoint) + assert.ok(math.isIdentity) + assert.ok(math.matrixMultiply) + assert.equal(typeof math.transformPoint, typeof function () { /* empty fn */ }) + assert.equal(typeof math.isIdentity, typeof function () { /* empty fn */ }) + assert.equal(typeof math.matrixMultiply, typeof function () { /* empty fn */ }) + }) + + it('Test svgedit.math.transformPoint() function', function () { + const { transformPoint } = math + + const m = svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 0; m.f = 0 + let pt = transformPoint(100, 200, m) + assert.equal(pt.x, 100) + assert.equal(pt.y, 200) + + m.e = 300; m.f = 400 + pt = transformPoint(100, 200, m) + assert.equal(pt.x, 400) + assert.equal(pt.y, 600) + + m.a = 0.5; m.b = 0.75 + m.c = 1.25; m.d = 2 + pt = transformPoint(100, 200, m) + assert.equal(pt.x, 100 * m.a + 200 * m.c + m.e) + assert.equal(pt.y, 100 * m.b + 200 * m.d + m.f) + }) + + it('Test svgedit.math.isIdentity() function', function () { + assert.ok(math.isIdentity(svg.createSVGMatrix())) + + const m = svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 0; m.f = 0 + assert.ok(math.isIdentity(m)) + }) + + it('Test svgedit.math.matrixMultiply() function', function () { + const mult = math.matrixMultiply + const { isIdentity } = math + + // translate there and back + const tr1 = svg.createSVGMatrix().translate(100, 50) + const tr2 = svg.createSVGMatrix().translate(-90, 0) + const tr3 = svg.createSVGMatrix().translate(-10, -50) + let I = mult(tr1, tr2, tr3) + assert.ok(isIdentity(I), 'Expected identity matrix when translating there and back') + + // rotate there and back + // TODO: currently Mozilla fails this when rotating back at -50 and then -40 degrees + // (b and c are *almost* zero, but not zero) + const rotThere = svg.createSVGMatrix().rotate(90) + const rotBack = svg.createSVGMatrix().rotate(-90) // TODO: set this to -50 + const rotBackMore = svg.createSVGMatrix().rotate(0) // TODO: set this to -40 + I = mult(rotThere, rotBack, rotBackMore) + assert.ok(isIdentity(I), 'Expected identity matrix when rotating there and back') + + // scale up and down + const scaleUp = svg.createSVGMatrix().scale(4) + const scaleDown = svg.createSVGMatrix().scaleNonUniform(0.25, 1) + const scaleDownMore = svg.createSVGMatrix().scaleNonUniform(1, 0.25) + I = mult(scaleUp, scaleDown, scaleDownMore) + assert.ok(isIdentity(I), 'Expected identity matrix when scaling up and down') + + // test multiplication with its inverse + I = mult(rotThere, rotThere.inverse()) + assert.ok(isIdentity(I), 'Expected identity matrix when multiplying a matrix by its inverse') + I = mult(rotThere.inverse(), rotThere) + assert.ok(isIdentity(I), 'Expected identity matrix when multiplying a matrix by its inverse') + }) + + it('Test svgedit.math.transformBox() function', function () { + const { transformBox } = math + + const m = svg.createSVGMatrix() + m.a = 1; m.b = 0 + m.c = 0; m.d = 1 + m.e = 0; m.f = 0 + + const r = transformBox(10, 10, 200, 300, m) + assert.equal(r.tl.x, 10) + assert.equal(r.tl.y, 10) + assert.equal(r.tr.x, 210) + assert.equal(r.tr.y, 10) + assert.equal(r.bl.x, 10) + assert.equal(r.bl.y, 310) + assert.equal(r.br.x, 210) + assert.equal(r.br.y, 310) + assert.equal(r.aabox.x, 10) + assert.equal(r.aabox.y, 10) + assert.equal(r.aabox.width, 200) + assert.equal(r.aabox.height, 300) + }) +}) diff --git a/data/5-svgedit-unit/path.cy.js b/data/5-svgedit-unit/path.cy.js new file mode 100644 index 0000000..201063d --- /dev/null +++ b/data/5-svgedit-unit/path.cy.js @@ -0,0 +1,182 @@ +/* globals SVGPathSeg */ +import 'pathseg' +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as utilities from '../../../packages/svgcanvas/core/utilities.js' +import * as pathModule from '../../../packages/svgcanvas/core/path.js' +import { Path, Segment } from '../../../packages/svgcanvas/core/path-method.js' +import { init as unitsInit } from '../../../packages/svgcanvas/core/units.js' + +describe('path', function () { + /** + * @typedef {GenericArray} EditorContexts + * @property {module:path.EditorContext} 0 + * @property {module:path.EditorContext} 1 + */ + + /** + * @param {SVGSVGElement} [svg] + * @returns {EditorContexts} + */ + function getMockContexts (svg) { + svg = svg || document.createElementNS(NS.SVG, 'svg') + const selectorParentGroup = document.createElementNS(NS.SVG, 'g') + selectorParentGroup.setAttribute('id', 'selectorParentGroup') + svg.append(selectorParentGroup) + return [ + /** + * @implements {module:path.EditorContext} + */ + { + getSvgRoot () { return svg }, + getZoom () { return 1 } + }, + /** + * @implements {module:utilities.EditorContext} + */ + { + getDOMDocument () { return svg }, + getDOMContainer () { return svg }, + getSvgRoot () { return svg } + } + ] + } + + it('Test svgedit.path.replacePathSeg', function () { + const path = document.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'M0,0 L10,11 L20,21Z') + + const [mockPathContext, mockUtilitiesContext] = getMockContexts() + pathModule.init(mockPathContext) + utilities.init(mockUtilitiesContext) + new Path(path) // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L') + assert.equal(path.pathSegList.getItem(1).x, 10) + assert.equal(path.pathSegList.getItem(1).y, 11) + + pathModule.replacePathSeg(SVGPathSeg.PATHSEG_LINETO_REL, 1, [30, 31], path) + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'l') + assert.equal(path.pathSegList.getItem(1).x, 30) + assert.equal(path.pathSegList.getItem(1).y, 31) + }) + + it('Test svgedit.path.Segment.setType simple', function () { + const path = document.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'M0,0 L10,11 L20,21Z') + + const [mockPathContext, mockUtilitiesContext] = getMockContexts() + pathModule.init(mockPathContext) + utilities.init(mockUtilitiesContext) + new Path(path) // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L') + assert.equal(path.pathSegList.getItem(1).x, 10) + assert.equal(path.pathSegList.getItem(1).y, 11) + + const segment = new Segment(1, path.pathSegList.getItem(1)) + segment.setType(SVGPathSeg.PATHSEG_LINETO_REL, [30, 31]) + assert.equal(segment.item.pathSegTypeAsLetter, 'l') + assert.equal(segment.item.x, 30) + assert.equal(segment.item.y, 31) + + // Also verify that the actual path changed. + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'l') + assert.equal(path.pathSegList.getItem(1).x, 30) + assert.equal(path.pathSegList.getItem(1).y, 31) + }) + + it('Test svgedit.path.Segment.setType with control points', function () { + // Setup the dom for a mock control group. + const svg = document.createElementNS(NS.SVG, 'svg') + const path = document.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'M0,0 C11,12 13,14 15,16 Z') + svg.append(path) + + const [mockPathContext, mockUtilitiesContext] = getMockContexts(svg) + pathModule.init(mockPathContext) + utilities.init(mockUtilitiesContext) + const segment = new Segment(1, path.pathSegList.getItem(1)) + segment.path = new Path(path) + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C') + assert.equal(path.pathSegList.getItem(1).x1, 11) + assert.equal(path.pathSegList.getItem(1).y1, 12) + assert.equal(path.pathSegList.getItem(1).x2, 13) + assert.equal(path.pathSegList.getItem(1).y2, 14) + assert.equal(path.pathSegList.getItem(1).x, 15) + assert.equal(path.pathSegList.getItem(1).y, 16) + + segment.setType(SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, [30, 31, 32, 33, 34, 35]) + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'c') + assert.equal(path.pathSegList.getItem(1).x1, 32) + assert.equal(path.pathSegList.getItem(1).y1, 33) + assert.equal(path.pathSegList.getItem(1).x2, 34) + assert.equal(path.pathSegList.getItem(1).y2, 35) + assert.equal(path.pathSegList.getItem(1).x, 30) + assert.equal(path.pathSegList.getItem(1).y, 31) + }) + + it('Test svgedit.path.Segment.move', function () { + const path = document.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'M0,0 L10,11 L20,21Z') + + const [mockPathContext, mockUtilitiesContext] = getMockContexts() + pathModule.init(mockPathContext) + utilities.init(mockUtilitiesContext) + new Path(path) // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L') + assert.equal(path.pathSegList.getItem(1).x, 10) + assert.equal(path.pathSegList.getItem(1).y, 11) + + const segment = new Segment(1, path.pathSegList.getItem(1)) + segment.move(-3, 4) + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L') + assert.equal(path.pathSegList.getItem(1).x, 7) + assert.equal(path.pathSegList.getItem(1).y, 15) + }) + + it('Test svgedit.path.Segment.moveCtrl', function () { + const path = document.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'M0,0 C11,12 13,14 15,16 Z') + + const [mockPathContext, mockUtilitiesContext] = getMockContexts() + pathModule.init(mockPathContext) + utilities.init(mockUtilitiesContext) + new Path(path) // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C') + assert.equal(path.pathSegList.getItem(1).x1, 11) + assert.equal(path.pathSegList.getItem(1).y1, 12) + assert.equal(path.pathSegList.getItem(1).x2, 13) + assert.equal(path.pathSegList.getItem(1).y2, 14) + assert.equal(path.pathSegList.getItem(1).x, 15) + assert.equal(path.pathSegList.getItem(1).y, 16) + + const segment = new Segment(1, path.pathSegList.getItem(1)) + segment.moveCtrl(1, 100, -200) + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C') + assert.equal(path.pathSegList.getItem(1).x1, 111) + assert.equal(path.pathSegList.getItem(1).y1, -188) + assert.equal(path.pathSegList.getItem(1).x2, 13) + assert.equal(path.pathSegList.getItem(1).y2, 14) + assert.equal(path.pathSegList.getItem(1).x, 15) + assert.equal(path.pathSegList.getItem(1).y, 16) + }) + + it('Test svgedit.path.convertPath', function () { + unitsInit({ + getRoundDigits () { return 5 } + }) + + const path = document.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'M40,55h20v20') + + const abs = pathModule.convertPath(path) + assert.equal(abs, 'M40,55L60,55L60,75') + + const rel = pathModule.convertPath(path, true) + assert.equal(rel, 'm40,55l20,0l0,20') + }) +}) diff --git a/data/5-svgedit-unit/recalculate.cy.js b/data/5-svgedit-unit/recalculate.cy.js new file mode 100644 index 0000000..dab7f0b --- /dev/null +++ b/data/5-svgedit-unit/recalculate.cy.js @@ -0,0 +1,176 @@ +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as utilities from '../../../packages/svgcanvas/core/utilities.js' +import * as coords from '../../../packages/svgcanvas/core/coords.js' +import * as recalculate from '../../../packages/svgcanvas/core/recalculate.js' + +describe('recalculate', function () { + const root = document.createElement('div') + root.id = 'root' + root.style.visibility = 'hidden' + + const svgroot = document.createElementNS(NS.SVG, 'svg') + svgroot.id = 'svgroot' + root.append(svgroot) + const svg = document.createElementNS(NS.SVG, 'svg') + svgroot.append(svg) + + const dataStorage = { + _storage: new WeakMap(), + put: function (element, key, obj) { + if (!this._storage.has(element)) { + this._storage.set(element, new Map()) + } + this._storage.get(element).set(key, obj) + }, + get: function (element, key) { + return this._storage.get(element).get(key) + }, + has: function (element, key) { + return this._storage.has(element) && this._storage.get(element).has(key) + }, + remove: function (element, key) { + const ret = this._storage.get(element).delete(key) + if (!this._storage.get(element).size === 0) { + this._storage.delete(element) + } + return ret + } + } + + let elemId = 1 + + /** + * Initilize modules to set up the tests. + * @returns {void} + */ + function setUp () { + utilities.init( + /** + * @implements {module:utilities.EditorContext} + */ + { + getSvgRoot () { return svg }, + getDOMDocument () { return null }, + getDOMContainer () { return null }, + getDataStorage () { return dataStorage } + } + ) + coords.init( + /** + * @implements {module:coords.EditorContext} + */ + { + getGridSnapping () { return false }, + getDrawing () { + return { + getNextId () { return String(elemId++) } + } + }, + getDataStorage () { return dataStorage } + } + ) + recalculate.init( + /** + * @implements {module:recalculate.EditorContext} + */ + { + getSvgRoot () { return svg }, + getStartTransform () { return '' }, + setStartTransform () { /* empty fn */ }, + getDataStorage () { return dataStorage } + } + ) + } + + let elem + + /** + * Initialize for tests and set up `rect` element. + * @returns {void} + */ + function setUpRect () { + setUp() + elem = document.createElementNS(NS.SVG, 'rect') + elem.setAttribute('x', '200') + elem.setAttribute('y', '150') + elem.setAttribute('width', '250') + elem.setAttribute('height', '120') + svg.append(elem) + } + + /** + * Initialize for tests and set up `text` element with `tspan` child. + * @returns {void} + */ + function setUpTextWithTspan () { + setUp() + elem = document.createElementNS(NS.SVG, 'text') + elem.setAttribute('x', '200') + elem.setAttribute('y', '150') + + const tspan = document.createElementNS(NS.SVG, 'tspan') + tspan.setAttribute('x', '200') + tspan.setAttribute('y', '150') + + const theText = 'Foo bar' + tspan.append(theText) + elem.append(tspan) + svg.append(elem) + } + + /** + * Tear down the tests (empty the svg element). + * @returns {void} + */ + afterEach(() => { + while (svg.hasChildNodes()) { + svg.firstChild.remove() + } + }) + + it('Test recalculateDimensions() on rect with identity matrix', function () { + setUpRect() + elem.setAttribute('transform', 'matrix(1,0,0,1,0,0)') + + recalculate.recalculateDimensions(elem) + + // Ensure that the identity matrix is swallowed and the element has no + // transform on it. + assert.equal(elem.hasAttribute('transform'), false) + }) + + it('Test recalculateDimensions() on rect with simple translate', function () { + setUpRect() + elem.setAttribute('transform', 'translate(100,50)') + + recalculate.recalculateDimensions(elem) + + assert.equal(elem.hasAttribute('transform'), false) + assert.equal(elem.getAttribute('x'), '300') + assert.equal(elem.getAttribute('y'), '200') + assert.equal(elem.getAttribute('width'), '250') + assert.equal(elem.getAttribute('height'), '120') + }) + + it('Test recalculateDimensions() on text w/tspan with simple translate', function () { + setUpTextWithTspan() + elem.setAttribute('transform', 'translate(100,50)') + + recalculate.recalculateDimensions(elem) + + // Ensure that the identity matrix is swallowed and the element has no + // transform on it. + assert.equal(elem.hasAttribute('transform'), false) + assert.equal(elem.getAttribute('x'), '300') + assert.equal(elem.getAttribute('y'), '200') + + const tspan = elem.firstElementChild + assert.equal(tspan.getAttribute('x'), '300') + assert.equal(tspan.getAttribute('y'), '200') + }) + + // TODO: Since recalculateDimensions() and surrounding code is + // probably the largest, most complicated and strange piece of + // code in SVG-edit, we need to write a whole lot of unit tests + // for it here. +}) diff --git a/data/5-svgedit-unit/sanitize.cy.js b/data/5-svgedit-unit/sanitize.cy.js new file mode 100644 index 0000000..cf7b281 --- /dev/null +++ b/data/5-svgedit-unit/sanitize.cy.js @@ -0,0 +1,17 @@ +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' +import * as sanitize from '../../../packages/svgcanvas/core/sanitize.js' + +describe('sanitize', function () { + const svg = document.createElementNS(NS.SVG, 'svg') + + it('Test sanitizeSvg() strips ws from style attr', function () { + const rect = document.createElementNS(NS.SVG, 'rect') + rect.setAttribute('style', 'stroke: blue ;\t\tstroke-width :\t\t40;') + // sanitizeSvg() requires the node to have a parent and a document. + svg.append(rect) + sanitize.sanitizeSvg(rect) + + assert.equal(rect.getAttribute('stroke'), 'blue') + assert.equal(rect.getAttribute('stroke-width'), '40') + }) +}) diff --git a/data/5-svgedit-unit/select.cy.js b/data/5-svgedit-unit/select.cy.js new file mode 100644 index 0000000..59ca906 --- /dev/null +++ b/data/5-svgedit-unit/select.cy.js @@ -0,0 +1,152 @@ +import * as select from '../../../packages/svgcanvas/core/select.js' +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' + +describe('select', function () { + const sandbox = document.createElement('div') + sandbox.id = 'sandbox' + + let svgroot + let svgContent + const mockConfig = { + dimensions: [640, 480] + } + const dataStorage = { + _storage: new WeakMap(), + put: function (element, key, obj) { + if (!this._storage.has(element)) { + this._storage.set(element, new Map()) + } + this._storage.get(element).set(key, obj) + }, + get: function (element, key) { + return this._storage.get(element).get(key) + }, + has: function (element, key) { + return this._storage.has(element) && this._storage.get(element).has(key) + }, + remove: function (element, key) { + const ret = this._storage.get(element).delete(key) + if (!this._storage.get(element).size === 0) { + this._storage.delete(element) + } + return ret + } + } + + /** + * @implements {module:select.SVGFactory} + */ + const mockSvgCanvas = { + curConfig: mockConfig, + createSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element) + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value) + }) + return elem + }, + getSvgRoot () { return svgroot }, + getSvgContent () { return svgContent }, + getDataStorage () { return dataStorage } + } + + /** + * Potentially reusable test set-up. + * @returns {void} + */ + beforeEach(() => { + svgroot = mockSvgCanvas.createSVGElement({ + element: 'svg', + attr: { id: 'svgroot' } + }) + svgContent = mockSvgCanvas.createSVGElement({ + element: 'svg', + attr: { id: 'svgcontent' } + }) + + svgroot.append(svgContent) + /* const rect = */ svgContent.append( + mockSvgCanvas.createSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '50', + y: '75', + width: '200', + height: '100' + } + }) + ) + sandbox.append(svgroot) + }) + + /* + function setUpWithInit () { + select.init(mockConfig, mockFactory); + } + */ + + /** + * Tear down the test by emptying our sandbox area. + * @returns {void} + */ + afterEach(() => { + while (sandbox.hasChildNodes()) { + sandbox.firstChild.remove() + } + }) + + it('Test svgedit.select package', function () { + assert.ok(select) + assert.ok(select.Selector) + assert.ok(select.SelectorManager) + assert.ok(select.init) + assert.ok(select.getSelectorManager) + assert.equal(typeof select, typeof {}) + assert.equal(typeof select.Selector, typeof function () { /* empty fn */ }) + assert.equal(typeof select.SelectorManager, typeof function () { /* empty fn */ }) + assert.equal(typeof select.init, typeof function () { /* empty fn */ }) + assert.equal(typeof select.getSelectorManager, typeof function () { /* empty fn */ }) + }) + + it('Test Selector DOM structure', function () { + assert.ok(svgroot) + assert.ok(svgroot.hasChildNodes()) + + // Verify non-existence of Selector DOM nodes + assert.equal(svgroot.childNodes.length, 1) + assert.equal(svgroot.childNodes.item(0), svgContent) + assert.ok(!svgroot.querySelector('#selectorParentGroup')) + + select.init(mockSvgCanvas) + + assert.equal(svgroot.childNodes.length, 3) + + // Verify existence of canvas background. + const cb = svgroot.childNodes.item(0) + assert.ok(cb) + assert.equal(cb.id, 'canvasBackground') + + assert.ok(svgroot.childNodes.item(1)) + assert.equal(svgroot.childNodes.item(1), svgContent) + + // Verify existence of selectorParentGroup. + const spg = svgroot.childNodes.item(2) + assert.ok(spg) + assert.equal(svgroot.querySelector('#selectorParentGroup'), spg) + assert.equal(spg.id, 'selectorParentGroup') + assert.equal(spg.tagName, 'g') + + // Verify existence of all grip elements. + assert.ok(spg.querySelector('#selectorGrip_resize_nw')) + assert.ok(spg.querySelector('#selectorGrip_resize_n')) + assert.ok(spg.querySelector('#selectorGrip_resize_ne')) + assert.ok(spg.querySelector('#selectorGrip_resize_e')) + assert.ok(spg.querySelector('#selectorGrip_resize_se')) + assert.ok(spg.querySelector('#selectorGrip_resize_s')) + assert.ok(spg.querySelector('#selectorGrip_resize_sw')) + assert.ok(spg.querySelector('#selectorGrip_resize_w')) + assert.ok(spg.querySelector('#selectorGrip_rotateconnector')) + assert.ok(spg.querySelector('#selectorGrip_rotate')) + }) +}) diff --git a/data/5-svgedit-unit/test1.cy.js b/data/5-svgedit-unit/test1.cy.js new file mode 100644 index 0000000..8300982 --- /dev/null +++ b/data/5-svgedit-unit/test1.cy.js @@ -0,0 +1,263 @@ +/* eslint-disable max-len, no-console */ +import SvgCanvas from '../../../packages/svgcanvas' + +describe('Basic Module', function () { + // helper functions + /* + const isIdentity = function (m) { + return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0); + }; + const matrixString = function (m) { + return [m.a, m.b, m.c, m.d, m.e, m.f].join(','); + }; + */ + + let svgCanvas + + const + // svgroot = document.getElementById('svgroot'), + // svgdoc = svgroot.documentElement, + svgns = 'http://www.w3.org/2000/svg' + const xlinkns = 'http://www.w3.org/1999/xlink' + + beforeEach(() => { + document.body.textContent = '' + const svgEditor = document.createElement('div') + svgEditor.id = 'svg_editor' + const svgcanvas = document.createElement('div') + svgcanvas.style.visibility = 'hidden' + svgcanvas.id = 'svgcanvas' + const workarea = document.createElement('div') + workarea.id = 'workarea' + workarea.append(svgcanvas) + const toolsLeft = document.createElement('div') + toolsLeft.id = 'tools_left' + + svgEditor.append(workarea, toolsLeft) + document.body.append(svgEditor) + + svgCanvas = new SvgCanvas( + document.getElementById('svgcanvas'), { + canvas_expansion: 3, + dimensions: [640, 480], + initFill: { + color: 'FF0000', // solid red + opacity: 1 + }, + initStroke: { + width: 5, + color: '000000', // solid black + opacity: 1 + }, + initOpacity: 1, + imgPath: '../editor/images', + langPath: 'locale/', + extPath: 'extensions/', + extensions: ['ext-arrows.js', 'ext-eyedropper.js'], + initTool: 'select', + wireframe: false + } + ) + }) + + it('Test existence of SvgCanvas object', function () { + assert.equal(typeof {}, typeof svgCanvas) + }) + + describe('Path Module', function () { + it('Test path conversion from absolute to relative', function () { + const convert = svgCanvas.pathActions.convertPath + + // TODO: Test these paths: + // "m400.00491,625.01379a1.78688,1.78688 0 1 1-3.57373,0a1.78688,1.78688 0 1 13.57373,0z" + // "m36.812,15.8566c-28.03099,0 -26.28099,12.15601 -26.28099,12.15601l0.03099,12.59399h26.75v3.781h-37.37399c0,0 -17.938,-2.034 -133.00001,26.25c115.06201,28.284 130.71801,27.281 130.71801,27.281h9.34399v-13.125c0,0 -0.504,-15.656 15.40601,-15.656h26.532c0,0 14.90599,0.241 14.90599,-14.406v-24.219c0,0 2.263,-14.65601 -27.032,-14.65601zm-14.75,8.4684c2.662,0 4.813,2.151 4.813,4.813c0,2.661 -2.151,4.812 -4.813,4.812c-2.661,0 -4.812,-2.151 -4.812,-4.812c0,-2.662 2.151,-4.813 4.812,-4.813z" + // "m 0,0 l 200,0 l 0,100 L 0,100" + + svgCanvas.setSvgString( + "" + + "" + + "" + + '' + ) + + const p1 = document.getElementById('p1') + const p2 = document.getElementById('p2') + const dAbs = p1.getAttribute('d') + const seglist = p1.pathSegList + + assert.equal(p1.nodeName, 'path', "Expected 'path', got") + + assert.equal(seglist.numberOfItems, 4, 'Number of segments before conversion') + + // verify segments before conversion + let curseg = seglist.getItem(0) + assert.equal(curseg.pathSegTypeAsLetter.toUpperCase(), 'M', 'Before conversion, segment #1 type') + curseg = seglist.getItem(1) + assert.equal(curseg.pathSegTypeAsLetter.toUpperCase(), 'L', 'Before conversion, segment #2 type') + curseg = seglist.getItem(3) + assert.equal(curseg.pathSegTypeAsLetter.toUpperCase(), 'Z', 'Before conversion, segment #3 type' + dAbs) + + // convert and verify segments + let d = convert(p1, true) + assert.equal(d, 'm100,100l100,0l-100,0z', 'Converted path to relative string') + + // TODO: see why this isn't working in SVG-edit + d = convert(p2, true) + console.log('Convert true', d) + d = convert(p2, false) + console.log('Convert false', d) + }) + }) + + describe('Import Module', function () { + it('Test import use', function () { + svgCanvas.setSvgString( + "" + + "" + + "" + + "" + + "" + + '' + ) + + const u = document.getElementById('the-use') + const fu = document.getElementById('foreign-use') + const nfu = document.getElementById('no-use') + + assert.equal((u && u.nodeName), 'use', 'Did not import element') + assert.equal(fu, null, 'Removed element that had a foreign href') + assert.equal(nfu, null, 'Removed element that had no href') + }) + + // This test shows that an element with an invalid attribute is still parsed in properly + // and only the attribute is not imported + it('Test invalid attribute', function () { + svgCanvas.setSvgString( + '' + + 'words' + + '' + ) + + const t = document.getElementById('the-text') + + assert.equal((t && t.nodeName), 'text', 'Did not import element') + assert.equal(t.getAttribute('d'), null, 'Imported a with a d attribute') + }) + + // This test makes sure import/export properly handles namespaced attributes + it('Test importing/exporting namespaced attributes', function () { + /* const setStr = */ svgCanvas.setSvgString( + '' + + '' + + '' + + '' + ) + const attrVal = document.getElementById('se_test_elem').getAttributeNS('http://svg-edit.googlecode.com', 'foo') + + assert.strictEqual(attrVal, 'bar', true, 'Preserved namespaced attribute on import') + + const output = svgCanvas.getSvgString() + const hasXlink = output.includes('xmlns:xlink="http://www.w3.org/1999/xlink"') + const hasSe = output.includes('xmlns:se=') + const hasFoo = output.includes('xmlns:foo=') + const hasAttr = output.includes('se:foo="bar"') + + assert.equal(hasAttr, true, 'Preserved namespaced attribute on export') + assert.equal(hasXlink, true, 'Included xlink: xmlns') + assert.equal(hasSe, true, 'Included se: xmlns') + assert.equal(hasFoo, false, 'Did not include foo: xmlns') + }) + + it('Test import math elements inside a foreignObject', function () { + /* const set = */ svgCanvas.setSvgString( + '' + + '' + + '' + + 'A' + + '0' + + '' + + '' + + '' + + '' + ) + const fo = document.getElementById('fo') + // we cannot use getElementById('math') because not all browsers understand MathML and do not know to use the @id attribute + // see Bug https://bugs.webkit.org/show_bug.cgi?id=35042 + const math = fo.firstChild + + assert.equal(Boolean(math), true, 'Math element exists') + assert.equal(math.nodeName, 'math', 'Math element has the proper nodeName') + assert.equal(math.getAttribute('id'), 'm', 'Math element has an id') + assert.equal(math.namespaceURI, 'http://www.w3.org/1998/Math/MathML', 'Preserved MathML namespace') + }) + + it('Test importing SVG into existing drawing', function () { + /* const doc = */ svgCanvas.setSvgString( + '' + + 'Layer 1' + + '' + + '' + + '' + + '' + ) + + svgCanvas.importSvgString( + '' + + '' + + '' + + '' + ) + + const svgContent = document.getElementById('svgcontent') + const circles = svgContent.getElementsByTagNameNS(svgns, 'circle') + const rects = svgContent.getElementsByTagNameNS(svgns, 'rect') + const ellipses = svgContent.getElementsByTagNameNS(svgns, 'ellipse') + assert.equal(circles.length, 2, 'Found two circles upon importing') + assert.equal(rects.length, 1, 'Found one rectangle upon importing') + assert.equal(ellipses.length, 1, 'Found one ellipse upon importing') + }) + + it('Test importing SVG remaps IDs', function () { + /* const doc = */ svgCanvas.setSvgString( + '' + + 'Layer 1' + + '' + + '' + + '' + + '' + + '' + ) + + svgCanvas.importSvgString( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ) + + const svgContent = document.getElementById('svgcontent') + const circles = svgContent.getElementsByTagNameNS(svgns, 'circle') + const rects = svgContent.getElementsByTagNameNS(svgns, 'rect') + // ellipses = svgContent.getElementsByTagNameNS(svgns, 'ellipse'), + const defs = svgContent.getElementsByTagNameNS(svgns, 'defs') + // grads = svgContent.getElementsByTagNameNS(svgns, 'linearGradient'), + const uses = svgContent.getElementsByTagNameNS(svgns, 'use') + assert.notEqual(circles.item(0).id, 'svg_1', 'Circle not re-identified') + assert.notEqual(rects.item(0).id, 'svg_3', 'Rectangle not re-identified') + // TODO: determine why this test fails in WebKit browsers + // assert.equal(grads.length, 1, 'Linear gradient imported'); + const grad = defs.item(0).firstChild + assert.notEqual(grad.id, 'svg_2', 'Linear gradient not re-identified') + assert.notEqual(circles.item(0).getAttribute('fill'), 'url(#svg_2)', 'Circle fill value not remapped') + assert.notEqual(rects.item(0).getAttribute('stroke'), 'url(#svg_2)', 'Rectangle stroke value not remapped') + assert.notEqual(uses.item(0).getAttributeNS(xlinkns, 'href'), '#svg_3') + }) + }) +}) diff --git a/data/5-svgedit-unit/units.cy.js b/data/5-svgedit-unit/units.cy.js new file mode 100644 index 0000000..e7c437e --- /dev/null +++ b/data/5-svgedit-unit/units.cy.js @@ -0,0 +1,91 @@ +import * as units from '../../../packages/svgcanvas/core/units.js' + +describe('units', function () { + /** + * Set up tests, supplying mock data. + * @returns {void} + */ + beforeEach(() => { + document.body.textContent = '' + const anchor = document.createElement('div') + anchor.id = 'anchor' + anchor.style.visibility = 'hidden' + + const elementsContainer = document.createElement('div') + elementsContainer.id = 'elementsContainer' + + const uniqueId = document.createElement('div') + uniqueId.id = 'uniqueId' + uniqueId.style.visibility = 'hidden' + + const nonUniqueId = document.createElement('div') + nonUniqueId.id = 'nonUniqueId' + nonUniqueId.style.visibility = 'hidden' + + elementsContainer.append(uniqueId, nonUniqueId) + + document.body.append(anchor, elementsContainer) + + units.init( + /** + * @implements {module:units.ElementContainer} + */ + { + getBaseUnit () { return 'cm' }, + getHeight () { return 600 }, + getWidth () { return 800 }, + getRoundDigits () { return 4 }, + getElement (elementId) { return document.getElementById(elementId) } + } + ) + }) + + it('Test svgedit.units package', function () { + assert.ok(units) + assert.equal(typeof units, typeof {}) + }) + + it('Test svgedit.units.shortFloat()', function () { + assert.ok(units.shortFloat) + assert.equal(typeof units.shortFloat, typeof function () { /* empty fn */ }) + + const { shortFloat } = units + assert.equal(shortFloat(0.00000001), 0) + assert.equal(shortFloat(1), 1) + assert.equal(shortFloat(3.45678), 3.4568) + assert.equal(shortFloat(1.23443), 1.2344) + assert.equal(shortFloat(1.23455), 1.2346) + }) + + it('Test svgedit.units.isValidUnit()', function () { + assert.ok(units.isValidUnit) + assert.equal(typeof units.isValidUnit, typeof function () { /* empty fn */ }) + + const { isValidUnit } = units + assert.ok(isValidUnit('0')) + assert.ok(isValidUnit('1')) + assert.ok(isValidUnit('1.1')) + assert.ok(isValidUnit('-1.1')) + assert.ok(isValidUnit('.6mm')) + assert.ok(isValidUnit('-.6cm')) + assert.ok(isValidUnit('6000in')) + assert.ok(isValidUnit('6px')) + assert.ok(isValidUnit('6.3pc')) + assert.ok(isValidUnit('-0.4em')) + assert.ok(isValidUnit('-0.ex')) + assert.ok(isValidUnit('40.123%')) + + assert.equal(isValidUnit('id', 'uniqueId', document.getElementById('uniqueId')), true) + assert.equal(isValidUnit('id', 'newId', document.getElementById('uniqueId')), true) + assert.equal(isValidUnit('id', 'uniqueId'), false) + assert.equal(isValidUnit('id', 'uniqueId', document.getElementById('nonUniqueId')), false) + }) + + it('Test svgedit.units.convertUnit()', function () { + assert.ok(units.convertUnit) + assert.equal(typeof units.convertUnit, typeof function () { /* empty fn */ }) + // cm in default setup + assert.equal(units.convertUnit(42), 1.1113) + assert.equal(units.convertUnit(42, 'px'), 42) + }) +}) diff --git a/data/5-svgedit-unit/utilities-bbox.cy.js b/data/5-svgedit-unit/utilities-bbox.cy.js new file mode 100644 index 0000000..9e5913d --- /dev/null +++ b/data/5-svgedit-unit/utilities-bbox.cy.js @@ -0,0 +1,513 @@ +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' + +// eslint-disable-next-line +chai.use(setAssertionMethods) + +describe('utilities bbox', function () { + /** + * Create an SVG element for a mock. + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function mockCreateSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element) + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value) + }) + return elem + } + let mockaddSVGElementsFromJsonCallCount = 0 + + /** + * Mock of {@link module:utilities.EditorContext#addSVGElementsFromJson}. + * @param {module:utilities.SVGElementJSON} json + * @returns {SVGElement} + */ + function mockaddSVGElementsFromJson (json) { + const elem = mockCreateSVGElement(json) + svgroot.append(elem) + mockaddSVGElementsFromJsonCallCount++ + return elem + } + const mockPathActions = { + resetOrientation (pth) { + if (pth?.nodeName !== 'path') { return false } + const tlist = pth.transform.baseVal + const m = math.transformListToTransform(tlist).matrix + tlist.clear() + pth.removeAttribute('transform') + const segList = pth.pathSegList + + const len = segList.numberOfItems + // let lastX, lastY; + + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i) + const type = seg.pathSegType + if (type === 1) { continue } + const pts = []; + ['', 1, 2].forEach(function (n) { + const x = seg['x' + n]; const y = seg['y' + n] + if (x !== undefined && y !== undefined) { + const pt = math.transformPoint(x, y, m) + pts.splice(pts.length, 0, pt.x, pt.y) + } + }) + path.replacePathSeg(type, i, pts, pth) + } + return undefined + } + } + + const EPSILON = 0.001 + + let svgroot + beforeEach(() => { + document.body.textContent = '' + + // const svg = document.createElementNS(NS.SVG, 'svg'); + const sandbox = document.createElement('div') + sandbox.id = 'sandbox' + document.body.append(sandbox) + + svgroot = mockCreateSVGElement({ + element: 'svg', + attr: { id: 'svgroot' } + }) + sandbox.append(svgroot) + + const mockSvgCanvas = { + createSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element) + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value) + }) + return elem + }, + getSvgRoot () { return svgroot } + } + + path.init(mockSvgCanvas) + mockaddSVGElementsFromJsonCallCount = 0 + }) + + it('Test svgedit.utilities package', function () { + assert.ok(utilities) + assert.ok(utilities.getBBoxWithTransform) + assert.ok(utilities.getStrokedBBox) + assert.ok(utilities.getRotationAngleFromTransformList) + assert.ok(utilities.getRotationAngle) + }) + + it('Test getBBoxWithTransform and no transform', function () { + const { getBBoxWithTransform } = utilities + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3' } + }) + svgroot.append(elem) + let bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 }) + assert.equal(mockaddSVGElementsFromJsonCallCount, 0) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + svgroot.append(elem) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + assert.equal(mockaddSVGElementsFromJsonCallCount, 0) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' } + }) + svgroot.append(elem) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }) + assert.equal(mockaddSVGElementsFromJsonCallCount, 0) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + const g = mockCreateSVGElement({ + element: 'g', + attr: {} + }) + g.append(elem) + svgroot.append(g) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + assert.equal(mockaddSVGElementsFromJsonCallCount, 0) + g.remove() + }) + + it('Test getBBoxWithTransform and a rotation transform', function () { + const { getBBoxWithTransform } = utilities + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M10,10 L20,20', transform: 'rotate(45 10,10)' } + }) + svgroot.append(elem) + let bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.close(bbox.x, 10, EPSILON) + assert.close(bbox.y, 10, EPSILON) + assert.close(bbox.width, 0, EPSILON) + assert.close(bbox.height, Math.sqrt(100 + 100), EPSILON) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '10', y: '10', width: '10', height: '20', transform: 'rotate(90 15,20)' } + }) + svgroot.append(elem) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.close(bbox.x, 5, EPSILON) + assert.close(bbox.y, 15, EPSILON) + assert.close(bbox.width, 20, EPSILON) + assert.close(bbox.height, 10, EPSILON) + assert.equal(mockaddSVGElementsFromJsonCallCount, 1) + elem.remove() + + const rect = { x: 10, y: 10, width: 10, height: 20 } + const angle = 45 + const origin = { x: 15, y: 20 } + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect2', x: rect.x, y: rect.y, width: rect.width, height: rect.height, transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')' } + }) + svgroot.append(elem) + mockaddSVGElementsFromJsonCallCount = 0 + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + const r2 = rotateRect(rect, angle, origin) + assert.close(bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x) + assert.close(bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y) + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width) + assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height) + assert.equal(mockaddSVGElementsFromJsonCallCount, 0) + elem.remove() + + // Same as previous but wrapped with g and the transform is with the g. + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect3', x: rect.x, y: rect.y, width: rect.width, height: rect.height } + }) + const g = mockCreateSVGElement({ + element: 'g', + attr: { transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')' } + }) + g.append(elem) + svgroot.append(g) + mockaddSVGElementsFromJsonCallCount = 0 + bbox = getBBoxWithTransform(g, mockaddSVGElementsFromJson, mockPathActions) + assert.close(bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x) + assert.close(bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y) + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width) + assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height) + assert.equal(mockaddSVGElementsFromJsonCallCount, 0) + g.remove() + + elem = mockCreateSVGElement({ + element: 'ellipse', + attr: { id: 'ellipse1', cx: '100', cy: '100', rx: '50', ry: '50', transform: 'rotate(45 100,100)' } + }) + svgroot.append(elem) + mockaddSVGElementsFromJsonCallCount = 0 + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + /** @todo: Review these test the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. */ + // assert.ok(bbox.x > 45 && bbox.x <= 50); + assert.ok(bbox.y > 45 && bbox.y <= 50) + // assert.ok(bbox.width >= 100 && bbox.width < 110); + // assert.ok(bbox.height >= 100 && bbox.height < 110); + assert.equal(mockaddSVGElementsFromJsonCallCount, 1) + elem.remove() + }) + + it('Test getBBoxWithTransform with rotation and matrix transforms', function () { + const { getBBoxWithTransform } = utilities + + let tx = 10 // tx right + let ty = 10 // tx down + let txInRotatedSpace = Math.sqrt(tx * tx + ty * ty) // translate in rotated 45 space. + let tyInRotatedSpace = 0 + let matrix = 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')' + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M10,10 L20,20', transform: 'rotate(45 10,10) ' + matrix } + }) + svgroot.append(elem) + let bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.close(bbox.x, 10 + tx, EPSILON) + assert.close(bbox.y, 10 + ty, EPSILON) + assert.close(bbox.width, 0, EPSILON) + assert.close(bbox.height, Math.sqrt(100 + 100), EPSILON) + elem.remove() + + txInRotatedSpace = tx // translate in rotated 90 space. + tyInRotatedSpace = -ty + matrix = 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')' + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '10', y: '10', width: '10', height: '20', transform: 'rotate(90 15,20) ' + matrix } + }) + svgroot.append(elem) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.close(bbox.x, 5 + tx, EPSILON) + assert.close(bbox.y, 15 + ty, EPSILON) + assert.close(bbox.width, 20, EPSILON) + assert.close(bbox.height, 10, EPSILON) + elem.remove() + + const rect = { x: 10, y: 10, width: 10, height: 20 } + const angle = 45 + const origin = { x: 15, y: 20 } + tx = 10 // tx right + ty = 10 // tx down + txInRotatedSpace = Math.sqrt(tx * tx + ty * ty) // translate in rotated 45 space. + tyInRotatedSpace = 0 + matrix = 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')' + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect2', x: rect.x, y: rect.y, width: rect.width, height: rect.height, transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ') ' + matrix } + }) + svgroot.append(elem) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + const r2 = rotateRect(rect, angle, origin) + assert.close(bbox.x, r2.x + tx, EPSILON, 'rect2 x is ' + r2.x) + assert.close(bbox.y, r2.y + ty, EPSILON, 'rect2 y is ' + r2.y) + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width) + assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height) + elem.remove() + + // Same as previous but wrapped with g and the transform is with the g. + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect3', x: rect.x, y: rect.y, width: rect.width, height: rect.height } + }) + const g = mockCreateSVGElement({ + element: 'g', + attr: { transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ') ' + matrix } + }) + g.append(elem) + svgroot.append(g) + bbox = getBBoxWithTransform(g, mockaddSVGElementsFromJson, mockPathActions) + assert.close(bbox.x, r2.x + tx, EPSILON, 'rect2 x is ' + r2.x) + assert.close(bbox.y, r2.y + ty, EPSILON, 'rect2 y is ' + r2.y) + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width) + assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height) + g.remove() + + elem = mockCreateSVGElement({ + element: 'ellipse', + attr: { id: 'ellipse1', cx: '100', cy: '100', rx: '50', ry: '50', transform: 'rotate(45 100,100) ' + matrix } + }) + svgroot.append(elem) + bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions) + /** @todo: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. */ + // assert.ok(bbox.x > 45 + tx && bbox.x <= 50 + tx); + assert.ok(bbox.y > 45 + ty && bbox.y <= 50 + ty) + // assert.ok(bbox.width >= 100 && bbox.width < 110); + // assert.ok(bbox.height >= 100 && bbox.height < 110); + elem.remove() + }) + + it('Test getStrokedBBox with stroke-width 10', function () { + const { getStrokedBBox } = utilities + + const strokeWidth = 10 + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3', 'stroke-width': strokeWidth } + }) + svgroot.append(elem) + let bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 2 + strokeWidth, height: 2 + strokeWidth }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': strokeWidth } + }) + svgroot.append(elem) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 5 + strokeWidth, height: 10 + strokeWidth }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6', 'stroke-width': strokeWidth } + }) + svgroot.append(elem) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 5 + strokeWidth, height: 5 + strokeWidth }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': strokeWidth } + }) + const g = mockCreateSVGElement({ + element: 'g', + attr: {} + }) + g.append(elem) + svgroot.append(g) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 5 + strokeWidth, height: 10 + strokeWidth }) + g.remove() + }) + + it("Test getStrokedBBox with stroke-width 'none'", function () { + const { getStrokedBBox } = utilities + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3', 'stroke-width': 'none' } + }) + svgroot.append(elem) + let bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': 'none' } + }) + svgroot.append(elem) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6', 'stroke-width': 'none' } + }) + svgroot.append(elem) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': 'none' } + }) + const g = mockCreateSVGElement({ + element: 'g', + attr: {} + }) + g.append(elem) + svgroot.append(g) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + g.remove() + }) + + it('Test getStrokedBBox with no stroke-width attribute', function () { + const { getStrokedBBox } = utilities + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3' } + }) + svgroot.append(elem) + let bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + svgroot.append(elem) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' } + }) + svgroot.append(elem) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + const g = mockCreateSVGElement({ + element: 'g', + attr: {} + }) + g.append(elem) + svgroot.append(g) + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + g.remove() + }) + + /** + * Returns radians for degrees. + * @param {Float} degrees + * @returns {Float} + */ + function radians (degrees) { + return degrees * Math.PI / 180 + } + + /** + * + * @param {module:utilities.BBoxObject} point + * @param {Float} angle + * @param {module:math.XYObject} origin + * @returns {module:math.XYObject} + */ + function rotatePoint (point, angle, origin = { x: 0, y: 0 }) { + const x = point.x - origin.x + const y = point.y - origin.y + const theta = radians(angle) + return { + x: x * Math.cos(theta) + y * Math.sin(theta) + origin.x, + y: x * Math.sin(theta) + y * Math.cos(theta) + origin.y + } + } + /** + * + * @param {module:utilities.BBoxObject} rect + * @param {Float} angle + * @param {module:math.XYObject} origin + * @returns {module:utilities.BBoxObject} + */ + function rotateRect (rect, angle, origin) { + const tl = rotatePoint({ x: rect.x, y: rect.y }, angle, origin) + const tr = rotatePoint({ x: rect.x + rect.width, y: rect.y }, angle, origin) + const br = rotatePoint({ x: rect.x + rect.width, y: rect.y + rect.height }, angle, origin) + const bl = rotatePoint({ x: rect.x, y: rect.y + rect.height }, angle, origin) + + const minx = Math.min(tl.x, tr.x, bl.x, br.x) + const maxx = Math.max(tl.x, tr.x, bl.x, br.x) + const miny = Math.min(tl.y, tr.y, bl.y, br.y) + const maxy = Math.max(tl.y, tr.y, bl.y, br.y) + + return { + x: minx, + y: miny, + width: (maxx - minx), + height: (maxy - miny) + } + } +}) diff --git a/data/5-svgedit-unit/utilities-performance.cy.js b/data/5-svgedit-unit/utilities-performance.cy.js new file mode 100644 index 0000000..72a3c10 --- /dev/null +++ b/data/5-svgedit-unit/utilities-performance.cy.js @@ -0,0 +1,236 @@ +/* eslint-disable max-len, no-console */ +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' + +describe('utilities performance', function () { + let currentLayer; let groupWithMatrixTransform; let textWithMatrixTransform + beforeEach(() => { + document.body.textContent = '' + const style = document.createElement('style') + style.id = 'styleoverrides' + style.media = 'screen' + style.textContent = ` + #svgcanvas svg * { + cursor: move; + pointer-events: all + } + #svgcanvas svg { + cursor: default + }` + + document.head.append(style) + + const editor = new DOMParser().parseFromString(`
+
+ + + + + + + +
+ + + + + + + + + Layer 1 + + + + + + + + + + + + Some text + + + + Layer 2 + + + + +
+
`, 'application/xml') + const newNode = document.body.ownerDocument.importNode(editor.documentElement, true) + document.body.append(newNode) + + currentLayer = document.getElementById('layer1') + groupWithMatrixTransform = document.getElementById('svg_group_with_matrix_transform') + textWithMatrixTransform = document.getElementById('svg_text_with_matrix_transform') + }) + + /** + * Create an SVG element for a mock. + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function mockCreateSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element) + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value) + }) + return elem + } + + /** + * Mock of {@link module:utilities.EditorContext#addSVGElementsFromJson}. + * @param {module:utilities.SVGElementJSON} json + * @returns {SVGElement} + */ + function mockaddSVGElementsFromJson (json) { + const elem = mockCreateSVGElement(json) + currentLayer.append(elem) + return elem + } + + /** + * Toward performance testing, fill document with clones of element. + * @param {SVGElement} elem + * @param {Integer} count + * @returns {void} + */ + function fillDocumentByCloningElement (elem, count) { + const elemId = elem.getAttribute('id') + '-' + for (let index = 0; index < count; index++) { + const clone = elem.cloneNode(true) // t: deep clone + // Make sure you set a unique ID like a real document. + clone.setAttribute('id', elemId + index) + const { parentNode } = elem + parentNode.append(clone) + } + } + + const mockPathActions = { + resetOrientation (path) { + if (path?.nodeName !== 'path') { return false } + const tlist = path.transform.baseVal + const m = math.transformListToTransform(tlist).matrix + tlist.clear() + path.removeAttribute('transform') + const segList = path.pathSegList + + const len = segList.numberOfItems + // let lastX, lastY; + + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i) + const type = seg.pathSegType + if (type === 1) { + continue + } + const pts = []; + ['', 1, 2].forEach(function (n) { + const x = seg['x' + n] + const y = seg['y' + n] + if (x !== undefined && y !== undefined) { + const pt = math.transformPoint(x, y, m) + pts.splice(pts.length, 0, pt.x, pt.y) + } + }) + // path.replacePathSeg(type, i, pts, path); + } + + return undefined + } + } + + // ////////////////////////////////////////////////////////// + // Performance times with various browsers on Macbook 2011 8MB RAM OS X El Capitan 10.11.4 + // + // To see 'Before Optimization' performance, making the following two edits. + // 1. utilities.getStrokedBBox - change if( elems.length === 1) to if( false && elems.length === 1) + // 2. utilities.getBBoxWithTransform - uncomment 'Old technique that was very slow' + + // Chrome + // Before Optimization + // Pass1 svgCanvas.getStrokedBBox total ms 4,218, ave ms 41.0, min/max 37 51 + // Pass2 svgCanvas.getStrokedBBox total ms 4,458, ave ms 43.3, min/max 32 63 + // Optimized Code + // Pass1 svgCanvas.getStrokedBBox total ms 1,112, ave ms 10.8, min/max 9 20 + // Pass2 svgCanvas.getStrokedBBox total ms 34, ave ms 0.3, min/max 0 20 + + // Firefox + // Before Optimization + // Pass1 svgCanvas.getStrokedBBox total ms 3,794, ave ms 36.8, min/max 33 48 + // Pass2 svgCanvas.getStrokedBBox total ms 4,049, ave ms 39.3, min/max 28 53 + // Optimized Code + // Pass1 svgCanvas.getStrokedBBox total ms 104, ave ms 1.0, min/max 0 23 + // Pass2 svgCanvas.getStrokedBBox total ms 71, ave ms 0.7, min/max 0 23 + + // Safari + // Before Optimization + // Pass1 svgCanvas.getStrokedBBox total ms 4,840, ave ms 47.0, min/max 45 62 + // Pass2 svgCanvas.getStrokedBBox total ms 4,849, ave ms 47.1, min/max 34 62 + // Optimized Code + // Pass1 svgCanvas.getStrokedBBox total ms 42, ave ms 0.4, min/max 0 23 + // Pass2 svgCanvas.getStrokedBBox total ms 17, ave ms 0.2, min/max 0 23 + + it('Test svgCanvas.getStrokedBBox() performance with matrix transforms', function () { + const { getStrokedBBox } = utilities + const { children } = currentLayer + + let lastTime; let now + let min = Number.MAX_VALUE + let max = 0 + let total = 0 + + fillDocumentByCloningElement(groupWithMatrixTransform, 50) + fillDocumentByCloningElement(textWithMatrixTransform, 50) + + // The first pass through all elements is slower. + const count = children.length + const start = lastTime = now = Date.now() + // Skip the first child which is the title. + for (let index = 1; index < count; index++) { + const child = children[index] + /* const obj = */ getStrokedBBox([child], mockaddSVGElementsFromJson, mockPathActions) + now = Date.now(); const delta = now - lastTime; lastTime = now + total += delta + min = Math.min(min, delta) + max = Math.max(max, delta) + } + total = lastTime - start + const ave = total / count + assert.isBelow(ave, 20, 'svgedit.utilities.getStrokedBBox average execution time is less than 20 ms') + console.log('Pass1 svgCanvas.getStrokedBBox total ms ' + total + ', ave ms ' + ave.toFixed(1) + ',\t min/max ' + min + ' ' + max) + + return new Promise((resolve) => { + // The second pass is two to ten times faster. + setTimeout(function () { + const ct = children.length + + const strt = lastTime = now = Date.now() + // Skip the first child which is the title. + for (let index = 1; index < ct; index++) { + const child = children[index] + /* const obj = */ getStrokedBBox([child], mockaddSVGElementsFromJson, mockPathActions) + now = Date.now(); const delta = now - lastTime; lastTime = now + total += delta + min = Math.min(min, delta) + max = Math.max(max, delta) + } + + total = lastTime - strt + const avg = total / ct + assert.isBelow(avg, 2, 'svgedit.utilities.getStrokedBBox average execution time is less than 1 ms') + console.log('Pass2 svgCanvas.getStrokedBBox total ms ' + total + ', ave ms ' + avg.toFixed(1) + ',\t min/max ' + min + ' ' + max) + + resolve() + }) + }) + }) +}) diff --git a/data/5-svgedit-unit/utilities.cy.js b/data/5-svgedit-unit/utilities.cy.js new file mode 100644 index 0000000..f73e3f7 --- /dev/null +++ b/data/5-svgedit-unit/utilities.cy.js @@ -0,0 +1,342 @@ +import * as utilities from '../../../packages/svgcanvas/core/utilities.js' +import { NS } from '../../../packages/svgcanvas/core/namespaces.js' + +describe('utilities', function () { + /** + * Create an element for test. + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function mockCreateSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element) + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value) + }) + return elem + } + /** + * Adds SVG Element per parameters and appends to root. + * @param {module:utilities.SVGElementJSON} json + * @returns {SVGElement} + */ + function mockaddSVGElementsFromJson (json) { + const elem = mockCreateSVGElement(json) + svgroot.append(elem) + return elem + } + const mockPathActions = { resetOrientation () { /* empty fn */ } } + let mockHistorySubCommands = [] + const mockHistory = { + BatchCommand: class { + addSubCommand (cmd) { + mockHistorySubCommands.push(cmd) + } + }, + RemoveElementCommand: class { + // Longhand needed since used as a constructor + constructor (elem, nextSibling, parent) { + this.elem = elem + this.nextSibling = nextSibling + this.parent = parent + } + }, + InsertElementCommand: class { + constructor (path) { // Longhand needed since used as a constructor + this.path = path + } + } + } + const mockCount = { + clearSelection: 0, + addToSelection: 0, + addCommandToHistory: 0 + } + + /** + * Increments clear seleciton count for mock test. + * @returns {void} + */ + function mockClearSelection () { + mockCount.clearSelection++ + } + /** + * Increments add selection count for mock test. + * @returns {void} + */ + function mockAddToSelection () { + mockCount.addToSelection++ + } + /** + * Increments add command to history count for mock test. + * @returns {void} + */ + function mockAddCommandToHistory () { + mockCount.addCommandToHistory++ + } + + const mockSvgCanvas = { + addSVGElementsFromJson: mockaddSVGElementsFromJson, + pathActions: mockPathActions, + clearSelection: mockClearSelection, + addToSelection: mockAddToSelection, + history: mockHistory, + addCommandToHistory: mockAddCommandToHistory + } + + let svg; let svgroot + beforeEach(() => { + document.body.textContent = '' + + mockHistorySubCommands = [] + mockCount.clearSelection = 0 + mockCount.addToSelection = 0 + mockCount.addCommandToHistory = 0 + + const sandbox = document.createElement('div') + svg = document.createElementNS(NS.SVG, 'svg') + svgroot = mockCreateSVGElement({ + element: 'svg', + attr: { id: 'svgroot' } + }) + sandbox.append(svgroot) + document.body.append(sandbox) + }) + + it('Test svgedit.utilities package', function () { + assert.ok(utilities) + assert.ok(utilities.toXml) + assert.equal(typeof utilities.toXml, typeof function () { /* empty fn */ }) + }) + + it('Test svgedit.utilities.toXml() function', function () { + const { toXml } = utilities + + assert.equal(toXml('a'), 'a') + assert.equal(toXml('ABC_'), 'ABC_') + assert.equal(toXml('PB&J'), 'PB&J') + assert.equal(toXml('2 < 5'), '2 < 5') + assert.equal(toXml('5 > 2'), '5 > 2') + assert.equal(toXml('\'<&>"'), ''<&>"') + }) + + it('Test svgedit.utilities.encode64() function', function () { + const { encode64 } = utilities + + assert.equal(encode64('abcdef'), 'YWJjZGVm') + assert.equal(encode64('12345'), 'MTIzNDU=') + assert.equal(encode64(' '), 'IA==') + assert.equal(encode64('`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?'), 'YH4hQCMkJV4mKigpLV89K1t7XX1cfDs6JyIsPC4+Lz8=') + }) + + it('Test svgedit.utilities.decode64() function', function () { + const { decode64 } = utilities + + assert.equal(decode64('YWJjZGVm'), 'abcdef') + assert.equal(decode64('MTIzNDU='), '12345') + assert.equal(decode64('IA=='), ' ') + assert.equal(decode64('YH4hQCMkJV4mKigpLV89K1t7XX1cfDs6JyIsPC4+Lz8='), '`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?') + }) + + it('Test svgedit.utilities.convertToXMLReferences() function', function () { + const convert = utilities.convertToXMLReferences + assert.equal(convert('ABC'), 'ABC') + // assert.equal(convert('�BC'), 'ÀBC'); + }) + + it('Test svgedit.utilities.bboxToObj() function', function () { + const { bboxToObj } = utilities + + const rect = svg.createSVGRect() + rect.x = 1 + rect.y = 2 + rect.width = 3 + rect.height = 4 + + const obj = bboxToObj(rect) + assert.equal(typeof obj, typeof {}) + assert.equal(obj.x, 1) + assert.equal(obj.y, 2) + assert.equal(obj.width, 3) + assert.equal(obj.height, 4) + }) + + it('Test getUrlFromAttr', function () { + assert.equal(utilities.getUrlFromAttr('url(#foo)'), '#foo') + assert.equal(utilities.getUrlFromAttr('url(somefile.svg#foo)'), 'somefile.svg#foo') + assert.equal(utilities.getUrlFromAttr('url("#foo")'), '#foo') + assert.equal(utilities.getUrlFromAttr('url("#foo")'), '#foo') + }) + + it('Test getPathDFromSegments', function () { + const { getPathDFromSegments } = utilities + + const doc = utilities.text2xml('') + const path = doc.createElementNS(NS.SVG, 'path') + path.setAttribute('d', 'm0,0l5,0l0,5l-5,0l0,-5z') + let d = getPathDFromSegments([ + ['M', [1, 2]], + ['Z', []] + ]) + assert.equal(d, 'M1,2 Z') + + d = getPathDFromSegments([ + ['M', [1, 2]], + ['M', [3, 4]], + ['Z', []] + ]) + assert.equal(d, 'M1,2 M3,4 Z') + + d = getPathDFromSegments([ + ['M', [1, 2]], + ['C', [3, 4, 5, 6]], + ['Z', []] + ]) + assert.equal(d, 'M1,2 C3,4 5,6 Z') + }) + + it('Test getPathDFromElement', function () { + const { getPathDFromElement } = utilities + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 Z' } + }) + svgroot.append(elem) + assert.equal(getPathDFromElement(elem), 'M0,1 Z') + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + svgroot.append(elem) + assert.equal(getPathDFromElement(elem), 'M0,1 L5,1 L5,11 L0,11 L0,1 Z') + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'roundrect', x: '0', y: '1', rx: '2', ry: '3', width: '10', height: '11' } + }) + svgroot.append(elem) + const closeEnough = /M0,4 C0,2.3\d* 0.9\d*,1 2,1 L8,1 C9.0\d*,1 10,2.3\d* 10,4 L10,9 C10,10.6\d* 9.0\d*,12 8,12 L2,12 C0.9\d*,12 0,10.6\d* 0,9 L0,4 Z/ + assert.equal(closeEnough.test(getPathDFromElement(elem)), true) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' } + }) + svgroot.append(elem) + assert.equal(getPathDFromElement(elem), 'M0,1L5,6') + elem.remove() + + elem = mockCreateSVGElement({ + element: 'circle', + attr: { id: 'circle', cx: '10', cy: '11', rx: '5', ry: '10' } + }) + svgroot.append(elem) + assert.equal(getPathDFromElement(elem), 'M5,11 C5,5.475138121546961 7.237569060773481,1 10,1 C12.762430939226519,1 15,5.475138121546961 15,11 C15,16.524861878453038 12.762430939226519,21 10,21 C7.237569060773481,21 5,16.524861878453038 5,11 Z') + elem.remove() + + elem = mockCreateSVGElement({ + element: 'polyline', + attr: { id: 'polyline', points: '0,1 5,1 5,11 0,11' } + }) + svgroot.append(elem) + assert.equal(getPathDFromElement(elem), 'M0,1 5,1 5,11 0,11') + elem.remove() + + assert.equal(getPathDFromElement({ tagName: 'something unknown' }), undefined) + }) + + it('Test getBBoxOfElementAsPath', function () { + /** + * Wrap `utilities.getBBoxOfElementAsPath` to convert bbox to object for testing. + * @type {module:utilities.getBBoxOfElementAsPath} + */ + function getBBoxOfElementAsPath (elem, addSVGElementsFromJson, pathActions) { + const bbox = utilities.getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions) + return utilities.bboxToObj(bbox) // need this for assert.equal() to work. + } + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 Z' } + }) + svgroot.append(elem) + let bbox = getBBoxOfElementAsPath(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 0, height: 0 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + svgroot.append(elem) + bbox = getBBoxOfElementAsPath(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }) + elem.remove() + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' } + }) + svgroot.append(elem) + bbox = getBBoxOfElementAsPath(elem, mockaddSVGElementsFromJson, mockPathActions) + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }) + elem.remove() + + // TODO: test element with transform. Need resetOrientation above to be working or mock it. + }) + + it('Test convertToPath rect', function () { + const { convertToPath } = utilities + const attrs = { + fill: 'red', + stroke: 'white', + 'stroke-width': '1', + visibility: 'hidden' + } + + const elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' } + }) + svgroot.append(elem) + const path = convertToPath(elem, attrs, mockSvgCanvas) + assert.equal(path.getAttribute('d'), 'M0,1 L5,1 L5,11 L0,11 L0,1 Z') + assert.equal(path.getAttribute('visibilituy'), null) + assert.equal(path.id, 'rect') + assert.equal(path.parentNode, svgroot) + assert.equal(elem.parentNode, null) + assert.equal(mockHistorySubCommands.length, 2) + assert.equal(mockCount.clearSelection, 1) + assert.equal(mockCount.addToSelection, 1) + assert.equal(mockCount.addCommandToHistory, 1) + path.remove() + }) + + it('Test convertToPath unknown element', function () { + const { convertToPath } = utilities + const attrs = { + fill: 'red', + stroke: 'white', + 'stroke-width': '1', + visibility: 'hidden' + } + + const elem = { + tagName: 'something unknown', + id: 'something-unknown', + getAttribute () { return '' }, + parentNode: svgroot + } + const path = convertToPath(elem, attrs, mockSvgCanvas) + assert.equal(path, null) + assert.equal(elem.parentNode, svgroot) + assert.equal(mockHistorySubCommands.length, 0) + assert.equal(mockCount.clearSelection, 0) + assert.equal(mockCount.addToSelection, 0) + assert.equal(mockCount.addCommandToHistory, 0) + }) +}) diff --git a/data/5-svgedit-unit/zoom.cy.js b/data/5-svgedit-unit/zoom.cy.js new file mode 100644 index 0000000..c2e7365 --- /dev/null +++ b/data/5-svgedit-unit/zoom.cy.js @@ -0,0 +1,212 @@ +import { visitAndApproveStorage } from '../../support/ui-test-helper.js' + +describe('UI - Zoom tool', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('should be able to open', function () { + cy.get('#tool-wrapper > input') + .click({ force: true }) + cy.get('#zoom') + .shadow() + .find('#options-container') + .should('have.css', 'display', 'flex') + }) + + it('should be able to close', function () { + cy.get('#tool_select') + .click({ force: true }) + .get('#zoom') + .shadow() + .find('#options-container') + .should('have.css', 'display', 'none') + }) + + it('should be able to input zoom level', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .shadow() + .find('input') + .type('200', { force: true }) + cy.get('#tool_select') + .click({ force: true }) + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * 2).toString()) + }) + }) + + it('should be able to increment zoom level', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .shadow() + .find('#arrow-up') + .click({ force: true }) + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * 1.1).toString()) + }) + }) + + it('should be able to decrement zoom level', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .shadow() + .find('#arrow-down') + .click({ force: true }) + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * 0.9).toString()) + }) + }) + + it('should be able to select from popup', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click({ force: true }) + .find('se-text') + .first() + .click({ force: true }) + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * (value / 100)).toString()) + .toString() + }) + }) + }) + + it('should be able to resize to fit the current selection', function () { + cy.get('#tool_path').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#tool_select') + .click({ force: true }) + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click({ force: true }) + .find("se-text[value='layer']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) + + it('should be able to resize to fit the canvas', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click({ force: true }) + .find("se-text[value='canvas']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) + + it('should be able to resize to fit the current layer', function () { + cy.get('#tool_path').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click({ force: true }) + .find("se-text[value='layer']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) + + it('should be able to resize to fit the current content', function () { + cy.get('#tool_path').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click({ force: true }) + .find("se-text[value='content']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) +}) diff --git a/golden/0-passing/actions.spec.ts b/golden/0-passing/actions.spec.ts index 1c872dd..91963e1 100644 --- a/golden/0-passing/actions.spec.ts +++ b/golden/0-passing/actions.spec.ts @@ -22,7 +22,7 @@ test.describe('Actions', () => { await page.locator('.action-email').press('Backspace'); await page .locator('.action-email') - .type('slow.typing@email.com', { delay: 100 }); + .pressSequentially('slow.typing@email.com', { delay: 100 }); await expect(page.locator('.action-email')).toHaveValue( 'slow.typing@email.com' ); @@ -32,7 +32,7 @@ test.describe('Actions', () => { // https://on.cypress.io/type await page .locator('.action-email') - .type('slow.typing@email.com', { delay: 100 }); + .pressSequentially('slow.typing@email.com', { delay: 100 }); await expect(page.locator('.action-email')).toHaveValue( 'slow.typing@email.com' ); diff --git a/golden/0-passing/connectors.spec.ts b/golden/0-passing/connectors.spec.ts index e1c4c5c..cbf11a3 100644 --- a/golden/0-passing/connectors.spec.ts +++ b/golden/0-passing/connectors.spec.ts @@ -12,7 +12,7 @@ test.describe('Connectors', () => { // https://on.cypress.io/then const $lis = page.locator('.connectors-list > li'); await expect($lis).toHaveCount(3); - await expect($lis.nth(0)).toHaveText('Walk the dog'); + await expect($lis.first()).toHaveText('Walk the dog'); await expect($lis.nth(1)).toHaveText('Feed the cat'); await expect($lis.nth(2)).toHaveText('Write JavaScript'); }); diff --git a/golden/0-passing/viewport.spec.ts b/golden/0-passing/viewport.spec.ts index 798d88a..2b3fab9 100644 --- a/golden/0-passing/viewport.spec.ts +++ b/golden/0-passing/viewport.spec.ts @@ -27,36 +27,21 @@ test.describe('Viewport', () => { // We added a cy.wait() between each viewport change so you can see // the change otherwise it is a little too fast to see :) // macbook-15 - await page.setViewportSize({ width: 1440, height: 900 }); - await page.waitForTimeout(200); // macbook-13 - await page.setViewportSize({ width: 1280, height: 800 }); - await page.waitForTimeout(200); // macbook-11 - await page.setViewportSize({ width: 1366, height: 768 }); - await page.waitForTimeout(200); // ipad-2 - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(200); // ipad-mini - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(200); // iphone-6+ - await page.setViewportSize({ width: 414, height: 736 }); - await page.waitForTimeout(200); // iphone-6 - await page.setViewportSize({ width: 375, height: 667 }); - await page.waitForTimeout(200); // iphone-5 - await page.setViewportSize({ width: 320, height: 568 }); - await page.waitForTimeout(200); // iphone-4 + await page.setViewportSize({ width: 1440, height: 900 }); // macbook-13 + await page.setViewportSize({ width: 1280, height: 800 }); // macbook-11 + await page.setViewportSize({ width: 1366, height: 768 }); // ipad-2 + await page.setViewportSize({ width: 768, height: 1024 }); // ipad-mini + await page.setViewportSize({ width: 768, height: 1024 }); // iphone-6+ + await page.setViewportSize({ width: 414, height: 736 }); // iphone-6 + await page.setViewportSize({ width: 375, height: 667 }); // iphone-5 + await page.setViewportSize({ width: 320, height: 568 }); // iphone-4 + await page.setViewportSize({ width: 320, height: 480 }); // iphone-3 await page.setViewportSize({ width: 320, height: 480 }); - await page.waitForTimeout(200); // iphone-3 - await page.setViewportSize({ width: 320, height: 480 }); - await page.waitForTimeout(200); // cy.viewport() accepts an orientation for all presets // the default orientation is 'portrait' // ipad-2 - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(200); // iphone-4, landscape + await page.setViewportSize({ width: 768, height: 1024 }); // iphone-4, landscape await page.setViewportSize({ width: 480, height: 320 }); - await page.waitForTimeout(200); - - // The viewport will be reset back to the default dimensions - // in between tests (the default can be set in cypress.config.{js|ts}) }); }); diff --git a/golden/0-passing/waiting.spec.ts b/golden/0-passing/waiting.spec.ts index 4cc9189..9c19cb6 100644 --- a/golden/0-passing/waiting.spec.ts +++ b/golden/0-passing/waiting.spec.ts @@ -10,11 +10,8 @@ test.describe('Waiting', () => { // https://on.cypress.io/wait test('cy.wait() - wait for a specific amount of time', async ({ page }) => { await page.locator('.wait-input1').fill('Wait 1000ms after typing'); - await page.waitForTimeout(1000); await page.locator('.wait-input2').fill('Wait 1000ms after typing'); - await page.waitForTimeout(1000); await page.locator('.wait-input3').fill('Wait 1000ms after typing'); - await page.waitForTimeout(1000); }); test('cy.wait() - wait for a specific route', async ({ page }) => { diff --git a/golden/2-advanced-examples/actions.spec.ts b/golden/2-advanced-examples/actions.spec.ts index f7c53f0..073fcaa 100644 --- a/golden/2-advanced-examples/actions.spec.ts +++ b/golden/2-advanced-examples/actions.spec.ts @@ -38,13 +38,11 @@ test.describe('Actions', () => { await page.keyboard.up('Shift'); await page .locator('.action-email') - .type('slow.typing@email.com', { delay: 100 }); + .pressSequentially('slow.typing@email.com', { delay: 100 }); await expect(page.locator('.action-email')).toHaveValue( 'slow.typing@email.com' ); - await page - .locator('.action-disabled') - .fill('disabled error checking', { force: true }); + await page.locator('.action-disabled').fill('disabled error checking'); await expect(page.locator('.action-disabled')).toHaveValue( 'disabled error checking' ); @@ -209,7 +207,7 @@ test.describe('Actions', () => { await locator.click(); // Ignore error checking prior to clicking - await page.locator('.action-opacity>.btn').click({ force: true }); + await page.locator('.action-opacity>.btn').click(); }); test('.dblclick() - double click on a DOM element', async ({ page }) => { @@ -277,16 +275,12 @@ test.describe('Actions', () => { ).toBeChecked(); // Ignore error checking prior to checking - await page.locator('.action-checkboxes [disabled]').check({ - force: true, - }); + await page.locator('.action-checkboxes [disabled]').check(); await expect(page.locator('.action-checkboxes [disabled]')).toBeChecked(); await page .locator('.action-radios [type="radio"]') .locator('input[value="radio3"]:scope') - .check({ - force: true, - }); + .check(); await expect(page.locator('.action-radios [type="radio"]')).toBeChecked(); }); @@ -340,9 +334,7 @@ test.describe('Actions', () => { ).not.toBeChecked(); // Ignore error checking prior to unchecking - await page.locator('.action-check [disabled]').uncheck({ - force: true, - }); + await page.locator('.action-check [disabled]').uncheck(); await expect(page.locator('.action-check [disabled]')).not.toBeChecked(); }); diff --git a/golden/2-advanced-examples/connectors.spec.ts b/golden/2-advanced-examples/connectors.spec.ts index 63803cf..305214e 100644 --- a/golden/2-advanced-examples/connectors.spec.ts +++ b/golden/2-advanced-examples/connectors.spec.ts @@ -47,7 +47,7 @@ test.describe('Connectors', () => { // https://on.cypress.io/then const $lis = page.locator('.connectors-list > li'); await expect($lis).toHaveCount(3); - await expect($lis.nth(0)).toHaveText('Walk the dog'); + await expect($lis.first()).toHaveText('Walk the dog'); await expect($lis.nth(1)).toHaveText('Feed the cat'); await expect($lis.nth(2)).toHaveText('Write JavaScript'); }); diff --git a/golden/2-advanced-examples/viewport.spec.ts b/golden/2-advanced-examples/viewport.spec.ts index 2a0c937..6a7485c 100644 --- a/golden/2-advanced-examples/viewport.spec.ts +++ b/golden/2-advanced-examples/viewport.spec.ts @@ -27,36 +27,21 @@ test.describe('Viewport', () => { // We added a cy.wait() between each viewport change so you can see // the change otherwise it is a little too fast to see :) // macbook-15 - await page.setViewportSize({ width: 1440, height: 900 }); - await page.waitForTimeout(200); // macbook-13 - await page.setViewportSize({ width: 1280, height: 800 }); - await page.waitForTimeout(200); // macbook-11 - await page.setViewportSize({ width: 1366, height: 768 }); - await page.waitForTimeout(200); // ipad-2 - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(200); // ipad-mini - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(200); // iphone-6+ - await page.setViewportSize({ width: 414, height: 736 }); - await page.waitForTimeout(200); // iphone-6 - await page.setViewportSize({ width: 375, height: 667 }); - await page.waitForTimeout(200); // iphone-5 - await page.setViewportSize({ width: 320, height: 568 }); - await page.waitForTimeout(200); // iphone-4 + await page.setViewportSize({ width: 1440, height: 900 }); // macbook-13 + await page.setViewportSize({ width: 1280, height: 800 }); // macbook-11 + await page.setViewportSize({ width: 1366, height: 768 }); // ipad-2 + await page.setViewportSize({ width: 768, height: 1024 }); // ipad-mini + await page.setViewportSize({ width: 768, height: 1024 }); // iphone-6+ + await page.setViewportSize({ width: 414, height: 736 }); // iphone-6 + await page.setViewportSize({ width: 375, height: 667 }); // iphone-5 + await page.setViewportSize({ width: 320, height: 568 }); // iphone-4 + await page.setViewportSize({ width: 320, height: 480 }); // iphone-3 await page.setViewportSize({ width: 320, height: 480 }); - await page.waitForTimeout(200); // iphone-3 - await page.setViewportSize({ width: 320, height: 480 }); - await page.waitForTimeout(200); // cy.viewport() accepts an orientation for all presets // the default orientation is 'portrait' // ipad-2 - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(200); // iphone-4, landscape + await page.setViewportSize({ width: 768, height: 1024 }); // iphone-4, landscape await page.setViewportSize({ width: 480, height: 320 }); - await page.waitForTimeout(200); - - // The viewport will be reset back to the default dimensions - // in between tests (the default can be set in cypress.config.{js|ts}) }); }); diff --git a/golden/2-advanced-examples/waiting.spec.ts b/golden/2-advanced-examples/waiting.spec.ts index f89b794..8891fdf 100644 --- a/golden/2-advanced-examples/waiting.spec.ts +++ b/golden/2-advanced-examples/waiting.spec.ts @@ -10,11 +10,8 @@ test.describe('Waiting', () => { // https://on.cypress.io/wait test('cy.wait() - wait for a specific amount of time', async ({ page }) => { await page.locator('.wait-input1').fill('Wait 1000ms after typing'); - await page.waitForTimeout(1000); await page.locator('.wait-input2').fill('Wait 1000ms after typing'); - await page.waitForTimeout(1000); await page.locator('.wait-input3').fill('Wait 1000ms after typing'); - await page.waitForTimeout(1000); }); test('cy.wait() - wait for a specific route', async ({ page }) => { diff --git a/golden/4-svgedit-ui/clipboard.spec.ts b/golden/4-svgedit-ui/clipboard.spec.ts new file mode 100644 index 0000000..f4eb8b1 --- /dev/null +++ b/golden/4-svgedit-ui/clipboard.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('UI - Clipboard', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('Editor - Copy and paste', async ({ page }) => { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + + ` + ); + await page.locator('#tool_source_save').click(); + await expect(page.locator('#testCircle')).toBeVisible(); + await expect(page.locator('#svg_1')).not.toBeVisible(); + await expect(page.locator('#svg_2')).not.toBeVisible(); + + // Copy. + await page.locator('#testCircle').click(); + await page.locator('#testCircle').click({ button: 'right' }); + await page.locator('#cmenu_canvas a[href="#copy"]').click(); + + // Paste. + // Scrollbars fail to recenter in Cypress test. Works fine in reality. + // Thus forcing click is needed since workspace is mostly offscreen. + await page.locator('#svgroot').click({ button: 'right' }); + await page.locator('#cmenu_canvas a[href="#paste"]').click(); + await expect(page.locator('#testCircle')).toBeVisible(); + await expect(page.locator('#svg_1')).toBeVisible(); + await expect(page.locator('#svg_2')).not.toBeVisible(); + + // Cut. + await page.locator('#testCircle').click(); + await page.locator('#testCircle').click({ button: 'right' }); + await page.locator('#cmenu_canvas a[href="#cut"]').click(); + await expect(page.locator('#testCircle')).not.toBeVisible(); + await expect(page.locator('#svg_1')).toBeVisible(); + await expect(page.locator('#svg_2')).not.toBeVisible(); + + // Paste. + // Scrollbars fail to recenter in Cypress test. Works fine in reality. + // Thus forcing click is needed since workspace is mostly offscreen. + await page.locator('#svgroot').click({ button: 'right' }); + await page.locator('#cmenu_canvas a[href="#paste"]').click(); + await expect(page.locator('#testCircle')).not.toBeVisible(); + await expect(page.locator('#svg_1')).toBeVisible(); + await expect(page.locator('#svg_2')).toBeVisible(); + + // Delete. + await page.locator('#svg_2').click(); + await page.locator('#svg_2').click({ button: 'right' }); + await page.locator('#cmenu_canvas a[href="#delete"]').click(); + await page.locator('#svg_1').click(); + await page.locator('#svg_1').click({ button: 'right' }); + await page.locator('#cmenu_canvas a[href="#delete"]').click(); + await expect(page.locator('#svg_1')).not.toBeVisible(); + await expect(page.locator('#svg_2')).not.toBeVisible(); + }); +}); diff --git a/golden/4-svgedit-ui/control-points.spec.ts b/golden/4-svgedit-ui/control-points.spec.ts new file mode 100644 index 0000000..7d257ac --- /dev/null +++ b/golden/4-svgedit-ui/control-points.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('UI - Control Points', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('Editor - No parameters: Drag control point of arc path', async ({ + page, + }) => { + const randomOffset = () => 2 + Math.round(10 + Math.random() * 40); + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + + ` + ); + await page.locator('#tool_source_save').click(); + await page.locator('#svg_1').click(); + await page.locator('#svg_1').click(); + await page.locator('#pathpointgrip_0').down(); + await page.mouse.move(randomOffset(), randomOffset()); + await page.mouse.up(); + await page.locator('#pathpointgrip_1').down(); + await page.mouse.move(randomOffset(), randomOffset()); + await page.mouse.up(); + await expect(page.locator('#svg_1[d]')).not.toHaveText(/NaN/); + }); +}); diff --git a/golden/4-svgedit-ui/export.spec.ts b/golden/4-svgedit-ui/export.spec.ts new file mode 100644 index 0000000..c62e36e --- /dev/null +++ b/golden/4-svgedit-ui/export.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; + +import { + visitAndApproveStorage, + openMainMenu, +} from '../../support/ui-test-helper.js'; + +test.describe('UI - Export tests', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('Editor - No parameters: Has export button', async ({ page }) => { + openMainMenu(); + await expect(page.locator('#tool_export')).toBeVisible(); + }); + + test('Editor - No parameters: Export button clicking; dialog opens', async ({ + page, + }) => { + openMainMenu(); + await page.locator('#tool_export').click(); + await expect(page.locator('#dialog_content select')).toBeVisible(); + }); +}); diff --git a/golden/4-svgedit-ui/issue-359.spec.ts b/golden/4-svgedit-ui/issue-359.spec.ts new file mode 100644 index 0000000..522861e --- /dev/null +++ b/golden/4-svgedit-ui/issue-359.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/359 +test.describe('Fix issue 359', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('can undo without throwing', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + + ` + ); + await page.locator('#tool_source_save').click(); + await page.locator('#tool_undo').click(); + await page.locator('#tool_redo').click(); // test also redo to make the test more comprehensive + // if the undo throws an error to the console, the test will fail + }); +}); diff --git a/golden/4-svgedit-ui/issue-407.spec.ts b/golden/4-svgedit-ui/issue-407.spec.ts new file mode 100644 index 0000000..0221892 --- /dev/null +++ b/golden/4-svgedit-ui/issue-407.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/407 +test.describe('Fix issue 407', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + test('can enter edit on text child', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + + hello + + + ` + ); + await page.locator('#tool_source_save').click(); + await page.locator('#svg_1').click(); + await page.locator('#svg_1').dblclick(); + await expect(page.locator('#a_text')).toBeVisible(); + /** @todo: need to understand the reason why this test now fails */ + // cy.get('#a_text') + // .trigger('mousedown', { which: 1, force: true }) + // .trigger('mouseup', { force: true }) + // .dblclick({ force: true }) + // svgedit use the #text text field to capture the text + // cy.get('#text').type('1234', {force: true}) + // cy.get('#a_text').should('have.text', 'he1234llo') + }); +}); diff --git a/golden/4-svgedit-ui/issue-408.spec.ts b/golden/4-svgedit-ui/issue-408.spec.ts new file mode 100644 index 0000000..94a3a9c --- /dev/null +++ b/golden/4-svgedit-ui/issue-408.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/408 +test.describe('Fix issue 408', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('should not throw when showing/saving svg content', async function ({ + page, + }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + + + + + ` + ); + await page.locator('#tool_source_save').click(); + await page.locator('#svg_6').click(); + await page.locator('#svg_6').dblclick(); // change context + await page.locator('#tool_source').click(); // reopen tool_source + await expect(page.locator('#tool_source_save')).toBeVisible(); // The save button should be here if it does not throw + }); +}); diff --git a/golden/4-svgedit-ui/issue-423.spec.ts b/golden/4-svgedit-ui/issue-423.spec.ts new file mode 100644 index 0000000..27e1e1a --- /dev/null +++ b/golden/4-svgedit-ui/issue-423.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/423 +test.describe('Fix issue 423', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('should not throw when undoing the move', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + + + + + + + ` + ); + await page.locator('#tool_source_save').click(); + await page.locator('#TANK1').down(); + await page.mouse.move(50, 0); + await page.mouse.up(); + await page.locator('#tool_undo').click(); + }); +}); diff --git a/golden/4-svgedit-ui/issue-660.spec.ts b/golden/4-svgedit-ui/issue-660.spec.ts new file mode 100644 index 0000000..9811512 --- /dev/null +++ b/golden/4-svgedit-ui/issue-660.spec.ts @@ -0,0 +1,34 @@ +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/660 +test.describe('Fix issue 660', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + await page.setViewportSize({ width: 512, height: 512 }); + }); + /** @todo: reenable this test when we understand why it is passing locally but not on ci */ + test.skip('can resize text', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + hello + + ` + ); + await page.locator('#tool_source_save').click(); + await expect(page.locator('#a_text')).toBeVisible(); + await page.locator('#a_text').down(); + await page.mouse.up(); + await page.locator('#selectorGrip_resize_s').down(); + await page.mouse.move(); + await page.mouse.up(); + + // svgedit use the #text text field to capture the text + await expect(page.locator('#a_text')).toHaveAttribute( + 'transform', + 'matrix(1 0 0 4.54639 0 -540.825)' + ); // Chrome 96 is matrix(1 0 0 4.17431 0 -325.367) + }); +}); diff --git a/golden/4-svgedit-ui/issue-699.spec.ts b/golden/4-svgedit-ui/issue-699.spec.ts new file mode 100644 index 0000000..dc5a8d2 --- /dev/null +++ b/golden/4-svgedit-ui/issue-699.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/699 +test.describe('Fix issue 699', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('should not throw error when undoing and redoing convert to path for a rectangle', async function ({ + page, + }) { + await page.locator('#tool_rect').click(); + await page.locator('#svgcontent').down(150, 150); + await page.mouse.move(250, 200); + await page.mouse.up(); + await page.locator('#tool_topath').click(); + await page.locator('#tool_undo').click(); + await page.locator('#tool_redo').click(); + await page.locator('#tool_undo').click(); + await page.locator('#tool_redo').click(); + }); +}); diff --git a/golden/4-svgedit-ui/issue-726.spec.ts b/golden/4-svgedit-ui/issue-726.spec.ts new file mode 100644 index 0000000..ac0d5ad --- /dev/null +++ b/golden/4-svgedit-ui/issue-726.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/726 +test.describe('Fix issue 726', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('Send forward and send backward should move one layer at a time', async function ({ + page, + }) { + await page.locator('#tool_rect').click(); + await page.locator('#svgcontent').down(250, 250); + await page.mouse.move(350, 350); + await page.mouse.up(); + await page.locator('#tool_rect').click(); + await page.locator('#svgcontent').down(10, 0); + await page.mouse.move(100, 100); + await page.mouse.up(); + await page.locator('#tool_rect').click(); + await page.locator('#svgcontent').down(10, 10); + await page.mouse.move(100, 100); + await page.mouse.up(); + await page + .locator('#svg_3') + .click({ button: 'right', position: { x: 0, y: 0 } }); + await page.locator('a:contains("Send Backward")').click(); + await expect(async () => { + const $div = page.locator('#svg_2'); + const id = $div.nth(0).previousElementSibling.id; + assert.equal(id, 'svg_3'); + }).toPass(); + }); +}); diff --git a/golden/4-svgedit-ui/issue-752.spec.ts b/golden/4-svgedit-ui/issue-752.spec.ts new file mode 100644 index 0000000..ef9c35b --- /dev/null +++ b/golden/4-svgedit-ui/issue-752.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../../support/ui-test-helper.js'; + +// See https://github.com/SVG-Edit/svgedit/issues/752 +test.describe('Fix issue 752', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('Moving an unsnapped shape will not cause selector box misalignment', async function ({ + page, + }) { + await page.locator('#tool_rect').click(); + await page.locator('#svgcontent').down(12, 12); + await page.mouse.move(99, 99); + await page.mouse.up(); + await page.locator('#svg_1').click(); + await page.locator('#tool_editor_prefs').click(); + const elem = page.locator('#grid_snapping_step'); + elem.FIXME_val('35'); + await page.locator('#grid_snapping_on').click(); + await page.locator('#tool_prefs_save').click(); + await page.locator('#svg_1').down(20, 20); + await page.mouse.move(203, 205); + await page.mouse.up(); + await expect(page.locator('#selectedBox0')).toHaveAttribute( + 'd', + 'M192,194 L284,194 284,286 192,286z' + ); + }); +}); diff --git a/golden/4-svgedit-ui/scenario.spec.ts b/golden/4-svgedit-ui/scenario.spec.ts new file mode 100644 index 0000000..c4fb3ba --- /dev/null +++ b/golden/4-svgedit-ui/scenario.spec.ts @@ -0,0 +1,351 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use text tools of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text', async function ({ page }) { + await page.locator('#tool_text').click(); + await page.locator('#svgroot').down(); + await page.mouse.up(); + + // svgedit use the #text text field to capture the text + await page.locator('#text').fill('AB'); + // force text position for snapshot tests being consistent on CI/Interactive + await page + .locator('#selected_x') + .locator('elix-number-spin-box') + .first() + .locator('#inner') + .first() + .press(process.platform === 'darwin' ? 'Meta+a' : 'Control+a'); + await page + .locator('#selected_x') + .locator('elix-number-spin-box') + .first() + .locator('#inner') + .first() + .fill('200'); + await page + .locator('#selected_y') + .locator('elix-number-spin-box') + .first() + .locator('#inner') + .first() + .press(process.platform === 'darwin' ? 'Meta+a' : 'Control+a'); + await page + .locator('#selected_y') + .locator('elix-number-spin-box') + .first() + .locator('#inner') + .first() + .fill('200'); + page.FIXME_svgSnapshot(); + + // cy.get('#svg_1').should('exist') + }); + test('check tool_clone', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_clone').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_italic', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_italic').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_bold', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_bold').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_x_y_coordinate', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 25; n++) { + await page + .locator('#selected_x') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#selected_y') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_font_size', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#font_size') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_stroke_width', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#stroke_width') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_stoke_fill_color', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#stroke_color').locator('#picker').first().click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(51) + .click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + await page.locator('#fill_color').locator('#picker').first().click(); + await page + .locator('#fill_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(3) + .click(); + await page + .locator('#fill_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_blur', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#blur') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_opacity', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#opacity') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_text_align_to_page', async function ({ page }) { + await page.locator('#svg_2').click(); + await page + .locator('#tool_position') + .locator('#select-container') + .first() + .click(); + await page + .locator('#tool_position') + .locator('se-list-item') + .nth(2) + .locator('[aria-label="option"]') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_class', async function ({ page }) { + await page.locator('#svg_2').click(); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('svg_2_class'); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_text_change_id', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#svg_2').click(); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('_id'); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2_id').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_text_delete', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_delete').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_font_family', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#tool_font_family') + .locator('select') + .selectOption({ label: 'Serif' }); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_decoration_underline', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_text_decoration_underline').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_decoration_linethrough', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_text_decoration_linethrough').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_decoration_overline', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_text_decoration_overline').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_letter_spacing', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#tool_letter_spacing') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_word_spacing', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 15; n++) { + await page + .locator('#tool_word_spacing') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_text_length', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 20; n++) { + await page + .locator('#tool_text_length') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_length_adjust', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#tool_length_adjust') + .locator('select') + .FIXME_select(1, { force: true }); + page.FIXME_svgSnapshot(); + }); + test('check tool_text_change_rotation', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 6; n++) { + await page + .locator('#angle') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + await expect(page.locator('#svg_1')).toHaveAttribute( + 'transform', + /rotate\(30/ + ); + // snapshot removed below for inconsistency between local and CI tests. + // cy.svgSnapshot() + }); +}); diff --git a/golden/4-svgedit-ui/scenario1.spec.ts b/golden/4-svgedit-ui/scenario1.spec.ts new file mode 100644 index 0000000..afa01a0 --- /dev/null +++ b/golden/4-svgedit-ui/scenario1.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('check tool shape and image of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_shape', async function ({ page }) { + await page.locator('#tool_shapelib').locator('.overall').first().click(); + await page.locator('[data-shape="heart"]').click(); + await page.locator('#svgroot').move(); + await page.mouse.down(); + await page.mouse.move(); + await page.mouse.up(); + await page.locator('#selectorGrip_rotate').down(); + await page.mouse.move(); + await page.mouse.up(); + + // issue with snapshot not being consistent on CI/Interactive + // cy.svgSnapshot() + // so we use typical DOM tests to validate + await expect(page.locator('#svg_1')).toHaveAttribute('d', /.*/); + + // cy.get('#a_text').should('have.attr', 'transform') + // .and('equal', 'matrix(1 0 0 4.54639 0 -540.825)') // Chrome 96 is matrix(1 0 0 4.17431 0 -325.367) + }); + test('check tool_image', async function ({ page }) { + await page.locator('#tool_image').click(); + await page.locator('#svgroot').down(); + await page.mouse.move(); + await page.mouse.up(); + + // eslint-disable-next-line promise/catch-or-return + const $win = await page.evaluateHandle('window'); + await $win.evaluate(($win) => { + $win.FIXME_contains('OK'); + }); + + // issue with snapshot not being consistent on CI/Interactive + // cy.svgSnapshot() + // so we use typical DOM tests to validate + await expect(page.locator('#svg_2')).toHaveAttribute( + 'xlink:href', + './images/logo.svg' + ); + }); +}); diff --git a/golden/4-svgedit-ui/scenario2.spec.ts b/golden/4-svgedit-ui/scenario2.spec.ts new file mode 100644 index 0000000..56178b1 --- /dev/null +++ b/golden/4-svgedit-ui/scenario2.spec.ts @@ -0,0 +1,149 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use ellipse and circle of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_circle', async function ({ page }) { + await page.locator('#tool_circle').click(); + await page.locator('#svgcontent').down(200, 200); + await page.mouse.move(300, 200); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_fhellipse', async function ({ page }) { + await page.locator('#tool_fhellipse').click(); + await page.locator('#svgcontent').down(400, 200); + await page.mouse.move(); + await page.mouse.move(); + await page.mouse.move(); + await page.mouse.move(); + await page.mouse.up(200, 100); + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse', async function ({ page }) { + await page.locator('#tool_ellipse').click(); + await page.locator('#svgcontent').down(100, 300); + await page.mouse.move(200, 200); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_circle_change_fill_color', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#js-se-palette').locator('.square').nth(8).click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_circle_change_opacity', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#opacity') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_change_rotation', async function ({ page }) { + await page.locator('#svg_3').click(); + for (let n = 0; n < 5; n++) { + await page + .locator('#angle') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_change_blur', async function ({ page }) { + await page.locator('#svg_3').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#blur') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_change_cx_cy_coordinate', async function ({ page }) { + await page.locator('#svg_3').click(); + for (let n = 0; n < 20; n++) { + await page + .locator('#ellipse_cx') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 20; n++) { + await page + .locator('#ellipse_cy') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_change_rx_ry_radius', async function ({ page }) { + await page.locator('#svg_3').click(); + for (let n = 0; n < 20; n++) { + await page + .locator('#ellipse_rx') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 20; n++) { + await page + .locator('#ellipse_ry') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_bring_to_back', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#tool_move_bottom').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_bring_to_front', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#tool_move_top').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_ellipse_clone', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#tool_clone').click(); + page.FIXME_svgSnapshot(); + }); +}); diff --git a/golden/4-svgedit-ui/scenario3.spec.ts b/golden/4-svgedit-ui/scenario3.spec.ts new file mode 100644 index 0000000..e296765 --- /dev/null +++ b/golden/4-svgedit-ui/scenario3.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use path tools of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_path', async function ({ page }) { + await page.locator('#tool_path').click(); + await page.locator('#svgcontent').down(50, 50); + await page.mouse.up(); + await page.mouse.move(100, 50); + await page.mouse.down(100, 50); + await page.mouse.up(); + await page.mouse.move(75, 150); + await page.mouse.down(75, 150); + await page.mouse.up(); + await page.mouse.move(0, 0); + await page.mouse.down(0, 0); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_path_change_node_xy', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#svg_1').dblclick(); + for (let n = 0; n < 25; n++) { + await page + .locator('#path_node_x') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#path_node_y') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_path_change_seg_type', async function ({ page }) { + // cy.get('#svg_1').click({ force: true }) + await page.locator('#svg_1').dblclick(); + await page + .locator('#seg_type') + .locator('select') + .selectOption({ label: '6' }); + await expect(page.locator('#seg_type').locator('select')).toHaveValue('6'); + await page.locator('#ctrlpointgrip_3c1').down(); + await page.mouse.move(130, 175); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_path_change_clone_node', async function ({ page }) { + // cy.get('#svg_1').click({ force: true }) + await page.locator('#svg_1').dblclick(); + await page.locator('#tool_node_clone').click(); + await page.locator('#pathpointgrip_4').down(); + await page.mouse.move(130, 175); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_path_openclose', async function ({ page }) { + await page.locator('#tool_select').click(); + await page.locator('#svg_1').click(); + await page.locator('#svg_1').dblclick(); + await page.locator('#tool_openclose_path').click(); + page.FIXME_svgSnapshot(); + }); + /* it('check tool_path_add_subpath', function () { + cy.get('#tool_add_subpath').click({ force: true }); + cy.get('#svgcontent') + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }); + cy.get('#tool_select').click({ force: true }); + cy.svgSnapshot(); + }); */ +}); diff --git a/golden/4-svgedit-ui/scenario4.spec.ts b/golden/4-svgedit-ui/scenario4.spec.ts new file mode 100644 index 0000000..426c22d --- /dev/null +++ b/golden/4-svgedit-ui/scenario4.spec.ts @@ -0,0 +1,217 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use rect/square tools of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_rect', async function ({ page }) { + await page.locator('#tool_rect').click(); + await page.locator('#svgcontent').down(150, 150); + await page.mouse.move(250, 200); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_fhrect', async function ({ page }) { + await page.locator('#tool_fhrect').click(); + await page.locator('#svgcontent').down(200, 80); + await page.mouse.move(320, 80); + await page.mouse.move(320, 180); + await page.mouse.move(200, 180); + await page.mouse.move(200, 80); + await page.mouse.up(200, 80); + page.FIXME_svgSnapshot(); + }); + test('check tool_square', async function ({ page }) { + await page.locator('#tool_square').click(); + await page.locator('#svgcontent').down(75, 150); + await page.mouse.move(125, 200); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_change_fill_color', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#js-se-palette').locator('.square').nth(8).click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_change_rotation', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 5; n++) { + await page + .locator('#angle') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_change_blur', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#blur') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_change_opacity', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#opacity') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_fhrect_change_x_y_coordinate', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 25; n++) { + await page + .locator('#selected_x') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#selected_y') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_fhrect_change_width_height', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 25; n++) { + await page + .locator('#rect_width') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#rect_height') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_square_clone', async function ({ page }) { + await page.locator('#svg_3').click(); + await page.locator('#tool_clone').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_square_bring_to_back', async function ({ page }) { + await page.locator('#svg_3').click(); + await page.locator('#tool_move_bottom').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_square_bring_to_front', async function ({ page }) { + await page.locator('#svg_3').click(); + await page.locator('#tool_move_top').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_square_change_corner_radius', async function ({ page }) { + await page.locator('#svg_4').click(); + for (let n = 0; n < 25; n++) { + await page + .locator('#rect_rx') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_change_to_path', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#tool_topath').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_delete', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_delete').click(); + await page.locator('#svg_3').click(); + await page.locator('#tool_delete').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_rect_change_class', async function ({ page }) { + await page.locator('#svg_2').click(); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('svg_2_class'); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_rect_change_id', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#svg_2').click(); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('_id'); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2_id').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); +}); diff --git a/golden/4-svgedit-ui/scenario5.spec.ts b/golden/4-svgedit-ui/scenario5.spec.ts new file mode 100644 index 0000000..3e6b27c --- /dev/null +++ b/golden/4-svgedit-ui/scenario5.spec.ts @@ -0,0 +1,233 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use line tools of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line', async function ({ page }) { + await page.locator('#tool_line').click(); + await page.locator('#svgcontent').move(200, 200); + await page.mouse.down(200, 200); + await page.mouse.move(250, 250); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line_change_class', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('svg_1_class'); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_1').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_1_class'); + }); + }); + test('check tool_line_change_id', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#svg_1').click(); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('_id'); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_1_id').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_1_class'); + }); + }); + test('check tool_line_change_rotation', async function ({ page }) { + await page.locator('#svg_1_id').click(); + for (let n = 0; n < 5; n++) { + await page + .locator('#angle') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_line_change_blur', async function ({ page }) { + await page.locator('#svg_1_id').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#blur') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_line_change_opacity', async function ({ page }) { + await page.locator('#svg_1_id').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#opacity') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_line_delete', async function ({ page }) { + await page.locator('#svg_1_id').click(); + await page.locator('#tool_delete').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line_clone', async function ({ page }) { + await page.locator('#tool_line').click(); + await page.locator('#svgcontent').move(200, 200); + await page.mouse.down(200, 200); + await page.mouse.move(250, 250); + await page.mouse.up(); + await page.locator('#svg_2').click(); + await page.locator('#tool_clone').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line_bring_to_back', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#tool_move_bottom').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line_bring_to_front', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#tool_move_top').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line_change_x_y_coordinate', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 25; n++) { + await page + .locator('#line_x1') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#line_y1') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#line_x2') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + for (let n = 0; n < 25; n++) { + await page + .locator('#line_y2') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_line_change_stroke_width', async function ({ page }) { + await page.locator('#svg_2').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#stroke_width') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_line_change_stoke_color', async function ({ page }) { + await page.locator('#svg_3').click(); + await page.locator('#stroke_color').locator('#picker').first().click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(9) + .click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_line_align_to_page', async function ({ page }) { + await page.locator('#svg_3').click(); + await page + .locator('#tool_position') + .locator('#select-container') + .first() + .click(); + await page + .locator('#tool_position') + .locator('se-list-item') + .nth(2) + .locator('[aria-label="option"]') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); +}); diff --git a/golden/4-svgedit-ui/scenario6.spec.ts b/golden/4-svgedit-ui/scenario6.spec.ts new file mode 100644 index 0000000..a3e3491 --- /dev/null +++ b/golden/4-svgedit-ui/scenario6.spec.ts @@ -0,0 +1,233 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use polygon tools of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon', async function ({ page }) { + await page.locator('#tool_polygon').click(); + await page.locator('#svgcontent').down(325, 250); + await page.mouse.move(325, 345); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_clone', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_clone').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_change_class', async function ({ page }) { + await page.locator('#svg_2').click(); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('svg_2_class'); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_polygon_change_id', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#svg_2').click(); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('_id'); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2_id').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_polygon_change_rotation', async function ({ page }) { + await page.locator('#svg_2_id').click(); + for (let n = 0; n < 5; n++) { + await page + .locator('#angle') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_change_blur', async function ({ page }) { + await page.locator('#svg_2_id').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#blur') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_change_opacity', async function ({ page }) { + await page.locator('#svg_2_id').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#opacity') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_bring_to_back', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_move_bottom').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_bring_to_front', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_move_top').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_delete', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_delete').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_align_to_page', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#tool_position') + .locator('#select-container') + .first() + .click(); + await page + .locator('#tool_position') + .locator('se-list-item') + .nth(2) + .locator('[aria-label="option"]') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + /* it('check tool_polygon_change_x_y_coordinate', function () { + cy.get('#svg_1').click({ force: true }); + for(let n = 0; n < 25; n ++){ + cy.get('#selected_x').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }); + } + for(let n = 0; n < 25; n ++){ + cy.get('#selected_y').shadow().find('elix-number-spin-box').eq(0).shadow().find('#upButton').eq(0) + .click({ force: true }); + } + cy.svgSnapshot(); + }); */ + test('check tool_polygon_change_stroke_width', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#stroke_width') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_change_stoke_fill_color', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#stroke_color').locator('#picker').first().click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(51) + .click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + await page.locator('#fill_color').locator('#picker').first().click(); + await page + .locator('#fill_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(3) + .click(); + await page + .locator('#fill_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_polygon_change_sides', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#polySides') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); +}); diff --git a/golden/4-svgedit-ui/scenario7.spec.ts b/golden/4-svgedit-ui/scenario7.spec.ts new file mode 100644 index 0000000..f30210c --- /dev/null +++ b/golden/4-svgedit-ui/scenario7.spec.ts @@ -0,0 +1,221 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('use star tools of svg-edit', function () { + test.beforeAll(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('check tool_source_set', async function ({ page }) { + await page.locator('#tool_source').click(); + await page.locator('#svg_source_textarea').fill( + ` + + Layer 1 + + ` + ); + await page.locator('#tool_source_save').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star', async function ({ page }) { + await page.locator('#tool_star').click(); + await page.locator('#svgcontent').down(300, 150); + await page.mouse.move(300, 250); + await page.mouse.up(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_clone', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#tool_clone').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_change_class', async function ({ page }) { + await page.locator('#svg_2').click(); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('svg_2_class'); + await page + .locator('#elem_class') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_star_change_id', async function ({ page }) { + await page.locator('#svg_2').click(); + await page.locator('#svg_2').click(); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .fill('_id'); + await page + .locator('#elem_id') + .locator('elix-input') + .first() + .locator('#inner') + .first() + .press('Enter'); + await page.locator('#svg_2_id').FIXME_should('satisfy', ($el) => { + const classList = Array.from($el[0].classList); + return classList.includes('svg_2_class'); + }); + }); + test('check tool_star_change_rotation', async function ({ page }) { + await page.locator('#svg_2_id').click(); + for (let n = 0; n < 5; n++) { + await page + .locator('#angle') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_star_change_blur', async function ({ page }) { + await page.locator('#svg_2_id').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#blur') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_star_change_opacity', async function ({ page }) { + await page.locator('#svg_2_id').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#opacity') + .locator('elix-number-spin-box') + .first() + .locator('#downButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_star_bring_to_back', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_move_bottom').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_bring_to_front', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_move_top').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_delete', async function ({ page }) { + await page.locator('#svg_2_id').click(); + await page.locator('#tool_delete').click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_align_to_page', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#tool_position') + .locator('#select-container') + .first() + .click(); + await page + .locator('#tool_position') + .locator('se-list-item') + .nth(2) + .locator('[aria-label="option"]') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_change_stroke_width', async function ({ page }) { + await page.locator('#svg_1').click(); + for (let n = 0; n < 10; n++) { + await page + .locator('#stroke_width') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + } + page.FIXME_svgSnapshot(); + }); + test('check tool_star_change_stoke_fill_color', async function ({ page }) { + await page.locator('#svg_1').click(); + await page.locator('#stroke_color').locator('#picker').first().click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(51) + .click(); + await page + .locator('#stroke_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + await page.locator('#fill_color').locator('#picker').first().click(); + await page + .locator('#fill_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('.QuickColor') + .nth(3) + .click(); + await page + .locator('#fill_color') + .locator('#color_picker') + .first() + .locator('#jGraduate_colPick') + .first() + .locator('#jPicker-table') + .first() + .locator('#Ok') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); + test('check tool_star_change_sides', async function ({ page }) { + await page.locator('#svg_1').click(); + await page + .locator('#starNumPoints') + .locator('elix-number-spin-box') + .first() + .locator('#upButton') + .first() + .click(); + page.FIXME_svgSnapshot(); + }); +}); diff --git a/golden/4-svgedit-ui/tool-selection.spec.ts b/golden/4-svgedit-ui/tool-selection.spec.ts new file mode 100644 index 0000000..8e77737 --- /dev/null +++ b/golden/4-svgedit-ui/tool-selection.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('UI - Tool selection', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('should set rectangle selection by click', async function ({ page }) { + await expect(page.locator('#tools_rect')).not.toHaveAttribute( + 'pressed', + /.*/ + ); + await page.locator('#tools_rect').dispatchEvent('click'); + await expect(page.locator('#tools_rect')).toHaveAttribute('pressed', /.*/); + }); +}); diff --git a/golden/5-svgedit-unit/browser-bugs/removeItem-setAttribute.spec.ts b/golden/5-svgedit-unit/browser-bugs/removeItem-setAttribute.spec.ts new file mode 100644 index 0000000..70a8023 --- /dev/null +++ b/golden/5-svgedit-unit/browser-bugs/removeItem-setAttribute.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Browser bugs', function () { + test('removeItem and setAttribute test (Chromium 843901; now fixed)', async function ({ + page, + }) { + // See https://bugs.chromium.org/p/chromium/issues/detail?id=843901 + const elem = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + elem.setAttribute('transform', 'matrix(1,0,0,1,0,0)'); + elem.transform.baseVal.removeItem(0); + elem.removeAttribute('transform'); + assert.equal(elem.hasAttribute('transform'), false); + }); +}); diff --git a/golden/5-svgedit-unit/contextmenu.spec.ts b/golden/5-svgedit-unit/contextmenu.spec.ts new file mode 100644 index 0000000..05650c8 --- /dev/null +++ b/golden/5-svgedit-unit/contextmenu.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; + +import * as contextmenu from '../../../src/editor/contextmenu.js'; + +test.describe('contextmenu', function () { + /** + * Tear down tests, resetting custom menus. + * @returns {void} + */ + test.afterEach(async ({ page }) => { + contextmenu.resetCustomMenus(); + }); + + test('Test svgedit.contextmenu package', async function ({ page }) { + assert.ok(contextmenu, 'contextmenu registered correctly'); + assert.ok(contextmenu.add, 'add registered correctly'); + assert.ok( + contextmenu.hasCustomHandler, + 'contextmenu hasCustomHandler registered correctly' + ); + assert.ok( + contextmenu.getCustomHandler, + 'contextmenu getCustomHandler registered correctly' + ); + }); + + test('Test svgedit.contextmenu does not add invalid menu item', async function ({ + page, + }) { + assert.throws( + () => contextmenu.add({ id: 'justanid' }), + null, + null, + 'menu item with just an id is invalid' + ); + + assert.throws( + () => contextmenu.add({ id: 'idandlabel', label: 'anicelabel' }), + null, + null, + 'menu item with just an id and label is invalid' + ); + + assert.throws( + () => + contextmenu.add({ + id: 'idandlabel', + label: 'anicelabel', + action: 'notafunction', + }), + null, + null, + 'menu item with action that is not a function is invalid' + ); + }); + + test('Test svgedit.contextmenu adds valid menu item', async function ({ + page, + }) { + const validItem = { + id: 'valid', + label: 'anicelabel', + action() { + /* empty fn */ + }, + }; + contextmenu.add(validItem); + + assert.ok( + contextmenu.hasCustomHandler('valid'), + 'Valid menu item is added.' + ); + assert.equal( + contextmenu.getCustomHandler('valid'), + validItem.action, + 'Valid menu action is added.' + ); + }); + + test('Test svgedit.contextmenu rejects valid duplicate menu item id', async function ({ + page, + }) { + const validItem1 = { + id: 'valid', + label: 'anicelabel', + action() { + /* empty fn */ + }, + }; + const validItem2 = { + id: 'valid', + label: 'anicelabel', + action() { + /* empty fn */ + }, + }; + contextmenu.add(validItem1); + + assert.throws( + () => contextmenu.add(validItem2), + null, + null, + 'duplicate menu item is rejected.' + ); + }); +}); diff --git a/golden/5-svgedit-unit/coords.spec.ts b/golden/5-svgedit-unit/coords.spec.ts new file mode 100644 index 0000000..5b96b18 --- /dev/null +++ b/golden/5-svgedit-unit/coords.spec.ts @@ -0,0 +1,346 @@ +import { test, expect } from '@playwright/test'; + +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as utilities from '../../../packages/svgcanvas/core/utilities.js'; +import * as coords from '../../../packages/svgcanvas/core/coords.js'; + +test.describe('coords', function () { + let elemId = 1; + + const root = document.createElement('div'); + root.id = 'root'; + root.style.visibility = 'hidden'; + document.body.append(root); + + /** + * Set up tests with mock data. + * @returns {void} + */ + test.beforeEach(async function ({ page }) { + const svgroot = document.createElementNS(NS.SVG, 'svg'); + svgroot.id = 'svgroot'; + root.append(svgroot); + this.svg = document.createElementNS(NS.SVG, 'svg'); + svgroot.append(this.svg); + + // Mock out editor context. + utilities.init( + /** + * @implements {module:utilities.EditorContext} + */ + { + getSvgRoot: () => { + return this.svg; + }, + getDOMDocument() { + return null; + }, + getDOMContainer() { + return null; + }, + } + ); + coords.init( + /** + * @implements {module:coords.EditorContext} + */ + { + getGridSnapping() { + return false; + }, + getDrawing() { + return { + getNextId() { + return String(elemId++); + }, + }; + }, + } + ); + }); + + /** + * Tear down tests, removing elements. + * @returns {void} + */ + test.afterEach(async function ({ page }) { + while (this.svg.hasChildNodes()) { + this.svg.firstChild.remove(); + } + }); + + test('Test remapElement(translate) for rect', async function ({ page }) { + const rect = document.createElementNS(NS.SVG, 'rect'); + rect.setAttribute('x', '200'); + rect.setAttribute('y', '150'); + rect.setAttribute('width', '250'); + rect.setAttribute('height', '120'); + this.svg.append(rect); + + const attrs = { + x: '200', + y: '150', + width: '125', + height: '75', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 100; + m.f = -50; + + coords.remapElement(rect, attrs, m); + + assert.equal(rect.getAttribute('x'), '300'); + assert.equal(rect.getAttribute('y'), '100'); + assert.equal(rect.getAttribute('width'), '125'); + assert.equal(rect.getAttribute('height'), '75'); + }); + + test('Test remapElement(scale) for rect', async function ({ page }) { + const rect = document.createElementNS(NS.SVG, 'rect'); + rect.setAttribute('width', '250'); + rect.setAttribute('height', '120'); + this.svg.append(rect); + + const attrs = { + x: '0', + y: '0', + width: '250', + height: '120', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 2; + m.b = 0; + m.c = 0; + m.d = 0.5; + m.e = 0; + m.f = 0; + + coords.remapElement(rect, attrs, m); + + assert.equal(rect.getAttribute('x'), '0'); + assert.equal(rect.getAttribute('y'), '0'); + assert.equal(rect.getAttribute('width'), '500'); + assert.equal(rect.getAttribute('height'), '60'); + }); + + test('Test remapElement(translate) for circle', async function ({ page }) { + const circle = document.createElementNS(NS.SVG, 'circle'); + circle.setAttribute('cx', '200'); + circle.setAttribute('cy', '150'); + circle.setAttribute('r', '125'); + this.svg.append(circle); + + const attrs = { + cx: '200', + cy: '150', + r: '125', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 100; + m.f = -50; + + coords.remapElement(circle, attrs, m); + + assert.equal(circle.getAttribute('cx'), '300'); + assert.equal(circle.getAttribute('cy'), '100'); + assert.equal(circle.getAttribute('r'), '125'); + }); + + test('Test remapElement(scale) for circle', async function ({ page }) { + const circle = document.createElementNS(NS.SVG, 'circle'); + circle.setAttribute('cx', '200'); + circle.setAttribute('cy', '150'); + circle.setAttribute('r', '250'); + this.svg.append(circle); + + const attrs = { + cx: '200', + cy: '150', + r: '250', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 2; + m.b = 0; + m.c = 0; + m.d = 0.5; + m.e = 0; + m.f = 0; + + coords.remapElement(circle, attrs, m); + + assert.equal(circle.getAttribute('cx'), '400'); + assert.equal(circle.getAttribute('cy'), '75'); + // Radius is the minimum that fits in the new bounding box. + assert.equal(circle.getAttribute('r'), '125'); + }); + + test('Test remapElement(translate) for ellipse', async function ({ page }) { + const ellipse = document.createElementNS(NS.SVG, 'ellipse'); + ellipse.setAttribute('cx', '200'); + ellipse.setAttribute('cy', '150'); + ellipse.setAttribute('rx', '125'); + ellipse.setAttribute('ry', '75'); + this.svg.append(ellipse); + + const attrs = { + cx: '200', + cy: '150', + rx: '125', + ry: '75', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 100; + m.f = -50; + + coords.remapElement(ellipse, attrs, m); + + assert.equal(ellipse.getAttribute('cx'), '300'); + assert.equal(ellipse.getAttribute('cy'), '100'); + assert.equal(ellipse.getAttribute('rx'), '125'); + assert.equal(ellipse.getAttribute('ry'), '75'); + }); + + test('Test remapElement(scale) for ellipse', async function ({ page }) { + const ellipse = document.createElementNS(NS.SVG, 'ellipse'); + ellipse.setAttribute('cx', '200'); + ellipse.setAttribute('cy', '150'); + ellipse.setAttribute('rx', '250'); + ellipse.setAttribute('ry', '120'); + this.svg.append(ellipse); + + const attrs = { + cx: '200', + cy: '150', + rx: '250', + ry: '120', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 2; + m.b = 0; + m.c = 0; + m.d = 0.5; + m.e = 0; + m.f = 0; + + coords.remapElement(ellipse, attrs, m); + + assert.equal(ellipse.getAttribute('cx'), '400'); + assert.equal(ellipse.getAttribute('cy'), '75'); + assert.equal(ellipse.getAttribute('rx'), '500'); + assert.equal(ellipse.getAttribute('ry'), '60'); + }); + + test('Test remapElement(translate) for line', async function ({ page }) { + const line = document.createElementNS(NS.SVG, 'line'); + line.setAttribute('x1', '50'); + line.setAttribute('y1', '100'); + line.setAttribute('x2', '120'); + line.setAttribute('y2', '200'); + this.svg.append(line); + + const attrs = { + x1: '50', + y1: '100', + x2: '120', + y2: '200', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 100; + m.f = -50; + + coords.remapElement(line, attrs, m); + + assert.equal(line.getAttribute('x1'), '150'); + assert.equal(line.getAttribute('y1'), '50'); + assert.equal(line.getAttribute('x2'), '220'); + assert.equal(line.getAttribute('y2'), '150'); + }); + + test('Test remapElement(scale) for line', async function ({ page }) { + const line = document.createElementNS(NS.SVG, 'line'); + line.setAttribute('x1', '50'); + line.setAttribute('y1', '100'); + line.setAttribute('x2', '120'); + line.setAttribute('y2', '200'); + this.svg.append(line); + + const attrs = { + x1: '50', + y1: '100', + x2: '120', + y2: '200', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 2; + m.b = 0; + m.c = 0; + m.d = 0.5; + m.e = 0; + m.f = 0; + + coords.remapElement(line, attrs, m); + + assert.equal(line.getAttribute('x1'), '100'); + assert.equal(line.getAttribute('y1'), '50'); + assert.equal(line.getAttribute('x2'), '240'); + assert.equal(line.getAttribute('y2'), '100'); + }); + + test('Test remapElement(translate) for text', async function ({ page }) { + const text = document.createElementNS(NS.SVG, 'text'); + text.setAttribute('x', '50'); + text.setAttribute('y', '100'); + this.svg.append(text); + + const attrs = { + x: '50', + y: '100', + }; + + // Create a translate. + const m = this.svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 100; + m.f = -50; + + coords.remapElement(text, attrs, m); + + assert.equal(text.getAttribute('x'), '150'); + assert.equal(text.getAttribute('y'), '50'); + }); +}); diff --git a/golden/5-svgedit-unit/draw.spec.ts b/golden/5-svgedit-unit/draw.spec.ts new file mode 100644 index 0000000..b0fc98e --- /dev/null +++ b/golden/5-svgedit-unit/draw.spec.ts @@ -0,0 +1,961 @@ +import { test, expect } from '@playwright/test'; + +import 'pathseg'; +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as draw from '../../../packages/svgcanvas/core/draw.js'; +import * as units from '../../../packages/svgcanvas/core/units.js'; + +test.describe('draw.Drawing', function () { + const addOwnSpies = (obj) => { + const methods = Object.keys(obj); + methods.forEach(async (method) => { + page.FIXME_spy(obj, method); + }); + }; + + const LAYER_CLASS = draw.Layer.CLASS_NAME; + const NONCE = 'foo'; + const LAYER1 = 'Layer 1'; + const LAYER2 = 'Layer 2'; + const LAYER3 = 'Layer 3'; + const PATH_ATTR = { + // clone will convert relative to absolute, so the test for equality fails. + // d: 'm7.38867,57.38867c0,-27.62431 22.37569,-50 50,-50c27.62431,0 50,22.37569 50,50c0,27.62431 -22.37569,50 -50,50c-27.62431,0 -50,-22.37569 -50,-50z', + d: 'M7.389,57.389C7.389,29.764 29.764,7.389 57.389,7.389C85.013,7.389 107.389,29.764 107.389,57.389C107.389,85.013 85.013,107.389 57.389,107.389C29.764,107.389 7.389,85.013 7.389,57.389z', + transform: 'rotate(45 57.388671875000036,57.388671874999986) ', + 'stroke-width': '5', + stroke: '#660000', + fill: '#ff0000', + }; + + units.init( + /** + * @implements {module:units.ElementContainer} + */ + { + // used by units.shortFloat - call path: cloneLayer -> copyElem -> convertPath -> pathDSegment -> shortFloat + getRoundDigits() { + return 3; + }, + } + ); + + // Simplifying from svgcanvas.js usage + const idprefix = 'svg_'; + + const getCurrentDrawing = function () { + return currentDrawing_; + }; + const setCurrentGroup = () => { + /* empty fn */ + }; + draw.init( + /** + * @implements {module:draw.DrawCanvasInit} + */ + { + getCurrentDrawing, + setCurrentGroup, + } + ); + + /** + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function createSVGElement(jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element); + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value); + }); + return elem; + } + + const setupSVGWith3Layers = function (svgElem) { + const layer1 = document.createElementNS(NS.SVG, 'g'); + const layer1Title = document.createElementNS(NS.SVG, 'title'); + layer1Title.append(LAYER1); + layer1.append(layer1Title); + svgElem.append(layer1); + + const layer2 = document.createElementNS(NS.SVG, 'g'); + const layer2Title = document.createElementNS(NS.SVG, 'title'); + layer2Title.append(LAYER2); + layer2.append(layer2Title); + svgElem.append(layer2); + + const layer3 = document.createElementNS(NS.SVG, 'g'); + const layer3Title = document.createElementNS(NS.SVG, 'title'); + layer3Title.append(LAYER3); + layer3.append(layer3Title); + svgElem.append(layer3); + + return [layer1, layer2, layer3]; + }; + + const createSomeElementsInGroup = function (group) { + group.append( + createSVGElement({ + element: 'path', + attr: PATH_ATTR, + }), + // createSVGElement({ + // element: 'path', + // attr: {d: 'M0,1L2,3'} + // }), + createSVGElement({ + element: 'rect', + attr: { x: '0', y: '1', width: '5', height: '10' }, + }), + createSVGElement({ + element: 'line', + attr: { x1: '0', y1: '1', x2: '5', y2: '6' }, + }) + ); + + const g = createSVGElement({ + element: 'g', + attr: {}, + }); + g.append( + createSVGElement({ + element: 'rect', + attr: { x: '0', y: '1', width: '5', height: '10' }, + }) + ); + group.append(g); + return 4; + }; + + const cleanupSVG = function (svgElem) { + while (svgElem.firstChild) { + svgElem.firstChild.remove(); + } + }; + + let sandbox; + let currentDrawing_; + let svg; + let svgN; + test.beforeEach(async ({ page }) => { + sandbox = document.createElement('div'); + sandbox.id = 'sandbox'; + sandbox.style.visibility = 'hidden'; + + svg = document.createElementNS(NS.SVG, 'svg'); + // Firefox throws exception in getBBox() when svg is not attached to DOM. + sandbox.append(svg); + + // Set up with nonce. + svgN = document.createElementNS(NS.SVG, 'svg'); + svgN.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE); + svgN.setAttributeNS(NS.SE, 'se:nonce', NONCE); + + const svgContent = document.createElementNS(NS.SVG, 'svg'); + currentDrawing_ = new draw.Drawing(svgContent, idprefix); + }); + + test('Test draw module', async function ({ page }) { + assert.ok(draw); + assert.equal(typeof draw, typeof {}); + + assert.ok(draw.Drawing); + assert.equal( + typeof draw.Drawing, + typeof function () { + /* empty fn */ + } + ); + }); + + test('Test document creation', async function ({ page }) { + let doc; + try { + doc = new draw.Drawing(); + assert.ok(false, 'Created drawing without a valid element'); + } catch (e) { + assert.ok(true); + } + + try { + doc = new draw.Drawing(svg); + assert.ok(doc); + assert.equal(typeof doc, typeof {}); + } catch (e) { + assert.ok( + false, + 'Could not create document from valid element: ' + e + ); + } + }); + + test('Test nonce', async function ({ page }) { + let doc = new draw.Drawing(svg); + assert.equal(doc.getNonce(), ''); + + doc = new draw.Drawing(svgN); + assert.equal(doc.getNonce(), NONCE); + assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE); + + doc.clearNonce(); + assert.ok(!doc.getNonce()); + assert.ok(!doc.getSvgElem().getAttributeNS(NS.SE, 'se:nonce')); + + doc.setNonce(NONCE); + assert.equal(doc.getNonce(), NONCE); + assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE); + }); + + test('Test getId() and getNextId() without nonce', async function ({ page }) { + const elem2 = document.createElementNS(NS.SVG, 'circle'); + elem2.id = 'svg_2'; + svg.append(elem2); + + const doc = new draw.Drawing(svg); + + assert.equal(doc.getId(), 'svg_0'); + + assert.equal(doc.getNextId(), 'svg_1'); + assert.equal(doc.getId(), 'svg_1'); + + assert.equal(doc.getNextId(), 'svg_3'); + assert.equal(doc.getId(), 'svg_3'); + + assert.equal(doc.getNextId(), 'svg_4'); + assert.equal(doc.getId(), 'svg_4'); + // clean out svg document + cleanupSVG(svg); + }); + + test('Test getId() and getNextId() with prefix without nonce', async function ({ + page, + }) { + const prefix = 'Bar-'; + const doc = new draw.Drawing(svg, prefix); + + assert.equal(doc.getId(), prefix + '0'); + + assert.equal(doc.getNextId(), prefix + '1'); + assert.equal(doc.getId(), prefix + '1'); + + assert.equal(doc.getNextId(), prefix + '2'); + assert.equal(doc.getId(), prefix + '2'); + + assert.equal(doc.getNextId(), prefix + '3'); + assert.equal(doc.getId(), prefix + '3'); + + cleanupSVG(svg); + }); + + test('Test getId() and getNextId() with nonce', async function ({ page }) { + const prefix = 'svg_' + NONCE; + + const elem2 = document.createElementNS(NS.SVG, 'circle'); + elem2.id = prefix + '_2'; + svgN.append(elem2); + + const doc = new draw.Drawing(svgN); + + assert.equal(doc.getId(), prefix + '_0'); + + assert.equal(doc.getNextId(), prefix + '_1'); + assert.equal(doc.getId(), prefix + '_1'); + + assert.equal(doc.getNextId(), prefix + '_3'); + assert.equal(doc.getId(), prefix + '_3'); + + assert.equal(doc.getNextId(), prefix + '_4'); + assert.equal(doc.getId(), prefix + '_4'); + + cleanupSVG(svgN); + }); + + test('Test getId() and getNextId() with prefix with nonce', async function ({ + page, + }) { + const PREFIX = 'Bar-'; + const doc = new draw.Drawing(svgN, PREFIX); + + const prefix = PREFIX + NONCE + '_'; + assert.equal(doc.getId(), prefix + '0'); + + assert.equal(doc.getNextId(), prefix + '1'); + assert.equal(doc.getId(), prefix + '1'); + + assert.equal(doc.getNextId(), prefix + '2'); + assert.equal(doc.getId(), prefix + '2'); + + assert.equal(doc.getNextId(), prefix + '3'); + assert.equal(doc.getId(), prefix + '3'); + + cleanupSVG(svgN); + }); + + test('Test releaseId()', async function ({ page }) { + const doc = new draw.Drawing(svg); + + const firstId = doc.getNextId(); + /* const secondId = */ doc.getNextId(); + + const result = doc.releaseId(firstId); + assert.ok(result); + assert.equal(doc.getNextId(), firstId); + assert.equal(doc.getNextId(), 'svg_3'); + + assert.ok(!doc.releaseId('bad-id')); + assert.ok(doc.releaseId(firstId)); + assert.ok(!doc.releaseId(firstId)); + + cleanupSVG(svg); + }); + + test('Test getNumLayers', async function ({ page }) { + const drawing = new draw.Drawing(svg); + assert.equal( + typeof drawing.getNumLayers, + typeof function () { + /* empty fn */ + } + ); + assert.equal(drawing.getNumLayers(), 0); + + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 3); + + cleanupSVG(svg); + }); + + test('Test hasLayer', async function ({ page }) { + setupSVGWith3Layers(svg); + const drawing = new draw.Drawing(svg); + drawing.identifyLayers(); + + assert.equal( + typeof drawing.hasLayer, + typeof function () { + /* empty fn */ + } + ); + assert.ok(!drawing.hasLayer('invalid-layer')); + + assert.ok(drawing.hasLayer(LAYER3)); + assert.ok(drawing.hasLayer(LAYER2)); + assert.ok(drawing.hasLayer(LAYER1)); + + cleanupSVG(svg); + }); + + test('Test identifyLayers() with empty document', async function ({ page }) { + const drawing = new draw.Drawing(svg); + assert.equal(drawing.getCurrentLayer(), null); + // By default, an empty document gets an empty group created. + drawing.identifyLayers(); + + // Check that element now has one child node + assert.ok(drawing.getSvgElem().hasChildNodes()); + assert.equal(drawing.getSvgElem().childNodes.length, 1); + + // Check that all_layers are correctly set up. + assert.equal(drawing.getNumLayers(), 1); + const emptyLayer = drawing.all_layers[0]; + assert.ok(emptyLayer); + const layerGroup = emptyLayer.getGroup(); + assert.equal(layerGroup, drawing.getSvgElem().firstChild); + assert.equal(layerGroup.tagName, 'g'); + assert.equal(layerGroup.getAttribute('class'), LAYER_CLASS); + assert.ok(layerGroup.hasChildNodes()); + assert.equal(layerGroup.childNodes.length, 1); + const firstChild = layerGroup.childNodes.item(0); + assert.equal(firstChild.tagName, 'title'); + + cleanupSVG(svg); + }); + + test('Test identifyLayers() with some layers', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + + assert.equal(svg.childNodes.length, 3); + + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 3); + assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0)); + assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1)); + assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2)); + + assert.equal( + drawing.all_layers[0].getGroup().getAttribute('class'), + LAYER_CLASS + ); + assert.equal( + drawing.all_layers[1].getGroup().getAttribute('class'), + LAYER_CLASS + ); + assert.equal( + drawing.all_layers[2].getGroup().getAttribute('class'), + LAYER_CLASS + ); + + cleanupSVG(svg); + }); + + test('Test identifyLayers() with some layers and orphans', async function ({ + page, + }) { + setupSVGWith3Layers(svg); + + const orphan1 = document.createElementNS(NS.SVG, 'rect'); + const orphan2 = document.createElementNS(NS.SVG, 'rect'); + svg.append(orphan1, orphan2); + + assert.equal(svg.childNodes.length, 5); + + const drawing = new draw.Drawing(svg); + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 4); + assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0)); + assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1)); + assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2)); + assert.equal(drawing.all_layers[3].getGroup(), svg.childNodes.item(3)); + + assert.equal( + drawing.all_layers[0].getGroup().getAttribute('class'), + LAYER_CLASS + ); + assert.equal( + drawing.all_layers[1].getGroup().getAttribute('class'), + LAYER_CLASS + ); + assert.equal( + drawing.all_layers[2].getGroup().getAttribute('class'), + LAYER_CLASS + ); + assert.equal( + drawing.all_layers[3].getGroup().getAttribute('class'), + LAYER_CLASS + ); + + const layer4 = drawing.all_layers[3].getGroup(); + assert.equal(layer4.tagName, 'g'); + assert.equal(layer4.childNodes.length, 3); + assert.equal(layer4.childNodes.item(1), orphan1); + assert.equal(layer4.childNodes.item(2), orphan2); + + cleanupSVG(svg); + }); + + test('Test getLayerName()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 3); + assert.equal(drawing.getLayerName(0), LAYER1); + assert.equal(drawing.getLayerName(1), LAYER2); + assert.equal(drawing.getLayerName(2), LAYER3); + + cleanupSVG(svg); + }); + + test('Test getCurrentLayer()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.getCurrentLayer); + assert.equal( + typeof drawing.getCurrentLayer, + typeof function () { + /* empty fn */ + } + ); + assert.ok(drawing.getCurrentLayer()); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup()); + + cleanupSVG(svg); + }); + + test('Test setCurrentLayer() and getCurrentLayerName()', async function ({ + page, + }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setCurrentLayer); + assert.equal( + typeof drawing.setCurrentLayer, + typeof function () { + /* empty fn */ + } + ); + + drawing.setCurrentLayer(LAYER2); + assert.equal(drawing.getCurrentLayerName(), LAYER2); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup()); + + drawing.setCurrentLayer(LAYER3); + assert.equal(drawing.getCurrentLayerName(), LAYER3); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup()); + + cleanupSVG(svg); + }); + + test('Test setCurrentLayerName()', async function ({ page }) { + const mockHrService = { + changeElement() { + // empty + }, + }; + addOwnSpies(mockHrService); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setCurrentLayerName); + assert.equal( + typeof drawing.setCurrentLayerName, + typeof function () { + /* empty fn */ + } + ); + + const oldName = drawing.getCurrentLayerName(); + const newName = 'New Name'; + assert.ok(drawing.layer_map[oldName]); + assert.equal(drawing.layer_map[newName], undefined); // newName shouldn't exist. + const result = drawing.setCurrentLayerName(newName, mockHrService); + assert.equal(result, newName); + assert.equal(drawing.getCurrentLayerName(), newName); + // Was the map updated? + assert.equal(drawing.layer_map[oldName], undefined); + assert.equal(drawing.layer_map[newName], drawing.current_layer); + // Was mockHrService called? + assert.ok(mockHrService.changeElement.calledOnce); + assert.equal( + oldName, + mockHrService.changeElement.getCall(0).args[1]['#text'] + ); + assert.equal( + newName, + mockHrService.changeElement.getCall(0).args[0].textContent + ); + + cleanupSVG(svg); + }); + + test('Test createLayer()', async function ({ page }) { + const mockHrService = { + startBatchCommand() { + /* empty fn */ + }, + endBatchCommand() { + /* empty fn */ + }, + insertElement() { + /* empty fn */ + }, + }; + addOwnSpies(mockHrService); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.createLayer); + assert.equal( + typeof drawing.createLayer, + typeof function () { + /* empty fn */ + } + ); + + const NEW_LAYER_NAME = 'Layer A'; + const layerG = drawing.createLayer(NEW_LAYER_NAME, mockHrService); + assert.equal(drawing.getNumLayers(), 4); + assert.equal(layerG, drawing.getCurrentLayer()); + assert.equal(layerG.getAttribute('class'), LAYER_CLASS); + assert.equal(NEW_LAYER_NAME, drawing.getCurrentLayerName()); + assert.equal(NEW_LAYER_NAME, drawing.getLayerName(3)); + + assert.equal(layerG, mockHrService.insertElement.getCall(0).args[0]); + assert.ok(mockHrService.startBatchCommand.calledOnce); + assert.ok(mockHrService.endBatchCommand.calledOnce); + + cleanupSVG(svg); + }); + + test('Test mergeLayer()', async function ({ page }) { + const mockHrService = { + startBatchCommand() { + /* empty fn */ + }, + endBatchCommand() { + /* empty fn */ + }, + moveElement() { + /* empty fn */ + }, + removeElement() { + /* empty fn */ + }, + }; + addOwnSpies(mockHrService); + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + const elementCount = createSomeElementsInGroup(layers[2]) + 1; // +1 for title element + assert.equal(layers[1].childElementCount, 1); + assert.equal(layers[2].childElementCount, elementCount); + drawing.identifyLayers(); + assert.equal(drawing.getCurrentLayer(), layers[2]); + + assert.ok(drawing.mergeLayer); + assert.equal( + typeof drawing.mergeLayer, + typeof function () { + /* empty fn */ + } + ); + + drawing.mergeLayer(mockHrService); + + assert.equal(drawing.getNumLayers(), 2); + assert.equal(svg.childElementCount, 2); + assert.equal(drawing.getCurrentLayer(), layers[1]); + assert.equal(layers[1].childElementCount, elementCount); + + // check history record + assert.ok(mockHrService.startBatchCommand.calledOnce); + assert.ok(mockHrService.endBatchCommand.calledOnce); + assert.equal( + mockHrService.startBatchCommand.getCall(0).args[0], + 'Merge Layer' + ); + assert.equal(mockHrService.moveElement.callCount, elementCount - 1); // -1 because the title was not moved. + assert.equal(mockHrService.removeElement.callCount, 2); // remove group and title. + + cleanupSVG(svg); + }); + + test('Test mergeLayer() when no previous layer to merge', async function ({ + page, + }) { + const mockHrService = { + startBatchCommand() { + /* empty fn */ + }, + endBatchCommand() { + /* empty fn */ + }, + moveElement() { + /* empty fn */ + }, + removeElement() { + /* empty fn */ + }, + }; + addOwnSpies(mockHrService); + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + drawing.identifyLayers(); + drawing.setCurrentLayer(LAYER1); + assert.equal(drawing.getCurrentLayer(), layers[0]); + + drawing.mergeLayer(mockHrService); + + assert.equal(drawing.getNumLayers(), 3); + assert.equal(svg.childElementCount, 3); + assert.equal(drawing.getCurrentLayer(), layers[0]); + assert.equal(layers[0].childElementCount, 1); + assert.equal(layers[1].childElementCount, 1); + assert.equal(layers[2].childElementCount, 1); + + // check history record + assert.equal(mockHrService.startBatchCommand.callCount, 0); + assert.equal(mockHrService.endBatchCommand.callCount, 0); + assert.equal(mockHrService.moveElement.callCount, 0); + assert.equal(mockHrService.removeElement.callCount, 0); + + cleanupSVG(svg); + }); + + test('Test mergeAllLayers()', async function ({ page }) { + const mockHrService = { + startBatchCommand() { + /* empty fn */ + }, + endBatchCommand() { + /* empty fn */ + }, + moveElement() { + /* empty fn */ + }, + removeElement() { + /* empty fn */ + }, + }; + addOwnSpies(mockHrService); + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + const elementCount = createSomeElementsInGroup(layers[0]) + 1; // +1 for title element + createSomeElementsInGroup(layers[1]); + createSomeElementsInGroup(layers[2]); + assert.equal(layers[0].childElementCount, elementCount); + assert.equal(layers[1].childElementCount, elementCount); + assert.equal(layers[2].childElementCount, elementCount); + drawing.identifyLayers(); + + assert.ok(drawing.mergeAllLayers); + assert.equal( + typeof drawing.mergeAllLayers, + typeof function () { + /* empty fn */ + } + ); + + drawing.mergeAllLayers(mockHrService); + + assert.equal(drawing.getNumLayers(), 1); + assert.equal(svg.childElementCount, 1); + assert.equal(drawing.getCurrentLayer(), layers[0]); + assert.equal(layers[0].childElementCount, elementCount * 3 - 2); // -2 because two titles were deleted. + + // check history record + assert.equal(mockHrService.startBatchCommand.callCount, 3); // mergeAllLayers + 2 * mergeLayer + assert.equal(mockHrService.endBatchCommand.callCount, 3); + assert.equal( + mockHrService.startBatchCommand.getCall(0).args[0], + 'Merge all Layers' + ); + assert.equal( + mockHrService.startBatchCommand.getCall(1).args[0], + 'Merge Layer' + ); + assert.equal( + mockHrService.startBatchCommand.getCall(2).args[0], + 'Merge Layer' + ); + // moveElement count is times 3 instead of 2, because one layer's elements were moved twice. + // moveElement count is minus 3 because the three titles were not moved. + assert.equal(mockHrService.moveElement.callCount, elementCount * 3 - 3); + assert.equal(mockHrService.removeElement.callCount, 2 * 2); // remove group and title twice. + + cleanupSVG(svg); + }); + + test('Test cloneLayer()', async function ({ page }) { + const mockHrService = { + startBatchCommand() { + /* empty fn */ + }, + endBatchCommand() { + /* empty fn */ + }, + insertElement() { + /* empty fn */ + }, + }; + addOwnSpies(mockHrService); + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + const layer3 = layers[2]; + const elementCount = createSomeElementsInGroup(layer3) + 1; // +1 for title element + assert.equal(layer3.childElementCount, elementCount); + drawing.identifyLayers(); + + assert.ok(drawing.cloneLayer); + assert.equal( + typeof drawing.cloneLayer, + typeof function () { + /* empty fn */ + } + ); + + const clone = drawing.cloneLayer('clone', mockHrService); + + assert.equal(drawing.getNumLayers(), 4); + assert.equal(svg.childElementCount, 4); + assert.equal(drawing.getCurrentLayer(), clone); + assert.equal(clone.childElementCount, elementCount); + + // check history record + assert.ok(mockHrService.startBatchCommand.calledOnce); // mergeAllLayers + 2 * mergeLayer + assert.ok(mockHrService.endBatchCommand.calledOnce); + assert.equal( + mockHrService.startBatchCommand.getCall(0).args[0], + 'Duplicate Layer' + ); + assert.equal(mockHrService.insertElement.callCount, 1); + assert.equal(mockHrService.insertElement.getCall(0).args[0], clone); + + // check that path is cloned properly + assert.equal(clone.childNodes.length, elementCount); + const path = clone.childNodes[1]; + assert.equal(path.id, 'svg_1'); + assert.equal(path.getAttribute('d'), PATH_ATTR.d); + assert.equal(path.getAttribute('transform'), PATH_ATTR.transform); + assert.equal(path.getAttribute('fill'), PATH_ATTR.fill); + assert.equal(path.getAttribute('stroke'), PATH_ATTR.stroke); + assert.equal(path.getAttribute('stroke-width'), PATH_ATTR['stroke-width']); + + // check that g is cloned properly + const g = clone.childNodes[4]; + assert.equal(g.childNodes.length, 1); + assert.equal(g.id, 'svg_4'); + + cleanupSVG(svg); + }); + + test('Test getLayerVisibility()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.getLayerVisibility); + assert.equal( + typeof drawing.getLayerVisibility, + typeof function () { + /* empty fn */ + } + ); + assert.ok(drawing.getLayerVisibility(LAYER1)); + assert.ok(drawing.getLayerVisibility(LAYER2)); + assert.ok(drawing.getLayerVisibility(LAYER3)); + + cleanupSVG(svg); + }); + + test('Test setLayerVisibility()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setLayerVisibility); + assert.equal( + typeof drawing.setLayerVisibility, + typeof function () { + /* empty fn */ + } + ); + + drawing.setLayerVisibility(LAYER3, false); + drawing.setLayerVisibility(LAYER2, true); + drawing.setLayerVisibility(LAYER1, false); + + assert.ok(!drawing.getLayerVisibility(LAYER1)); + assert.ok(drawing.getLayerVisibility(LAYER2)); + assert.ok(!drawing.getLayerVisibility(LAYER3)); + + drawing.setLayerVisibility(LAYER3, 'test-string'); + assert.ok(!drawing.getLayerVisibility(LAYER3)); + + cleanupSVG(svg); + }); + + test('Test getLayerOpacity()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.getLayerOpacity); + assert.equal( + typeof drawing.getLayerOpacity, + typeof function () { + /* empty fn */ + } + ); + assert.strictEqual(drawing.getLayerOpacity(LAYER1), 1.0); + assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0); + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0); + + cleanupSVG(svg); + }); + + test('Test setLayerOpacity()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setLayerOpacity); + assert.equal( + typeof drawing.setLayerOpacity, + typeof function () { + /* empty fn */ + } + ); + + drawing.setLayerOpacity(LAYER1, 0.4); + drawing.setLayerOpacity(LAYER2, 'invalid-string'); + drawing.setLayerOpacity(LAYER3, -1.4); + + assert.strictEqual(drawing.getLayerOpacity(LAYER1), 0.4); + assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0); + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0); + + drawing.setLayerOpacity(LAYER3, 100); + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0); + + cleanupSVG(svg); + }); + + test('Test deleteCurrentLayer()', async function ({ page }) { + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + drawing.setCurrentLayer(LAYER2); + + const curLayer = drawing.getCurrentLayer(); + assert.equal(curLayer, drawing.all_layers[1].getGroup()); + const deletedLayer = drawing.deleteCurrentLayer(); + + assert.equal(curLayer, deletedLayer); + assert.equal(drawing.getNumLayers(), 2); + assert.equal(LAYER1, drawing.all_layers[0].getName()); + assert.equal(LAYER3, drawing.all_layers[1].getName()); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup()); + }); + + test('Test svgedit.draw.randomizeIds()', async function ({ page }) { + // Confirm in LET_DOCUMENT_DECIDE mode that the document decides + // if there is a nonce. + let drawing = new draw.Drawing(svgN.cloneNode(true)); + assert.ok(drawing.getNonce()); + + drawing = new draw.Drawing(svg.cloneNode(true)); + assert.ok(!drawing.getNonce()); + + // Confirm that a nonce is set once we're in ALWAYS_RANDOMIZE mode. + draw.randomizeIds(true, drawing); + assert.ok(drawing.getNonce()); + + // Confirm new drawings in ALWAYS_RANDOMIZE mode have a nonce. + drawing = new draw.Drawing(svg.cloneNode(true)); + assert.ok(drawing.getNonce()); + + drawing.clearNonce(); + assert.ok(!drawing.getNonce()); + + // Confirm new drawings in NEVER_RANDOMIZE mode do not have a nonce + // but that their se:nonce attribute is left alone. + draw.randomizeIds(false, drawing); + assert.ok(!drawing.getNonce()); + assert.ok(drawing.getSvgElem().getAttributeNS(NS.SE, 'nonce')); + + drawing = new draw.Drawing(svg.cloneNode(true)); + assert.ok(!drawing.getNonce()); + + drawing = new draw.Drawing(svgN.cloneNode(true)); + assert.ok(!drawing.getNonce()); + }); +}); diff --git a/golden/5-svgedit-unit/history.spec.ts b/golden/5-svgedit-unit/history.spec.ts new file mode 100644 index 0000000..fbabcf9 --- /dev/null +++ b/golden/5-svgedit-unit/history.spec.ts @@ -0,0 +1,701 @@ +import { test, expect } from '@playwright/test'; + +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as utilities from '../../../packages/svgcanvas/core/utilities.js'; +import * as history from '../../../packages/svgcanvas/core/history.js'; + +test.describe('history', function () { + // TODO(codedread): Write tests for handling history events. + + utilities.mock({ + getHref() { + return '#foo'; + }, + setHref() { + /* empty fn */ + }, + getRotationAngle() { + return 0; + }, + }); + + // const svg = document.createElementNS(NS.SVG, 'svg'); + let undoMgr = null; + + class MockCommand extends history.Command { + constructor(optText) { + super(); + this.text = optText; + } + + apply(handler) { + super.apply(handler, () => { + /* empty fn */ + }); + } + + unapply(handler) { + super.unapply(handler, () => { + /* empty fn */ + }); + } + + elements() { + return []; + } + } + + /* + class MockHistoryEventHandler { + handleHistoryEvent (eventType, command) {} + } + */ + + /** + * Set up tests (with undo manager). + * @returns {void} + */ + test.beforeEach(async function ({ page }) { + undoMgr = new history.UndoManager(); + + document.body.textContent = ''; + this.divparent = document.createElement('div'); + this.divparent.id = 'divparent'; + this.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; + } + + this.divparent.append(this.div1, this.div2, this.div3); + + this.div4.style.visibility = 'hidden'; + this.div4.append(this.div5); + + document.body.append(this.divparent, this.div); + }); + /** + * Tear down tests, destroying undo manager. + * @returns {void} + */ + test.afterEach(async ({ page }) => { + undoMgr = null; + }); + + test('Test svgedit.history package', async function ({ page }) { + assert.ok(history); + assert.ok(history.MoveElementCommand); + assert.ok(history.InsertElementCommand); + assert.ok(history.ChangeElementCommand); + assert.ok(history.RemoveElementCommand); + assert.ok(history.BatchCommand); + assert.ok(history.UndoManager); + assert.equal( + typeof history.MoveElementCommand, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof history.InsertElementCommand, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof history.ChangeElementCommand, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof history.RemoveElementCommand, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof history.BatchCommand, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof history.UndoManager, + typeof function () { + /* empty fn */ + } + ); + }); + + test('Test UndoManager methods', async function ({ page }) { + assert.ok(undoMgr); + assert.ok(undoMgr.addCommandToHistory); + assert.ok(undoMgr.getUndoStackSize); + assert.ok(undoMgr.getRedoStackSize); + assert.ok(undoMgr.resetUndoStack); + assert.ok(undoMgr.getNextUndoCommandText); + assert.ok(undoMgr.getNextRedoCommandText); + + assert.equal(typeof undoMgr, typeof {}); + assert.equal( + typeof undoMgr.addCommandToHistory, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof undoMgr.getUndoStackSize, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof undoMgr.getRedoStackSize, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof undoMgr.resetUndoStack, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof undoMgr.getNextUndoCommandText, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof undoMgr.getNextRedoCommandText, + typeof function () { + /* empty fn */ + } + ); + }); + + test('Test UndoManager.addCommandToHistory() function', async function ({ + page, + }) { + assert.equal(undoMgr.getUndoStackSize(), 0); + undoMgr.addCommandToHistory(new MockCommand()); + assert.equal(undoMgr.getUndoStackSize(), 1); + undoMgr.addCommandToHistory(new MockCommand()); + assert.equal(undoMgr.getUndoStackSize(), 2); + }); + + test('Test UndoManager.getUndoStackSize() and getRedoStackSize() functions', async function ({ + page, + }) { + undoMgr.addCommandToHistory(new MockCommand()); + undoMgr.addCommandToHistory(new MockCommand()); + undoMgr.addCommandToHistory(new MockCommand()); + + assert.equal(undoMgr.getUndoStackSize(), 3); + assert.equal(undoMgr.getRedoStackSize(), 0); + + undoMgr.undo(); + assert.equal(undoMgr.getUndoStackSize(), 2); + assert.equal(undoMgr.getRedoStackSize(), 1); + + undoMgr.undo(); + assert.equal(undoMgr.getUndoStackSize(), 1); + assert.equal(undoMgr.getRedoStackSize(), 2); + + undoMgr.undo(); + assert.equal(undoMgr.getUndoStackSize(), 0); + assert.equal(undoMgr.getRedoStackSize(), 3); + + undoMgr.undo(); + assert.equal(undoMgr.getUndoStackSize(), 0); + assert.equal(undoMgr.getRedoStackSize(), 3); + + undoMgr.redo(); + assert.equal(undoMgr.getUndoStackSize(), 1); + assert.equal(undoMgr.getRedoStackSize(), 2); + + undoMgr.redo(); + assert.equal(undoMgr.getUndoStackSize(), 2); + assert.equal(undoMgr.getRedoStackSize(), 1); + + undoMgr.redo(); + assert.equal(undoMgr.getUndoStackSize(), 3); + assert.equal(undoMgr.getRedoStackSize(), 0); + + undoMgr.redo(); + assert.equal(undoMgr.getUndoStackSize(), 3); + assert.equal(undoMgr.getRedoStackSize(), 0); + }); + + test('Test UndoManager.resetUndoStackSize() function', async function ({ + page, + }) { + undoMgr.addCommandToHistory(new MockCommand()); + undoMgr.addCommandToHistory(new MockCommand()); + undoMgr.addCommandToHistory(new MockCommand()); + undoMgr.undo(); + + assert.equal(undoMgr.getUndoStackSize(), 2); + assert.equal(undoMgr.getRedoStackSize(), 1); + + undoMgr.resetUndoStack(); + + assert.equal(undoMgr.getUndoStackSize(), 0); + assert.equal(undoMgr.getRedoStackSize(), 0); + }); + + test('Test UndoManager.getNextUndoCommandText() function', async function ({ + page, + }) { + assert.equal(undoMgr.getNextUndoCommandText(), ''); + + undoMgr.addCommandToHistory(new MockCommand('First')); + undoMgr.addCommandToHistory(new MockCommand('Second')); + undoMgr.addCommandToHistory(new MockCommand('Third')); + + assert.equal(undoMgr.getNextUndoCommandText(), 'Third'); + + undoMgr.undo(); + assert.equal(undoMgr.getNextUndoCommandText(), 'Second'); + + undoMgr.undo(); + assert.equal(undoMgr.getNextUndoCommandText(), 'First'); + + undoMgr.undo(); + assert.equal(undoMgr.getNextUndoCommandText(), ''); + + undoMgr.redo(); + assert.equal(undoMgr.getNextUndoCommandText(), 'First'); + + undoMgr.redo(); + assert.equal(undoMgr.getNextUndoCommandText(), 'Second'); + + undoMgr.redo(); + assert.equal(undoMgr.getNextUndoCommandText(), 'Third'); + + undoMgr.redo(); + assert.equal(undoMgr.getNextUndoCommandText(), 'Third'); + }); + + test('Test UndoManager.getNextRedoCommandText() function', async function ({ + page, + }) { + assert.equal(undoMgr.getNextRedoCommandText(), ''); + + undoMgr.addCommandToHistory(new MockCommand('First')); + undoMgr.addCommandToHistory(new MockCommand('Second')); + undoMgr.addCommandToHistory(new MockCommand('Third')); + + assert.equal(undoMgr.getNextRedoCommandText(), ''); + + undoMgr.undo(); + assert.equal(undoMgr.getNextRedoCommandText(), 'Third'); + + undoMgr.undo(); + assert.equal(undoMgr.getNextRedoCommandText(), 'Second'); + + undoMgr.undo(); + assert.equal(undoMgr.getNextRedoCommandText(), 'First'); + + undoMgr.redo(); + assert.equal(undoMgr.getNextRedoCommandText(), 'Second'); + + undoMgr.redo(); + assert.equal(undoMgr.getNextRedoCommandText(), 'Third'); + + undoMgr.redo(); + assert.equal(undoMgr.getNextRedoCommandText(), ''); + }); + + test('Test UndoManager.undo() and redo() functions', async function ({ + page, + }) { + let lastCalled = null; + const cmd1 = new MockCommand(); + const cmd2 = new MockCommand(); + const cmd3 = new MockCommand(); + cmd1.apply = function () { + lastCalled = 'cmd1.apply'; + }; + cmd2.apply = function () { + lastCalled = 'cmd2.apply'; + }; + cmd3.apply = function () { + lastCalled = 'cmd3.apply'; + }; + cmd1.unapply = function () { + lastCalled = 'cmd1.unapply'; + }; + cmd2.unapply = function () { + lastCalled = 'cmd2.unapply'; + }; + cmd3.unapply = function () { + lastCalled = 'cmd3.unapply'; + }; + + undoMgr.addCommandToHistory(cmd1); + undoMgr.addCommandToHistory(cmd2); + undoMgr.addCommandToHistory(cmd3); + + assert.ok(!lastCalled); + + undoMgr.undo(); + assert.equal(lastCalled, 'cmd3.unapply'); + + undoMgr.redo(); + assert.equal(lastCalled, 'cmd3.apply'); + + undoMgr.undo(); + undoMgr.undo(); + assert.equal(lastCalled, 'cmd2.unapply'); + + undoMgr.undo(); + assert.equal(lastCalled, 'cmd1.unapply'); + lastCalled = null; + + undoMgr.undo(); + assert.ok(!lastCalled); + + undoMgr.redo(); + assert.equal(lastCalled, 'cmd1.apply'); + + undoMgr.redo(); + assert.equal(lastCalled, 'cmd2.apply'); + + undoMgr.redo(); + assert.equal(lastCalled, 'cmd3.apply'); + lastCalled = null; + + undoMgr.redo(); + assert.ok(!lastCalled); + }); + + test('Test MoveElementCommand', async function ({ page }) { + let move = new history.MoveElementCommand( + this.div3, + this.div1, + this.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); + + move.apply(); + assert.equal(this.divparent.firstElementChild, this.div1); + assert.equal( + this.divparent.firstElementChild.nextElementSibling, + this.div2 + ); + assert.equal(this.divparent.lastElementChild, this.div3); + + move = new history.MoveElementCommand(this.div1, null, this.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); + + move.apply(); + assert.equal(this.divparent.firstElementChild, this.div1); + assert.equal( + this.divparent.firstElementChild.nextElementSibling, + this.div2 + ); + assert.equal(this.divparent.lastElementChild, this.div3); + + move = new history.MoveElementCommand(this.div2, this.div5, this.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); + + 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); + }); + + test('Test InsertElementCommand', async function ({ page }) { + let insert = new history.InsertElementCommand(this.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); + + 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); + + insert = new history.InsertElementCommand(this.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); + + 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); + }); + + test('Test RemoveElementCommand', async function ({ page }) { + const div6 = document.createElement('div'); + div6.id = 'div6'; + + let remove = new history.RemoveElementCommand(div6, null, this.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); + + 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); + + remove = new history.RemoveElementCommand(div6, this.div2, this.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); + + 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); + }); + + test('Test ChangeElementCommand', async function ({ page }) { + this.div1.setAttribute('title', 'new title'); + let change = new history.ChangeElementCommand(this.div1, { + title: 'old title', + class: 'foo', + }); + assert.ok(change.unapply); + assert.ok(change.apply); + assert.equal( + typeof change.unapply, + typeof function () { + /* empty fn */ + } + ); + 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'); + + change.apply(); + assert.equal(this.div1.getAttribute('title'), 'new title'); + assert.ok(!this.div1.getAttribute('class')); + + this.div1.textContent = 'inner text'; + change = new history.ChangeElementCommand(this.div1, { '#text': null }); + + change.unapply(); + assert.ok(!this.div1.textContent); + + change.apply(); + assert.equal(this.div1.textContent, 'inner text'); + + this.div1.textContent = ''; + change = new history.ChangeElementCommand(this.div1, { + '#text': 'old text', + }); + + change.unapply(); + assert.equal(this.div1.textContent, 'old text'); + + change.apply(); + assert.ok(!this.div1.textContent); + + // TODO(codedread): Refactor this #href stuff in history.js and svgcanvas.js + const rect = document.createElementNS(NS.SVG, 'rect'); + let justCalled = null; + let gethrefvalue = null; + let sethrefvalue = null; + utilities.mock({ + getHref(elem) { + assert.equal(elem, rect); + justCalled = 'getHref'; + return gethrefvalue; + }, + setHref(elem, val) { + assert.equal(elem, rect); + assert.equal(val, sethrefvalue); + justCalled = 'setHref'; + }, + getRotationAngle() { + return 0; + }, + }); + + gethrefvalue = '#newhref'; + change = new history.ChangeElementCommand(rect, { '#href': '#oldhref' }); + assert.equal(justCalled, 'getHref'); + + justCalled = null; + sethrefvalue = '#oldhref'; + change.unapply(); + assert.equal(justCalled, 'setHref'); + + justCalled = null; + sethrefvalue = '#newhref'; + change.apply(); + assert.equal(justCalled, 'setHref'); + + const line = document.createElementNS(NS.SVG, 'line'); + line.setAttribute('class', 'newClass'); + change = new history.ChangeElementCommand(line, { class: 'oldClass' }); + + assert.ok(change.unapply); + assert.ok(change.apply); + assert.equal( + typeof change.unapply, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof change.apply, + typeof function () { + /* empty fn */ + } + ); + + change.unapply(); + assert.equal(line.getAttribute('class'), 'oldClass'); + + change.apply(); + assert.equal(line.getAttribute('class'), 'newClass'); + }); + + test('Test BatchCommand', async function ({ page }) { + let concatResult = ''; + MockCommand.prototype.apply = function () { + concatResult += this.text; + }; + + const batch = new history.BatchCommand(); + assert.ok(batch.unapply); + assert.ok(batch.apply); + assert.ok(batch.addSubCommand); + assert.ok(batch.isEmpty); + assert.equal(typeof batch.unapply, 'function'); + assert.equal(typeof batch.apply, 'function'); + assert.equal(typeof batch.addSubCommand, 'function'); + assert.equal(typeof batch.isEmpty, 'function'); + + assert.ok(batch.isEmpty()); + + batch.addSubCommand(new MockCommand('a')); + assert.ok(!batch.isEmpty()); + batch.addSubCommand(new MockCommand('b')); + batch.addSubCommand(new MockCommand('c')); + + assert.ok(!concatResult); + batch.apply(); + assert.equal(concatResult, 'abc'); + + MockCommand.prototype.apply = function () { + /* empty fn */ + }; + MockCommand.prototype.unapply = function () { + concatResult += this.text; + }; + concatResult = ''; + assert.ok(!concatResult); + batch.unapply(); + assert.equal(concatResult, 'cba'); + + MockCommand.prototype.unapply = function () { + /* empty fn */ + }; + }); +}); diff --git a/golden/5-svgedit-unit/math.spec.ts b/golden/5-svgedit-unit/math.spec.ts new file mode 100644 index 0000000..dd6162f --- /dev/null +++ b/golden/5-svgedit-unit/math.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@playwright/test'; + +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as math from '../../../packages/svgcanvas/core/math.js'; + +test.describe('math', function () { + const svg = document.createElementNS(NS.SVG, 'svg'); + + test('Test svgedit.math package', async function ({ page }) { + assert.ok(math); + assert.ok(math.transformPoint); + assert.ok(math.isIdentity); + assert.ok(math.matrixMultiply); + assert.equal( + typeof math.transformPoint, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof math.isIdentity, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof math.matrixMultiply, + typeof function () { + /* empty fn */ + } + ); + }); + + test('Test svgedit.math.transformPoint() function', async function ({ + page, + }) { + const { transformPoint } = math; + + const m = svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 0; + m.f = 0; + let pt = transformPoint(100, 200, m); + assert.equal(pt.x, 100); + assert.equal(pt.y, 200); + + m.e = 300; + m.f = 400; + pt = transformPoint(100, 200, m); + assert.equal(pt.x, 400); + assert.equal(pt.y, 600); + + m.a = 0.5; + m.b = 0.75; + m.c = 1.25; + m.d = 2; + pt = transformPoint(100, 200, m); + assert.equal(pt.x, 100 * m.a + 200 * m.c + m.e); + assert.equal(pt.y, 100 * m.b + 200 * m.d + m.f); + }); + + test('Test svgedit.math.isIdentity() function', async function ({ page }) { + assert.ok(math.isIdentity(svg.createSVGMatrix())); + + const m = svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 0; + m.f = 0; + assert.ok(math.isIdentity(m)); + }); + + test('Test svgedit.math.matrixMultiply() function', async function ({ + page, + }) { + const mult = math.matrixMultiply; + const { isIdentity } = math; + + // translate there and back + const tr1 = svg.createSVGMatrix().translate(100, 50); + const tr2 = svg.createSVGMatrix().translate(-90, 0); + const tr3 = svg.createSVGMatrix().translate(-10, -50); + let I = mult(tr1, tr2, tr3); + assert.ok( + isIdentity(I), + 'Expected identity matrix when translating there and back' + ); + + // rotate there and back + // TODO: currently Mozilla fails this when rotating back at -50 and then -40 degrees + // (b and c are *almost* zero, but not zero) + const rotThere = svg.createSVGMatrix().rotate(90); + const rotBack = svg.createSVGMatrix().rotate(-90); // TODO: set this to -50 + const rotBackMore = svg.createSVGMatrix().rotate(0); // TODO: set this to -40 + I = mult(rotThere, rotBack, rotBackMore); + assert.ok( + isIdentity(I), + 'Expected identity matrix when rotating there and back' + ); + + // scale up and down + const scaleUp = svg.createSVGMatrix().scale(4); + const scaleDown = svg.createSVGMatrix().scaleNonUniform(0.25, 1); + const scaleDownMore = svg.createSVGMatrix().scaleNonUniform(1, 0.25); + I = mult(scaleUp, scaleDown, scaleDownMore); + assert.ok( + isIdentity(I), + 'Expected identity matrix when scaling up and down' + ); + + // test multiplication with its inverse + I = mult(rotThere, rotThere.inverse()); + assert.ok( + isIdentity(I), + 'Expected identity matrix when multiplying a matrix by its inverse' + ); + I = mult(rotThere.inverse(), rotThere); + assert.ok( + isIdentity(I), + 'Expected identity matrix when multiplying a matrix by its inverse' + ); + }); + + test('Test svgedit.math.transformBox() function', async function ({ page }) { + const { transformBox } = math; + + const m = svg.createSVGMatrix(); + m.a = 1; + m.b = 0; + m.c = 0; + m.d = 1; + m.e = 0; + m.f = 0; + + const r = transformBox(10, 10, 200, 300, m); + assert.equal(r.tl.x, 10); + assert.equal(r.tl.y, 10); + assert.equal(r.tr.x, 210); + assert.equal(r.tr.y, 10); + assert.equal(r.bl.x, 10); + assert.equal(r.bl.y, 310); + assert.equal(r.br.x, 210); + assert.equal(r.br.y, 310); + assert.equal(r.aabox.x, 10); + assert.equal(r.aabox.y, 10); + assert.equal(r.aabox.width, 200); + assert.equal(r.aabox.height, 300); + }); +}); diff --git a/golden/5-svgedit-unit/path.spec.ts b/golden/5-svgedit-unit/path.spec.ts new file mode 100644 index 0000000..8b91e0f --- /dev/null +++ b/golden/5-svgedit-unit/path.spec.ts @@ -0,0 +1,201 @@ +import { test, expect } from '@playwright/test'; + +/* globals SVGPathSeg */ +import 'pathseg'; +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as utilities from '../../../packages/svgcanvas/core/utilities.js'; +import * as pathModule from '../../../packages/svgcanvas/core/path.js'; +import { Path, Segment } from '../../../packages/svgcanvas/core/path-method.js'; +import { init as unitsInit } from '../../../packages/svgcanvas/core/units.js'; + +test.describe('path', function () { + /** + * @typedef {GenericArray} EditorContexts + * @property {module:path.EditorContext} 0 + * @property {module:path.EditorContext} 1 + */ + + /** + * @param {SVGSVGElement} [svg] + * @returns {EditorContexts} + */ + function getMockContexts(svg) { + svg = svg || document.createElementNS(NS.SVG, 'svg'); + const selectorParentGroup = document.createElementNS(NS.SVG, 'g'); + selectorParentGroup.setAttribute('id', 'selectorParentGroup'); + svg.append(selectorParentGroup); + return [ + /** + * @implements {module:path.EditorContext} + */ + { + getSvgRoot() { + return svg; + }, + getZoom() { + return 1; + }, + }, + /** + * @implements {module:utilities.EditorContext} + */ + { + getDOMDocument() { + return svg; + }, + getDOMContainer() { + return svg; + }, + getSvgRoot() { + return svg; + }, + }, + ]; + } + + test('Test svgedit.path.replacePathSeg', async function ({ page }) { + const path = document.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'M0,0 L10,11 L20,21Z'); + + const [mockPathContext, mockUtilitiesContext] = getMockContexts(); + pathModule.init(mockPathContext); + utilities.init(mockUtilitiesContext); + new Path(path); // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L'); + assert.equal(path.pathSegList.getItem(1).x, 10); + assert.equal(path.pathSegList.getItem(1).y, 11); + + pathModule.replacePathSeg(SVGPathSeg.PATHSEG_LINETO_REL, 1, [30, 31], path); + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'l'); + assert.equal(path.pathSegList.getItem(1).x, 30); + assert.equal(path.pathSegList.getItem(1).y, 31); + }); + + test('Test svgedit.path.Segment.setType simple', async function ({ page }) { + const path = document.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'M0,0 L10,11 L20,21Z'); + + const [mockPathContext, mockUtilitiesContext] = getMockContexts(); + pathModule.init(mockPathContext); + utilities.init(mockUtilitiesContext); + new Path(path); // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L'); + assert.equal(path.pathSegList.getItem(1).x, 10); + assert.equal(path.pathSegList.getItem(1).y, 11); + + const segment = new Segment(1, path.pathSegList.getItem(1)); + segment.setType(SVGPathSeg.PATHSEG_LINETO_REL, [30, 31]); + assert.equal(segment.item.pathSegTypeAsLetter, 'l'); + assert.equal(segment.item.x, 30); + assert.equal(segment.item.y, 31); + + // Also verify that the actual path changed. + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'l'); + assert.equal(path.pathSegList.getItem(1).x, 30); + assert.equal(path.pathSegList.getItem(1).y, 31); + }); + + test('Test svgedit.path.Segment.setType with control points', async function ({ + page, + }) { + // Setup the dom for a mock control group. + const svg = document.createElementNS(NS.SVG, 'svg'); + const path = document.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'M0,0 C11,12 13,14 15,16 Z'); + svg.append(path); + + const [mockPathContext, mockUtilitiesContext] = getMockContexts(svg); + pathModule.init(mockPathContext); + utilities.init(mockUtilitiesContext); + const segment = new Segment(1, path.pathSegList.getItem(1)); + segment.path = new Path(path); + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C'); + assert.equal(path.pathSegList.getItem(1).x1, 11); + assert.equal(path.pathSegList.getItem(1).y1, 12); + assert.equal(path.pathSegList.getItem(1).x2, 13); + assert.equal(path.pathSegList.getItem(1).y2, 14); + assert.equal(path.pathSegList.getItem(1).x, 15); + assert.equal(path.pathSegList.getItem(1).y, 16); + + segment.setType( + SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, + [30, 31, 32, 33, 34, 35] + ); + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'c'); + assert.equal(path.pathSegList.getItem(1).x1, 32); + assert.equal(path.pathSegList.getItem(1).y1, 33); + assert.equal(path.pathSegList.getItem(1).x2, 34); + assert.equal(path.pathSegList.getItem(1).y2, 35); + assert.equal(path.pathSegList.getItem(1).x, 30); + assert.equal(path.pathSegList.getItem(1).y, 31); + }); + + test('Test svgedit.path.Segment.move', async function ({ page }) { + const path = document.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'M0,0 L10,11 L20,21Z'); + + const [mockPathContext, mockUtilitiesContext] = getMockContexts(); + pathModule.init(mockPathContext); + utilities.init(mockUtilitiesContext); + new Path(path); // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L'); + assert.equal(path.pathSegList.getItem(1).x, 10); + assert.equal(path.pathSegList.getItem(1).y, 11); + + const segment = new Segment(1, path.pathSegList.getItem(1)); + segment.move(-3, 4); + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L'); + assert.equal(path.pathSegList.getItem(1).x, 7); + assert.equal(path.pathSegList.getItem(1).y, 15); + }); + + test('Test svgedit.path.Segment.moveCtrl', async function ({ page }) { + const path = document.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'M0,0 C11,12 13,14 15,16 Z'); + + const [mockPathContext, mockUtilitiesContext] = getMockContexts(); + pathModule.init(mockPathContext); + utilities.init(mockUtilitiesContext); + new Path(path); // eslint-disable-line no-new + + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C'); + assert.equal(path.pathSegList.getItem(1).x1, 11); + assert.equal(path.pathSegList.getItem(1).y1, 12); + assert.equal(path.pathSegList.getItem(1).x2, 13); + assert.equal(path.pathSegList.getItem(1).y2, 14); + assert.equal(path.pathSegList.getItem(1).x, 15); + assert.equal(path.pathSegList.getItem(1).y, 16); + + const segment = new Segment(1, path.pathSegList.getItem(1)); + segment.moveCtrl(1, 100, -200); + assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C'); + assert.equal(path.pathSegList.getItem(1).x1, 111); + assert.equal(path.pathSegList.getItem(1).y1, -188); + assert.equal(path.pathSegList.getItem(1).x2, 13); + assert.equal(path.pathSegList.getItem(1).y2, 14); + assert.equal(path.pathSegList.getItem(1).x, 15); + assert.equal(path.pathSegList.getItem(1).y, 16); + }); + + test('Test svgedit.path.convertPath', async function ({ page }) { + unitsInit({ + getRoundDigits() { + return 5; + }, + }); + + const path = document.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'M40,55h20v20'); + + const abs = pathModule.convertPath(path); + assert.equal(abs, 'M40,55L60,55L60,75'); + + const rel = pathModule.convertPath(path, true); + assert.equal(rel, 'm40,55l20,0l0,20'); + }); +}); diff --git a/golden/5-svgedit-unit/recalculate.spec.ts b/golden/5-svgedit-unit/recalculate.spec.ts new file mode 100644 index 0000000..de59ea8 --- /dev/null +++ b/golden/5-svgedit-unit/recalculate.spec.ts @@ -0,0 +1,206 @@ +import { test, expect } from '@playwright/test'; + +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as utilities from '../../../packages/svgcanvas/core/utilities.js'; +import * as coords from '../../../packages/svgcanvas/core/coords.js'; +import * as recalculate from '../../../packages/svgcanvas/core/recalculate.js'; + +test.describe('recalculate', function () { + const root = document.createElement('div'); + root.id = 'root'; + root.style.visibility = 'hidden'; + + const svgroot = document.createElementNS(NS.SVG, 'svg'); + svgroot.id = 'svgroot'; + root.append(svgroot); + const svg = document.createElementNS(NS.SVG, 'svg'); + svgroot.append(svg); + + const dataStorage = { + _storage: new WeakMap(), + put: function (element, key, obj) { + if (!this._storage.has(element)) { + this._storage.set(element, new Map()); + } + this._storage.get(element).set(key, obj); + }, + get: function (element, key) { + return this._storage.get(element).get(key); + }, + has: function (element, key) { + return this._storage.has(element) && this._storage.get(element).has(key); + }, + remove: function (element, key) { + const ret = this._storage.get(element).delete(key); + if (!this._storage.get(element).size === 0) { + this._storage.delete(element); + } + return ret; + }, + }; + + let elemId = 1; + + /** + * Initilize modules to set up the tests. + * @returns {void} + */ + function setUp() { + utilities.init( + /** + * @implements {module:utilities.EditorContext} + */ + { + getSvgRoot() { + return svg; + }, + getDOMDocument() { + return null; + }, + getDOMContainer() { + return null; + }, + getDataStorage() { + return dataStorage; + }, + } + ); + coords.init( + /** + * @implements {module:coords.EditorContext} + */ + { + getGridSnapping() { + return false; + }, + getDrawing() { + return { + getNextId() { + return String(elemId++); + }, + }; + }, + getDataStorage() { + return dataStorage; + }, + } + ); + recalculate.init( + /** + * @implements {module:recalculate.EditorContext} + */ + { + getSvgRoot() { + return svg; + }, + getStartTransform() { + return ''; + }, + setStartTransform() { + /* empty fn */ + }, + getDataStorage() { + return dataStorage; + }, + } + ); + } + + let elem; + + /** + * Initialize for tests and set up `rect` element. + * @returns {void} + */ + function setUpRect() { + setUp(); + elem = document.createElementNS(NS.SVG, 'rect'); + elem.setAttribute('x', '200'); + elem.setAttribute('y', '150'); + elem.setAttribute('width', '250'); + elem.setAttribute('height', '120'); + svg.append(elem); + } + + /** + * Initialize for tests and set up `text` element with `tspan` child. + * @returns {void} + */ + function setUpTextWithTspan() { + setUp(); + elem = document.createElementNS(NS.SVG, 'text'); + elem.setAttribute('x', '200'); + elem.setAttribute('y', '150'); + + const tspan = document.createElementNS(NS.SVG, 'tspan'); + tspan.setAttribute('x', '200'); + tspan.setAttribute('y', '150'); + + const theText = 'Foo bar'; + tspan.append(theText); + elem.append(tspan); + svg.append(elem); + } + + /** + * Tear down the tests (empty the svg element). + * @returns {void} + */ + test.afterEach(async ({ page }) => { + while (svg.hasChildNodes()) { + svg.firstChild.remove(); + } + }); + + test('Test recalculateDimensions() on rect with identity matrix', async function ({ + page, + }) { + setUpRect(); + elem.setAttribute('transform', 'matrix(1,0,0,1,0,0)'); + + recalculate.recalculateDimensions(elem); + + // Ensure that the identity matrix is swallowed and the element has no + // transform on it. + assert.equal(elem.hasAttribute('transform'), false); + }); + + test('Test recalculateDimensions() on rect with simple translate', async function ({ + page, + }) { + setUpRect(); + elem.setAttribute('transform', 'translate(100,50)'); + + recalculate.recalculateDimensions(elem); + + assert.equal(elem.hasAttribute('transform'), false); + assert.equal(elem.getAttribute('x'), '300'); + assert.equal(elem.getAttribute('y'), '200'); + assert.equal(elem.getAttribute('width'), '250'); + assert.equal(elem.getAttribute('height'), '120'); + }); + + test('Test recalculateDimensions() on text w/tspan with simple translate', async function ({ + page, + }) { + setUpTextWithTspan(); + elem.setAttribute('transform', 'translate(100,50)'); + + recalculate.recalculateDimensions(elem); + + // Ensure that the identity matrix is swallowed and the element has no + // transform on it. + assert.equal(elem.hasAttribute('transform'), false); + assert.equal(elem.getAttribute('x'), '300'); + assert.equal(elem.getAttribute('y'), '200'); + + const tspan = elem.firstElementChild; + assert.equal(tspan.getAttribute('x'), '300'); + assert.equal(tspan.getAttribute('y'), '200'); + }); + + // TODO: Since recalculateDimensions() and surrounding code is + // probably the largest, most complicated and strange piece of + // code in SVG-edit, we need to write a whole lot of unit tests + // for it here. +}); diff --git a/golden/5-svgedit-unit/sanitize.spec.ts b/golden/5-svgedit-unit/sanitize.spec.ts new file mode 100644 index 0000000..48b0cb6 --- /dev/null +++ b/golden/5-svgedit-unit/sanitize.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test'; + +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; +import * as sanitize from '../../../packages/svgcanvas/core/sanitize.js'; + +test.describe('sanitize', function () { + const svg = document.createElementNS(NS.SVG, 'svg'); + + test('Test sanitizeSvg() strips ws from style attr', async function ({ + page, + }) { + const rect = document.createElementNS(NS.SVG, 'rect'); + rect.setAttribute('style', 'stroke: blue ;\t\tstroke-width :\t\t40;'); + // sanitizeSvg() requires the node to have a parent and a document. + svg.append(rect); + sanitize.sanitizeSvg(rect); + + assert.equal(rect.getAttribute('stroke'), 'blue'); + assert.equal(rect.getAttribute('stroke-width'), '40'); + }); +}); diff --git a/golden/5-svgedit-unit/select.spec.ts b/golden/5-svgedit-unit/select.spec.ts new file mode 100644 index 0000000..a1efc51 --- /dev/null +++ b/golden/5-svgedit-unit/select.spec.ts @@ -0,0 +1,180 @@ +import { test, expect } from '@playwright/test'; + +import * as select from '../../../packages/svgcanvas/core/select.js'; +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; + +test.describe('select', function () { + const sandbox = document.createElement('div'); + sandbox.id = 'sandbox'; + + let svgroot; + let svgContent; + const mockConfig = { + dimensions: [640, 480], + }; + const dataStorage = { + _storage: new WeakMap(), + put: function (element, key, obj) { + if (!this._storage.has(element)) { + this._storage.set(element, new Map()); + } + this._storage.get(element).set(key, obj); + }, + get: function (element, key) { + return this._storage.get(element).get(key); + }, + has: function (element, key) { + return this._storage.has(element) && this._storage.get(element).has(key); + }, + remove: function (element, key) { + const ret = this._storage.get(element).delete(key); + if (!this._storage.get(element).size === 0) { + this._storage.delete(element); + } + return ret; + }, + }; + + /** + * @implements {module:select.SVGFactory} + */ + const mockSvgCanvas = { + curConfig: mockConfig, + createSVGElement(jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element); + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value); + }); + return elem; + }, + getSvgRoot() { + return svgroot; + }, + getSvgContent() { + return svgContent; + }, + getDataStorage() { + return dataStorage; + }, + }; + + /** + * Potentially reusable test set-up. + * @returns {void} + */ + test.beforeEach(async ({ page }) => { + svgroot = mockSvgCanvas.createSVGElement({ + element: 'svg', + attr: { id: 'svgroot' }, + }); + svgContent = mockSvgCanvas.createSVGElement({ + element: 'svg', + attr: { id: 'svgcontent' }, + }); + + svgroot.append(svgContent); + /* const rect = */ svgContent.append( + mockSvgCanvas.createSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '50', + y: '75', + width: '200', + height: '100', + }, + }) + ); + sandbox.append(svgroot); + }); + + /* + function setUpWithInit () { + select.init(mockConfig, mockFactory); + } + */ + + /** + * Tear down the test by emptying our sandbox area. + * @returns {void} + */ + test.afterEach(async ({ page }) => { + while (sandbox.hasChildNodes()) { + sandbox.firstChild.remove(); + } + }); + + test('Test svgedit.select package', async function ({ page }) { + assert.ok(select); + assert.ok(select.Selector); + assert.ok(select.SelectorManager); + assert.ok(select.init); + assert.ok(select.getSelectorManager); + assert.equal(typeof select, typeof {}); + assert.equal( + typeof select.Selector, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof select.SelectorManager, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof select.init, + typeof function () { + /* empty fn */ + } + ); + assert.equal( + typeof select.getSelectorManager, + typeof function () { + /* empty fn */ + } + ); + }); + + test('Test Selector DOM structure', async function ({ page }) { + assert.ok(svgroot); + assert.ok(svgroot.hasChildNodes()); + + // Verify non-existence of Selector DOM nodes + assert.equal(svgroot.childNodes.length, 1); + assert.equal(svgroot.childNodes.item(0), svgContent); + assert.ok(!svgroot.querySelector('#selectorParentGroup')); + + select.init(mockSvgCanvas); + + assert.equal(svgroot.childNodes.length, 3); + + // Verify existence of canvas background. + const cb = svgroot.childNodes.item(0); + assert.ok(cb); + assert.equal(cb.id, 'canvasBackground'); + + assert.ok(svgroot.childNodes.item(1)); + assert.equal(svgroot.childNodes.item(1), svgContent); + + // Verify existence of selectorParentGroup. + const spg = svgroot.childNodes.item(2); + assert.ok(spg); + assert.equal(svgroot.querySelector('#selectorParentGroup'), spg); + assert.equal(spg.id, 'selectorParentGroup'); + assert.equal(spg.tagName, 'g'); + + // Verify existence of all grip elements. + assert.ok(spg.querySelector('#selectorGrip_resize_nw')); + assert.ok(spg.querySelector('#selectorGrip_resize_n')); + assert.ok(spg.querySelector('#selectorGrip_resize_ne')); + assert.ok(spg.querySelector('#selectorGrip_resize_e')); + assert.ok(spg.querySelector('#selectorGrip_resize_se')); + assert.ok(spg.querySelector('#selectorGrip_resize_s')); + assert.ok(spg.querySelector('#selectorGrip_resize_sw')); + assert.ok(spg.querySelector('#selectorGrip_resize_w')); + assert.ok(spg.querySelector('#selectorGrip_rotateconnector')); + assert.ok(spg.querySelector('#selectorGrip_rotate')); + }); +}); diff --git a/golden/5-svgedit-unit/test1.spec.ts b/golden/5-svgedit-unit/test1.spec.ts new file mode 100644 index 0000000..6ddf1d6 --- /dev/null +++ b/golden/5-svgedit-unit/test1.spec.ts @@ -0,0 +1,317 @@ +import { test, expect } from '@playwright/test'; + +/* eslint-disable max-len, no-console */ +import SvgCanvas from '../../../packages/svgcanvas'; + +test.describe('Basic Module', function () { + // helper functions + /* + const isIdentity = function (m) { + return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0); + }; + const matrixString = function (m) { + return [m.a, m.b, m.c, m.d, m.e, m.f].join(','); + }; + */ + + let svgCanvas; + + const // svgroot = document.getElementById('svgroot'), + // svgdoc = svgroot.documentElement, + svgns = 'http://www.w3.org/2000/svg'; + const xlinkns = 'http://www.w3.org/1999/xlink'; + + test.beforeEach(async ({ page }) => { + document.body.textContent = ''; + const svgEditor = document.createElement('div'); + svgEditor.id = 'svg_editor'; + const svgcanvas = document.createElement('div'); + svgcanvas.style.visibility = 'hidden'; + svgcanvas.id = 'svgcanvas'; + const workarea = document.createElement('div'); + workarea.id = 'workarea'; + workarea.append(svgcanvas); + const toolsLeft = document.createElement('div'); + toolsLeft.id = 'tools_left'; + + svgEditor.append(workarea, toolsLeft); + document.body.append(svgEditor); + + svgCanvas = new SvgCanvas(document.getElementById('svgcanvas'), { + canvas_expansion: 3, + dimensions: [640, 480], + initFill: { + color: 'FF0000', // solid red + opacity: 1, + }, + initStroke: { + width: 5, + color: '000000', // solid black + opacity: 1, + }, + initOpacity: 1, + imgPath: '../editor/images', + langPath: 'locale/', + extPath: 'extensions/', + extensions: ['ext-arrows.js', 'ext-eyedropper.js'], + initTool: 'select', + wireframe: false, + }); + }); + + test('Test existence of SvgCanvas object', async function ({ page }) { + assert.equal(typeof {}, typeof svgCanvas); + }); + + test.describe('Path Module', function () { + test('Test path conversion from absolute to relative', async function ({ + page, + }) { + const convert = svgCanvas.pathActions.convertPath; + + // TODO: Test these paths: + // "m400.00491,625.01379a1.78688,1.78688 0 1 1-3.57373,0a1.78688,1.78688 0 1 13.57373,0z" + // "m36.812,15.8566c-28.03099,0 -26.28099,12.15601 -26.28099,12.15601l0.03099,12.59399h26.75v3.781h-37.37399c0,0 -17.938,-2.034 -133.00001,26.25c115.06201,28.284 130.71801,27.281 130.71801,27.281h9.34399v-13.125c0,0 -0.504,-15.656 15.40601,-15.656h26.532c0,0 14.90599,0.241 14.90599,-14.406v-24.219c0,0 2.263,-14.65601 -27.032,-14.65601zm-14.75,8.4684c2.662,0 4.813,2.151 4.813,4.813c0,2.661 -2.151,4.812 -4.813,4.812c-2.661,0 -4.812,-2.151 -4.812,-4.812c0,-2.662 2.151,-4.813 4.812,-4.813z" + // "m 0,0 l 200,0 l 0,100 L 0,100" + + svgCanvas.setSvgString( + "" + + "" + + "" + + '' + ); + + const p1 = document.getElementById('p1'); + const p2 = document.getElementById('p2'); + const dAbs = p1.getAttribute('d'); + const seglist = p1.pathSegList; + + assert.equal(p1.nodeName, 'path', "Expected 'path', got"); + + assert.equal( + seglist.numberOfItems, + 4, + 'Number of segments before conversion' + ); + + // verify segments before conversion + let curseg = seglist.getItem(0); + assert.equal( + curseg.pathSegTypeAsLetter.toUpperCase(), + 'M', + 'Before conversion, segment #1 type' + ); + curseg = seglist.getItem(1); + assert.equal( + curseg.pathSegTypeAsLetter.toUpperCase(), + 'L', + 'Before conversion, segment #2 type' + ); + curseg = seglist.getItem(3); + assert.equal( + curseg.pathSegTypeAsLetter.toUpperCase(), + 'Z', + 'Before conversion, segment #3 type' + dAbs + ); + + // convert and verify segments + let d = convert(p1, true); + assert.equal( + d, + 'm100,100l100,0l-100,0z', + 'Converted path to relative string' + ); + + // TODO: see why this isn't working in SVG-edit + d = convert(p2, true); + console.log('Convert true', d); + d = convert(p2, false); + console.log('Convert false', d); + }); + }); + + test.describe('Import Module', function () { + test('Test import use', async function ({ page }) { + svgCanvas.setSvgString( + "" + + "" + + "" + + "" + + "" + + '' + ); + + const u = document.getElementById('the-use'); + const fu = document.getElementById('foreign-use'); + const nfu = document.getElementById('no-use'); + + assert.equal(u && u.nodeName, 'use', 'Did not import element'); + assert.equal(fu, null, 'Removed element that had a foreign href'); + assert.equal(nfu, null, 'Removed element that had no href'); + }); + + // This test shows that an element with an invalid attribute is still parsed in properly + // and only the attribute is not imported + test('Test invalid attribute', async function ({ page }) { + svgCanvas.setSvgString( + '' + + 'words' + + '' + ); + + const t = document.getElementById('the-text'); + + assert.equal(t && t.nodeName, 'text', 'Did not import element'); + assert.equal( + t.getAttribute('d'), + null, + 'Imported a with a d attribute' + ); + }); + + // This test makes sure import/export properly handles namespaced attributes + test('Test importing/exporting namespaced attributes', async function ({ + page, + }) { + /* const setStr = */ svgCanvas.setSvgString( + '' + + '' + + '' + + '' + ); + const attrVal = document + .getElementById('se_test_elem') + .getAttributeNS('http://svg-edit.googlecode.com', 'foo'); + + assert.strictEqual( + attrVal, + 'bar', + true, + 'Preserved namespaced attribute on import' + ); + + const output = svgCanvas.getSvgString(); + const hasXlink = output.includes( + 'xmlns:xlink="http://www.w3.org/1999/xlink"' + ); + const hasSe = output.includes('xmlns:se='); + const hasFoo = output.includes('xmlns:foo='); + const hasAttr = output.includes('se:foo="bar"'); + + assert.equal(hasAttr, true, 'Preserved namespaced attribute on export'); + assert.equal(hasXlink, true, 'Included xlink: xmlns'); + assert.equal(hasSe, true, 'Included se: xmlns'); + assert.equal(hasFoo, false, 'Did not include foo: xmlns'); + }); + + test('Test import math elements inside a foreignObject', async function ({ + page, + }) { + /* const set = */ svgCanvas.setSvgString( + '' + + '' + + '' + + 'A' + + '0' + + '' + + '' + + '' + + '' + ); + const fo = document.getElementById('fo'); + // we cannot use getElementById('math') because not all browsers understand MathML and do not know to use the @id attribute + // see Bug https://bugs.webkit.org/show_bug.cgi?id=35042 + const math = fo.firstChild; + + assert.equal(Boolean(math), true, 'Math element exists'); + assert.equal( + math.nodeName, + 'math', + 'Math element has the proper nodeName' + ); + assert.equal(math.getAttribute('id'), 'm', 'Math element has an id'); + assert.equal( + math.namespaceURI, + 'http://www.w3.org/1998/Math/MathML', + 'Preserved MathML namespace' + ); + }); + + test('Test importing SVG into existing drawing', async function ({ page }) { + /* const doc = */ svgCanvas.setSvgString( + '' + + 'Layer 1' + + '' + + '' + + '' + + '' + ); + + svgCanvas.importSvgString( + '' + + '' + + '' + + '' + ); + + const svgContent = document.getElementById('svgcontent'); + const circles = svgContent.getElementsByTagNameNS(svgns, 'circle'); + const rects = svgContent.getElementsByTagNameNS(svgns, 'rect'); + const ellipses = svgContent.getElementsByTagNameNS(svgns, 'ellipse'); + assert.equal(circles.length, 2, 'Found two circles upon importing'); + assert.equal(rects.length, 1, 'Found one rectangle upon importing'); + assert.equal(ellipses.length, 1, 'Found one ellipse upon importing'); + }); + + test('Test importing SVG remaps IDs', async function ({ page }) { + /* const doc = */ svgCanvas.setSvgString( + '' + + 'Layer 1' + + '' + + '' + + '' + + '' + + '' + ); + + svgCanvas.importSvgString( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); + + const svgContent = document.getElementById('svgcontent'); + const circles = svgContent.getElementsByTagNameNS(svgns, 'circle'); + const rects = svgContent.getElementsByTagNameNS(svgns, 'rect'); + // ellipses = svgContent.getElementsByTagNameNS(svgns, 'ellipse'), + const defs = svgContent.getElementsByTagNameNS(svgns, 'defs'); + // grads = svgContent.getElementsByTagNameNS(svgns, 'linearGradient'), + const uses = svgContent.getElementsByTagNameNS(svgns, 'use'); + assert.notEqual(circles.item(0).id, 'svg_1', 'Circle not re-identified'); + assert.notEqual(rects.item(0).id, 'svg_3', 'Rectangle not re-identified'); + // TODO: determine why this test fails in WebKit browsers + // assert.equal(grads.length, 1, 'Linear gradient imported'); + const grad = defs.item(0).firstChild; + assert.notEqual(grad.id, 'svg_2', 'Linear gradient not re-identified'); + assert.notEqual( + circles.item(0).getAttribute('fill'), + 'url(#svg_2)', + 'Circle fill value not remapped' + ); + assert.notEqual( + rects.item(0).getAttribute('stroke'), + 'url(#svg_2)', + 'Rectangle stroke value not remapped' + ); + assert.notEqual(uses.item(0).getAttributeNS(xlinkns, 'href'), '#svg_3'); + }); + }); +}); diff --git a/golden/5-svgedit-unit/units.spec.ts b/golden/5-svgedit-unit/units.spec.ts new file mode 100644 index 0000000..bcc5a1c --- /dev/null +++ b/golden/5-svgedit-unit/units.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; + +import * as units from '../../../packages/svgcanvas/core/units.js'; + +test.describe('units', function () { + /** + * Set up tests, supplying mock data. + * @returns {void} + */ + test.beforeEach(async ({ page }) => { + document.body.textContent = ''; + const anchor = document.createElement('div'); + anchor.id = 'anchor'; + anchor.style.visibility = 'hidden'; + + const elementsContainer = document.createElement('div'); + elementsContainer.id = 'elementsContainer'; + + const uniqueId = document.createElement('div'); + uniqueId.id = 'uniqueId'; + uniqueId.style.visibility = 'hidden'; + + const nonUniqueId = document.createElement('div'); + nonUniqueId.id = 'nonUniqueId'; + nonUniqueId.style.visibility = 'hidden'; + + elementsContainer.append(uniqueId, nonUniqueId); + + document.body.append(anchor, elementsContainer); + + units.init( + /** + * @implements {module:units.ElementContainer} + */ + { + getBaseUnit() { + return 'cm'; + }, + getHeight() { + return 600; + }, + getWidth() { + return 800; + }, + getRoundDigits() { + return 4; + }, + getElement(elementId) { + return document.getElementById(elementId); + }, + } + ); + }); + + test('Test svgedit.units package', async function ({ page }) { + assert.ok(units); + assert.equal(typeof units, typeof {}); + }); + + test('Test svgedit.units.shortFloat()', async function ({ page }) { + assert.ok(units.shortFloat); + assert.equal( + typeof units.shortFloat, + typeof function () { + /* empty fn */ + } + ); + + const { shortFloat } = units; + assert.equal(shortFloat(0.00000001), 0); + assert.equal(shortFloat(1), 1); + assert.equal(shortFloat(3.45678), 3.4568); + assert.equal(shortFloat(1.23443), 1.2344); + assert.equal(shortFloat(1.23455), 1.2346); + }); + + test('Test svgedit.units.isValidUnit()', async function ({ page }) { + assert.ok(units.isValidUnit); + assert.equal( + typeof units.isValidUnit, + typeof function () { + /* empty fn */ + } + ); + + const { isValidUnit } = units; + assert.ok(isValidUnit('0')); + assert.ok(isValidUnit('1')); + assert.ok(isValidUnit('1.1')); + assert.ok(isValidUnit('-1.1')); + assert.ok(isValidUnit('.6mm')); + assert.ok(isValidUnit('-.6cm')); + assert.ok(isValidUnit('6000in')); + assert.ok(isValidUnit('6px')); + assert.ok(isValidUnit('6.3pc')); + assert.ok(isValidUnit('-0.4em')); + assert.ok(isValidUnit('-0.ex')); + assert.ok(isValidUnit('40.123%')); + + assert.equal( + isValidUnit('id', 'uniqueId', document.getElementById('uniqueId')), + true + ); + assert.equal( + isValidUnit('id', 'newId', document.getElementById('uniqueId')), + true + ); + assert.equal(isValidUnit('id', 'uniqueId'), false); + assert.equal( + isValidUnit('id', 'uniqueId', document.getElementById('nonUniqueId')), + false + ); + }); + + test('Test svgedit.units.convertUnit()', async function ({ page }) { + assert.ok(units.convertUnit); + assert.equal( + typeof units.convertUnit, + typeof function () { + /* empty fn */ + } + ); + // cm in default setup + assert.equal(units.convertUnit(42), 1.1113); + assert.equal(units.convertUnit(42, 'px'), 42); + }); +}); diff --git a/golden/5-svgedit-unit/utilities-bbox.spec.ts b/golden/5-svgedit-unit/utilities-bbox.spec.ts new file mode 100644 index 0000000..b564cec --- /dev/null +++ b/golden/5-svgedit-unit/utilities-bbox.spec.ts @@ -0,0 +1,751 @@ +import { test, expect } from '@playwright/test'; + +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'; + +// eslint-disable-next-line +chai.use(setAssertionMethods); + +test.describe('utilities bbox', function () { + /** + * Create an SVG element for a mock. + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function mockCreateSVGElement(jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element); + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value); + }); + return elem; + } + let mockaddSVGElementsFromJsonCallCount = 0; + + /** + * Mock of {@link module:utilities.EditorContext#addSVGElementsFromJson}. + * @param {module:utilities.SVGElementJSON} json + * @returns {SVGElement} + */ + function mockaddSVGElementsFromJson(json) { + const elem = mockCreateSVGElement(json); + svgroot.append(elem); + mockaddSVGElementsFromJsonCallCount++; + return elem; + } + const mockPathActions = { + resetOrientation(pth) { + if (pth?.nodeName !== 'path') { + return false; + } + const tlist = pth.transform.baseVal; + const m = math.transformListToTransform(tlist).matrix; + tlist.clear(); + pth.removeAttribute('transform'); + const segList = pth.pathSegList; + + const len = segList.numberOfItems; + // let lastX, lastY; + + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i); + const type = seg.pathSegType; + if (type === 1) { + continue; + } + const pts = []; + ['', 1, 2].forEach(function (n) { + const x = seg['x' + n]; + const y = seg['y' + n]; + if (x !== undefined && y !== undefined) { + const pt = math.transformPoint(x, y, m); + pts.splice(pts.length, 0, pt.x, pt.y); + } + }); + path.replacePathSeg(type, i, pts, pth); + } + return undefined; + }, + }; + + const EPSILON = 0.001; + + let svgroot; + test.beforeEach(async ({ page }) => { + document.body.textContent = ''; + + // const svg = document.createElementNS(NS.SVG, 'svg'); + const sandbox = document.createElement('div'); + sandbox.id = 'sandbox'; + document.body.append(sandbox); + + svgroot = mockCreateSVGElement({ + element: 'svg', + attr: { id: 'svgroot' }, + }); + sandbox.append(svgroot); + + const mockSvgCanvas = { + createSVGElement(jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element); + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value); + }); + return elem; + }, + getSvgRoot() { + return svgroot; + }, + }; + + path.init(mockSvgCanvas); + mockaddSVGElementsFromJsonCallCount = 0; + }); + + test('Test svgedit.utilities package', async function ({ page }) { + assert.ok(utilities); + assert.ok(utilities.getBBoxWithTransform); + assert.ok(utilities.getStrokedBBox); + assert.ok(utilities.getRotationAngleFromTransformList); + assert.ok(utilities.getRotationAngle); + }); + + test('Test getBBoxWithTransform and no transform', async function ({ page }) { + const { getBBoxWithTransform } = utilities; + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3' }, + }); + svgroot.append(elem); + let bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 }); + assert.equal(mockaddSVGElementsFromJsonCallCount, 0); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + svgroot.append(elem); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + assert.equal(mockaddSVGElementsFromJsonCallCount, 0); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }, + }); + svgroot.append(elem); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }); + assert.equal(mockaddSVGElementsFromJsonCallCount, 0); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + const g = mockCreateSVGElement({ + element: 'g', + attr: {}, + }); + g.append(elem); + svgroot.append(g); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + assert.equal(mockaddSVGElementsFromJsonCallCount, 0); + g.remove(); + }); + + test('Test getBBoxWithTransform and a rotation transform', async function ({ + page, + }) { + const { getBBoxWithTransform } = utilities; + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M10,10 L20,20', transform: 'rotate(45 10,10)' }, + }); + svgroot.append(elem); + let bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.close(bbox.x, 10, EPSILON); + assert.close(bbox.y, 10, EPSILON); + assert.close(bbox.width, 0, EPSILON); + assert.close(bbox.height, Math.sqrt(100 + 100), EPSILON); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '10', + y: '10', + width: '10', + height: '20', + transform: 'rotate(90 15,20)', + }, + }); + svgroot.append(elem); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.close(bbox.x, 5, EPSILON); + assert.close(bbox.y, 15, EPSILON); + assert.close(bbox.width, 20, EPSILON); + assert.close(bbox.height, 10, EPSILON); + assert.equal(mockaddSVGElementsFromJsonCallCount, 1); + elem.remove(); + + const rect = { x: 10, y: 10, width: 10, height: 20 }; + const angle = 45; + const origin = { x: 15, y: 20 }; + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect2', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')', + }, + }); + svgroot.append(elem); + mockaddSVGElementsFromJsonCallCount = 0; + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + const r2 = rotateRect(rect, angle, origin); + assert.close(bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x); + assert.close(bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y); + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width); + assert.close( + bbox.height, + r2.height, + EPSILON, + 'rect2 height is ' + r2.height + ); + assert.equal(mockaddSVGElementsFromJsonCallCount, 0); + elem.remove(); + + // Same as previous but wrapped with g and the transform is with the g. + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect3', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }, + }); + const g = mockCreateSVGElement({ + element: 'g', + attr: { + transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')', + }, + }); + g.append(elem); + svgroot.append(g); + mockaddSVGElementsFromJsonCallCount = 0; + bbox = getBBoxWithTransform(g, mockaddSVGElementsFromJson, mockPathActions); + assert.close(bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x); + assert.close(bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y); + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width); + assert.close( + bbox.height, + r2.height, + EPSILON, + 'rect2 height is ' + r2.height + ); + assert.equal(mockaddSVGElementsFromJsonCallCount, 0); + g.remove(); + + elem = mockCreateSVGElement({ + element: 'ellipse', + attr: { + id: 'ellipse1', + cx: '100', + cy: '100', + rx: '50', + ry: '50', + transform: 'rotate(45 100,100)', + }, + }); + svgroot.append(elem); + mockaddSVGElementsFromJsonCallCount = 0; + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + /** @todo: Review these test the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. */ + // assert.ok(bbox.x > 45 && bbox.x <= 50); + assert.ok(bbox.y > 45 && bbox.y <= 50); + // assert.ok(bbox.width >= 100 && bbox.width < 110); + // assert.ok(bbox.height >= 100 && bbox.height < 110); + assert.equal(mockaddSVGElementsFromJsonCallCount, 1); + elem.remove(); + }); + + test('Test getBBoxWithTransform with rotation and matrix transforms', async function ({ + page, + }) { + const { getBBoxWithTransform } = utilities; + + let tx = 10; // tx right + let ty = 10; // tx down + let txInRotatedSpace = Math.sqrt(tx * tx + ty * ty); // translate in rotated 45 space. + let tyInRotatedSpace = 0; + let matrix = + 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')'; + let elem = mockCreateSVGElement({ + element: 'path', + attr: { + id: 'path', + d: 'M10,10 L20,20', + transform: 'rotate(45 10,10) ' + matrix, + }, + }); + svgroot.append(elem); + let bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.close(bbox.x, 10 + tx, EPSILON); + assert.close(bbox.y, 10 + ty, EPSILON); + assert.close(bbox.width, 0, EPSILON); + assert.close(bbox.height, Math.sqrt(100 + 100), EPSILON); + elem.remove(); + + txInRotatedSpace = tx; // translate in rotated 90 space. + tyInRotatedSpace = -ty; + matrix = + 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')'; + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '10', + y: '10', + width: '10', + height: '20', + transform: 'rotate(90 15,20) ' + matrix, + }, + }); + svgroot.append(elem); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.close(bbox.x, 5 + tx, EPSILON); + assert.close(bbox.y, 15 + ty, EPSILON); + assert.close(bbox.width, 20, EPSILON); + assert.close(bbox.height, 10, EPSILON); + elem.remove(); + + const rect = { x: 10, y: 10, width: 10, height: 20 }; + const angle = 45; + const origin = { x: 15, y: 20 }; + tx = 10; // tx right + ty = 10; // tx down + txInRotatedSpace = Math.sqrt(tx * tx + ty * ty); // translate in rotated 45 space. + tyInRotatedSpace = 0; + matrix = + 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')'; + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect2', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + transform: + 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ') ' + matrix, + }, + }); + svgroot.append(elem); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + const r2 = rotateRect(rect, angle, origin); + assert.close(bbox.x, r2.x + tx, EPSILON, 'rect2 x is ' + r2.x); + assert.close(bbox.y, r2.y + ty, EPSILON, 'rect2 y is ' + r2.y); + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width); + assert.close( + bbox.height, + r2.height, + EPSILON, + 'rect2 height is ' + r2.height + ); + elem.remove(); + + // Same as previous but wrapped with g and the transform is with the g. + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect3', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }, + }); + const g = mockCreateSVGElement({ + element: 'g', + attr: { + transform: + 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ') ' + matrix, + }, + }); + g.append(elem); + svgroot.append(g); + bbox = getBBoxWithTransform(g, mockaddSVGElementsFromJson, mockPathActions); + assert.close(bbox.x, r2.x + tx, EPSILON, 'rect2 x is ' + r2.x); + assert.close(bbox.y, r2.y + ty, EPSILON, 'rect2 y is ' + r2.y); + assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width); + assert.close( + bbox.height, + r2.height, + EPSILON, + 'rect2 height is ' + r2.height + ); + g.remove(); + + elem = mockCreateSVGElement({ + element: 'ellipse', + attr: { + id: 'ellipse1', + cx: '100', + cy: '100', + rx: '50', + ry: '50', + transform: 'rotate(45 100,100) ' + matrix, + }, + }); + svgroot.append(elem); + bbox = getBBoxWithTransform( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + /** @todo: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. */ + // assert.ok(bbox.x > 45 + tx && bbox.x <= 50 + tx); + assert.ok(bbox.y > 45 + ty && bbox.y <= 50 + ty); + // assert.ok(bbox.width >= 100 && bbox.width < 110); + // assert.ok(bbox.height >= 100 && bbox.height < 110); + elem.remove(); + }); + + test('Test getStrokedBBox with stroke-width 10', async function ({ page }) { + const { getStrokedBBox } = utilities; + + const strokeWidth = 10; + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3', 'stroke-width': strokeWidth }, + }); + svgroot.append(elem); + let bbox = getStrokedBBox( + [elem], + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { + x: 0 - strokeWidth / 2, + y: 1 - strokeWidth / 2, + width: 2 + strokeWidth, + height: 2 + strokeWidth, + }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '0', + y: '1', + width: '5', + height: '10', + 'stroke-width': strokeWidth, + }, + }); + svgroot.append(elem); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { + x: 0 - strokeWidth / 2, + y: 1 - strokeWidth / 2, + width: 5 + strokeWidth, + height: 10 + strokeWidth, + }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'line', + attr: { + id: 'line', + x1: '0', + y1: '1', + x2: '5', + y2: '6', + 'stroke-width': strokeWidth, + }, + }); + svgroot.append(elem); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { + x: 0 - strokeWidth / 2, + y: 1 - strokeWidth / 2, + width: 5 + strokeWidth, + height: 5 + strokeWidth, + }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '0', + y: '1', + width: '5', + height: '10', + 'stroke-width': strokeWidth, + }, + }); + const g = mockCreateSVGElement({ + element: 'g', + attr: {}, + }); + g.append(elem); + svgroot.append(g); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { + x: 0 - strokeWidth / 2, + y: 1 - strokeWidth / 2, + width: 5 + strokeWidth, + height: 10 + strokeWidth, + }); + g.remove(); + }); + + test("Test getStrokedBBox with stroke-width 'none'", async function ({ + page, + }) { + const { getStrokedBBox } = utilities; + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3', 'stroke-width': 'none' }, + }); + svgroot.append(elem); + let bbox = getStrokedBBox( + [elem], + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '0', + y: '1', + width: '5', + height: '10', + 'stroke-width': 'none', + }, + }); + svgroot.append(elem); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'line', + attr: { + id: 'line', + x1: '0', + y1: '1', + x2: '5', + y2: '6', + 'stroke-width': 'none', + }, + }); + svgroot.append(elem); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'rect', + x: '0', + y: '1', + width: '5', + height: '10', + 'stroke-width': 'none', + }, + }); + const g = mockCreateSVGElement({ + element: 'g', + attr: {}, + }); + g.append(elem); + svgroot.append(g); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + g.remove(); + }); + + test('Test getStrokedBBox with no stroke-width attribute', async function ({ + page, + }) { + const { getStrokedBBox } = utilities; + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 L2,3' }, + }); + svgroot.append(elem); + let bbox = getStrokedBBox( + [elem], + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + svgroot.append(elem); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }, + }); + svgroot.append(elem); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + const g = mockCreateSVGElement({ + element: 'g', + attr: {}, + }); + g.append(elem); + svgroot.append(g); + bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + g.remove(); + }); + + /** + * Returns radians for degrees. + * @param {Float} degrees + * @returns {Float} + */ + function radians(degrees) { + return (degrees * Math.PI) / 180; + } + + /** + * + * @param {module:utilities.BBoxObject} point + * @param {Float} angle + * @param {module:math.XYObject} origin + * @returns {module:math.XYObject} + */ + function rotatePoint(point, angle, origin = { x: 0, y: 0 }) { + const x = point.x - origin.x; + const y = point.y - origin.y; + const theta = radians(angle); + return { + x: x * Math.cos(theta) + y * Math.sin(theta) + origin.x, + y: x * Math.sin(theta) + y * Math.cos(theta) + origin.y, + }; + } + /** + * + * @param {module:utilities.BBoxObject} rect + * @param {Float} angle + * @param {module:math.XYObject} origin + * @returns {module:utilities.BBoxObject} + */ + function rotateRect(rect, angle, origin) { + const tl = rotatePoint({ x: rect.x, y: rect.y }, angle, origin); + const tr = rotatePoint( + { x: rect.x + rect.width, y: rect.y }, + angle, + origin + ); + const br = rotatePoint( + { x: rect.x + rect.width, y: rect.y + rect.height }, + angle, + origin + ); + const bl = rotatePoint( + { x: rect.x, y: rect.y + rect.height }, + angle, + origin + ); + + const minx = Math.min(tl.x, tr.x, bl.x, br.x); + const maxx = Math.max(tl.x, tr.x, bl.x, br.x); + const miny = Math.min(tl.y, tr.y, bl.y, br.y); + const maxy = Math.max(tl.y, tr.y, bl.y, br.y); + + return { + x: minx, + y: miny, + width: maxx - minx, + height: maxy - miny, + }; + } +}); diff --git a/golden/5-svgedit-unit/utilities-performance.spec.ts b/golden/5-svgedit-unit/utilities-performance.spec.ts new file mode 100644 index 0000000..c1be11d --- /dev/null +++ b/golden/5-svgedit-unit/utilities-performance.spec.ts @@ -0,0 +1,293 @@ +import { test, expect } from '@playwright/test'; + +/* eslint-disable max-len, no-console */ +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'; + +test.describe('utilities performance', function () { + let currentLayer; + let groupWithMatrixTransform; + let textWithMatrixTransform; + test.beforeEach(async ({ page }) => { + document.body.textContent = ''; + const style = document.createElement('style'); + style.id = 'styleoverrides'; + style.media = 'screen'; + style.textContent = ` + #svgcanvas svg * { + cursor: move; + pointer-events: all + } + #svgcanvas svg { + cursor: default + }`; + + document.head.append(style); + + const editor = new DOMParser().parseFromString( + `
+
+ + + + + + + +
+ + + + + + + + + Layer 1 + + + + + + + + + + + + Some text + + + + Layer 2 + + + + +
+
`, + 'application/xml' + ); + const newNode = document.body.ownerDocument.importNode( + editor.documentElement, + true + ); + document.body.append(newNode); + + currentLayer = document.getElementById('layer1'); + groupWithMatrixTransform = document.getElementById( + 'svg_group_with_matrix_transform' + ); + textWithMatrixTransform = document.getElementById( + 'svg_text_with_matrix_transform' + ); + }); + + /** + * Create an SVG element for a mock. + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function mockCreateSVGElement(jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element); + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value); + }); + return elem; + } + + /** + * Mock of {@link module:utilities.EditorContext#addSVGElementsFromJson}. + * @param {module:utilities.SVGElementJSON} json + * @returns {SVGElement} + */ + function mockaddSVGElementsFromJson(json) { + const elem = mockCreateSVGElement(json); + currentLayer.append(elem); + return elem; + } + + /** + * Toward performance testing, fill document with clones of element. + * @param {SVGElement} elem + * @param {Integer} count + * @returns {void} + */ + function fillDocumentByCloningElement(elem, count) { + const elemId = elem.getAttribute('id') + '-'; + for (let index = 0; index < count; index++) { + const clone = elem.cloneNode(true); // t: deep clone + // Make sure you set a unique ID like a real document. + clone.setAttribute('id', elemId + index); + const { parentNode } = elem; + parentNode.append(clone); + } + } + + const mockPathActions = { + resetOrientation(path) { + if (path?.nodeName !== 'path') { + return false; + } + const tlist = path.transform.baseVal; + const m = math.transformListToTransform(tlist).matrix; + tlist.clear(); + path.removeAttribute('transform'); + const segList = path.pathSegList; + + const len = segList.numberOfItems; + // let lastX, lastY; + + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i); + const type = seg.pathSegType; + if (type === 1) { + continue; + } + const pts = []; + ['', 1, 2].forEach(function (n) { + const x = seg['x' + n]; + const y = seg['y' + n]; + if (x !== undefined && y !== undefined) { + const pt = math.transformPoint(x, y, m); + pts.splice(pts.length, 0, pt.x, pt.y); + } + }); + // path.replacePathSeg(type, i, pts, path); + } + + return undefined; + }, + }; + + // ////////////////////////////////////////////////////////// + // Performance times with various browsers on Macbook 2011 8MB RAM OS X El Capitan 10.11.4 + // + // To see 'Before Optimization' performance, making the following two edits. + // 1. utilities.getStrokedBBox - change if( elems.length === 1) to if( false && elems.length === 1) + // 2. utilities.getBBoxWithTransform - uncomment 'Old technique that was very slow' + + // Chrome + // Before Optimization + // Pass1 svgCanvas.getStrokedBBox total ms 4,218, ave ms 41.0, min/max 37 51 + // Pass2 svgCanvas.getStrokedBBox total ms 4,458, ave ms 43.3, min/max 32 63 + // Optimized Code + // Pass1 svgCanvas.getStrokedBBox total ms 1,112, ave ms 10.8, min/max 9 20 + // Pass2 svgCanvas.getStrokedBBox total ms 34, ave ms 0.3, min/max 0 20 + + // Firefox + // Before Optimization + // Pass1 svgCanvas.getStrokedBBox total ms 3,794, ave ms 36.8, min/max 33 48 + // Pass2 svgCanvas.getStrokedBBox total ms 4,049, ave ms 39.3, min/max 28 53 + // Optimized Code + // Pass1 svgCanvas.getStrokedBBox total ms 104, ave ms 1.0, min/max 0 23 + // Pass2 svgCanvas.getStrokedBBox total ms 71, ave ms 0.7, min/max 0 23 + + // Safari + // Before Optimization + // Pass1 svgCanvas.getStrokedBBox total ms 4,840, ave ms 47.0, min/max 45 62 + // Pass2 svgCanvas.getStrokedBBox total ms 4,849, ave ms 47.1, min/max 34 62 + // Optimized Code + // Pass1 svgCanvas.getStrokedBBox total ms 42, ave ms 0.4, min/max 0 23 + // Pass2 svgCanvas.getStrokedBBox total ms 17, ave ms 0.2, min/max 0 23 + + test('Test svgCanvas.getStrokedBBox() performance with matrix transforms', async function ({ + page, + }) { + const { getStrokedBBox } = utilities; + const { children } = currentLayer; + + let lastTime; + let now; + let min = Number.MAX_VALUE; + let max = 0; + let total = 0; + + fillDocumentByCloningElement(groupWithMatrixTransform, 50); + fillDocumentByCloningElement(textWithMatrixTransform, 50); + + // The first pass through all elements is slower. + const count = children.length; + const start = (lastTime = now = Date.now()); + // Skip the first child which is the title. + for (let index = 1; index < count; index++) { + const child = children[index]; + /* const obj = */ getStrokedBBox( + [child], + mockaddSVGElementsFromJson, + mockPathActions + ); + now = Date.now(); + const delta = now - lastTime; + lastTime = now; + total += delta; + min = Math.min(min, delta); + max = Math.max(max, delta); + } + total = lastTime - start; + const ave = total / count; + assert.isBelow( + ave, + 20, + 'svgedit.utilities.getStrokedBBox average execution time is less than 20 ms' + ); + console.log( + 'Pass1 svgCanvas.getStrokedBBox total ms ' + + total + + ', ave ms ' + + ave.toFixed(1) + + ',\t min/max ' + + min + + ' ' + + max + ); + + return new Promise((resolve) => { + // The second pass is two to ten times faster. + setTimeout(function () { + const ct = children.length; + + const strt = (lastTime = now = Date.now()); + // Skip the first child which is the title. + for (let index = 1; index < ct; index++) { + const child = children[index]; + /* const obj = */ getStrokedBBox( + [child], + mockaddSVGElementsFromJson, + mockPathActions + ); + now = Date.now(); + const delta = now - lastTime; + lastTime = now; + total += delta; + min = Math.min(min, delta); + max = Math.max(max, delta); + } + + total = lastTime - strt; + const avg = total / ct; + assert.isBelow( + avg, + 2, + 'svgedit.utilities.getStrokedBBox average execution time is less than 1 ms' + ); + console.log( + 'Pass2 svgCanvas.getStrokedBBox total ms ' + + total + + ', ave ms ' + + avg.toFixed(1) + + ',\t min/max ' + + min + + ' ' + + max + ); + + resolve(); + }); + }); + }); +}); diff --git a/golden/5-svgedit-unit/utilities.spec.ts b/golden/5-svgedit-unit/utilities.spec.ts new file mode 100644 index 0000000..2c05174 --- /dev/null +++ b/golden/5-svgedit-unit/utilities.spec.ts @@ -0,0 +1,401 @@ +import { test, expect } from '@playwright/test'; + +import * as utilities from '../../../packages/svgcanvas/core/utilities.js'; +import { NS } from '../../../packages/svgcanvas/core/namespaces.js'; + +test.describe('utilities', function () { + /** + * Create an element for test. + * @param {module:utilities.SVGElementJSON} jsonMap + * @returns {SVGElement} + */ + function mockCreateSVGElement(jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap.element); + Object.entries(jsonMap.attr).forEach(([attr, value]) => { + elem.setAttribute(attr, value); + }); + return elem; + } + /** + * Adds SVG Element per parameters and appends to root. + * @param {module:utilities.SVGElementJSON} json + * @returns {SVGElement} + */ + function mockaddSVGElementsFromJson(json) { + const elem = mockCreateSVGElement(json); + svgroot.append(elem); + return elem; + } + const mockPathActions = { + resetOrientation() { + /* empty fn */ + }, + }; + let mockHistorySubCommands = []; + const mockHistory = { + BatchCommand: class { + addSubCommand(cmd) { + mockHistorySubCommands.push(cmd); + } + }, + RemoveElementCommand: class { + // Longhand needed since used as a constructor + constructor(elem, nextSibling, parent) { + this.elem = elem; + this.nextSibling = nextSibling; + this.parent = parent; + } + }, + InsertElementCommand: class { + constructor(path) { + // Longhand needed since used as a constructor + this.path = path; + } + }, + }; + const mockCount = { + clearSelection: 0, + addToSelection: 0, + addCommandToHistory: 0, + }; + + /** + * Increments clear seleciton count for mock test. + * @returns {void} + */ + function mockClearSelection() { + mockCount.clearSelection++; + } + /** + * Increments add selection count for mock test. + * @returns {void} + */ + function mockAddToSelection() { + mockCount.addToSelection++; + } + /** + * Increments add command to history count for mock test. + * @returns {void} + */ + function mockAddCommandToHistory() { + mockCount.addCommandToHistory++; + } + + const mockSvgCanvas = { + addSVGElementsFromJson: mockaddSVGElementsFromJson, + pathActions: mockPathActions, + clearSelection: mockClearSelection, + addToSelection: mockAddToSelection, + history: mockHistory, + addCommandToHistory: mockAddCommandToHistory, + }; + + let svg; + let svgroot; + test.beforeEach(async ({ page }) => { + document.body.textContent = ''; + + mockHistorySubCommands = []; + mockCount.clearSelection = 0; + mockCount.addToSelection = 0; + mockCount.addCommandToHistory = 0; + + const sandbox = document.createElement('div'); + svg = document.createElementNS(NS.SVG, 'svg'); + svgroot = mockCreateSVGElement({ + element: 'svg', + attr: { id: 'svgroot' }, + }); + sandbox.append(svgroot); + document.body.append(sandbox); + }); + + test('Test svgedit.utilities package', async function ({ page }) { + assert.ok(utilities); + assert.ok(utilities.toXml); + assert.equal( + typeof utilities.toXml, + typeof function () { + /* empty fn */ + } + ); + }); + + test('Test svgedit.utilities.toXml() function', async function ({ page }) { + const { toXml } = utilities; + + assert.equal(toXml('a'), 'a'); + assert.equal(toXml('ABC_'), 'ABC_'); + assert.equal(toXml('PB&J'), 'PB&J'); + assert.equal(toXml('2 < 5'), '2 < 5'); + assert.equal(toXml('5 > 2'), '5 > 2'); + assert.equal(toXml('\'<&>"'), ''<&>"'); + }); + + test('Test svgedit.utilities.encode64() function', async function ({ page }) { + const { encode64 } = utilities; + + assert.equal(encode64('abcdef'), 'YWJjZGVm'); + assert.equal(encode64('12345'), 'MTIzNDU='); + assert.equal(encode64(' '), 'IA=='); + assert.equal( + encode64('`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?'), + 'YH4hQCMkJV4mKigpLV89K1t7XX1cfDs6JyIsPC4+Lz8=' + ); + }); + + test('Test svgedit.utilities.decode64() function', async function ({ page }) { + const { decode64 } = utilities; + + assert.equal(decode64('YWJjZGVm'), 'abcdef'); + assert.equal(decode64('MTIzNDU='), '12345'); + assert.equal(decode64('IA=='), ' '); + assert.equal( + decode64('YH4hQCMkJV4mKigpLV89K1t7XX1cfDs6JyIsPC4+Lz8='), + '`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?' + ); + }); + + test('Test svgedit.utilities.convertToXMLReferences() function', async function ({ + page, + }) { + const convert = utilities.convertToXMLReferences; + assert.equal(convert('ABC'), 'ABC'); + // assert.equal(convert('�BC'), 'ÀBC'); + }); + + test('Test svgedit.utilities.bboxToObj() function', async function ({ + page, + }) { + const { bboxToObj } = utilities; + + const rect = svg.createSVGRect(); + rect.x = 1; + rect.y = 2; + rect.width = 3; + rect.height = 4; + + const obj = bboxToObj(rect); + assert.equal(typeof obj, typeof {}); + assert.equal(obj.x, 1); + assert.equal(obj.y, 2); + assert.equal(obj.width, 3); + assert.equal(obj.height, 4); + }); + + test('Test getUrlFromAttr', async function ({ page }) { + assert.equal(utilities.getUrlFromAttr('url(#foo)'), '#foo'); + assert.equal( + utilities.getUrlFromAttr('url(somefile.svg#foo)'), + 'somefile.svg#foo' + ); + assert.equal(utilities.getUrlFromAttr('url("#foo")'), '#foo'); + assert.equal(utilities.getUrlFromAttr('url("#foo")'), '#foo'); + }); + + test('Test getPathDFromSegments', async function ({ page }) { + const { getPathDFromSegments } = utilities; + + const doc = utilities.text2xml(''); + const path = doc.createElementNS(NS.SVG, 'path'); + path.setAttribute('d', 'm0,0l5,0l0,5l-5,0l0,-5z'); + let d = getPathDFromSegments([ + ['M', [1, 2]], + ['Z', []], + ]); + assert.equal(d, 'M1,2 Z'); + + d = getPathDFromSegments([ + ['M', [1, 2]], + ['M', [3, 4]], + ['Z', []], + ]); + assert.equal(d, 'M1,2 M3,4 Z'); + + d = getPathDFromSegments([ + ['M', [1, 2]], + ['C', [3, 4, 5, 6]], + ['Z', []], + ]); + assert.equal(d, 'M1,2 C3,4 5,6 Z'); + }); + + test('Test getPathDFromElement', async function ({ page }) { + const { getPathDFromElement } = utilities; + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 Z' }, + }); + svgroot.append(elem); + assert.equal(getPathDFromElement(elem), 'M0,1 Z'); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + svgroot.append(elem); + assert.equal(getPathDFromElement(elem), 'M0,1 L5,1 L5,11 L0,11 L0,1 Z'); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { + id: 'roundrect', + x: '0', + y: '1', + rx: '2', + ry: '3', + width: '10', + height: '11', + }, + }); + svgroot.append(elem); + const closeEnough = + /M0,4 C0,2.3\d* 0.9\d*,1 2,1 L8,1 C9.0\d*,1 10,2.3\d* 10,4 L10,9 C10,10.6\d* 9.0\d*,12 8,12 L2,12 C0.9\d*,12 0,10.6\d* 0,9 L0,4 Z/; + assert.equal(closeEnough.test(getPathDFromElement(elem)), true); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }, + }); + svgroot.append(elem); + assert.equal(getPathDFromElement(elem), 'M0,1L5,6'); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'circle', + attr: { id: 'circle', cx: '10', cy: '11', rx: '5', ry: '10' }, + }); + svgroot.append(elem); + assert.equal( + getPathDFromElement(elem), + 'M5,11 C5,5.475138121546961 7.237569060773481,1 10,1 C12.762430939226519,1 15,5.475138121546961 15,11 C15,16.524861878453038 12.762430939226519,21 10,21 C7.237569060773481,21 5,16.524861878453038 5,11 Z' + ); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'polyline', + attr: { id: 'polyline', points: '0,1 5,1 5,11 0,11' }, + }); + svgroot.append(elem); + assert.equal(getPathDFromElement(elem), 'M0,1 5,1 5,11 0,11'); + elem.remove(); + + assert.equal( + getPathDFromElement({ tagName: 'something unknown' }), + undefined + ); + }); + + test('Test getBBoxOfElementAsPath', async function ({ page }) { + /** + * Wrap `utilities.getBBoxOfElementAsPath` to convert bbox to object for testing. + * @type {module:utilities.getBBoxOfElementAsPath} + */ + function getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions) { + const bbox = utilities.getBBoxOfElementAsPath( + elem, + addSVGElementsFromJson, + pathActions + ); + return utilities.bboxToObj(bbox); // need this for assert.equal() to work. + } + + let elem = mockCreateSVGElement({ + element: 'path', + attr: { id: 'path', d: 'M0,1 Z' }, + }); + svgroot.append(elem); + let bbox = getBBoxOfElementAsPath( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 0, height: 0 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + svgroot.append(elem); + bbox = getBBoxOfElementAsPath( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 }); + elem.remove(); + + elem = mockCreateSVGElement({ + element: 'line', + attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }, + }); + svgroot.append(elem); + bbox = getBBoxOfElementAsPath( + elem, + mockaddSVGElementsFromJson, + mockPathActions + ); + assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 }); + elem.remove(); + + // TODO: test element with transform. Need resetOrientation above to be working or mock it. + }); + + test('Test convertToPath rect', async function ({ page }) { + const { convertToPath } = utilities; + const attrs = { + fill: 'red', + stroke: 'white', + 'stroke-width': '1', + visibility: 'hidden', + }; + + const elem = mockCreateSVGElement({ + element: 'rect', + attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }, + }); + svgroot.append(elem); + const path = convertToPath(elem, attrs, mockSvgCanvas); + assert.equal(path.getAttribute('d'), 'M0,1 L5,1 L5,11 L0,11 L0,1 Z'); + assert.equal(path.getAttribute('visibilituy'), null); + assert.equal(path.id, 'rect'); + assert.equal(path.parentNode, svgroot); + assert.equal(elem.parentNode, null); + assert.equal(mockHistorySubCommands.length, 2); + assert.equal(mockCount.clearSelection, 1); + assert.equal(mockCount.addToSelection, 1); + assert.equal(mockCount.addCommandToHistory, 1); + path.remove(); + }); + + test('Test convertToPath unknown element', async function ({ page }) { + const { convertToPath } = utilities; + const attrs = { + fill: 'red', + stroke: 'white', + 'stroke-width': '1', + visibility: 'hidden', + }; + + const elem = { + tagName: 'something unknown', + id: 'something-unknown', + getAttribute() { + return ''; + }, + parentNode: svgroot, + }; + const path = convertToPath(elem, attrs, mockSvgCanvas); + assert.equal(path, null); + assert.equal(elem.parentNode, svgroot); + assert.equal(mockHistorySubCommands.length, 0); + assert.equal(mockCount.clearSelection, 0); + assert.equal(mockCount.addToSelection, 0); + assert.equal(mockCount.addCommandToHistory, 0); + }); +}); diff --git a/golden/5-svgedit-unit/zoom.spec.ts b/golden/5-svgedit-unit/zoom.spec.ts new file mode 100644 index 0000000..7345ac0 --- /dev/null +++ b/golden/5-svgedit-unit/zoom.spec.ts @@ -0,0 +1,167 @@ +import { test, expect } from '@playwright/test'; + +import { visitAndApproveStorage } from '../../support/ui-test-helper.js'; + +test.describe('UI - Zoom tool', function () { + test.beforeEach(async ({ page }) => { + visitAndApproveStorage(); + }); + + test('should be able to open', async function ({ page }) { + await page.locator('#tool-wrapper > input').click(); + await expect(page.locator('#zoom').locator('#options-container')).toHaveCSS( + 'display', + 'flex' + ); + }); + + test('should be able to close', async function ({ page }) { + await page.locator('#tool_select').click(); + await expect(page.locator('#zoom').locator('#options-container')).toHaveCSS( + 'display', + 'none' + ); + }); + + test('should be able to input zoom level', async function ({ page }) { + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('input').fill('200'); + await width.locator('#tool_select').click(); + await expect(width.locator('#canvasBackground')).toHaveAttribute( + 'width', + (width * 2).toString() + ); + }); + + test('should be able to increment zoom level', async function ({ page }) { + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#arrow-up').click(); + await expect(width.locator('#canvasBackground')).toHaveAttribute( + 'width', + (width * 1.1).toString() + ); + }); + + test('should be able to decrement zoom level', async function ({ page }) { + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#arrow-down').click(); + await expect(width.locator('#canvasBackground')).toHaveAttribute( + 'width', + (width * 0.9).toString() + ); + }); + + test('should be able to select from popup', async function ({ page }) { + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#zoom').click(); + await width.locator('se-text').first().click(); + await width.locator('se-text').first().FIXME_invoke('attr', 'value'); + const value = width.locator('se-text').first(); + await expect(value.locator('#canvasBackground')).toHaveAttribute( + 'width', + (width * (value / 100)).toString() + ); + }); + + test('should be able to resize to fit the current selection', async function ({ + page, + }) { + await page.locator('#tool_path').click(); + await page.locator('#svgcontent').down(50, 50); + await page.mouse.up(); + await page.mouse.move(100, 50); + await page.mouse.down(100, 50); + await page.mouse.up(); + await page.mouse.move(75, 150); + await page.mouse.down(75, 150); + await page.mouse.up(); + await page.mouse.move(0, 0); + await page.mouse.down(0, 0); + await page.mouse.up(); + await page.locator('#tool_select').click(); + await page.locator('#tool_select').down(50, 50); + await page.mouse.move(100, 50); + await page.mouse.up(); + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#zoom').click(); + await width.locator("se-text[value='layer']").click(); + await width.locator('#zoom').FIXME_invoke('attr', 'value'); + const value = width.locator('#zoom'); + await expect(value.locator('#canvasBackground')).not.toHaveAttribute( + 'width', + '100' + ); + }); + + test('should be able to resize to fit the canvas', async function ({ page }) { + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#zoom').click(); + await width.locator("se-text[value='canvas']").click(); + await width.locator('#zoom').FIXME_invoke('attr', 'value'); + const value = width.locator('#zoom'); + await expect(value.locator('#canvasBackground')).not.toHaveAttribute( + 'width', + '100' + ); + }); + + test('should be able to resize to fit the current layer', async function ({ + page, + }) { + await page.locator('#tool_path').click(); + await page.locator('#svgcontent').down(50, 50); + await page.mouse.up(); + await page.mouse.move(100, 50); + await page.mouse.down(100, 50); + await page.mouse.up(); + await page.mouse.move(75, 150); + await page.mouse.down(75, 150); + await page.mouse.up(); + await page.mouse.move(0, 0); + await page.mouse.down(0, 0); + await page.mouse.up(); + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#zoom').click(); + await width.locator("se-text[value='layer']").click(); + await width.locator('#zoom').FIXME_invoke('attr', 'value'); + const value = width.locator('#zoom'); + await expect(value.locator('#canvasBackground')).not.toHaveAttribute( + 'width', + '100' + ); + }); + + test('should be able to resize to fit the current content', async function ({ + page, + }) { + await page.locator('#tool_path').click(); + await page.locator('#svgcontent').down(50, 50); + await page.mouse.up(); + await page.mouse.move(100, 50); + await page.mouse.down(100, 50); + await page.mouse.up(); + await page.mouse.move(75, 150); + await page.mouse.down(75, 150); + await page.mouse.up(); + await page.mouse.move(0, 0); + await page.mouse.down(0, 0); + await page.mouse.up(); + await page.locator('#canvasBackground').FIXME_invoke('attr', 'width'); + const width = page.locator('#canvasBackground'); + await width.locator('#zoom').click(); + await width.locator("se-text[value='content']").click(); + await width.locator('#zoom').FIXME_invoke('attr', 'value'); + const value = width.locator('#zoom'); + await expect(value.locator('#canvasBackground')).not.toHaveAttribute( + 'width', + '100' + ); + }); +}); diff --git a/src/mapCy.ts b/src/mapCy.ts index d4fb19c..970805c 100644 --- a/src/mapCy.ts +++ b/src/mapCy.ts @@ -379,6 +379,10 @@ export const createCyMapping = (api: BabelAPI) => { } eq(subject: Subject, args: t.Expression[]): ReturnValue { + if (t.isNumericLiteral(args[0]) && args[0].value === 0) + return wrap(subject.chain('first', [], SubjectType.Locator)); + if (t.isNumericLiteral(args[0]) && args[0].value === -1) + return wrap(subject.chain('last', [], SubjectType.Locator)); return wrap(subject.chain('nth', args, SubjectType.Locator)); } @@ -530,6 +534,10 @@ export const createCyMapping = (api: BabelAPI) => { return returnAsyncFixme(subject, 'select', args); } + shadow(subject: Subject): ReturnValue { + return wrap(subject); + } + should(subject: Subject, args: t.Expression[], context: Context): ReturnValue { if (t.isFunctionExpression(args[0]) || t.isArrowFunctionExpression(args[0])) return this._then('should', subject, args, context); @@ -712,10 +720,17 @@ export const createCyMapping = (api: BabelAPI) => { } trigger(subject: Subject, args: t.Expression[]): ReturnValue { - // It is unsafe to chain to trigger, hence re-root. + const options = utils.callOptions(args, undefined, ['force']); + if (utils.isStringLiteralEqual(args[0], ['mousedown', 'mouseup', 'mousemove'])) { + // .trigger('mousedown', x, y, { ...options }) + const pageMouse = pageSubject.member('mouse', SubjectType.Mouse); + const event = args[0].value; + const suffix = event.substring('mouse'.length); + return wrap(pageMouse, subject.callAsync(suffix, args.length > 2 ? [args[1], args[2]] : [])); + } return wrap( - pageSubject, - subject.callAsync('dispatchEvent', args)); + subject, + subject.callAsync('dispatchEvent', [args[0], ...options])); } type(subject: Subject, args: t.Expression[]): ReturnValue { @@ -749,13 +764,13 @@ export const createCyMapping = (api: BabelAPI) => { } } - const options = utils.callOptions(args, ['force', 'timeout', 'delay']); - const optionsWithoutDelay = utils.callOptions(args, ['force', 'timeout']); - const effectiveMethod = utils.hasCallOption(args, 'delay') ? 'type' : 'fill'; + const options = utils.callOptions(args, ['timeout', 'delay']); + const effectiveMethod = utils.hasCallOption(args, 'delay') ? 'pressSequentially' : 'fill'; if (!tokens.find(t => !!t.key)) return wrap(subject, subject.callAsync(effectiveMethod, [args[0], ...options])); + const optionsWithoutDelay = utils.callOptions(args, ['timeout']); const pageKeyboard = pageSubject.member('keyboard', SubjectType.Keyboard); const modifiers: string[] = []; const result: t.Expression[] = []; @@ -868,8 +883,10 @@ export const createCyMapping = (api: BabelAPI) => { wait(subject: Subject, args: t.Expression[], context: Context): ReturnValue { const [arg1, arg2] = args; - if (arg1 && t.isNumericLiteral(arg1) && !arg2) - return wrap(subject, subject.callAsync('waitForTimeout', args)); + if (arg1 && t.isNumericLiteral(arg1) && !arg2) { + // Erase all the wait for timeouts. + return wrap(subject); + } if (arg1 && t.isStringLiteral(arg1) && arg1.value.startsWith('@')) { const alias = arg1.value.substring(1); const aliasType = context.aliasTypes.get(alias); @@ -988,7 +1005,7 @@ export const createCyMapping = (api: BabelAPI) => { t.arrayExpression(modifiers))); } - properties.push(...utils.callOptionProperties(args, ['force', 'timeout'])); + properties.push(...utils.callOptionProperties(args, ['timeout'])); multiple = utils.getBooleanProperty(clickOptions, 'multiple'); } @@ -1006,7 +1023,6 @@ export const createCyMapping = (api: BabelAPI) => { if (position) properties.push(this._positionProperty(subject, position as any)); - const clickExpression = subject.callAsync(kind === 'dblclick' ? 'dblclick' : 'click', properties.length ? [t.objectExpression(properties)] : []); if (position) { return wrap(subject, t.blockStatement([ @@ -1018,7 +1034,7 @@ export const createCyMapping = (api: BabelAPI) => { } _check(kind: 'check' | 'uncheck', subject: Subject, args: t.Expression[]): ReturnValue { - const options = utils.callOptions(args, ['force', 'timeout']); + const options = utils.callOptions(args, ['timeout']); const checkCall = (value: t.StringLiteral) => { const additionalSelector = `input[value="${value.value}"]:scope`; const locatorWithValue = subject.chain('locator', [t.stringLiteral(additionalSelector)], SubjectType.Locator); diff --git a/src/mapping.ts b/src/mapping.ts index b1e8015..e435989 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -29,6 +29,7 @@ export enum SubjectType { Response = 'Response', Value = 'Value', Keyboard = 'Keyboard', + Mouse = 'Mouse', Void = 'Void', } diff --git a/src/transform.ts b/src/transform.ts index f10286a..b5e6513 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -89,7 +89,7 @@ export const transform = declare((api: BabelAPI) => { return; if (prefixWithTest(path, 'context', false, false)) return; - for (const fixture of ['beforeEach', 'afterEach']) { + for (const fixture of ['before', 'beforeEach', 'afterEach']) { if (prefixWithTest(path, fixture, true, true)) return; } @@ -225,6 +225,10 @@ export const transform = declare((api: BabelAPI) => { callback.async = true; if (fixture === 'context') fixture = 'describe'; + if (fixture === 'before') + fixture = 'beforeAll'; + if (fixture === 'after') + fixture = 'afterAll'; if (addPageFixture) { callback.params = [t.objectExpression([ t.objectProperty(t.identifier('page'), t.identifier('page'), false, true) @@ -359,6 +363,13 @@ export const transform = declare((api: BabelAPI) => { } } + // .type({selectall}).type(...) + if (method === 'type' && expression.arguments.length > 1 && followingMethod === 'type') { + if (utils.isStringLiteralEqual(expression.arguments[0], '{selectall}')) { + // consume the token. + continue; + } + } result.push(entry); } return result; diff --git a/src/utils.ts b/src/utils.ts index df6ff24..f6cf965 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -42,6 +42,22 @@ export const createUtils = (api: BabelAPI) => { return property as t.ObjectProperty; } + getProperties(arg: t.ObjectExpression, names: string[] | undefined, exclude?: string[]): t.ObjectProperty[] { + const result: t.ObjectProperty[] = []; + for (const property of arg.properties) { + if (!t.isObjectProperty(property)) + continue; + if (!t.isIdentifier(property.key)) + continue; + if (exclude?.includes(property.key.name)) + continue; + if (names && !names.includes(property.key.name)) + continue; + result.push(property); + } + return result; + } + getMethod(arg: t.ObjectExpression, name: string) { const property = arg.properties.find(p => t.isObjectMethod(p) && t.isIdentifier(p.key, { name })); return (property as t.ObjectMethod); @@ -62,11 +78,11 @@ export const createUtils = (api: BabelAPI) => { return node; } - callOptionProperties(args: Arg[], names: string[]): t.ObjectProperty[] { + callOptionProperties(args: Arg[], names: string[] | undefined, exclude?: string[]): t.ObjectProperty[] { const optionsArg = args[args.length - 1]; if (!t.isObjectExpression(optionsArg)) return []; - return names.map(n => this.getProperty(optionsArg, n)).filter(Boolean); + return this.getProperties(optionsArg, names, exclude); } callOptionPropertyValue(args: Arg[], name: string): t.Expression | undefined { @@ -77,8 +93,8 @@ export const createUtils = (api: BabelAPI) => { return !!this.callOptionProperties(args, [name]).length; } - callOptions(args: Arg[], names: string[]): t.ObjectExpression[] { - const properties = this.callOptionProperties(args, names); + callOptions(args: Arg[], names: string[] | undefined, exclude?: string[]): t.ObjectExpression[] { + const properties = this.callOptionProperties(args, names, exclude); return properties.length ? [t.objectExpression(properties)] : []; } @@ -93,8 +109,9 @@ export const createUtils = (api: BabelAPI) => { return true; } - isStringLiteralEqual(node: t.Node, value: string): boolean { - return t.isStringLiteral(node) && node.value === value; + isStringLiteralEqual(node: t.Node, value: string | string[]): node is t.StringLiteral { + const values = Array.isArray(value) ? value : [value]; + return t.isStringLiteral(node) && values.includes(node.value); } escapeForRegex(text: string) {