action.skip

Sample Macros

To help you create successful macros that take advantage of all the capabilities of the Macro Editor, these samples are available as a starting point.

Basic Host Interaction

This sample illustrates basic host interaction, including:

  • Sending data to the host
  • Waiting for screens to display
  • Using the yield keyword to wait for asynchronous functions
  • Reading text from the screen
  • Displaying basic information to the user
  • Handling error basics

All macros have the following objects available by default:

  1. session - main entry point for access to the host. Can connect, disconnect and provides access to the PresentationSpace.

    The PresentationSpace object obtained from the session represents the screen and provides many common capabilities, such as getting and setting the cursor location, sending data to the host, and reading from the screen.

  2. wait - provides a simple way to wait for various host states before continuing to send more data or read from the screen.

  3. UI - provides basic user interface capabilities. Display data to the user or prompt them for information.

    // Create a new macro function
    var macro = createMacro(function*(){
    'use strict';
    
    // All macros have the following objects available by default:
    // 1. session - Main entry point for access to the host. Can connect, disconnect and provides access to the PresentationSpace. 
    //    The PresentationSpace object obtained from the session represents the screen and provides many common capabilities such as getting and setting the
    //    cursor location, sending data to the host and reading from the screen.
    // 2. wait - Provides a simple way to wait for various host states before continuing to send more data or read from the screen.
    // 3. ui - Provides basic User Interaction capabilities. Display data to the user or prompt them for information.
    
    // Declare a variable for reading and displaying some screen data.
    // As a best practice all variables should be declared near the top of a function.
    var numberOfAccounts = 0;
    
    // Start by obtaining the PresentationSpace object, which provides many common screen operations.
    var ps = session.getPresentationSpace();
    
    try {
        // Can set and get the cursor location
        ps.setCursorPosition(new Position(24, 2));
    
        // Use the sendKeys function to send characters to the host
        ps.sendKeys('cics');
    
        // SendKeys is also used to send host keys such as PA and PF keys.  
        // See "Control Keys" in the documentation for all available options
        ps.sendKeys(ControlKey.ENTER);
    
        // Wait for the cursor to be at the correct position. 
        // The wait object provides various functions for waiting for certain states to occur
        // so that you can proceed to either send more keys or read data from the screen.
        yield wait.forCursor(new Position(24, 2));
    
        // You can mix characters and control keys in one sendKeys call. 
        ps.sendKeys('data' + ControlKey.TAB + ControlKey.TAB + 'more data' + ControlKey.ENTER);
    
        // The "yield" keyword must be used in front of all "wait" and "ui" function calls.
        // It tells the browser to pause execution of the macro until the 
        // (asynchronous) wait function returns.  Consult the documentation for which functions
        // require the yield keyword.
        yield wait.forCursor(new Position(10, 26));
        ps.sendKeys('accounts' + ControlKey.ENTER);
    
        // Can also wait for text to appear at certain areas on the screen
        yield wait.forText('ACCOUNTS', new Position(3, 36)) ;
        ps.sendKeys('1' + ControlKey.ENTER);
    
        // All wait functions will timeout if the criteria is not met within a time limit.
        // Can increase timeouts with an optional parameter in the wait functions (in milliseconds)
        // All timeouts are specified in milliseconds and the default value is 10 seconds (10000ms).
        yield wait.forCursor(new Position(1, 1), 15000);
        ps.sendKeys('A' + ControlKey.ENTER);
    
        // PS provides the getText function for reading text from the screen
        numberOfAccounts = ps.getText(new Position(12, 3), 5);
    
        // Use the ui object to display some data from the screen
        ui.message('Number of active accounts: ' + numberOfAccounts);
    
        // The try / catch allows all errors to be caught and reported in a central location
    } catch (error) {
        // Again we use the ui object to display a message that an error occurred
        yield ui.message('Error: ' + error.message);
    }
    //End Generated Macro
    });
    
    // Run the macro and return the results to the Macro Runner
    // The return statement is required as the application leverages 
    // this to know if the macro succeeded and when it is finished
    return macro();
    

User Interaction

This sample illustrates how to use the provided API methods to prompt the user for input or alert them with a message.

