Repository: ROpdebee/mb-userscripts (ts)

Issue:
Violentmonkey Beta fails to find enqueued images
https://community.metabrainz.org/t/ropdebees-userscripts-support-thread/551947/62

> It didn’t take long for the next bug to reveal itself using Violentmonkey BETA 2.13.0.19

```
instrument.js:102 [mb_enhanced_cover_art_uploads] Searching for images in Bandcamp release…
instrument.js:102 [mb_enhanced_cover_art_uploads] Found 1 image(s) in Bandcamp release
instrument.js:102 [mb_enhanced_cover_art_uploads] Fetching https://f4.bcbits.com/img/a0273152139_10.jpg (1/1)
instrument.js:102 [mb_enhanced_cover_art_uploads] Maximised https://f4.bcbits.com/img/a0273152139_10.jpg to https://f4.bcbits.com/img/a0273152139_0.jpg
instrument.js:102 [mb_enhanced_cover_art_uploads] Failed to enqueue some images Error: Could not find image a0273152139_0.0.png in queued uploads
    at assert (MB： Enhanced Cover Art Uploads.user.js:106014:4515)
    at assertDefined (MB： Enhanced Cover Art Uploads.user.js:106014:4592)
    at setImageParameters (MB： Enhanced Cover Art Uploads.user.js:108139:5)
    at _catch (MB： Enhanced Cover Art Uploads.user.js:106011:5950)
    at MB： Enhanced Cover Art Uploads.user.js:106014:9916
    at MB： Enhanced Cover Art Uploads.user.js:106011:3654
    at MB： Enhanced Cover Art Uploads.user.js:106014:9985
    at async Promise.all (/release/d21b249f-1770-4af3-9fad-8f5752a82b04/index 0)
(anonymous) @ instrument.js:102
onError @ MB： Enhanced Cover Art Uploads.user.js:106014
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:106014
fireHandlers @ MB： Enhanced Cover Art Uploads.user.js:106014
error @ MB： Enhanced Cover Art Uploads.user.js:106014
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108528
Promise.then (async)
_catch @ MB： Enhanced Cover Art Uploads.user.js:106011
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108525
t @ MB： Enhanced Cover Art Uploads.user.js:106011
_forOf @ MB： Enhanced Cover Art Uploads.user.js:106011
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108520
Promise.then (async)
_continue @ MB： Enhanced Cover Art Uploads.user.js:106011
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108508
_call @ MB： Enhanced Cover Art Uploads.user.js:106011
processSeedingParameters @ MB： Enhanced Cover Art Uploads.user.js:108504
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108869
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108879
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:108880
window.VMina7ijceh13uhapolm @ MB： Enhanced Cover Art Uploads.user.js:108880
i @ injected-web.js:1
Vt @ injected-web.js:1
set @ injected-web.js:1
(anonymous) @ MB： Enhanced Cover Art Uploads.user.js:1
pt @ VM309 injected.js:1
mt @ VM309 injected.js:1
await in mt (async)
ft @ VM309 injected.js:1
ut @ VM309 injected.js:1
await in ut (async)
(anonymous) @ VM309 injected.js:1
await in (anonymous) (async)
(anonymous) @ VM309 injected.js:1
Re @ VM309 injected.js:1
dt @ VM309 injected.js:1
qt @ VM309 injected.js:1
await in qt (async)
(anonymous) @ VM309 injected.js:1
n @ VM309 injected.js:1
(anonymous) @ VM309 injected.js:1
(anonymous) @ VM309 injected.js:1
instrument.js:102 [mb_enhanced_cover_art_uploads] Successfully added 1 image(s)
```

Pull request:
fix(ecau): drop images without jQuery
So apparently my previous experimentation didn't go far enough and it turns out we don't actually need jQuery to get MB to handle the drop event. The reason we were using jQuery was that we can't "officially" instantiate our own `DropEvent`s with a `FileList` with our own files in the `dataTransfer` property (either because of read-only properties, private constructors, or some other security precaution). However, I never tried just defining the necessary properties using `Object.defineProperty`, which can circumvent the readonly status. Turns out, that just works. :tada:

Fixes #440.

Tested successfully on:
- [x] Firefox/Violentmonkey
- [x] Firefox/Violentmonkey BETA
- [x] Firefox/Tampermonkey
- [x] Firefox/Greasemonkey 4
- [x] Chromium/Violentmonkey
- [x] Chromium/Violentmonkey BETA
- [x] Chromium/Tampermonkey
- [x] Waterfox/Greasemonkey 3

