diff --git a/build/test-utils/entitydialogutils.ts b/build/test-utils/entitydialogutils.ts new file mode 100644 index 0000000..3aebf02 --- /dev/null +++ b/build/test-utils/entitydialogutils.ts @@ -0,0 +1,41 @@ +import { EntityDialog } from "@serenity-is/corelib"; +import { waitForAjaxRequests } from "./waitutils"; + +export class EntityDialogTestWrapper> { + constructor(public readonly actual: TDialog) { + } + + clickDeleteButton(): Promise { + var button = this.actual.element.findFirst(".delete-button"); + if (!button.length) + throw "Delete button not found in the dialog!"; + if (button.hasClass("disabled")) + throw "Delete button is disabled!"; + const spy = jest.spyOn(window, "confirm").mockReturnValue(true); + button.click(); + spy.mockRestore(); + return waitForAjaxRequests(); + } + + clickSaveButton(): Promise { + var button = this.actual.element.findFirst(".save-and-close-button"); + if (!button.length) + throw "Save button not found in the dialog!"; + if (button.hasClass("disabled")) + throw "Save button is disabled!"; + button.click(); + return waitForAjaxRequests(); + } + + + setTextInput(name: string, value: any) { + var input = this.actual["byId"](name); + if (!input.length) + throw `Input not found in the dialog: ${name}!`; + input.val(value).trigger("change"); + } + + waitForAjaxRequests(timeout: number = 10000): Promise { + return waitForAjaxRequests(timeout); + } +} \ No newline at end of file diff --git a/build/test-utils/index.ts b/build/test-utils/index.ts index b59531c..e6154fc 100644 --- a/build/test-utils/index.ts +++ b/build/test-utils/index.ts @@ -1 +1,3 @@ -export * from "./mocks"; \ No newline at end of file +export * from "./entitydialogutils"; +export * from "./mocks"; +export * from "./waitutils"; \ No newline at end of file diff --git a/build/test-utils/jsdom-global.js b/build/test-utils/jsdom-global.js index e091301..09dab16 100644 --- a/build/test-utils/jsdom-global.js +++ b/build/test-utils/jsdom-global.js @@ -5,6 +5,7 @@ const JSDOMEnvironment = pkg.default || pkg.JSDOMEnvironment || pkg; export default class JSDOMEnvironmentGlobal extends JSDOMEnvironment { async setup() { await super.setup(); + addCSSEscape(this.global); this.global.jsdom = this.dom; } @@ -12,4 +13,93 @@ export default class JSDOMEnvironmentGlobal extends JSDOMEnvironment { this.global.jsdom = undefined; await super.teardown(); } -} \ No newline at end of file +} + + + +function addCSSEscape(window) { + if (typeof window !== "undefined" && (!window.CSS || !window.CSS.escape)) { + // https://drafts.csswg.org/cssom/#serialize-an-identifier + var cssEscape = function (value) { + if (arguments.length == 0) { + throw new TypeError('`CSS.escape` requires an argument.'); + } + var string = String(value); + var length = string.length; + var index = -1; + var codeUnit; + var result = ''; + var firstCodeUnit = string.charCodeAt(0); + + if ( + // If the character is the first character and is a `-` (U+002D), and + // there is no second character, […] + length == 1 && + firstCodeUnit == 0x002D + ) { + return '\\' + string; + } + + while (++index < length) { + codeUnit = string.charCodeAt(index); + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER + // (U+FFFD). + if (codeUnit == 0x0000) { + result += '\uFFFD'; + continue; + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index == 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit == 0x002D + ) + ) { + // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point + result += '\\' + codeUnit.toString(16) + ' '; + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002D || + codeUnit == 0x005F || + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // https://drafts.csswg.org/cssom/#escape-a-character + result += '\\' + string.charAt(index); + } + return result; + }; + + if (!window.CSS) { + window.CSS = {}; + } + + window.CSS.escape = cssEscape; + } +} diff --git a/build/test-utils/mocks.ts b/build/test-utils/mocks.ts index 5677449..67a10e6 100644 --- a/build/test-utils/mocks.ts +++ b/build/test-utils/mocks.ts @@ -53,8 +53,8 @@ export function mockFetch(map?: { [urlOrService: string]: ((info: MockFetchInfo) fetchSpy = (window as any).fetch = jest.fn(async (url: string, init: RequestInit) => { var callback = fetchMap[url] ?? fetchMap["*"]; if (!callback) { - console.error(`Fetch is not configured on the mock fetch implementation: (${url})!`); - throw `Fetch is not configured on the mock fetch implementation: (${url})!`; + console.error(`Mock fetch is not configured for URL: (${url})!`); + throw `Mock fetch is not configured for URL: (${url})!`; } var requestData = typeof init.body == "string" ? JSON.parse(init.body) : null; diff --git a/build/test-utils/tsconfig.json b/build/test-utils/tsconfig.json index 8eb96dc..f6cf40a 100644 --- a/build/test-utils/tsconfig.json +++ b/build/test-utils/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { "noEmit": true, + "lib": [ + "es2015", + "dom" + ], "outDir": "./out", "typeRoots": [ "../../../Serenity/node_modules/@types", diff --git a/build/test-utils/waitutils.ts b/build/test-utils/waitutils.ts new file mode 100644 index 0000000..0d20f50 --- /dev/null +++ b/build/test-utils/waitutils.ts @@ -0,0 +1,22 @@ +import { getActiveRequests } from "@serenity-is/corelib"; + +export function waitForAjaxRequests(timeout: number = 10000): Promise { + return waitUntil(() => typeof globalThis.jQuery !== 'undefined' ? globalThis.jQuery.active == 0 : (getActiveRequests() <= 0), timeout); +} + +export function waitUntil(predicate: () => boolean, timeout: number = 10000, checkInterval: number = 10): Promise { + var start = Date.now(); + return new Promise((resolve, reject) => { + let interval = setInterval(() => { + if (!predicate()) { + if (Date.now() - start > timeout) { + clearInterval(interval); + reject("Timed out while waiting for condition to be true!"); + } + return; + } + clearInterval(interval); + resolve(void 0); + }, checkInterval) + }) +} \ No newline at end of file