var macro = createMacro(function*(){
  'use strict';

  // The "ui" object provides functions for prompting the user for information and displaying information

  // Declare variables for later use
  var username;
  var password;   
  var flavor;
  var scoops;

  //Begin Generated Macro 
  var ps = session.getPresentationSpace();

  try {
    // Prompt the user to enter their name and store it in a variable.
    // Note that 'yield' keyword is needed to block execution while waiting for the user input.
    username = yield ui.prompt('Please enter your username');

    // Prompt the user to enter a value with a default provided to them.
    flavor = yield ui.prompt('What is your favorite flavor of ice cream?', 'Chocolate');

    // Prompt the user to enter private information by using the 'mask' option and the input field will be masked as they type.
    // If a parameter is not used, 'null' can be used to specify that it isn't to be used. 
    // Here we illustrate that by specifying that we don't need to show a default value .
    password = yield ui.prompt('Please enter your password', null, true);      

    // The prompt function returns null if the user clicks the 'Cancel' button instead of the 'OK' button. 
    // One way to handle that case is to wrap the call in a try/catch block.
    scoops = yield ui.prompt('How many scoops would you like?');
    if (scoops === null) {
      // This will exit the macro.
      return;
      // Alternatively could throw an Error and have it be caught in the "catch" below
     }
     // Use the collected values to order our ice cream
    ps.sendKeys(username + ControlKey.TAB + password + ControlKey.ENTER);
    yield wait.forCursor(new Position(5, 1));
    ps.sendKeys(flavor + ControlKey.TAB + scoops + ControlKey.ENTER);

     // Display a message to the user.  Using the 'yield' keyword in front of the call will block
     // further execution of the macro until the user clicks the 'OK' button.
    yield ui.message('Order successful.  Enjoy your ' + scoops + ' scoops of ' + flavor + ' ice cream ' + username + '!');
    } catch (error) {
     // Here we use the ui object to display a message that an error occurred
    yield ui.message(error.message);
    }
    //End Generated Macro

});

return macro();

Paging Through Data

This sample illustrates how to page through a variable number of screens and process the data on each screen.

 // Create a new macro function.
var macro = createMacro(function*(){
  'use strict';

  // Create variable(s) for later use
  var password;
  var accountNumber;
  var transactionCount = 0;
  var row = 0;

  // Obtain a reference to the PresentationSpace object.
  var ps = session.getPresentationSpace();

  try {
    // Enter Username and Password to log on to the application.
    yield wait.forCursor(new Position(19, 48));
    ps.sendKeys('bjones' + ControlKey.TAB);

    yield wait.forCursor(new Position(20, 48));
    password = yield ui.prompt('Password:', null, true);
    ps.sendKeys(password);
    ps.sendKeys(ControlKey.ENTER);

    // Enter an application command.
    yield wait.forCursor(new Position(20, 38));
    ps.sendKeys('4');
    ps.sendKeys(ControlKey.ENTER);

    // Going to list transactions for an account.
    yield wait.forCursor(new Position(13, 25));
    ps.sendKeys('2');
    // Input an account number. Hard coded here for simplicity.
    yield wait.forCursor(new Position(15, 25));
    accountNumber = yield ui.prompt('Account Number:', '167439459');
    ps.sendKeys(accountNumber);
    ps.sendKeys(ControlKey.ENTER);

    // Wait until on account profile screen 
    yield wait.forText('ACCOUNT PROFILE', new Position(3, 33));

    // Search for text that indicates the last page of record has been reached
    while (ps.getText(new Position(22, 12), 9) !== 'LAST PAGE') {

      // While the last page of record has not been reached, go to the next page of records.
      ps.sendKeys(ControlKey.PF2);
      yield wait.forCursor(new Position(1, 1));

      // If the cursor position does not change between record screens, and there is no text 
      // on the screen you can check to confirm a screen is updated, you may wait for a 
      // fixed time period after an aid key is sent for the screen to settle.
      // For example:
      // yield wait.forFixedTime(1000);

      // For each of the rows, increment the count variable if it contains data.
      for (row = 5; row <= 21; row++) {

        // There are 2 columns on the screen. Check data on column 1.
        // In this example we know that if there is a space at a particular 
        // position then there is a transaction.
        if (ps.getText(new Position(row, 8), 1) !== ' ') {
          transactionCount++;
        }
        // Check data on column 2.
        if (ps.getText(new Position(row, 49), 1) !== ' ') {
          transactionCount++;
        }
      }
    }

    // After going through all record pages, display the number of records in a message box.
    yield ui.message('There are ' + transactionCount + ' records found for account ' + accountNumber + '.');

    // Log out of the application
    ps.sendKeys(ControlKey.PF13);
    ps.sendKeys(ControlKey.PF12);

    // The try / catch allows all errors to be caught and reported in a central location
  } catch (error) {
    // Here we use the ui object to display a message that an error occurred
    yield ui.message(error.message);
  } 
});  

