From 52f1b595e8c340c651cf38ee03dd005e5db3f44f Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Tue, 25 Feb 2025 03:30:59 +0100 Subject: [PATCH] Add runtime tests --- .../desktop/@wailsio/runtime/package.json | 12 + .../@wailsio/runtime/src/cancellable.test.js | 430 ++++++++++++++++++ .../@wailsio/runtime/src/events.test.js | 193 +++++--- .../runtime/src/promises_aplus.test.js | 44 ++ .../desktop/@wailsio/runtime/vitest.config.ts | 8 + 5 files changed, 618 insertions(+), 69 deletions(-) create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/package.json b/v3/internal/runtime/desktop/@wailsio/runtime/package.json index 3f24339c3..b19175913 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/package.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/package.json @@ -28,6 +28,8 @@ "./dist/drag.js" ], "scripts": { + "check": "npx tsc --noEmit", + "test": "npx vitest run", "clean": "npx rimraf ./dist ./docs ./types ./tsconfig.tsbuildinfo", "generate:events": "task generate:events", "generate": "npm run generate:events", @@ -39,11 +41,21 @@ "prepack": "npm run build" }, "devDependencies": { + "happy-dom": "^17.1.1", + "promises-aplus-tests": "2.1.2", "rimraf": "^5.0.5", "typedoc": "^0.27.7", "typedoc-plugin-markdown": "^4.4.2", "typedoc-plugin-mdn-links": "^4.0.13", "typedoc-plugin-missing-exports": "^3.1.0", "typescript": "^5.7.3", + "vitest": "^3.0.6" + }, + "overrides": { + "promises-aplus-tests": { + "mocha": "^11.1.0", + "sinon": "^19.0.2", + "underscore": "^1.13.7" + } } } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js new file mode 100644 index 000000000..4a86a2abe --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js @@ -0,0 +1,430 @@ +import * as util from "node:util"; +import { describe, it, beforeEach, afterEach, assert, expect, vi } from "vitest"; +import { CancelError, CancellablePromise, CancelledRejectionError } from "./cancellable"; + +// TODO: In order of importance: +// TODO: test cancellation of subpromises the main promise resolves to. +// TODO: test cancellation of promise chains built by calling then() and friends: +// - all promises up the chain should be cancelled; +// - rejection handlers should be always executed with the CancelError of their parent promise in the chain; +// - promises returned from rejection handlers should be cancelled too; +// - if a rejection handler throws or returns a promise that ultimately rejects, +// it should be reported as an unhandled rejection, +// - unless it is a CancelError with the same reason given for cancelling the returned promise. +// TODO: test multiple calls to cancel() (second and later should have no effect). + +let expectedUnhandled = new Map(); + +process.on('unhandledRejection', function (error, promise) { + let reason = error; + if (reason instanceof CancelledRejectionError) { + promise = reason.promise; + reason = reason.cause; + } + + let reasons = expectedUnhandled.get(promise); + const callbacks = reasons?.get(reason); + if (callbacks) { + for (const cb of callbacks) { + try { + cb(reason, promise); + } catch (e) { + console.error("Exception in unhandled rejection callback.", e); + } + } + + reasons.delete(reason); + if (reasons.size === 0) { + expectedUnhandled.delete(promise); + } + return; + } + + console.log(util.format("Unhandled rejection.\nReason: %o\nPromise: %o", reason, promise)); + throw error; +}); + +function ignoreUnhandled(reason, promise) { + expectUnhandled(reason, promise, null); +} + +function expectUnhandled(reason, promise, cb) { + let reasons = expectedUnhandled.get(promise); + if (!reasons) { + reasons = new Map(); + expectedUnhandled.set(promise, reasons); + } + let callbacks = reasons.get(reason); + if (!callbacks) { + callbacks = []; + reasons.set(reason, callbacks); + } + if (cb) { + callbacks.push(cb); + } +} + +afterEach(() => { + vi.resetAllMocks(); + vi.restoreAllMocks(); +}); + +const dummyValue = { value: "value" }; +const dummyCause = { dummy: "dummy" }; +const dummyError = new Error("dummy"); +const oncancelled = vi.fn().mockName("oncancelled"); +const sentinel = vi.fn().mockName("sentinel"); +const unhandled = vi.fn().mockName("unhandled"); + +const resolutionPatterns = [ + ["forever", "pending", (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => test( + new cls(() => {}, cb) + )], + ["already", "fulfilled", (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + prw.resolve(value ?? dummyValue); + return test(prw.promise); + }], + ["immediately", "fulfilled", (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + prw.resolve(value ?? dummyValue); + return tp; + }], + ["eventually", "fulfilled", async (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + await new Promise((resolve) => { + setTimeout(() => { + prw.resolve(value ?? dummyValue); + resolve(); + }, 50); + }); + return tp; + }], + ["already", "rejected", (test, reason, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + prw.reject(reason ?? dummyError); + return test(prw.promise); + }], + ["immediately", "rejected", (test, reason, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + prw.reject(reason ?? dummyError); + return tp; + }], + ["eventually", "rejected", async (test, reason, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + await new Promise((resolve) => { + setTimeout(() => { + prw.reject(reason ?? dummyError); + resolve(); + }, 50); + }); + return tp; + }], +]; + +describe("CancellablePromise.cancel", ()=> { + it("should suppress its own unhandled cancellation error", async () => { + const p = new CancellablePromise(() => {}); + p.cancel(); + + process.on('unhandledRejection', sentinel); + await new Promise((resolve) => setTimeout(resolve, 100)); + process.off('unhandledRejection', sentinel); + + expect(sentinel).not.toHaveBeenCalled(); + }); + + it.for([ + ["rejections", dummyError], + ["cancellation errors", new CancelError("dummy", { cause: dummyCause })], + ])("should not suppress arbitrary unhandled %s", async ([kind, err]) => { + const p = new CancellablePromise(() => { throw err; }); + p.cancel(); + + await new Promise((resolve) => { + expectUnhandled(err, p, unhandled); + expectUnhandled(err, p, resolve); + }); + + expect(unhandled).toHaveBeenCalledExactlyOnceWith(err, p); + }); + + describe.for(resolutionPatterns)("when applied to %s %s promises", ([time, state, test]) => { + if (time === "already") { + it("should have no effect", () => test(async (promise) => { + promise.then(sentinel, sentinel); + + let reason; + try { + promise.cancel(); + await promise; + assert(state === "fulfilled", "Promise fulfilled unexpectedly"); + } catch (err) { + reason = err; + assert(state === "rejected", "Promise rejected unexpectedly"); + } + + expect(sentinel).toHaveBeenCalled(); + expect(oncancelled).not.toHaveBeenCalled(); + expect(reason).not.toBeInstanceOf(CancelError); + })); + } else { + if (state === "rejected") { + it("should report late rejections as unhandled", () => test(async (promise) => { + promise.cancel(); + + await new Promise((resolve) => { + expectUnhandled(dummyError, promise, unhandled); + expectUnhandled(dummyError, promise, resolve); + }); + + expect(unhandled).toHaveBeenCalledExactlyOnceWith(dummyError, promise); + })); + } + + it("should reject with a CancelError", () => test(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + promise.cancel(); + await promise; + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + })); + + it("should call the oncancelled callback synchronously", () => test(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + try { + promise.cancel(); + sentinel(); + await promise; + } catch {} + + expect(oncancelled).toHaveBeenCalledBefore(sentinel); + })); + + it("should propagate the given cause", () => test(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + promise.cancel(dummyCause); + await promise; + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + expect(reason).toHaveProperty('cause', dummyCause); + expect(oncancelled).toHaveBeenCalledWith(reason.cause); + })); + } + }); +}); + +const onabort = vi.fn().mockName("abort"); + +const abortPatterns = [ + ["never", "standalone", (test) => { + const signal = new AbortSignal(); + signal.addEventListener('abort', onabort, { capture: true }); + return test(signal); + }], + ["already", "standalone", (test) => { + const signal = AbortSignal.abort(dummyCause); + onabort(); + return test(signal); + }], + ["eventually", "standalone", (test) => { + const signal = AbortSignal.timeout(25); + signal.addEventListener('abort', onabort, { capture: true }); + return test(signal); + }], + ["never", "controller-bound", (test) => { + const signal = new AbortController().signal; + signal.addEventListener('abort', onabort, { capture: true }); + return test(signal); + }], + ["already", " controller-bound", (test) => { + const ctrl = new AbortController(); + ctrl.signal.addEventListener('abort', onabort, { capture: true }); + ctrl.abort(dummyCause); + return test(ctrl.signal); + }], + ["immediately", "controller-bound", (test) => { + const ctrl = new AbortController(); + ctrl.signal.addEventListener('abort', onabort, { capture: true }); + const tp = test(ctrl.signal); + ctrl.abort(dummyCause); + return tp; + }], + ["eventually", "controller-bound", (test) => { + const ctrl = new AbortController(); + ctrl.signal.addEventListener('abort', onabort, { capture: true }); + const tp = test(ctrl.signal); + setTimeout(() => ctrl.abort(dummyCause), 25); + return tp; + }] +]; + +describe("CancellablePromise.cancelOn", ()=> { + it("should return the target promise for chaining", () => { + const p = new CancellablePromise(() => {}); + expect(p.cancelOn(AbortSignal.abort())).toBe(p); + }); + + function tests(abortTime, mode, testSignal, resolveTime, state, testPromise) { + if (abortTime !== "never") { + it(`should call CancellablePromise.cancel ${abortTime === "already" ? "immediately" : "on abort"} with the abort reason as cause`, () => testSignal((signal) => testPromise(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + const cancelSpy = vi.spyOn(promise, 'cancel'); + + promise.catch(() => {}); + promise.cancelOn(signal); + + if (signal.aborted) { + sentinel(); + } else { + await new Promise((resolve) => { + signal.onabort = () => { + sentinel(); + resolve(); + }; + }); + } + + expect(cancelSpy).toHaveBeenCalledAfter(onabort); + expect(cancelSpy).toHaveBeenCalledBefore(sentinel); + expect(cancelSpy).toHaveBeenCalledExactlyOnceWith(signal.reason); + }))); + } + + if ( + resolveTime === "already" + || abortTime === "never" + || ( + ["immediately", "eventually"].includes(abortTime) + && ["already", "immediately"].includes(resolveTime) + ) + ) { + it("should have no effect", () => testSignal((signal) => testPromise(async (promise) => { + promise.then(sentinel, sentinel); + + let reason; + try { + if (resolveTime !== "forever") { + await promise.cancelOn(signal); + assert(state === "fulfilled", "Promise fulfilled unexpectedly"); + } else { + await Promise.race([promise, new Promise((resolve) => setTimeout(resolve, 100))]).then(sentinel); + } + } catch (err) { + reason = err; + assert(state === "rejected", "Promise rejected unexpectedly"); + } + + if (abortTime !== "never" && !signal.aborted) { + // Wait for the AbortSignal to have actually aborted. + await new Promise((resolve) => signal.onabort = resolve); + } + + expect(sentinel).toHaveBeenCalled(); + expect(oncancelled).not.toHaveBeenCalled(); + expect(reason).not.toBeInstanceOf(CancelError); + }))); + } else { + if (state === "rejected") { + it("should report late rejections as unhandled", () => testSignal((signal) => testPromise(async (promise) => { + promise.cancelOn(signal); + + await new Promise((resolve) => { + expectUnhandled(dummyError, promise, unhandled); + expectUnhandled(dummyError, promise, resolve); + }); + + expect(unhandled).toHaveBeenCalledExactlyOnceWith(dummyError, promise); + }))); + } + + it("should reject with a CancelError", () => testSignal((signal) => testPromise(async (promise)=> { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + await promise.cancelOn(signal); + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + }))); + + it(`should call the oncancelled callback ${abortTime === "already" ? "" : "a"}synchronously`, () => testSignal((signal) => testPromise(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + try { + promise.cancelOn(signal); + sentinel(); + await promise; + } catch {} + + expect(oncancelled).toHaveBeenCalledAfter(onabort); + if (abortTime === "already") { + expect(oncancelled).toHaveBeenCalledBefore(sentinel); + } else { + expect(oncancelled).toHaveBeenCalledAfter(sentinel); + } + }))); + + it("should propagate the abort reason as cause", () => testSignal((signal) => testPromise(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + await promise.cancelOn(signal); + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + expect(reason).toHaveProperty('cause', signal.reason); + expect(oncancelled).toHaveBeenCalledWith(signal.reason); + }))); + } + } + + describe.for(abortPatterns)("when called with %s aborted %s signals", ([abortTime, mode, testSignal]) => { + describe.for(resolutionPatterns)("when applied to %s %s promises", ([resolveTime, state, testPromise]) => { + tests(abortTime, mode, testSignal, resolveTime, state, testPromise); + }); + }); + + describe.for(resolutionPatterns)("when applied to %s %s promises", ([resolveTime, state, testPromise]) => { + describe.for(abortPatterns)("when called with %s aborted %s signals", ([abortTime, mode, testSignal]) => { + tests(abortTime, mode, testSignal, resolveTime, state, testPromise); + }); + }); +}); \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js index c46868cb0..c21804f53 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js @@ -1,18 +1,28 @@ - -import { On, Off, OffAll, OnMultiple, WailsEvent, dispatchWailsEvent, eventListeners, Once } from './events'; - +import { On, Off, OffAll, OnMultiple, WailsEvent, Once } from './events'; +import { eventListeners } from "./listener"; import { expect, describe, it, vi, afterEach, beforeEach } from 'vitest'; +const dispatchWailsEvent = window._wails.dispatchWailsEvent; + afterEach(() => { OffAll(); vi.resetAllMocks(); }); -describe('OnMultiple', () => { - let testEvent = new WailsEvent('a', {}); +describe("OnMultiple", () => { + const testEvent = { name: 'a', data: ["hello", "events"] }; + const cb = vi.fn((ev) => { + expect(ev).toBeInstanceOf(WailsEvent); + expect(ev).toMatchObject(testEvent); + }); - it('should stop after a specified number of times', () => { - const cb = vi.fn(); + it("should dispatch a properly initialised WailsEvent", () => { + OnMultiple('a', cb, 5); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalled(); + }); + + it("should stop after the specified number of times", () => { OnMultiple('a', cb, 5); dispatchWailsEvent(testEvent); dispatchWailsEvent(testEvent); @@ -20,77 +30,122 @@ describe('OnMultiple', () => { dispatchWailsEvent(testEvent); dispatchWailsEvent(testEvent); dispatchWailsEvent(testEvent); - expect(cb).toBeCalledTimes(5); + expect(cb).toHaveBeenCalledTimes(5); }); - it('should return a cancel fn', () => { - const cb = vi.fn() - const cancel = OnMultiple('a', cb, 5) - dispatchWailsEvent(testEvent) - dispatchWailsEvent(testEvent) - cancel() - dispatchWailsEvent(testEvent) - dispatchWailsEvent(testEvent) - expect(cb).toBeCalledTimes(2) - }) -}) - -describe('On', () => { - it('should create a listener with a count of -1', () => { - On('a', () => {}) - expect(eventListeners.get("a")[0].maxCallbacks).toBe(-1) - }) - - it('should return a cancel fn', () => { - const cancel = On('a', () => {}) + it("should return a cancel fn", () => { + const cancel = OnMultiple('a', cb, 5); + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); cancel(); - }) -}) + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); + expect(cb).toBeCalledTimes(2); + }); +}); -describe('Once', () => { - it('should create a listener with a count of 1', () => { - Once('a', () => {}) - expect(eventListeners.get("a")[0].maxCallbacks).toBe(1) - }) +describe("On", () => { + let testEvent = { name: 'a', data: ["hello", "events"], sender: "window" }; + const cb = vi.fn((ev) => { + expect(ev).toBeInstanceOf(WailsEvent); + expect(ev).toMatchObject(testEvent); + }); - it('should return a cancel fn', () => { - const cancel = EventsOn('a', () => {}) + it("should dispatch a properly initialised WailsEvent", () => { + On('a', cb); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalled(); + }); + + it("should never stop", () => { + On('a', cb); + expect(eventListeners.get('a')[0].maxCallbacks).toBe(-1); + dispatchWailsEvent(testEvent); + expect(eventListeners.get('a')[0].maxCallbacks).toBe(-1); + }); + + it("should return a cancel fn", () => { + const cancel = On('a', cb) + dispatchWailsEvent(testEvent); cancel(); - }) + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalledTimes(1); + }); +}); + +describe("Once", () => { + const testEvent = { name: 'a', data: ["hello", "events"] }; + const cb = vi.fn((ev) => { + expect(ev).toBeInstanceOf(WailsEvent); + expect(ev).toMatchObject(testEvent); + }); + + it("should dispatch a properly initialised WailsEvent", () => { + Once('a', cb); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalled(); + }); + + it("should stop after one time", () => { + Once('a', cb) + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it("should return a cancel fn", () => { + const cancel = Once('a', cb) + cancel(); + dispatchWailsEvent(testEvent); + expect(cb).not.toHaveBeenCalled(); + }); }) -describe('Off', () => { +describe("Off", () => { + const cba = vi.fn(), cbb = vi.fn(), cbc = vi.fn(); + beforeEach(() => { - On('a', () => {}) - On('a', () => {}) - On('a', () => {}) - On('b', () => {}) - On('c', () => {}) - }) + On('a', cba); + On('a', cba); + On('a', cba); + On('b', cbb); + On('c', cbc); + On('c', cbc); + }); - it('should cancel all event listeners for a single type', () => { - Off('a') - expect(eventListeners.get('a')).toBeUndefined() - expect(eventListeners.get('b')).not.toBeUndefined() - expect(eventListeners.get('c')).not.toBeUndefined() - }) + it("should cancel all event listeners for a single type", () => { + Off('a'); + dispatchWailsEvent({ name: 'a' }); + dispatchWailsEvent({ name: 'b' }); + dispatchWailsEvent({ name: 'c' }); + expect(cba).not.toHaveBeenCalled(); + expect(cbb).toHaveBeenCalledTimes(1); + expect(cbc).toHaveBeenCalledTimes(2); + }); - it('should cancel all event listeners for multiple types', () => { - Off('a', 'b') - expect(eventListeners.get('a')).toBeUndefined() - expect(eventListeners.get('b')).toBeUndefined() - expect(eventListeners.get('c')).not.toBeUndefined() - }) -}) + it("should cancel all event listeners for multiple types", () => { + Off('a', 'c') + dispatchWailsEvent({ name: 'a' }); + dispatchWailsEvent({ name: 'b' }); + dispatchWailsEvent({ name: 'c' }); + expect(cba).not.toHaveBeenCalled(); + expect(cbb).toHaveBeenCalledTimes(1); + expect(cbc).not.toHaveBeenCalled(); + }); +}); -describe('OffAll', () => { - it('should cancel all event listeners', () => { - On('a', () => {}) - On('a', () => {}) - On('a', () => {}) - On('b', () => {}) - On('c', () => {}) - OffAll() - expect(eventListeners.size).toBe(0) - }) -}) +describe("OffAll", () => { + it("should cancel all event listeners", () => { + const cba = vi.fn(), cbb = vi.fn(), cbc = vi.fn(); + On('a', cba); + On('a', cba); + On('a', cba); + On('b', cbb); + On('c', cbc); + On('c', cbc); + expect(cba).not.toHaveBeenCalled(); + expect(cbb).not.toHaveBeenCalled(); + expect(cbc).not.toHaveBeenCalled(); + }); +}); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js new file mode 100644 index 000000000..e4806b6e0 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js @@ -0,0 +1,44 @@ +import * as util from "util"; +import * as V from "vitest"; +import { CancellablePromise } from "./cancellable"; + +// The Promises/A+ suite handles some errors late. +process.on('rejectionHandled', function () {}); + +// The Promises/A+ suite leaves some errors unhandled. +process.on('unhandledRejection', function (reason, promise) { + if (promise instanceof CancellablePromise && reason != null && typeof reason === 'object') { + for (const key of ['dummy', 'other', 'sentinel']) { + if (reason[key] === key) { + return; + } + } + } + throw new Error(`Unhandled rejection at: ${util.inspect(promise)}; reason: ${util.inspect(reason)}`, { cause: reason }); +}); + +// Emulate a minimal version of the mocha BDD API using vitest primitives. +global.context = global.describe = V.describe; +global.specify = global.it = function it(desc, fn) { + let viTestFn = fn; + if (fn && fn.length) { + viTestFn = () => new Promise((done) => fn(done)); + } + V.it(desc, viTestFn); +} +global.before = function(desc, fn) { V.beforeAll(typeof desc === 'function' ? desc : fn) }; +global.after = function(desc, fn) { V.afterAll(typeof desc === 'function' ? desc : fn) }; +global.beforeEach = function(desc, fn) { V.beforeEach(typeof desc === 'function' ? desc : fn) }; +global.afterEach = function(desc, fn) { V.afterEach(typeof desc === 'function' ? desc : fn) }; + +require('promises-aplus-tests').mocha({ + resolved(value) { + return CancellablePromise.resolve(value); + }, + rejected(reason) { + return CancellablePromise.reject(reason); + }, + deferred() { + return CancellablePromise.withResolvers(); + } +}); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts b/v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts new file mode 100644 index 000000000..efb60170a --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: 'happy-dom', + testTimeout: 200 + }, +});