diff --git a/package-lock.json b/package-lock.json index edf0c0ccf22..8812373ebdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1019,7 +1019,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1036,7 +1035,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1053,7 +1051,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1070,7 +1067,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1087,7 +1083,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1104,7 +1099,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1121,7 +1115,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1138,7 +1131,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1155,7 +1147,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1172,7 +1163,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1189,7 +1179,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1206,7 +1195,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1223,7 +1211,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1240,7 +1227,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1257,7 +1243,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1274,7 +1259,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1291,7 +1275,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1308,7 +1291,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1325,7 +1307,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1342,7 +1323,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1359,7 +1339,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1376,7 +1355,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1393,7 +1371,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1410,7 +1387,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1427,7 +1403,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1444,7 +1419,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1565,7 +1539,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1579,7 +1553,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -1588,7 +1562,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -1597,7 +1571,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1607,13 +1581,13 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1839,6 +1813,10 @@ "resolved": "examples/ssr", "link": true }, + "node_modules/@microsoft/fast-test-harness": { + "resolved": "packages/fast-test-harness", + "link": true + }, "node_modules/@microsoft/fast-todo-app-example": { "resolved": "examples/todo-app", "link": true @@ -2252,7 +2230,6 @@ "version": "1.56.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.56.1" @@ -2368,7 +2345,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2382,7 +2358,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2396,7 +2371,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2410,7 +2384,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2424,7 +2397,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2438,7 +2410,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2452,7 +2423,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2466,7 +2436,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2480,7 +2449,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2494,7 +2462,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2508,7 +2475,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2522,7 +2488,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2536,7 +2501,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2550,7 +2514,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2564,7 +2527,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2578,7 +2540,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2592,7 +2553,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2606,7 +2566,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2620,7 +2579,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2634,7 +2592,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2648,7 +2605,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2662,7 +2618,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2676,7 +2631,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2690,7 +2644,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2704,7 +2657,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3034,7 +2986,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -3750,13 +3701,12 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "devOptional": true }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -3868,7 +3818,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3882,7 +3831,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4156,7 +4104,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -4165,7 +4112,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4391,7 +4337,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4513,7 +4458,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4523,7 +4467,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -4532,7 +4475,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4545,7 +4487,6 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -4943,7 +4884,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -5019,7 +4959,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5044,7 +4983,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5211,7 +5149,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5286,7 +5223,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5633,7 +5569,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "engines": { "node": ">= 0.10" } @@ -5760,6 +5695,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-ssh": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", @@ -6069,9 +6010,9 @@ } }, "node_modules/liquidjs": { - "version": "10.25.0", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.25.0.tgz", - "integrity": "sha512-XpO7AiGULTG4xcTlwkcTI5JreFG7b6esLCLp+aUSh7YuQErJZEoUXre9u9rbdb0057pfWG4l0VursvLd5Q/eAw==", + "version": "10.25.5", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.25.5.tgz", + "integrity": "sha512-GKiKeZjJDdVoQAu+S9rzkYsYnYhcep5W3WwZXgb5f+yq484P/k9JqamBbGYu+LBEixcUAXZr2jogdAIjB3ki1w==", "license": "MIT", "dependencies": { "commander": "^10.0.0" @@ -6197,7 +6138,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6489,7 +6429,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -6838,7 +6777,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6858,6 +6796,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -7068,7 +7015,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7087,7 +7033,6 @@ "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.56.1" @@ -7106,7 +7051,6 @@ "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", - "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -7119,7 +7063,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7142,7 +7085,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7273,7 +7215,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -7301,7 +7242,6 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7504,7 +7444,6 @@ "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -7574,6 +7513,55 @@ "node": ">=0.1.90" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7630,8 +7618,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/section-matter": { "version": "1.0.0", @@ -7792,7 +7779,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7812,7 +7798,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7829,7 +7814,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7848,7 +7832,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7960,7 +7943,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7970,7 +7952,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -7980,7 +7962,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -8243,7 +8225,7 @@ "version": "5.31.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -8261,7 +8243,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "devOptional": true }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -8520,7 +8502,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -8529,7 +8510,6 @@ "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -8604,7 +8584,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -8622,7 +8601,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8793,6 +8771,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8956,6 +8940,376 @@ "@microsoft/fast-element": "^2.10.3" } }, + "packages/fast-test-harness": { + "name": "@microsoft/fast-test-harness", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "express": "5.2.1" + }, + "bin": { + "fast-test-harness": "start.mjs" + }, + "devDependencies": { + "@microsoft/fast-build": "*", + "@microsoft/fast-html": "*" + }, + "engines": { + "node": ">=22.18.0" + }, + "peerDependencies": { + "@microsoft/fast-build": ">=0.3.0", + "@microsoft/fast-html": ">= 1.0.0-alpha.46 || ^1.0.0", + "@playwright/test": ">=1.40.0", + "vite": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@microsoft/fast-build": { + "optional": true + }, + "@microsoft/fast-html": { + "optional": true + } + } + }, + "packages/fast-test-harness/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/fast-test-harness/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "packages/fast-test-harness/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/fast-test-harness/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/fast-test-harness/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/fast-test-harness/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/fast-test-harness/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/fast-test-harness/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "packages/fast-test-harness/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "packages/fast-test-harness/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "packages/fast-test-harness/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "packages/fast-test-harness/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/fast-test-harness/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/fast-test-harness/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "sites/website": { "name": "@microsoft/fast-site", "version": "0.6.2", diff --git a/packages/fast-test-harness/README.md b/packages/fast-test-harness/README.md new file mode 100644 index 00000000000..6135bad6979 --- /dev/null +++ b/packages/fast-test-harness/README.md @@ -0,0 +1,218 @@ +# FAST Test Harness + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +The `fast-test-harness` package is a Playwright testing harness for FAST Element web components with CSR and SSR support. + +## Installation + +To install `fast-test-harness` using `npm`: + +```shell +npm install --save-dev @microsoft/fast-test-harness +``` + +## Writing tests + +Import `test` and `expect` from the harness. Configure the component tag name with `test.use()`, then call `fastPage.setTemplate()` in each test to render it. + +```ts +import { expect, test } from "@microsoft/fast-test-harness"; + +test.describe("Button", () => { + test.use({ tagName: "my-button", innerHTML: "Click me" }); + + test("should render", async ({ fastPage }) => { + await fastPage.setTemplate(); + await expect(fastPage.element).toBeVisible(); + }); + + test("should accept attributes", async ({ fastPage }) => { + await fastPage.setTemplate({ + attributes: { appearance: "primary", disabled: true }, + }); + await expect(fastPage.element).toHaveAttribute("appearance", "primary"); + }); + + test("should work inside a form", async ({ fastPage }) => { + await fastPage.setTemplate(` +
+ Submit +
+ `); + await expect(fastPage.element).toBeVisible(); + }); +}); +``` + +Use `updateTemplate()` to modify attributes or innerHTML after the initial render without navigating away from the page: + +```ts +await fastPage.setTemplate(); +await fastPage.updateTemplate(fastPage.element, { attributes: { disabled: true } }); +``` + +The `toHaveCustomState` assertion checks `ElementInternals` custom states: + +```ts +await expect(element).toHaveCustomState("checked"); +``` + +## Fixture options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `tagName` | `string` | `""` | Custom element tag name | +| `innerHTML` | `string` | `""` | Default inner HTML | +| `waitFor` | `string[]` | `[]` | Additional elements to wait for before testing | +| `ssr` | `boolean` | `false` | Use SSR mode (or set `PLAYWRIGHT_TEST_SSR=true`) | + +## Test directory setup + +The harness serves a Vite dev server from a `test/` directory in your project. CSR and SSR modes use different entry points from the same directory. + +``` +test/ +├── index.html # CSR: loads main.ts +├── ssr.html # SSR: template with comment placeholders +├── vite.config.ts # Vite config (shared by both modes) +└── src/ + ├── main.ts # CSR: registers components, applies theme + ├── entry-client.ts # SSR: registers components for hydration + └── entry-server.ts # SSR: exports render() for fixture generation +``` + +### CSR files + +**`index.html`** loads a script that registers your components: + +```html + + + + + + + +``` + +**`main.ts`** registers components and applies global config. The body starts empty; `setTemplate()` injects HTML per test. + +```ts +import "./define-all.js"; +import { setTheme } from "./theme.js"; +setTheme(lightTheme); +``` + +### SSR files + +**`ssr.html`** contains comment placeholders the server fills in per request: + +```html + + + + <!--fixturetitle--> + + + + + + + + +``` + +**`entry-client.ts`** registers components for DSD hydration using `defineAsync`: + +```ts +import { TemplateElement } from "@microsoft/fast-html"; +TemplateElement.define({ name: "f-template" }); + +// Load all define-async modules +const modules = import.meta.glob("../../src/*/define-async.{ts,js}"); +Promise.all(Object.values(modules).map(m => m())); +``` + +**`entry-server.ts`** exports a `render()` function that the server calls for each `setTemplate()` request. It returns three strings that get injected into `ssr.html`: + +```ts +export function render(queryObj: Record): { + template: string; // HTML → + fixture: string; // rendered element HTML → + preloadLinks: string; // tags → +}; +``` + +Each component needs three build artifacts for SSR: an `` (`.template.html`), a DSD template (`.template-dsd.html`), and optionally a stylesheet (`.styles.css`). Use `renderFixture` and `renderTemplate` from the harness to assemble the output: + +```ts +import { readAsset, resolveAssetUrl } from "@microsoft/fast-test-harness/ssr/assets.js"; +import { renderFixture, renderTemplate } from "@microsoft/fast-test-harness/ssr/render.js"; + +const fTemplate = readAsset("@my-scope/button/template.html"); +const dsd = readAsset("@my-scope/button/template-dsd.html"); +const styles = resolveAssetUrl("@my-scope/button/styles.css"); + +export function render(queryObj: Record = {}) { + return { + template: renderTemplate(fTemplate, styles), + fixture: renderFixture(queryObj, dsd, styles), + preloadLinks: "", + }; +} +``` + +## Server + +The package includes an Express + Vite server that handles both CSR page serving and SSR fixture generation. Run it directly or let Playwright manage it via `webServer`: + +```ts +// playwright.config.ts +export default defineConfig({ + webServer: { + command: "fast-test-harness", + port: 5173, + reuseExistingServer: true, + }, +}); +``` + +For custom setup, import `startServer`: + +```ts +import { startServer } from "@microsoft/fast-test-harness/server.mjs"; +await startServer(process.cwd(), "./test", "./test/vite.config.ts"); +``` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `cwd` | `process.cwd()` | Static file serving root | +| `root` | `/test` | Vite root (contains `index.html`, `ssr.html`) | +| `configFile` | `/vite.config.ts` | Vite config path | + +| Environment variable | Default | Description | +|---------------------|---------|-------------| +| `PORT` | `5173` | Server port | +| `BASE` | `/` | Base URL path | +| `PLAYWRIGHT_TEST_SSR` | — | Set `"true"` for SSR mode | + +## Rendering utilities + +**`renderFixture(queryObj, dsdTemplate?, styles?, templateData?, childTemplates?)`** builds the fixture element HTML. Injects the DSD template inside the element when provided. `childTemplates` is a `Record` that injects DSD into nested custom elements found in the innerHTML or raw HTML. + +**`renderTemplate(rawTemplate, styles)`** replaces `{{styles}}` in an f-template HTML string with a `` tag for the given stylesheet URL. + +**`readAsset(specifier)`** reads a file as UTF-8 from a package export path or filesystem path using `import.meta.resolve`. + +**`resolveAssetUrl(specifier, root?)`** resolves a specifier to a server-relative URL path for use in `` tags. + +## Exports + +| Specifier | Contents | +|-----------|----------| +| `@microsoft/fast-test-harness` | `test`, `expect`, `CSRFixture`, `SSRFixture`, `readAsset`, `resolveAssetUrl`, `renderFixture`, `renderTemplate` | +| `@microsoft/fast-test-harness/server.mjs` | `startServer`, `app` | +| `@microsoft/fast-test-harness/ssr/render.js` | `renderFixture`, `renderTemplate`, `renderPreloadLinks` | +| `@microsoft/fast-test-harness/ssr/assets.js` | `readAsset`, `resolveAssetUrl` | +| `@microsoft/fast-test-harness/public/*` | Static assets (base CSS) | diff --git a/packages/fast-test-harness/package.json b/packages/fast-test-harness/package.json new file mode 100644 index 00000000000..c33f405d364 --- /dev/null +++ b/packages/fast-test-harness/package.json @@ -0,0 +1,71 @@ +{ + "name": "@microsoft/fast-test-harness", + "private": true, + "version": "0.0.1", + "author": { + "name": "Microsoft", + "url": "https://discord.gg/FcSNfg4" + }, + "description": "Playwright testing harness for FAST web components", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/fast.git", + "directory": "packages/fast-test-harness" + }, + "bugs": { + "url": "https://github.com/Microsoft/fast/issues/new/choose" + }, + "type": "module", + "bin": { + "fast-test-harness": "./start.mjs" + }, + "exports": { + ".": { + "types": "./dist/dts/index.d.ts", + "test": "./src/index.ts", + "default": "./dist/esm/index.js" + }, + "./vite.config.mjs": { + "default": "./vite.config.mjs" + }, + "./ssr/*.js": { + "types": "./dist/dts/ssr/*.d.ts", + "test": "./src/ssr/*.ts", + "default": "./dist/esm/ssr/*.js" + }, + "./playwright.config.ts": "./playwright.config.ts", + "./public/*": "./public/*", + "./server.mjs": "./server.mjs", + "./start.mjs": "./start.mjs", + "./package.json": "./package.json" + }, + "scripts": { + "build": "npm run build:tsc", + "build:tsc": "tsc -p tsconfig.build.json" + }, + "dependencies": { + "express": "5.2.1" + }, + "devDependencies": { + "@microsoft/fast-html": "*", + "@microsoft/fast-build": "*" + }, + "peerDependencies": { + "@microsoft/fast-build": ">=0.3.0", + "@microsoft/fast-html": ">= 1.0.0-alpha.46 || ^1.0.0", + "@playwright/test": ">=1.40.0", + "vite": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@microsoft/fast-build": { + "optional": true + }, + "@microsoft/fast-html": { + "optional": true + } + }, + "engines": { + "node": ">=22.18.0" + } +} diff --git a/packages/fast-test-harness/playwright.config.ts b/packages/fast-test-harness/playwright.config.ts new file mode 100644 index 00000000000..9e678c9d5eb --- /dev/null +++ b/packages/fast-test-harness/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + retries: 3, + fullyParallel: true, + use: { + contextOptions: { + reducedMotion: "reduce", + }, + }, + projects: [ + { name: "chromium", use: devices["Desktop Chrome"] }, + { name: "firefox", use: devices["Desktop Firefox"] }, + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + deviceScaleFactor: 1, + }, + }, + ], + reporter: "list", + testMatch: "src/**/*.pw.spec.ts", + webServer: { + command: "node start.mjs", + port: 5273, + reuseExistingServer: true, + stdout: "pipe", + stderr: "pipe", + }, +}); diff --git a/packages/fast-test-harness/public/styles.css b/packages/fast-test-harness/public/styles.css new file mode 100644 index 00000000000..c02a31260b8 --- /dev/null +++ b/packages/fast-test-harness/public/styles.css @@ -0,0 +1,15 @@ +/* + * Base styles for the FAST test harness. + * Consumers can override or extend these in their own test setups. + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, sans-serif; +} diff --git a/packages/fast-test-harness/server.mjs b/packages/fast-test-harness/server.mjs new file mode 100644 index 00000000000..2dec893b9a5 --- /dev/null +++ b/packages/fast-test-harness/server.mjs @@ -0,0 +1,163 @@ +import fs from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import express from "express"; + +const __dirname = fileURLToPath(dirname(import.meta.url)); + +const PORT = process.env.PORT || 5273; +const base = process.env.BASE || "/"; + +export const app = express(); + +export async function startServer(cwd = process.cwd(), root, configFile) { + root = root ?? resolve(cwd, "./test"); + configFile = configFile ?? resolve(root, "vite.config.ts"); + const indexPath = resolve(root, "./index.html"); + + const tempDir = resolve(root, "temp"); + await fs.rm(tempDir, { recursive: true, force: true }); + await fs.mkdir(tempDir, { recursive: true }); + const realTempDir = await fs.realpath(tempDir); + + const pendingGenerations = new Map(); + let cachedIndexHtml = null; + const fixtureCache = new Map(); + + const { createServer } = await import("vite"); + + const vite = await createServer({ + root, + configFile, + server: { + middlewareMode: true, + watch: { + ignored: ["**/temp/**", "**/ssr-*.html"], + }, + }, + appType: "custom", + publicDir: resolve(__dirname, "./public"), + }); + + app.use(vite.middlewares); + + app.use(express.static(cwd)); + + app.post("/generate-fixture", express.json(), async (req, res) => { + try { + if (!req.body.testId) { + throw new Error("testId is required"); + } + + if (!/^[a-z0-9_-]+$/i.test(req.body.testId)) { + throw new Error("testId contains invalid characters"); + } + + if (req.body.attributes) { + req.body.attributes = JSON.parse(req.body.attributes); + } + + if (req.body.styles) { + req.body.styles = JSON.parse(req.body.styles); + } + + const testId = req.body.testId; + const filename = `ssr-${testId}.html`; + const filePath = resolve(realTempDir, filename); + + const url = `/${filename}`; + + if (pendingGenerations.has(filename)) { + await pendingGenerations.get(filename); + return res.status(200).json({ url }); + } + + const generateTask = (async () => { + const templateFile = await fs.readFile( + resolve(root, "./ssr.html"), + "utf-8", + ); + const page = await vite.transformIndexHtml(url, templateFile); + + const { render } = await vite.ssrLoadModule("/src/entry-server.js"); + + const { template, fixture, preloadLinks } = render(req.body); + + const styleTags = (req.body.styles || []) + .map(s => ``) + .join("\n"); + + const html = page + .replace( + "", + () => req.body.testTitle || "FAST Test Harness (SSR)", + ) + .replace("", () => template ?? "") + .replace("", () => fixture ?? "") + .replace( + "", + () => `${preloadLinks ?? ""}${styleTags}`, + ); + + fixtureCache.set(url, html); + + // Write to disk for debugging; served from cache above. + await fs.writeFile(filePath, html, "utf-8"); + })(); + + pendingGenerations.set(filename, generateTask); + + try { + await generateTask; + res.status(200).json({ url }); + } finally { + pendingGenerations.delete(filename); + } + } catch (e) { + vite?.ssrFixStacktrace?.(e); + console.log(e.stack); + res.status(500).end("Internal Server Error"); + } + }); + + // Serve SSR fixtures from cache without hitting the filesystem. + app.get("/ssr-:id.html", (req, res, next) => { + const url = req.path; + const cached = fixtureCache.get(url); + if (cached) { + return res.status(200).set({ "Content-Type": "text/html" }).send(cached); + } + next(); + }); + + // This server is a Playwright test harness, not a production service. + // It only serves localhost during test runs (local and CI). Rate limiting + // is unnecessary. + app.use("*all", async (req, res, next) => { + // Only serve the HTML shell for navigation requests, not for + // module/asset requests that Vite's middleware didn't handle. + const accept = req.headers.accept || ""; + if (!accept.includes("text/html")) { + return next(); + } + + try { + const url = req.originalUrl.replace(base, ""); + + if (!cachedIndexHtml) { + const indexFile = await fs.readFile(indexPath, "utf-8"); + cachedIndexHtml = await vite.transformIndexHtml(url, indexFile); + } + + res.status(200).set({ "Content-Type": "text/html" }).send(cachedIndexHtml); + } catch (e) { + vite?.ssrFixStacktrace?.(e); + console.log(e.stack); + res.status(500).end("Internal Server Error"); + } + }); + + app.listen(PORT, () => { + console.log(`Server started at http://localhost:${PORT}`); + }); +} diff --git a/packages/fast-test-harness/src/fixtures/assertions.ts b/packages/fast-test-harness/src/fixtures/assertions.ts new file mode 100644 index 00000000000..fef35270982 --- /dev/null +++ b/packages/fast-test-harness/src/fixtures/assertions.ts @@ -0,0 +1,69 @@ +import { + expect as baseExpect, + type ExpectMatcherState, + type Locator, +} from "@playwright/test"; + +/** + * Evaluate whether an element has the given state on its `elementInternals` + * property using the `:state()` pseudo-class. + * + * @param locator - The Playwright locator for the element. + * @param state - The name of the state. + * @param options - Optional timeout configuration. + */ +export async function toHaveCustomState( + this: ExpectMatcherState, + locator: Locator, + state: string, + options?: { timeout?: number }, +) { + const assertionName = "toHaveCustomState"; + let pass: boolean; + let matcherResult: any; + const expected: boolean = !this.isNot; + + try { + baseExpect( + await locator.evaluate( + (el, state) => el.matches(`:state(${state})`), + state, + options, + ), + ).toEqual(true); + pass = true; + } catch (err: any) { + matcherResult = err.matcherResult; + pass = false; + } + + const message = pass + ? () => + this.utils.matcherHint(assertionName, undefined, undefined, { + isNot: this.isNot, + }) + + "\n\n" + + `Locator: ${locator}\n` + + `Expected: ${this.isNot ? "not" : ""}${this.utils.printExpected(expected)}\n` + + (matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : "") + : () => + this.utils.matcherHint(assertionName, undefined, undefined, { + isNot: this.isNot, + }) + + "\n\n" + + `Locator: ${locator}\n` + + `Expected: ${this.utils.printExpected(expected)}\n` + + (matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : ""); + + return { + name: assertionName, + message, + pass, + expected, + actual: matcherResult?.actual, + }; +} diff --git a/packages/fast-test-harness/src/fixtures/csr-fixture.ts b/packages/fast-test-harness/src/fixtures/csr-fixture.ts new file mode 100644 index 00000000000..0ad0b62ae1d --- /dev/null +++ b/packages/fast-test-harness/src/fixtures/csr-fixture.ts @@ -0,0 +1,249 @@ +import type { Locator, Page } from "@playwright/test"; + +export type ThemeTokens = Record; + +export type InitialTemplateAttributes = Record; + +export type TemplateAttributes = Record; + +/** + * The options for configuring the fixture's template. + */ +export type InitialTemplateOptions = { + attributes?: InitialTemplateAttributes; + innerHTML?: string; +}; + +export type FixtureOptions = Omit & { + attributes?: TemplateAttributes; +}; + +/** + * The template configuration, which can be a raw HTML string or fixture + * options. + */ +export type TemplateOrOptions = InitialTemplateOptions | string; + +/** + * A fixture for testing FAST components with Playwright. + */ +export class CSRFixture { + /** + * The Playwright locator for the custom element. + */ + public readonly element: Locator; + + /** + * The tag name of the custom element. + */ + protected readonly tagName: string; + + /** + * The inner HTML of the custom element. + */ + protected readonly innerHTML: string; + + /** + * Additional custom elements to wait for before running the test. + */ + protected readonly waitFor: string[]; + + /** + * Creates an instance of the CSRFixture. + * + * @param page - The Playwright page object. + * @param tagName - The tag name of the custom element. + * @param innerHTML - The inner HTML of the custom element. + * @param waitFor - Additional custom elements to wait for. + */ + constructor( + public readonly page: Page, + tagName: string, + innerHTML: string, + waitFor: string[] = [], + ) { + this.tagName = tagName; + this.innerHTML = innerHTML; + this.element = this.page.locator(this.tagName); + this.waitFor = waitFor; + } + + /** + * Adds a style tag to the page. + * + * @param options - The options for the style tag. + * @see {@link Page.addStyleTag} + */ + async addStyleTag(options: Parameters[0]): Promise { + await this.page.addStyleTag(options); + } + + /** + * Navigates to the specified URL. + * + * @param url - The URL to navigate to. Defaults to "/". + */ + async goto(url: string = "/") { + await this.page.goto(url); + } + + /** + * Applies a set of design tokens as CSS custom properties on the body. + * + * @param tokens - A record mapping token names to values. Each key will + * be prefixed with `--` and set as a CSS custom property. + */ + async applyTokens(tokens: ThemeTokens): Promise { + await this.page.evaluate(async theme => { + Object.entries(theme).forEach(([key, value]) => { + document.body.style.setProperty( + `--${key}`, + typeof value === "string" ? value : `${value}`, + ); + }); + }, tokens); + } + + /** + * Generates the default template for the fixture. + */ + private defaultTemplate( + tagName: string = this.tagName, + attributes: InitialTemplateAttributes = {}, + innerHTML: string = this.innerHTML, + ) { + const attributesString = Object.entries(attributes) + .map(([key, value]) => { + if (value === true) { + return key; + } + + return `${key}="${value.replace(/"/g, "")}"`; + }) + .join(" "); + + return `<${tagName} ${attributesString}>${innerHTML}`; + } + + /** + * Sets the template for the fixture's page. + * + * When `templateOrOptions` is an object, the method merges specific + * template options with values configured via the Playwright `test.use` + * configuration for the current test suite. + * + * If `templateOrOptions` is a string, it is treated as the complete HTML + * body for the fixture. + * + * If `templateOrOptions` is not provided, the method uses the default + * template based on the fixture's `tagName` and `innerHTML` properties. + * + * @param templateOrOptions - The template configuration. + */ + async setTemplate(templateOrOptions?: TemplateOrOptions): Promise { + const template = + typeof templateOrOptions === "string" + ? templateOrOptions + : this.defaultTemplate( + this.tagName, + templateOrOptions?.attributes, + templateOrOptions?.innerHTML, + ); + + await this.page.locator("body").evaluate((node, template) => { + const fragment = document.createRange().createContextualFragment(template); + node.innerHTML = ""; + node.append(fragment); + }, template); + + if (this.tagName) { + await this.waitForStability(); + } + } + + /** + * Waits for the fixture to reach a stable state. + * + * This includes waiting for the custom element and any additional + * specified elements to be defined and for the body to become stable. + */ + protected async waitForStability(): Promise { + if ((await this.element.count()) > 0) { + const elements = await this.page + .locator([this.tagName, ...this.waitFor].join(",")) + .all(); + + await Promise.allSettled( + elements.map(element => + element.waitFor({ + state: "attached", + timeout: 1000, + }), + ), + ); + } + + await this.waitForCustomElement(this.tagName, ...this.waitFor); + + await (await this.page.locator("body").elementHandle())?.waitForElementState( + "stable", + ); + } + + /** + * Updates the content of the fixture by modifying the specified + * element's attributes and/or inner HTML. + * + * @param locator - The locator or selector for the element to update. + * @param options - The options for updating the element. + */ + async updateTemplate( + locator: string | Locator, + options: FixtureOptions, + ): Promise { + const element = + typeof locator === "string" ? this.page.locator(locator) : locator; + + await element.evaluate((node, options) => { + if (options.innerHTML !== undefined) { + node.innerHTML = options.innerHTML; + } + + if (options.attributes) { + const attributesAsJSON = options.attributes; + + Object.entries(attributesAsJSON).forEach(([key, value]) => { + if (value === true) { + node.setAttribute(key, ""); + } else if (value === false) { + node.removeAttribute(key); + } else if (typeof value === "string") { + node.setAttribute(key, value); + } + }); + } + }, options); + } + + /** + * Waits for the specified custom elements to be defined in the + * browser's CustomElementRegistry. + * + * @param tagName - The primary tag name to wait for. + * @param tagNames - Additional tag names to wait for. + */ + async waitForCustomElement( + tagName: string = this.tagName, + ...tagNames: string[] + ): Promise { + if (!tagName && !tagNames.length) { + return; + } + + await this.page.waitForFunction( + (tagNames: string[]) => + Promise.all(tagNames.map(t => customElements.whenDefined(t))), + [tagName, ...tagNames], + ); + } +} diff --git a/packages/fast-test-harness/src/fixtures/index.ts b/packages/fast-test-harness/src/fixtures/index.ts new file mode 100644 index 00000000000..36b959481b0 --- /dev/null +++ b/packages/fast-test-harness/src/fixtures/index.ts @@ -0,0 +1,89 @@ +import { expect as baseExpect, test as baseTest } from "@playwright/test"; +import { toHaveCustomState } from "./assertions.js"; +import { CSRFixture } from "./csr-fixture.js"; +import { SSRFixture } from "./ssr-fixture.js"; + +const isSSR = process.env.PLAYWRIGHT_TEST_SSR === "true"; + +type FixtureOptions = { + /** + * Additional HTML to insert into the element. + */ + innerHTML: string; + + /** + * Indicates if the test is running in SSR mode. + */ + ssr: boolean; + + /** + * The tag name of the custom element to test. + */ + tagName: string; + + /** + * Additional custom elements to wait for before running the test. + */ + waitFor: string[]; +}; + +type Fixtures = { + fastPage: CSRFixture | SSRFixture; +}; + +export const test = baseTest.extend({ + /** + * The inner HTML to set on the fixture's custom element. This can be used + * to provide slotted content or otherwise customize the fixture's template. + */ + innerHTML: ["", { option: true }], + + /** + * The tag name of the custom element to test. This is used to construct the + * fixture's template and to determine when the page has finished loading. + */ + tagName: ["", { option: true }], + + /** + * Additional custom elements to wait for before running the test. + */ + waitFor: [[], { option: true }], + + /** + * Indicates if the test is running in SSR mode. When `true`, the fixture + * uses the `SSRFixture` class, which generates server-rendered fixtures. + * + * This can be set directly in a fixture via `test.use({ ssr: true })`, or + * indirectly with the environment variable `PLAYWRIGHT_TEST_SSR=true`. + */ + ssr: [!!isSSR, { option: true }], + + async fastPage( + { page, innerHTML, ssr, tagName, waitFor }, + use, + testInfo, + ): Promise { + const testId = testInfo.titlePath + .join("-") + .replace(/[^a-z0-9-]/gi, "_") + .toLowerCase(); + + const testTitle = ssr ? `${testInfo.titlePath.join(" › ")}` : undefined; + + const fastPage = ssr + ? new SSRFixture(page, tagName, innerHTML, waitFor, testId, testTitle) + : new CSRFixture(page, tagName, innerHTML, waitFor); + + if (!ssr) { + await fastPage.goto(); + await page.emulateMedia({ reducedMotion: "reduce" }); + await fastPage.waitForCustomElement(tagName, ...waitFor); + } + + await use(fastPage); + }, +}); + +export const expect = baseExpect.extend({ + toHaveCustomState, +}); diff --git a/packages/fast-test-harness/src/fixtures/ssr-fixture.ts b/packages/fast-test-harness/src/fixtures/ssr-fixture.ts new file mode 100644 index 00000000000..251b269e915 --- /dev/null +++ b/packages/fast-test-harness/src/fixtures/ssr-fixture.ts @@ -0,0 +1,150 @@ +import type { Page } from "@playwright/test"; +import { CSRFixture, type TemplateOrOptions } from "./csr-fixture.js"; + +export class SSRFixture extends CSRFixture { + /** + * Styles buffered before {@link setTemplate} is called. + */ + private pendingStyles: Parameters[0][] = []; + + /** + * Whether the template has been rendered. + */ + private templateRendered = false; + + constructor( + page: Page, + tagName: string, + innerHTML: string = "", + waitFor: string[] = [], + private readonly testId?: string, + private readonly testTitle?: string, + ) { + super(page, tagName, innerHTML, waitFor); + } + + /** + * Buffers style tags added before {@link setTemplate} so they can be + * included in the generated SSR page. After the template has been + * rendered, calls pass through to the page directly. + * + * @param options - The options for the style tag. + * @see {@link Page.addStyleTag} + */ + override async addStyleTag( + options: Parameters[0], + ): Promise { + if (this.templateRendered) { + await this.page.addStyleTag(options); + return; + } + this.pendingStyles.push(options); + } + + /** + * Sets up the test fixture by posting the template or configuration + * to the SSR generation endpoint. + * + * This method constructs a request body based on the provided + * `templateOrOptions` and the current test context (like `testId` + * and `testTitle`). It sends this data to the `/generate-fixture` + * endpoint to create a server-side rendered fixture, navigates the + * page to the resulting URL, and waits for the page to stabilize. + * + * @param templateOrOptions - The template configuration. + */ + override async setTemplate(templateOrOptions?: TemplateOrOptions): Promise { + const body: Record = {}; + + if (this.testId) { + body.testId = this.testId; + body.testTitle = this.testTitle || this.formatTestTitle(this.testId); + } + + if (typeof templateOrOptions === "string") { + body.html = templateOrOptions.trim(); + } + + if (typeof templateOrOptions === "object") { + if (typeof templateOrOptions.innerHTML === "string") { + body.innerHTML = templateOrOptions.innerHTML; + } + + if (templateOrOptions.attributes) { + const cleanedAttributes: Record = {}; + Object.entries(templateOrOptions.attributes).forEach(([key, value]) => { + cleanedAttributes[key.trim()] = + typeof value === "string" ? value.trim() : value; + }); + body.attributes = JSON.stringify(cleanedAttributes); + } + } + + if (!body.html && typeof templateOrOptions !== "string") { + body.tagName = this.tagName; + if (!body.innerHTML && typeof templateOrOptions?.innerHTML !== "string") { + body.innerHTML = this.innerHTML; + } + } + + Object.entries(body).forEach(([key, value]) => { + if (typeof value === "string") { + body[key] = value.replace(/\s+/g, " ").trim(); + } + }); + + if (this.pendingStyles.length) { + body.styles = JSON.stringify( + this.pendingStyles.map(s => s?.content).filter((c): c is string => !!c), + ); + } + + const response = await this.page.request.post("/generate-fixture", { + data: body, + }); + + if (!response.ok()) { + throw new Error( + `Failed to generate fixture: ${response.status()} ${response.statusText()}`, + ); + } + + const result = await response.json(); + + if (!result.url) { + throw new Error(`Invalid response from server: ${JSON.stringify(result)}`); + } + + await this.page.goto(result.url); + + await this.waitForStability(); + + this.templateRendered = true; + this.pendingStyles.length = 0; + } + + /** + * Formats the test title based on the provided test ID. + */ + private formatTestTitle(testId: string): string { + const match = testId.match(/^(.*?\.(ts|js))-(.+)$/); + if (!match) { + return testId + .split("_") + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + } + + const filePath = match[1]; + const testParts = match[3]; + + const sections = testParts.split("-").map(section => + section + .split("_") + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + ); + + return `${sections.join(" › ")} (${filePath})`; + } +} diff --git a/packages/fast-test-harness/src/index.ts b/packages/fast-test-harness/src/index.ts new file mode 100644 index 00000000000..406decdd3a2 --- /dev/null +++ b/packages/fast-test-harness/src/index.ts @@ -0,0 +1,5 @@ +export { CSRFixture, type ThemeTokens } from "./fixtures/csr-fixture.js"; +export { expect, test } from "./fixtures/index.js"; +export { SSRFixture } from "./fixtures/ssr-fixture.js"; +export { readAsset, resolveAssetUrl } from "./ssr/assets.js"; +export { renderFixture, renderTemplate } from "./ssr/render.js"; diff --git a/packages/fast-test-harness/src/ssr/assets.ts b/packages/fast-test-harness/src/ssr/assets.ts new file mode 100644 index 00000000000..5ff9a5eac9b --- /dev/null +++ b/packages/fast-test-harness/src/ssr/assets.ts @@ -0,0 +1,67 @@ +import { readFileSync } from "node:fs"; +import { join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Reads a file as a UTF-8 string. Accepts bare package specifiers + * (resolved through `package.json` exports) or filesystem paths + * (relative to `process.cwd()`). + * + * @example + * ```ts + * import { readAsset } from "@microsoft/fast-test-harness/ssr/assets.js"; + * + * const template = readAsset("@my-scope/button/template.html"); + * const local = readAsset("./dist/button/button.template.html"); + * ``` + * + * @param specifier - A bare package specifier (e.g., + * `"@my-scope/button/template.html"`) or a relative/absolute + * filesystem path. + * @returns The file contents as a string. + */ +export function readAsset(specifier: string): string { + return readFileSync(resolveSpecifier(specifier), "utf8"); +} + +/** + * Resolves a specifier to a URL path relative to the server root. + * The returned string starts with `/` and uses forward slashes, + * making it suitable for `href` attributes in `` or `