From 732cfd443256cc5f93f7a377ee4442a19c540f9a Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sun, 3 May 2026 16:44:28 +0200 Subject: [PATCH 1/2] test(e2e): add Obsidian runtime coverage --- .gitignore | 3 + README.md | 17 + manifest.json | 2 +- package-lock.json | 1971 ++++++++++++++++++++++++++-- package.json | 6 +- tests/e2e/harness.ts | 309 +++++ tests/e2e/podnotes-runtime.test.ts | 489 +++++++ tsconfig.json | 4 +- vitest.e2e.config.ts | 12 + 9 files changed, 2736 insertions(+), 77 deletions(-) create mode 100644 tests/e2e/harness.ts create mode 100644 tests/e2e/podnotes-runtime.test.ts create mode 100644 vitest.e2e.config.ts diff --git a/.gitignore b/.gitignore index 6f730b6..005fbaa 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ __snapshots__ # Documentation build output docs/site + +# Obsidian E2E failure artifacts +.obsidian-e2e-artifacts diff --git a/README.md b/README.md index 892a564..715c391 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,23 @@ Here are the features that will help you do that 👇. Other installation options can be found in the [documentation](https://chhoumann.github.io/PodNotes). +## Development + +- `npm run test` runs the jsdom/unit test suite. +- `npm run build` type-checks and bundles the plugin. +- `npm run test:e2e` builds the plugin, then runs the local Obsidian-backed E2E suite. + +The E2E suite is local-only. It depends on Obsidian being installed, the +`obsidian` CLI being available on `PATH`, and the target vault being open and +reachable. The default target vault is `dev`; override it with +`PODNOTES_E2E_VAULT` when needed. Failed E2E runs may write artifacts to +`.obsidian-e2e-artifacts/`. + +Before running E2E, make sure the target vault's +`.obsidian/plugins/podnotes/main.js` and `manifest.json` symlinks point at this +checkout. The tests intentionally fail during preflight instead of relinking the +vault automatically. + ## Screenshots ### Demo diff --git a/manifest.json b/manifest.json index 785a9ec..916d96d 100644 --- a/manifest.json +++ b/manifest.json @@ -8,4 +8,4 @@ "authorUrl": "https://bagerbach.com", "fundingUrl": "https://buymeacoffee.com/chhoumann", "isDesktopOnly": false -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index dfe876f..5d73917 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-import": "^2.31.0", "jsdom": "^27.2.0", "obsidian": "1.10.3", + "obsidian-e2e": "0.6.0", "semantic-release": "^25.0.2", "svelte": "^5.43.14", "svelte-check": "^4.3.4", @@ -1589,6 +1590,850 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@oxc-project/runtime": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.127.0.tgz", + "integrity": "sha512-UQYLxAhDDPHm++szfa4z0RTdcPq5vaywrAoEA2n1YaAKeanXQdjHsoT6x1gP3U97RN8LZ7yHsSOrKPCcA6mCqw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.46.0.tgz", + "integrity": "sha512-b1doV4WRcJU+BESSlCvCjV+5CEr/T6h0frArAdV26Nir+gGNFNaylvDiiMPfF1pxeV0txZEs38ojzJaxBYg+ng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.46.0.tgz", + "integrity": "sha512-v6+HhjsoV3GO0u2u9jLSAZrvWfTraDxKofUIQ7/ktS7tzS+epVsxdHmeM+XxuNcAY/nWxxU1Sg4JcGTNRXraBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.46.0.tgz", + "integrity": "sha512-3eeooJGrqGIlI5MyryDZsAcKXSmKIgAD4yYtfRrRJzXZ0UTFZtiSveIur56YPrGMYZwT4XyVhHsMqrNwr1XeFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.46.0.tgz", + "integrity": "sha512-QG8BDM0CXWbu84k2SKmCqfEddPQPFiBicwtYnLqHRWZZl57HbtOLRMac/KTq2NO4AEc4ICCBpFxJIV9zcqYfkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.46.0.tgz", + "integrity": "sha512-9DdCqS/n2ncu/Chazvt3cpgAjAmIGQDz7hFKSrNItMApyV/Ja9mz3hD4JakIE3nS8PW9smEbPWnb389QLBY4nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.46.0.tgz", + "integrity": "sha512-Dgs7VeE2jT0LHMhw6tPEt0xQYe54kBqHEovmWsv4FVQlegCOvlIJNx0S8n4vj8WUtpT+Z6BD2HhKJPLglLxvZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.46.0.tgz", + "integrity": "sha512-Zxn3adhTH13JKnU4xXJj8FeEfF680XjXh3gSShKl57HCMBRde2tUJTgogV/1MSHA80PJEVrDa7r66TLVq3Ia7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.46.0.tgz", + "integrity": "sha512-+TWipjrgVM8D7aIdDD0tlr3teLTTvQTn7QTE5BpT10H1Fj82gfdn9X6nn2sDgx/MepuSCfSnzFNJq2paLL0OiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.46.0.tgz", + "integrity": "sha512-aAUPBWJ1lGwwnxZUEDLJ94+Iy6MuwJwPxUgO4sCA5mEEyDk7b+cDQ+JpX1VR150Zoyd+D49gsrUzpUK5h587Eg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.46.0.tgz", + "integrity": "sha512-ufBCJukyFX/UDrokP/r6BGDoTInnsDs7bxyzKAgMiZlt2Qu8GPJSJ6Zm6whIiJzKk0naxA8ilwmbO1LMw6Htxw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.46.0.tgz", + "integrity": "sha512-eqtlC2YmPqjun76R1gVfGLuKWx7NuEnLEAudZ7n6ipSKbCZTqIKSs1b5Y8K/JHZsRpLkeSmAAjig5HOIg8fQzQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.46.0.tgz", + "integrity": "sha512-yccVOO2nMXkQLGgy0He3EQEwKD7NF0zEk+/OWmroznkqXyJdN6bfK0LtNnr6/14Bh3FjpYq7bP33l/VloCnxpA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.46.0.tgz", + "integrity": "sha512-aAf7fG23OQCey6VRPj9IeCraoYtpgtx0ZyJ1CXkPyT1wjzBE7c3xtuxHe/AdHaJfVVb/SXpSk8Gl1LzyQupSqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.46.0.tgz", + "integrity": "sha512-q0JPsTMyJNjYrBvYFDz4WbVsafNZaPCZv4RnFypRotLqpKROtBZcEaXQW4eb9YmvLU3NckVemLJnzkSZSdmOxw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.46.0.tgz", + "integrity": "sha512-7LsLY9Cw57GPkhSR+duI3mt9baRczK/DtHYSldQ4BEU92da9igBQNl4z7Vq5U9NNPsh1FmpKvv1q9WDtiUQR1A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.46.0.tgz", + "integrity": "sha512-lHiBOz8Duaku7JtRNLlps3j++eOaICPZSd8FCVmTDM4DFOPT71Bjn7g6iar1z7StXlKRweUKxWUs4sA+zWGDXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.46.0.tgz", + "integrity": "sha512-/5ktYUliP89RhgC37DBH1x20U5zPSZMy3cMEcO0j3793rbHP9MWsknBwQB6eozRzWmYrh0IFM/p20EbPvDlYlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.46.0.tgz", + "integrity": "sha512-3WTnoiuIr8XvV0DIY7SN+1uJSwKf4sPpcbHfobcRT9JutGcLaef/miyBB87jxd3aqH+mS0+G5lsgHuXLUwjjpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.46.0.tgz", + "integrity": "sha512-IXxiQpkYnOwNfP23vzwSfhdpxJzyiPTY7eTn6dn3DsriKddESzM8i6kfq9R7CD/PUJwCvQT22NgtygBeug3KoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.22.0.tgz", + "integrity": "sha512-/exgXceakHbQrzaHTtKOe7MuDATaWMCCWpsCDQCZKeYhLGXzComipTrCYnHzAXrdnNBb5r5K+RRf5A6ormrhMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.22.0.tgz", + "integrity": "sha512-xFGdIahlmUbK+/MpZ5y08D0ewMGLDbd2Vki5wxVFYg50lSrtgPAtdDl+kqKZLNaFu0zpMar8n9wv1le05sL/jw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.22.0.tgz", + "integrity": "sha512-53RvC9f77eUo+V1dfQNwGVnsIfPJFMibRR0ee128EUpYNDOZe/ojmCfuXJeU7cY91V7r7fZSm42KPJocXUX8og==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.22.0.tgz", + "integrity": "sha512-evZcJAZ9hjNyuN69RnXwbt+U2pAOcYt+yvqukgugiCkRm4iBZ0R0CvpY1tgfG2XcGUhEPh8dljO+nPZTEVGpCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.22.0.tgz", + "integrity": "sha512-7jTO+k1mr5BxRAI2fxc1NRcE3MAbHNZ0Vef9SD1yAR6d1E6qEv5D/D7yuHpQpw6AO3qoecSVo2Jzr+JirN61+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.22.0.tgz", + "integrity": "sha512-7lbl9XFcqO+scsynxMzTQdl0XUe6sBUCyY/oGWvCB+JmV4U+70vzSyZJdTEzzxtkZiNnUVFFh9RJLmoiQSne+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.61.0.tgz", + "integrity": "sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.61.0.tgz", + "integrity": "sha512-CkwLR69MUnyv5wjzebvbbtTSUwqLxM35CXE79bHqDIK+NtKmPEUpStTcLQRZMCo4MP0qRT6TXIQVpK0ZVScnMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.61.0.tgz", + "integrity": "sha512-8JbefTkbmvqkqWjmQrHke+MdpgT2UghhD/ktM4FOQSpGeCgbMToJEKdl9zwhr/YWTl92i4QI1KiTwVExpcUN8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.61.0.tgz", + "integrity": "sha512-uWpoxDT47hTnDLcdEh5jVbso8rlTTu5o0zuqa9J8E0JAKmIWn7kGFEIB03Pycn2hd2vKxybPGLhjURy/9We5FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.61.0.tgz", + "integrity": "sha512-K/o4hEyW7flfMel0iBVznmMBt7VIMHGdjADocHKpK1DUF9erpWnJ+BSSWd2W0c8K3mPtpph+CuHzRU6CI3l9jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.61.0.tgz", + "integrity": "sha512-P6040ZkcyweJ0Po9yEFqJCdvZnf3VNCGs1SIHgXDf8AAQNC6ID/heXQs9iSgo2FH7gKaKq32VWc59XZwL34C5Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.61.0.tgz", + "integrity": "sha512-bwxrGCzTZkuB+THv2TQ1aTkVEfv5oz8sl+0XZZCpoYzErJD8OhPQOTA0ENPd1zJz8QsVdSzSrS2umKtPq4/JXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.61.0.tgz", + "integrity": "sha512-vkhb9/wKguMkLlrm3FoJW/Xmdv31GgYAE+x8lxxQ+7HeOxXUySI0q36a3NTVIuQUdLzxCI1zzMGsk1o37FOe3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.61.0.tgz", + "integrity": "sha512-bl1dQh8LnVqsj6oOQAcxwbuOmNJkwc4p6o//HTBZhNTzJy21TLDwAviMqUFNUxDHkPGpmdKTSN4tWTjLryP8xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.61.0.tgz", + "integrity": "sha512-QoOX6KB2IiEpyOj/HKqaxi+NQHPnOgNgnr22n9N4ANJCzXkUlj1UmeAbFb4PpqdlHIzvGDM5xZ0OKtcLq9RhiQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.61.0.tgz", + "integrity": "sha512-1TGcTerjY6p152wCof3oKElccq3xHljS/Mucp04gV/4ATpP6nO7YNnp7opEg6SHkv2a57/b4b8Ndm9znJ1/qAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.61.0.tgz", + "integrity": "sha512-65wXEmZIrX2ADwC8i/qFL4EWLSbeuBpAm3suuX1vu4IQkKd+wLT/HU/BOl84kp91u2SxPkPDyQgu4yrqp8vwVA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.61.0.tgz", + "integrity": "sha512-TVvhgMvor7Qa6COeXxCJ7ENOM+lcAOGsQ0iUdPSCv2hxb9qSHLQ4XF1h50S6RE1gBOJ0WV3rNukg4JJJP1LWRA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.61.0.tgz", + "integrity": "sha512-SjpS5uYuFoDnDdZPwZE59ndF95AsY47R5MliuneTWR1pDm2CxGJaYXbKULI71t5TVfLQUWmrHEGRL9xvuq6dnA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.61.0.tgz", + "integrity": "sha512-gGfAeGD4sNJGILZbc/yKcIimO9wQnPMoYp9swAaKeEtwsSQAbU+rsdQze5SBtIP6j0QDzeYd4XSSUCRCF+LIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.61.0.tgz", + "integrity": "sha512-OlVT0LrG/ct33EVtWRyR+B/othwmDWeRxfi13wUdPeb3lAT5TgTcFDcfLfarZtzB4W1nWF/zICMgYdkggX2WmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.61.0.tgz", + "integrity": "sha512-vI//NZPJk6DToiovPtaiwD4iQ7kO1r5ReWQD0sOOyKRtP3E2f6jxin4uvwi3OvDzHA2EFfd7DcZl5dtkQh7g1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.61.0.tgz", + "integrity": "sha512-0ySj4/4zd2XjePs3XAQq7IigIstN4LPQZgCyigX5/ERMLjdWAJfnxcTsrtxZxuij8guJW8foXuHmhGxW0H4dDA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.61.0.tgz", + "integrity": "sha512-0xgSiyeqDLDZxXoe9CVJrOx3TUVsfyoOY7cNi03JbItNcC9WCZqrSNdrAbHONxhSPaVh/lzfnDcON1RqSUMhHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -1634,6 +2479,14 @@ "node": ">=12" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", @@ -2561,9 +3414,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, @@ -3086,57 +3939,394 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.13.tgz", - "integrity": "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A==", + "node_modules/@vitest/runner": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.13.tgz", + "integrity": "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.13", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.13.tgz", + "integrity": "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.13", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.13.tgz", + "integrity": "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.13.tgz", + "integrity": "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.13", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@voidzero-dev/vite-plus-darwin-arm64": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-arm64/-/vite-plus-darwin-arm64-0.1.20.tgz", + "integrity": "sha512-ykCOJk91h0IEMvljYGTauI4Svxr/CatZAitofvtEFqaTCLE3n06QCHD8qWphMM784VnPz1G/J2xuewxbQduNlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@voidzero-dev/vite-plus-darwin-x64": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-x64/-/vite-plus-darwin-x64-0.1.20.tgz", + "integrity": "sha512-5XxNW9cYEh85Z4BErALyWh/tLP/NZmxNXzUQ0FanhHreI2Zq7FfgbSqQNvC7/sYsPYTWf74RlxmIjzV7R/Lb5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@voidzero-dev/vite-plus-linux-arm64-gnu": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-gnu/-/vite-plus-linux-arm64-gnu-0.1.20.tgz", + "integrity": "sha512-Mc7npPBd9t/h0haURVCZGae+TfB0Yx2Ex8HbPKOVA4hnN9ynlMhMpLRFfTQAicDKYbEGDhfBcbCIX0vVv4vacA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@voidzero-dev/vite-plus-linux-arm64-musl": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-musl/-/vite-plus-linux-arm64-musl-0.1.20.tgz", + "integrity": "sha512-Oh/pxMdTLR/wsDl/OONjItjLOeTewFBLuKkH5RQmcI9g3AVqKzLj1/uawujgysBI5E25tonRRK7I2q/zu8Uqvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@voidzero-dev/vite-plus-linux-x64-gnu": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-gnu/-/vite-plus-linux-x64-gnu-0.1.20.tgz", + "integrity": "sha512-msO1ZoUX5aSK8L6kN1C3XQO4CcH9aFsNPRSNcO1cjk1kTnaLyVYzkVxgvbh3vk7nzZAAMkmyZ4SlMpqJrdahrg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@voidzero-dev/vite-plus-linux-x64-musl": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-musl/-/vite-plus-linux-x64-musl-0.1.20.tgz", + "integrity": "sha512-U93urREvg23ZFDkxKkkfWWIOI4GI9erhbWAZpXG+GeYqygWKrVC6PUTXiuexVg3/CFg2sSMTdm1W6V7TFG5hYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@voidzero-dev/vite-plus-test": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-test/-/vite-plus-test-0.1.20.tgz", + "integrity": "sha512-vy2dJYw1bhgQ/+BrQrfwPlSKzQ2mm3YLJ9kGF7Yo0UJ2P3XKpshtgFIWLjSg/IASnC93OAx0c/7j3NM0I1RMuA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@voidzero-dev/vite-plus-core": "0.1.20", + "es-module-lexer": "^1.7.0", + "obug": "^2.1.1", + "pixelmatch": "^7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "std-env": "^4.0.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "ws": "^8.18.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/@voidzero-dev/vite-plus-test/node_modules/@voidzero-dev/vite-plus-core": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.1.20.tgz", + "integrity": "sha512-4KmzRfzwTeG3JuvDijrdqWusSgRvLMKDPrVsDdtbDVVjEMq0VnM8lSH+Nvepd6Pg+SuSVUP212OIfH/3Yn1bfA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/runtime": "=0.127.0", + "@oxc-project/types": "=0.127.0", + "lightningcss": "^1.30.2", + "postcss": "^8.5.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.21.10", + "@tsdown/exe": "0.21.10", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "publint": "^0.3.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "typescript": "^5.0.0 || ^6.0.0", + "unplugin-unused": "^0.5.0", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "publint": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@voidzero-dev/vite-plus-test/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.13", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "peer": true }, - "node_modules/@vitest/snapshot": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.13.tgz", - "integrity": "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ==", + "node_modules/@voidzero-dev/vite-plus-test/node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.13", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/spy": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.13.tgz", - "integrity": "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw==", + "node_modules/@voidzero-dev/vite-plus-win32-arm64-msvc": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-arm64-msvc/-/vite-plus-win32-arm64-msvc-0.1.20.tgz", + "integrity": "sha512-deXfe3h2OpzKV88s1PMUgVOJfN9LlnDDpIEVH6y2+YAXwlTSO7YeKBj2QmyS6ALZCI4Rfp4HOsB0OKMVBfEqww==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@vitest/utils": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.13.tgz", - "integrity": "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA==", + "node_modules/@voidzero-dev/vite-plus-win32-x64-msvc": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-x64-msvc/-/vite-plus-win32-x64-msvc-0.1.20.tgz", + "integrity": "sha512-ygdgQgo0N9oUI1Q2IdYBcvr+KLY6riaqLY/bkWNYtvHS4uk8a4GuEd0F08znWt2E8sFm29i35bYIzI6fFY2EBg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.13", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/acorn": { @@ -4557,6 +5747,17 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7545,28 +8746,313 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lines-and-columns": { @@ -8109,6 +9595,17 @@ "node": ">=4" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10831,6 +12328,34 @@ "@codemirror/view": "6.38.6" } }, + "node_modules/obsidian-e2e": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/obsidian-e2e/-/obsidian-e2e-0.6.0.tgz", + "integrity": "sha512-mbyij8q5tCXFRGZj5RFy7fS7F9ZIJ9bxNa9sBCsl7aoMsp3jWBW4vUjfN1Q2ZDMagDH7ideKYkMeCOb1NbrPRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.8.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite-plus": "^0.1.11" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "peer": true + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11024,6 +12549,112 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxfmt": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.46.0.tgz", + "integrity": "sha512-CopwJOwPAjZ9p76fCvz+mSOJTw9/NY3cSksZK3VO/bUQ8UoEcketNgUuYS0UB3p+R9XnXe7wGGXUmyFxc7QxJA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.46.0", + "@oxfmt/binding-android-arm64": "0.46.0", + "@oxfmt/binding-darwin-arm64": "0.46.0", + "@oxfmt/binding-darwin-x64": "0.46.0", + "@oxfmt/binding-freebsd-x64": "0.46.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.46.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.46.0", + "@oxfmt/binding-linux-arm64-gnu": "0.46.0", + "@oxfmt/binding-linux-arm64-musl": "0.46.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.46.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.46.0", + "@oxfmt/binding-linux-riscv64-musl": "0.46.0", + "@oxfmt/binding-linux-s390x-gnu": "0.46.0", + "@oxfmt/binding-linux-x64-gnu": "0.46.0", + "@oxfmt/binding-linux-x64-musl": "0.46.0", + "@oxfmt/binding-openharmony-arm64": "0.46.0", + "@oxfmt/binding-win32-arm64-msvc": "0.46.0", + "@oxfmt/binding-win32-ia32-msvc": "0.46.0", + "@oxfmt/binding-win32-x64-msvc": "0.46.0" + } + }, + "node_modules/oxlint": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.61.0.tgz", + "integrity": "sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.61.0", + "@oxlint/binding-android-arm64": "1.61.0", + "@oxlint/binding-darwin-arm64": "1.61.0", + "@oxlint/binding-darwin-x64": "1.61.0", + "@oxlint/binding-freebsd-x64": "1.61.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.61.0", + "@oxlint/binding-linux-arm-musleabihf": "1.61.0", + "@oxlint/binding-linux-arm64-gnu": "1.61.0", + "@oxlint/binding-linux-arm64-musl": "1.61.0", + "@oxlint/binding-linux-ppc64-gnu": "1.61.0", + "@oxlint/binding-linux-riscv64-gnu": "1.61.0", + "@oxlint/binding-linux-riscv64-musl": "1.61.0", + "@oxlint/binding-linux-s390x-gnu": "1.61.0", + "@oxlint/binding-linux-x64-gnu": "1.61.0", + "@oxlint/binding-linux-x64-musl": "1.61.0", + "@oxlint/binding-openharmony-arm64": "1.61.0", + "@oxlint/binding-win32-arm64-msvc": "1.61.0", + "@oxlint/binding-win32-ia32-msvc": "1.61.0", + "@oxlint/binding-win32-x64-msvc": "1.61.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.18.0" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.22.0.tgz", + "integrity": "sha512-ku4MecLmCQIj1ScCtzNAqTuyl0BJQ02B36fJT+c5XQihHpYSFak+FC3GYO5fPyYk4oDwi0w0S7hTvrpNzuZhig==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "0.22.0", + "@oxlint-tsgolint/darwin-x64": "0.22.0", + "@oxlint-tsgolint/linux-arm64": "0.22.0", + "@oxlint-tsgolint/linux-x64": "0.22.0", + "@oxlint-tsgolint/win32-arm64": "0.22.0", + "@oxlint-tsgolint/win32-x64": "0.22.0" + } + }, "node_modules/p-each-series": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", @@ -11326,6 +12957,20 @@ "node": ">=4" } }, + "node_modules/pixelmatch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.2.0.tgz", + "integrity": "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pkg-conf": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", @@ -11403,6 +13048,17 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12578,6 +14234,22 @@ "node": ">=4" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/skin-tone": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", @@ -13046,21 +14718,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-preprocess": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", @@ -13351,6 +15008,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", @@ -13407,6 +15075,17 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -13836,6 +15515,136 @@ } } }, + "node_modules/vite-plus": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/vite-plus/-/vite-plus-0.1.20.tgz", + "integrity": "sha512-hxJqXTxiiFhszwAeD0MvKlztVuXE4TztTdJ64BPxGqgY67F0PDa5eZkUsrN91Ae8aYUMfweW6V/J57OUO9/0zw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@voidzero-dev/vite-plus-core": "0.1.20", + "@voidzero-dev/vite-plus-test": "0.1.20", + "oxfmt": "=0.46.0", + "oxlint": "=1.61.0", + "oxlint-tsgolint": "=0.22.0" + }, + "bin": { + "oxfmt": "bin/oxfmt", + "oxlint": "bin/oxlint", + "vp": "bin/vp" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@voidzero-dev/vite-plus-darwin-arm64": "0.1.20", + "@voidzero-dev/vite-plus-darwin-x64": "0.1.20", + "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.20", + "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.20", + "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.20", + "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.20", + "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.20", + "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.20" + } + }, + "node_modules/vite-plus/node_modules/@voidzero-dev/vite-plus-core": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.1.20.tgz", + "integrity": "sha512-4KmzRfzwTeG3JuvDijrdqWusSgRvLMKDPrVsDdtbDVVjEMq0VnM8lSH+Nvepd6Pg+SuSVUP212OIfH/3Yn1bfA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/runtime": "=0.127.0", + "@oxc-project/types": "=0.127.0", + "lightningcss": "^1.30.2", + "postcss": "^8.5.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.21.10", + "@tsdown/exe": "0.21.10", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "publint": "^0.3.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "typescript": "^5.0.0 || ^6.0.0", + "unplugin-unused": "^0.5.0", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "publint": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -14339,6 +16148,22 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", diff --git a/package.json b/package.json index afd69df..358dbc7 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ "dev": "vite build --watch --mode development", "build": "npm run typecheck && vite build", "typecheck": "tsc --noEmit", - "lint": "eslint \"src/**/*.{ts,cts,mts}\" --max-warnings=0", - "format:check": "biome check package.json manifest.json tsconfig.json eslint.config.mjs vite.config.ts vitest.config.ts", + "lint": "eslint \"src/**/*.{ts,cts,mts}\" \"tests/e2e/**/*.ts\" --max-warnings=0", + "format:check": "biome check package.json manifest.json tsconfig.json eslint.config.mjs vite.config.ts vitest.config.ts vitest.e2e.config.ts tests/e2e", "version": "node version-bump.mjs && git add manifest.json versions.json", "semantic-release": "semantic-release", "test": "npm run check:a11y && vitest", + "test:e2e": "npm run build && vitest run --config vitest.e2e.config.ts", "check:a11y": "svelte-check --fail-on-warnings", "docs:build": "mkdocs build -f docs/mkdocs.yml -d site", "docs:deploy": "npm run docs:build && npx wrangler pages deploy docs/site --project-name podnotes --branch master" @@ -36,6 +37,7 @@ "eslint-plugin-import": "^2.31.0", "jsdom": "^27.2.0", "obsidian": "1.10.3", + "obsidian-e2e": "0.6.0", "semantic-release": "^25.0.2", "svelte": "^5.43.14", "svelte-check": "^4.3.4", diff --git a/tests/e2e/harness.ts b/tests/e2e/harness.ts new file mode 100644 index 0000000..a303873 --- /dev/null +++ b/tests/e2e/harness.ts @@ -0,0 +1,309 @@ +import { readlink } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + acquireVaultRunLock, + captureFailureArtifacts, + clearVaultRunLockMarker, + createObsidianClient, + createSandboxApi, + type ObsidianClient, + type PluginHandle, + type PluginReloadOptions, + type SandboxApi, + type VaultRunLock, +} from "obsidian-e2e"; +import { afterAll, afterEach, beforeAll, beforeEach } from "vitest"; + +export const PLUGIN_ID = "podnotes"; +export const VIEW_TYPE = "podcast_player_view"; +export const E2E_VAULT = process.env.PODNOTES_E2E_VAULT ?? "dev"; +export const E2E_BIN = process.env.OBSIDIAN_BIN ?? "obsidian"; +export const WAIT_OPTS = { timeoutMs: 15_000, intervalMs: 200 }; +export const RELOAD_OPTIONS: PluginReloadOptions = { + waitUntilReady: true, + timeoutMs: 30_000, + readyOptions: { + commandId: `${PLUGIN_ID}:hrpn`, + ...WAIT_OPTS, + }, +}; + +type HarnessState = { + lock?: VaultRunLock; + obsidian?: ObsidianClient; + plugin?: PluginHandle; + sandbox?: SandboxApi; +}; + +export type PodNotesE2EContext = { + obsidian: ObsidianClient; + plugin: PluginHandle; + sandbox: SandboxApi; +}; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "..", +); + +export function createPodNotesE2EHarness(testName: string) { + const state: HarnessState = {}; + + beforeAll(async () => { + state.obsidian = createObsidianClient({ + vault: E2E_VAULT, + bin: E2E_BIN, + timeoutMs: 20_000, + intervalMs: 200, + }); + await state.obsidian.verify(); + + state.lock = await acquireVaultRunLock({ + vaultName: E2E_VAULT, + vaultPath: await state.obsidian.vaultPath(), + onBusy: "wait", + timeoutMs: 60_000, + }); + await state.lock.publishMarker(state.obsidian); + + await assertDevVaultSymlinks(await state.obsidian.vaultPath()); + + state.plugin = state.obsidian.plugin(PLUGIN_ID); + state.sandbox = await createSandboxApi({ + obsidian: state.obsidian, + sandboxRoot: "__obsidian_e2e__", + testName, + }); + + await state.obsidian.dev.resetDiagnostics().catch(() => undefined); + await reloadPodNotes(state.plugin, state.obsidian); + }, 30_000); + + beforeEach((ctx) => { + ctx.onTestFailed(async () => { + if (!state.obsidian) return; + + await captureFailureArtifacts( + { id: ctx.task.id, name: ctx.task.name }, + state.obsidian, + { + captureOnFailure: true, + plugin: state.plugin, + }, + ).catch((error) => { + console.warn("PodNotes E2E artifact capture failed", error); + }); + }); + }); + + beforeEach(async () => { + await state.obsidian?.dev.resetDiagnostics().catch(() => undefined); + }); + + afterEach(async () => { + if (!state.plugin || !state.obsidian) return; + + await restorePodNotesData(state.plugin, state.obsidian); + }); + + afterAll(async () => { + const errors: unknown[] = []; + + await runTeardown("restore plugin data", errors, () => { + if (!state.plugin || !state.obsidian) return undefined; + return restorePodNotesData(state.plugin, state.obsidian); + }); + await runTeardown("clean sandbox", errors, () => state.sandbox?.cleanup()); + await runTeardown("clear vault lock marker", errors, () => { + if (!state.obsidian) return undefined; + return clearVaultRunLockMarker(state.obsidian); + }); + await runTeardown("release vault lock", errors, () => + state.lock?.release(), + ); + + if (errors.length > 0) { + throw errors[0]; + } + }, 30_000); + + return (): PodNotesE2EContext => { + if (!state.obsidian || !state.plugin || !state.sandbox) { + throw new Error("PodNotes E2E harness is not initialized."); + } + + return { + obsidian: state.obsidian, + plugin: state.plugin, + sandbox: state.sandbox, + }; + }; +} + +export async function reloadPodNotes( + plugin: PluginHandle, + obsidian: ObsidianClient, +): Promise { + await plugin.reload(RELOAD_OPTIONS); + await waitForPodNotesReady(obsidian); +} + +export async function restorePodNotesData( + plugin: PluginHandle, + obsidian: ObsidianClient, +): Promise { + await plugin.disable(); + await plugin.restoreData(); + await plugin.enable(); + await waitForPodNotesReady(obsidian); +} + +export async function waitForPodNotesReady( + obsidian: ObsidianClient, +): Promise { + await obsidian.waitFor( + async () => { + return await obsidian.dev.evalJson(` + Boolean( + app.plugins.plugins.${PLUGIN_ID}?.api && + app.workspace.protocolHandlers?.has(${JSON.stringify(PLUGIN_ID)}) + ) + `); + }, + { + ...WAIT_OPTS, + message: "PodNotes plugin did not become ready.", + }, + ); +} + +type AsyncEvalEnvelope = + | { ok: true; value: T } + | { error: { message: string; stack?: string }; ok: false }; + +export async function evalJsonAsync( + obsidian: ObsidianClient, + code: string, +): Promise { + const envelope = await obsidian.dev.eval>(` + (async () => { + const code = ${JSON.stringify(code)}; + try { + const value = await (0, eval)(code); + return JSON.stringify({ ok: true, value }); + } catch (error) { + return JSON.stringify({ + ok: false, + error: { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }, + }); + } + })() + `); + + if (!envelope.ok) { + throw new Error( + [ + `Failed to evaluate async Obsidian code: ${envelope.error.message}`, + envelope.error.stack ?? "", + ] + .filter(Boolean) + .join("\n"), + ); + } + + return envelope.value; +} + +export async function openPodNotesView( + obsidian: ObsidianClient, +): Promise { + const result = await evalJsonAsync<{ + activeViewType: string | null; + count: number; + ok: boolean; + }>( + obsidian, + ` + (async () => { + const leaves = app.workspace.getLeavesOfType(${JSON.stringify(VIEW_TYPE)}); + const leaf = leaves[0] ?? app.workspace.getRightLeaf(false); + + if (!leaf) { + return { ok: false, count: 0, activeViewType: null }; + } + + await leaf.setViewState({ type: ${JSON.stringify(VIEW_TYPE)} }); + await app.workspace.revealLeaf(leaf); + app.workspace.setActiveLeaf(leaf, { focus: true }); + + return { + ok: true, + count: app.workspace.getLeavesOfType(${JSON.stringify(VIEW_TYPE)}).length, + activeViewType: app.workspace.activeLeaf?.view?.getViewType?.() ?? leaf.view?.getViewType?.() ?? null, + }; + })() + `, + ); + + if (!result.ok || result.count < 1 || result.activeViewType !== VIEW_TYPE) { + throw new Error(`Failed to open PodNotes view: ${JSON.stringify(result)}`); + } +} + +async function assertDevVaultSymlinks(vaultPath: string): Promise { + const pluginDir = path.join(vaultPath, ".obsidian", "plugins", PLUGIN_ID); + + await assertSymlinkTarget(pluginDir, "main.js"); + await assertSymlinkTarget(pluginDir, "manifest.json"); +} + +async function assertSymlinkTarget( + pluginDir: string, + fileName: string, +): Promise { + const linkPath = path.join(pluginDir, fileName); + const expected = path.join(repoRoot, fileName); + let target: string; + + try { + target = await readlink(linkPath); + } catch (error) { + throw new Error( + [ + "PodNotes E2E preflight failed.", + `Expected ${linkPath} to be a symlink to ${expected}.`, + `Could not read symlink: ${error instanceof Error ? error.message : String(error)}`, + ].join(" "), + ); + } + + const resolvedTarget = path.resolve(path.dirname(linkPath), target); + if (resolvedTarget !== expected) { + throw new Error( + [ + "PodNotes E2E preflight failed.", + `Expected ${linkPath} to point at ${expected}.`, + `It currently points at ${resolvedTarget}.`, + "Repoint the dev vault plugin symlink intentionally before running npm run test:e2e.", + ].join(" "), + ); + } +} + +async function runTeardown( + label: string, + errors: unknown[], + step: () => Promise | unknown, +): Promise { + try { + await step(); + } catch (error) { + errors.push(error); + console.warn(`PodNotes E2E teardown failed during ${label}`, error); + } +} diff --git a/tests/e2e/podnotes-runtime.test.ts b/tests/e2e/podnotes-runtime.test.ts new file mode 100644 index 0000000..d8c6bbd --- /dev/null +++ b/tests/e2e/podnotes-runtime.test.ts @@ -0,0 +1,489 @@ +import type { PluginHandle, SandboxApi } from "obsidian-e2e"; +import { describe, expect, test } from "vitest"; +import type DownloadedEpisode from "../../src/types/DownloadedEpisode"; +import type { Episode } from "../../src/types/Episode"; +import type { IPodNotesSettings } from "../../src/types/IPodNotesSettings"; +import type { LocalEpisode } from "../../src/types/LocalEpisode"; +import { + createPodNotesE2EHarness, + evalJsonAsync, + openPodNotesView, + PLUGIN_ID, + RELOAD_OPTIONS, + VIEW_TYPE, + WAIT_OPTS, + waitForPodNotesReady, +} from "./harness"; + +type PodNotesData = Partial; + +type PlaybackState = { + currentTime: number | null; + hasPlayer: boolean; + isPlaying: boolean; + title: string | null; +}; + +const getContext = createPodNotesE2EHarness("podnotes-runtime"); + +describe("PodNotes runtime", () => { + test("registers commands, protocol handling, and the player view", async () => { + const { obsidian } = getContext(); + + await openPodNotesView(obsidian); + + const state = await obsidian.dev.evalJson<{ + hasProtocolHandler: boolean; + hasShowCommand: boolean; + viewCount: number; + }>(` + (() => ({ + hasProtocolHandler: app.workspace.protocolHandlers?.has(${JSON.stringify(PLUGIN_ID)}) ?? false, + hasShowCommand: Boolean(app.commands?.commands?.[${JSON.stringify(`${PLUGIN_ID}:podnotes-show-leaf`)}]), + viewCount: app.workspace.getLeavesOfType(${JSON.stringify(VIEW_TYPE)}).length, + }))() + `); + + expect(state).toMatchObject({ + hasProtocolHandler: true, + hasShowCommand: true, + }); + expect(state.viewCount).toBeGreaterThan(0); + expect(await obsidian.dev.runtimeErrors()).toEqual([]); + }); + + test("opens timestamp URI links at requested progress even after an episode is played", async () => { + const { obsidian, plugin, sandbox } = getContext(); + const audioPath = await seedAudio(sandbox, "finished-episode.mp3"); + const episode = createLocalEpisode("E2E Finished Episode", audioPath); + + await seedRuntimeData(plugin, sandbox, episode, { + played: { duration: 3600, time: 3600 }, + timestampTemplate: "- {{linktime}}", + }); + await waitForPodNotesReady(obsidian); + await openPodNotesView(obsidian); + + await invokePodNotesUri(obsidian, episode, 240); + await dispatchLoadedMetadata(obsidian); + + const state = await waitForPlaybackState( + obsidian, + (value) => + value.hasPlayer && + value.title === episode.title && + value.isPlaying && + value.currentTime === 240, + ); + + expect(state).toMatchObject({ + currentTime: 240, + hasPlayer: true, + isPlaying: true, + title: episode.title, + }); + }); + + test("preserves zero-second timestamp URI links at runtime", async () => { + const { obsidian, plugin, sandbox } = getContext(); + const audioPath = await seedAudio(sandbox, "zero-second-episode.mp3"); + const episode = createLocalEpisode("E2E Zero Second Episode", audioPath); + + await seedRuntimeData(plugin, sandbox, episode, { + played: { duration: 3600, time: 3600 }, + timestampTemplate: "- {{linktime}}", + }); + await waitForPodNotesReady(obsidian); + await openPodNotesView(obsidian); + + await invokePodNotesUri(obsidian, episode, 0); + await dispatchLoadedMetadata(obsidian); + + const state = await waitForPlaybackState( + obsidian, + (value) => + value.hasPlayer && + value.title === episode.title && + value.isPlaying && + value.currentTime === 0, + ); + + expect(state).toMatchObject({ + currentTime: 0, + hasPlayer: true, + isPlaying: true, + title: episode.title, + }); + }); + + test("seeks immediately when the linked episode is already loaded", async () => { + const { obsidian, plugin, sandbox } = getContext(); + const audioPath = await seedAudio(sandbox, "already-loaded-episode.mp3"); + const episode = createLocalEpisode("E2E Already Loaded Episode", audioPath); + + await seedRuntimeData(plugin, sandbox, episode, { + currentEpisode: episode, + timestampTemplate: "- {{linktime}}", + }); + await waitForPodNotesReady(obsidian); + await openPodNotesView(obsidian); + await setPlayback(obsidian, { currentTime: 999, paused: true }); + + await invokePodNotesUri( + obsidian, + { + ...episode, + filePath: "https://invalid.invalid/podnotes-e2e.xml", + streamUrl: "https://invalid.invalid/podnotes-e2e.xml", + url: "https://invalid.invalid/podnotes-e2e.xml", + }, + 12, + ); + await dispatchAudioPlay(obsidian); + + const state = await waitForPlaybackState( + obsidian, + (value) => + value.title === episode.title && + value.isPlaying && + value.currentTime === 12, + ); + + expect(state).toMatchObject({ + currentTime: 12, + isPlaying: true, + title: episode.title, + }); + expect(await obsidian.dev.notices()).not.toContainEqual( + expect.objectContaining({ message: "Episode not found" }), + ); + }); + + test("captures a linked timestamp into the active editor", async () => { + const { obsidian, plugin, sandbox } = getContext(); + const audioPath = await seedAudio(sandbox, "capture-episode.mp3"); + const episode = createLocalEpisode("E2E Capture Episode", audioPath); + const notePath = sandbox.path("capture-target.md"); + + await seedRuntimeData(plugin, sandbox, episode, { + currentEpisode: episode, + timestampTemplate: "- {{linktime}}", + }); + await waitForPodNotesReady(obsidian); + await sandbox.write("capture-target.md", "", { waitForContent: true }); + await obsidian.open({ path: notePath }); + await obsidian.waitForActiveFile(notePath, WAIT_OPTS); + await setPlayback(obsidian, { currentTime: 125, paused: false }); + + await obsidian.command(`${PLUGIN_ID}:capture-timestamp`).run(); + + const expectedLink = `- ${expectedTimestampLink("00:02:05", episode, 125)}`; + const content = await sandbox.waitForContent( + "capture-target.md", + (value) => value.includes(expectedLink), + WAIT_OPTS, + ); + + expect(content).toContain(expectedLink); + expect(content).toContain("obsidian://podnotes"); + expect(content).toContain("episodeName=E2E+Capture+Episode"); + expect(content).toContain("time=125"); + }); + + test("persists API volume changes and clamps out-of-range values", async () => { + const { obsidian, plugin } = getContext(); + + await plugin.updateDataAndReload((data) => { + data.defaultVolume = 1; + }, RELOAD_OPTIONS); + await waitForPodNotesReady(obsidian); + + await setVolume(obsidian, 0.42); + await plugin.waitForData( + (data) => data.defaultVolume === 0.42, + WAIT_OPTS, + ); + + await setVolume(obsidian, 1.5); + const data = await plugin.waitForData( + (value) => value.defaultVolume === 1, + WAIT_OPTS, + ); + const runtimeVolume = await getVolume(obsidian); + + expect(data.defaultVolume).toBe(1); + expect(runtimeVolume).toBe(1); + }); +}); + +async function seedAudio( + sandbox: SandboxApi, + fileName: string, +): Promise { + await sandbox.write(fileName, "podnotes e2e audio placeholder", { + waitForContent: true, + waitOptions: WAIT_OPTS, + }); + + return sandbox.path(fileName); +} + +function createLocalEpisode(title: string, audioPath: string): LocalEpisode { + return { + title, + streamUrl: audioPath, + url: audioPath, + description: "", + content: "", + podcastName: "local file", + filePath: audioPath, + }; +} + +async function seedRuntimeData( + plugin: PluginHandle, + sandbox: SandboxApi, + episode: LocalEpisode, + options: { + currentEpisode?: Episode; + played?: { duration: number; time: number }; + timestampTemplate?: string; + } = {}, +): Promise { + const placeholderEpisode = createLocalEpisode( + "E2E Placeholder Episode", + sandbox.path("placeholder.mp3"), + ); + const localEpisodes = [episode]; + + if (!options.currentEpisode) { + localEpisodes.push(placeholderEpisode); + } + + await plugin.updateDataAndReload((data) => { + data.currentEpisode = options.currentEpisode ?? placeholderEpisode; + data.defaultVolume = 1; + data.downloadedEpisodes = { + [episode.podcastName]: [toDownloadedEpisode(episode)], + }; + data.favorites = createPlaylist("Favorites", "lucide-star", []); + data.localFiles = createPlaylist("Local Files", "folder", localEpisodes); + data.playedEpisodes = options.played + ? { + [episodeKey(episode)]: { + title: episode.title, + podcastName: episode.podcastName, + time: options.played.time, + duration: options.played.duration, + finished: true, + }, + } + : {}; + data.playlists = {}; + data.queue = createPlaylist("Queue", "list-ordered", []); + data.timestamp = { + template: options.timestampTemplate ?? "- {{time}}", + offset: 0, + }; + }, RELOAD_OPTIONS); +} + +function createPlaylist( + name: string, + icon: "folder" | "list-ordered" | "lucide-star", + episodes: Episode[], +) { + return { + icon, + name, + episodes, + shouldEpisodeRemoveAfterPlay: name === "Queue", + shouldRepeat: false, + }; +} + +function toDownloadedEpisode(episode: LocalEpisode): DownloadedEpisode { + return { + ...episode, + filePath: episode.filePath ?? episode.streamUrl, + size: 1, + }; +} + +function episodeKey(episode: Episode): string { + return `${episode.podcastName}::${episode.title}`; +} + +async function invokePodNotesUri( + obsidian: Parameters[0], + episode: LocalEpisode, + time: number, +): Promise { + const result = await evalJsonAsync<{ error?: string; ok: boolean }>( + obsidian, + ` + (async () => { + const handler = app.workspace.protocolHandlers.get(${JSON.stringify(PLUGIN_ID)}); + if (!handler) { + return { ok: false, error: "PodNotes protocol handler is not registered." }; + } + + await handler({ + action: ${JSON.stringify(PLUGIN_ID)}, + url: ${JSON.stringify(episode.filePath ?? episode.streamUrl)}, + episodeName: ${JSON.stringify(episode.title)}, + time: ${JSON.stringify(String(time))}, + }); + + return { ok: true }; + })() + `, + ); + + if (!result.ok) { + throw new Error(result.error ?? "PodNotes URI handler failed."); + } +} + +async function dispatchLoadedMetadata(obsidian: { + dev: { evalJson: (code: string) => Promise }; +}): Promise { + const result = await obsidian.dev.evalJson<{ error?: string; ok: boolean }>(` + (() => { + const audio = document.querySelector(".podcast-view audio"); + if (!audio) { + return { ok: false, error: "No PodNotes audio element found." }; + } + + Object.defineProperty(audio, "duration", { + configurable: true, + value: 3600, + }); + audio.dispatchEvent(new Event("loadedmetadata")); + Object.defineProperty(audio, "paused", { + configurable: true, + value: false, + }); + audio.dispatchEvent(new Event("play")); + + return { ok: true }; + })() + `); + + if (!result.ok) { + throw new Error(result.error ?? "Failed to dispatch loadedmetadata."); + } +} + +async function dispatchAudioPlay(obsidian: { + dev: { evalJson: (code: string) => Promise }; +}): Promise { + const result = await obsidian.dev.evalJson<{ error?: string; ok: boolean }>(` + (() => { + const audio = document.querySelector(".podcast-view audio"); + if (!audio) { + return { ok: false, error: "No PodNotes audio element found." }; + } + + Object.defineProperty(audio, "paused", { + configurable: true, + value: false, + }); + audio.dispatchEvent(new Event("play")); + + return { ok: true }; + })() + `); + + if (!result.ok) { + throw new Error(result.error ?? "Failed to dispatch audio play."); + } +} + +async function waitForPlaybackState( + obsidian: { + dev: { evalJson: (code: string) => Promise }; + sleep: (ms: number) => Promise; + }, + predicate: (state: PlaybackState) => boolean, +): Promise { + const startedAt = Date.now(); + let lastState: PlaybackState | null = null; + + while (Date.now() - startedAt < WAIT_OPTS.timeoutMs) { + lastState = await getPlaybackState(obsidian); + if (predicate(lastState)) return lastState; + await obsidian.sleep(WAIT_OPTS.intervalMs); + } + + throw new Error( + `Timed out waiting for playback state. Last state: ${JSON.stringify(lastState)}`, + ); +} + +async function getPlaybackState(obsidian: { + dev: { evalJson: (code: string) => Promise }; +}): Promise { + return await obsidian.dev.evalJson(` + (() => { + const plugin = app.plugins.plugins.${PLUGIN_ID}; + return { + currentTime: plugin?.api?.currentTime ?? null, + hasPlayer: Boolean(document.querySelector(".episode-player")), + isPlaying: Boolean(plugin?.api?.isPlaying), + title: plugin?.api?.podcast?.title ?? null, + }; + })() + `); +} + +async function setPlayback( + obsidian: { dev: { evalJson: (code: string) => Promise } }, + { currentTime, paused }: { currentTime: number; paused: boolean }, +): Promise { + await obsidian.dev.evalJson(` + (() => { + const api = app.plugins.plugins.${PLUGIN_ID}.api; + api.currentTime = ${JSON.stringify(currentTime)}; + if (${JSON.stringify(paused)}) { + api.stop(); + } else { + api.start(); + } + return true; + })() + `); +} + +async function setVolume( + obsidian: { dev: { evalJson: (code: string) => Promise } }, + value: number, +): Promise { + await obsidian.dev.evalJson(` + (() => { + app.plugins.plugins.${PLUGIN_ID}.api.volume = ${JSON.stringify(value)}; + return true; + })() + `); +} + +async function getVolume(obsidian: { + dev: { evalJson: (code: string) => Promise }; +}): Promise { + return await obsidian.dev.evalJson(` + app.plugins.plugins.${PLUGIN_ID}.api.volume + `); +} + +function expectedTimestampLink( + label: string, + episode: LocalEpisode, + time: number, +): string { + const uri = new URL("obsidian://podnotes"); + uri.searchParams.set("episodeName", episode.title); + uri.searchParams.set("url", episode.filePath ?? episode.streamUrl); + uri.searchParams.set("time", String(time)); + + return `[${label}](${uri.href})`; +} diff --git a/tsconfig.json b/tsconfig.json index 74716c6..df1c1a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,8 @@ "src/**/*.ts", "src/**/*.d.ts", "vite.config.ts", - "vitest.config.ts" + "vitest.config.ts", + "vitest.e2e.config.ts", + "tests/e2e/**/*.ts" ] } diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts new file mode 100644 index 0000000..e5bd82b --- /dev/null +++ b/vitest.e2e.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/e2e/**/*.test.ts"], + environment: "node", + testTimeout: 60_000, + hookTimeout: 30_000, + fileParallelism: false, + maxWorkers: 1, + }, +}); From 3f2d672ddb0c42160ae3dcd931bfff895fbdf8ad Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sun, 3 May 2026 18:18:07 +0200 Subject: [PATCH 2/2] codex: address PR review feedback (#172) --- tests/e2e/harness.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/harness.ts b/tests/e2e/harness.ts index a303873..210dd28 100644 --- a/tests/e2e/harness.ts +++ b/tests/e2e/harness.ts @@ -79,7 +79,7 @@ export function createPodNotesE2EHarness(testName: string) { await state.obsidian.dev.resetDiagnostics().catch(() => undefined); await reloadPodNotes(state.plugin, state.obsidian); - }, 30_000); + }, 90_000); beforeEach((ctx) => { ctx.onTestFailed(async () => {