您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
xUnit style testing.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/478188/1269821/NH_xunit.js
// ==UserScript== // ==UserLibrary== // @name NH_xunit // @description xUnit style testing. // @version 0 // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html // @homepageURL https://github.com/nexushoratio/userscripts // @supportURL https://github.com/nexushoratio/userscripts/issues // @match https://www.example.com/* // ==/UserLibrary== // ==/UserScript== window.NexusHoratio ??= {}; window.NexusHoratio.xunit = (function base() { 'use strict'; /** @type {number} - Bumped per release. */ const version = 0; /** * @type {object} - For testing support (to be replaced with `TestCase`). */ const testing = { enabled: false, funcs: [], testCases: [], }; /** * An xUnit style test framework. * * TODO(#172): WIP. * * Many expected methods exist, such as setUp, setUpClass, addCleanup, * addClassCleanup, etc. No tearDown methods, however; use addCleanup. * * Generally, register the class with a test runner that will do them all in * turn. One approach is to use a static initializer block at the top of * the class. */ class TestCase { /** Instantiate a TestCase. */ constructor() { if (new.target === TestCase) { throw new TypeError('Abstract class; do not instantiate directly.'); } } /** Called once before any instances are created. */ static setUpClass() { // Empty. } /** * Register a function with arguments to run after all tests in the class * have ran. * @param {function} func - Function to call. * @param {...*} rest - Arbitrary arguments to func. */ static addClassCleanup(func, ...rest) { this.classCleanups.push([func, rest]); } /** Execute all functions registered with addClassCleanup. */ static doClassCleanups() { while (this.classCleanups.length) { const [func, rest] = this.classCleanups.pop(); // TODO: decide how resistant these should be to exceptions func(...rest); } } static Skip = class extends Error {} /** Called once before each test method. */ setUp() { // eslint-disable-line class-methods-use-this // Empty. } /** * Register a function with arguments to run after a test. * @param {function} func - Function to call. * @param {...*} rest - Arbitrary arguments to func. */ addCleanup(func, ...rest) { this.#cleanups.push([func, rest]); } // TODO: Add assertions as needed. static classCleanups = []; #cleanups = []; } /** Test TestCase. */ class TestTestCase extends TestCase { } testing.testCases.push(TestTestCase); /** Accumulated results from running a TestCase. */ class TestResult { /** * Record a successful execution. * @param {string} name - Name of the TestCase.testMethod. */ addSuccess(name) { this.successes.push(name); } /** * Record an unexpected exception from a execution. * @param {string} name - Name of the TestCase.testMethod. * @param {Error} exception - Exception caught. */ addError(name, exception) { this.errors.push({ name: name, error: exception.name, message: exception.message, }); } /** * Record a test failure. * @param {string} name - Name of the TestCase.testMethod. * @param {string} message - Message from the test or framework. */ addFailure(name, message) { this.failures.push({ name: name, message: message, }); } /** * Record a test skipped. * @param {string} name - Name of the TestCase.testMethod. * @param {string} message - Reason the test was skipped. */ addSkip(name, message) { this.skipped.push({ name: name, message: message, }); } /** Successes. */ successes = []; /** Unexpected exceptions. */ errors = []; /** Explicit test failures (failed asserts). */ failures = []; /** Skipped tests. */ skipped = []; } /** Assembles and drives execution of {@link TestCase}s. */ class TestRunner { /** @param {function(): TestCase} tests - TestCases to execute. */ constructor(tests) { const badKlasses = []; for (const klass of tests) { if (!(klass.prototype instanceof TestCase)) { badKlasses.push(klass); } } if (badKlasses.length) { const msg = `Bad class count: ${badKlasses.length}`; for (const klass of badKlasses) { // eslint-disable-next-line no-console console.error('Not a TestCase:', klass); } throw new Error(`Bad classes: ${msg}`); } this.#tests = tests; } /** * Run each test in turn. * @returns {TestResult} - Collected results. */ runTests() { const result = new TestResult(); let lastKlass = null; for (const klass of this.#tests) { if (klass !== lastKlass) { this.#doClassCleanUps(lastKlass, result); this.#doSetUpClass(klass, result); } lastKlass = klass; // TODO: introspection to find test methods and execute them. } this.#doClassCleanUps(lastKlass, result); return result; } #tests /** * @param {TestCase} klass - TestCase to process. * @param {TestResult} result - Result to use if any errors. */ #doClassCleanUps = (klass, result) => { if (klass) { const name = `${klass.name}.doClassCleanups`; try { klass.doClassCleanups(); } catch (e) { result.addError(name, e); } } } /** * @param {TestCase} klass - TestCase to process. * @param {TestResult} result - Result to use if any errors. */ #doSetUpClass = (klass, result) => { const name = `${klass.name}.setUpClass`; try { klass.setUpClass(); } catch (e) { if (e instanceof TestCase.Skip) { result.addSkip(name, e.message); } else { result.addError(name, e); } } } } /** TestRunner TestCase. */ class RunnerTestCase extends TestCase { } testing.testCases.push(RunnerTestCase); /** * Run registered TestCases. * @returns {TestResult} - Accumulated results of these tests. */ function runTests() { const runner = new TestRunner(testing.testCases); return runner.runTests(); } return { version: version, testing: testing, TestCase: TestCase, runTests: runTests, }; }());