// Here we run the macro and return the results to the Macro Runner
// The return statement is required as the application leverages 
// this to know if the macro succeeded
return macro();

Invoking a Web Service

This sample illustrates how to make an AJAX / REST call directly from a macro to a web service. You can integrate data from your host application into the web service call or from the web service into your host application.

In this example, we are calling the Verastream Host Integrator (VHI) CICSAcctsDemo REST service. However, you can easily adapt the code to call any web service. You are not limited to VHI.

In the example, the call goes through a proxy configured in the session server (shown below) to avoid a “Same Origin Policy” complication. If you are using a web service that supports Cross-origin Resource Sharing (CORS) and are using a modern browser, the proxy is unnecessary.

Since the jQuery library is available in macros, you may use the $.post() function directly to invoke REST services.

This example also demonstrates how to wrap a jQuery REST call in a new Promise. The promise returned from the custom function below allows "yield" to be used in the main macro code. This allows the main macro execution to wait until the service call is complete before continuing.

 var macro = createMacro(function*() {
  'use strict'; 

  // Create a few variables for later user
  var username;
  var password;
  var accountNumber;
  var accountDetails;

  // Create a function that will make an AJAX / REST call to a VHI Web Service. 
  // Could be adjusted to call any web service, not just VHI.
  // If not using CORS, the request will likely need to pass through a 
  // proxy on the session server. See sample notes for more information.
  /**
   * Hand-coded helper function to encapsulate AJAX / REST parameters, invoke the 
   * REST service and return the results inside a Promise.
   * @param {Number} acctNum to send to the REST query.
   * @param {String} username to access the REST service.
   * @param {String} password to access the REST service.
   * @return {Promise} containing $.post() results that are compatible with yield.
  */
  var getAccountDetails = function (acctNum, username, password) {
    var url = "proxy1/model/CICSAcctsDemo/GetAccountDetail";
    var args = {"filters": {"AcctNum": acctNum}, "envVars": {"Username": username, "Password": password}};

    // Wrap a jQuery AJAX / HTTP POST call in a new Promise.
    // The promise being returned here allows the macro to yield / wait
    // for its completion.
    return Promise.resolve($.post(url, JSON.stringify(args)))
      .catch(function (error) {
      // Map errors that happen in the jQuery call to our Promise. 
      throw new Error('REST API Error: ' + error.statusText);
    });
  };

  // Begin Generated Macro
  var ps = session.getPresentationSpace();
  try {
    // Could interact with the host here, log into a host app, etc...
    // Gather username and password
    username = yield ui.prompt('Username:');
    password = yield ui.prompt('Password:', null, true);
    accountNumber = yield ui.prompt('Account Number:');
    if (!username || !password || !accountNumber) {
      throw new Error('Username or password not specified'); 
    }

    // Invoke external REST service, and yields / waits for the call to complete.
    accountDetails = yield getAccountDetails(accountNumber, username, password);

    // We now have the data from our external service. 
    // Can integrate the data into our local host app or simply display it to the user.
    // For this sample we simply display the resulting account details. 
    if (accountDetails.result && accountDetails.result.length > 0) {
      yield ui.message(accountDetails.result[0].FirstName + ' $' + accountDetails.result[0].AcctBalance);
    } else {
      yield ui.message('No records found for account: ' + accountNumber);    
    }
  } catch (error) {
    // If an error occurred during the AJAX / REST call
    // or username / password gathering we will end up here.
    yield ui.message(error.message);
  }
});

// Run our macro
return macro();

Cross Origin Scripting Proxy Support

If you have web services that do not support CORS, any AJAX/REST calls will fail if they attempt to access a server other than the one where the Host Access for the Cloud application originated. This is a browser security feature.

