From 4b7758344ab422ef0b23d008c36384f64c0aedf5 Mon Sep 17 00:00:00 2001 From: nd0ut Date: Tue, 10 Oct 2023 12:54:30 +0300 Subject: [PATCH 1/4] chore: fix `fullPath` fallback type --- abstract/UploaderBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index bdbc155d4..388a17e3c 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -176,7 +176,7 @@ export class UploaderBlock extends ActivityBlock { fileSize: file.size, silentUpload: silent ?? false, source: source ?? UploadSource.API, - fullPath, + fullPath: fullPath ?? null, }); } From 9590f4edf499d48b20e089e155731e1e331e21a0 Mon Sep 17 00:00:00 2001 From: nd0ut Date: Tue, 10 Oct 2023 13:51:32 +0300 Subject: [PATCH 2/4] chore: add `withResolvers`, `waitForAttribute` and `delay` utils --- package-lock.json | 290 ++++++++++++++++++++++++++++++++- package.json | 1 + utils/delay.js | 4 + utils/waitForAttribute.js | 39 +++++ utils/waitForAttribute.test.js | 58 +++++++ utils/withResolvers.js | 21 +++ 6 files changed, 407 insertions(+), 6 deletions(-) create mode 100644 utils/delay.js create mode 100644 utils/waitForAttribute.js create mode 100644 utils/waitForAttribute.test.js create mode 100644 utils/withResolvers.js diff --git a/package-lock.json b/package-lock.json index ff6182de8..9591691da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "puppeteer": "^19.8.5", "rimraf": "^5.0.0", "shipjs": "^0.26.3", + "sinon": "^16.1.0", "stylelint": "^15.4.0", "stylelint-config-standard": "^32.0.0", "stylelint-declaration-block-no-ignored-properties": "^2.7.0", @@ -2643,6 +2644,50 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@slack/types": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@slack/types/-/types-1.10.0.tgz", @@ -5010,9 +5055,9 @@ "dev": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", "dev": true, "engines": { "node": ">=0.3.1" @@ -7769,6 +7814,12 @@ "node": "*" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -8240,6 +8291,12 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -9308,6 +9365,28 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -9895,6 +9974,21 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -11107,6 +11201,45 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.1.0.tgz", + "integrity": "sha512-ZSgzF0vwmoa8pq0GEynqfdnpEDyP1PkYmEChnkjW0Vyh8IDlyFEJ+fkMhCP0il6d5cJjPl2PUsnUSAuP5sttOQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -12185,6 +12318,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -14576,6 +14718,52 @@ "picomatch": "^2.2.2" } }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@slack/types": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@slack/types/-/types-1.10.0.tgz", @@ -16426,9 +16614,9 @@ "dev": true }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", "dev": true }, "dir-glob": { @@ -18470,6 +18658,12 @@ "through": ">=2.2.7 <3" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -18830,6 +19024,12 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -19534,6 +19734,30 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, "no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -19977,6 +20201,23 @@ } } }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -20866,6 +21107,37 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "sinon": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.1.0.tgz", + "integrity": "sha512-ZSgzF0vwmoa8pq0GEynqfdnpEDyP1PkYmEChnkjW0Vyh8IDlyFEJ+fkMhCP0il6d5cJjPl2PUsnUSAuP5sttOQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -21683,6 +21955,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", diff --git a/package.json b/package.json index dc16cd3df..67c389076 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "puppeteer": "^19.8.5", "rimraf": "^5.0.0", "shipjs": "^0.26.3", + "sinon": "^16.1.0", "stylelint": "^15.4.0", "stylelint-config-standard": "^32.0.0", "stylelint-declaration-block-no-ignored-properties": "^2.7.0", diff --git a/utils/delay.js b/utils/delay.js new file mode 100644 index 000000000..5eb71b3ff --- /dev/null +++ b/utils/delay.js @@ -0,0 +1,4 @@ +// @ts-check + +/** @param {number} ms */ +export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/utils/waitForAttribute.js b/utils/waitForAttribute.js new file mode 100644 index 000000000..e4b08eb0e --- /dev/null +++ b/utils/waitForAttribute.js @@ -0,0 +1,39 @@ +// @ts-check + +/** + * @param {{ + * element: HTMLElement; + * attribute: string; + * onMutate: (value: string) => void; + * onTimeout: () => void; + * timeout?: number; + * }} options + */ +export const waitForAttribute = ({ element, attribute, onMutate, onTimeout, timeout = 300 }) => { + const timeoutId = setTimeout(() => { + observer.disconnect(); + onTimeout(); + }, timeout); + /** @param {MutationRecord} mutation */ + const handleMutation = (mutation) => { + const attrValue = element.getAttribute(attribute); + if (mutation.type === 'attributes' && mutation.attributeName === attribute && attrValue !== null) { + clearTimeout(timeoutId); + observer.disconnect(); + onMutate(attrValue); + } + }; + const currentAttrValue = element.getAttribute(attribute); + if (currentAttrValue !== null) { + clearTimeout(timeoutId); + onMutate(currentAttrValue); + } + const observer = new MutationObserver((mutations) => { + const mutation = mutations[mutations.length - 1]; + handleMutation(mutation); + }); + observer.observe(element, { + attributes: true, + attributeFilter: [attribute], + }); +}; diff --git a/utils/waitForAttribute.test.js b/utils/waitForAttribute.test.js new file mode 100644 index 000000000..e739bf878 --- /dev/null +++ b/utils/waitForAttribute.test.js @@ -0,0 +1,58 @@ +import { expect } from '@esm-bundle/chai'; +import { spy } from 'sinon'; +import { delay } from './delay'; +import { waitForAttribute } from './waitForAttribute'; + +const TEST_ATTRIBUTE = 'test-attribute'; + +describe('waitForAttribute', () => { + it('should call onTimeout callback when timeout is over', async () => { + const element = document.createElement('div'); + const onMutate = spy(); + const onTimeout = spy(); + waitForAttribute({ + element, + attribute: TEST_ATTRIBUTE, + onMutate, + onTimeout, + timeout: 10, + }); + await delay(100); + expect(onMutate.called).to.be.false; + expect(onTimeout.calledOnce).to.be.true; + }); + it('should call onMutate callback when attribute is set async', async () => { + const element = document.createElement('div'); + const onMutate = spy(); + const onTimeout = spy(); + waitForAttribute({ + element, + attribute: TEST_ATTRIBUTE, + onMutate, + onTimeout, + timeout: 10, + }); + element.setAttribute(TEST_ATTRIBUTE, 'test'); + await delay(100); + expect(onMutate.calledOnce).to.be.true; + expect(onMutate.getCall(0).args[0]).to.equal('test'); + expect(onTimeout.called).to.be.false; + }); + it('should call onMutate callback when attribute is set sync', async () => { + const element = document.createElement('div'); + element.setAttribute(TEST_ATTRIBUTE, 'test'); + const onMutate = spy(); + const onTimeout = spy(); + waitForAttribute({ + element, + attribute: TEST_ATTRIBUTE, + onMutate, + onTimeout, + timeout: 10, + }); + await delay(100); + expect(onMutate.calledOnce).to.be.true; + expect(onMutate.getCall(0).args[0]).to.equal('test'); + expect(onTimeout.called).to.be.false; + }); +}); diff --git a/utils/withResolvers.js b/utils/withResolvers.js new file mode 100644 index 000000000..921b669de --- /dev/null +++ b/utils/withResolvers.js @@ -0,0 +1,21 @@ +// @ts-check + +/** @template [T=void] Default is `void` */ +export function withResolvers() { + /** @type {(value: T | PromiseLike) => void} */ + let resolve; + /** @type {(reason: unknown) => void} */ + let reject; + /** @type {Promise} */ + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + return { + // @ts-expect-error TODO: fix used before assigned + resolve, + // @ts-expect-error TODO: fix used before assigned + reject, + promise, + }; +} From 11d5a94c9131398138eda27011616745ee4b45fe Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 12 Oct 2023 11:44:47 +0300 Subject: [PATCH 3/4] fix: require `ctx-name` attribute for all the public blocks and wait for it with 300ms timeout this is required mostly for Angular projects, because it sets binded attributes async --- abstract/Block.js | 19 +++++++++++++- abstract/SolutionBlock.js | 1 + blocks/Config/Config.js | 2 ++ blocks/DataOutput/DataOutput.js | 1 + blocks/ShadowWrapper/ShadowWrapper.js | 15 ++++++----- blocks/UploadCtxProvider/UploadCtxProvider.js | 4 ++- solutions/file-uploader/inline/demo.htm | 4 +-- solutions/file-uploader/minimal/demo.htm | 1 + solutions/file-uploader/regular/demo.htm | 4 +-- utils/waitForAttribute.js | 8 +++--- utils/waitForAttribute.test.js | 26 +++++++++---------- 11 files changed, 56 insertions(+), 29 deletions(-) diff --git a/abstract/Block.js b/abstract/Block.js index c475ffaa3..ef5df9694 100644 --- a/abstract/Block.js +++ b/abstract/Block.js @@ -8,6 +8,7 @@ import { sharedConfigKey } from './sharedConfigKey.js'; import { toKebabCase } from '../utils/toKebabCase.js'; import { warnOnce } from '../utils/warnOnce.js'; import { getPluralForm } from '../utils/getPluralForm.js'; +import { waitForAttribute } from '../utils/waitForAttribute.js'; const TAG_PREFIX = 'lr-'; @@ -16,6 +17,7 @@ export class Block extends BaseComponent { /** @type {string | null} */ static StateConsumerScope = null; static className = ''; + requireCtxName = false; allowCustomTemplate = true; init$ = blockCtx(); @@ -137,7 +139,22 @@ export class Block extends BaseComponent { this.constructor['template'] = null; this.processInnerHtml = true; } - super.connectedCallback(); + if (this.requireCtxName) { + waitForAttribute({ + element: this, + attribute: 'ctx-name', + onSuccess: () => { + // async wait for ctx-name attribute to be set, needed for Angular because it sets attributes after mount + // TODO: should be moved to the symbiote core + super.connectedCallback(); + }, + onTimeout: () => { + console.error('Attribute `ctx-name` is required and it is not set.'); + }, + }); + } else { + super.connectedCallback(); + } } disconnectedCallback() { diff --git a/abstract/SolutionBlock.js b/abstract/SolutionBlock.js index 9dc23104d..6a4160940 100644 --- a/abstract/SolutionBlock.js +++ b/abstract/SolutionBlock.js @@ -2,6 +2,7 @@ import { ShadowWrapper } from '../blocks/ShadowWrapper/ShadowWrapper.js'; import { uploaderBlockCtx } from './CTX.js'; export class SolutionBlock extends ShadowWrapper { + requireCtxName = true; init$ = uploaderBlockCtx(this); _template = null; diff --git a/blocks/Config/Config.js b/blocks/Config/Config.js index 0368efbf9..fa169455f 100644 --- a/blocks/Config/Config.js +++ b/blocks/Config/Config.js @@ -4,6 +4,7 @@ import { initialConfig } from './initialConfig.js'; import { sharedConfigKey } from '../../abstract/sharedConfigKey.js'; import { toKebabCase } from '../../utils/toKebabCase.js'; import { normalizeConfigValue } from './normalizeConfigValue.js'; +import { waitForAttribute } from '../../utils/waitForAttribute.js'; const allConfigKeys = /** @type {(keyof import('../../types').ConfigType)[]} */ (Object.keys(initialConfig)); @@ -40,6 +41,7 @@ const attrStateMapping = /** @type {Record { - let href = this.getAttribute(CSS_ATTRIBUTE); - if (href) { + waitForAttribute({ + element: this, + attribute: CSS_ATTRIBUTE, + onSuccess: (href) => { this.attachShadow({ mode: 'open', }); @@ -53,11 +55,12 @@ export function shadowed(Base) { }; // @ts-ignore TODO: fix this this.shadowRoot.prepend(link); - } else { + }, + onTimeout: () => { console.error( 'Attribute `css-src` is required and it is not set. See migration guide: https://uploadcare.com/docs/file-uploader/migration-to-0.25.0/' ); - } + }, }); } }; diff --git a/blocks/UploadCtxProvider/UploadCtxProvider.js b/blocks/UploadCtxProvider/UploadCtxProvider.js index 9b978809c..70b169dda 100644 --- a/blocks/UploadCtxProvider/UploadCtxProvider.js +++ b/blocks/UploadCtxProvider/UploadCtxProvider.js @@ -1,3 +1,5 @@ import { UploaderBlock } from '../../abstract/UploaderBlock.js'; -export class UploadCtxProvider extends UploaderBlock {} +export class UploadCtxProvider extends UploaderBlock { + requireCtxName = true; +} diff --git a/solutions/file-uploader/inline/demo.htm b/solutions/file-uploader/inline/demo.htm index d2672b8f9..dd824b284 100644 --- a/solutions/file-uploader/inline/demo.htm +++ b/solutions/file-uploader/inline/demo.htm @@ -27,7 +27,7 @@

Multiple sources

- +

Single source

@@ -60,5 +60,5 @@

Single source

- + diff --git a/solutions/file-uploader/minimal/demo.htm b/solutions/file-uploader/minimal/demo.htm index cc64fce46..d28290f17 100644 --- a/solutions/file-uploader/minimal/demo.htm +++ b/solutions/file-uploader/minimal/demo.htm @@ -18,6 +18,7 @@

Live example

Button with modal window - +

Single source

@@ -37,6 +37,6 @@

Single source

- + diff --git a/utils/waitForAttribute.js b/utils/waitForAttribute.js index e4b08eb0e..fd80e3b30 100644 --- a/utils/waitForAttribute.js +++ b/utils/waitForAttribute.js @@ -4,12 +4,12 @@ * @param {{ * element: HTMLElement; * attribute: string; - * onMutate: (value: string) => void; + * onSuccess: (value: string) => void; * onTimeout: () => void; * timeout?: number; * }} options */ -export const waitForAttribute = ({ element, attribute, onMutate, onTimeout, timeout = 300 }) => { +export const waitForAttribute = ({ element, attribute, onSuccess, onTimeout, timeout = 300 }) => { const timeoutId = setTimeout(() => { observer.disconnect(); onTimeout(); @@ -20,13 +20,13 @@ export const waitForAttribute = ({ element, attribute, onMutate, onTimeout, time if (mutation.type === 'attributes' && mutation.attributeName === attribute && attrValue !== null) { clearTimeout(timeoutId); observer.disconnect(); - onMutate(attrValue); + onSuccess(attrValue); } }; const currentAttrValue = element.getAttribute(attribute); if (currentAttrValue !== null) { clearTimeout(timeoutId); - onMutate(currentAttrValue); + onSuccess(currentAttrValue); } const observer = new MutationObserver((mutations) => { const mutation = mutations[mutations.length - 1]; diff --git a/utils/waitForAttribute.test.js b/utils/waitForAttribute.test.js index e739bf878..5c47d6b4f 100644 --- a/utils/waitForAttribute.test.js +++ b/utils/waitForAttribute.test.js @@ -8,51 +8,51 @@ const TEST_ATTRIBUTE = 'test-attribute'; describe('waitForAttribute', () => { it('should call onTimeout callback when timeout is over', async () => { const element = document.createElement('div'); - const onMutate = spy(); + const onSuccess = spy(); const onTimeout = spy(); waitForAttribute({ element, attribute: TEST_ATTRIBUTE, - onMutate, + onSuccess, onTimeout, timeout: 10, }); await delay(100); - expect(onMutate.called).to.be.false; + expect(onSuccess.called).to.be.false; expect(onTimeout.calledOnce).to.be.true; }); - it('should call onMutate callback when attribute is set async', async () => { + it('should call onSuccess callback when attribute is set async', async () => { const element = document.createElement('div'); - const onMutate = spy(); + const onSuccess = spy(); const onTimeout = spy(); waitForAttribute({ element, attribute: TEST_ATTRIBUTE, - onMutate, + onSuccess, onTimeout, timeout: 10, }); element.setAttribute(TEST_ATTRIBUTE, 'test'); await delay(100); - expect(onMutate.calledOnce).to.be.true; - expect(onMutate.getCall(0).args[0]).to.equal('test'); + expect(onSuccess.calledOnce).to.be.true; + expect(onSuccess.getCall(0).args[0]).to.equal('test'); expect(onTimeout.called).to.be.false; }); - it('should call onMutate callback when attribute is set sync', async () => { + it('should call onSuccess callback when attribute is set sync', async () => { const element = document.createElement('div'); element.setAttribute(TEST_ATTRIBUTE, 'test'); - const onMutate = spy(); + const onSuccess = spy(); const onTimeout = spy(); waitForAttribute({ element, attribute: TEST_ATTRIBUTE, - onMutate, + onSuccess, onTimeout, timeout: 10, }); await delay(100); - expect(onMutate.calledOnce).to.be.true; - expect(onMutate.getCall(0).args[0]).to.equal('test'); + expect(onSuccess.calledOnce).to.be.true; + expect(onSuccess.getCall(0).args[0]).to.equal('test'); expect(onTimeout.called).to.be.false; }); }); From f9d62d4b13557758a954153995df010456429595 Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 12 Oct 2023 11:48:54 +0300 Subject: [PATCH 4/4] chore: fix lint --- blocks/Config/Config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/blocks/Config/Config.js b/blocks/Config/Config.js index fa169455f..4841dad02 100644 --- a/blocks/Config/Config.js +++ b/blocks/Config/Config.js @@ -4,7 +4,6 @@ import { initialConfig } from './initialConfig.js'; import { sharedConfigKey } from '../../abstract/sharedConfigKey.js'; import { toKebabCase } from '../../utils/toKebabCase.js'; import { normalizeConfigValue } from './normalizeConfigValue.js'; -import { waitForAttribute } from '../../utils/waitForAttribute.js'; const allConfigKeys = /** @type {(keyof import('../../types').ConfigType)[]} */ (Object.keys(initialConfig));