mirror of
https://github.com/GetOpenScript/OpenScript.git
synced 2026-09-18 09:45:43 +00:00
Complete OpenScript runtime roadmap
This commit is contained in:
@@ -1,43 +1,42 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseMeta, normalizeMatch, getBoilerplate } from '../src/utils/parser.js';
|
||||
import { wrapScriptCode } from '../src/utils/userScripts.js';
|
||||
import {
|
||||
buildScriptCode, refreshRequireCaches, syncUserScripts, wrapScriptCode,
|
||||
} from '../src/utils/userScripts.js';
|
||||
|
||||
test('parseMeta extracts standard Tampermonkey metadata', () => {
|
||||
const sample = `
|
||||
test('parseMeta extracts only OpenScript metadata', () => {
|
||||
const meta = parseMeta(`
|
||||
// ==UserScript==
|
||||
// @name Test Script
|
||||
// @version 2.1.0
|
||||
// @description Sample description
|
||||
// @author Alice
|
||||
// @match https://gemini.google.com/*
|
||||
// @namespace legacy
|
||||
// @match https://example.com/*
|
||||
// @include https://ignored.example/*
|
||||
// @run-at document-start
|
||||
// @grant none
|
||||
// @require https://cdn.example/library.js
|
||||
// ==/UserScript==
|
||||
`);
|
||||
|
||||
console.log('hello');
|
||||
`;
|
||||
|
||||
const meta = parseMeta(sample);
|
||||
assert.equal(meta.name, 'Test Script');
|
||||
assert.equal(meta.version, '2.1.0');
|
||||
assert.equal(meta.description, 'Sample description');
|
||||
assert.equal(meta.author, 'Alice');
|
||||
assert.deepEqual(meta.matches, ['https://gemini.google.com/*', 'https://example.com/*']);
|
||||
assert.equal(meta.runAt, 'document_start');
|
||||
assert.deepEqual(meta, {
|
||||
name: 'Test Script',
|
||||
description: 'Sample description',
|
||||
matches: ['https://example.com/*'],
|
||||
runAt: 'document_start',
|
||||
requires: ['https://cdn.example/library.js'],
|
||||
});
|
||||
});
|
||||
|
||||
test('parseMeta falls back to defaults when fields are missing', () => {
|
||||
const sample = `
|
||||
// ==UserScript==
|
||||
// ==/UserScript==
|
||||
`;
|
||||
const meta = parseMeta(sample);
|
||||
const meta = parseMeta('// ==UserScript==\n// ==/UserScript==');
|
||||
assert.equal(meta.name, 'Untitled Script');
|
||||
assert.equal(meta.version, '1.0.0');
|
||||
assert.equal(meta.description, '');
|
||||
assert.deepEqual(meta.matches, ['*://*/*']);
|
||||
assert.equal(meta.runAt, 'document_idle');
|
||||
assert.deepEqual(meta.requires, []);
|
||||
});
|
||||
|
||||
test('normalizeMatch formats URL patterns for Chrome userScripts API', () => {
|
||||
@@ -46,21 +45,98 @@ test('normalizeMatch formats URL patterns for Chrome userScripts API', () => {
|
||||
assert.equal(normalizeMatch('*://*/*'), '*://*/*');
|
||||
});
|
||||
|
||||
test('wrapScriptCode injects OpenScript.env and GM_getValue polyfill', () => {
|
||||
const code = 'console.log(env.API_KEY, GM_getValue("API_KEY"));';
|
||||
const wrapped = wrapScriptCode(code, { API_KEY: 'secret123' });
|
||||
test('wrapScriptCode provides async OpenScript APIs without GM polyfills', () => {
|
||||
const code = 'const cached = await OpenScript.storage.get("cache");\nif (!cached) return;';
|
||||
const wrapped = wrapScriptCode(code, { API_KEY: 'secret123' }, 'storage-token');
|
||||
|
||||
assert.ok(wrapped.includes('OpenScript'));
|
||||
assert.match(wrapped, /async function\(OpenScript, env\)/);
|
||||
assert.ok(wrapped.includes('"API_KEY":"secret123"'));
|
||||
assert.ok(wrapped.includes('globalThis.OpenScript'));
|
||||
assert.ok(wrapped.includes('GM_getValue'));
|
||||
assert.ok(wrapped.includes("call('list')"));
|
||||
assert.ok(wrapped.includes('storage-token'));
|
||||
assert.ok(wrapped.includes(code));
|
||||
assert.ok(!wrapped.includes('GM_getValue'));
|
||||
assert.doesNotThrow(() => new Function(wrapped));
|
||||
});
|
||||
|
||||
test('getBoilerplate produces valid Tampermonkey template without namespace', () => {
|
||||
test('buildScriptCode prepends cached requirements in declared order', () => {
|
||||
const script = {
|
||||
code: 'useLibraries();',
|
||||
requires: ['https://cdn.example/a.js', 'https://cdn.example/b.js'],
|
||||
requireCache: {
|
||||
'https://cdn.example/a.js': 'const a = 1;',
|
||||
'https://cdn.example/b.js': 'const b = 2;',
|
||||
},
|
||||
};
|
||||
const bundle = buildScriptCode(script);
|
||||
assert.ok(bundle.indexOf('const a = 1;') < bundle.indexOf('const b = 2;'));
|
||||
assert.ok(bundle.indexOf('const b = 2;') < bundle.indexOf('useLibraries();'));
|
||||
});
|
||||
|
||||
test('refreshRequireCaches downloads once and falls back to cached code', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let calls = 0;
|
||||
globalThis.fetch = async url => {
|
||||
calls++;
|
||||
if (url.endsWith('bad.js')) throw new Error('offline');
|
||||
return { ok: true, text: async () => 'globalThis.library = true;' };
|
||||
};
|
||||
|
||||
try {
|
||||
const good = 'https://cdn.example/good.js';
|
||||
const bad = 'https://cdn.example/bad.js';
|
||||
const result = await refreshRequireCaches([
|
||||
{ name: 'One', requires: [good, bad], requireCache: { [bad]: 'cached();' } },
|
||||
{ name: 'Two', requires: [good] },
|
||||
]);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(result.scripts[0].requireCache[good], 'globalThis.library = true;');
|
||||
assert.equal(result.scripts[0].requireCache[bad], 'cached();');
|
||||
assert.equal(result.scripts[1].requireCache[good], 'globalThis.library = true;');
|
||||
assert.equal(result.warnings.length, 1);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('syncUserScripts migrates tokens and registers isolated script worlds', async () => {
|
||||
const originalChrome = globalThis.chrome;
|
||||
const data = {
|
||||
scripts: [{ id: 'script_1', name: 'One', code: 'return;', enabled: true, matches: ['*://*/*'] }],
|
||||
secrets: { TOKEN: 'secret' },
|
||||
};
|
||||
let registered;
|
||||
const configured = [];
|
||||
globalThis.chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
get: async key => ({ [key]: data[key] }),
|
||||
set: async values => Object.assign(data, values),
|
||||
},
|
||||
sync: { get: async key => ({ [key]: data[key] }) },
|
||||
},
|
||||
userScripts: {
|
||||
getScripts: async () => [],
|
||||
configureWorld: async value => configured.push(value),
|
||||
register: async value => { registered = value; },
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await syncUserScripts();
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(data.scripts[0].storageToken);
|
||||
assert.deepEqual(configured, [{ worldId: 'script_1', messaging: true }]);
|
||||
assert.equal(registered[0].worldId, 'script_1');
|
||||
assert.ok(registered[0].js[0].code.includes('"TOKEN":"secret"'));
|
||||
} finally {
|
||||
globalThis.chrome = originalChrome;
|
||||
}
|
||||
});
|
||||
|
||||
test('getBoilerplate is wrapper-free and minimalist', () => {
|
||||
const template = getBoilerplate('My Script');
|
||||
assert.ok(template.includes('// @name My Script'));
|
||||
assert.ok(!template.includes('@namespace'));
|
||||
const parsed = parseMeta(template);
|
||||
assert.equal(parsed.name, 'My Script');
|
||||
assert.ok(template.includes("console.log('Running on', location.hostname);"));
|
||||
for (const legacy of ['@namespace', '@grant', '@version', '@author', '(function'])
|
||||
assert.ok(!template.includes(legacy));
|
||||
});
|
||||
|
||||
52
tests/storage.test.js
Normal file
52
tests/storage.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
garbageCollectScriptStorage, runScriptStorageOperation,
|
||||
} from '../src/utils/storage.js';
|
||||
|
||||
const mockChromeStorage = initial => {
|
||||
const data = structuredClone(initial);
|
||||
globalThis.chrome = { storage: { local: {
|
||||
get: async key => key === null ? { ...data } : Object.hasOwn(data, key) ? { [key]: data[key] } : {},
|
||||
set: async values => Object.assign(data, values),
|
||||
remove: async keys => [keys].flat().forEach(key => delete data[key]),
|
||||
} } };
|
||||
return data;
|
||||
};
|
||||
|
||||
test('script storage isolates, lists, and deletes values by script ID', async () => {
|
||||
const data = mockChromeStorage({
|
||||
scripts: [
|
||||
{ id: 'script_a', storageToken: 'token-a' },
|
||||
{ id: 'script_b', storageToken: 'token-b' },
|
||||
],
|
||||
storage_script_b_shared: 'private-b',
|
||||
});
|
||||
|
||||
await runScriptStorageOperation('token-a', 'set', 'shared', { count: 1 });
|
||||
assert.deepEqual(data.storage_script_a_shared, { count: 1 });
|
||||
assert.deepEqual(await runScriptStorageOperation('token-a', 'get', 'shared'), {
|
||||
found: true, value: { count: 1 },
|
||||
});
|
||||
assert.deepEqual(await runScriptStorageOperation('token-a', 'get', 'missing'), {
|
||||
found: false, value: undefined,
|
||||
});
|
||||
assert.deepEqual(await runScriptStorageOperation('token-a', 'list'), { keys: ['shared'] });
|
||||
await runScriptStorageOperation('token-a', 'delete', 'shared');
|
||||
assert.equal(data.storage_script_a_shared, undefined);
|
||||
await assert.rejects(() => runScriptStorageOperation('token-bad', 'list'), /Invalid script storage token/);
|
||||
});
|
||||
|
||||
test('script storage garbage collection removes only orphaned script keys', async () => {
|
||||
const data = mockChromeStorage({
|
||||
scripts: [{ id: 'script_a' }],
|
||||
storage_script_a_keep: 1,
|
||||
storage_script_deleted_remove: 2,
|
||||
unrelated: 3,
|
||||
});
|
||||
const removed = await garbageCollectScriptStorage(['script_a']);
|
||||
assert.deepEqual(removed, ['storage_script_deleted_remove']);
|
||||
assert.equal(data.storage_script_a_keep, 1);
|
||||
assert.equal(data.storage_script_deleted_remove, undefined);
|
||||
assert.equal(data.unrelated, 3);
|
||||
});
|
||||
Reference in New Issue
Block a user