The Host Access for the Cloud server provides a way to explicitly proxy to trusted remote servers.

  • Open ..\<install_dir>\sessionserver\microservice\sessionserver\service.yml for editing.

  • In theenv section add:

    name: zfe.proxy.mappings
    value: proxy-path=proxy-to-address
    

    Where proxy-path refers to the desired url-mapping and proxy-to-address refers to the URL where the call will be proxied.

  • In this example:

    name: zfe.proxy.mappings
    value: proxy1=http://remote-vhi-server:9680/vhi-rs/
    

    Calls made to <server:port>/proxy1 will be proxied to http://remote-vhi-server:9680/vhi-rs/

  • Multiple proxy mappings can be specified using a comma to separate the individual proxy mappings

  • Keep in mind that even when a REST server supports CORS headers, some older browsers may not, so this example may still be relevant.

hint

Your service.yml file may be replaced whenever you redeploy Host Access for the Cloud. Always back up your files.

Working with Data Cells and Attributes

This macro illustrates how to use DataCells and AttributeSet to inspect a given row/column on the screen for text and attributes. In this sample you can see:

  • How to get a collection of DataCells for a given position and length.

  • How to iterate through DataCells to build up a text string

  • How, for comparison, you can also do a similar thing using getText().

  • And finally, how to work with attributes, get a string listing, or determine whether specific ones are set at a given screen location.

var macro = createMacro(function*() {
    'use strict';

    // Obtain the PresentationSpace for interacting with the host
    var ps = session.getPresentationSpace();

    // Declare variables for later use
    var cells;
    var text;
    var attrs;

    // Set the default timeout for "wait" functions
    wait.setDefaultTimeout(10000);

    // Sample macro for working with DataCells and Attributes
    try {
        yield wait.forCursor(new Position(24, 2));

        // Get DataCells from the presentation space.
        // Row 19, col 3 is the prompt, 35 characters long
        // "Choose from the following commands:"
        cells = ps.getDataCells({row:19, col:3}, 35);
        text = '';

        // You can display text using getText
        yield ui.message("Screen text: " + ps.getText({row:19, col:3}, 35));

        // Or you can assemble the text from the DataCells at each position
        for(var index = 0; index < cells.length; index++) {
            text = text.concat(cells[index].getChar());
        }
        // And display the text
        yield ui.message("Cells text: " + text);

        // Get the attributes for the first DataCell (cell[0])
        attrs = cells[0].getAttributes();

        // Display whether we have any attributes on the data cell
        yield ui.message("Attribute set is empty: " + attrs.isEmpty());

        // Display how many attributes are set
        yield ui.message("Number of attributes: " + attrs.size());

        // Display which attributes are set
        yield ui.message("Attributes: " + attrs.toString());

        // Now display whether the high intensity attribute is set
        yield ui.message("Is high intensity: " + 
                         attrs.contains(Attribute.HIGH_INTENSITY));

        // Now display whether the underline attribute is set
        yield ui.message("Is underline: " + 
                         attrs.contains(Attribute.UNDERLINE));

        // Now display whether alphanumeric, intensified and pen-detectable attributes are set
        yield ui.message("Is alphanumeric, intensified and pen-detectable: " + 
                         attrs.containsAll([Attribute.ALPHA_NUMERIC, Attribute.HIGH_INTENSITY, Attribute.PEN_DETECTABLE]));

        // Now display whether underline, intensified and pen-detectable attributes are set
        yield ui.message("Is underline, intensified and pen-detectable: " + 
                         attrs.containsAll([Attribute.UNDERLINE, Attribute.HIGH_INTENSITY, Attribute.PEN_DETECTABLE]));
    } catch (error) {
        yield ui.message(error);
    }
    //End Generated Macro
});

// Run the macro
return macro();

Using Fields and Field Lists

This macro sample illustrates how to use common functions to interact with the fields in the Macro API. For example, how to get field text, view field information, and use field.setText as an alternative to sendKeys to interact with the host.

note

Due to browser considerations, ui.message collapses strings of spaces down to a single space. The spaces are preserved in the actual JavaScript.

