From 4551d992e933434cc72aaac7d646a7340f32b11f Mon Sep 17 00:00:00 2001 From: 1world1dream <1059037014@qq.com> Date: Tue, 25 Apr 2023 19:09:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20fix=20execution=20bug=20in=20xml=20?= =?UTF-8?q?=E2=87=84=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.js | 194 ++++++++++ package.json | 3 +- .../extension/__test__/bpmn-adapter.test.js | 334 ++++++++++++++---- packages/extension/src/bpmn-adapter/index.ts | 67 ++-- .../extension/src/bpmn-adapter/json2xml.ts | 103 ++++-- .../extension/src/bpmn-adapter/xml2json.ts | 13 +- 6 files changed, 577 insertions(+), 137 deletions(-) create mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..13a45d97e --- /dev/null +++ b/jest.config.js @@ -0,0 +1,194 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/en/configuration.html + */ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/xw/v6yjfmzn2hvglj9fm0cb4xbm0000ks/T/jest_gp", + + // Automatically clear mock calls and instances between every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-jsdom", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/package.json b/package.json index fad797271..db8a89e91 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "docs:build": "vuepress build docs", "watch": "lerna watch -- lerna run build --since --ignore examples", "watch:core": "lerna watch --scope @logicflow/core -- lerna run build --scope @logicflow/core", - "watch:extension": "lerna watch --scope @logicflow/extension -- lerna run build --scope @logicflow/extension" + "watch:extension": "lerna watch --scope @logicflow/extension -- lerna run build --scope @logicflow/extension", + "test": "jest" }, "dependencies": {}, "devDependencies": { diff --git a/packages/extension/__test__/bpmn-adapter.test.js b/packages/extension/__test__/bpmn-adapter.test.js index 004e30e00..5fe39365a 100644 --- a/packages/extension/__test__/bpmn-adapter.test.js +++ b/packages/extension/__test__/bpmn-adapter.test.js @@ -1,92 +1,284 @@ import { toXmlJson, toNormalJson } from '../src/bpmn-adapter/index'; import { lfJson2Xml } from '../src/bpmn-adapter/json2xml'; +import { lfXml2Json } from '../src/bpmn-adapter/xml2json'; +/** + * @jest-environment jsdom + */ test('transform data from json to xml', () => { - const json = - '{ "bpmn2:extensionElements": { "flowable:taskListener": [ { "event": "create", "flowable:field": { "name": "taskType", "stringValue": "Execute", "#cdata-section": "function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}" } }, { "event": "end", "flowable:field": { "name": "taskType", "stringValue": "Execute", "#text": "test" } } ] } }'; - const normalJson = JSON.parse(json); - expect(toXmlJson(normalJson)).toStrictEqual({ - 'bpmn2:extensionElements': { - 'flowable:taskListener': [ - { - '-event': 'create', - 'flowable:field': { - '-name': 'taskType', - '-stringValue': 'Execute', - '#cdata-section': - 'function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}', - }, + // const json = + // '{ "bpmn2:extensionElements": { "flowable:taskListener": [ { "event": "create", "flowable:field": { "name": "taskType", "stringValue": "Execute", "#cdata-section": "function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}" } }, { "event": "end", "flowable:field": { "name": "taskType", "stringValue": "Execute", "#text": "test" } } ] } }'; + // const normalJson = JSON.parse(json); + // expect(toXmlJson(normalJson)).toStrictEqual({ + // 'bpmn2:extensionElements': { + // 'flowable:taskListener': [ + // { + // '-event': 'create', + // 'flowable:field': { + // '-name': 'taskType', + // '-stringValue': 'Execute', + // '#cdata-section': + // 'function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}', + // }, + // }, + // { + // '-event': 'end', + // 'flowable:field': { + // '-name': 'taskType', + // '-stringValue': 'Execute', + // '#text': 'test', + // }, + // }, + // ], + // }, + // }); + + // const xmlJson = { + // 'bpmn2:extensionElements': { + // 'flowable:taskListener': [ + // { + // '-event': 'create', + // 'flowable:field': { + // '-name': 'taskType', + // '-stringValue': 'Execute', + // '#cdata-section': + // 'function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}', + // }, + // }, + // { + // '-event': 'end', + // 'flowable:field': { + // '-name': 'taskType', + // '-stringValue': 'Execute', + // '#text': 'test', + // }, + // }, + // ], + // }, + // }; + // expect(toNormalJson(xmlJson)).toStrictEqual({ + // 'bpmn2:extensionElements': { + // 'flowable:taskListener': [ + // { + // event: 'create', + // 'flowable:field': { + // name: 'taskType', + // stringValue: 'Execute', + // '#cdata-section': + // 'function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}', + // }, + // }, + // { + // event: 'end', + // 'flowable:field': { + // name: 'taskType', + // stringValue: 'Execute', + // '#text': 'test', + // }, + // }, + // ], + // }, + // }); + + // expect(lfJson2Xml(toXmlJson(normalJson))) + // .toStrictEqual(` + // + // + // + // + // + // + // + // test + // + // + // `); + const testObj = { + nodes: [ + { id: 'node_1', type: 'rect', x: 100, y: 200, properties: {} }, + { id: 'node_2', type: 'circle', x: 300, y: 160, properties: {} }, + ], + edges: [ + { + id: 'ca1dea84-c5e8-4344-b888-8bb09666ac42', + type: 'polyline', + sourceNodeId: 'node_1', + targetNodeId: 'node_2', + startPoint: { x: 150, y: 200 }, + endPoint: { x: 250, y: 160 }, + properties: {}, + pointsList: [ + { x: 150, y: 200 }, + { x: 200, y: 200 }, + { x: 200, y: 160 }, + { x: 250, y: 160 }, + ], + }, + ], + }; + expect(toXmlJson()(testObj)).toStrictEqual({ + nodes: [ + { + '-id': 'node_1', + '-type': 'rect', + '-x': 100, + '-y': 200, + '-properties': {}, + }, + { + '-id': 'node_2', + '-type': 'circle', + '-x': 300, + '-y': 160, + '-properties': {}, + }, + ], + edges: [ + { + '-id': 'ca1dea84-c5e8-4344-b888-8bb09666ac42', + '-type': 'polyline', + '-sourceNodeId': 'node_1', + '-targetNodeId': 'node_2', + '-startPoint': { + '-x': 150, + '-y': 200, }, - { - '-event': 'end', - 'flowable:field': { - '-name': 'taskType', - '-stringValue': 'Execute', - '#text': 'test', + '-endPoint': { + '-x': 250, + '-y': 160, + }, + '-properties': {}, + '-pointsList': [ + { + '-x': 150, + '-y': 200, + }, + { + '-x': 200, + '-y': 200, }, + { + '-x': 200, + '-y': 160, + }, + { + '-x': 250, + '-y': 160, + }, + ], + }, + ], + }); + + const xmlJson = toXmlJson()(testObj); + expect(toNormalJson(xmlJson)).toStrictEqual({ + nodes: [ + { + id: 'node_1', + type: 'rect', + x: 100, + y: 200, + properties: {}, + }, + { + id: 'node_2', + type: 'circle', + x: 300, + y: 160, + properties: {}, + }, + ], + edges: [ + { + id: 'ca1dea84-c5e8-4344-b888-8bb09666ac42', + type: 'polyline', + sourceNodeId: 'node_1', + targetNodeId: 'node_2', + startPoint: { + x: 150, + y: 200, }, - ], - }, + endPoint: { + x: 250, + y: 160, + }, + properties: {}, + pointsList: [ + { + x: 150, + y: 200, + }, + { + x: 200, + y: 200, + }, + { + x: 200, + y: 160, + }, + { + x: 250, + y: 160, + }, + ], + }, + ], }); - const xmlJson = { - 'bpmn2:extensionElements': { - 'flowable:taskListener': [ + expect(lfJson2Xml(xmlJson)).toStrictEqual( + '\t\n\t\n \t\n \t\n \t\n', + ); + + const xml = lfJson2Xml(xmlJson); + expect(lfXml2Json(xml)).toStrictEqual({ + nodes: [ + { + '-id': 'node_1', + '-type': 'rect', + '-x': 100, + '-y': 200, + '-properties': {}, + }, + { + '-id': 'node_2', + '-type': 'circle', + '-x': 300, + '-y': 160, + '-properties': {}, + }, + ], + edges: { + '-id': 'ca1dea84-c5e8-4344-b888-8bb09666ac42', + '-type': 'polyline', + '-sourceNodeId': 'node_1', + '-targetNodeId': 'node_2', + '-startPoint': { + x: 150, + y: 200, + }, + '-endPoint': { + x: 250, + y: 160, + }, + '-properties': {}, + '-pointsList': [ { - '-event': 'create', - 'flowable:field': { - '-name': 'taskType', - '-stringValue': 'Execute', - '#cdata-section': - 'function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}', - }, + x: 150, + y: 200, }, { - '-event': 'end', - 'flowable:field': { - '-name': 'taskType', - '-stringValue': 'Execute', - '#text': 'test', - }, + x: 200, + y: 200, }, - ], - }, - }; - expect(toNormalJson(xmlJson)).toStrictEqual({ - 'bpmn2:extensionElements': { - 'flowable:taskListener': [ { - event: 'create', - 'flowable:field': { - name: 'taskType', - stringValue: 'Execute', - '#cdata-section': - 'function matchwo(a,b){if (a < b && a < 0) then{return 1;}else{return 0;}}', - }, + x: 200, + y: 160, }, { - event: 'end', - 'flowable:field': { - name: 'taskType', - stringValue: 'Execute', - '#text': 'test', - }, + x: 250, + y: 160, }, ], }, }); - - expect(lfJson2Xml(toXmlJson(normalJson))) - .toStrictEqual(` - - - - - - - - test - - -`); }); diff --git a/packages/extension/src/bpmn-adapter/index.ts b/packages/extension/src/bpmn-adapter/index.ts index 6b153fd99..2d19b158a 100644 --- a/packages/extension/src/bpmn-adapter/index.ts +++ b/packages/extension/src/bpmn-adapter/index.ts @@ -1,5 +1,5 @@ import { getBpmnId } from './bpmnIds'; -import { lfJson2Xml } from './json2xml'; +import { handleAttributes, lfJson2Xml } from './json2xml'; import { lfXml2Json } from './xml2json'; import { @@ -73,44 +73,55 @@ const defaultAttrs = [ * xmlJson中property会以“-”开头 * 如果没有“-”表示为子节点 * fix issue https://github.com/didi/LogicFlow/issues/718, contain the process of #text/#cdata and array + * @param retainedFields retainedField会和默认的defaultRetainedFields: + * ["properties", "startPoint", "endPoint", "pointsList"]合并 + * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性 * @reference node type reference https://www.w3schools.com/xml/dom_nodetype.asp */ +const defaultRetainedFields = ['properties', 'startPoint', 'endPoint', 'pointsList']; -function toXmlJson(json: string | any[] | Object) { - const xmlJson = {}; - if (typeof json === 'string') { - return json; - } - if (Array.isArray(json)) { - return json.map(j => toXmlJson(j)); - } - Object.entries(json).forEach(([key, value]) => { - if (typeof value !== 'object') { - // node type reference https://www.w3schools.com/xml/dom_nodetype.asp - if (key.indexOf('-') === 0 || ['#text', '#cdata-section', '#comment'].includes(key)) { - xmlJson[key] = value; - } else { - xmlJson[`-${key}`] = value; +function toXmlJson(retainedFields: string[]) { + return (json: string | any[] | Object) => { + function ToXmlJson(obj: string | any[] | Object) { + const xmlJson = {}; + if (typeof obj === 'string') { + return obj; } - } else { - xmlJson[key] = toXmlJson(value); + if (Array.isArray(obj)) { + return obj.map((j) => ToXmlJson(j)); + } + Object.entries(obj).forEach(([key, value]) => { + if (typeof value !== 'object') { + // node type reference https://www.w3schools.com/xml/dom_nodetype.asp + if ( + key.indexOf('-') === 0 + || ['#text', '#cdata-section', '#comment'].includes(key) + ) { + xmlJson[key] = value; + } else { + xmlJson[`-${key}`] = value; + } + } else if (defaultRetainedFields.concat(retainedFields).includes(key)) { + xmlJson[`-${key}`] = ToXmlJson(value); + } else { + xmlJson[key] = ToXmlJson(value); + } + }); + return xmlJson; } - }); - return xmlJson; + return ToXmlJson(json); + }; } - /** * 将xmlJson转换为普通的json,在内部使用。 */ -function toNormalJson(xmlJson: Object) { +function toNormalJson(xmlJson) { const json = {}; Object.entries(xmlJson).forEach(([key, value]) => { - if (typeof value === 'string') { - if (key.indexOf('-') === 0) { - json[key.substring(1)] = value; - } else { - json[key] = value; - } + if (key.indexOf('-') === 0) { + json[key.substring(1)] = handleAttributes(value); + } else if (typeof value === 'string') { + json[key] = value; } else if (Object.prototype.toString.call(value) === '[object Object]') { json[key] = toNormalJson(value); } else if (Array.isArray(value)) { diff --git a/packages/extension/src/bpmn-adapter/json2xml.ts b/packages/extension/src/bpmn-adapter/json2xml.ts index 25bfd0ff8..83b4836ab 100644 --- a/packages/extension/src/bpmn-adapter/json2xml.ts +++ b/packages/extension/src/bpmn-adapter/json2xml.ts @@ -1,55 +1,92 @@ -function type(obj: any) { +function type(obj) { return Object.prototype.toString.call(obj); } -function addSpace(depth: number) { - return ' '.repeat(depth); +function addSpace(depth) { + return " ".repeat(depth); } -const tn = '\t\n'; +function handleAttributes(o:any) { + let t = o; + if (type(o) === "[object Object]") { + t = {}; + Object.keys(o).forEach((k) => { + let tk = k; + if (k.charAt(0) === "-") { + tk = k.substring(1); + } + t[tk] = handleAttributes(o[k]); + }); + } else if (Array.isArray(o)) { + t = []; + o.forEach((item, index) => { + t[index] = handleAttributes(item); + }); + } + return t; +}; + +function getAttributes(obj: any) { + let tmp = obj; + try { + if (typeof tmp !== "string") { + tmp = JSON.parse(obj); + } + } catch (error) { + + tmp = JSON.stringify(handleAttributes(obj)).replace(/"/g, "'"); + } + return tmp; +} + +const tn = "\t\n"; // @see issue https://github.com/didi/LogicFlow/issues/718, refactoring of function toXml function toXml(obj: string | any[] | Object, name: string, depth: number) { const frontSpace = addSpace(depth); - if (name === '#text') { + let str = ""; + if (name === "#text") { return tn + frontSpace + obj; - } else if (name === '#cdata-section') { - return tn + frontSpace + ''; - } else if (name === '#comment') { - return tn + frontSpace + ''; - } else if (`${name}`.charAt(0) === '-') { - return ' ' + name.substring(1) + '="' + obj.toString() + '"'; + } else if (name === "#cdata-section") { + return tn + frontSpace + ""; + } else if (name === "#comment") { + return tn + frontSpace + ""; } - let str = ''; - if (Array.isArray(obj)) { - obj.forEach((item) => { - str += toXml(item, name, depth + 1); - }); - } else if (type(obj) === '[object Object]') { - const keys = Object.keys(obj); - let attributes = ''; - let children = ''; - str += (depth === 0 ? '' : tn + frontSpace) + '<' + name; - keys.forEach((k) => { - k.charAt(0) === '-' - ? (attributes += toXml(obj[k], k, depth + 1)) - : (children += toXml(obj[k], k, depth + 1)); - }); - str += - attributes + - (children !== '' ? `>${children}${tn + frontSpace}` : ' />'); + if (`${name}`.charAt(0) === "-") { + return " " + name.substring(1) + '="' + getAttributes(obj) + '"'; } else { - str += tn + frontSpace + `<$${name}>${obj.toString()}`; + if (Array.isArray(obj)) { + obj.forEach((item) => { + str += toXml(item, name, depth + 1); + }); + } else if (type(obj) === "[object Object]") { + const keys = Object.keys(obj); + let attributes = ""; + let children = ""; + str += (depth === 0 ? "" : tn + frontSpace) + "<" + name; + keys.forEach((k) => { + k.charAt(0) === "-" + ? (attributes += toXml(obj[k], k, depth + 1)) + : (children += toXml(obj[k], k, depth + 1)); + }); + str += + attributes + + (children !== "" ? `>${children}${tn + frontSpace}` : " />"); + } else { + str += tn + frontSpace + `<$${name}>${obj.toString()}`; + } } + return str; } function lfJson2Xml(o: Object) { - let xmlStr = ''; + let xmlStr = "\t\n"; for (var m in o) { xmlStr += toXml(o[m], m, 0); } - return xmlStr; + return xmlStr + "\t\n"; } -export { lfJson2Xml }; +export { lfJson2Xml, handleAttributes }; + diff --git a/packages/extension/src/bpmn-adapter/xml2json.ts b/packages/extension/src/bpmn-adapter/xml2json.ts index ea806f2d7..6ef2b51c9 100644 --- a/packages/extension/src/bpmn-adapter/xml2json.ts +++ b/packages/extension/src/bpmn-adapter/xml2json.ts @@ -106,7 +106,7 @@ XML.ObjTree.prototype.parseDOM = function (root) { tmp[root.nodeName] = json; // root nodeName json = tmp; } - return json; + return json["LogicFlow"]; }; // method: parseElement( element ) @@ -129,16 +129,21 @@ XML.ObjTree.prototype.parseElement = function (elem) { var retVal = null; var cnt = {}; - // parse attributes if (elem.attributes && elem.attributes.length) { retVal = {}; for (var i = 0; i < elem.attributes.length; i++) { var key = elem.attributes[i].nodeName; - if (typeof (key) != "string") continue; + if (typeof key != "string") continue; var val = elem.attributes[i].nodeValue; + try { + val = JSON.parse(elem.attributes[i].nodeValue.replace(/'/g, '"')); + console.log(val); + } catch (error) { + var val = elem.attributes[i].nodeValue; + } if (!val) continue; key = this.attr_prefix + key; - if (typeof (cnt[key]) == "undefined") cnt[key] = 0; + if (typeof cnt[key] == "undefined") cnt[key] = 0; cnt[key]++; this.addNode(retVal, key, cnt[key], val); }