Patch:
diff --git a/package-lock.json b/package-lock.json
index 468cf432..bf60f25c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,6 @@
       "version": "1.0.0",
       "license": "MIT; https://opensource.org/licenses/MIT",
       "dependencies": {
-        "jquery": "3.6.0",
         "p-throttle": "5.0.0",
         "ts-custom-error": "3.2.0"
       },
@@ -37,7 +36,6 @@
         "@types/greasemonkey": "4.0.3",
         "@types/jest": "28.1.1",
         "@types/jest-when": "3.5.0",
-        "@types/jquery": "3.5.14",
         "@types/postcss-preset-env": "6.7.3",
         "@types/rollup__plugin-virtual": "2.0.1",
         "@types/rollup-plugin-progress": "1.1.1",
@@ -3514,15 +3512,6 @@
         "@types/jest": "*"
       }
     },
-    "node_modules/@types/jquery": {
-      "version": "3.5.14",
-      "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
-      "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
-      "dev": true,
-      "dependencies": {
-        "@types/sizzle": "*"
-      }
-    },
     "node_modules/@types/jsdom": {
       "version": "16.2.14",
       "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz",
@@ -3735,12 +3724,6 @@
         "@types/pollyjs__core": "*"
       }
     },
-    "node_modules/@types/sizzle": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
-      "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
-      "dev": true
-    },
     "node_modules/@types/stack-utils": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -9754,11 +9737,6 @@
         "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
-    "node_modules/jquery": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
-      "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
-    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -16837,15 +16815,6 @@
         "@types/jest": "*"
       }
     },
-    "@types/jquery": {
-      "version": "3.5.14",
-      "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
-      "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
-      "dev": true,
-      "requires": {
-        "@types/sizzle": "*"
-      }
-    },
     "@types/jsdom": {
       "version": "16.2.14",
       "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz",
@@ -17045,12 +17014,6 @@
         "@types/pollyjs__core": "*"
       }
     },
-    "@types/sizzle": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
-      "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
-      "dev": true
-    },
     "@types/stack-utils": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -21438,11 +21401,6 @@
         }
       }
     },
-    "jquery": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
-      "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
-    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index 7f15d88d..f218b849 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,6 @@
     "@types/greasemonkey": "4.0.3",
     "@types/jest": "28.1.1",
     "@types/jest-when": "3.5.0",
-    "@types/jquery": "3.5.14",
     "@types/postcss-preset-env": "6.7.3",
     "@types/rollup__plugin-virtual": "2.0.1",
     "@types/rollup-plugin-progress": "1.1.1",
@@ -106,7 +105,6 @@
     "warcio": "1.5.1"
   },
   "dependencies": {
-    "jquery": "3.6.0",
     "p-throttle": "5.0.0",
     "ts-custom-error": "3.2.0"
   }
diff --git a/src/mb_enhanced_cover_art_uploads/form.ts b/src/mb_enhanced_cover_art_uploads/form.ts
index 654c403e..ac2b565e 100644
--- a/src/mb_enhanced_cover_art_uploads/form.ts
+++ b/src/mb_enhanced_cover_art_uploads/form.ts
@@ -13,12 +13,6 @@ export async function enqueueImages({ images }: FetchedImages, defaultTypes: Art
     }));
 }
 
