diff --git a/.changeset/fast-apricots-yawn.md b/.changeset/fast-apricots-yawn.md new file mode 100644 index 0000000..4980138 --- /dev/null +++ b/.changeset/fast-apricots-yawn.md @@ -0,0 +1,5 @@ +--- +'earwurm': minor +--- + +Earwurm now empties all events on teardown. diff --git a/.changeset/shiny-snakes-breathe.md b/.changeset/shiny-snakes-breathe.md new file mode 100644 index 0000000..e144870 --- /dev/null +++ b/.changeset/shiny-snakes-breathe.md @@ -0,0 +1,5 @@ +--- +'earwurm': minor +--- + +Earwurm now triggers autoSuspend conditionally on init and whenever state changes to "running". diff --git a/.changeset/spicy-plums-reflect.md b/.changeset/spicy-plums-reflect.md new file mode 100644 index 0000000..cd860ce --- /dev/null +++ b/.changeset/spicy-plums-reflect.md @@ -0,0 +1,5 @@ +--- +'earwurm': minor +--- + +Revise some method types for TypeScript strict mode. diff --git a/.eslintrc b/.eslintrc index 1350e4a..3e71759 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,6 @@ "no-console": "warn", "prettier/prettier": ["error"], "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/method-signature-style": "off", "@typescript-eslint/strict-boolean-expressions": "off" } } diff --git a/package-lock.json b/package-lock.json index 9961ed6..f39d1d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,24 @@ { "name": "earwurm", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "earwurm", - "version": "0.1.0", + "version": "0.2.0", "license": "ISC", "dependencies": { - "emitten": "^0.4.2" + "emitten": "^0.4.3" }, "devDependencies": { "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.0", - "@types/node": "^18.14.1", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@vitest/coverage-c8": "^0.29.1", - "@vitest/ui": "^0.29.1", - "eslint": "^8.34.0", + "@types/node": "^18.14.6", + "@typescript-eslint/eslint-plugin": "^5.54.0", + "@vitest/coverage-c8": "^0.29.2", + "@vitest/ui": "^0.29.2", + "eslint": "^8.35.0", "eslint-config-prettier": "^8.6.0", "eslint-config-standard-with-typescript": "^34.0.0", "eslint-plugin-import": "^2.27.5", @@ -29,13 +29,13 @@ "typescript": "^4.9.5", "vite": "^4.1.4", "vite-plugin-dts": "^2.0.2", - "vitest": "^0.29.1" + "vitest": "^0.29.2" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "emitten": "^0.4.2" + "emitten": "^0.4.3" } }, "node_modules/@babel/code-frame": { @@ -705,9 +705,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz", + "integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -745,6 +745,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/js": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz", + "integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -1188,9 +1197,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz", - "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==", + "version": "18.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", + "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==", "dev": true }, "node_modules/@types/normalize-package-data": { @@ -1206,14 +1215,14 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz", - "integrity": "sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.54.0.tgz", + "integrity": "sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.53.0", - "@typescript-eslint/type-utils": "5.53.0", - "@typescript-eslint/utils": "5.53.0", + "@typescript-eslint/scope-manager": "5.54.0", + "@typescript-eslint/type-utils": "5.54.0", + "@typescript-eslint/utils": "5.54.0", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -1255,14 +1264,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.53.0.tgz", - "integrity": "sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.54.0.tgz", + "integrity": "sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.53.0", - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/typescript-estree": "5.53.0", + "@typescript-eslint/scope-manager": "5.54.0", + "@typescript-eslint/types": "5.54.0", + "@typescript-eslint/typescript-estree": "5.54.0", "debug": "^4.3.4" }, "engines": { @@ -1282,13 +1291,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", - "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.54.0.tgz", + "integrity": "sha512-VTPYNZ7vaWtYna9M4oD42zENOBrb+ZYyCNdFs949GcN8Miwn37b8b7eMj+EZaq7VK9fx0Jd+JhmkhjFhvnovhg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/visitor-keys": "5.53.0" + "@typescript-eslint/types": "5.54.0", + "@typescript-eslint/visitor-keys": "5.54.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1299,13 +1308,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz", - "integrity": "sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.54.0.tgz", + "integrity": "sha512-WI+WMJ8+oS+LyflqsD4nlXMsVdzTMYTxl16myXPaCXnSgc7LWwMsjxQFZCK/rVmTZ3FN71Ct78ehO9bRC7erYQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.53.0", - "@typescript-eslint/utils": "5.53.0", + "@typescript-eslint/typescript-estree": "5.54.0", + "@typescript-eslint/utils": "5.54.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1326,9 +1335,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", - "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.54.0.tgz", + "integrity": "sha512-nExy+fDCBEgqblasfeE3aQ3NuafBUxZxgxXcYfzYRZFHdVvk5q60KhCSkG0noHgHRo/xQ/BOzURLZAafFpTkmQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1339,13 +1348,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", - "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.54.0.tgz", + "integrity": "sha512-X2rJG97Wj/VRo5YxJ8Qx26Zqf0RRKsVHd4sav8NElhbZzhpBI8jU54i6hfo9eheumj4oO4dcRN1B/zIVEqR/MQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/visitor-keys": "5.53.0", + "@typescript-eslint/types": "5.54.0", + "@typescript-eslint/visitor-keys": "5.54.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1381,16 +1390,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.53.0.tgz", - "integrity": "sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.54.0.tgz", + "integrity": "sha512-cuwm8D/Z/7AuyAeJ+T0r4WZmlnlxQ8wt7C7fLpFlKMR+dY6QO79Cq1WpJhvZbMA4ZeZGHiRWnht7ZJ8qkdAunw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.53.0", - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/typescript-estree": "5.53.0", + "@typescript-eslint/scope-manager": "5.54.0", + "@typescript-eslint/types": "5.54.0", + "@typescript-eslint/typescript-estree": "5.54.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -1428,12 +1437,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", - "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.54.0.tgz", + "integrity": "sha512-xu4wT7aRCakGINTLGeyGqDn+78BwFlggwBjnHa1ar/KaGagnmwLYmlrXIrgAaQ3AE1Vd6nLfKASm7LrFHNbKGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/types": "5.54.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1445,9 +1454,9 @@ } }, "node_modules/@vitest/coverage-c8": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.29.1.tgz", - "integrity": "sha512-YQZp1xGNxOcZD/zQBvD4bNpUDMJW7+FhBAEBlgvJp+DQ+aNK+dKcoWOTfsod27KQhXSr6UUYI8EYXWCOQqY6Eg==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.29.2.tgz", + "integrity": "sha512-NmD3WirQCeQjjKfHu4iEq18DVOBFbLn9TKVdMpyi5YW2EtnS+K22/WE+9/wRrepOhyeTxuEFgxUVkCAE1GhbnQ==", "dev": true, "dependencies": { "c8": "^7.13.0", @@ -1462,23 +1471,23 @@ } }, "node_modules/@vitest/expect": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.1.tgz", - "integrity": "sha512-VFt1u34D+/L4pqjLA8VGPdHbdF8dgjX9Nq573L9KG6/7MIAL9jmbEIKpXudmxjoTwcyczOXRyDuUWBQHZafjoA==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.2.tgz", + "integrity": "sha512-wjrdHB2ANTch3XKRhjWZN0UueFocH0cQbi2tR5Jtq60Nb3YOSmakjdAvUa2JFBu/o8Vjhj5cYbcMXkZxn1NzmA==", "dev": true, "dependencies": { - "@vitest/spy": "0.29.1", - "@vitest/utils": "0.29.1", + "@vitest/spy": "0.29.2", + "@vitest/utils": "0.29.2", "chai": "^4.3.7" } }, "node_modules/@vitest/runner": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.1.tgz", - "integrity": "sha512-VZ6D+kWpd/LVJjvxkt79OA29FUpyrI5L/EEwoBxH5m9KmKgs1QWNgobo/CGQtIWdifLQLvZdzYEK7Qj96w/ixQ==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.2.tgz", + "integrity": "sha512-A1P65f5+6ru36AyHWORhuQBJrOOcmDuhzl5RsaMNFe2jEkoj0faEszQS4CtPU/LxUYVIazlUtZTY0OEZmyZBnA==", "dev": true, "dependencies": { - "@vitest/utils": "0.29.1", + "@vitest/utils": "0.29.2", "p-limit": "^4.0.0", "pathe": "^1.1.0" } @@ -1499,18 +1508,18 @@ } }, "node_modules/@vitest/spy": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.1.tgz", - "integrity": "sha512-sRXXK44pPzaizpiZOIQP7YMhxIs80J/b6v1yR3SItpxG952c8tdA7n0O2j4OsVkjiO/ZDrjAYFrXL3gq6hLx6Q==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.2.tgz", + "integrity": "sha512-Hc44ft5kaAytlGL2PyFwdAsufjbdOvHklwjNy/gy/saRbg9Kfkxfh+PklLm1H2Ib/p586RkQeNFKYuJInUssyw==", "dev": true, "dependencies": { "tinyspy": "^1.0.2" } }, "node_modules/@vitest/ui": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.29.1.tgz", - "integrity": "sha512-CtcFlqcxNFT+5geTYfjOMOje4iae2DGsOnW1//7cre2xc43mFVOJYG7ZPq1wOUisDqSCyPLP2+zvAAlFJxBiUA==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.29.2.tgz", + "integrity": "sha512-GpCExCMptrS1z3Xf6kz35Xdvjc2eTBy9OIIwW3HjePVxw9Q++ZoEaIBVimRTTGzSe40XiAI/ZyR0H0Ya9brqLA==", "dev": true, "dependencies": { "fast-glob": "^3.2.12", @@ -1521,9 +1530,9 @@ } }, "node_modules/@vitest/utils": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.1.tgz", - "integrity": "sha512-6npOEpmyE6zPS2wsWb7yX5oDpp6WY++cC5BX6/qaaMhGC3ZlPd8BbTz3RtGPi1PfPerPvfs4KqS/JDOIaB6J3w==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.2.tgz", + "integrity": "sha512-F14/Uc+vCdclStS2KEoXJlOLAEyqRhnw0gM27iXw9bMTcyKRPJrQ+rlC6XZ125GIPvvKYMPpVxNhiou6PsEeYQ==", "dev": true, "dependencies": { "cli-truncate": "^3.1.0", @@ -2334,9 +2343,9 @@ "dev": true }, "node_modules/emitten": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/emitten/-/emitten-0.4.2.tgz", - "integrity": "sha512-M5IFvbnn+OJ3jXGEqQZ3fsDWa+UU5ajMQ5gnvWPd1pdCsGoS/6Rv/253Th4LoSueWfb9uiArp+JSLnKtFem89A==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/emitten/-/emitten-0.4.3.tgz", + "integrity": "sha512-H8LglEU3yZMiXjEGiISqvkTMg50H+BKcTgeGoBWVAdAmJuw2CrMI4et43Xu0K+vTZeNIs/25F8a3+AFWE6O15g==", "engines": { "node": ">=18.0.0" } @@ -2511,12 +2520,13 @@ } }, "node_modules/eslint": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", - "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz", + "integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.4.1", + "@eslint/eslintrc": "^2.0.0", + "@eslint/js": "8.35.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2530,7 +2540,7 @@ "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", "espree": "^9.4.0", - "esquery": "^1.4.0", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", @@ -3118,9 +3128,9 @@ } }, "node_modules/esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -3821,13 +3831,13 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" }, "funding": { @@ -5358,9 +5368,9 @@ } }, "node_modules/rollup": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.17.3.tgz", - "integrity": "sha512-p5LaCXiiOL/wrOkj8djsIDFmyU9ysUxcyW+EKRLHb6TKldJzXpImjcRSR+vgo09DBdofGcOoLOsRyxxG2n5/qQ==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.18.0.tgz", + "integrity": "sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -6019,9 +6029,9 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", - "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.4.0.tgz", + "integrity": "sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==", "dev": true }, "node_modules/tinypool": { @@ -6355,9 +6365,9 @@ } }, "node_modules/ufo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.0.tgz", - "integrity": "sha512-LQc2s/ZDMaCN3QLpa+uzHUOQ7SdV0qgv3VBXOolQGXTaaZpIur6PwUclF5nN2hNkiTRcUugXd1zFOW3FLJ135Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", + "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", "dev": true }, "node_modules/unbox-primitive": { @@ -6476,9 +6486,9 @@ } }, "node_modules/vite-node": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.1.tgz", - "integrity": "sha512-Ey9bTlQOQrCxQN0oJ7sTg+GrU4nTMLg44iKTFCKf31ry60csqQz4E+Q04hdWhwE4cTgpxUC+zEB1kHbf5jNkFA==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.2.tgz", + "integrity": "sha512-5oe1z6wzI3gkvc4yOBbDBbgpiWiApvuN4P55E8OI131JGrSuo4X3SOZrNmZYo4R8Zkze/dhi572blX0zc+6SdA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -6558,18 +6568,18 @@ } }, "node_modules/vitest": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.1.tgz", - "integrity": "sha512-iSy6d9VwsIn7pz5I8SjVwdTLDRGKNZCRJVzROwjt0O0cffoymKwazIZ2epyMpRGpeL5tsXAl1cjXiT7agTyVug==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.2.tgz", + "integrity": "sha512-ydK9IGbAvoY8wkg29DQ4ivcVviCaUi3ivuPKfZEVddMTenFHUfB8EEDXQV8+RasEk1ACFLgMUqAaDuQ/Nk+mQA==", "dev": true, "dependencies": { "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.29.1", - "@vitest/runner": "0.29.1", - "@vitest/spy": "0.29.1", - "@vitest/utils": "0.29.1", + "@vitest/expect": "0.29.2", + "@vitest/runner": "0.29.2", + "@vitest/spy": "0.29.2", + "@vitest/utils": "0.29.2", "acorn": "^8.8.1", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -6585,7 +6595,7 @@ "tinypool": "^0.3.1", "tinyspy": "^1.0.2", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.29.1", + "vite-node": "0.29.2", "why-is-node-running": "^2.2.2" }, "bin": { diff --git a/package.json b/package.json index 8841298..9077293 100644 --- a/package.json +++ b/package.json @@ -47,21 +47,21 @@ "lint": "eslint . --ext .ts,.tsx", "test": "vitest", "test:ui": "vitest --ui", - "coverage": "vitest --coverage", + "coverage": "vitest --run --coverage", "report": "changeset", "release": "npm run build && changeset publish" }, "dependencies": { - "emitten": "^0.4.2" + "emitten": "^0.4.3" }, "devDependencies": { "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.0", - "@types/node": "^18.14.1", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@vitest/coverage-c8": "^0.29.1", - "@vitest/ui": "^0.29.1", - "eslint": "^8.34.0", + "@types/node": "^18.14.6", + "@typescript-eslint/eslint-plugin": "^5.54.0", + "@vitest/coverage-c8": "^0.29.2", + "@vitest/ui": "^0.29.2", + "eslint": "^8.35.0", "eslint-config-prettier": "^8.6.0", "eslint-config-standard-with-typescript": "^34.0.0", "eslint-plugin-import": "^2.27.5", @@ -72,9 +72,9 @@ "typescript": "^4.9.5", "vite": "^4.1.4", "vite-plugin-dts": "^2.0.2", - "vitest": "^0.29.1" + "vitest": "^0.29.2" }, "peerDependencies": { - "emitten": "^0.4.2" + "emitten": "^0.4.3" } } diff --git a/src/Earwurm.ts b/src/Earwurm.ts index 2cf8b46..306b714 100644 --- a/src/Earwurm.ts +++ b/src/Earwurm.ts @@ -11,6 +11,7 @@ import type { ManagerConfig, LibraryEntry, LibraryKeys, + StackEventMap, } from './types'; import {Stack} from './Stack'; @@ -46,7 +47,8 @@ export class Earwurm extends EmittenCommon { this.#gainNode.connect(this.#context.destination); this.#gainNode.gain.setValueAtTime(this._volume, this.#context.currentTime); - this.#autoSuspend(); + + if (this._unlocked) this.#autoSuspend(); this.#context.addEventListener('statechange', this.#handleStateChange); } @@ -178,13 +180,6 @@ export class Earwurm extends EmittenCommon { this.#library.forEach((stack) => stack.teardown()); this.#setLibrary([]); - this.#queuedResume = false; - - if (this.#suspendId) { - clearTimeout(this.#suspendId); - this.#suspendId = 0; - } - this.#context .close() .then(() => { @@ -200,6 +195,8 @@ export class Earwurm extends EmittenCommon { ]); }); + this.empty(); + return this; } @@ -230,10 +227,14 @@ export class Earwurm extends EmittenCommon { getErrorMessage(error), ]); }); - - this.#queuedResume = false; } + this.#clearSuspendResume(); + } + + #clearSuspendResume() { + this.#queuedResume = false; + if (this.#suspendId) { clearTimeout(this.#suspendId); this.#suspendId = 0; @@ -253,8 +254,10 @@ export class Earwurm extends EmittenCommon { if (value === 'running') { this._unlocked = true; + this.#autoSuspend(); } else if (value === 'closed') { this._unlocked = false; + this.#clearSuspendResume(); } } @@ -287,7 +290,12 @@ export class Earwurm extends EmittenCommon { this.#setState(this.#context.state); }; - #handleStackStateChange = () => { + #handleStackStateChange: StackEventMap['statechange'] = (state) => { + // We don't care about re-setting the auto-suspension each time + // a new `Sound` is prepared... but it will do that anyways + // since `Stack` returns to `idle` once loaded. + if (state === 'loading') return; + if (this.playing) { this.#autoResume(); } else { diff --git a/src/tests/Earwurm.test.ts b/src/tests/Earwurm.test.ts index b7a863f..c304a0e 100644 --- a/src/tests/Earwurm.test.ts +++ b/src/tests/Earwurm.test.ts @@ -1,11 +1,24 @@ import {describe, it, expect, vi} from 'vitest'; import {Earwurm} from '../Earwurm'; +import {Stack} from '../Stack'; +import type {Sound} from '../Sound'; import {tokens} from '../tokens'; -import type {ManagerEventMap} from '../types'; +import type { + ManagerEventMap, + ManagerConfig, + LibraryEntry, + LibraryKeys, +} from '../types'; import {mockData} from './mock'; describe('Earwurm component', () => { + const mockEntries: LibraryEntry[] = [ + {id: 'Zero', path: mockData.audio}, + {id: 'One', path: 'to/no/file.mp3'}, + {id: 'Two', path: ''}, + ]; + describe('initialization', () => { const testManager = new Earwurm(); @@ -40,20 +53,17 @@ describe('Earwurm component', () => { const testManager = new Earwurm(); expect(testManager.keys).toHaveLength(0); + testManager.add(...mockEntries); - testManager.add( - {id: 'One', path: mockData.audio}, - {id: 'Two', path: 'to/no/file.mp3'}, - {id: 'Three', path: ''}, - ); - - expect(testManager.keys).toStrictEqual(['One', 'Two', 'Three']); - testManager.remove('One'); - expect(testManager.keys).toStrictEqual(['Two', 'Three']); + expect(testManager.keys).toStrictEqual(['Zero', 'One', 'Two']); + testManager.remove('Zero'); + expect(testManager.keys).toStrictEqual(['One', 'Two']); }); }); describe('state', () => { + const clickEvent = new Event('click'); + it('triggers `statechange` event for every state', async () => { const testManager = new Earwurm(); const spyState: ManagerEventMap['statechange'] = vi.fn((_state) => {}); @@ -64,8 +74,6 @@ describe('Earwurm component', () => { expect(testManager.state).toBe('suspended'); testManager.unlock(); - - const clickEvent = new Event('click'); document.dispatchEvent(clickEvent); // await vi.advanceTimersToNextTimerAsync(); @@ -94,17 +102,565 @@ describe('Earwurm component', () => { describe('playing', () => { it('is `true` when any Sound is `playing`', async () => { const testManager = new Earwurm(); + const mockEntry = mockEntries[0]; - testManager.add({id: 'One', path: mockData.audio}); + testManager.add(mockEntry); - const stack = testManager.get('One'); + const stack = testManager.get(mockEntry.id); const sound = await stack?.prepare(); expect(testManager.playing).toBe(false); sound?.play(); expect(testManager.playing).toBe(true); - vi.advanceTimersByTime(10); + vi.advanceTimersByTime(mockData.playDurationMs); expect(testManager.playing).toBe(false); }); }); + + describe('get()', () => { + const testManager = new Earwurm(); + + it('returns `undefined` when there is no matching Stack', async () => { + const requestedStack = testManager.get('FakeId'); + expect(requestedStack).toBe(undefined); + }); + + it('returns the requested Stack when present', async () => { + const mockEntry = mockEntries[0]; + testManager.add(mockEntry); + + const requestedStack = testManager.get(mockEntry.id); + + expect(requestedStack).toBeInstanceOf(Stack); + expect(requestedStack?.id).toBe(mockEntry.id); + }); + }); + + describe('has()', () => { + const testManager = new Earwurm(); + + it('returns `false` when there is no matching Stack', async () => { + const hasStack = testManager.has('FakeId'); + expect(hasStack).toBe(false); + }); + + it('returns `true` when the requested Stack is present', async () => { + const mockEntry = mockEntries[0]; + testManager.add(mockEntry); + + const hasStack = testManager.has(mockEntry.id); + expect(hasStack).toBe(true); + }); + }); + + describe('unlock()', () => { + const clickEvent = new Event('click'); + + it('unlocks AudioContext if not already unlocked', async () => { + const testManager = new Earwurm(); + + expect(testManager.unlocked).toBe(false); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(testManager.unlocked).toBe(true); + }); + + it('does not unlock if already unlocked', async () => { + const testManager = new Earwurm(); + const spyStateChange: ManagerEventMap['statechange'] = vi.fn( + (_state) => {}, + ); + + testManager.on('statechange', spyStateChange); + expect(spyStateChange).not.toBeCalled(); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(spyStateChange).toBeCalledTimes(1); + expect(spyStateChange).toBeCalledWith('running'); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(spyStateChange).not.toBeCalledTimes(2); + }); + + it('restores lock upon close', async () => { + const testManager = new Earwurm(); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(testManager.unlocked).toBe(true); + + testManager.teardown(); + document.dispatchEvent(clickEvent); + + expect(testManager.unlocked).toBe(false); + }); + + it('returns instance', async () => { + const testManager = new Earwurm(); + + const beforeUnlock = testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(beforeUnlock).toBeInstanceOf(Earwurm); + + const afterUnlock = testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(afterUnlock).toBeInstanceOf(Earwurm); + }); + }); + + describe('add()', () => { + it('creates a new Stack for each entry', async () => { + const testManager = new Earwurm(); + const capturedKeys = testManager.add(...mockEntries); + + expect(testManager.keys).toHaveLength(mockEntries.length); + expect(capturedKeys).toStrictEqual(mockEntries.map(({id}) => id)); + }); + + it('replaces any existing Stacks', async () => { + const testManager = new Earwurm(); + + const mockChangedEntries: LibraryEntry[] = [ + { + id: 'Unique', + path: 'does/not/overwrite/anything.wav', + }, + { + id: mockEntries[1].id, + path: 'some/new/path/file.webm', + }, + { + id: mockEntries[2].id, + path: 'another/changed/path.mp3', + }, + ]; + + testManager.add(...mockEntries); + + const stack1 = testManager.get(mockEntries[1].id); + const stack2 = testManager.get(mockEntries[2].id); + + expect(stack1?.path).toBe(mockEntries[1].path); + expect(stack2?.path).toBe(mockEntries[2].path); + expect(testManager.keys).toHaveLength(3); + + testManager.add(...mockChangedEntries); + + const updatedStack1 = testManager.get(mockEntries[1].id); + const updatedStack2 = testManager.get(mockEntries[2].id); + + expect(updatedStack1?.path).toBe(mockChangedEntries[1].path); + expect(updatedStack2?.path).toBe(mockChangedEntries[2].path); + expect(testManager.keys).toHaveLength(4); + }); + + // TODO: Figure out how best to read `fadeMs` and `request` from Stack. + it.skip('passes `fadeMs` and `request` to Stack', async () => { + const mockConfig: ManagerConfig = { + fadeMs: 100, + request: { + integrity: 'foo', + method: 'bar', + referrer: 'baz', + keepalive: true, + }, + }; + + const testManager = new Earwurm(mockConfig); + testManager.add(...mockEntries); + + const stack = testManager.get(mockEntries[0].id); + expect(stack).toBeInstanceOf(Stack); + }); + }); + + describe('remove()', () => { + it('removes any present Stacks', async () => { + const testManager = new Earwurm(); + + testManager.add(...mockEntries); + + const mockRemovedKeys: LibraryKeys = [ + mockEntries[0].id, + mockEntries[1].id, + ]; + const capturedKeys = testManager.remove(...mockRemovedKeys); + + expect(testManager.keys).toStrictEqual([mockEntries[2].id]); + expect(capturedKeys).toStrictEqual(mockRemovedKeys); + }); + + it('returns empty array when no Stacks match', async () => { + const testManager = new Earwurm(); + + testManager.add(...mockEntries); + + const capturedKeys = testManager.remove('Foo', 'Bar'); + expect(capturedKeys).toStrictEqual([]); + }); + + it('tears down Stacks before removing from library', async () => { + const testManager = new Earwurm(); + + const mockChangedEntries: LibraryEntry[] = [ + { + id: mockEntries[1].id, + path: 'some/new/path/file.webm', + }, + { + id: mockEntries[2].id, + path: 'another/changed/path.mp3', + }, + ]; + + const spyStack1StateChange = vi.fn(); + const spyStack2StateChange = vi.fn(); + + testManager.add(...mockEntries); + + const stack1 = testManager.get(mockEntries[1].id); + stack1?.on('statechange', spyStack1StateChange); + await stack1?.prepare().then((sound) => sound.play()); + + expect(spyStack1StateChange).toBeCalledTimes(3); + expect(stack1?.state).toBe('playing'); + + const stack2 = testManager.get(mockEntries[2].id); + stack2?.on('statechange', spyStack2StateChange); + await stack2?.prepare().then((sound) => sound.play()); + + expect(spyStack2StateChange).toBeCalledTimes(3); + expect(stack2?.state).toBe('playing'); + + testManager.add(...mockChangedEntries); + + expect(spyStack1StateChange).toBeCalledTimes(4); + expect(stack1?.state).toBe('idle'); + + expect(spyStack2StateChange).toBeCalledTimes(4); + expect(stack2?.state).toBe('idle'); + }); + }); + + describe('stop()', () => { + it('stops all playing sounds with each Stack', async () => { + const testManager = new Earwurm(); + + const stacks: Stack[] = []; + const stackCount = mockEntries.length; + + const sounds: Array> = []; + const soundsPerStack = 4; + + testManager.add(...mockEntries); + + for (let i = 0; i < stackCount; i++) { + const matchedStack = testManager.get(mockEntries[i].id); + if (matchedStack) stacks.push(matchedStack); + } + + stacks.forEach((stack) => { + for (let i = 0; i < soundsPerStack; i++) { + sounds.push(stack.prepare()); + } + }); + + for await (const sound of sounds) { + sound.play(); + } + + expect(testManager.playing).toBe(true); + expect(testManager.keys).toHaveLength(3); + + testManager.stop(); + + expect(testManager.playing).toBe(false); + expect(testManager.keys).toHaveLength(3); + }); + + it('returns instance', async () => { + const testManager = new Earwurm(); + + testManager.add(...mockEntries); + + const result = testManager.stop(); + expect(result).toBeInstanceOf(Earwurm); + }); + }); + + describe('teardown()', () => { + const clickEvent = new Event('click'); + + it('calls `teardown` on every Stack', async () => { + const testManager = new Earwurm(); + const spyStackTeardown = vi.spyOn(Stack.prototype, 'teardown'); + + testManager.add(...mockEntries); + testManager.teardown(); + + expect(spyStackTeardown).toBeCalledTimes(mockEntries.length); + }); + + it('empties the `library`', async () => { + const testManager = new Earwurm(); + + testManager.add(...mockEntries); + expect(testManager.keys).toHaveLength(mockEntries.length); + + testManager.teardown(); + expect(testManager.keys).toStrictEqual([]); + }); + + it('does not resume the AudioContext', async () => { + const testManager = new Earwurm(); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + vi.advanceTimersByTime(tokens.suspendAfterMs - 1); + expect(testManager.state).toBe('running'); + + testManager.teardown(); + expect(testManager.state).toBe('closed'); + + // You shouldn't really be able to add entries once closed... + testManager.add(...mockEntries); + + const stack0 = testManager.get(mockEntries[0].id); + const stack0Sound = await stack0?.prepare(); + + expect(testManager.state).toBe('closed'); + stack0Sound?.play(); + expect(testManager.state).toBe('closed'); + + vi.advanceTimersByTime(tokens.suspendAfterMs); + expect(testManager.state).toBe('closed'); + }); + + it('closes the AudioContext', async () => { + const testManager = new Earwurm(); + const spyClose = vi.spyOn(AudioContext.prototype, 'close'); + + testManager.teardown(); + + expect(spyClose).toBeCalled(); + }); + + it('removes state change listener, preventing further state changes', async () => { + const testManager = new Earwurm(); + expect(testManager.state).toBe('suspended'); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(testManager.state).toBe('running'); + + testManager.teardown(); + expect(testManager.state).toBe('closed'); + + testManager.add(...mockEntries); + + const stack = testManager.get(mockEntries[0].id); + const sound = await stack?.prepare(); + + sound?.play(); + + // Not sure how to check active events on the `AudioContext`. + expect(testManager.state).toBe('closed'); + }); + + // TODO: Figure out how to test the emitted error. + it('throws error if AudioContext cannot be closed', async () => { + const testManager = new Earwurm(); + const mockErrorMessage = 'Mock error message'; + + const spyError: ManagerEventMap['error'] = vi.fn((_error) => {}); + testManager.on('error', spyError); + + vi.spyOn(AudioContext.prototype, 'close').mockImplementationOnce(() => { + throw new Error(mockErrorMessage); + }); + + expect(() => testManager.teardown()).toThrowError(mockErrorMessage); + + /* + expect(spyError).toBeCalledWith([ + 'Failed to close the Earwurm AudioContext.', + mockErrorMessage, + ]); + */ + }); + + it('removes any event listeners', async () => { + const testManager = new Earwurm(); + const spyError = vi.fn(); + const spyStateChange = vi.fn(); + + testManager.on('error', spyError); + testManager.on('statechange', spyStateChange); + + expect(testManager.activeEvents).toHaveLength(2); + testManager.teardown(); + expect(testManager.activeEvents).toHaveLength(0); + }); + + it('returns instance', async () => { + const testManager = new Earwurm(); + + testManager.add(...mockEntries); + + const result = testManager.teardown(); + expect(result).toBeInstanceOf(Earwurm); + }); + }); + + describe('#autoSuspend()', () => { + const clickEvent = new Event('click'); + + it('registers once `state` is `running`', async () => { + const testManager = new Earwurm(); + expect(testManager.state).toBe('suspended'); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(testManager.state).toBe('running'); + vi.advanceTimersByTime(tokens.suspendAfterMs - 1); + expect(testManager.state).toBe('running'); + + vi.advanceTimersByTime(1); + expect(testManager.state).toBe('suspended'); + }); + + // TODO: Figure out a way to force `AudioContext.state` + // to initialize with `running`. + it.todo('can register upon initialization'); + + it('resets countdown when Stack states change', async () => { + const testManager = new Earwurm(); + testManager.add(...mockEntries); + + const stack0 = testManager.get(mockEntries[0].id); + + // Beginning of suspension timer. + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(testManager.state).toBe('running'); + vi.advanceTimersByTime(tokens.suspendAfterMs - 1); + expect(testManager.state).toBe('running'); + + // `Sound` preparation triggers `Stack` state changes, + // resulting in a suspension timer reset. + const stack0Sound = await stack0?.prepare(); + + vi.advanceTimersByTime(1); + expect(testManager.state).toBe('running'); + vi.advanceTimersByTime(tokens.suspendAfterMs - 2); + expect(testManager.state).toBe('running'); + + expect(stack0?.playing).toBe(false); + // Another suspension timer reset. + stack0Sound?.play(); + expect(stack0?.playing).toBe(true); + + vi.advanceTimersByTime(1); + expect(testManager.state).toBe('running'); + + // End of `Sound` + Reset of suspension timer. + vi.advanceTimersByTime(mockData.playDurationMs - 1); + expect(stack0?.playing).toBe(false); + + vi.advanceTimersByTime(tokens.suspendAfterMs - 1); + expect(testManager.state).toBe('running'); + + vi.advanceTimersByTime(1); + expect(testManager.state).toBe('suspended'); + }); + + it('triggers state changes', async () => { + const testManager = new Earwurm(); + const spyStateChange: ManagerEventMap['statechange'] = vi.fn( + (_state) => {}, + ); + + testManager.on('statechange', spyStateChange); + + expect(spyStateChange).not.toBeCalled(); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + expect(spyStateChange).toBeCalledWith('running'); + vi.advanceTimersByTime(tokens.suspendAfterMs); + expect(spyStateChange).toBeCalledWith('suspending'); + expect(spyStateChange).toBeCalledWith('suspended'); + }); + + // TODO: Is there any good way / value in testing these conditions? + it.todo('does not re-register while `suspending`'); + it.todo('does not re-register if already `suspended`'); + + it('does not allow suspension if `closed`', async () => { + const testManager = new Earwurm(); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + vi.advanceTimersByTime(tokens.suspendAfterMs); + expect(testManager.state).toBe('suspended'); + + testManager.teardown(); + expect(testManager.state).toBe('closed'); + + vi.advanceTimersByTime(tokens.suspendAfterMs); + expect(testManager.state).toBe('closed'); + }); + }); + + describe('#autoResume()', () => { + const clickEvent = new Event('click'); + + it('resumes once a `Sound` plays', async () => { + const testManager = new Earwurm(); + testManager.add(...mockEntries); + + testManager.unlock(); + document.dispatchEvent(clickEvent); + + const stack0 = testManager.get(mockEntries[0].id); + const stack0Sound = await stack0?.prepare(); + + expect(testManager.state).toBe('running'); + vi.advanceTimersByTime(tokens.suspendAfterMs); + expect(testManager.state).toBe('suspended'); + + stack0Sound?.play(); + expect(testManager.state).toBe('running'); + + vi.advanceTimersByTime(mockData.playDurationMs); + expect(testManager.state).toBe('running'); + + vi.advanceTimersByTime(tokens.suspendAfterMs); + expect(testManager.state).toBe('suspended'); + }); + + // TODO: Cannot test this until we can "interrupt" the suspension. + it('queues a resume if `state` is `suspending`'); + + // TODO: Figure out how to test this. + it('throws error if the resume fails'); + }); + + // Both `statechange` and `error` are covered in other tests. + // describe('events', () => {}); }); diff --git a/src/tests/Stack.test.ts b/src/tests/Stack.test.ts index e0d2801..17c0fc9 100644 --- a/src/tests/Stack.test.ts +++ b/src/tests/Stack.test.ts @@ -3,6 +3,7 @@ import {describe, it, expect, vi} from 'vitest'; import {Stack} from '../Stack'; import {Sound} from '../Sound'; import {tokens} from '../tokens'; +import {arrayOfLength} from '../utilities'; import type {StackEventMap, SoundEventMap} from '../types'; import {mockData} from './mock'; @@ -52,22 +53,25 @@ describe('Stack component', () => { it('contains ids of each unexpired Sound', async () => { const testStack = new Stack(...mockConstructorArgs); + const mockDurationHalf = Math.floor(mockData.playDurationMs / 2); + const mockDurationQuarter = Math.floor(mockData.playDurationMs / 4); + const sound1 = await testStack.prepare('One'); const sound2 = await testStack.prepare('Two'); const sound3 = await testStack.prepare('Three'); sound1.play(); - vi.advanceTimersByTime(4); + vi.advanceTimersByTime(mockDurationQuarter); sound2.play(); - vi.advanceTimersByTime(4); + vi.advanceTimersByTime(mockDurationQuarter); sound3.play(); expect(testStack.keys).toStrictEqual(['One', 'Two', 'Three']); - vi.advanceTimersByTime(2); + vi.advanceTimersByTime(mockDurationHalf); expect(testStack.keys).toStrictEqual(['Two', 'Three']); - vi.advanceTimersByTime(4); + vi.advanceTimersByTime(mockDurationQuarter); expect(testStack.keys).toStrictEqual(['Three']); - vi.advanceTimersByTime(4); + vi.advanceTimersByTime(mockDurationQuarter); expect(testStack.keys).toStrictEqual([]); }); }); @@ -121,7 +125,7 @@ describe('Stack component', () => { expect(testStack.state).toBe('playing'); expect(testStack.playing).toBe(true); - vi.advanceTimersByTime(10); + vi.advanceTimersByTime(mockData.playDurationMs); expect(testStack.state).toBe('idle'); expect(testStack.playing).toBe(false); @@ -391,6 +395,8 @@ describe('Stack component', () => { expect(testStack.state).toBe('playing'); }); + it.todo('passes `request` to `fetchAudioBuffer`'); + it('emits error when encountered', async () => { const mockStackId = 'TestLoadFail'; const mockPath = 'fake/path/file.mp3'; @@ -449,10 +455,6 @@ describe('Stack component', () => { {fadeMs: mockFadeMs}, ]; - function arrayOfLength(length: number) { - return Array.from(Array(length)); - } - it('constructs Sound', async () => { const testStack = new Stack(...mockConstructorArgs); @@ -499,7 +501,7 @@ describe('Stack component', () => { // Fill the `queue` up with the exact max number of Sounds. const pendingSounds = arrayOfLength(Stack.maxStackSize).map( - async () => await testStack.prepare(), + async (_index) => await testStack.prepare(), ); const sounds = await Promise.all(pendingSounds); @@ -528,7 +530,7 @@ describe('Stack component', () => { // Add more sounds before any current Sound has finished playing. const additionalSounds = arrayOfLength(additionalSoundsCount).map( - async () => await testStack.prepare(), + async (_index) => await testStack.prepare(), ); await Promise.all(additionalSounds); diff --git a/src/tests/helpers.test.ts b/src/tests/helpers.test.ts index 9b87d50..a42be56 100644 --- a/src/tests/helpers.test.ts +++ b/src/tests/helpers.test.ts @@ -11,7 +11,7 @@ import {mockData, audioBufferSourceNodeEndedEvent} from './mock'; describe('Helpers', () => { const mockContext = new AudioContext(); - describe('getErrorMessage', () => { + describe('getErrorMessage()', () => { it('returns message from basic object', () => { const mockError = { message: 'foo', @@ -41,7 +41,7 @@ describe('Helpers', () => { }); }); - describe('fetchAudioBuffer', () => { + describe('fetchAudioBuffer()', () => { it('throws parse Error on bogus path', async () => { const mockPath = './path/nowhere.webm'; @@ -61,7 +61,7 @@ describe('Helpers', () => { it.todo('passes custom options to fetch'); }); - describe('scratchBuffer', () => { + describe('scratchBuffer()', () => { it('creates a short silent AudioBuffer', () => { const result = scratchBuffer(mockContext); @@ -73,7 +73,7 @@ describe('Helpers', () => { }); }); - describe('unlockAudioContext', () => { + describe('unlockAudioContext()', () => { afterEach(() => { vi.advanceTimersToNextTimer(); }); diff --git a/src/tests/mock/MockAudioScheduledSourceNode.ts b/src/tests/mock/MockAudioScheduledSourceNode.ts index 704f984..c4b6a33 100644 --- a/src/tests/mock/MockAudioScheduledSourceNode.ts +++ b/src/tests/mock/MockAudioScheduledSourceNode.ts @@ -1,4 +1,5 @@ import {MockAudioNode} from './MockAudioNode'; +import {mockData} from './mock-data'; /* import {createErrorMessage} from './mock-utils'; @@ -18,7 +19,7 @@ export class MockAudioScheduledSourceNode // Artificial timeout for sound duration. setTimeout(() => { this.dispatchEvent(new Event('ended')); - }, 10); + }, mockData.playDurationMs); } stop(_when?: number | undefined): void { diff --git a/src/tests/mock/mock-data.ts b/src/tests/mock/mock-data.ts index c58e937..3adf1c8 100644 --- a/src/tests/mock/mock-data.ts +++ b/src/tests/mock/mock-data.ts @@ -3,4 +3,5 @@ // rely on external URLs for our tests. export const mockData = { audio: 'https://assets.mixkit.co/music/download/mixkit-happy-times-158.mp3', + playDurationMs: 40, }; diff --git a/src/tests/utilities.test.ts b/src/tests/utilities.test.ts index 664e1f1..fd209ed 100644 --- a/src/tests/utilities.test.ts +++ b/src/tests/utilities.test.ts @@ -1,9 +1,32 @@ import {describe, it, expect} from 'vitest'; -import {clamp, progressPercentage, msToSec, secToMs} from '../utilities'; +import { + arrayOfLength, + clamp, + progressPercentage, + msToSec, + secToMs, +} from '../utilities'; describe('Utilities', () => { + describe('Array', () => { + describe('arrayOfLength()', () => { + it('returns an empty array by default', () => { + const result = arrayOfLength(); + expect(result).toStrictEqual([]); + }); + + it('returns an array of incremented index values', () => { + const mockLength = 6; + const result = arrayOfLength(mockLength); + + expect(result).toStrictEqual([0, 1, 2, 3, 4, 5]); + expect(result).toHaveLength(mockLength); + }); + }); + }); + describe('Numbers', () => { - describe('clamp', () => { + describe('clamp()', () => { it('returns preference', () => { const mockArgs = { preference: 10, @@ -41,7 +64,7 @@ describe('Utilities', () => { }); }); - describe('progressPercentage', () => { + describe('progressPercentage()', () => { it('returns percentage integer', () => { const result = progressPercentage(6, 20); expect(result).toBe(30); @@ -57,14 +80,14 @@ describe('Utilities', () => { }); describe('Time', () => { - describe('msToSec', () => { + describe('msToSec()', () => { it('converts to seconds', () => { const result = msToSec(1234); expect(result).toBe(1.234); }); }); - describe('secToMs', () => { + describe('secToMs()', () => { it('converts to milliseconds', () => { const result = secToMs(5.678); expect(result).toBe(5678); diff --git a/src/types.ts b/src/types.ts index cb3c62d..83c0fd0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,8 +10,8 @@ export type ManagerState = AudioContextState | 'suspending' | 'interrupted'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type ManagerEventMap = { - statechange(state: ManagerState): void; - error(error: CombinedErrorMessage): void; + statechange: (state: ManagerState) => void; + error: (error: CombinedErrorMessage) => void; }; export interface ManagerConfig { @@ -40,8 +40,8 @@ export interface StackError { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type StackEventMap = { - statechange(state: StackState): void; - error(error: StackError): void; + statechange: (state: StackState) => void; + error: (error: StackError) => void; }; export interface StackConfig { @@ -65,8 +65,8 @@ export interface SoundEndedEvent { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type SoundEventMap = { - statechange(state: SoundState): void; - ended(event: SoundEndedEvent): void; + statechange: (state: SoundState) => void; + ended: (event: SoundEndedEvent) => void; // loop(ended: boolean): void; }; diff --git a/src/utilities/array.ts b/src/utilities/array.ts new file mode 100644 index 0000000..d5af360 --- /dev/null +++ b/src/utilities/array.ts @@ -0,0 +1,3 @@ +export function arrayOfLength(length = 0): number[] { + return Array.from(Array(length)).map((_item, index) => index); +} diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 7be1790..cfcb7c2 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,3 +1,3 @@ +export {arrayOfLength} from './array'; export {clamp, progressPercentage} from './numbers'; - export {msToSec, secToMs} from './time';