mock-violentmonkey allows you to mock Violentmonkey's api for testing Violentmonkey userscripts. mock-violentmonkey allows you to have seperated contexts for testing different scenarious without them interfering with each other. It was written with ava in mind but should work with other test runners.
I've stopped active development on this library. I'll continue to provide updates for dependencies and address any bugs that pop up. As for features, I consider the library complete. If you think there's something crucial missing, please open an issue to discuss potential additions.
This the whole magic. violentMonkeyContext seperates the various testing contexts from each other. This allows you to use GM_setValue
and the like and not worry about it affecting other tests.
test(
'title1',
violentMonkeyContext(t => {
// This is seperate
}),
);
test(
'title2',
violentMonkeyContext(t => {
// from here
}),
);
This is similar to violentMonkeyContext, but this makes use of ava macros and, as a result, can only be used with ava.
test('title', violentMonkeyContextMacro(), t => {
t.is(GM_getValue('a'), 'b');
});
// same as
test(
'title',
violentMonkeyContext(t => {
t.is(GM_getValue('a'), 'b');
}),
);
This allows you to simulate seperate tabs. It is currently only useful for GM_addValueChangeListener
and GM_removeValueChangeListener
.
test(
'title',
violentMonkeyContext(t => {
GM_addValueChangeListener('key', (key, oldValue, newValue, remote) => {
console.log(remote);
});
GM_setValue('key', 1); // Will log false
tabContext(() => {
GM_setValue('key', 2); // Will log true
});
}),
);
✔️ = supported, ❌ = not supported
Function | Support | Notes |
---|---|---|
GM_info / GM.info |
✔️ | Default value |
GM_getValue / GM.getValue |
✔️ | |
GM_setValue / GM.setValue |
✔️ | |
GM_deleteValue / GM.deleteValue |
✔️ | |
GM_listValues / GM.listValues |
✔️ | |
GM_addValueChangeListener |
✔️ | |
GM_removeValueChangeListener |
✔️ | |
GM_getResourceText |
✔️ | |
GM_getResourceURL / GM.getResourceURL |
✔️ | This returns an object url, which cannot be fetched with regular http libs in node, use GM_getResourceText instead. |
GM_addStyle / GM.addStyle |
✔️ | |
GM_openInTab / GM.openInTab |
✔️ | |
GM_registerMenuCommand |
✔️ | |
GM_unregisterMenuCommand |
✔️ | |
GM_notification / GM.notification |
✔️ | Because Chromium and Firefox's notifications behave slightly different, mock-violentmonkey's implementation allows you to simulate either with Firefox's behaviour by default. More info |
GM_setClipboard / GM.setClipboard |
✔️ | This doesn't actually set the clipboard. |
GM_xmlhttpRequest / GM.xmlHttpRequest |
✔️ | Can be used in combination with setBaseUrl |
GM_download |
✔️ | Can be used in combination with setBaseUrl |
The GM_*
and GM.*
api is added to the global scope so that userscripts have access to them.
With Typescript you can either import them or tell Typescript that they're globals.
If you import GM_info
it is not a getter, you have to call it.
import {GM_info} from 'mock-violentmonkey';
test(
'title',
violentMonkeyContext(t => {
console.log(GM_info());
}),
);
import {ScriptInfo} from 'mock-violentmonkey';
declare const GM_info: ScriptInfo;
test(
'title',
violentMonkeyContext(t => {
console.log(GM_info);
}),
);
Additionally, mock-violentmonkey has some helper functions for setting up tests.
This provides an easy way of updating GM_info
/ GM.info
. The object is mutable but update_GM_info
provides an easy way of updating it in one go.
test(
'title',
violentMonkeyContext(t => {
update_GM_info({
version: '2.0.0', // Version of Violentmonkey
platform: {
// Supports deep assignment
arch: 'arm', // update platform arch
},
script: {
version: '1.5.2', // Version of userscript
matches: ['https://github.com/*'], // Merges old array and new array
},
});
// Same as
GM_info.version = '2.0.0';
GM_info.platform.arch = 'arm';
GM_info.script.version = '1.5.2';
GM_info.script.matches.push('https://github.com/*');
}),
);
Because mock-violentmonkey can't access the headers you need to make sure to add the resources manually before testing code that requires @resource
tags.
type SetResource = (name: string, url: string) => Promise<string>;
// In the userscript
// @resource example.org https://example.org
// When testing
await setResource('example.org', 'https://example.org/');
Because there's no UI a different method of triggering menu commands is necessary. To simulate clicking a menu command you can use this command instead.
GM_registerMenuCommand('Open settings', openSettings);
triggerMenuCommand('Open settings'); // Calls `openSettings`
This sets the time after which the notification will timeout and be removed (like closing a notification). Defaults to 50ms. The notification takes the value at the time of creating the notification.
type SetNotificationTimeout = (timeout: number) => void;
This provides a way of finding all the notifications by their text, image, or title and removing, clicking, or closing all in one go.
Setting any of the selectors to undefined allows any value. That's why findNotifications({})
returns all notifications.
type FindNotifications = (selectors: {
text?: string;
image?: string;
title?: string;
}) => {
remove: () => void; // Remove notifications that match the selectors
click: () => void; // Click notifications that match the selectors
close: () => void; // Close notifications that match the selectors
count: () => number; // Get count of notifications that match the selectors
};
This allows you to have GM_notification
/ GM.notification
behave like with Firefox / Chromium. See more about the differences here
type SetNotificationCompat = (platform: 'Firefox' | 'Chromium') => void;
This allows you to get the current clipboard value for the current context.
type GetClipbard = () => {data: string; type: string} | undefined;
This allows you to see all open tabs and close them or see the resolved options.
If url is undefined, it returns all tabs.
type GetTabs = (url?: string | RegExp) => Tab[];
type Tab = {
close: () => void;
url: string;
options: {
active: boolean;
container: number;
insert: boolean;
pinned: boolean;
};
};
Instead of polluting the global namespace, this allows you to only enable whatever is required.
Only document
and window
are added to the global namespace by default.
This should always be used for FormData
,
because node's implementation of FormData
does not work with jsdom's File
.
This is not aware of violentmonkey-contexts, so calling it once at the start of the file is enough.
console.log(typeof FormData); // undefined
enableDomGlobal('FormData');
console.log(typeof FormData); // function
If your code should behave differently depending on the location, you can set the location with setBaseUrl
.
If not called, it defaults to http://localhost:5000/
. This is also used for relative urls.
This should run before calling GM_xmlhttpRequest
,
getWindow
or other functions reliant on jsdom,
ideally at the beginning of the vm context.
test(
'setBaseUrl before',
violentMonkeyContext(t => {
setBaseUrl('https://google.com/');
console.log(getWindow().location.href); // https://google.com/
}),
);
test(
'setBaseUrl after',
violentMonkeyContext(t => {
// Initialise the window.
getWindow();
// The window is already initialised, changing the baseUrl doesn't do anything
setBaseUrl('https://google.com/');
console.log(getWindow().location.href); // http:localhost:5000/
}),
);
Instead of actually saving files to the disk, GM_download
saves them to memory as a buffer and getDownloads
provides a way of gaining access to them.
type GetDownloads = () => Record<string, Buffer>;
test('title', violentMonkeyContextMacro(), t => {
console.log(getDownloads()); // {}
GM_download({
url: 'https://example.com/',
name: 'example-com.html',
onload: () => {
console.log(getDownloads());
/* {
"example-com.html": <Buffer 3c 21 ...>
} */
},
});
});
This is similar to getDownloads
but it only returns the corresponding buffer of the passed filename.
type GetDownload = (name: string) => Buffer | undefined;
test('title', violentMonkeyContextMacro(), t => {
console.log(getDownload('example-com.html')); // undefined
GM_download({
url: 'https://example.com/',
name: 'example-com.html',
onload: () => {
console.log(getDownload('example-com.html'));
// <Buffer 3c 21 ...>
},
});
});
MIT