diff --git a/package-lock.json b/package-lock.json index d4b2181b8f..a168d6b02d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", - "@vitest/browser": "^2.0.4", + "@vitest/browser": "^2.1.5", "all-contributors-cli": "^6.19.0", "concurrently": "^8.2.2", "connect-modrewrite": "^0.10.1", @@ -35,6 +35,7 @@ "i18next": "^19.0.2", "i18next-browser-languagedetector": "^4.0.1", "lint-staged": "^15.1.0", + "msw": "^2.6.3", "rollup": "^4.9.6", "rollup-plugin-string": "^3.0.0", "rollup-plugin-visualizer": "^5.12.0", @@ -42,7 +43,7 @@ "unplugin-swc": "^1.4.2", "vite": "^5.0.2", "vite-plugin-string": "^1.2.2", - "vitest": "^2.0.4", + "vitest": "^2.1.5", "webdriverio": "^9.0.7", "zod": "^3.23.8" } @@ -387,12 +388,12 @@ } }, "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", - "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", "dev": true, "dependencies": { - "cookie": "^0.5.0" + "cookie": "^0.7.2" } }, "node_modules/@bundled-es-modules/statuses": { @@ -930,33 +931,32 @@ "dev": true }, "node_modules/@inquirer/confirm": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz", - "integrity": "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.1.tgz", + "integrity": "sha512-6ycMm7k7NUApiMGfVc32yIPp28iPKxhGRMqoNDiUjq2RyTAkbs5Fx0TdzBqhabcKvniDdAAvHCmsRjnNfTsogw==", "dev": true, "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3" + "@inquirer/core": "^10.0.1", + "@inquirer/type": "^3.0.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/core": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.1.0.tgz", - "integrity": "sha512-RZVfH//2ytTjmaBIzeKT1zefcQZzuruwkpTwwbe/i2jTl4o9M+iML5ChULzz6iw1Ok8iUBBsRCjY2IEbD8Ft4w==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.0.1.tgz", + "integrity": "sha512-KKTgjViBQUi3AAssqjUFMnMO3CM3qwCHvePV9EW+zTKGKafFGFF01sc1yOIYjLJ7QU52G/FbzKc+c01WLzXmVQ==", "dev": true, "dependencies": { - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.3", - "@types/mute-stream": "^0.0.4", - "@types/node": "^22.5.2", - "@types/wrap-ansi": "^3.0.0", + "@inquirer/figures": "^1.0.7", + "@inquirer/type": "^3.0.0", "ansi-escapes": "^4.3.2", - "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", - "mute-stream": "^1.0.0", + "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", @@ -976,12 +976,12 @@ } }, "node_modules/@inquirer/core/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@inquirer/core/node_modules/wrap-ansi": { @@ -999,33 +999,24 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", - "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@inquirer/type": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.3.tgz", - "integrity": "sha512-xUQ14WQGR/HK5ei+2CvgcwoH9fQ4PgPGmVFSN0pc1+fVyDL3MREhyAY7nxEErSu6CkllBM3D7e3e+kOvtu+eIg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.0.tgz", + "integrity": "sha512-YYykfbw/lefC7yKj7nanzQXILM7r3suIvyFlCcMskc99axmsSewXWkAfXKwMbgxL76iAFVmRwmYdwNZNc8gjog==", "dev": true, - "dependencies": { - "mute-stream": "^1.0.0" - }, "engines": { "node": ">=18" - } - }, - "node_modules/@inquirer/type/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@isaacs/cliui": { @@ -1183,16 +1174,16 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", - "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.0.tgz", + "integrity": "sha512-lDiHQMCBV9qz8c7+zxaNFQtWWaSogTYkqJ3Pg+FGYYC76nsfSxkMQ0df8fojyz16E+w4vp57NLjN2muNG7LugQ==", "dev": true, "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", - "outvariant": "^1.2.1", + "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" }, "engines": { @@ -1267,9 +1258,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "dev": true }, "node_modules/@promptbook/utils": { @@ -2056,15 +2047,6 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", "dev": true }, - "node_modules/@types/mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "22.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz", @@ -2134,12 +2116,6 @@ "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", "dev": true }, - "node_modules/@types/wrap-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "dev": true - }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -2166,17 +2142,19 @@ "dev": true }, "node_modules/@vitest/browser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-2.0.5.tgz", - "integrity": "sha512-VbOYtu/6R3d7ASZREcrJmRY/sQuRFO9wMVsEDqfYbWiJRh2fDNi8CL1Csn7Ux31pOcPmmM5QvzFCMpiojvVh8g==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-2.1.5.tgz", + "integrity": "sha512-JrpnxvkrjlBrF7oXbK/YytWVYfJIzWYeDKppANlUaisBKwDso+yXlWocAJrANx8gUxyirF355Yx80S+SKQqayg==", "dev": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.5.2", - "@vitest/utils": "2.0.5", - "magic-string": "^0.30.10", - "msw": "^2.3.2", - "sirv": "^2.0.4", + "@vitest/mocker": "2.1.5", + "@vitest/utils": "2.1.5", + "magic-string": "^0.30.12", + "msw": "^2.6.4", + "sirv": "^3.0.0", + "tinyrainbow": "^1.2.0", "ws": "^8.18.0" }, "funding": { @@ -2184,7 +2162,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "2.0.5", + "vitest": "2.1.5", "webdriverio": "*" }, "peerDependenciesMeta": { @@ -2200,24 +2178,59 @@ } }, "node_modules/@vitest/expect": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", - "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz", + "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==", "dev": true, "dependencies": { - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", - "chai": "^5.1.1", + "@vitest/spy": "2.1.5", + "@vitest/utils": "2.1.5", + "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz", + "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/@vitest/pretty-format": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", - "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", + "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", "dev": true, "dependencies": { "tinyrainbow": "^1.2.0" @@ -2227,12 +2240,12 @@ } }, "node_modules/@vitest/runner": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", - "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz", + "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==", "dev": true, "dependencies": { - "@vitest/utils": "2.0.5", + "@vitest/utils": "2.1.5", "pathe": "^1.1.2" }, "funding": { @@ -2240,13 +2253,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", - "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", + "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", "dev": true, "dependencies": { - "@vitest/pretty-format": "2.0.5", - "magic-string": "^0.30.10", + "@vitest/pretty-format": "2.1.5", + "magic-string": "^0.30.12", "pathe": "^1.1.2" }, "funding": { @@ -2254,41 +2267,31 @@ } }, "node_modules/@vitest/spy": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", - "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz", + "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==", "dev": true, "dependencies": { - "tinyspy": "^3.0.0" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", - "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz", + "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==", "dev": true, "dependencies": { - "@vitest/pretty-format": "2.0.5", - "estree-walker": "^3.0.3", - "loupe": "^3.1.1", + "@vitest/pretty-format": "2.1.5", + "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vue/compiler-core": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.0.tgz", @@ -3137,9 +3140,9 @@ } }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dev": true, "dependencies": { "assertion-error": "^2.0.1", @@ -3309,18 +3312,6 @@ "node": ">=8" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-truncate": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", @@ -3655,9 +3646,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "engines": { "node": ">= 0.6" @@ -3803,12 +3794,12 @@ "optional": true }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4418,6 +4409,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -4755,6 +4752,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5185,15 +5191,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -5380,6 +5377,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6760,13 +6766,10 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true }, "node_modules/lru-cache": { "version": "5.1.1", @@ -6787,9 +6790,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -7758,33 +7761,35 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/msw": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.1.tgz", - "integrity": "sha512-HXcoQPzYTwEmVk+BGIcRa0vLabBT+J20SSSeYh/QfajaK5ceA6dlD4ZZjfz2dqGEq4vRNCPLP6eXsB94KllPFg==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.6.5.tgz", + "integrity": "sha512-PnlnTpUlOrj441kYQzzFhzMzMCGFT6a2jKUBG7zSpLkYS5oh8Arrbc0dL8/rNAtxaoBy0EVs2mFqj2qdmWK7lQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", "@bundled-es-modules/tough-cookie": "^0.1.6", - "@inquirer/confirm": "^3.0.0", - "@mswjs/interceptors": "^0.29.0", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", "@open-draft/until": "^2.1.0", "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", "chalk": "^4.1.2", + "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", - "outvariant": "^1.4.2", - "path-to-regexp": "^6.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", "strict-event-emitter": "^0.5.1", - "type-fest": "^4.9.0", + "type-fest": "^4.26.1", "yargs": "^17.7.2" }, "bin": { @@ -7797,13 +7802,9 @@ "url": "https://github.com/sponsors/mswjs" }, "peerDependencies": { - "graphql": ">= 16.8.x", - "typescript": ">= 4.7.x" + "typescript": ">= 4.8.x" }, "peerDependenciesMeta": { - "graphql": { - "optional": true - }, "typescript": { "optional": true } @@ -7824,9 +7825,9 @@ } }, "node_modules/msw/node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "dev": true, "engines": { "node": ">=16" @@ -8438,9 +8439,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, "node_modules/path-type": { @@ -9720,9 +9721,9 @@ } }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", "dev": true, "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -9730,7 +9731,7 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/slash": { @@ -9946,9 +9947,9 @@ } }, "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", "dev": true }, "node_modules/streamx": { @@ -10215,6 +10216,12 @@ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true + }, "node_modules/tinypool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", @@ -10234,9 +10241,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", - "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "engines": { "node": ">=14.0.0" @@ -10894,15 +10901,15 @@ } }, "node_modules/vite-node": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", - "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", + "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.5", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", - "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -10928,29 +10935,30 @@ } }, "node_modules/vitest": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", - "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@vitest/expect": "2.0.5", - "@vitest/pretty-format": "^2.0.5", - "@vitest/runner": "2.0.5", - "@vitest/snapshot": "2.0.5", - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", - "chai": "^5.1.1", - "debug": "^4.3.5", - "execa": "^8.0.1", - "magic-string": "^0.30.10", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz", + "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.5", + "@vitest/mocker": "2.1.5", + "@vitest/pretty-format": "^2.1.5", + "@vitest/runner": "2.1.5", + "@vitest/snapshot": "2.1.5", + "@vitest/spy": "2.1.5", + "@vitest/utils": "2.1.5", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", "pathe": "^1.1.2", - "std-env": "^3.7.0", - "tinybench": "^2.8.0", - "tinypool": "^1.0.0", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.0.5", + "vite-node": "2.1.5", "why-is-node-running": "^2.3.0" }, "bin": { @@ -10965,8 +10973,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.0.5", - "@vitest/ui": "2.0.5", + "@vitest/browser": "2.1.5", + "@vitest/ui": "2.1.5", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index 37c0804100..fa6c436353 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", - "@vitest/browser": "^2.0.4", + "@vitest/browser": "^2.1.5", "all-contributors-cli": "^6.19.0", "concurrently": "^8.2.2", "connect-modrewrite": "^0.10.1", @@ -48,6 +48,7 @@ "i18next": "^19.0.2", "i18next-browser-languagedetector": "^4.0.1", "lint-staged": "^15.1.0", + "msw": "^2.6.3", "rollup": "^4.9.6", "rollup-plugin-string": "^3.0.0", "rollup-plugin-visualizer": "^5.12.0", @@ -55,7 +56,7 @@ "unplugin-swc": "^1.4.2", "vite": "^5.0.2", "vite-plugin-string": "^1.2.2", - "vitest": "^2.0.4", + "vitest": "^2.1.5", "webdriverio": "^9.0.7", "zod": "^3.23.8" }, @@ -83,5 +84,10 @@ "hooks": { "pre-commit": "lint-staged" } + }, + "msw": { + "workerDirectory": [ + "test" + ] } } \ No newline at end of file diff --git a/preview/vite.config.mjs b/preview/vite.config.mjs index 301192ed7f..3f528f6925 100644 --- a/preview/vite.config.mjs +++ b/preview/vite.config.mjs @@ -3,6 +3,7 @@ import vitePluginString from 'vite-plugin-string'; export default defineConfig({ root: './', + appType: 'mpa', plugins: [ vitePluginString({ include: [ diff --git a/src/core/main.js b/src/core/main.js index 6914a754da..06d6251afb 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -665,7 +665,6 @@ import rendering from './rendering'; import renderer from './p5.Renderer'; import renderer2D from './p5.Renderer2D'; import graphics from './p5.Graphics'; -// import element from './p5.Element'; p5.registerAddon(transform); p5.registerAddon(structure); @@ -674,6 +673,5 @@ p5.registerAddon(rendering); p5.registerAddon(renderer); p5.registerAddon(renderer2D); p5.registerAddon(graphics); -// p5.registerAddon(element); export default p5; diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 65315ba989..eda81d8ba3 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -4,7 +4,7 @@ * @for p5 */ -import p5 from './main'; +// import p5 from './main'; import * as constants from './constants'; import primitives2D from '../shape/2d_primitives'; import attributes from '../shape/attributes'; @@ -15,6 +15,7 @@ import image from '../image/image'; import loadingDisplaying from '../image/loading_displaying'; import pixels from '../image/pixels'; import transform from './transform'; +import { Framebuffer } from '../webgl/p5.Framebuffer'; import primitives3D from '../webgl/3d_primitives'; import light from '../webgl/light'; @@ -30,7 +31,7 @@ class Graphics { this._pInst = pInst; this._renderer = new renderers[r](this._pInst, w, h, false, canvas); - p5.prototype._initializeInstanceVariables.apply(this); + this._initializeInstanceVariables(this); this._renderer._applyDefaults(); return this; @@ -552,7 +553,7 @@ class Graphics { * */ createFramebuffer(options) { - return new p5.Framebuffer(this._renderer, options); + return new Framebuffer(this._renderer, options); } _assert3d(name) { @@ -561,6 +562,29 @@ class Graphics { `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` ); }; + + _initializeInstanceVariables() { + this._accessibleOutputs = { + text: false, + grid: false, + textLabel: false, + gridLabel: false + }; + + this._styles = []; + + this._bezierDetail = 20; + this._curveDetail = 20; + + this._colorMode = constants.RGB; + this._colorMaxes = { + rgb: [255, 255, 255, 255], + hsb: [360, 100, 100, 1], + hsl: [360, 100, 100, 1] + }; + + this._downKeys = {}; //Holds the key codes of currently pressed keys + } }; function graphics(p5, fn){ diff --git a/src/image/image.js b/src/image/image.js index b8a9034cd0..9c1e9f9ee1 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -10,6 +10,8 @@ * for drawing images to the main display canvas. */ import * as omggif from 'omggif'; +import { Element } from '../dom/p5.Element'; +import { Framebuffer } from '../webgl/p5.Framebuffer'; function image(p5, fn){ /** @@ -278,10 +280,10 @@ function image(p5, fn){ if (args[0] instanceof HTMLCanvasElement) { htmlCanvas = args[0]; args.shift(); - } else if (args[0] instanceof p5.Element) { + } else if (args[0] instanceof Element) { htmlCanvas = args[0].elt; args.shift(); - } else if (args[0] instanceof p5.Framebuffer) { + } else if (args[0] instanceof Framebuffer) { const framebuffer = args[0]; temporaryGraphics = this.createGraphics(framebuffer.width, framebuffer.height); @@ -325,6 +327,7 @@ function image(p5, fn){ } htmlCanvas.toBlob(blob => { + console.log("here"); fn.downloadFile(blob, filename, extension); if(temporaryGraphics) temporaryGraphics.remove(); }, mimeType); @@ -658,10 +661,10 @@ function image(p5, fn){ fn.saveFrames = function(fName, ext, _duration, _fps, callback) { p5._validateParameters('saveFrames', arguments); let duration = _duration || 3; - duration = fn.constrain(duration, 0, 15); + duration = Math.max(Math.min(duration, 15), 0); duration = duration * 1000; let fps = _fps || 15; - fps = fn.constrain(fps, 0, 22); + fps = Math.max(Math.min(fps, 22), 0); let count = 0; const makeFrame = fn._makeFrame; diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 9fea1d4cac..e46f901c51 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -7,6 +7,7 @@ import canvas from '../core/helpers'; import * as constants from '../core/constants'; +import { request } from '../io/files'; import * as omggif from 'omggif'; import { GIFEncoder, quantize, nearestColorIndex } from 'gifenc'; @@ -19,28 +20,31 @@ function loadingDisplaying(p5, fn){ * files should be relative, such as `'assets/thundercat.jpg'`. URLs such as * `'https://example.com/thundercat.jpg'` may be blocked due to browser * security. Raw image data can also be passed as a base64 encoded image in - * the form `'data:image/png;base64,arandomsequenceofcharacters'`. + * the form `'data:image/png;base64,arandomsequenceofcharacters'`. The `path` + * parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * object for more advanced usage. * * The second parameter is optional. If a function is passed, it will be * called once the image has loaded. The callback function can optionally use - * the new p5.Image object. + * the new p5.Image object. The return value of the + * function will be used as the final return value of `loadImage()`. * * The third parameter is also optional. If a function is passed, it will be * called if the image fails to load. The callback function can optionally use - * the event error. + * the event error. The return value of the function will be used as the final + * return value of `loadImage()`. * - * Images can take time to load. Calling `loadImage()` in - * preload() ensures images load before they're - * used in setup() or draw(). + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * @method loadImage - * @param {String} path path of the image to be loaded or base64 encoded image. + * @param {String|Request} path path of the image to be loaded or base64 encoded image. * @param {function(p5.Image)} [successCallback] function called with * p5.Image once it * loads. * @param {function(Event)} [failureCallback] function called with event * error if the image fails to load. - * @return {p5.Image} the p5.Image object. + * @return {Promise} the p5.Image object. * * @example *
@@ -48,11 +52,8 @@ function loadingDisplaying(p5, fn){ * let img; * * // Load the image and create a p5.Image object. - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { + * async function setup() { + * img = await loadImage('assets/laDefense.jpg'); * createCanvas(100, 100); * * // Draw the image. @@ -107,110 +108,60 @@ function loadingDisplaying(p5, fn){ failureCallback ) { p5._validateParameters('loadImage', arguments); - const pImg = new p5.Image(1, 1, this); - const self = this; - const req = new Request(path, { - method: 'GET', - mode: 'cors' - }); + try{ + let pImg = new p5.Image(1, 1, this); - return fetch(path, req) - .then(async response => { - // GIF section - const contentType = response.headers.get('content-type'); - if (contentType === null) { - console.warn( - 'The image you loaded does not have a Content-Type header. If you are using the online editor consider reuploading the asset.' - ); - } - if (contentType && contentType.includes('image/gif')) { - await response.arrayBuffer().then( - arrayBuffer => new Promise((resolve, reject) => { - if (arrayBuffer) { - const byteArray = new Uint8Array(arrayBuffer); - try{ - _createGif( - byteArray, - pImg, - successCallback, - failureCallback, - (pImg => { - resolve(pImg); - }).bind(self) - ); - }catch(e){ - console.error(e.toString(), e.stack); - if (typeof failureCallback === 'function') { - failureCallback(e); - } else { - console.error(e); - } - reject(e); - } - } - }) - ).catch( - e => { - if (typeof failureCallback === 'function') { - failureCallback(e); - } else { - console.error(e); - } - } - ); - } else { - // Non-GIF Section - const img = new Image(); - - await new Promise((resolve, reject) => { - img.onload = () => { - pImg.width = pImg.canvas.width = img.width; - pImg.height = pImg.canvas.height = img.height; - - // Draw the image into the backing canvas of the p5.Image - pImg.drawingContext.drawImage(img, 0, 0); - pImg.modified = true; - if (typeof successCallback === 'function') { - successCallback(pImg); - } - resolve(); - }; - - img.onerror = e => { - p5._friendlyFileLoadError(0, img.src); - if (typeof failureCallback === 'function') { - failureCallback(e); - } else { - console.error(e); - } - reject(); - }; - - // Set crossOrigin in case image is served with CORS headers. - // This will let us draw to the canvas without tainting it. - // See https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image - // When using data-uris the file will be loaded locally - // so we don't need to worry about crossOrigin with base64 file types. - if (path.indexOf('data:image/') !== 0) { - img.crossOrigin = 'Anonymous'; - } - // start loading the image - img.src = path; - }); - } - pImg.modified = true; - return pImg; - }) - .catch(e => { - p5._friendlyFileLoadError(0, path); - if (typeof failureCallback === 'function') { - failureCallback(e); - } else { - console.error(e); - } + const req = new Request(path, { + method: 'GET', + mode: 'cors' }); - // return pImg; + + const { data, headers } = await request(req, 'bytes'); + + // GIF section + const contentType = headers.get('content-type'); + + if (contentType === null) { + console.warn( + 'The image you loaded does not have a Content-Type header. If you are using the online editor consider reuploading the asset.' + ); + } + + if (contentType && contentType.includes('image/gif')) { + await _createGif( + data, + pImg + ); + + } else { + // Non-GIF Section + const blob = new Blob([data]); + const img = await createImageBitmap(blob); + + pImg.width = pImg.canvas.width = img.width; + pImg.height = pImg.canvas.height = img.height; + + // Draw the image into the backing canvas of the p5.Image + pImg.drawingContext.drawImage(img, 0, 0); + } + + pImg.modified = true; + + if(successCallback){ + return successCallback(pImg); + }else{ + return pImg; + } + + } catch(err) { + p5._friendlyFileLoadError(0, path); + if (typeof failureCallback === 'function') { + return failureCallback(err); + } else { + throw err; + } + } }; /** @@ -651,31 +602,25 @@ function loadingDisplaying(p5, fn){ /** * Helper function for loading GIF-based images */ - function _createGif( - arrayBuffer, - pImg, - successCallback, - failureCallback, - finishCallback - ) { + async function _createGif(arrayBuffer, pImg) { + // TODO: Replace with ImageDecoder once it is widely available + // https://developer.mozilla.org/en-US/docs/Web/API/ImageDecoder const gifReader = new omggif.GifReader(arrayBuffer); pImg.width = pImg.canvas.width = gifReader.width; pImg.height = pImg.canvas.height = gifReader.height; const frames = []; const numFrames = gifReader.numFrames(); let framePixels = new Uint8ClampedArray(pImg.width * pImg.height * 4); + const loadGIFFrameIntoImage = (frameNum, gifReader) => { try { gifReader.decodeAndBlitFrameRGBA(frameNum, framePixels); } catch (e) { p5._friendlyFileLoadError(8, pImg.src); - if (typeof failureCallback === 'function') { - failureCallback(e); - } else { - console.error(e); - } + throw e; } }; + for (let j = 0; j < numFrames; j++) { const frameInfo = gifReader.frameInfo(j); const prevFrameData = pImg.drawingContext.getImageData( @@ -764,10 +709,7 @@ function loadingDisplaying(p5, fn){ }; } - if (typeof successCallback === 'function') { - successCallback(pImg); - } - finishCallback(); + return pImg; } /** diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 3cb428313c..b87827be81 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -283,8 +283,6 @@ class Image { * *
*/ - /** - */ updatePixels(x, y, w, h) { // Renderer2D.prototype.updatePixels.call(this, x, y, w, h); const pixelsState = this._pixelsState; @@ -472,7 +470,7 @@ class Image { return region; } - _getPixel(...args) { + _getPixel(x, y) { let imageData, index; imageData = this.drawingContext.getImageData(x, y, 1, 1).data; index = 0; @@ -1454,7 +1452,7 @@ class Image { /** * Saves the image to a file. * - * By default, `img.save()` saves the image as a PNG image called + * By default, `img.save()` saves the image as a PNG image called * `untitled.png`. * * The first parameter, `filename`, is optional. It's a string that sets the @@ -1517,6 +1515,12 @@ class Image { } } + async toBlob() { + return new Promise(resolve => { + this.canvas.toBlob(resolve); + }); + } + // GIF Section /** * Restarts an animated GIF at its first frame. diff --git a/src/io/files.js b/src/io/files.js index ba7dd0b8ef..bd588d4aa9 100644 --- a/src/io/files.js +++ b/src/io/files.js @@ -6,6 +6,71 @@ */ import * as fileSaver from 'file-saver'; +import { Renderer } from '../core/p5.Renderer'; +import { Graphics } from '../core/p5.Graphics'; + +class HTTPError extends Error { + status; + response; + ok; +} + +export async function request(path, type){ + try { + const res = await fetch(path); + + if (res.ok) { + let data; + switch(type) { + case 'json': + data = await res.json(); + break; + case 'text': + data = await res.text(); + break; + case 'arrayBuffer': + data = await res.arrayBuffer(); + break; + case 'blob': + data = await res.blob(); + break; + case 'bytes': + // TODO: Chrome does not implement res.bytes() yet + if(res.bytes){ + data = await res.bytes(); + }else{ + const d = await res.arrayBuffer(); + data = new Uint8Array(d); + } + break; + default: + throw new Error('Unsupported response type'); + } + + return { data, headers: res.headers }; + + } else { + const err = new HTTPError(res.statusText); + err.status = res.status; + err.response = res; + err.ok = false; + + throw err; + } + + } catch(err) { + // Handle both fetch error and HTTP error + if (err instanceof TypeError) { + console.log('You may have encountered a CORS error'); + } else if (err instanceof HTTPError) { + console.log('You have encountered a HTTP error'); + } else if (err instanceof SyntaxError) { + console.log('There is an error parsing the response to requested data structure'); + } + + throw err; + } +} function files(p5, fn){ /** @@ -18,31 +83,35 @@ function files(p5, fn){ * data in an object with strings as keys. Values can be strings, numbers, * Booleans, arrays, `null`, or other objects. * - * The first parameter, `path`, is always a string with the path to the file. + * The first parameter, `path`, is a string with the path to the file. * Paths to local files should be relative, as in * `loadJSON('assets/data.json')`. URLs such as * `'https://example.com/data.json'` may be blocked due to browser security. + * The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * object for more advanced usage. * * The second parameter, `successCallback`, is optional. If a function is * passed, as in `loadJSON('assets/data.json', handleData)`, then the * `handleData()` function will be called once the data loads. The object * created from the JSON data will be passed to `handleData()` as its only argument. + * The return value of the `handleData()` function will be used as the final return + * value of `loadJSON('assets/data.json', handleData)`. * * The third parameter, `failureCallback`, is also optional. If a function is * passed, as in `loadJSON('assets/data.json', handleData, handleFailure)`, * then the `handleFailure()` function will be called if an error occurs while * loading. The `Error` object will be passed to `handleFailure()` as its only - * argument. + * argument. The return value of the `handleFailure()` function will be used as the + * final return value of `loadJSON('assets/data.json', handleData, handleFailure)`. * - * Note: Data can take time to load. Calling `loadJSON()` within - * preload() ensures data loads before it's used in - * setup() or draw(). + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * @method loadJSON - * @param {String} path path of the JSON file to be loaded. + * @param {String|Request} path path of the JSON file to be loaded. * @param {Function} [successCallback] function to call once the data is loaded. Will be passed the object. * @param {Function} [errorCallback] function to call if the data fails to load. Will be passed an `Error` event object. - * @return {Object} object containing the loaded data. + * @return {Promise} object containing the loaded data. * * @example * @@ -50,12 +119,8 @@ function files(p5, fn){ * * let myData; * - * // Load the JSON and create an object. - * function preload() { - * myData = loadJSON('assets/data.json'); - * } - * - * function setup() { + * async function setup() { + * myData = await loadJSON('assets/data.json'); * createCanvas(100, 100); * * background(200); @@ -76,12 +141,8 @@ function files(p5, fn){ * * let myData; * - * // Load the JSON and create an object. - * function preload() { - * myData = loadJSON('assets/data.json'); - * } - * - * function setup() { + * async function setup() { + * myData = await loadJSON('assets/data.json'); * createCanvas(100, 100); * * background(200); @@ -109,12 +170,8 @@ function files(p5, fn){ * * let myData; * - * // Load the GeoJSON and create an object. - * function preload() { - * myData = loadJSON('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson'); - * } - * - * function setup() { + * async function setup() { + * myData = await loadJSON('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson'); * createCanvas(100, 100); * * background(200); @@ -143,14 +200,12 @@ function files(p5, fn){ * let bigQuake; * * // Load the GeoJSON and preprocess it. - * function preload() { - * loadJSON( + * async function setup() { + * await loadJSON( * 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson', * handleData * ); - * } * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -189,15 +244,13 @@ function files(p5, fn){ * let bigQuake; * * // Load the GeoJSON and preprocess it. - * function preload() { - * loadJSON( + * async function setup() { + * await loadJSON( * 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson', * handleData, * handleError * ); - * } * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -236,60 +289,21 @@ function files(p5, fn){ * * */ - fn.loadJSON = async function (...args) { - p5._validateParameters('loadJSON', args); - const path = args[0]; - let callback; - let errorCallback; - let options; - - const ret = {}; // object needed for preload - let t = 'json'; - - // check for explicit data type argument - for (let i = 1; i < args.length; i++) { - const arg = args[i]; - if (typeof arg === 'string') { - if (arg === 'json') { - t = arg; - } - } else if (typeof arg === 'function') { - if (!callback) { - callback = arg; - } else { - errorCallback = arg; - } + fn.loadJSON = async function (path, successCallback, errorCallback) { + p5._validateParameters('loadJSON', arguments); + + try{ + const { data } = await request(path, 'json'); + if (successCallback) successCallback(data); + return data; + } catch(err) { + p5._friendlyFileLoadError(5, path); + if(errorCallback) { + return errorCallback(err); + } else { + throw err; } } - - await new Promise(resolve => this.httpDo( - path, - 'GET', - options, - t, - resp => { - for (const k in resp) { - ret[k] = resp[k]; - } - if (typeof callback !== 'undefined') { - callback(resp); - } - - resolve() - }, - err => { - // Error handling - p5._friendlyFileLoadError(5, path); - - if (errorCallback) { - errorCallback(err); - } else { - throw err; - } - } - )); - - return ret; }; /** @@ -299,31 +313,34 @@ function files(p5, fn){ * Paths to local files should be relative, as in * `loadStrings('assets/data.txt')`. URLs such as * `'https://example.com/data.txt'` may be blocked due to browser security. + * The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * object for more advanced usage. * * The second parameter, `successCallback`, is optional. If a function is * passed, as in `loadStrings('assets/data.txt', handleData)`, then the * `handleData()` function will be called once the data loads. The array * created from the text data will be passed to `handleData()` as its only - * argument. + * argument. The return value of the `handleData()` function will be used as + * the final return value of `loadStrings('assets/data.txt', handleData)`. * * The third parameter, `failureCallback`, is also optional. If a function is * passed, as in `loadStrings('assets/data.txt', handleData, handleFailure)`, * then the `handleFailure()` function will be called if an error occurs while * loading. The `Error` object will be passed to `handleFailure()` as its only - * argument. + * argument. The return value of the `handleFailure()` function will be used as + * the final return value of `loadStrings('assets/data.txt', handleData, handleFailure)`. * - * Note: Data can take time to load. Calling `loadStrings()` within - * preload() ensures data loads before it's used in - * setup() or draw(). + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * @method loadStrings - * @param {String} path path of the text file to be loaded. + * @param {String|Request} path path of the text file to be loaded. * @param {Function} [successCallback] function to call once the data is * loaded. Will be passed the array. * @param {Function} [errorCallback] function to call if the data fails to * load. Will be passed an `Error` event * object. - * @return {String[]} new array containing the loaded text. + * @return {Promise} new array containing the loaded text. * * @example * @@ -331,12 +348,9 @@ function files(p5, fn){ * * let myData; * - * // Load the text and create an array. - * function preload() { - * myData = loadStrings('assets/test.txt'); - * } + * async function setup() { + * myData = await loadStrings('assets/test.txt'); * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -362,11 +376,9 @@ function files(p5, fn){ * let lastLine; * * // Load the text and preprocess it. - * function preload() { - * loadStrings('assets/test.txt', handleData); - * } + * async function setup() { + * await loadStrings('assets/test.txt', handleData); * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -394,11 +406,9 @@ function files(p5, fn){ * let lastLine; * * // Load the text and preprocess it. - * function preload() { - * loadStrings('assets/test.txt', handleData, handleError); - * } + * async function setup() { + * await loadStrings('assets/test.txt', handleData, handleError); * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -426,65 +436,23 @@ function files(p5, fn){ * * */ - fn.loadStrings = async function (...args) { - p5._validateParameters('loadStrings', args); - - const ret = []; - let callback, errorCallback; - - for (let i = 1; i < args.length; i++) { - const arg = args[i]; - if (typeof arg === 'function') { - if (typeof callback === 'undefined') { - callback = arg; - } else if (typeof errorCallback === 'undefined') { - errorCallback = arg; - } + fn.loadStrings = async function (path, successCallback, errorCallback) { + p5._validateParameters('loadStrings', arguments); + + try{ + let { data } = await request(path, 'text'); + data = data.split(/\r?\n/); + + if (successCallback) successCallback(data); + return data; + } catch(err) { + p5._friendlyFileLoadError(3, path); + if(errorCallback) { + errorCallback(err); + } else { + throw err; } } - - await new Promise(resolve => fn.httpDo.call( - this, - args[0], - 'GET', - 'text', - data => { - // split lines handling mac/windows/linux endings - const lines = data - .replace(/\r\n/g, '\r') - .replace(/\n/g, '\r') - .split(/\r/); - - // safe insert approach which will not blow up stack when inserting - // >100k lines, but still be faster than iterating line-by-line. based on - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#Examples - const QUANTUM = 32768; - for (let i = 0, len = lines.length; i < len; i += QUANTUM) { - Array.prototype.push.apply( - ret, - lines.slice(i, Math.min(i + QUANTUM, len)) - ); - } - - if (typeof callback !== 'undefined') { - callback(ret); - } - - resolve() - }, - function (err) { - // Error handling - p5._friendlyFileLoadError(3, arguments[0]); - - if (errorCallback) { - errorCallback(err); - } else { - throw err; - } - } - )); - - return ret; }; /** @@ -495,17 +463,14 @@ function files(p5, fn){ * format). Table only looks for a header row if the 'header' option is * included. * - * This method is asynchronous, meaning it may not finish before the next - * line in your sketch is executed. Calling loadTable() inside preload() - * guarantees to complete the operation before setup() and draw() are called. - * Outside of preload(), you may supply a callback function to handle the - * object: + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * All files loaded and saved use UTF-8 encoding. This method is suitable for fetching files up to size of 64MB. + * * @method loadTable - * @param {String} filename name of the file or URL to load - * @param {String} [extension] parse the table by comma-separated values "csv", semicolon-separated - * values "ssv", or tab-separated values "tsv" + * @param {String|Request} filename name of the file or URL to load + * @param {String} [separator] the separator character used by the file, defaults to `','` * @param {String} [header] "header" to indicate table has header row * @param {Function} [callback] function to be executed after * loadTable() completes. On success, the @@ -514,7 +479,7 @@ function files(p5, fn){ * @param {Function} [errorCallback] function to be executed if * there is an error, response is passed * in as first argument - * @return {Object} Table object containing data + * @return {Promise} Table object containing data * * @example *
@@ -529,16 +494,9 @@ function files(p5, fn){ * * let table; * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * //the file can be remote - * //table = loadTable("http://p5js.org/reference/assets/mammals.csv", - * // "csv", "header"); - * } + * async function setup() { + * table = await loadTable('assets/mammals.csv', 'csv', 'header'); * - * function setup() { * //count the columns * print(table.getRowCount() + ' total rows in table'); * print(table.getColumnCount() + ' total columns in table'); @@ -557,209 +515,50 @@ function files(p5, fn){ * *
*/ - fn.loadTable = async function (path) { - // p5._validateParameters('loadTable', arguments); - let callback; - let errorCallback; - const options = []; - let header = false; - const ext = path.substring(path.lastIndexOf('.') + 1, path.length); - - let sep; - if (ext === 'csv') { - sep = ','; - } else if (ext === 'ssv') { - sep = ';'; - } else if (ext === 'tsv') { - sep = '\t'; - } - - for (let i = 1; i < arguments.length; i++) { - if (typeof arguments[i] === 'function') { - if (typeof callback === 'undefined') { - callback = arguments[i]; - } else if (typeof errorCallback === 'undefined') { - errorCallback = arguments[i]; - } - } else if (typeof arguments[i] === 'string') { - options.push(arguments[i]); - if (arguments[i] === 'header') { - header = true; - } - if (arguments[i] === 'csv') { - sep = ','; - } else if (arguments[i] === 'ssv') { - sep = ';'; - } else if (arguments[i] === 'tsv') { - sep = '\t'; - } + fn.loadTable = async function (path, separator, header, successCallback, errorCallback) { + if(typeof arguments[arguments.length-1] === 'function'){ + if(typeof arguments[arguments.length-2] === 'function'){ + successCallback = arguments[arguments.length-2]; + errorCallback = arguments[arguments.length-1]; + }else{ + successCallback = arguments[arguments.length-1]; } } - const t = new p5.Table(); - - await new Promise(resolve => this.httpDo( - path, - 'GET', - 'table', - resp => { - const state = {}; - - // define constants - const PRE_TOKEN = 0, - MID_TOKEN = 1, - POST_TOKEN = 2, - POST_RECORD = 4; - - const QUOTE = '"', - CR = '\r', - LF = '\n'; - - const records = []; - let offset = 0; - let currentRecord = null; - let currentChar; - - const tokenBegin = () => { - state.currentState = PRE_TOKEN; - state.token = ''; - }; - - const tokenEnd = () => { - currentRecord.push(state.token); - tokenBegin(); - }; - - const recordBegin = () => { - state.escaped = false; - currentRecord = []; - tokenBegin(); - }; - - const recordEnd = () => { - state.currentState = POST_RECORD; - records.push(currentRecord); - currentRecord = null; - }; - - for (; ;) { - currentChar = resp[offset++]; - - // EOF - if (currentChar == null) { - if (state.escaped) { - throw new Error('Unclosed quote in file.'); - } - if (currentRecord) { - tokenEnd(); - recordEnd(); - break; - } - } - if (currentRecord === null) { - recordBegin(); - } - - // Handle opening quote - if (state.currentState === PRE_TOKEN) { - if (currentChar === QUOTE) { - state.escaped = true; - state.currentState = MID_TOKEN; - continue; - } - state.currentState = MID_TOKEN; - } - - // mid-token and escaped, look for sequences and end quote - if (state.currentState === MID_TOKEN && state.escaped) { - if (currentChar === QUOTE) { - if (resp[offset] === QUOTE) { - state.token += QUOTE; - offset++; - } else { - state.escaped = false; - state.currentState = POST_TOKEN; - } - } else if (currentChar === CR) { - continue; - } else { - state.token += currentChar; - } - continue; - } + if(typeof separator !== 'string') separator = ','; + if(typeof header === 'function') header = false; - // fall-through: mid-token or post-token, not escaped - if (currentChar === CR) { - if (resp[offset] === LF) { - offset++; - } - tokenEnd(); - recordEnd(); - } else if (currentChar === LF) { - tokenEnd(); - recordEnd(); - } else if (currentChar === sep) { - tokenEnd(); - } else if (state.currentState === MID_TOKEN) { - state.token += currentChar; - } - } - - // set up column names - if (header) { - t.columns = records.shift(); - } else { - for (let i = 0; i < records[0].length; i++) { - t.columns[i] = 'null'; - } - } - let row; - for (let i = 0; i < records.length; i++) { - //Handles row of 'undefined' at end of some CSVs - if (records[i].length === 1) { - if (records[i][0] === 'undefined' || records[i][0] === '') { - continue; - } - } - row = new p5.TableRow(); - row.arr = records[i]; - row.obj = makeObject(records[i], t.columns); - t.addRow(row); - } - if (typeof callback === 'function') { - callback(t); - } + try{ + let { data } = await request(path, 'text'); + data = data.split(/\r?\n/); - resolve() - }, - err => { - // Error handling - p5._friendlyFileLoadError(2, path); + let ret = new p5.Table(); - if (errorCallback) { - errorCallback(err); - } else { - console.error(err); - } + if(header){ + ret.columns = data.shift().split(separator); + }else{ + ret.columns = data[0].split(separator).map(() => null); } - )); - return t; - }; + data.forEach((line) => { + const row = new p5.TableRow(line, separator); + ret.addRow(row); + }); - // helper function to turn a row into a JSON object - function makeObject(row, headers) { - headers = headers || []; - if (typeof headers === 'undefined') { - for (let j = 0; j < row.length; j++) { - headers[j.toString()] = j; + if (successCallback) { + successCallback(ret); + } else { + return ret; + } + } catch(err) { + p5._friendlyFileLoadError(2, path); + if(errorCallback) { + return errorCallback(err); + } else { + throw err; } } - return Object.fromEntries( - headers - .map((key, i) => [key, row[i]]) - ); - } + }; /** * Loads an XML file to create a p5.XML object. @@ -773,33 +572,36 @@ function files(p5, fn){ * The first parameter, `path`, is always a string with the path to the file. * Paths to local files should be relative, as in * `loadXML('assets/data.xml')`. URLs such as `'https://example.com/data.xml'` - * may be blocked due to browser security. + * may be blocked due to browser security. The `path` parameter can also be defined + * as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * object for more advanced usage. * * The second parameter, `successCallback`, is optional. If a function is * passed, as in `loadXML('assets/data.xml', handleData)`, then the * `handleData()` function will be called once the data loads. The * p5.XML object created from the data will be passed - * to `handleData()` as its only argument. + * to `handleData()` as its only argument. The return value of the `handleData()` + * function will be used as the final return value of `loadXML('assets/data.xml', handleData)`. * * The third parameter, `failureCallback`, is also optional. If a function is * passed, as in `loadXML('assets/data.xml', handleData, handleFailure)`, then * the `handleFailure()` function will be called if an error occurs while * loading. The `Error` object will be passed to `handleFailure()` as its only - * argument. + * argument. The return value of the `handleFailure()` function will be used as the + * final return value of `loadXML('assets/data.xml', handleData, handleFailure)`. * - * Note: Data can take time to load. Calling `loadXML()` within - * preload() ensures data loads before it's used in - * setup() or draw(). + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * @method loadXML - * @param {String} path path of the XML file to be loaded. + * @param {String|Request} path path of the XML file to be loaded. * @param {Function} [successCallback] function to call once the data is * loaded. Will be passed the * p5.XML object. * @param {Function} [errorCallback] function to call if the data fails to * load. Will be passed an `Error` event * object. - * @return {p5.XML} XML data loaded into a p5.XML + * @return {Promise} XML data loaded into a p5.XML * object. * * @example @@ -808,11 +610,9 @@ function files(p5, fn){ * let myXML; * * // Load the XML and create a p5.XML object. - * function preload() { - * myXML = loadXML('assets/animals.xml'); - * } + * async function setup() { + * myXML = await loadXML('assets/animals.xml'); * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -850,11 +650,9 @@ function files(p5, fn){ * let lastMammal; * * // Load the XML and create a p5.XML object. - * function preload() { - * loadXML('assets/animals.xml', handleData); - * } + * async function setup() { + * await loadXML('assets/animals.xml', handleData); * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -886,11 +684,9 @@ function files(p5, fn){ * let lastMammal; * * // Load the XML and preprocess it. - * function preload() { - * loadXML('assets/animals.xml', handleData, handleError); - * } + * async function setup() { + * await loadXML('assets/animals.xml', handleData, handleError); * - * function setup() { * createCanvas(100, 100); * * background(200); @@ -922,69 +718,44 @@ function files(p5, fn){ * * */ - fn.loadXML = async function (...args) { - const ret = new p5.XML(); - let callback, errorCallback; - - for (let i = 1; i < args.length; i++) { - const arg = args[i]; - if (typeof arg === 'function') { - if (typeof callback === 'undefined') { - callback = arg; - } else if (typeof errorCallback === 'undefined') { - errorCallback = arg; - } + fn.loadXML = async function (path, successCallback, errorCallback) { + try{ + const parser = new DOMParser(); + + let { data } = await request(path, 'text'); + const parsedDOM = parser.parseFromString(data, 'application/xml'); + data = new p5.XML(parsedDOM); + + if (successCallback) successCallback(data); + return data; + } catch(err) { + p5._friendlyFileLoadError(1, path); + if(errorCallback) { + errorCallback(err); + } else { + throw err; } } - - await new Promise(resolve => this.httpDo( - args[0], - 'GET', - 'xml', - xml => { - for (const key in xml) { - ret[key] = xml[key]; - } - if (typeof callback !== 'undefined') { - callback(ret); - } - - resolve() - }, - function (err) { - // Error handling - p5._friendlyFileLoadError(1, arguments[0]); - - if (errorCallback) { - errorCallback(err); - } else { - throw err; - } - } - )); - - return ret; }; /** * This method is suitable for fetching files up to size of 64MB. + * * @method loadBytes - * @param {String} file name of the file or URL to load + * @param {String|Request} file name of the file or URL to load * @param {Function} [callback] function to be executed after loadBytes() * completes * @param {Function} [errorCallback] function to be executed if there * is an error - * @returns {Object} an object whose 'bytes' property will be the loaded buffer + * @returns {Promise} an object whose 'bytes' property will be the loaded buffer * * @example *
* let data; * - * function preload() { - * data = loadBytes('assets/mammals.xml'); - * } + * async function setup() { + * data = await loadBytes('assets/mammals.xml'); * - * function setup() { * for (let i = 0; i < 5; i++) { * console.log(data.bytes[i].toString(16)); * } @@ -992,48 +763,47 @@ function files(p5, fn){ * } *
*/ - fn.loadBytes = async function (file, callback, errorCallback) { - const ret = {}; - - await new Promise(resolve => this.httpDo( - file, - 'GET', - 'arrayBuffer', - arrayBuffer => { - ret.bytes = new Uint8Array(arrayBuffer); - - if (typeof callback === 'function') { - callback(ret); - } - - resolve(); - }, - err => { - // Error handling - p5._friendlyFileLoadError(6, file); + fn.loadBytes = async function (path, successCallback, errorCallback) { + try{ + let { data } = await request(path, 'arrayBuffer'); + data = new Uint8Array(data); + if (successCallback) successCallback(data); + return data; + } catch(err) { + p5._friendlyFileLoadError(6, path); + if(errorCallback) { + errorCallback(err); + } else { + throw err; + } + } + }; - if (errorCallback) { - errorCallback(err); - } else { - throw err; - } + fn.loadBlob = async function(path, successCallback, errorCallback) { + try{ + const { data } = await request(path, 'blob'); + if (successCallback) successCallback(data); + return data; + } catch(err) { + if(errorCallback) { + errorCallback(err); + } else { + throw err; } - )); - return ret; + } }; /** * Method for executing an HTTP GET request. If data type is not specified, - * p5 will try to guess based on the URL, defaulting to text. This is equivalent to + * it will default to `'text'`. This is equivalent to * calling httpDo(path, 'GET'). The 'binary' datatype will return * a Blob object, and the 'arrayBuffer' datatype will return an ArrayBuffer * which can be used to initialize typed arrays (such as Uint8Array). * * @method httpGet - * @param {String} path name of the file or url to load + * @param {String|Request} path name of the file or url to load * @param {String} [datatype] "json", "jsonp", "binary", "arrayBuffer", * "xml", or "text" - * @param {Object|Boolean} [data] param data passed sent with request * @param {Function} [callback] function to be executed after * httpGet() completes, data is passed in * as first argument @@ -1048,16 +818,12 @@ function files(p5, fn){ * // Examples use USGS Earthquake API: * // https://earthquake.usgs.gov/fdsnws/event/1/#methods * let earthquakes; - * function preload() { + * async function setup() { * // Get the most recent earthquake in the database * let url = 'https://earthquake.usgs.gov/fdsnws/event/1/query?' + * 'format=geojson&limit=1&orderby=time'; - * httpGet(url, 'json', function(response) { - * // when the HTTP request completes, populate the variable that holds the - * // earthquake data used in the visualization. - * earthquakes = response; - * }); + * earthquakes = await httpGet(url, 'json'); * } * * function draw() { @@ -1078,42 +844,42 @@ function files(p5, fn){ */ /** * @method httpGet - * @param {String} path - * @param {Object|Boolean} data - * @param {Function} [callback] - * @param {Function} [errorCallback] - * @return {Promise} - */ - /** - * @method httpGet - * @param {String} path - * @param {Function} callback - * @param {Function} [errorCallback] + * @param {String|Request} path + * @param {Function} callback + * @param {Function} [errorCallback] * @return {Promise} */ - fn.httpGet = function (...args) { - p5._validateParameters('httpGet', args); + fn.httpGet = async function (path, datatype='text', successCallback, errorCallback) { + p5._validateParameters('httpGet', arguments); - args.splice(1, 0, 'GET'); - return fn.httpDo.apply(this, args); + if (typeof datatype === 'function') { + errorCallback = successCallback; + successCallback = datatype; + datatype = 'text'; + } + + // This is like a more primitive version of the other load functions. + // If the user wanted to customize more behavior, pass in Request to path. + + return this.httpDo(path, 'GET', datatype, successCallback, errorCallback); }; /** * Method for executing an HTTP POST request. If data type is not specified, - * p5 will try to guess based on the URL, defaulting to text. This is equivalent to + * it will default to `'text'`. This is equivalent to * calling httpDo(path, 'POST'). * * @method httpPost - * @param {String} path name of the file or url to load - * @param {String} [datatype] "json", "jsonp", "xml", or "text". + * @param {String|Request} path name of the file or url to load + * @param {Object|Boolean} [data] param data passed sent with request + * @param {String} [datatype] "json", "jsonp", "xml", or "text". * If omitted, httpPost() will guess. - * @param {Object|Boolean} [data] param data passed sent with request - * @param {Function} [callback] function to be executed after - * httpPost() completes, data is passed in - * as first argument - * @param {Function} [errorCallback] function to be executed if - * there is an error, response is passed - * in as first argument + * @param {Function} [callback] function to be executed after + * httpPost() completes, data is passed in + * as first argument + * @param {Function} [errorCallback] function to be executed if + * there is an error, response is passed + * in as first argument * @return {Promise} A promise that resolves with the data when the operation * completes successfully or rejects with the error after * one occurs. @@ -1167,46 +933,95 @@ function files(p5, fn){ */ /** * @method httpPost - * @param {String} path - * @param {Object|Boolean} data - * @param {Function} [callback] - * @param {Function} [errorCallback] + * @param {String|Request} path + * @param {Object|Boolean} data + * @param {Function} [callback] + * @param {Function} [errorCallback] * @return {Promise} */ /** * @method httpPost - * @param {String} path - * @param {Function} callback - * @param {Function} [errorCallback] + * @param {String|Request} path + * @param {Function} [callback] + * @param {Function} [errorCallback] * @return {Promise} */ - fn.httpPost = function (...args) { - p5._validateParameters('httpPost', args); + fn.httpPost = async function (path, data, datatype='text', successCallback, errorCallback) { + p5._validateParameters('httpPost', arguments); + + // This behave similarly to httpGet and additional options should be passed + // as a `Request`` to path. Both method and body will be overridden. + // Will try to infer correct Content-Type for given data. + + if (typeof data === 'function') { + // Assume both data and datatype are functions as data should not be function + successCallback = data; + errorCallback = datatype; + data = undefined; + datatype = 'text'; + + } else if (typeof datatype === 'function') { + // Data is provided but not datatype\ + errorCallback = successCallback; + successCallback = datatype; + datatype = 'text'; + } - args.splice(1, 0, 'POST'); - return fn.httpDo.apply(this, args); + let reqData = data; + let contentType = 'text/plain'; + // Normalize data + if(data instanceof p5.XML) { + reqData = data.serialize(); + contentType = 'application/xml'; + + } else if(data instanceof p5.Image) { + reqData = await data.toBlob(); + contentType = 'image/png'; + + } else if (typeof data === 'object') { + reqData = JSON.stringify(data); + contentType = 'application/json'; + } + + const requestOptions = { + method: 'POST', + body: reqData, + headers: { + 'Content-Type': contentType + } + }; + + if (reqData) { + requestOptions.body = reqData; + } + + const req = new Request(path, requestOptions); + + return this.httpDo(req, 'POST', datatype, successCallback, errorCallback); }; /** * Method for executing an HTTP request. If data type is not specified, - * p5 will try to guess based on the URL, defaulting to text.

- * For more advanced use, you may also pass in the path as the first argument - * and a object as the second argument, the signature follows the one specified - * in the Fetch API specification. + * it will default to `'text'`. + * + * This function is meant for more advanced usage of HTTP requests in p5.js. It is + * best used when a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * object is passed to the `path` parameter. + * * This method is suitable for fetching files up to size of 64MB when "GET" is used. * * @method httpDo - * @param {String} path name of the file or url to load - * @param {String} [method] either "GET", "POST", or "PUT", - * defaults to "GET" - * @param {String} [datatype] "json", "jsonp", "xml", or "text" - * @param {Object} [data] param data passed sent with request - * @param {Function} [callback] function to be executed after - * httpGet() completes, data is passed in - * as first argument - * @param {Function} [errorCallback] function to be executed if - * there is an error, response is passed - * in as first argument + * @param {String|Request} path name of the file or url to load + * @param {String} [method] either "GET", "POST", "PUT", "DELETE", + * or other HTTP request methods + * @param {String} [datatype] "json", "jsonp", "xml", or "text" + * @param {Object} [data] param data passed sent with request + * @param {Function} [callback] function to be executed after + * httpGet() completes, data is passed in + * as first argument + * @param {Function} [errorCallback] function to be executed if + * there is an error, response is passed + * in as first argument * @return {Promise} A promise that resolves with the data when the operation * completes successfully or rejects with the error after * one occurs. @@ -1221,7 +1036,7 @@ function files(p5, fn){ * let earthquakes; * let eqFeatureIndex = 0; * - * function preload() { + * function setup() { * let url = 'https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson'; * httpDo( * url, @@ -1260,135 +1075,67 @@ function files(p5, fn){ */ /** * @method httpDo - * @param {String} path - * @param {Object} options Request object options as documented in the - * "fetch" API - * reference - * @param {Function} [callback] - * @param {Function} [errorCallback] + * @param {String|Request} path + * @param {Function} [callback] + * @param {Function} [errorCallback] * @return {Promise} */ - fn.httpDo = function (...args) { - let type; - let callback; - let errorCallback; - let request; - let promise; - let cbCount = 0; - let contentType = 'text/plain'; - // Trim the callbacks off the end to get an idea of how many arguments are passed - for (let i = args.length - 1; i > 0; i--) { - if (typeof args[i] === 'function') { - cbCount++; - } else { - break; - } + fn.httpDo = async function (path, method, datatype, successCallback, errorCallback) { + // This behave similarly to httpGet but even more primitive. The user + // will most likely want to pass in a Request to path, the only convenience + // is that datatype will be taken into account to parse the response. + + if(typeof datatype === 'function'){ + errorCallback = successCallback; + successCallback = datatype; + datatype = undefined; } - // The number of arguments minus callbacks - const argsCount = args.length - cbCount; - const path = args[0]; - if ( - argsCount === 2 && - typeof path === 'string' && - typeof args[1] === 'object' - ) { - // Intended for more advanced use, pass in Request parameters directly - request = new Request(path, args[1]); - callback = args[2]; - errorCallback = args[3]; - } else { - // Provided with arguments - let method = 'GET'; - let data; - for (let j = 1; j < args.length; j++) { - const a = args[j]; - if (typeof a === 'string') { - if (a === 'GET' || a === 'POST' || a === 'PUT' || a === 'DELETE') { - method = a; - } else if ( - a === 'json' || - a === 'binary' || - a === 'arrayBuffer' || - a === 'xml' || - a === 'text' || - a === 'table' - ) { - type = a; - } else { - data = a; - } - } else if (typeof a === 'number') { - data = a.toString(); - } else if (typeof a === 'object') { - if (a instanceof p5.XML) { - data = a.serialize(); - contentType = 'application/xml'; - } else { - data = JSON.stringify(a); - contentType = 'application/json'; - } - } else if (typeof a === 'function') { - if (!callback) { - callback = a; - } else { - errorCallback = a; - } - } + // Try to infer data type if it is defined + if(!datatype){ + const extension = typeof path === 'string' ? + path.split(".").pop() : + path.url.split(".").pop(); + switch(extension) { + case 'json': + datatype = 'json'; + break; + + case 'jpg': + case 'jpeg': + case 'png': + case 'webp': + case 'gif': + datatype = 'blob'; + break; + + case 'xml': + // NOTE: still need to normalize type handling/mapping + // datatype = 'xml'; + case 'txt': + default: + datatype = 'text'; } + } - let headers = - method === 'GET' - ? new Headers() - : new Headers({ 'Content-Type': contentType }); + const req = new Request(path, { + method + }); - request = new Request(path, { - method, - mode: 'cors', - body: data, - headers - }); - } - // do some sort of smart type checking - if (!type) { - if (path.includes('json')) { - type = 'json'; - } else if (path.includes('xml')) { - type = 'xml'; + try{ + const { data } = await request(req, datatype); + if (successCallback) { + return successCallback(data); } else { - type = 'text'; + return data; } - } - - promise = fetch(request); - promise = promise.then(res => { - if (!res.ok) { - const err = new Error(res.body); - err.status = res.status; - err.ok = false; - throw err; + } catch(err) { + if(errorCallback) { + return errorCallback(err); } else { - switch (type) { - case 'json': - return res.json(); - case 'binary': - return res.blob(); - case 'arrayBuffer': - return res.arrayBuffer(); - case 'xml': - return res.text().then(text => { - const parser = new DOMParser(); - const xml = parser.parseFromString(text, 'text/xml'); - return new p5.XML(xml.documentElement); - }); - default: - return res.text(); - } + throw err; } - }); - promise.then(callback || (() => { })); - promise.catch(errorCallback || console.error); - return promise; + } }; /** @@ -1396,9 +1143,6 @@ function files(p5, fn){ * @submodule Output * @for p5 */ - - window.URL = window.URL || window.webkitURL; - // private array of p5.PrintWriter objects fn._pWriters = []; @@ -1889,8 +1633,8 @@ function files(p5, fn){ * with line breaks.`); * */ - fn.save = function (object, _filename, _options) { + // TODO: parameters is not used correctly // parse the arguments and figure out which things we are saving const args = arguments; // ================================================= @@ -1901,15 +1645,17 @@ function files(p5, fn){ if (args.length === 0) { fn.saveCanvas(cnv); return; - } else if (args[0] instanceof p5.Renderer || args[0] instanceof p5.Graphics) { - // otherwise, parse the arguments + } else if (args[0] instanceof Renderer || args[0] instanceof Graphics) { + // otherwise, parse the arguments // if first param is a p5Graphics, then saveCanvas fn.saveCanvas(args[0].canvas, args[1], args[2]); return; + } else if (args.length === 1 && typeof args[0] === 'string') { // if 1st param is String and only one arg, assume it is canvas filename fn.saveCanvas(cnv, args[0]); + } else { // ================================================= // OPTION 2: extension clarifies saveStrings vs. saveJSON @@ -2062,10 +1808,10 @@ function files(p5, fn){ * * */ - fn.saveJSON = function (json, filename, opt) { + fn.saveJSON = function (json, filename, optimize) { p5._validateParameters('saveJSON', arguments); let stringify; - if (opt) { + if (optimize) { stringify = JSON.stringify(json); } else { stringify = JSON.stringify(json, undefined, 2); @@ -2073,9 +1819,6 @@ function files(p5, fn){ this.saveStrings(stringify.split('\n'), filename, 'json'); }; - fn.saveJSONObject = fn.saveJSON; - fn.saveJSONArray = fn.saveJSON; - /** * Saves an `Array` of `String`s to a file, one per line. * @@ -2212,9 +1955,9 @@ function files(p5, fn){ fn.saveStrings = function (list, filename, extension, isCRLF) { p5._validateParameters('saveStrings', arguments); const ext = extension || 'txt'; - const pWriter = this.createWriter(filename, ext); - for (let i = 0; i < list.length; i++) { - isCRLF ? pWriter.write(list[i] + '\r\n') : pWriter.write(list[i] + '\n'); + const pWriter = new p5.PrintWriter(filename, ext); + for (let item of list) { + isCRLF ? pWriter.write(item + '\r\n') : pWriter.write(item + '\n'); } pWriter.close(); pWriter.clear(); @@ -2276,6 +2019,7 @@ function files(p5, fn){ let ext; if (options === undefined) { ext = filename.substring(filename.lastIndexOf('.') + 1, filename.length); + if(ext === filename) ext = 'csv'; } else { ext = options; } @@ -2403,34 +2147,13 @@ function files(p5, fn){ fn.downloadFile = function (data, fName, extension) { const fx = _checkFileExtension(fName, extension); const filename = fx[0]; + let saveData = data; - if (data instanceof Blob) { - fileSaver.saveAs(data, filename); - return; + if (!(saveData instanceof Blob)) { + saveData = new Blob([data]); } - const a = document.createElement('a'); - a.href = data; - a.download = filename; - - // Firefox requires the link to be added to the DOM before click() - a.onclick = e => { - destroyClickedElement(e); - e.stopPropagation(); - }; - - a.style.display = 'none'; - document.body.appendChild(a); - - // Safari will open this file in the same page as a confusing Blob. - if (fn._isSafari()) { - let aText = 'Hello, Safari user! To download this file...\n'; - aText += '1. Go to File --> Save As.\n'; - aText += '2. Choose "Page Source" as the Format.\n'; - aText += `3. Name it with this extension: ."${fx[1]}"`; - alert(aText); - } - a.click(); + fileSaver.saveAs(saveData, filename); }; /** diff --git a/src/io/p5.TableRow.js b/src/io/p5.TableRow.js index eb171e2766..85bbf6cc61 100644 --- a/src/io/p5.TableRow.js +++ b/src/io/p5.TableRow.js @@ -33,44 +33,44 @@ function tableRow(p5, fn){ } /** - * Stores a value in the TableRow's specified column. - * The column may be specified by either its ID or title. - * - * @method set - * @param {String|Integer} column Column ID (Number) - * or Title (String) - * @param {String|Number} value The value to be stored - * - * @example - *
- * // Given the CSV file "mammals.csv" in the project's "assets" folder: - * // - * // id,species,name - * // 0,Capra hircus,Goat - * // 1,Panthera pardus,Leopard - * // 2,Equus zebra,Zebra - * - * let table; - * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * } - * - * function setup() { - * let rows = table.getRows(); - * for (let r = 0; r < rows.length; r++) { - * rows[r].set('name', 'Unicorn'); - * } - * - * //print the results - * print(table.getArray()); - * - * describe('no image displayed'); - * } - *
- */ + * Stores a value in the TableRow's specified column. + * The column may be specified by either its ID or title. + * + * @method set + * @param {String|Integer} column Column ID (Number) + * or Title (String) + * @param {String|Number} value The value to be stored + * + * @example + *
+ * // Given the CSV file "mammals.csv" in the project's "assets" folder: + * // + * // id,species,name + * // 0,Capra hircus,Goat + * // 1,Panthera pardus,Leopard + * // 2,Equus zebra,Zebra + * + * let table; + * + * function preload() { + * //my table is comma separated value "csv" + * //and has a header specifying the columns labels + * table = loadTable('assets/mammals.csv', 'csv', 'header'); + * } + * + * function setup() { + * let rows = table.getRows(); + * for (let r = 0; r < rows.length; r++) { + * rows[r].set('name', 'Unicorn'); + * } + * + * //print the results + * print(table.getArray()); + * + * describe('no image displayed'); + * } + *
+ */ set(column, value) { // if typeof column is string, use .obj if (typeof column === 'string') { @@ -94,131 +94,131 @@ function tableRow(p5, fn){ } /** - * Stores a Float value in the TableRow's specified column. - * The column may be specified by either its ID or title. - * - * @method setNum - * @param {String|Integer} column Column ID (Number) - * or Title (String) - * @param {Number|String} value The value to be stored - * as a Float - * @example - *
- * // Given the CSV file "mammals.csv" in the project's "assets" folder: - * // - * // id,species,name - * // 0,Capra hircus,Goat - * // 1,Panthera pardus,Leopard - * // 2,Equus zebra,Zebra - * - * let table; - * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * } - * - * function setup() { - * let rows = table.getRows(); - * for (let r = 0; r < rows.length; r++) { - * rows[r].setNum('id', r + 10); - * } - * - * print(table.getArray()); - * - * describe('no image displayed'); - * } - *
- */ + * Stores a Float value in the TableRow's specified column. + * The column may be specified by either its ID or title. + * + * @method setNum + * @param {String|Integer} column Column ID (Number) + * or Title (String) + * @param {Number|String} value The value to be stored + * as a Float + * @example + *
+ * // Given the CSV file "mammals.csv" in the project's "assets" folder: + * // + * // id,species,name + * // 0,Capra hircus,Goat + * // 1,Panthera pardus,Leopard + * // 2,Equus zebra,Zebra + * + * let table; + * + * function preload() { + * //my table is comma separated value "csv" + * //and has a header specifying the columns labels + * table = loadTable('assets/mammals.csv', 'csv', 'header'); + * } + * + * function setup() { + * let rows = table.getRows(); + * for (let r = 0; r < rows.length; r++) { + * rows[r].setNum('id', r + 10); + * } + * + * print(table.getArray()); + * + * describe('no image displayed'); + * } + *
+ */ setNum(column, value) { const floatVal = parseFloat(value); this.set(column, floatVal); } /** - * Stores a String value in the TableRow's specified column. - * The column may be specified by either its ID or title. - * - * @method setString - * @param {String|Integer} column Column ID (Number) - * or Title (String) - * @param {String|Number|Boolean|Object} value The value to be stored - * as a String - * @example - *
- * // Given the CSV file "mammals.csv" in the project's "assets" folder: - * // - * // id,species,name - * // 0,Capra hircus,Goat - * // 1,Panthera pardus,Leopard - * // 2,Equus zebra,Zebra - * - * let table; - * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * } - * - * function setup() { - * let rows = table.getRows(); - * for (let r = 0; r < rows.length; r++) { - * let name = rows[r].getString('name'); - * rows[r].setString('name', 'A ' + name + ' named George'); - * } - * - * print(table.getArray()); - * - * describe('no image displayed'); - * } - *
- */ + * Stores a String value in the TableRow's specified column. + * The column may be specified by either its ID or title. + * + * @method setString + * @param {String|Integer} column Column ID (Number) + * or Title (String) + * @param {String|Number|Boolean|Object} value The value to be stored + * as a String + * @example + *
+ * // Given the CSV file "mammals.csv" in the project's "assets" folder: + * // + * // id,species,name + * // 0,Capra hircus,Goat + * // 1,Panthera pardus,Leopard + * // 2,Equus zebra,Zebra + * + * let table; + * + * function preload() { + * //my table is comma separated value "csv" + * //and has a header specifying the columns labels + * table = loadTable('assets/mammals.csv', 'csv', 'header'); + * } + * + * function setup() { + * let rows = table.getRows(); + * for (let r = 0; r < rows.length; r++) { + * let name = rows[r].getString('name'); + * rows[r].setString('name', 'A ' + name + ' named George'); + * } + * + * print(table.getArray()); + * + * describe('no image displayed'); + * } + *
+ */ setString(column, value) { const stringVal = value.toString(); this.set(column, stringVal); } /** - * Retrieves a value from the TableRow's specified column. - * The column may be specified by either its ID or title. - * - * @method get - * @param {String|Integer} column columnName (string) or - * ID (number) - * @return {String|Number} - * - * @example - *
- * // Given the CSV file "mammals.csv" in the project's "assets" folder: - * // - * // id,species,name - * // 0,Capra hircus,Goat - * // 1,Panthera pardus,Leopard - * // 2,Equus zebra,Zebra - * - * let table; - * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * } - * - * function setup() { - * let names = []; - * let rows = table.getRows(); - * for (let r = 0; r < rows.length; r++) { - * names.push(rows[r].get('name')); - * } - * - * print(names); - * - * describe('no image displayed'); - * } - *
- */ + * Retrieves a value from the TableRow's specified column. + * The column may be specified by either its ID or title. + * + * @method get + * @param {String|Integer} column columnName (string) or + * ID (number) + * @return {String|Number} + * + * @example + *
+ * // Given the CSV file "mammals.csv" in the project's "assets" folder: + * // + * // id,species,name + * // 0,Capra hircus,Goat + * // 1,Panthera pardus,Leopard + * // 2,Equus zebra,Zebra + * + * let table; + * + * function preload() { + * //my table is comma separated value "csv" + * //and has a header specifying the columns labels + * table = loadTable('assets/mammals.csv', 'csv', 'header'); + * } + * + * function setup() { + * let names = []; + * let rows = table.getRows(); + * for (let r = 0; r < rows.length; r++) { + * names.push(rows[r].get('name')); + * } + * + * print(names); + * + * describe('no image displayed'); + * } + *
+ */ get(column) { if (typeof column === 'string') { return this.obj[column]; @@ -228,45 +228,45 @@ function tableRow(p5, fn){ } /** - * Retrieves a Float value from the TableRow's specified - * column. The column may be specified by either its ID or - * title. - * - * @method getNum - * @param {String|Integer} column columnName (string) or - * ID (number) - * @return {Number} Float Floating point number - * @example - *
- * // Given the CSV file "mammals.csv" in the project's "assets" folder: - * // - * // id,species,name - * // 0,Capra hircus,Goat - * // 1,Panthera pardus,Leopard - * // 2,Equus zebra,Zebra - * - * let table; - * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * } - * - * function setup() { - * let rows = table.getRows(); - * let minId = Infinity; - * let maxId = -Infinity; - * for (let r = 0; r < rows.length; r++) { - * let id = rows[r].getNum('id'); - * minId = min(minId, id); - * maxId = min(maxId, id); - * } - * print('minimum id = ' + minId + ', maximum id = ' + maxId); - * describe('no image displayed'); - * } - *
- */ + * Retrieves a Float value from the TableRow's specified + * column. The column may be specified by either its ID or + * title. + * + * @method getNum + * @param {String|Integer} column columnName (string) or + * ID (number) + * @return {Number} Float Floating point number + * @example + *
+ * // Given the CSV file "mammals.csv" in the project's "assets" folder: + * // + * // id,species,name + * // 0,Capra hircus,Goat + * // 1,Panthera pardus,Leopard + * // 2,Equus zebra,Zebra + * + * let table; + * + * function preload() { + * //my table is comma separated value "csv" + * //and has a header specifying the columns labels + * table = loadTable('assets/mammals.csv', 'csv', 'header'); + * } + * + * function setup() { + * let rows = table.getRows(); + * let minId = Infinity; + * let maxId = -Infinity; + * for (let r = 0; r < rows.length; r++) { + * let id = rows[r].getNum('id'); + * minId = min(minId, id); + * maxId = min(maxId, id); + * } + * print('minimum id = ' + minId + ', maximum id = ' + maxId); + * describe('no image displayed'); + * } + *
+ */ getNum(column) { let ret; if (typeof column === 'string') { @@ -282,47 +282,47 @@ function tableRow(p5, fn){ } /** - * Retrieves an String value from the TableRow's specified - * column. The column may be specified by either its ID or - * title. - * - * @method getString - * @param {String|Integer} column columnName (string) or - * ID (number) - * @return {String} String - * @example - *
- * // Given the CSV file "mammals.csv" in the project's "assets" folder: - * // - * // id,species,name - * // 0,Capra hircus,Goat - * // 1,Panthera pardus,Leopard - * // 2,Equus zebra,Zebra - * - * let table; - * - * function preload() { - * //my table is comma separated value "csv" - * //and has a header specifying the columns labels - * table = loadTable('assets/mammals.csv', 'csv', 'header'); - * } - * - * function setup() { - * let rows = table.getRows(); - * let longest = ''; - * for (let r = 0; r < rows.length; r++) { - * let species = rows[r].getString('species'); - * if (longest.length < species.length) { - * longest = species; - * } - * } - * - * print('longest: ' + longest); - * - * describe('no image displayed'); - * } - *
- */ + * Retrieves an String value from the TableRow's specified + * column. The column may be specified by either its ID or + * title. + * + * @method getString + * @param {String|Integer} column columnName (string) or + * ID (number) + * @return {String} String + * @example + *
+ * // Given the CSV file "mammals.csv" in the project's "assets" folder: + * // + * // id,species,name + * // 0,Capra hircus,Goat + * // 1,Panthera pardus,Leopard + * // 2,Equus zebra,Zebra + * + * let table; + * + * function preload() { + * //my table is comma separated value "csv" + * //and has a header specifying the columns labels + * table = loadTable('assets/mammals.csv', 'csv', 'header'); + * } + * + * function setup() { + * let rows = table.getRows(); + * let longest = ''; + * for (let r = 0; r < rows.length; r++) { + * let species = rows[r].getString('species'); + * if (longest.length < species.length) { + * longest = species; + * } + * } + * + * print('longest: ' + longest); + * + * describe('no image displayed'); + * } + *
+ */ getString(column) { if (typeof column === 'string') { return this.obj[column].toString(); diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 4163cb04c7..66270dbb0a 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -6,8 +6,18 @@ * @requires p5.Geometry */ -import { Geometry } from "./p5.Geometry"; -import { Vector } from "../math/p5.Vector"; +import { Geometry } from './p5.Geometry'; +import { Vector } from '../math/p5.Vector'; +import { request } from '../io/files'; + +async function fileExists(url) { + try { + const response = await fetch(url, { method: 'HEAD' }); + return response.ok; + } catch (error) { + return false; + } +} function loading(p5, fn){ /** @@ -21,10 +31,11 @@ function loading(p5, fn){ * There are three ways to call `loadModel()` with optional parameters to help * process the model. * - * The first parameter, `path`, is always a `String` with the path to the - * file. Paths to local files should be relative, as in - * `loadModel('assets/model.obj')`. URLs such as - * `'https://example.com/model.obj'` may be blocked due to browser security. + * The first parameter, `path`, is a `String` with the path to the file. Paths + * to local files should be relative, as in `loadModel('assets/model.obj')`. + * URLs such as `'https://example.com/model.obj'` may be blocked due to browser + * security. The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * object for more advanced usage. * * The first way to call `loadModel()` has three optional parameters after the * file path. The first optional parameter, `successCallback`, is a function @@ -75,21 +86,20 @@ function loading(p5, fn){ * loadModel('assets/model.obj', options); * ``` * - * Models can take time to load. Calling `loadModel()` in - * preload() ensures models load before they're - * used in setup() or draw(). + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * Note: There’s no support for colored STL files. STL files with color will * be rendered without color. * * @method loadModel - * @param {String} path path of the model to be loaded. + * @param {String|Request} path path of the model to be loaded. * @param {Boolean} normalize if `true`, scale the model to fit the canvas. * @param {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed * the p5.Geometry object. * @param {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object. * @param {String} [fileType] model’s file extension. Either `'.obj'` or `'.stl'`. - * @return {p5.Geometry} the p5.Geometry object + * @return {Promise} the p5.Geometry object * * @example *
@@ -99,11 +109,9 @@ function loading(p5, fn){ * let shape; * * // Load the file and create a p5.Geometry object. - * function preload() { - * shape = loadModel('assets/teapot.obj'); - * } + * async function setup() { + * shape = await loadModel('assets/teapot.obj'); * - * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white teapot drawn against a gray background.'); @@ -129,11 +137,9 @@ function loading(p5, fn){ * * // Load the file and create a p5.Geometry object. * // Normalize the geometry's size to fit the canvas. - * function preload() { - * shape = loadModel('assets/teapot.obj', true); - * } + * async function setup() { + * shape = await loadModel('assets/teapot.obj', true); * - * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white teapot drawn against a gray background.'); @@ -158,11 +164,9 @@ function loading(p5, fn){ * let shape; * * // Load the file and create a p5.Geometry object. - * function preload() { + * function setup() { * loadModel('assets/teapot.obj', true, handleModel); - * } * - * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white teapot drawn against a gray background.'); @@ -194,11 +198,9 @@ function loading(p5, fn){ * let shape; * * // Load the file and create a p5.Geometry object. - * function preload() { + * function setup() { * loadModel('assets/wrong.obj', true, handleModel, handleError); - * } * - * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white teapot drawn against a gray background.'); @@ -235,11 +237,9 @@ function loading(p5, fn){ * let shape; * * // Load the file and create a p5.Geometry object. - * function preload() { + * function setup() { * loadModel('assets/teapot.obj', true, handleModel, handleError, '.obj'); - * } * - * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white teapot drawn against a gray background.'); @@ -282,11 +282,9 @@ function loading(p5, fn){ * }; * * // Load the file and create a p5.Geometry object. - * function preload() { + * function setup() { * loadModel('assets/teapot.obj', options); - * } * - * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white teapot drawn against a gray background.'); @@ -318,15 +316,15 @@ function loading(p5, fn){ */ /** * @method loadModel - * @param {String} path + * @param {String|Request} path * @param {function(p5.Geometry)} [successCallback] * @param {function(Event)} [failureCallback] * @param {String} [fileType] - * @return {p5.Geometry} new p5.Geometry object. + * @return {Promise} new p5.Geometry object. */ /** * @method loadModel - * @param {String} path + * @param {String|Request} path * @param {Object} [options] loading options. * @param {function(p5.Geometry)} [options.successCallback] * @param {function(Event)} [options.failureCallback] @@ -334,48 +332,62 @@ function loading(p5, fn){ * @param {Boolean} [options.normalize] * @param {Boolean} [options.flipU] * @param {Boolean} [options.flipV] - * @return {p5.Geometry} new p5.Geometry object. + * @return {Promise} new p5.Geometry object. */ - fn.loadModel = async function (path, options) { + fn.loadModel = async function (path, fileType, normalize, successCallback, failureCallback) { p5._validateParameters('loadModel', arguments); - let normalize = false; - let successCallback; - let failureCallback; + let flipU = false; let flipV = false; - let fileType = path.slice(-4); - if (options && typeof options === 'object') { - normalize = options.normalize || false; - successCallback = options.successCallback; - failureCallback = options.failureCallback; - fileType = options.fileType || fileType; - flipU = options.flipU || false; - flipV = options.flipV || false; - } else if (typeof options === 'boolean') { - normalize = options; - successCallback = arguments[2]; - failureCallback = arguments[3]; - if (typeof arguments[4] !== 'undefined') { - fileType = arguments[4]; - } + + if (typeof fileType === 'object') { + // Passing in options object + normalize = fileType.normalize || false; + successCallback = fileType.successCallback; + failureCallback = fileType.failureCallback; + fileType = fileType.fileType || fileType; + flipU = fileType.flipU || false; + flipV = fileType.flipV || false; + } else { - successCallback = typeof arguments[1] === 'function' ? arguments[1] : undefined; - failureCallback = arguments[2]; - if (typeof arguments[3] !== 'undefined') { - fileType = arguments[3]; + // Passing in individual parameters + if(typeof arguments[arguments.length-1] === 'function'){ + if(typeof arguments[arguments.length-2] === 'function'){ + successCallback = arguments[arguments.length-2]; + failureCallback = arguments[arguments.length-1]; + }else{ + successCallback = arguments[arguments.length-1]; + } + } + + if (typeof fileType === 'string') { + if(typeof normalize !== 'boolean') normalize = false; + + } else if (typeof fileType === 'boolean') { + normalize = fileType; + fileType = path.slice(-4); + + } else { + fileType = path.slice(-4); + normalize = false; } } + if (fileType.toLowerCase() !== '.obj' && fileType.toLowerCase() !== '.stl') { + fileType = '.obj'; + } + const model = new Geometry(); model.gid = `${path}|${normalize}`; - const self = this; async function getMaterials(lines) { const parsedMaterialPromises = []; - for (let i = 0; i < lines.length; i++) { - const mtllibMatch = lines[i].match(/^mtllib (.+)/); + for (let line of lines) { + const mtllibMatch = line.match(/^mtllib (.+)/); + if (mtllibMatch) { + // Object has material let mtlPath = ''; const mtlFilename = mtllibMatch[1]; const objPathParts = path.split('/'); @@ -386,10 +398,11 @@ function loading(p5, fn){ } else { mtlPath = mtlFilename; } + parsedMaterialPromises.push( fileExists(mtlPath).then(exists => { if (exists) { - return parseMtl(self, mtlPath); + return parseMtl(mtlPath); } else { console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`); return {}; @@ -402,6 +415,7 @@ function loading(p5, fn){ ); } } + try { const parsedMaterials = await Promise.all(parsedMaterialPromises); const materials = Object.assign({}, ...parsedMaterials); @@ -411,135 +425,104 @@ function loading(p5, fn){ } } + try{ + if (fileType.match(/\.stl$/i)) { + const { data } = await request(path, 'arrayBuffer'); + parseSTL(model, data); - async function fileExists(url) { - try { - const response = await fetch(url, { method: 'HEAD' }); - return response.ok; - } catch (error) { - return false; - } - } - if (fileType.match(/\.stl$/i)) { - await new Promise(resolve => this.httpDo( - path, - 'GET', - 'arrayBuffer', - arrayBuffer => { - parseSTL(model, arrayBuffer); - - if (normalize) { - model.normalize(); - } + if (normalize) { + model.normalize(); + } - if (flipU) { - model.flipU(); - } + if (flipU) { + model.flipU(); + } - if (flipV) { - model.flipV(); - } + if (flipV) { + model.flipV(); + } - resolve(); - if (typeof successCallback === 'function') { - successCallback(model); - } - }, - failureCallback - )); - } else if (fileType.match(/\.obj$/i)) { - await new Promise(resolve => this.loadStrings( - path, - async lines => { - try { - const parsedMaterials = await getMaterials(lines); - - parseObj(model, lines, parsedMaterials); - - } catch (error) { - if (failureCallback) { - failureCallback(error); - } else { - p5._friendlyError('Error during parsing: ' + error.message); - } - return; - } - finally { - if (normalize) { - model.normalize(); - } - if (flipU) { - model.flipU(); - } - if (flipV) { - model.flipV(); - } + if (successCallback) { + return successCallback(model); + } else { + return model; + } - resolve(); - if (typeof successCallback === 'function') { - successCallback(model); - } - } - }, - failureCallback - )); - } else { + } else if (fileType.match(/\.obj$/i)) { + const { data } = await request(path, 'text'); + const lines = data.split('\n'); + + const parsedMaterials = await getMaterials(lines); + parseObj(model, lines, parsedMaterials); + + if (normalize) { + model.normalize(); + } + if (flipU) { + model.flipU(); + } + if (flipV) { + model.flipV(); + } + + if (successCallback) { + return successCallback(model); + } else { + return model; + } + } + } catch(err) { p5._friendlyFileLoadError(3, path); - if (failureCallback) { - failureCallback(); + if(failureCallback) { + return failureCallback(err); } else { - p5._friendlyError( - 'Sorry, the file type is invalid. Only OBJ and STL files are supported.' - ); + throw err; } } - return model; }; - function parseMtl(p5, mtlPath) { - return new Promise((resolve, reject) => { - let currentMaterial = null; - let materials = {}; - p5.loadStrings( - mtlPath, - lines => { - for (let line = 0; line < lines.length; ++line) { - const tokens = lines[line].trim().split(/\s+/); - if (tokens[0] === 'newmtl') { - const materialName = tokens[1]; - currentMaterial = materialName; - materials[currentMaterial] = {}; - } else if (tokens[0] === 'Kd') { - //Diffuse color - materials[currentMaterial].diffuseColor = [ - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ]; - } else if (tokens[0] === 'Ka') { - //Ambient Color - materials[currentMaterial].ambientColor = [ - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ]; - } else if (tokens[0] === 'Ks') { - //Specular color - materials[currentMaterial].specularColor = [ - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ]; - - } else if (tokens[0] === 'map_Kd') { - //Texture path - materials[currentMaterial].texturePath = tokens[1]; - } - } - resolve(materials); - }, reject - ); - }); + async function parseMtl(mtlPath) { + let currentMaterial = null; + let materials = {}; + + const { data } = await request(mtlPath, "text"); + const lines = data.split('\n'); + + for (let line = 0; line < lines.length; ++line) { + const tokens = lines[line].trim().split(/\s+/); + if (tokens[0] === 'newmtl') { + const materialName = tokens[1]; + currentMaterial = materialName; + materials[currentMaterial] = {}; + } else if (tokens[0] === 'Kd') { + //Diffuse color + materials[currentMaterial].diffuseColor = [ + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ]; + } else if (tokens[0] === 'Ka') { + //Ambient Color + materials[currentMaterial].ambientColor = [ + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ]; + } else if (tokens[0] === 'Ks') { + //Specular color + materials[currentMaterial].specularColor = [ + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ]; + + } else if (tokens[0] === 'map_Kd') { + //Texture path + materials[currentMaterial].texturePath = tokens[1]; + } + } + + return materials; } /** @@ -589,7 +572,7 @@ function loading(p5, fn){ } else if (tokens[0] === 'v' || tokens[0] === 'vn') { // Check if this line describes a vertex or vertex normal. // It will have three numeric parameters. - const vertex = new p5.Vector( + const vertex = new Vector( parseFloat(tokens[1]), parseFloat(tokens[2]), parseFloat(tokens[3]) @@ -632,7 +615,7 @@ function loading(p5, fn){ model.uvs.push(loadedVerts.vt[vertParts[1]] ? loadedVerts.vt[vertParts[1]].slice() : [0, 0]); model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ? - loadedVerts.vn[vertParts[2]].copy() : new p5.Vector()); + loadedVerts.vn[vertParts[2]].copy() : new Vector()); usedVerts[vertString][currentMaterial] = vertIndex; face.push(vertIndex); @@ -684,6 +667,7 @@ function loading(p5, fn){ // If both are true or both are false, throw an error because the model is inconsistent throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.'); } + return model; } diff --git a/src/webgl/material.js b/src/webgl/material.js index 0c0a8e035f..288d83f822 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -8,6 +8,7 @@ import * as constants from '../core/constants'; import { RendererGL } from './p5.RendererGL'; import { Shader } from './p5.Shader'; +import { request } from '../io/files'; function material(p5, fn){ /** @@ -34,26 +35,27 @@ function material(p5, fn){ * The third parameter, `successCallback`, is optional. If a function is * passed, it will be called once the shader has loaded. The callback function * can use the new p5.Shader object as its - * parameter. + * parameter. The return value of the `successCallback()` function will be used + * as the final return value of `loadShader()`. * * The fourth parameter, `failureCallback`, is also optional. If a function is * passed, it will be called if the shader fails to load. The callback - * function can use the event error as its parameter. + * function can use the event error as its parameter. The return value of the ` + * failureCallback()` function will be used as the final return value of `loadShader()`. * - * Shaders can take time to load. Calling `loadShader()` in - * preload() ensures shaders load before they're - * used in setup() or draw(). + * This function returns a `Promise` and should be used in an `async` setup with + * `await`. See the examples for the usage syntax. * * Note: Shaders can only be used in WebGL mode. * * @method loadShader - * @param {String} vertFilename path of the vertex shader to be loaded. - * @param {String} fragFilename path of the fragment shader to be loaded. + * @param {String|Request} vertFilename path of the vertex shader to be loaded. + * @param {String|Request} fragFilename path of the fragment shader to be loaded. * @param {Function} [successCallback] function to call once the shader is loaded. Can be passed the * p5.Shader object. * @param {Function} [failureCallback] function to call if the shader fails to load. Can be passed an * `Error` event object. - * @return {p5.Shader} new shader created from the vertex and fragment shader files. + * @return {Promise} new shader created from the vertex and fragment shader files. * * @example *
@@ -63,11 +65,9 @@ function material(p5, fn){ * let mandelbrot; * * // Load the shader and create a p5.Shader object. - * function preload() { - * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); - * } + * async function setup() { + * mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag'); * - * function setup() { * createCanvas(100, 100, WEBGL); * * // Compile and apply the p5.Shader object. @@ -94,11 +94,9 @@ function material(p5, fn){ * let mandelbrot; * * // Load the shader and create a p5.Shader object. - * function preload() { - * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); - * } + * async function setup() { + * mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag'); * - * function setup() { * createCanvas(100, 100, WEBGL); * * // Use the p5.Shader object. @@ -120,55 +118,32 @@ function material(p5, fn){ * *
*/ - fn.loadShader = function ( + fn.loadShader = async function ( vertFilename, fragFilename, successCallback, failureCallback ) { p5._validateParameters('loadShader', arguments); - if (!failureCallback) { - failureCallback = console.error; - } const loadedShader = new Shader(); - const self = this; - let loadedFrag = false; - let loadedVert = false; + try { + loadedShader._vertSrc = await request(vertFilename, 'text'); + loadedShader._fragSrc = await request(fragFilename, 'text'); - const onLoad = () => { - self._decrementPreload(); if (successCallback) { - successCallback(loadedShader); + return successCallback(loadedShader); + } else { + return loadedShader } - }; - - this.loadStrings( - vertFilename, - result => { - loadedShader._vertSrc = result.join('\n'); - loadedVert = true; - if (loadedFrag) { - onLoad(); - } - }, - failureCallback - ); - - this.loadStrings( - fragFilename, - result => { - loadedShader._fragSrc = result.join('\n'); - loadedFrag = true; - if (loadedVert) { - onLoad(); - } - }, - failureCallback - ); - - return loadedShader; + } catch(err) { + if (failureCallback) { + return failureCallback(err); + } else { + throw err; + } + } }; /** diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index f912f552bd..7be2831076 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -6,7 +6,6 @@ * @requires core */ -// import p5 from '../core/main'; import * as constants from '../core/constants'; import { Element } from '../dom/p5.Element'; import { Renderer } from '../core/p5.Renderer'; diff --git a/test/js/mocks.js b/test/js/mocks.js new file mode 100644 index 0000000000..c70284553c --- /dev/null +++ b/test/js/mocks.js @@ -0,0 +1,34 @@ +import { vi } from 'vitest'; +import { http, HttpResponse, passthrough } from 'msw'; +import { setupWorker } from 'msw/browser'; + +// HTTP requests mocks +const httpMocks = [ + http.get('404file', () => { + return new HttpResponse('Not Found', { + status: 404, + statusText: 'Not Found', + }); + }), + http.all('*', ({request}) => { + return passthrough(); + }) +]; + +export const httpMock = setupWorker(...httpMocks); + +// p5.js module mocks +export const mockP5 = { + _validateParameters: vi.fn(), + _friendlyFileLoadError: vi.fn(), + _friendlyError: vi.fn() +}; + +const mockCanvas = document.createElement('canvas'); +export const mockP5Prototype = { + saveCanvas: vi.fn(), + elt: mockCanvas, + _curElement: { + elt: mockCanvas + } +}; diff --git a/test/js/p5_helpers.js b/test/js/p5_helpers.js index a8a72daf24..363c0d462e 100644 --- a/test/js/p5_helpers.js +++ b/test/js/p5_helpers.js @@ -25,66 +25,6 @@ export function testSketchWithPromise(name, sketch_fn) { return test(name, test_fn); } -export function testWithDownload(name, fn, asyncFn = false) { - const test_fn = function() { - return new Promise((resolve, reject) => { - let blobContainer = {}; - - const prevClick = HTMLAnchorElement.prototype.click; - const prevDispatchEvent = HTMLAnchorElement.prototype.dispatchEvent; - const blockDownloads = () => { - HTMLAnchorElement.prototype.click = () => {}; - HTMLAnchorElement.prototype.dispatchEvent = () => {}; - } - const unblockDownloads = () => { - HTMLAnchorElement.prototype.click = prevClick; - HTMLAnchorElement.prototype.dispatchEvent = prevDispatchEvent; - } - - // create a backup of createObjectURL - let couBackup = window.URL.createObjectURL; - - // file-saver uses createObjectURL as an intermediate step. If we - // modify the definition a just a little bit we can capture whenever - // it is called and also peek in the data that was passed to it - window.URL.createObjectURL = blob => { - blobContainer.blob = blob; - return couBackup(blob); - }; - blockDownloads(); - - let error; - if (asyncFn) { - fn(blobContainer) - .then(() => { - window.URL.createObjectURL = couBackup; - }) - .catch(err => { - error = err; - }) - .finally(() => { - // restore createObjectURL to the original one - window.URL.createObjectURL = couBackup; - error ? reject(error) : resolve(); - unblockDownloads(); - }); - } else { - try { - fn(blobContainer); - } catch (err) { - error = err; - } - // restore createObjectURL to the original one - window.URL.createObjectURL = couBackup; - error ? reject(error) : resolve(); - unblockDownloads(); - } - }); - }; - - return test(name, test_fn); -} - // Tests should run only for the unminified script export function testUnMinified(name, test_fn) { return !window.IS_TESTING_MINIFIED_VERSION ? test(name, test_fn) : null; diff --git a/test/mockServiceWorker.js b/test/mockServiceWorker.js new file mode 100644 index 0000000000..89bce29129 --- /dev/null +++ b/test/mockServiceWorker.js @@ -0,0 +1,295 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.6.5' +const INTEGRITY_CHECKSUM = 'ca7800994cc8bfb5eb961e037c877074' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + headers.delete('accept', 'msw/passthrough') + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/test/unit/events/mouse.js b/test/unit/events/mouse.js index e17e7fef77..0165d4199a 100644 --- a/test/unit/events/mouse.js +++ b/test/unit/events/mouse.js @@ -1,7 +1,7 @@ import p5 from '../../../src/app.js'; import { parallelSketches } from '../../js/p5_helpers'; -suite('Mouse Events', function() { +suite.todo('Mouse Events', function() { let myp5; let canvas; @@ -200,7 +200,7 @@ suite('Mouse Events', function() { assert.isNumber(myp5.pwinMouseY); }); - test('pwinMouseY should be previous vertical position of mouse relative to the window', function() { + test('pwinMouseY should be previous vertical position of mouse relative to the window', async function() { window.dispatchEvent(mouseEvent1); // dispatch first mouse event window.dispatchEvent(mouseEvent2); // dispatch second mouse event assert.strictEqual(myp5.pwinMouseY, mouseEvent1.clientY); diff --git a/test/unit/image/downloading.js b/test/unit/image/downloading.js index 612653cdf9..d2bdd4a794 100644 --- a/test/unit/image/downloading.js +++ b/test/unit/image/downloading.js @@ -1,214 +1,199 @@ -import p5 from '../../../src/app.js'; -import { testWithDownload } from '../../js/p5_helpers'; - -suite.todo('downloading animated gifs', function() { - let myp5; - let myGif; - - beforeAll(function() { - return new Promise(resolve => { - new p5(function(p) { - p.setup = function() { - myp5 = p; - resolve(); - }; - }); - }); - }); - - afterAll(function() { - myp5.remove(); - }); +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import * as fileSaver from 'file-saver'; +import { vi } from 'vitest'; +import image from '../../../src/image/image'; +import files from '../../../src/io/files'; +import loading from '../../../src/image/loading_displaying'; +import p5Image from '../../../src/image/p5.Image'; + +vi.mock('file-saver'); + +expect.extend({ + tobeGif: (received) => { + if (received.type === 'image/gif') { + return { + message: 'expect blob to have type image/gif', + pass: true + } + } else { + return { + message: 'expect blob to have type image/gif', + pass: false + } + } + }, + tobePng: (received) => { + if (received.type === 'image/png') { + return { + message: 'expect blob to have type image/png', + pass: true + } + } else { + return { + message: 'expect blob to have type image/png', + pass: false + } + } + }, + tobeJpg: (received) => { + if (received.type === 'image/jpeg') { + return { + message: 'expect blob to have type image/jpeg', + pass: true + } + } else { + return { + message: 'expect blob to have type image/jpeg', + pass: false + } + } + } +}); - let imagePath = 'unit/assets/nyan_cat.gif'; +const wait = async (time) => { + return new Promise(resolve => setTimeout(resolve, time)); +} - beforeEach(function loadMyGif(done) { - myp5.loadImage(imagePath, function(pImg) { - myGif = pImg; - done(); - }); +suite('Downloading', () => { + beforeAll(async function() { + image(mockP5, mockP5Prototype); + files(mockP5, mockP5Prototype); + loading(mockP5, mockP5Prototype); + p5Image(mockP5, mockP5Prototype); }); - suite('p5.prototype.encodeAndDownloadGif', function() { - test('should be a function', function() { - assert.ok(myp5.encodeAndDownloadGif); - assert.typeOf(myp5.encodeAndDownloadGif, 'function'); - }); - test('should not throw an error', function() { - myp5.encodeAndDownloadGif(myGif); - }); - testWithDownload('should download a gif', function(blobContainer) { - myp5.encodeAndDownloadGif(myGif); - let gifBlob = blobContainer.blob; - assert.strictEqual(gifBlob.type, 'image/gif'); - }); + afterEach(() => { + vi.clearAllMocks(); }); -}); -suite.todo('p5.prototype.saveCanvas', function() { - let myp5; + suite('downloading animated gifs', function() { + let myGif; + const imagePath = '/test/unit/assets/nyan_cat.gif'; - let waitForBlob = async function(blc) { - let sleep = function(ms) { - return new Promise(r => setTimeout(r, ms)); - }; - while (!blc.blob) { - await sleep(5); - } - }; - - beforeAll(function() { - return new Promise(resolve => { - new p5(function(p) { - p.setup = function() { - myp5 = p; - myCanvas = p.createCanvas(20, 20); - p.background(255, 0, 0); - resolve(); - }; - }); + beforeAll(async function() { + myGif = await mockP5Prototype.loadImage(imagePath); }); - }); - - afterAll(function() { - myp5.remove(); - }); - - test('should be a function', function() { - assert.ok(myp5.saveCanvas); - assert.typeOf(myp5.saveCanvas, 'function'); - }); - testWithDownload( - 'should download a png file', - async function(blobContainer) { - myp5.saveCanvas(); - // since a function with callback is used in saveCanvas - // until the blob is made available to us. - await waitForBlob(blobContainer); - let myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/png'); - }, - true - ); - - testWithDownload( - 'should download a jpg file I', - async function(blobContainer) { - myp5.saveCanvas('filename.jpg'); - await waitForBlob(blobContainer); - let myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/jpeg'); - }, - true - ); - - testWithDownload( - 'should download a jpg file II', - async function(blobContainer) { - myp5.saveCanvas('filename', 'jpg'); - await waitForBlob(blobContainer); - let myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/jpeg'); - }, - true - ); -}); + suite('p5.prototype.encodeAndDownloadGif', function() { + test('should be a function', function() { + assert.ok(mockP5Prototype.encodeAndDownloadGif); + assert.typeOf(mockP5Prototype.encodeAndDownloadGif, 'function'); + }); -suite('p5.prototype.saveFrames', function() { - let myp5; + test('should not throw an error', function() { + mockP5Prototype.encodeAndDownloadGif(myGif); + }); - beforeAll(function() { - return new Promise(resolve => { - new p5(function(p) { - p.setup = function() { - myp5 = p; - p.createCanvas(10, 10); - resolve(); - }; + test('should download a gif', async () => { + mockP5Prototype.encodeAndDownloadGif(myGif); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.tobeGif(), + 'untitled.gif' + ); }); }); }); - afterAll(function() { - myp5.remove(); - }); + suite('p5.prototype.saveCanvas', function() { + test('should be a function', function() { + assert.ok(mockP5Prototype.saveCanvas); + assert.typeOf(mockP5Prototype.saveCanvas, 'function'); + }); - test('should be a function', function() { - assert.ok(myp5.saveFrames); - assert.typeOf(myp5.saveFrames, 'function'); - }); + test('should download a png file', async () => { + mockP5Prototype.saveCanvas(); + await wait(100); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.tobePng(), + 'untitled.png' + ); + }); - test('should get frames in callback (png)', function(done) { - myp5.saveFrames('aaa', 'png', 0.5, 25, function cb1(arr) { - assert.typeOf(arr, 'array', 'we got an array'); - for (let i = 0; i < arr.length; i++) { - assert.ok(arr[i].imageData); - assert.strictEqual(arr[i].ext, 'png'); - assert.strictEqual(arr[i].filename, `aaa${i}`); - } - done(); + test('should download a jpg file I', async () => { + mockP5Prototype.saveCanvas('filename.jpg'); + await wait(100); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.tobeJpg(), + 'filename.jpg' + ); }); - }); - test('should get frames in callback (jpg)', function(done) { - myp5.saveFrames('bbb', 'jpg', 0.5, 25, function cb2(arr2) { - assert.typeOf(arr2, 'array', 'we got an array'); - for (let i = 0; i < arr2.length; i++) { - assert.ok(arr2[i].imageData); - assert.strictEqual(arr2[i].ext, 'jpg'); - assert.strictEqual(arr2[i].filename, `bbb${i}`); - } - done(); + test('should download a jpg file II', async () => { + mockP5Prototype.saveCanvas('filename', 'jpg'); + await wait(100); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.tobeJpg(), + 'filename.jpg' + ); }); }); -}); -suite('p5.prototype.saveGif', function() { - let myp5; + suite('p5.prototype.saveFrames', function() { + test('should be a function', function() { + assert.ok(mockP5Prototype.saveFrames); + assert.typeOf(mockP5Prototype.saveFrames, 'function'); + }); - let waitForBlob = async function(blc) { - let sleep = function(ms) { - return new Promise(r => setTimeout(r, ms)); - }; - while (!blc.blob) { - await sleep(5); - } - }; - - beforeAll(function() { - return new Promise(resolve => { - new p5(function(p) { - p.setup = function() { - myp5 = p; - p.createCanvas(10, 10); + test('should get frames in callback (png)', async () => { + return new Promise(resolve => { + mockP5Prototype.saveFrames('aaa', 'png', 0.5, 25, function cb1(arr) { + assert.typeOf(arr, 'array', 'we got an array'); + for (let i = 0; i < arr.length; i++) { + assert.ok(arr[i].imageData); + assert.equal(arr[i].ext, 'png'); + assert.equal(arr[i].filename, `aaa${i}`); + } resolve(); - }; + }); }); }); - }); - afterAll(function() { - myp5.remove(); + test('should get frames in callback (png)', async () => { + return new Promise(resolve => { + mockP5Prototype.saveFrames('aaa', 'jpg', 0.5, 25, function cb1(arr) { + assert.typeOf(arr, 'array', 'we got an array'); + for (let i = 0; i < arr.length; i++) { + assert.ok(arr[i].imageData); + assert.equal(arr[i].ext, 'jpg'); + assert.equal(arr[i].filename, `aaa${i}`); + } + resolve(); + }); + }); + }); }); - test('should be a function', function() { - assert.ok(myp5.saveGif); - assert.typeOf(myp5.saveGif, 'function'); - }); + suite('p5.prototype.saveGif', function() { + test('should be a function', function() { + assert.ok(mockP5Prototype.saveGif); + assert.typeOf(mockP5Prototype.saveGif, 'function'); + }); - test('should not throw an error', function() { - myp5.saveGif('myGif', 3); - }); + // TODO: this implementation need refactoring + test.todo('should not throw an error', async () => { + await mockP5Prototype.saveGif('myGif', 3); + }); - test('should not throw an error', function() { - myp5.saveGif('myGif', 3, { delay: 2, frames: 'seconds' }); - }); + test.todo('should not throw an error', async () => { + await mockP5Prototype.saveGif('myGif', 3, { delay: 2, frames: 'seconds' }); + }); - testWithDownload('should download a GIF', async function(blobContainer) { - myp5.saveGif('myGif', 3, 2); - await waitForBlob(blobContainer); - let gifBlob = blobContainer.blob; - assert.strictEqual(gifBlob.type, 'image/gif'); + test.todo('should download a GIF', async () => { + await mockP5Prototype.saveGif('myGif', 3, 2); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.tobeGif(), + 'myGif.gif' + ); + }); }); }); diff --git a/test/unit/image/loading.js b/test/unit/image/loading.js index 4b81d1193f..801705294e 100644 --- a/test/unit/image/loading.js +++ b/test/unit/image/loading.js @@ -1,3 +1,7 @@ +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import loadingDisplaying from '../../../src/image/loading_displaying'; +import image from '../../../src/image/p5.Image'; + import p5 from '../../../src/app.js'; import { vi } from 'vitest'; @@ -28,374 +32,212 @@ var testImageRender = function(file, sketch) { }); }; -suite.todo('loading images', function() { - var myp5; - - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - - // Make sure draw() exists so timing functions still run each frame - // and we can test gif animation - p.draw = function() {}; - }); - }); +suite('loading images', function() { + const imagePath = '/test/unit/assets/cat.jpg'; + const singleFrameGif = '/test/unit/assets/target_small.gif'; + const animatedGif = '/test/unit/assets/white_black.gif'; + const nyanCatGif = '/test/unit/assets/nyan_cat.gif'; + const disposeNoneGif = '/test/unit/assets/dispose_none.gif'; + const disposeBackgroundGif = '/test/unit/assets/dispose_background.gif'; + const disposePreviousGif = '/test/unit/assets/dispose_previous.gif'; + const invalidFile = '404file'; - afterAll(function() { - myp5.remove(); + beforeAll(async function() { + loadingDisplaying(mockP5, mockP5Prototype); + image(mockP5, mockP5Prototype); + await httpMock.start({quiet: true}); }); - var imagePath = 'unit/assets/cat.jpg'; - - test('should call successCallback when image loads', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage(imagePath, resolve, reject); - }).then(function(pImg) { - assert.ok(pImg, 'cat.jpg loaded'); - assert.isTrue(pImg instanceof p5.Image); - }); + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadImage(invalidFile)) + .rejects + .toThrow('Not Found'); }); - test('should call failureCallback when unable to load image', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage( - 'invalid path', - function(pImg) { - reject('Entered success callback.'); - }, - resolve - ); - }).then(function(event) { - assert.equal(event.type, 'error'); - }); - }); - - test('should draw image with defaults', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/cat.jpg', resolve, reject); - }).then(function(img) { - myp5.image(img, 0, 0); - return testImageRender('unit/assets/cat.jpg', myp5).then(function(res) { - assert.isTrue(res); + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadImage(invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); }); }); }); - test('static image should not have gifProperties', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/cat.jpg', resolve, reject); - }).then(function(img) { - assert.isTrue(img.gifProperties === null); + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadImage(imagePath, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); }); }); - test('single frame GIF should not have gifProperties', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/target_small.gif', resolve, reject); - }).then(function(img) { - assert.isTrue(img.gifProperties === null); - }); + test('returns an object with correct data', async () => { + const pImg = await mockP5Prototype.loadImage(imagePath); + assert.ok(pImg, 'cat.jpg loaded'); + assert.isTrue(pImg instanceof mockP5.Image); }); - test('first frame of GIF should be painted after load', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/white_black.gif', resolve, reject); - }).then(function(img) { - assert.deepEqual(img.get(0, 0), [255, 255, 255, 255]); + test('passes an object with correct data to success callback', async () => { + await mockP5Prototype.loadImage(imagePath, (pImg) => { + assert.ok(pImg, 'cat.jpg loaded'); + assert.isTrue(pImg instanceof mockP5.Image); }); }); - test('animated gifs animate correctly', function() { - const wait = function(ms) { - return new Promise(function(resolve) { - setTimeout(resolve, ms); - }); - }; - let img; - return new Promise(function(resolve, reject) { - img = myp5.loadImage('unit/assets/nyan_cat.gif', resolve, reject); - }).then(function() { - assert.equal(img.gifProperties.displayIndex, 0); - myp5.image(img, 0, 0); - - // This gif has frames that are around for 100ms each. - // After 100ms has elapsed, the display index should - // increment when we draw the image. - return wait(100); - }).then(function() { - return new Promise(function(resolve) { - window.requestAnimationFrame(resolve); - }); - }).then(function() { - myp5.image(img, 0, 0); - assert.equal(img.gifProperties.displayIndex, 1); - }); - }); + // TODO: this is more of an integration test, possibly delegate to visual test + // test('should draw image with defaults', function() { + // return new Promise(function(resolve, reject) { + // myp5.loadImage('unit/assets/cat.jpg', resolve, reject); + // }).then(function(img) { + // myp5.image(img, 0, 0); + // return testImageRender('unit/assets/cat.jpg', myp5).then(function(res) { + // assert.isTrue(res); + // }); + // }); + // }); - var backgroundColor = [135, 206, 235, 255]; - var blue = [0, 0, 255, 255]; - var transparent = [0, 0, 0, 0]; - test('animated gifs work with no disposal', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/dispose_none.gif', resolve, reject); - }).then(function(img) { - // Frame 0 shows the background - assert.deepEqual(img.get(7, 12), backgroundColor); - // Frame 1 draws on top of the background - img.setFrame(1); - assert.deepEqual(img.get(7, 12), blue); - // Frame 2 does not erase untouched parts of frame 2 - img.setFrame(2); - assert.deepEqual(img.get(7, 12), blue); - }); + test('static image should not have gifProperties', async () => { + const img = await mockP5Prototype.loadImage(imagePath); + assert.isNull(img.gifProperties); }); - test('animated gifs work with background disposal', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/dispose_background.gif', resolve, reject); - }).then(function(img) { - // Frame 0 shows the background - assert.deepEqual(img.get(7, 12), backgroundColor); - // Frame 1 draws on top of the background - img.setFrame(1); - assert.deepEqual(img.get(7, 12), blue); - // Frame 2 erases the content added in frame 2 - img.setFrame(2); - assert.deepEqual(img.get(7, 12), transparent); - }); + test('single frame GIF should not have gifProperties', async () => { + const img = await mockP5Prototype.loadImage(singleFrameGif); + assert.isNull(img.gifProperties); }); - test('animated gifs work with previous disposal', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/dispose_previous.gif', resolve, reject); - }).then(function(img) { - // Frame 0 shows the background - assert.deepEqual(img.get(7, 12), backgroundColor); - // Frame 1 draws on top of the background - img.setFrame(1); - assert.deepEqual(img.get(7, 12), blue); - // Frame 2 returns the content added in frame 2 to its previous value - img.setFrame(2); - assert.deepEqual(img.get(7, 12), backgroundColor); - }); + test('first frame of GIF should be painted after load', async () => { + const img = await mockP5Prototype.loadImage(animatedGif); + assert.deepEqual(img.get(0, 0), [255, 255, 255, 255]); }); - /* TODO: make this resilient to platform differences in image resizing. - test('should draw cropped image', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage('unit/assets/target.gif', resolve, reject); - }).then(function(img) { - myp5.image(img, 0, 0, 6, 6, 5, 5, 6, 6); - return testImageRender('unit/assets/target_small.gif', myp5).then( - function(res) { - assert.isTrue(res); - } - ); - }); - }); - */ - - // Test loading image in preload() with success callback - // test('Test in preload() with success callback'); - // test('Test in setup() after preload()'); - // These tests don't work correctly (You can't use suite and test like that) - // they simply get added at the root level. - // var mySketch = function(this_p5) { - // var myImage; - // this_p5.preload = function() { - // suite('Test in preload() with success callback', function() { - // test('Load asynchronously and use success callback', function(done) { - // myImage = this_p5.loadImage('unit/assets/cat.jpg', function() { - // assert.ok(myImage); - // done(); - // }); - // }); + // test('animated gifs animate correctly', function() { + // const wait = function(ms) { + // return new Promise(function(resolve) { + // setTimeout(resolve, ms); // }); // }; - - // this_p5.setup = function() { - // suite('setup() after preload() with success callback', function() { - // test('should be loaded if preload() finished', function(done) { - // assert.isTrue(myImage instanceof p5.Image); - // assert.isTrue(myImage.width > 0 && myImage.height > 0); - // done(); - // }); + // let img; + // return new Promise(function(resolve, reject) { + // img = myp5.loadImage('unit/assets/nyan_cat.gif', resolve, reject); + // }).then(function() { + // assert.equal(img.gifProperties.displayIndex, 0); + // myp5.image(img, 0, 0); + + // // This gif has frames that are around for 100ms each. + // // After 100ms has elapsed, the display index should + // // increment when we draw the image. + // return wait(100); + // }).then(function() { + // return new Promise(function(resolve) { + // window.requestAnimationFrame(resolve); // }); - // }; - // }; - // new p5(mySketch, null, false); - - // // Test loading image in preload() without success callback - // mySketch = function(this_p5) { - // var myImage; - // this_p5.preload = function() { - // myImage = this_p5.loadImage('unit/assets/cat.jpg'); - // }; - - // this_p5.setup = function() { - // suite('setup() after preload() without success callback', function() { - // test('should be loaded now preload() finished', function(done) { - // assert.isTrue(myImage instanceof p5.Image); - // assert.isTrue(myImage.width > 0 && myImage.height > 0); - // done(); - // }); - // }); - // }; - // }; - // new p5(mySketch, null, false); - - // // Test loading image failure in preload() without failure callback - // mySketch = function(this_p5) { - // this_p5.preload = function() { - // this_p5.loadImage('', function() { - // throw new Error('Should not be called'); - // }); - // }; - - // this_p5.setup = function() { - // throw new Error('Should not be called'); - // }; - // }; - // new p5(mySketch, null, false); - - // // Test loading image failure in preload() with failure callback - // mySketch = function(this_p5) { - // var myImage; - // this_p5.preload = function() { - // suite('Test loading image failure in preload() with failure callback', function() { - // test('Load fail and use failure callback', function(done) { - // myImage = this_p5.loadImage('', function() { - // assert.fail(); - // done(); - // }, function() { - // assert.ok(myImage); - // done(); - // }); - // }); - // }); - // }; - - // this_p5.setup = function() { - // suite('setup() after preload() failure with failure callback', function() { - // test('should be loaded now preload() finished', function(done) { - // assert.isTrue(myImage instanceof p5.Image); - // assert.isTrue(myImage.width === 1 && myImage.height === 1); - // done(); - // }); - // }); - // }; - // }; - // new p5(mySketch, null, false); -}); - -suite.todo('loading animated gif images', function() { - var myp5; + // }).then(function() { + // myp5.image(img, 0, 0); + // assert.equal(img.gifProperties.displayIndex, 1); + // }); + // }); - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + const backgroundColor = [135, 206, 235, 255]; + const blue = [0, 0, 255, 255]; + const transparent = [0, 0, 0, 0]; + test('animated gifs work with no disposal', async () => { + const img = await mockP5Prototype.loadImage(disposeNoneGif); + // Frame 0 shows the background + assert.deepEqual(img.get(7, 12), backgroundColor); + // Frame 1 draws on top of the background + img.setFrame(1); + assert.deepEqual(img.get(7, 12), blue); + // Frame 2 does not erase untouched parts of frame 2 + img.setFrame(2); + assert.deepEqual(img.get(7, 12), blue); }); - afterAll(function() { - myp5.remove(); + test('animated gifs work with background disposal', async () => { + const img = await mockP5Prototype.loadImage(disposeBackgroundGif); + // Frame 0 shows the background + assert.deepEqual(img.get(7, 12), backgroundColor); + // Frame 1 draws on top of the background + img.setFrame(1); + assert.deepEqual(img.get(7, 12), blue); + // Frame 2 erases the content added in frame 2 + img.setFrame(2); + assert.deepEqual(img.get(7, 12), transparent); }); - var imagePath = 'unit/assets/nyan_cat.gif'; - - test('should call successCallback when image loads', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage(imagePath, resolve, reject); - }).then(function(pImg) { - assert.ok(pImg, 'nyan_cat.gif loaded'); - assert.isTrue(pImg instanceof p5.Image); - }); + test('animated gifs work with previous disposal', async () => { + const img = await mockP5Prototype.loadImage(disposePreviousGif); + // Frame 0 shows the background + assert.deepEqual(img.get(7, 12), backgroundColor); + // Frame 1 draws on top of the background + img.setFrame(1); + assert.deepEqual(img.get(7, 12), blue); + // Frame 2 returns the content added in frame 2 to its previous value + img.setFrame(2); + assert.deepEqual(img.get(7, 12), backgroundColor); }); - test('should call failureCallback when unable to load image', function() { - return new Promise(function(resolve, reject) { - myp5.loadImage( - 'invalid path', - function(pImg) { - reject('Entered success callback.'); - }, - resolve + // /* TODO: make this resilient to platform differences in image resizing. + // test('should draw cropped image', function() { + // return new Promise(function(resolve, reject) { + // myp5.loadImage('unit/assets/target.gif', resolve, reject); + // }).then(function(img) { + // myp5.image(img, 0, 0, 6, 6, 5, 5, 6, 6); + // return testImageRender('unit/assets/target_small.gif', myp5).then( + // function(res) { + // assert.isTrue(res); + // } + // ); + // }); + // }); + // */ + + test('should construct gifProperties correctly after preload', async () => { + const gifImage = await mockP5Prototype.loadImage(nyanCatGif); + assert.isTrue(gifImage instanceof p5.Image); + + const nyanCatGifProperties = { + displayIndex: 0, + loopCount: 0, + loopLimit: null, + numFrames: 6, + playing: true, + timeDisplayed: 0 + }; + assert.isTrue(gifImage.gifProperties !== null); + for (let prop in nyanCatGifProperties) { + assert.deepEqual( + gifImage.gifProperties[prop], + nyanCatGifProperties[prop] ); - }).then(function(event) { - assert.equal(event.type, 'error'); - }); - }); + } + assert.deepEqual( + gifImage.gifProperties.numFrames, + gifImage.gifProperties.frames.length + ); + for (let i = 0; i < gifImage.gifProperties.numFrames; i++) { + assert.isTrue( + gifImage.gifProperties.frames[i].image instanceof ImageData + ); + assert.isTrue(gifImage.gifProperties.frames[i].delay === 100); + } - // test('should construct gifProperties correctly after preload', function() { - // var mySketch = function(this_p5) { - // var gifImage; - // this_p5.preload = function() { - // suite('Test in preload() with success callback', function() { - // test('Load asynchronously and use success callback', function(done) { - // gifImage = this_p5.loadImage(imagePath, function() { - // assert.ok(gifImage); - // done(); - // }); - // }); - // }); - // }; - - // this_p5.setup = function() { - // suite('setup() after preload() with success callback', function() { - // test('should be loaded if preload() finished', function(done) { - // assert.isTrue(gifImage instanceof p5.Image); - // assert.isTrue(gifImage.width > 0 && gifImage.height > 0); - // done(); - // }); - // test('gifProperties should be correct after preload', function done() { - // assert.isTrue(gifImage instanceof p5.Image); - // var nyanCatGifProperties = { - // displayIndex: 0, - // loopCount: 0, - // loopLimit: null, - // numFrames: 6, - // playing: true, - // timeDisplayed: 0 - // }; - // assert.isTrue(gifImage.gifProperties !== null); - // for (var prop in nyanCatGifProperties) { - // assert.deepEqual( - // gifImage.gifProperties[prop], - // nyanCatGifProperties[prop] - // ); - // } - // assert.deepEqual( - // gifImage.gifProperties.numFrames, - // gifImage.gifProperties.frames.length - // ); - // for (var i = 0; i < gifImage.gifProperties.numFrames; i++) { - // assert.isTrue( - // gifImage.gifProperties.frames[i].image instanceof ImageData - // ); - // assert.isTrue(gifImage.gifProperties.frames[i].delay === 100); - // } - // }); - // test('should be able to modify gifProperties state', function() { - // assert.isTrue(gifImage.gifProperties.timeDisplayed === 0); - // gifImage.pause(); - // assert.isTrue(gifImage.gifProperties.playing === false); - // gifImage.play(); - // assert.isTrue(gifImage.gifProperties.playing === true); - // gifImage.setFrame(2); - // assert.isTrue(gifImage.gifProperties.displayIndex === 2); - // gifImage.reset(); - // assert.isTrue(gifImage.gifProperties.displayIndex === 0); - // assert.isTrue(gifImage.gifProperties.timeDisplayed === 0); - // }); - // }); - // }; - // }; - // new p5(mySketch, null, false); - // }); + assert.equal(gifImage.gifProperties.timeDisplayed, 0); + gifImage.pause(); + assert.isFalse(gifImage.gifProperties.playing); + gifImage.play(); + assert.isTrue(gifImage.gifProperties.playing); + gifImage.setFrame(2); + assert.equal(gifImage.gifProperties.displayIndex, 2); + gifImage.reset(); + assert.equal(gifImage.gifProperties.displayIndex, 0); + assert.equal(gifImage.gifProperties.timeDisplayed, 0); + }); }); suite.todo('displaying images', function() { diff --git a/test/unit/io/files.js b/test/unit/io/files.js index 4d23fb8b66..bde79ddf91 100644 --- a/test/unit/io/files.js +++ b/test/unit/io/files.js @@ -1,288 +1,167 @@ -import p5 from '../../../src/app.js'; -import { testWithDownload } from '../../js/p5_helpers'; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import files from '../../../src/io/files'; +import { vi } from 'vitest'; +import * as fileSaver from 'file-saver'; -suite('Files', function() { - var myp5; +vi.mock('file-saver'); - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); +suite('Files', function() { + beforeAll(async function() { + files(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - afterAll(function() { - myp5.remove(); + afterEach(() => { + vi.clearAllMocks(); }); // httpDo suite('httpDo()', function() { - test('should be a function', function() { - assert.ok(myp5.httpDo); - assert.isFunction(myp5.httpDo); - }); - - test('should work when provided with just a path', function() { - return new Promise(function(resolve, reject) { - myp5.httpDo('unit/assets/sentences.txt', resolve, reject); - }).then(function(data) { - assert.ok(data); - assert.isString(data); - }); + test('should work when provided with just a path', async function() { + const data = await mockP5Prototype.httpDo('/test/unit/assets/sentences.txt'); + assert.ok(data); + assert.isString(data); }); - test('should accept method parameter', function() { - return new Promise(function(resolve, reject) { - myp5.httpDo('unit/assets/sentences.txt', 'GET', resolve, reject); - }).then(function(data) { - assert.ok(data); - assert.isString(data); - }); + test('should accept method parameter', async function() { + const data = await mockP5Prototype.httpDo('/test/unit/assets/sentences.txt', 'GET'); + assert.ok(data); + assert.isString(data); }); - test('should accept type parameter', function() { - return new Promise(function(resolve, reject) { - myp5.httpDo('unit/assets/array.json', 'text', resolve, reject); - }).then(function(data) { - assert.ok(data); - assert.isString(data); - }); + test('should accept method and type parameter together', async function() { + const data = await mockP5Prototype.httpDo('/test/unit/assets/sentences.txt', 'GET', 'text'); + assert.ok(data); + assert.isString(data); }); - test('should accept method and type parameter together', function() { - return new Promise(function(resolve, reject) { - myp5.httpDo('unit/assets/array.json', 'GET', 'text', resolve, reject); - }).then(function(data) { - assert.ok(data); - assert.isString(data); - }); - }); - - test.todo('should pass error object to error callback function', function() { - return new Promise(function(resolve, reject) { - myp5.httpDo( - 'unit/assets/sen.txt', - function(data) { - console.log(data); - reject('Incorrectly succeeded.'); - }, - resolve - ); - }).then(function(err) { - assert.isFalse(err.ok, 'err.ok is false'); - assert.equal(err.status, 404, 'Error status is 404'); - }); - }); - - test('should return a promise', function() { - var promise = myp5.httpDo('unit/assets/sentences.txt'); - assert.instanceOf(promise, Promise); - return promise.then(function(data) { - assert.ok(data); - assert.isString(data); - }); - }); - - test('should return a promise that rejects on error', function() { - return new Promise(function(resolve, reject) { - var promise = myp5.httpDo('404file'); - assert.instanceOf(promise, Promise); - promise.then(function(data) { - reject(new Error('promise resolved.')); - }); - resolve( - promise.catch(function(error) { - assert.instanceOf(error, Error); - }) - ); - }); + test('should handle promise error correctly', async function() { + await expect(mockP5Prototype.httpDo('/test/unit/assets/sen.txt')) + .rejects + .toThrow('Not Found'); }); }); // saveStrings() suite('p5.prototype.saveStrings', function() { test('should be a function', function() { - assert.ok(myp5.saveStrings); - assert.typeOf(myp5.saveStrings, 'function'); + assert.ok(mockP5Prototype.saveStrings); + assert.typeOf(mockP5Prototype.saveStrings, 'function'); }); - testWithDownload( - 'should download a file with expected contents', - async function(blobContainer) { - let strings = ['some', 'words']; + test('should download a file with expected contents', async () => { + const strings = ['some', 'words']; + mockP5Prototype.saveStrings(strings, 'myfile'); - myp5.saveStrings(strings, 'myfile'); - - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - // Each element on a separate line with a trailing line-break - assert.strictEqual(text, strings.join('\n') + '\n'); - }, - true - ); + const saveData = new Blob([strings.join('\n')]); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'myfile.txt'); + }); - testWithDownload( - 'should download a file with expected contents with CRLF', - async function(blobContainer) { - let strings = ['some', 'words']; + test('should download a file with expected contents with CRLF', async () => { + const strings = ['some', 'words']; + mockP5Prototype.saveStrings(strings, 'myfile', 'txt', true); - myp5.saveStrings(strings, 'myfile', 'txt', true); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - // Each element on a separate line with a trailing CRLF - assert.strictEqual(text, strings.join('\r\n') + '\r\n'); - }, - true - ); + const saveData = new Blob([strings.join('\r\n')]); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'myfile.txt'); + }); }); // saveJSON() suite('p5.prototype.saveJSON', function() { test('should be a function', function() { - assert.ok(myp5.saveJSON); - assert.typeOf(myp5.saveJSON, 'function'); + assert.ok(mockP5Prototype.saveJSON); + assert.typeOf(mockP5Prototype.saveJSON, 'function'); }); - testWithDownload( - 'should download a file with expected contents', - async function(blobContainer) { - let myObj = { hi: 'hello' }; + test('should download a file with expected contents', async () => { + const myObj = { hi: 'hello' }; + mockP5Prototype.saveJSON(myObj, 'myfile'); - myp5.saveJSON(myObj, 'myfile'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - let json = JSON.parse(text); - // Each element on a separate line with a trailing line-break - assert.deepEqual(myObj, json); - }, - true // asyncFn = true - ); + const saveData = new Blob([JSON.stringify(myObj, null, 2)]); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'myfile.json'); + }); }); // writeFile() suite('p5.prototype.writeFile', function() { test('should be a function', function() { - assert.ok(myp5.writeFile); - assert.typeOf(myp5.writeFile, 'function'); + assert.ok(mockP5Prototype.writeFile); + assert.typeOf(mockP5Prototype.writeFile, 'function'); }); - testWithDownload( - 'should download a file with expected contents (text)', - async function(blobContainer) { - let myArray = ['hello', 'hi']; - myp5.writeFile(myArray, 'myfile'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - assert.strictEqual(text, myArray.join('')); - }, - true // asyncFn = true - ); + test('should download a file with expected contents (text)', async () => { + const myArray = ['hello', 'hi']; + mockP5Prototype.writeFile(myArray, 'myfile'); + + const saveData = new Blob(myArray); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'myfile'); + }); }); // downloadFile() suite('p5.prototype.downloadFile', function() { test('should be a function', function() { - assert.ok(myp5.writeFile); - assert.typeOf(myp5.writeFile, 'function'); + assert.ok(mockP5Prototype.downloadFile); + assert.typeOf(mockP5Prototype.downloadFile, 'function'); + }); + + test('should download a file with expected contents', async () => { + const myArray = ['hello', 'hi']; + const inBlob = new Blob(myArray); + mockP5Prototype.downloadFile(inBlob, 'myfile'); + + const saveData = new Blob(myArray); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'myfile'); }); - testWithDownload( - 'should download a file with expected contents', - async function(blobContainer) { - let myArray = ['hello', 'hi']; - let inBlob = new Blob(myArray); - myp5.downloadFile(inBlob, 'myfile'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - assert.strictEqual(text, myArray.join('')); - }, - true // asyncFn = true - ); }); // save() suite('p5.prototype.save', function() { suite('saving images', function() { - let waitForBlob = async function(blc) { - let sleep = function(ms) { - return new Promise(r => setTimeout(r, ms)); - }; - while (!blc.blob) { - await sleep(5); - } - }; - beforeAll(function() { - myp5.createCanvas(20, 20); - myp5.background(255, 0, 0); - }); - test('should be a function', function() { - assert.ok(myp5.save); - assert.typeOf(myp5.save, 'function'); + assert.ok(mockP5Prototype.save); + assert.typeOf(mockP5Prototype.save, 'function'); }); - testWithDownload( - 'should download a png file', - async function(blobContainer) { - myp5.save(); - await waitForBlob(blobContainer); - let myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/png'); - - blobContainer.blob = null; - let gb = myp5.createGraphics(100, 100); - myp5.save(gb); - await waitForBlob(blobContainer); - myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/png'); - }, - true - ); - - testWithDownload( - 'should download a jpg file', - async function(blobContainer) { - myp5.save('filename.jpg'); - await waitForBlob(blobContainer); - let myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/jpeg'); + // Test the call to `saveCanvas` + // `saveCanvas` is responsible for testing download is correct + test('should call saveCanvas', async () => { + mockP5Prototype.save(); + expect(mockP5Prototype.saveCanvas).toHaveBeenCalledTimes(1); + expect(mockP5Prototype.saveCanvas).toHaveBeenCalledWith(mockP5Prototype.elt); + }); - blobContainer.blob = null; - let gb = myp5.createGraphics(100, 100); - myp5.save(gb, 'filename.jpg'); - await waitForBlob(blobContainer); - myBlob = blobContainer.blob; - assert.strictEqual(myBlob.type, 'image/jpeg'); - }, - true - ); + test('should call saveCanvas with filename', async () => { + mockP5Prototype.save('filename.jpg'); + expect(mockP5Prototype.saveCanvas).toHaveBeenCalledTimes(1); + expect(mockP5Prototype.saveCanvas) + .toHaveBeenCalledWith(mockP5Prototype.elt, 'filename.jpg'); + }); }); suite('saving strings and json', function() { - testWithDownload( - 'should download a text file', - async function(blobContainer) { - let myStrings = ['aaa', 'bbb']; - myp5.save(myStrings, 'filename'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - assert.strictEqual(text, myStrings.join('\n') + '\n'); - }, - true - ); + test('should download a text file', async () => { + const myStrings = ['aaa', 'bbb']; + mockP5Prototype.save(myStrings, 'filename'); - testWithDownload( - 'should download a json file', - async function(blobContainer) { - let myObj = { hi: 'hello' }; - myp5.save(myObj, 'filename.json'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - let outObj = JSON.parse(text); - assert.deepEqual(outObj, myObj); - }, - true - ); + const saveData = new Blob([myStrings.join('\n')]); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'filename.txt'); + }); + + test('should download a json file', async () => { + const myObj = { hi: 'hello' }; + mockP5Prototype.save(myObj, 'filename.json'); + + const saveData = new Blob([JSON.stringify(myObj, null, 2)]); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs).toHaveBeenCalledWith(saveData, 'filename.json'); + }); }); }); }); diff --git a/test/unit/io/loadBytes.js b/test/unit/io/loadBytes.js index 296a996afc..34efe595a2 100644 --- a/test/unit/io/loadBytes.js +++ b/test/unit/io/loadBytes.js @@ -1,134 +1,71 @@ -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import files from '../../../src/io/files'; -suite.todo('loadBytes', function() { - var invalidFile = '404file'; - var validFile = 'unit/assets/nyan_cat.gif'; +suite('loadBytes', function() { + const invalidFile = '404file'; + const validFile = '/test/unit/assets/nyan_cat.gif'; - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadBytes(invalidFile); - setTimeout(resolve, 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; + beforeAll(async () => { + files(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadBytes( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadBytes(invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadBytes(validFile); - }; - - sketch.setup = function() { - resolve(); - }; + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadBytes(invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadBytes( - validFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadBytes(validFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. setTimeout(resolve, 50); - } - }; + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); + }); }); - test('returns the correct object', async function() { - const object = await promisedSketch(function(sketch, resolve, reject) { - var _object; - sketch.preload = function() { - _object = sketch.loadBytes(validFile, function() {}, reject); - }; + test('returns the correct object', async () => { + const data = await mockP5Prototype.loadBytes(validFile); + assert.instanceOf(data, Uint8Array); - sketch.setup = function() { - resolve(_object); - }; - }); - assert.isObject(object); - // Check data format - expect(object.bytes).to.satisfy(function(v) { - return Array.isArray(v) || v instanceof Uint8Array; - }); // Validate data - var str = 'GIF89a'; + const str = 'GIF89a'; // convert the string to a byte array - var rgb = str.split('').map(function(e) { + const rgb = str.split('').map(function(e) { return e.charCodeAt(0); }); // this will convert a Uint8Aray to [], if necessary: - var loaded = Array.prototype.slice.call(object.bytes, 0, str.length); + const loaded = Array.prototype.slice.call(data, 0, str.length); assert.deepEqual(loaded, rgb); }); - test('passes an object to success callback for object JSON', async function() { - const object = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadBytes(validFile, resolve, reject); - }; - }); - assert.isObject(object); - // Check data format - expect(object.bytes).to.satisfy(function(v) { - return Array.isArray(v) || v instanceof Uint8Array; - }); - // Validate data - var str = 'GIF89a'; - // convert the string to a byte array - var rgb = str.split('').map(function(e) { - return e.charCodeAt(0); + test('passes athe correct object to success callback', async () => { + await mockP5Prototype.loadBytes(validFile, (data) => { + assert.instanceOf(data, Uint8Array); + + // Validate data + const str = 'GIF89a'; + // convert the string to a byte array + const rgb = str.split('').map(function(e) { + return e.charCodeAt(0); + }); + // this will convert a Uint8Aray to [], if necessary: + const loaded = Array.prototype.slice.call(data, 0, str.length); + assert.deepEqual(loaded, rgb); }); - // this will convert a Uint8Aray to [], if necessary: - var loaded = Array.prototype.slice.call(object.bytes, 0, str.length); - assert.deepEqual(loaded, rgb); }); }); diff --git a/test/unit/io/loadImage.js b/test/unit/io/loadImage.js deleted file mode 100644 index fd1f15317a..0000000000 --- a/test/unit/io/loadImage.js +++ /dev/null @@ -1,109 +0,0 @@ -import p5 from '../../../src/app.js'; -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; - -suite.todo('loadImage', function() { - var invalidFile = '404file'; - var validFile = 'unit/assets/nyan_cat.gif'; - - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadImage(invalidFile); - setTimeout(resolve(), 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; - }); - - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadImage( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; - }); - - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadImage(validFile); - }; - - sketch.setup = function() { - resolve(); - }; - }); - - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadImage( - validFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { - setTimeout(resolve, 50); - } - }; - }); - - test('returns an object with correct data', async function() { - const image = await promisedSketch(function(sketch, resolve, reject) { - var _image; - sketch.preload = function() { - _image = sketch.loadImage(validFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(_image); - }; - }); - assert.instanceOf(image, p5.Image); - }); - - test('passes an object with correct data to callback', async function() { - const image = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadImage(validFile, resolve, reject); - }; - }); - assert.instanceOf(image, p5.Image); - }); -}); diff --git a/test/unit/io/loadJSON.js b/test/unit/io/loadJSON.js index 8882efbeb3..333695b042 100644 --- a/test/unit/io/loadJSON.js +++ b/test/unit/io/loadJSON.js @@ -1,137 +1,66 @@ -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import files from '../../../src/io/files'; -suite.todo('loadJSON', function() { - var invalidFile = '404file'; - var jsonArrayFile = 'unit/assets/array.json'; - var jsonObjectFile = 'unit/assets/object.json'; +suite('loadJSON', function() { + const invalidFile = '404file'; + const jsonArrayFile = '/test/unit/assets/array.json'; + const jsonObjectFile = '/test/unit/assets/object.json'; - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadJSON(invalidFile, reject, function() { - setTimeout(resolve, 50); - }); - }; - - sketch.setup = function() { - reject(new Error('Entered setup')); - }; - - sketch.draw = function() { - reject(new Error('Entered draw')); - }; + beforeAll(async () => { + files(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadJSON( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadJSON(invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadJSON(jsonObjectFile); - }; - - sketch.setup = function() { - resolve(); - }; + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadJSON(invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadJSON( - jsonObjectFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadJSON(jsonObjectFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. setTimeout(resolve, 50); - } - }; + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); + }); }); - test('returns an object for object JSON.', async function() { - const json = await promisedSketch(function(sketch, resolve, reject) { - var json; - sketch.preload = function() { - json = sketch.loadJSON(jsonObjectFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(json); - }; - }); - assert.isObject(json); + test('returns an object for object JSON.', async () => { + const data = await mockP5Prototype.loadJSON(jsonObjectFile); + assert.isObject(data); + assert.isNotArray(data); }); - test('passes an object to success callback for object JSON.', async function() { - const json = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadJSON(jsonObjectFile, resolve, reject); - }; + test('passes an object to success callback for object JSON.', async () => { + await mockP5Prototype.loadJSON(jsonObjectFile, (data) => { + assert.isObject(data); }); - assert.isObject(json); }); - // Does not work with the current loadJSON. - test('returns an array for array JSON.'); - // test('returns an array for array JSON.', async function() { - // const json = await promisedSketch(function(sketch, resolve, reject) { - // var json; - // sketch.preload = function() { - // json = sketch.loadJSON(jsonArrayFile, function() {}, reject); - // }; - // - // sketch.setup = function() { - // resolve(json); - // }; - // }); - // assert.isArray(json); - // assert.lengthOf(json, 3); - // }); + test('returns an array for array JSON.', async () => { + const data = await mockP5Prototype.loadJSON(jsonArrayFile); + assert.isArray(data); + assert.lengthOf(data, 3); + }); test('passes an array to success callback for array JSON.', async function() { - const json = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadJSON(jsonArrayFile, resolve, reject); - }; + await mockP5Prototype.loadJSON(jsonArrayFile, (data) => { + assert.isArray(data); + assert.lengthOf(data, 3); }); - assert.isArray(json); - assert.lengthOf(json, 3); }); }); diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 1d94c38f54..694f87e37f 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -1,200 +1,118 @@ -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; - -suite.todo('loadModel', function() { - var invalidFile = '404file'; - var validFile = 'unit/assets/teapot.obj'; - var validObjFileforMtl='unit/assets/octa-color.obj'; - var validSTLfile = 'unit/assets/ascii.stl'; - var inconsistentColorObjFile = 'unit/assets/eg1.obj'; - var objMtlMissing = 'unit/assets/objMtlMissing.obj'; - var validSTLfileWithoutExtension = 'unit/assets/ascii'; - - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadModel(invalidFile); - setTimeout(resolve, 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import loading from '../../../src/webgl/loading'; +import { Geometry } from '../../../src/webgl/p5.Geometry'; + +suite('loadModel', function() { + const invalidFile = '404file'; + const validFile = '/test/unit/assets/teapot.obj'; + const validObjFileforMtl = '/test/unit/assets/octa-color.obj'; + const validSTLfile = '/test/unit/assets/ascii.stl'; + const inconsistentColorObjFile = '/test/unit/assets/eg1.obj'; + const objMtlMissing = '/test/unit/assets/objMtlMissing.obj'; + const validSTLfileWithoutExtension = '/test/unit/assets/ascii'; + + beforeAll(async () => { + loading(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadModel( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadModel(invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadModel(validFile); - }; - - sketch.setup = function() { - resolve(); - }; + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadModel(invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('success callback is called', function( - sketch, - done - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadModel( - validFile, - function() { - hasBeenCalled = true; - done(); - }, - function(err) { - done(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - done(new Error('Setup called prior to success callback')); - } - }; + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadModel(validFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); + }); }); test('loads OBJ file with associated MTL file correctly', async function(){ - const model = await promisedSketch(function (sketch,resolve,reject){ - sketch.preload=function(){ - sketch.loadModel(validObjFileforMtl,resolve,reject); - }; - }); - const expectedColors=[ - 0, 0, 0.5, - 0, 0, 0.5, - 0, 0, 0.5, - 0, 0, 0.942654, - 0, 0, 0.942654, - 0, 0, 0.942654, - 0, 0.815632, 1, - 0, 0.815632, 1, - 0, 0.815632, 1, - 0, 0.965177, 1, - 0, 0.965177, 1, - 0, 0.965177, 1, - 0.848654, 1, 0.151346, - 0.848654, 1, 0.151346, - 0.848654, 1, 0.151346, - 1, 0.888635, 0, - 1, 0.888635, 0, - 1, 0.888635, 0, - 1, 0.77791, 0, - 1, 0.77791, 0, - 1, 0.77791, 0, - 0.5, 0, 0, - 0.5, 0, 0, - 0.5, 0, 0 + const model = await mockP5Prototype.loadModel(validObjFileforMtl); + + const expectedColors = [ + 0, 0, 0.5, 1, + 0, 0, 0.5, 1, + 0, 0, 0.5, 1, + 0, 0, 0.942654, 1, + 0, 0, 0.942654, 1, + 0, 0, 0.942654, 1, + 0, 0.815632, 1, 1, + 0, 0.815632, 1, 1, + 0, 0.815632, 1, 1, + 0, 0.965177, 1, 1, + 0, 0.965177, 1, 1, + 0, 0.965177, 1, 1, + 0.848654, 1, 0.151346, 1, + 0.848654, 1, 0.151346, 1, + 0.848654, 1, 0.151346, 1, + 1, 0.888635, 0, 1, + 1, 0.888635, 0, 1, + 1, 0.888635, 0, 1, + 1, 0.77791, 0, 1, + 1, 0.77791, 0, 1, + 1, 0.77791, 0, 1, + 0.5, 0, 0, 1, + 0.5, 0, 0, 1, + 0.5, 0, 0, 1 ]; - assert.deepEqual(model.vertexColors,expectedColors); + + assert.deepEqual(model.vertexColors, expectedColors); }); + test('inconsistent vertex coloring throws error', async function() { // Attempt to load the model and catch the error - let errorCaught = null; - try { - await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadModel(inconsistentColorObjFile, resolve, reject); - }; - }); - } catch (error) { - errorCaught = error; - } - - // Assert that an error was caught and that it has the expected message - assert.instanceOf(errorCaught, Error, 'No error thrown for inconsistent vertex coloring'); - assert.equal(errorCaught.message, 'Model coloring is inconsistent. Either all vertices should have colors or none should.', 'Unexpected error message for inconsistent vertex coloring'); + await expect(mockP5Prototype.loadModel(inconsistentColorObjFile)) + .rejects + .toThrow('Model coloring is inconsistent. Either all vertices should have colors or none should.'); }); test('missing MTL file shows OBJ model without vertexColors', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadModel(objMtlMissing, resolve, reject); - }; - }); - assert.instanceOf(model, p5.Geometry); + const model = await mockP5Prototype.loadModel(objMtlMissing); + assert.instanceOf(model, Geometry); assert.equal(model.vertexColors.length, 0, 'Model should not have vertex colors'); }); test('returns an object with correct data', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - var _model; - sketch.preload = function() { - _model = sketch.loadModel(validFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(_model); - }; - }); - assert.instanceOf(model, p5.Geometry); + const model = await mockP5Prototype.loadModel(validFile); + assert.instanceOf(model, Geometry); }); test('passes an object with correct data to callback', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadModel(validFile, resolve, reject); - }; + await mockP5Prototype.loadModel(validFile, (model) => { + assert.instanceOf(model, Geometry); }); - assert.instanceOf(model, p5.Geometry); }); test('resolves STL file correctly', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadModel(validSTLfile, resolve, reject); - }; - }); - assert.instanceOf(model, p5.Geometry); + const model = await mockP5Prototype.loadModel(validSTLfile); + assert.instanceOf(model, Geometry); }); test('resolves STL file correctly with explicit extension', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadModel(validSTLfileWithoutExtension, resolve, reject, '.stl'); - }; - }); - assert.instanceOf(model, p5.Geometry); + const model = await mockP5Prototype.loadModel(validSTLfileWithoutExtension, '.stl'); + assert.instanceOf(model, Geometry); }); test('resolves STL file correctly with case insensitive extension', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadModel(validSTLfileWithoutExtension, resolve, reject, '.STL'); - }; - }); - assert.instanceOf(model, p5.Geometry); + const model = await mockP5Prototype.loadModel(validSTLfileWithoutExtension, '.STL'); + assert.instanceOf(model, Geometry); }); }); diff --git a/test/unit/io/loadShader.js b/test/unit/io/loadShader.js index 0351a0ba63..51aef19716 100644 --- a/test/unit/io/loadShader.js +++ b/test/unit/io/loadShader.js @@ -1,162 +1,70 @@ -import p5 from '../../../src/app.js'; -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; - -suite.todo('loadShader', function() { - var invalidFile = '404file'; - var vertFile = 'unit/assets/vert.glsl'; - var fragFile = 'unit/assets/frag.glsl'; - - testSketchWithPromise('error with vert prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadShader(invalidFile, fragFile); - setTimeout(resolve, 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import material from '../../../src/webgl/material'; +import { Shader } from '../../../src/webgl/p5.Shader'; + +suite('loadShader', function() { + const invalidFile = '404file'; + const vertFile = '/test/unit/assets/vert.glsl'; + const fragFile = '/test/unit/assets/frag.glsl'; + + beforeAll(async () => { + material(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error with frag prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadShader(vertFile, invalidFile); - setTimeout(resolve, 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; + test('throws error when encountering HTTP errors in vert shader', async () => { + await expect(mockP5Prototype.loadShader(invalidFile, fragFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('error callback is called for vert', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadShader( - invalidFile, - fragFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors in frag shader', async () => { + await expect(mockP5Prototype.loadShader(vertFile, invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('error callback is called for frag', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadShader( - vertFile, - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('error callback is called for vert shader', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadShader(invalidFile, fragFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadShader(vertFile, fragFile); - }; - - sketch.setup = function() { - resolve(); - }; + test('error callback is called for frag shader', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadShader(vertFile, invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadShader( - vertFile, - fragFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadShader(vertFile, fragFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. setTimeout(resolve, 50); - } - }; + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); + }); }); test('returns an object with correct data', async function() { - const shader = await promisedSketch(function(sketch, resolve, reject) { - var _shader; - sketch.preload = function() { - _shader = sketch.loadShader(vertFile, fragFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(_shader); - }; - }); - assert.instanceOf(shader, p5.Shader); + const shader = await mockP5Prototype.loadShader(vertFile, fragFile); + assert.instanceOf(shader, Shader); }); test('passes an object with correct data to callback', async function() { - const model = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadShader(vertFile, fragFile, resolve, reject); - }; - }); - assert.instanceOf(model, p5.Shader); - }); - - test('does not run setup after complete when called outside of preload', async function() { - let setupCallCount = 0; - await promisedSketch(function(sketch, resolve, reject) { - sketch.setup = function() { - setupCallCount++; - sketch.loadShader(vertFile, fragFile, resolve, reject); - }; + await mockP5Prototype.loadShader(vertFile, fragFile, (shader) => { + assert.instanceOf(shader, Shader); }); - assert.equal(setupCallCount, 1); }); }); diff --git a/test/unit/io/loadStrings.js b/test/unit/io/loadStrings.js index c3e3ae7d30..b8db23cb53 100644 --- a/test/unit/io/loadStrings.js +++ b/test/unit/io/loadStrings.js @@ -1,124 +1,70 @@ -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import files from '../../../src/io/files'; -suite.todo('loadStrings', function() { +suite('loadStrings', function() { const invalidFile = '404file'; - const validFile = 'unit/assets/sentences.txt'; - const fileWithEmptyLines = 'unit/assets/empty_lines.txt'; - const fileWithManyLines = 'unit/assets/many_lines.txt'; + const validFile = '/test/unit/assets/sentences.txt'; + const fileWithEmptyLines = '/test/unit/assets/empty_lines.txt'; + const fileWithManyLines = '/test/unit/assets/many_lines.txt'; - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadStrings(invalidFile, reject, function() { - setTimeout(resolve, 50); - }); - }; - - sketch.setup = function() { - reject(new Error('Entered setup')); - }; - - sketch.draw = function() { - reject(new Error('Entered draw')); - }; + beforeAll(async () => { + files(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadStrings( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadStrings(invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadStrings(validFile); - }; - - sketch.setup = resolve(); - }); - - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadStrings(validFile, resolve, function(err) { - reject(new Error('Error callback was entered: ' + err)); + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadStrings(invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); }); - }; - - sketch.setup = function() { - reject(new Error('Setup called prior to success callback')); - }; + }); }); - test('returns an array of strings', async function() { - const strings = await promisedSketch(function(sketch, resolve, reject) { - let strings; - sketch.preload = function() { - strings = sketch.loadStrings(validFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(strings); - }; + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadStrings(validFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); }); + }); + test('returns an array of strings', async () => { + const strings = await mockP5Prototype.loadStrings(validFile); assert.isArray(strings); - for (let i = 0; i < strings.length; i++) { - assert.isString(strings[i]); + for(let string of strings){ + assert.isString(string); } }); - test('passes an array to success callback', async function() { - const strings = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadStrings(validFile, resolve, reject); - }; + test('passes an array to success callback', async () => { + await mockP5Prototype.loadStrings(validFile, (strings) => { + assert.isArray(strings); + for(let string of strings){ + assert.isString(string); + } }); - assert.isArray(strings); - for (let i = 0; i < strings.length; i++) { - assert.isString(strings[i]); - } }); - test('should include empty strings', async function() { - const strings = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadStrings(fileWithEmptyLines, resolve, reject); - }; - }); + test('should include empty strings', async () => { + const strings = await mockP5Prototype.loadStrings(fileWithEmptyLines); assert.isArray(strings, 'Array passed to callback function'); assert.lengthOf(strings, 6, 'length of data is 6'); }); - test('can load file with many lines', async function() { - const strings = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadStrings(fileWithManyLines, resolve, reject); - }; - }); + test('can load file with many lines', async () => { + const strings = await mockP5Prototype.loadStrings(fileWithManyLines); assert.isArray(strings, 'Array passed to callback function'); assert.lengthOf(strings, 131073, 'length of data is 131073'); }); diff --git a/test/unit/io/loadTable.js b/test/unit/io/loadTable.js index d5c378033c..de2b52803b 100644 --- a/test/unit/io/loadTable.js +++ b/test/unit/io/loadTable.js @@ -1,168 +1,90 @@ -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; - -suite.todo('loadTable', function() { - var invalidFile = '404file'; - var validFile = 'unit/assets/csv.csv'; - - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadTable(invalidFile); - setTimeout(resolve, 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import files from '../../../src/io/files'; +import table from '../../../src/io/p5.Table'; +import tableRow from '../../../src/io/p5.TableRow'; + +suite('loadTable', function() { + const invalidFile = '404file'; + const validFile = '/test/unit/assets/csv.csv'; + + beforeAll(async () => { + files(mockP5, mockP5Prototype); + table(mockP5, mockP5Prototype); + tableRow(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadTable( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadTable(invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadTable(validFile); - }; - - sketch.setup = function() { - resolve(); - }; + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadTable(invalidFile, () => { + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadTable( - validFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadTable(validFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. setTimeout(resolve, 50); - } - }; + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); + }); }); - test('returns an object with correct data', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - let _table; - sketch.preload = function() { - _table = sketch.loadTable(validFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(_table); - }; - }); - assert.equal(table.getRowCount(), 4); + test('returns an object with correct data', async () => { + const table = await mockP5Prototype.loadTable(validFile); + assert.equal(table.getRowCount(), 5); assert.strictEqual(table.getRow(1).getString(0), 'David'); assert.strictEqual(table.getRow(1).getNum(1), 31); }); - test('passes an object to success callback for object JSON', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadTable(validFile, resolve, reject); - }; + test('passes an object with correct data to success callback', async () => { + await mockP5Prototype.loadTable(validFile, (table) => { + assert.equal(table.getRowCount(), 5); + assert.strictEqual(table.getRow(1).getString(0), 'David'); + assert.strictEqual(table.getRow(1).getNum(1), 31); }); - assert.equal(table.getRowCount(), 4); - assert.strictEqual(table.getRow(1).getString(0), 'David'); - assert.strictEqual(table.getRow(1).getNum(1), 31); }); - test('csv option returns the correct data', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadTable(validFile, 'csv', resolve, reject); - }; - }); - assert.equal(table.getRowCount(), 4); + test('separator option returns the correct data', async () => { + const table = await mockP5Prototype.loadTable(validFile, ','); + assert.equal(table.getRowCount(), 5); assert.strictEqual(table.getRow(1).getString(0), 'David'); assert.strictEqual(table.getRow(1).getNum(1), 31); }); - test('using the header option works', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadTable(validFile, 'header', resolve, reject); - }; - }); - assert.equal(table.getRowCount(), 3); - assert.strictEqual(table.getRow(0).getString('name'), 'David'); - assert.strictEqual(table.getRow(0).getNum('age'), 31); - }); - - test('allows the csv and header options together', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadTable(validFile, 'csv', 'header', resolve, reject); - }; - }); - assert.equal(table.getRowCount(), 3); - assert.strictEqual(table.getRow(0).getString('name'), 'David'); - assert.strictEqual(table.getRow(0).getNum('age'), 31); + test('using the header option works', async () => { + const table = await mockP5Prototype.loadTable(validFile, ',', true); + assert.equal(table.getRowCount(), 4); + assert.strictEqual(table.getRow(0).getString(0), 'David'); + assert.strictEqual(table.getRow(0).getNum(1), 31); }); - test('CSV files should handle commas within quoted fields', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadTable(validFile, resolve, reject); - }; - }); - assert.equal(table.getRowCount(), 4); + test.todo('CSV files should handle commas within quoted fields', async () => { + // TODO: Current parsing does not handle quoted fields + const table = await mockP5Prototype.loadTable(validFile); + assert.equal(table.getRowCount(), 5); assert.equal(table.getRow(2).get(0), 'David, Jr.'); assert.equal(table.getRow(2).getString(0), 'David, Jr.'); assert.equal(table.getRow(2).get(1), '11'); assert.equal(table.getRow(2).getString(1), 11); }); - test('CSV files should handle escaped quotes and returns within quoted fields', async function() { - const table = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadTable(validFile, resolve, reject); - }; - }); - assert.equal(table.getRowCount(), 4); + test.todo('CSV files should handle escaped quotes and returns within quoted fields', async () => { + // TODO: Current parsing does not handle quoted fields + const table = await mockP5Prototype.loadTable(validFile); + assert.equal(table.getRowCount(), 5); assert.equal(table.getRow(3).get(0), 'David,\nSr. "the boss"'); }); }); diff --git a/test/unit/io/loadXML.js b/test/unit/io/loadXML.js index 317709b809..9ed8109e68 100644 --- a/test/unit/io/loadXML.js +++ b/test/unit/io/loadXML.js @@ -1,112 +1,58 @@ -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; - -suite.todo('loadXML', function() { - var invalidFile = '404file'; - var validFile = 'unit/assets/books.xml'; - - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadXML(invalidFile); - setTimeout(resolve, 50); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import files from '../../../src/io/files'; +import xml from '../../../src/io/p5.XML'; + +suite('loadXML', function() { + const invalidFile = '404file'; + const validFile = '/test/unit/assets/books.xml'; + + beforeAll(async () => { + files(mockP5, mockP5Prototype); + xml(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); }); - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadXML( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; + test('throws error when encountering HTTP errors', async () => { + await expect(mockP5Prototype.loadXML(invalidFile)) + .rejects + .toThrow('Not Found'); }); - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadXML(validFile); - }; - - sketch.setup = function() { - resolve(); - }; + test('error callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadXML(invalidFile, () => { + console.log("here"); + reject("Success callback executed"); + }, () => { + // Wait a bit so that if both callbacks are executed we will get an error. + setTimeout(resolve, 50); + }); + }); }); - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadXML( - validFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { + test('success callback is called', async () => { + await new Promise((resolve, reject) => { + mockP5Prototype.loadXML(validFile, () => { + // Wait a bit so that if both callbacks are executed we will get an error. setTimeout(resolve, 50); - } - }; + }, (err) => { + reject(`Error callback called: ${err.toString()}`); + }); + }); }); - test('returns an object with correct data', async function() { - const xml = await promisedSketch(function(sketch, resolve, reject) { - let _xml; - sketch.preload = function() { - _xml = sketch.loadXML(validFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(_xml); - }; - }); + test('returns an object with correct data', async () => { + const xml = await mockP5Prototype.loadXML(validFile); assert.isObject(xml); - var children = xml.getChildren('book'); + const children = xml.getChildren('book'); assert.lengthOf(children, 12); }); - test('passes an object with correct data', async function() { - const xml = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadXML(validFile, resolve, reject); - }; + test('passes an object with correct data to success callback', async () => { + await mockP5Prototype.loadXML(validFile, (xml) => { + assert.isObject(xml); + const children = xml.getChildren('book'); + assert.lengthOf(children, 12); }); - assert.isObject(xml); - var children = xml.getChildren('book'); - assert.lengthOf(children, 12); }); }); diff --git a/test/unit/io/saveModel.js b/test/unit/io/saveModel.js deleted file mode 100644 index 8cd0009ba1..0000000000 --- a/test/unit/io/saveModel.js +++ /dev/null @@ -1,118 +0,0 @@ -import p5 from '../../../src/app.js'; -import { testWithDownload } from '../../js/p5_helpers'; - -suite.todo('saveModel',function() { - var myp5; - - beforeAll(function(done) { - new p5(function(p) { - p.setup = function() { - myp5 = p; - done(); - }; - }); - }); - - afterAll(function() { - myp5.remove(); - }); - - testWithDownload( - 'should download an .obj file with expected contents', - async function(blobContainer) { - //.obj content as a string - const objContent = `v 100 0 0 - v 0 -100 0 - v 0 0 -100 - v 0 100 0 - v 100 0 0 - v 0 0 -100 - v 0 100 0 - v 0 0 100 - v 100 0 0 - v 0 100 0 - v 0 0 -100 - v -100 0 0 - v -100 0 0 - v 0 -100 0 - v 0 0 100 - v 0 0 -100 - v 0 -100 0 - v -100 0 0 - v 0 100 0 - v -100 0 0 - v 0 0 100 - v 0 0 100 - v 0 -100 0 - v 100 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vt 0 0 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - vn 0 0 1 - f 1 2 3 - f 4 5 6 - f 7 8 9 - f 10 11 12 - f 13 14 15 - f 16 17 18 - f 19 20 21 - f 22 23 24 - `; - - const objBlob = new Blob([objContent], { type: 'text/plain' }); - - myp5.downloadFile(objBlob, 'model', 'obj'); - - let myBlob = blobContainer.blob; - - let text = await myBlob.text(); - - assert.strictEqual(text, objContent); - }, - true - ); -}); diff --git a/test/unit/io/saveTable.js b/test/unit/io/saveTable.js index f6abb05d56..fb41726dab 100644 --- a/test/unit/io/saveTable.js +++ b/test/unit/io/saveTable.js @@ -1,94 +1,92 @@ -import p5 from '../../../src/app.js'; -import { testWithDownload } from '../../js/p5_helpers'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import * as fileSaver from 'file-saver'; +import { vi } from 'vitest'; +import files from '../../../src/io/files'; +import table from '../../../src/io/p5.Table'; +import tableRow from '../../../src/io/p5.TableRow'; -suite.todo('saveTable', function() { - let validFile = 'unit/assets/csv.csv'; - let myp5; +vi.mock('file-saver'); + +suite('saveTable', function() { + const validFile = '/test/unit/assets/csv.csv'; let myTable; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + beforeAll(async function() { + files(mockP5, mockP5Prototype); + table(mockP5, mockP5Prototype); + tableRow(mockP5, mockP5Prototype); + myTable = await mockP5Prototype.loadTable(validFile, 'csv', 'header'); }); - afterAll(function() { - myp5.remove(); + afterEach(() => { + vi.clearAllMocks(); }); - beforeEach(async function loadMyTable() { - await new Promise(resolve => { - myp5.loadTable(validFile, 'csv', 'header', function(table) { - myTable = table; - resolve(); - }); - }); + test('should be a function', function() { + assert.ok(mockP5Prototype.saveTable); + assert.typeOf(mockP5Prototype.saveTable, 'function'); }); - test('should be a function', function() { - assert.ok(myp5.saveTable); - assert.typeOf(myp5.saveTable, 'function'); + test('should download a file with expected contents', async () => { + mockP5Prototype.saveTable(myTable, 'filename'); + + // TODO: Need comprehensive way to compare blobs in spy call + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.any(Blob), + 'filename.csv' + ); }); - testWithDownload( - 'should download a file with expected contents', - async function(blobContainer) { - myp5.saveTable(myTable, 'filename'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - let myTableStr = myTable.columns.join(',') + '\n'; - for (let i = 0; i < myTable.rows.length; i++) { - myTableStr += myTable.rows[i].arr.join(',') + '\n'; - } + test('should download a file with expected contents (tsv)', async () => { + mockP5Prototype.saveTable(myTable, 'filename', 'tsv'); - assert.strictEqual(text, myTableStr); - }, - true - ); + // TODO: Need comprehensive way to compare blobs in spy call + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.any(Blob), + 'filename.tsv' + ); + }); - testWithDownload( - 'should download a file with expected contents (tsv)', - async function(blobContainer) { - myp5.saveTable(myTable, 'filename', 'tsv'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - let myTableStr = myTable.columns.join('\t') + '\n'; - for (let i = 0; i < myTable.rows.length; i++) { - myTableStr += myTable.rows[i].arr.join('\t') + '\n'; - } - assert.strictEqual(text, myTableStr); - }, - true - ); + test('should download a file with expected contents (html)', async () => { + mockP5Prototype.saveTable(myTable, 'filename', 'html'); - testWithDownload( - 'should download a file with expected contents (html)', - async function(blobContainer) { - myp5.saveTable(myTable, 'filename', 'html'); - let myBlob = blobContainer.blob; - let text = await myBlob.text(); - let domparser = new DOMParser(); - let htmldom = domparser.parseFromString(text, 'text/html'); - let trs = htmldom.querySelectorAll('tr'); - for (let i = 0; i < trs.length; i++) { - let tds = trs[i].querySelectorAll('td'); - for (let j = 0; j < tds.length; j++) { - // saveTable generates an HTML file with indentation spaces and line-breaks. The browser ignores these - // while displaying. But they will still remain a part of the parsed DOM and hence must be removed. - // More info at: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace - let tdText = tds[j].innerHTML.trim().replace(/\n/g, ''); - let tbText; - if (i === 0) { - tbText = myTable.columns[j].trim().replace(/\n/g, ''); - } else { - tbText = myTable.rows[i - 1].arr[j].trim().replace(/\n/g, ''); - } - assert.strictEqual(tdText, tbText); - } - } - }, - true - ); + expect(fileSaver.saveAs).toHaveBeenCalledTimes(1); + expect(fileSaver.saveAs) + .toHaveBeenCalledWith( + expect.any(Blob), + 'filename.html' + ); + }); + // testWithDownload( + // 'should download a file with expected contents (html)', + // async function(blobContainer) { + // myp5.saveTable(myTable, 'filename', 'html'); + // let myBlob = blobContainer.blob; + // let text = await myBlob.text(); + // let domparser = new DOMParser(); + // let htmldom = domparser.parseFromString(text, 'text/html'); + // let trs = htmldom.querySelectorAll('tr'); + // for (let i = 0; i < trs.length; i++) { + // let tds = trs[i].querySelectorAll('td'); + // for (let j = 0; j < tds.length; j++) { + // // saveTable generates an HTML file with indentation spaces and line-breaks. The browser ignores these + // // while displaying. But they will still remain a part of the parsed DOM and hence must be removed. + // // More info at: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace + // let tdText = tds[j].innerHTML.trim().replace(/\n/g, ''); + // let tbText; + // if (i === 0) { + // tbText = myTable.columns[j].trim().replace(/\n/g, ''); + // } else { + // tbText = myTable.rows[i - 1].arr[j].trim().replace(/\n/g, ''); + // } + // assert.strictEqual(tdText, tbText); + // } + // } + // }, + // true + // ); }); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 7d3b6d020f..2a3059a204 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -117,7 +117,7 @@ visualSuite('WebGL', function() { 'Object with different texture coordinates per use of vertex keeps the coordinates intact', async function(p5, screenshot) { p5.createCanvas(50, 50, p5.WEBGL); - const tex = await new Promise(resolve => p5.loadImage('/unit/assets/cat.jpg', resolve)); + const tex = await p5.loadImage('/unit/assets/cat.jpg'); const cube = await new Promise(resolve => p5.loadModel('/unit/assets/cube-textures.obj', resolve)); cube.normalize(); p5.background(255); @@ -230,7 +230,7 @@ visualSuite('WebGL', function() { visualSuite('ShaderFunctionality', function() { visualTest('FillShader', async (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); - const img = await new Promise(resolve => p5.loadImage('/unit/assets/cat.jpg', resolve)); + const img = await p5.loadImage('/unit/assets/cat.jpg'); const fillShader = p5.createShader( ` attribute vec3 aPosition; @@ -281,7 +281,7 @@ visualSuite('WebGL', function() { visualTest('ImageShader', async (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); - const img = await new Promise(resolve => p5.loadImage('/unit/assets/cat.jpg', resolve)); + const img = await p5.loadImage('/unit/assets/cat.jpg'); const imgShader = p5.createShader( ` precision mediump float;