@@ -51,6 +51,7 @@ define(function (require, exports, module) {
5151 Commands = brackets . getModule ( "command/Commands" ) ,
5252 PreferencesManager = brackets . getModule ( "preferences/PreferencesManager" ) ,
5353 Editor = brackets . getModule ( "editor/Editor" ) ,
54+ Dialogs = brackets . getModule ( "widgets/Dialogs" ) ,
5455 _ = brackets . getModule ( "thirdparty/lodash" ) ,
5556 ProjectManager = brackets . getModule ( "project/ProjectManager" ) ;
5657
@@ -312,6 +313,12 @@ define(function (require, exports, module) {
312313 }
313314 }
314315
316+ function validateNotEqual ( obj1 , obj2 ) {
317+ if ( _ . isEqual ( obj1 , obj2 ) ) {
318+ throw new Error ( `validateEqual: expected ${ JSON . stringify ( obj1 ) } to NOT equal ${ JSON . stringify ( obj2 ) } ` ) ;
319+ }
320+ }
321+
315322 /**
316323 * validates if the given mark type is present in the specified selections
317324 * @param {string } markType
@@ -343,8 +350,8 @@ define(function (require, exports, module) {
343350 return jsPromise ( CommandManager . execute ( Commands . FILE_CLOSE_ALL , { _forceClose : true } ) ) ;
344351 }
345352
346- function execCommand ( commandID , args ) {
347- return jsPromise ( CommandManager . execute ( commandID , args ) ) ;
353+ function execCommand ( commandID , arg ) {
354+ return jsPromise ( CommandManager . execute ( commandID , arg ) ) ;
348355 }
349356
350357 function undo ( ) {
@@ -383,9 +390,236 @@ define(function (require, exports, module) {
383390 }
384391 } ;
385392
393+ /**
394+ * Waits for a polling function to succeed or until a timeout is reached.
395+ * The polling function is periodically invoked to check for success, and
396+ * the function rejects with a timeout message if the timeout duration elapses.
397+ *
398+ * @param {function } pollFn - A function that returns `true` or a promise resolving to `true`/`false`
399+ * to indicate success and stop waiting.
400+ * The function will be called repeatedly until it succeeds or times out.
401+ * @param {string|function } _timeoutMessageOrMessageFn - A helpful string message or an async function
402+ * that returns a string message to reject with in case of timeout.
403+ * Example:
404+ * - String: "Condition not met within the allowed time."
405+ * - Function: `async () => "Timeout while waiting for the process to complete."`
406+ * @param {number } [timeoutms=2000] - The maximum time to wait in milliseconds before timing out. Defaults to 2 seconds.
407+ * @param {number } [pollInterval=10] - The interval in milliseconds at which `pollFn` is invoked. Defaults to 10ms.
408+ * @returns {Promise<void> } A promise that resolves when `pollFn` succeeds or rejects with a timeout message.
409+ *
410+ * @throws {Error } If `timeoutms` or `pollInterval` is not a number.
411+ *
412+ * @example
413+ * // Example 1: Using a string as the timeout message
414+ * awaitsFor(
415+ * () => document.getElementById("element") !== null,
416+ * "Element did not appear within the allowed time.",
417+ * 5000,
418+ * 100
419+ * ).then(() => {
420+ * console.log("Element appeared!");
421+ * }).catch(err => {
422+ * console.error(err.message);
423+ * });
424+ *
425+ * @example
426+ * // Example 2: Using a function as the timeout message
427+ * awaitsFor(
428+ * () => document.getElementById("element") !== null,
429+ * async () => {
430+ * const el = document.getElementById("element");
431+ * return `expected ${el} to be null`;
432+ * },
433+ * 10000,
434+ * 500
435+ * ).then(() => {
436+ * console.log("Element appeared!");
437+ * }).catch(err => {
438+ * console.error(err.message);
439+ * });
440+ */
441+ function awaitsFor ( pollFn , _timeoutMessageOrMessageFn , timeoutms = 2000 , pollInterval = 10 ) {
442+ if ( typeof _timeoutMessageOrMessageFn === "number" ) {
443+ timeoutms = _timeoutMessageOrMessageFn ;
444+ pollInterval = timeoutms ;
445+ }
446+ if ( ! ( typeof timeoutms === "number" && typeof pollInterval === "number" ) ) {
447+ throw new Error ( "awaitsFor: invalid parameters when awaiting for " + _timeoutMessageOrMessageFn ) ;
448+ }
449+
450+ async function _getExpectMessage ( _timeoutMessageOrMessageFn ) {
451+ try {
452+ if ( typeof _timeoutMessageOrMessageFn === "function" ) {
453+ _timeoutMessageOrMessageFn = _timeoutMessageOrMessageFn ( ) ;
454+ if ( _timeoutMessageOrMessageFn instanceof Promise ) {
455+ _timeoutMessageOrMessageFn = await _timeoutMessageOrMessageFn ;
456+ }
457+ }
458+ } catch ( e ) {
459+ _timeoutMessageOrMessageFn = "Error executing expected message function:" + e . stack ;
460+ }
461+ return _timeoutMessageOrMessageFn ;
462+ }
463+
464+ function _timeoutPromise ( promise , ms ) {
465+ const timeout = new Promise ( ( _ , reject ) => {
466+ setTimeout ( async ( ) => {
467+ _timeoutMessageOrMessageFn = await _getExpectMessage ( _timeoutMessageOrMessageFn ) ;
468+ reject ( new Error ( _timeoutMessageOrMessageFn || `Promise timed out after ${ ms } ms` ) ) ;
469+ } , ms ) ;
470+ } ) ;
471+
472+ return Promise . race ( [ promise , timeout ] ) ;
473+ }
474+
475+ return new Promise ( ( resolve , reject ) => {
476+ let startTime = Date . now ( ) ,
477+ lapsedTime ;
478+ async function pollingFn ( ) {
479+ try {
480+ let result = pollFn ( ) ;
481+
482+ // If pollFn returns a promise, await it
483+ if ( Object . prototype . toString . call ( result ) === "[object Promise]" ) {
484+ // we cant simply check for result instanceof Promise as the Promise may be returned from
485+ // an iframe and iframe has a different instance of Promise than this js context.
486+ result = await _timeoutPromise ( result , timeoutms ) ;
487+ }
488+
489+ if ( result ) {
490+ resolve ( ) ;
491+ return ;
492+ }
493+ lapsedTime = Date . now ( ) - startTime ;
494+ if ( lapsedTime > timeoutms ) {
495+ _timeoutMessageOrMessageFn = await _getExpectMessage ( _timeoutMessageOrMessageFn ) ;
496+ reject ( "awaitsFor timed out waiting for - " + _timeoutMessageOrMessageFn ) ;
497+ return ;
498+ }
499+ setTimeout ( pollingFn , pollInterval ) ;
500+ } catch ( e ) {
501+ reject ( e ) ;
502+ }
503+ }
504+ pollingFn ( ) ;
505+ } ) ;
506+ }
507+
508+ async function waitForModalDialog ( dialogClass , friendlyName , timeout = 2000 ) {
509+ dialogClass = dialogClass || "" ;
510+ friendlyName = friendlyName || dialogClass || "Modal Dialog" ;
511+ await awaitsFor ( ( ) => {
512+ let $dlg = $ ( `.modal.instance${ dialogClass } ` ) ;
513+ return $dlg . length >= 1 ;
514+ } , `Waiting for Modal Dialog to show ${ friendlyName } ` , timeout ) ;
515+ }
516+
517+ async function waitForModalDialogClosed ( dialogClass , friendlyName , timeout = 2000 ) {
518+ dialogClass = dialogClass || "" ;
519+ friendlyName = friendlyName || dialogClass || "Modal Dialog" ;
520+ await awaitsFor ( ( ) => {
521+ let $dlg = $ ( `.modal.instance${ dialogClass } ` ) ;
522+ return $dlg . length === 0 ;
523+ } , `Waiting for Modal Dialog to not there ${ friendlyName } ` , timeout ) ;
524+ }
525+
526+ /** Clicks on a button within a specified dialog.
527+ * This function identifies a dialog using its class and locates a button either by its selector or button ID.
528+ * Validation to ensure the dialog and button exist and that the button is enabled before attempting to click.
529+ *
530+ * @param {string } selectorOrButtonID - The selector or button ID to identify the button to be clicked.
531+ * Example (as selector): ".my-button-class".
532+ * Example (as button ID): "ok".
533+ * @param {string } dialogClass - The class of the dialog (optional). If omitted, defaults to an empty string.
534+ * Example: "my-dialog-class".
535+ * @param {boolean } isButtonID - If `true`, `selectorOrButtonid` is treated as a button ID.
536+ * If `false`, it is treated as a jQuery selector. Default is `false`.
537+ *
538+ * @throws {Error } Throws an error if:
539+ * - The specified dialog does not exist.
540+ * - Multiple buttons match the given selector or ID.
541+ * - No button matches the given selector or ID.
542+ * - The button is disabled and cannot be clicked.
543+ *
544+ */
545+ function _clickDialogButtonWithSelector ( selectorOrButtonID , dialogClass , isButtonID ) {
546+ dialogClass = dialogClass || "" ;
547+ const $dlg = $ ( `.modal.instance${ dialogClass } ` ) ;
548+
549+ if ( ! $dlg . length ) {
550+ throw new Error ( `No such dialog present: "${ dialogClass } "` ) ;
551+ }
552+
553+ const $button = isButtonID ?
554+ $dlg . find ( ".dialog-button[data-button-id='" + selectorOrButtonID + "']" ) :
555+ $dlg . find ( selectorOrButtonID ) ;
556+ if ( $button . length > 1 ) {
557+ throw new Error ( `Multiple button in dialog "${ selectorOrButtonID } "` ) ;
558+ } else if ( ! $button . length ) {
559+ throw new Error ( `No such button in dialog "${ selectorOrButtonID } "` ) ;
560+ }
561+
562+ if ( $button . prop ( "disabled" ) ) {
563+ throw new Error ( `Cannot click, button is disabled. "${ selectorOrButtonID } "` ) ;
564+ }
565+
566+ $button . click ( ) ;
567+ }
568+
569+ /**
570+ * Clicks on a button within a specified dialog using its button ID.
571+ *
572+ * @param {string } buttonID - The unique ID of the button to be clicked. usually One of the
573+ * __PR.Dialogs.DIALOG_BTN_* symbolic constants or a custom id. You can find the button
574+ * id in the dialog by inspecting the button and checking its `data-button-id` attribute
575+ * Example: __PR.Dialogs.DIALOG_BTN_OK.
576+ * @param {string } [dialogClass] - The class of the dialog containing the button. Optional, if only one dialog
577+ * is present, you can omit this.
578+ * Example: "my-dialog-class".
579+ * @throws {Error } Throws an error if:
580+ * - The specified dialog does not exist.
581+ * - No button matches the given button ID.
582+ * - Multiple buttons match the given button ID.
583+ * - The button is disabled and cannot be clicked.
584+ *
585+ * @example
586+ * // Example: Click a button by its ID
587+ * __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_OK, "my-dialog-class");
588+ * __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_OK); // if only 1 dialog is present, can omit the dialog class
589+ * __PR.clickDialogButtonID("customBtnID", "my-dialog-class");
590+ */
591+ function clickDialogButtonID ( buttonID , dialogClass ) {
592+ _clickDialogButtonWithSelector ( buttonID , dialogClass , true ) ;
593+ }
594+
595+ /**
596+ * Clicks on a button within a specified dialog using a selector.
597+ *
598+ * @param {string } buttonSelector - A jQuery selector to identify the button to be clicked.
599+ * Example: ".showImageBtn".
600+ * @param {string } [dialogClass] - The class of the dialog containing the button. Optional, if only one dialog
601+ * is present, you can omit this.
602+ * Example: "my-dialog-class".
603+ * @throws {Error } Throws an error if:
604+ * - The specified dialog does not exist.
605+ * - No button matches the given selector.
606+ * - Multiple buttons match the given selector.
607+ * - The button is disabled and cannot be clicked.
608+ *
609+ * @example
610+ * // Example: Click a button using a selector
611+ * __PR.clickDialogButton(".showImageBtn", "my-dialog-class");
612+ * __PR.clickDialogButton(".showImageBtn"); // if only 1 dialog is present, can omit the dialog class
613+ */
614+ function clickDialogButton ( buttonSelector , dialogClass ) {
615+ _clickDialogButtonWithSelector ( buttonSelector , dialogClass , false ) ;
616+ }
617+
386618 const __PR = {
387619 openFile, setCursors, expectCursorsToBe, keydown, typeAtCursor, validateText, validateAllMarks, validateMarks,
388- closeFile, closeAll, undo, redo, setPreference, getPreference, validateEqual, EDITING
620+ closeFile, closeAll, undo, redo, setPreference, getPreference, validateEqual, validateNotEqual, execCommand,
621+ awaitsFor, waitForModalDialog, waitForModalDialogClosed, clickDialogButtonID, clickDialogButton,
622+ EDITING , $, Commands, Dialogs
389623 } ;
390624
391625 async function runMacro ( macroText ) {
0 commit comments