var macro = createMacro(function*() {
    'use strict';

    // Obtain the PresentationSpace for interacting with the host
    var ps = session.getPresentationSpace();

    // Declare variables for later use
    var fields;
    var field;
    var searchString = 'z/VM';

    // Set the default timeout for "wait" functions
    wait.setDefaultTimeout(10000);

    // Sample macro for working with FieldList and Fields
    try {
        yield wait.forCursor(new Position(24, 2));

        // Get the field list.
        fields = ps.getFields();

        // Run through the entire list of fields and display the field info.
        for(var index = 0; index < fields.size(); index++) {
            field = fields.get(index);

            yield ui.message("Field " + index + " info: " + field.toString());
        }        

        yield ui.message("Now, find a field containing the text '" + searchString + "'");
        field = fields.findField(new Position(1, 1), searchString);                

        if(field !== null) {
            yield ui.message("Found field info: " + field.toString());
            yield ui.message("Found field foreground is green? " + (Color.GREEN === field.getForegroundColor()));
            yield ui.message("Found field background is default? " + (Color.BLANK_UNSPECIFIED === field.getBackgroundColor()));            
        }        

        // Now, find command field and modify it.
        field = fields.findField(new Position(23, 80));
        if(field !== null) {
            field.setText("cics");
        }

        yield ui.message("Click to send 'cics' to host.");
        ps.sendKeys(ControlKey.ENTER);

        // Wait for new screen; get new fields.
        yield wait.forCursor(new Position(10, 26));
        fields = ps.getFields();

        // Find user field and set it.
        field = fields.findField(new Position(10, 24));        
        if(field !== null) {        
            field.setText("myusername");
        }

        // Find password field and set it.
        field = fields.findField(new Position(11, 24));
        if(field !== null) {        
            field.setText("mypassword");        
        }

        yield ui.message("Click to send login to host.");
        ps.sendKeys(ControlKey.ENTER);        

        // Wait for new screen; get new fields.
        yield wait.forCursor(new Position(1, 1));
        fields = ps.getFields();

        // Find command field and set logoff command.
        field = fields.findField(new Position(24, 45));
        if(field !== null) {        
            field.setText("cesf logoff");
        }

        yield ui.message("Click to send logoff to host.");
        ps.sendKeys(ControlKey.ENTER);

    } catch (error) {
        yield ui.message(error);
    }
    //End Generated Macro
});

// Run the macro
return macro();

Automatic Sign-On Macro for Mainframes

In this example the Autosignon object is used to create a macro that uses the credentials associated with a user to obtain a pass ticket from the Digital Certificate Access Server (DCAS).

var macro = createMacro(function*() {
    'use strict';

    // Obtain the PresentationSpace for interacting with the host
    var ps = session.getPresentationSpace();

    // Variable for login pass ticket
    var passTicket;

    // Login application ID
    var appId = 'CICSV41A';

    // Set the default timeout for "wait" functions
    wait.setDefaultTimeout(10000);

    // Begin Generated Macro
    try {
        yield wait.forCursor(new Position(24, 2));

        // Obtain a pass ticket from DCAS.
        passTicket = yield autoSignon.getPassTicket(appId);

        ps.sendKeys('cics');
        ps.sendKeys(ControlKey.ENTER);

        yield wait.forCursor(new Position(10, 26));

        // Replace generated username with sendUserName(passTicket) ... 
        yield autoSignon.sendUserName(passTicket);

        // ps.sendKeys('bvtst01' + ControlKey.TAB + ControlKey.TAB);        
        ps.sendKeys(ControlKey.TAB + ControlKey.TAB);

        yield wait.forCursor(new Position(11, 26));

        // Replace generated password with sendPassword(passTicket) ...
        yield autoSignon.sendPassword(passTicket);

        // var userInput3 = yield ui.prompt('Password:', '', true);
        // if (userInput3 === null) {
            // throw new Error('Password not provided');
        // }
        // ps.sendKeys(userInput3);
        ps.sendKeys(ControlKey.ENTER);

        yield wait.forCursor(new Position(1, 1));
        yield ui.message('Logged in. Log me off.');
        ps.sendKeys('cesf logoff');
        ps.sendKeys(ControlKey.ENTER);
    } catch (error) {
        yield ui.message(error);
    }
    //End Generated Macro
});

// Run the macro
return macro();

Using File Transfer (IND$File)

This series of sample macros demonstrate how to use the File Transfer API to retrieve a list of files, download a file, and upload a file to a 3270 host.

note

You must be logged in and at a ready prompt before running these macros.


List files

This macro demonstrates how to use the File Transfer API to retrieve a list of files on a 3270 host using IND$File transfer. The IND$File transfer object is retrieved from the file transfer factory and then used to obtain an array of HostFile objects from either TSO or CMS.