-declare global {
-    interface Window {
-        $: typeof jQuery;
-    }
-}
-
 async function enqueueImage(image: FetchedImage, defaultTypes: ArtworkTypeIDs[], defaultComment: string): Promise<void> {
     dropImage(image.content);
     await retryTimes(setImageParameters.bind(
@@ -30,23 +24,18 @@ async function enqueueImage(image: FetchedImage, defaultTypes: ArtworkTypeIDs[],
 }
 
 function dropImage(imageData: File): void {
-    // Fake event to trigger the drop event on the drag'n'drop element
-    // Using jQuery because native JS cannot manually trigger such events
-    // for security reasons.
-    const $ = getFromPageContext('$');
-    const dropEvent = $.Event('drop') as JQuery.TriggeredEvent;
-    // We need to clone the underlying data since we might be running as a
-    // content script, meaning that even though we trigger the event through
-    // unsafeWindow, the page context may not be able to access the event's
-    // properties.
-    dropEvent.originalEvent = cloneIntoPageContext({
-        dataTransfer: { files: [imageData] },
-    } as unknown as DragEvent);
-
-    // Note that we're using MB's own jQuery here, not a script-local one.
-    // We need to reuse MB's own jQuery to be able to trigger the event
-    // handler.
-    $('#drop-zone').trigger(dropEvent);
+    // Greasemonkey acts up unless we use the page context `DataTransfer`.
+    const DataTransfer = getFromPageContext('DataTransfer');
+    const dataTransfer = new DataTransfer();
+
+    // `files` is a readonly property, but we can circumvent that with Object.defineProperty.
+    Object.defineProperty(dataTransfer, 'files', {
+        value: cloneIntoPageContext([imageData]),
+    });
+
+    const dropEvent = new DragEvent('drop', { dataTransfer });
+
+    qs('#drop-zone').dispatchEvent(dropEvent);
 }
 
 function setImageParameters(imageName: string, imageTypes: ArtworkTypeIDs[], imageComment: string): void {
diff --git a/src/mb_enhanced_cover_art_uploads/tsconfig.json b/src/mb_enhanced_cover_art_uploads/tsconfig.json
index e97c66e0..00f4bccd 100644
--- a/src/mb_enhanced_cover_art_uploads/tsconfig.json
+++ b/src/mb_enhanced_cover_art_uploads/tsconfig.json
@@ -5,6 +5,6 @@
         { "path": "../lib/" }
     ],
     "compilerOptions": {
-        "types": ["jquery", "nativejsx/types/jsx", "greasemonkey"]
+        "types": ["nativejsx/types/jsx", "greasemonkey"]
     }
 }


Test patch:
diff --git a/tests/unit/mb_enhanced_cover_art_uploads/form.test.ts b/tests/unit/mb_enhanced_cover_art_uploads/form.test.ts
index a1b123c3..ff39220f 100644
--- a/tests/unit/mb_enhanced_cover_art_uploads/form.test.ts
+++ b/tests/unit/mb_enhanced_cover_art_uploads/form.test.ts
@@ -1,19 +1,37 @@
 import fs from 'node:fs/promises';
 import path from 'node:path';
 
-import $ from 'jquery';
-
 import { ArtworkTypeIDs } from '@lib/MB/CoverArt';
 import { EditNote } from '@lib/MB/EditNote';
+import { qs } from '@lib/util/dom';
 import { enqueueImages, fillEditNote } from '@src/mb_enhanced_cover_art_uploads/form';
 
 import { createFetchedImage, createImageFile } from './test-utils/dummy-data';
 
-// @ts-expect-error need to inject a jQuery
-global.$ = $;
+beforeAll(() => {
+    // Need to mock DataTransfer and DragEvent.
+    class DataTransfer {
+        public readonly files: File[];
+
+        public constructor(files: File[]) {
+            this.files = files;
+        }
+    }
+
+    class DragEvent {
+        public readonly dataTransfer: DataTransfer;
 
-async function insertFileRows(evt: JQuery.TriggeredEvent): Promise<void> {
-    const files = (evt.originalEvent as (DragEvent | undefined))?.dataTransfer?.files;
+        public constructor(type: string, initOpts: { dataTransfer: DataTransfer }) {
+            this.dataTransfer = initOpts.dataTransfer;
+        }
+    }
+
+    global.DataTransfer = DataTransfer as unknown as typeof window.DataTransfer;
+    global.DragEvent = DragEvent as unknown as typeof window.DragEvent;
+});
+
+async function insertFileRows(evt: DragEvent): Promise<void> {
+    const files = evt.dataTransfer?.files;
     if (!files) return;
 
     const fileRowPath = path.resolve('.', 'tests', 'test-data', 'mb_enhanced_cover_art_uploads', 'form-row.html');
@@ -37,12 +55,13 @@ function getComment(row: HTMLTableRowElement | null): string | undefined {
 
 describe('enqueuing images', () => {
     const mockHtml = '<div id="drop-zone"/><table><tbody data-bind="foreach: files_to_upload"/></table>';
-    const onDropMock = jest.fn().mockImplementation(insertFileRows);
 
     beforeEach(() => {
         document.body.innerHTML = mockHtml;
-        onDropMock.mockClear();
-        $('#drop-zone').on('drop', onDropMock);
+        jest.spyOn(qs<HTMLElement>('#drop-zone'), 'dispatchEvent').mockImplementation((evt) => {
+            void insertFileRows(evt as unknown as DragEvent);
+            return true;
+        });
     });
 
     it('triggers the correct drop event', async () => {
@@ -53,14 +72,7 @@ describe('enqueuing images', () => {
             })],
         });
 
-        expect(onDropMock).toHaveBeenCalledOnce();
-        expect(onDropMock).toHaveBeenCalledWith(expect.objectContaining({
-            originalEvent: {
-                dataTransfer: {
-                    files: [image],
-                },
-            },
-        }));
+        expect(document.querySelector('tr')).not.toBeNil();
     });
 
     it('fills the correct parameters', async () => {