From fc3eb7e1427a41a4590d4a0e857eb655cbb623e6 Mon Sep 17 00:00:00 2001 From: The Jared Wilcurt Date: Mon, 20 Jan 2025 09:48:29 -0500 Subject: [PATCH] Classic Formatter (#87) --- package-lock.json | 61 +---------- package.json | 5 +- src/formatMarkup.js | 6 +- src/formatters/classic.js | 24 +++++ src/formatters/diffable.js | 4 +- src/loadOptions.js | 51 ++++++++- tests/unit/src/formatMarkup.test.js | 19 ++++ tests/unit/src/formatters/classic.test.js | 120 ++++++++++++++++++++++ tests/unit/src/loadOptions.test.js | 33 +++++- types.js | 74 ++++++++----- 10 files changed, 310 insertions(+), 87 deletions(-) create mode 100644 src/formatters/classic.js create mode 100644 tests/unit/src/formatters/classic.test.js diff --git a/package-lock.json b/package-lock.json index b96ee4b..55e173c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "vue3-snapshot-serializer", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vue3-snapshot-serializer", - "version": "2.2.0", + "version": "2.3.0", "license": "MIT", "dependencies": { "cheerio": "^1.0.0", - "htmlparser2": "^10.0.0" + "htmlparser2": "^10.0.0", + "js-beautify": "^1.15.1" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -687,7 +688,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -768,14 +768,12 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true, "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1387,7 +1385,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -1436,7 +1433,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1449,7 +1445,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1491,7 +1486,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/boolbase": { @@ -1638,7 +1632,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1651,14 +1644,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -1684,7 +1675,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -1695,7 +1685,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1836,14 +1825,12 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", - "dev": true, "license": "MIT", "dependencies": { "@one-ini/wasm": "0.1.1", @@ -1862,7 +1849,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1872,7 +1858,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -1888,7 +1873,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/encoding-sniffer": { @@ -2284,7 +2268,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -2316,7 +2299,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -2350,7 +2332,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2360,7 +2341,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -2507,7 +2487,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, "license": "ISC" }, "node_modules/is-extglob": { @@ -2524,7 +2503,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2547,7 +2525,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -2608,7 +2585,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -2624,7 +2600,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", - "dev": true, "license": "MIT", "dependencies": { "config-chain": "^1.1.13", @@ -2646,7 +2621,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -2752,7 +2726,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/magic-string": { @@ -2809,7 +2782,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -2852,7 +2824,6 @@ "version": "7.2.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, "license": "ISC", "dependencies": { "abbrev": "^2.0.0" @@ -2930,7 +2901,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -3010,7 +2980,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3020,7 +2989,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -3100,7 +3068,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, "license": "ISC" }, "node_modules/punycode": { @@ -3169,7 +3136,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3182,7 +3148,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3195,7 +3160,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3212,7 +3176,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -3281,7 +3244,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -3300,7 +3262,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3315,7 +3276,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3325,14 +3285,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3345,7 +3303,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -3362,7 +3319,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3375,7 +3331,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3760,7 +3715,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3803,7 +3757,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -3822,7 +3775,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3840,7 +3792,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3850,14 +3801,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3872,7 +3821,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3885,7 +3833,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 6888b85..a309fa3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vue3-snapshot-serializer", "type": "module", - "version": "2.2.0", + "version": "2.3.0", "description": "Vitest snapshot serializer for Vue 3 components", "main": "index.js", "scripts": { @@ -13,7 +13,8 @@ }, "dependencies": { "cheerio": "^1.0.0", - "htmlparser2": "^10.0.0" + "htmlparser2": "^10.0.0", + "js-beautify": "^1.15.1" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/formatMarkup.js b/src/formatMarkup.js index 1f7541e..fcff63b 100644 --- a/src/formatMarkup.js +++ b/src/formatMarkup.js @@ -6,8 +6,9 @@ * we apply custom formatting based on the global vueSnapshots.formatting settings. */ -import { logger } from './helpers.js'; +import { classicFormatter } from './formatters/classic.js'; import { diffableFormatter } from './formatters/diffable.js'; +import { logger } from './helpers.js'; /** * Applies the usere's supplied formatting function, or uses the built-in @@ -21,6 +22,9 @@ export const formatMarkup = function (markup) { if (globalThis.vueSnapshots.formatter === 'diffable') { markup = diffableFormatter(markup); } + if (globalThis.vueSnapshots.formatter === 'classic') { + markup = classicFormatter(markup); + } if (typeof(globalThis.vueSnapshots.postProcessor) === 'function') { markup = globalThis.vueSnapshots.postProcessor(markup); if (typeof(markup) !== 'string') { diff --git a/src/formatters/classic.js b/src/formatters/classic.js new file mode 100644 index 0000000..23f32f0 --- /dev/null +++ b/src/formatters/classic.js @@ -0,0 +1,24 @@ +// @ts-check + +/** + * @file This formatter is the same used in jest-serializer-vue-tjw. It is meant + * to help people migrate from that libary to this one. + */ + +import beautify from 'js-beautify'; + +/** @typedef {import('../../types.js').CLASSICFORMATTING} CLASSICFORMATTING */ + +/** + * The classic formatter which uses js-beautify.html, exactly matching + * jest-serializer-vue-tjw. + * + * @param {string} markup The markup to format. + * @return {string} The formatted markup. + */ +export const classicFormatter = function (markup) { + /** @type {CLASSICFORMATTING} */ + const formatting = globalThis.vueSnapshots.classicFormatting; + + return beautify.html(markup, formatting); +}; diff --git a/src/formatters/diffable.js b/src/formatters/diffable.js index fda535a..51f8e8a 100644 --- a/src/formatters/diffable.js +++ b/src/formatters/diffable.js @@ -7,8 +7,8 @@ * the global vueSnapshots.formatting settings. */ -/** @typedef {import('../types.js').ASTNODE} ASTNODE */ -/** @typedef {import('../types.js').FORMATTING} FORMATTING */ +/** @typedef {import('../../types.js').ASTNODE} ASTNODE */ +/** @typedef {import('../../types.js').FORMATTING} FORMATTING */ import { ESCAPABLE_RAW_TEXT_ELEMENTS, diff --git a/src/loadOptions.js b/src/loadOptions.js index f966b9e..7cdc7d6 100644 --- a/src/loadOptions.js +++ b/src/loadOptions.js @@ -31,6 +31,11 @@ export const formattingBooleanDefaults = { escapeInnerText: true, selfClosingTag: false }; +const ALLOWED_FORMATTERS = [ + 'classic', + 'diffable', + 'none' +]; const TAGS_WITH_WHITESPACE_PRESERVED_DEFAULTS = ['a', 'pre']; const VOID_ELEMENTS_DEFAULT = 'xhtml'; const ALLOWED_VOID_ELEMENTS = Object.freeze([ @@ -38,6 +43,13 @@ const ALLOWED_VOID_ELEMENTS = Object.freeze([ 'xhtml', 'xml' ]); +const CLASSIC_FORMATTING_INDENT_CHAR_DEFAULT = ' '; +const CLASSIC_FORMATTING_INDENT_INNER_HTML_DEFAULT = true; +const CLASSIC_FORMATTING_INDENT_SIZE_DEFAULT = 2; +const CLASSIC_FORMATTING_INLINE_DEFAULT = []; +const CLASSIC_FORMATTING_SEP_DEFAULT = '\n'; +const CLASSIC_FORMATTING_UNFORMATTED_DEFAULT = ['code', 'pre']; + /** * Loads the default settings if valid settings are not supplied. @@ -87,7 +99,7 @@ export const loadOptions = function () { globalThis.vueSnapshots.attributesToClear = attributesToClear; // Formatter - if (!['none', 'diffable'].includes(globalThis.vueSnapshots.formatter)) { + if (!ALLOWED_FORMATTERS.includes(globalThis.vueSnapshots.formatter)) { if (globalThis.vueSnapshots.formatter) { logger('Allowed values for global.vueSnapshots.formatter are \'none\' and \'diffable\'.'); } @@ -105,6 +117,14 @@ export const loadOptions = function () { logger('When setting the formatter to anything other than \'diffable\', all formatting options are ignored.'); } + if ( + globalThis.vueSnapshots.formatter !== 'classic' && + typeof(globalThis.vueSnapshots.classicFormatting) === 'object' && + Object.keys(globalThis.vueSnapshots.classicFormatting).length + ) { + logger('When setting the formatter to anything other than \'classic\', all classicFormatting options are ignored.'); + } + // Formatting if (globalThis.vueSnapshots.formatter === 'diffable') { if (!globalThis.vueSnapshots.formatting) { @@ -191,6 +211,33 @@ export const loadOptions = function () { delete globalThis.vueSnapshots.formatting; } + // Classic Formatting + if (globalThis.vueSnapshots.formatter === 'classic') { + if (!globalThis.vueSnapshots.classicFormatting) { + globalThis.vueSnapshots.classicFormatting = {}; + } + if (!globalThis.vueSnapshots.classicFormatting.indent_char) { + globalThis.vueSnapshots.classicFormatting.indent_char = CLASSIC_FORMATTING_INDENT_CHAR_DEFAULT; + } + if (typeof(globalThis.vueSnapshots.classicFormatting.indent_inner_html) !== 'boolean') { + globalThis.vueSnapshots.classicFormatting.indent_inner_html = CLASSIC_FORMATTING_INDENT_INNER_HTML_DEFAULT; + } + if (typeof(globalThis.vueSnapshots.classicFormatting.indent_size) !== 'number') { + globalThis.vueSnapshots.classicFormatting.indent_size = CLASSIC_FORMATTING_INDENT_SIZE_DEFAULT; + } + if (!Array.isArray(globalThis.vueSnapshots.classicFormatting.inline)) { + globalThis.vueSnapshots.classicFormatting.inline = CLASSIC_FORMATTING_INLINE_DEFAULT; + } + if (typeof(globalThis.vueSnapshots.classicFormatting.sep) !== 'string') { + globalThis.vueSnapshots.classicFormatting.sep = CLASSIC_FORMATTING_SEP_DEFAULT; + } + if (!Array.isArray(globalThis.vueSnapshots.classicFormatting.unformatted)) { + globalThis.vueSnapshots.classicFormatting.unformatted = CLASSIC_FORMATTING_UNFORMATTED_DEFAULT; + } + } else { + delete globalThis.vueSnapshots.classicFormatting; + } + if (typeof(globalThis.vueSnapshots.postProcessor) !== 'function') { if (globalThis.vueSnapshots.postProcessor) { logger('The postProcessor option must be a function that returns a string, or undefined.'); @@ -198,6 +245,7 @@ export const loadOptions = function () { delete globalThis.vueSnapshots.postProcessor; } + /** * Clean up settings */ @@ -205,6 +253,7 @@ export const loadOptions = function () { const permittedRootKeys = [ ...Object.keys(booleanDefaults), 'attributesToClear', + 'classicFormatting', 'formatter', 'formatting', 'postProcessor' diff --git a/tests/unit/src/formatMarkup.test.js b/tests/unit/src/formatMarkup.test.js index 1ffeec1..3571155 100644 --- a/tests/unit/src/formatMarkup.test.js +++ b/tests/unit/src/formatMarkup.test.js @@ -58,6 +58,25 @@ describe('Format markup', () => { .not.toHaveBeenCalled(); }); + test('Uses classing formatting', () => { + globalThis.vueSnapshots.formatter = 'classic'; + + expect(unformattedMarkup) + .toMatchInlineSnapshot(` + + `); + + expect(console.info) + .not.toHaveBeenCalled(); + }); + test('Applies custom formatting', () => { globalThis.vueSnapshots.postProcessor = function (input) { return input.toUpperCase(); diff --git a/tests/unit/src/formatters/classic.test.js b/tests/unit/src/formatters/classic.test.js new file mode 100644 index 0000000..0ad3bc9 --- /dev/null +++ b/tests/unit/src/formatters/classic.test.js @@ -0,0 +1,120 @@ +import { mount } from '@vue/test-utils'; + +import { classicFormatter } from '@/formatters/classic.js'; + +describe('classicFormatter', () => { + let MyComponent; + + beforeEach(() => { + MyComponent = { template: '' }; + globalThis.vueSnapshots = { + formatter: 'classic', + classicFormatting: {} + }; + }); + + test('No arguments', () => { + expect(classicFormatter()) + .toEqual(''); + }); + + test('Empty attributes', () => { + MyComponent.template = '
Text

'; + + expect(mount(MyComponent)) + .toMatchInlineSnapshot(` +
Text
+

+ `); + }); + + test('Self Closing Tags', () => { + MyComponent.template = '
'; + const wrapper = mount(MyComponent); + + expect(wrapper) + .toMatchInlineSnapshot(` +
+ + + + + + + + + + + `); + }); + + test('Void elements', () => { + const INPUT = ''; + + expect(INPUT) + .toMatchInlineSnapshot(` + + + + `); + }); + + describe('Stubbed components', () => { + test('Fake TR in TBODY fragment', () => { + const markup = ` + + Text + + + `.trim(); + + expect(markup) + .toMatchInlineSnapshot(` + + + Text + + + + `); + }); + + test('Fake TR in normal table', () => { + const markup = ` + + + + + +
Text
+ `.trim(); + + expect(markup) + .toMatchInlineSnapshot(` + + + + + + + +
Text
+ `); + }); + }); + + test('Tags with whitespace preserved', () => { + MyComponent.template = `
Hello World
+ Hello World +
Hello World
`; + + const wrapper = mount(MyComponent); + + expect(wrapper) + .toMatchInlineSnapshot(` +
Hello World
+ Hello World +
Hello World
+ `); + }); +}); diff --git a/tests/unit/src/loadOptions.test.js b/tests/unit/src/loadOptions.test.js index de21a82..9e6dbb4 100644 --- a/tests/unit/src/loadOptions.test.js +++ b/tests/unit/src/loadOptions.test.js @@ -10,7 +10,8 @@ describe('Load options', () => { beforeEach(() => { console.info = vi.fn(); globalThis.vueSnapshots = { - formatting: {} + formatting: {}, + classicFormatting: {} }; }); @@ -365,6 +366,36 @@ describe('Load options', () => { }); }); + describe('Classic formatter', () => { + test('Logs that classic formatting is ignored', () => { + globalThis.vueSnapshots.formatter = 'none'; + globalThis.vueSnapshots.classicFormatting.sep = '/r/n'; + loadOptions(); + + expect(console.info) + .toHaveBeenCalledWith('Vue 3 Snapshot Serializer: When setting the formatter to anything other than \'classic\', all classicFormatting options are ignored.'); + + expect(globalThis.vueSnapshots.classicFormatting) + .toEqual(undefined); + }); + + test('Loads default settings for classic formatter', () => { + globalThis.vueSnapshots.classicFormatting = {}; + globalThis.vueSnapshots.formatter = 'classic'; + loadOptions(); + + expect(globalThis.vueSnapshots.classicFormatting) + .toEqual({ + indent_char: ' ', + indent_inner_html: true, + indent_size: 2, + inline: [], + sep: '\n', + unformatted: ['code', 'pre'] + }); + }); + }); + describe('Diffable Formatter', () => { describe('Preserve whitespace in tags', () => { beforeEach(() => { diff --git a/types.js b/types.js index c6ace14..d981fdd 100644 --- a/types.js +++ b/types.js @@ -11,7 +11,34 @@ * @return {string} Mutated version of the markup string to store in a snapshot. */ -/** @typedef {'none'|'diffable'} FORMATTER */ +/** @typedef {'normal'|'keep'|'separate'} INDENTSCRIPTS */ + +/** @typedef {'auto'|'force'|'force-aligned'|'force-expand-multiline'|'aligned-multiple'|'preserve'|'preserve-aligned'} WRAPATTRIBUTES */ + +/** + * @typedef {object} CLASSICFORMATTING + * @property {string[]} [content_unformatted=['pre','textarea']] Tags where their inner content should skip formatting. + * @property {string[]} [extra_liners=['head','body','/html']] Array of tags that get extra lines. + * @property {string} [indent_char=' '] The character to use for indentation, like a space or tab. + * @property {boolean} [indent_body_inner_html=true] Breaks up inner HTML to multiple lines with indentation for the body tag. + * @property {boolean} [indent_handlebars=true] Applies indentation to handlebars syntax. + * @property {boolean} [indent_head_inner_html=true] Breaks up inner HTML to multiple lines with indentation for the head tag. + * @property {boolean} [indent_inner_html=true] Breaks up inner HTML to multiple lines with indentation. + * @property {INDENTSCRIPTS} [indent_scripts='normal'] How to handle script indentation. + * @property {number} [indent_size=2] How many indentation characters to use. + * @property {string[]} [inline=[]] Array of string tag names to represent inline (vs block) elements. + * @property {boolean} [inline_custom_elements=true] Whether custom elements should be counted as inline. + * @property {string} [sep='\n'] The characters to use for new line separtors. + * @property {string[]} [templating=['auto']] What templating languages to support ('django', 'erb', 'handlebars', 'php') defaults to all. + * @property {string[]} [unformatted=['code','pre']] Array of string tag names to skip formatting for. + * @property {string} [unformatted_content_delimiter] String for delimiting unformated content. + * @property {string[]} [void_elements] Defaults to ['area','base','br','col','embed','hr','img','input','keygen','link','menuitem','meta','param','source','track','wbr','!doctype','?xml','basefont','isindex']. + * @property {WRAPATTRIBUTES} [wrap_attributes='auto'] Settings for attribute alignment. + * @property {number} [wrap_attributes_min_attrs=2] When to wrap attributes. + * @property {number} [wrap_attributes_indent_size] Uses the indent_size if not specified. + */ + +/** @typedef {'diffable'|'classic'|'none'} FORMATTER */ /** @typedef {'html'|'xhtml'|'xml'} VOIDELEMENTS */ @@ -28,28 +55,29 @@ */ /** - * @typedef {object} SETTINGS - * @property {boolean} [verbose=true] Logs to the console errors or other messages if true. - * @property {string[]} [attributesToClear=[]] Takes an array of attribute strings, like `['title', 'id']`, to remove the values from these attributes. `` becomes ``. - * @property {boolean} [addInputValues=true] Display current internal element value on `input`, `textarea`, and `select` fields. `` becomes ``. Requires passing in the VTU `wrapper`, not `wrapper.html()`. - * @property {boolean} [sortAttributes=true] Sorts the attributes inside HTML elements in the snapshot. This greatly reduces snapshot noise, making diffs easier to read. - * @property {boolean} [sortClasses=true] Sorts the classes inside the `class` attribute on all HTML elements in the snapshot. This greatly reduces snapshot noise, making diffs easier to read. - * @property {boolean} [stringifyAttributes=true] Injects the real values of dynamic attributes/props into the snapshot. `to="[object Object]"` becomes `to="{ name: 'home' }"`. Requires passing in the VTU `wrapper`, not `wrapper.html()`. - * @property {boolean} [removeServerRendered=true] Removes `data-server-rendered="true"` from your snapshots if true. - * @property {boolean} [removeDataVId=true] Removes `data-v-1234abcd=""` from your snapshots if true. Useful if 3rd-party components use scoped styles to reduce snapshot noise when updating dependencies. - * @property {boolean} [removeDataTest=true] Removes `data-test="whatever"` from your snapshots if true. - * @property {boolean} [removeDataTestid=true] Removes `data-testid="whatever"` from your snapshots if true. - * @property {boolean} [removeDataTestId=true] Removes `data-test-id="whatever"` from your snapshots if true. - * @property {boolean} [removeDataQa=false] Removes `data-qa="whatever"` from your snapshots if true. `data-qa` is usually used by non-dev QA members. If they change in your snapshot, that indicates it may break someone else's E2E tests. So most using `data-qa` prefer they be left in by default. - * @property {boolean} [removeDataCy=false] Removes `data-cy="whatever"` from your snapshots if true. `data-cy` is used by Cypress end-to-end tests. If they change in your snapshot, that indicates it may break an E2E test. So most using data-cy prefer they be left in by default. - * @property {boolean} [removeDataPw=false] Removes `data-pw="whatever"` from your snapshots if true. `data-pw` is used by Playwright end-to-end tests. If they change in your snapshot, that indicates it may break an E2E test. So most using data-pw prefer they be left in by default. - * @property {boolean} [removeIdTest=false] Removes `id="test-whatever"` or `id="testWhatever"` from snapshots. **Warning:** You should never use ID's for test tokens, as they can also be used by JS and CSS, making them more brittle and their intent less clear. Use `data-test-id` instead. - * @property {boolean} [removeClassTest=false] Removes all CSS classes that start with "test", like `class="test-whatever"`. **Warning:** Don't use this approach. Use `data-test` instead. It is better suited for this because it doesn't conflate CSS and test tokens. - * @property {boolean} [removeComments=false] Removes all HTML comments from your snapshots. This is false by default, as sometimes these comments can infer important information about how your DOM was rendered. However, this is mostly just personal preference. - * @property {boolean} [clearInlineFunctions=false] Replaces `
` or `
` with this placeholder `
`. - * @property {POSTPROCESSOR} [postProcessor] This is a custom function you can pass in. It will be handed a string of formatted markup and must return a string (not a promise). It runs right after the formatter. - * @property {FORMATTER} [formatter='diffable'] Function to use for formatting the markup output. Accepts 'none', 'diffable', or a function. If using a custom function it will be handed a string of markup and must return a string (not a promise). - * @property {FORMATTING} [formatting] An object containing settings specific to the "diffable" formatter. + * @typedef {object} SETTINGS + * @property {boolean} [verbose=true] Logs to the console errors or other messages if true. + * @property {string[]} [attributesToClear=[]] Takes an array of attribute strings, like `['title', 'id']`, to remove the values from these attributes. `` becomes ``. + * @property {boolean} [addInputValues=true] Display current internal element value on `input`, `textarea`, and `select` fields. `` becomes ``. Requires passing in the VTU `wrapper`, not `wrapper.html()`. + * @property {boolean} [sortAttributes=true] Sorts the attributes inside HTML elements in the snapshot. This greatly reduces snapshot noise, making diffs easier to read. + * @property {boolean} [sortClasses=true] Sorts the classes inside the `class` attribute on all HTML elements in the snapshot. This greatly reduces snapshot noise, making diffs easier to read. + * @property {boolean} [stringifyAttributes=true] Injects the real values of dynamic attributes/props into the snapshot. `to="[object Object]"` becomes `to="{ name: 'home' }"`. Requires passing in the VTU `wrapper`, not `wrapper.html()`. + * @property {boolean} [removeServerRendered=true] Removes `data-server-rendered="true"` from your snapshots if true. + * @property {boolean} [removeDataVId=true] Removes `data-v-1234abcd=""` from your snapshots if true. Useful if 3rd-party components use scoped styles to reduce snapshot noise when updating dependencies. + * @property {boolean} [removeDataTest=true] Removes `data-test="whatever"` from your snapshots if true. + * @property {boolean} [removeDataTestid=true] Removes `data-testid="whatever"` from your snapshots if true. + * @property {boolean} [removeDataTestId=true] Removes `data-test-id="whatever"` from your snapshots if true. + * @property {boolean} [removeDataQa=false] Removes `data-qa="whatever"` from your snapshots if true. `data-qa` is usually used by non-dev QA members. If they change in your snapshot, that indicates it may break someone else's E2E tests. So most using `data-qa` prefer they be left in by default. + * @property {boolean} [removeDataCy=false] Removes `data-cy="whatever"` from your snapshots if true. `data-cy` is used by Cypress end-to-end tests. If they change in your snapshot, that indicates it may break an E2E test. So most using data-cy prefer they be left in by default. + * @property {boolean} [removeDataPw=false] Removes `data-pw="whatever"` from your snapshots if true. `data-pw` is used by Playwright end-to-end tests. If they change in your snapshot, that indicates it may break an E2E test. So most using data-pw prefer they be left in by default. + * @property {boolean} [removeIdTest=false] Removes `id="test-whatever"` or `id="testWhatever"` from snapshots. **Warning:** You should never use ID's for test tokens, as they can also be used by JS and CSS, making them more brittle and their intent less clear. Use `data-test-id` instead. + * @property {boolean} [removeClassTest=false] Removes all CSS classes that start with "test", like `class="test-whatever"`. **Warning:** Don't use this approach. Use `data-test` instead. It is better suited for this because it doesn't conflate CSS and test tokens. + * @property {boolean} [removeComments=false] Removes all HTML comments from your snapshots. This is false by default, as sometimes these comments can infer important information about how your DOM was rendered. However, this is mostly just personal preference. + * @property {boolean} [clearInlineFunctions=false] Replaces `
` or `
` with this placeholder `
`. + * @property {POSTPROCESSOR} [postProcessor] This is a custom function you can pass in. It will be handed a string of formatted markup and must return a string (not a promise). It runs right after the formatter. + * @property {FORMATTER} [formatter='diffable'] Function to use for formatting the markup output. Accepts 'none', 'diffable', or 'classic'. + * @property {FORMATTING} [formatting] An object containing settings specific to the "diffable" formatter. + * @property {CLASSICFORMATTING} [classicFormatting] An object containing settings specific to the "classic" formatter. */ /** @typedef {'root'|'tag'|'text'|'comment'|'doctype'|'cdata'|'script'|'style'|'directive'} ASTNODETYPE */