var macro = createMacro(function*() {
    'use strict';

    try {
        var fileTransfer = fileTransferFactory.getInd$File();
        var hostFiles = yield fileTransfer.getHostFileListing();

        yield ui.message('Found ' + hostFiles.length + ' files');
        if (hostFiles.length > 0) {
            var firstFile = hostFiles[0];
            var msg1 = 'The catalog name is ' + firstFile.getParent() + '.  ';
            var msg2 = 'The first file is ' + firstFile.getName();
            yield ui.message(msg1 + msg2);
        }
    } catch (error) {
        yield ui.message(error);
    }
});

// Run the macro
return macro();

Download file

This macro shows how to use the File Transfer API to download a file from a 3270 host using IND$File transfer. The IND$File transfer object is retrieved from the file transfer factory. In this example, the transfer method is set to ASCII to demonstrate use of the setTransferOptions function.

The sample macro downloads the first file returned from a call to getHostFileListing by creating a download URI with a call to the getDownloadUrl function. The macro can be used in either a CMS or TSO environment but the choice must be specified on the first line or the code modified slightly for the intended system.

var hostEnvironment = 'CMS';  // 'TSO'
// Construct file path, ie catalog/file.name or catalog/partition/file
function getPath (fileNode) {
    var prefix = fileNode.getParent() ? fileNode.getParent() + '/' : '';
    return prefix + fileNode.getName();
}

var macro = createMacro(function*() {
    'use strict';

    try {
        var fileTransfer = fileTransferFactory.getInd$File();

        // The transferMethod options are 'binary' and 'ascii'
        fileTransfer.setTransferOptions({transferMethod: 'ascii'});

        // This demo retrieves the first file returned in the listing
        var hostFiles = yield fileTransfer.getHostFileListing();
        var firstHostFile = hostFiles[0];

        if (hostEnvironment === 'CMS') {
           yield wait.forText('Ready', new Position(1,1), 5000);
        }

        // Download
        // If you already know the path of the file you want, just pass that to getDownloadURL()
        var downloadUrl = fileTransfer.getDownloadURL(getPath(firstHostFile));

        // This changes the browser location. You may experience different results on different browsers
        window.location = downloadUrl;

        // If you want to read the file contents into a variable instead of downloading
        // it, you can use jQuery
        // var fileContents = yield $.get(downloadUrl);

    } catch (error) {
        yield ui.message(error);
    }
});

// Run the macro
return macro();

Upload file

This macro illustrates how to use the File Transfer API to upload a file to a 3270 host using IND$File transfer. The sample macro prompts the user to choose a file from the local file system by triggering the browser’s file selection dialog. It then retrieves the current catalog on TSO or drive identifier on CMS by calling getHostFileListing. Finally, the sendFile function is called to deliver the selected local file to the host.

The macro can be used in either a CMS or TSO environment but the choice should be specified on the first line. In this example, the transfer method is set to ascii; you may want to change this to binary.

var hostEnvironment = 'CMS';  // 'TSO'
// Open the browser's file chooser dialog programmatically
function promptForFileToUpload () {
    return new Promise(function (resolve, reject) {
        // We are not notified if the user cancels the file chooser dialog so reject after 30 seconds
        var timerId = setTimeout(reject.bind(null, 'Timed out waiting for file selection'), 30000);
        var fileSelector = document.createElement('input');
        fileSelector.setAttribute('type', 'file');
        fileSelector.onchange = function (evt) {
            var file = evt.target.files[0];
            clearTimeout(timerId);
            resolve(file);
        };
        fileSelector.click();
    });
}

var macro = createMacro(function*() {
    'use strict';

    try {
        var fileTransfer = fileTransferFactory.getInd$File();

       // The transferMethod options are 'binary' and 'ascii'
        fileTransfer.setTransferOptions({transferMethod: 'ascii'});        

        var localFile = yield promptForFileToUpload();

        // Retrieve the current catalog name and append the selected file name to it
        var hostFiles = yield fileTransfer.getHostFileListing();
        var destination = hostFiles[0].getParent() + '/' + localFile.name;

        if (hostEnvironment === 'CMS') {
           yield wait.forText('Ready', new Position(1,1), 5000);
        }

        var result = yield fileTransfer.sendFile(localFile, destination);

    } catch (error) {
        yield ui.message(error);
    }
});

// Run the macro
return macro();