Banner

Using TypeScript with TestComplete

Jun 30, 2018
()

TypeScript lets you write JavaScript that is easier to extend, maintain and is more readable. For example, I’ve always had issues with web browser scripting because there are several TestComplete script objects to simply open, navigate, and close a browser.

So, would you rather work with TestComplete script running wild like this example?

// unwrapped TestComplete script
var browserRunner = Browsers.Item(btChrome);
browserRunner.Run();
browserRunner.Navigate(url);
var window = Sys.Browser("*").BrowserWindow(0);
window.Maximize();
Log.Picture(window, "screenshot");
window.Close();
…or express your scripting ideas this way:

// TestComplete script wrapped using TypeScript
new Browser(btChrome)
    .run()
    .navigate(url)
    .maximize()
    .screenshot()
    .close();


 I vote for example #2. Both examples have the same functionality, but the second example is easier to follow and is much more compact.

So What’s the Holdup?

Unfortunately, TestComplete doesn’t understand TypeScript. TypeScript eventually compiles to JavaScript, but we have to get past the syntax checker first. This TypeScript code in TestComplete doesn’t get past the first line of syntax checking where it balks at “enum”.

TS1

What if we edit the code in a TypeScript supported IDE? The IDE is on a first-name basis with TypeScript, but TestComplete’s Log is a stranger in these parts.

TS2

Defining TestComplete Runtime Objects for TypeScript

TypeScript allows us to define the “shape” of objects from external libraries, including TestComplete runtime objects. By “shape”, I mean the name, members and types of the objects in an external library. Take a look at the declaration of the Log object below:

/** Partial wrapper for TestComplete Log object */
declare var Log: {
    Message(message: string, additional?: string, priority?: number, attributes?: any): void;
};

The declaration for Log informs the TypeScript IDE and compiler to allow TestComplete’s Log object. Message defines that the first parameter “message” is a required string and the remaining optional parameters will accept any type. The TypeScript IDE is muy happy with the change.

TS3

Type Safety

The use of any gets us past the syntax checker, but doesn’t help us code more correctly. For example, to change the font color in a logged message, I need to call Log.CreateNewAttributes(), then assign and use the attributes in Log.Message. Coding directly in TestComplete JavaScript, I can log a message that shows in a red font using the script below:

var attributes = Log.CreateNewAttributes();
attributes.FontColor = clRed;
Log.Message("This message displays in a red font", "Additional info", 300, attributes);

Nothing prevents me from using generic JSON instead of the object returned by Log.CreateNewAttributes(). TestComplete logs the message without complaint, but the behavior is incorrect — the logged message doesn’t show in red.

var attributes = { FontColor: clRed };
Log.Message("This message shows with a black font", "Additional info", 300, attributes);

By ratcheting down the TypeScript definition, we can build in increasingly stronger type safety. The LogAttributes declaration defines its properties:

/** TestComplete TypeScript definitions */
  
declare class LogAttributes {
    BackColor: number;
    Bold: boolean;
    ExtendedMessageAsPlainText: boolean;
    FontColor: number;
    Italic: boolean;
    StrikeOut: boolean;
    Underline: boolean;
}
  
/** Partial wrapper for TestComplete Log object */
declare var Log: {
    Message(message: string, additional?: string, priority?: number, attributes?: LogAttributes): void;
    CreateNewAttributes(): LogAttributes;   
};
  
declare var clRed: number;


In the screenshot of the TypeScript IDE, the second example flags the attributes object as not being compatible with the Log.Message() method, warning us that we’re not supplying the correct parameter type.

TS4

Note: There are limits to what TypeScript can help us with. For example, TypeScript doesn’t know anything about TestComplete’s runtime objects. TypeScript doesallow type compatibility through structural subtyping. If all the members in the second example’s attributes object were defined, i.e. “BackColor”, “Bold” and so on, TypeScript would allow it. But the TestComplete Log runtime object will still not perform correctly because it doesn’t have the object returned from CreateNewAttributes, it just has an object with the same properties. Even with these limitations, TypeScript does get us closer to our goals of type safety and easier to maintain code.

Wrapping TestComplete Objects

This brings us back around to wrapping TestComplete objects to perform useful actions. The Browser TypeScript class below wraps several disparate pieces. Normally, I have to remember which object runs and navigates the browser, how to get the page after navigating and how to get the browser window so I can move or close the window.

declare var Browsers: any;
declare var Sys: any;
declare var btChrome: number;
  
class Browser {
  BrowserType: number;
  BrowserRunner: any;
  Process: any;
  BrowserWindow: any;
  CurrentPage: any;
  CurrentUrl: string;
  
  constructor(browserType: number) {
    this.BrowserType = browserType;
    this.BrowserRunner = Browsers.Item(browserType);
  }
  
  run() {
    this.BrowserRunner.Run();
    this.Process = Sys.Browser("*");
    this.BrowserWindow = this.Process.BrowserWindow(0);
    return this;
  }
  
  navigate(url: string) {
    this.CurrentUrl = url;
    this.BrowserRunner.Navigate(url);
    this.refresh();
    return this;
  }
  
  refresh() {
    this.CurrentPage = this.Process.Page("*");
    return this;
  }
  
  validate() {
    if (!this.CurrentPage || !this.CurrentPage.contentDocument) {
      Log.Error(this.CurrentUrl + " not found.");
    }
    return this;
  }
  
  close() {
    this.BrowserWindow.close();
    return this;
  }
  
  maximize() {
    this.BrowserWindow.maximize();
    return this;
  }
}

The compiled TypeScript boils down to pure JavaScript that runs in TestComplete:

function testBrowser() {
    var url = "http://www.linotadros.com";
    new Browser(btChrome)
        .run()
        .navigate(url)
        .validate()
        .maximize()
        .close();
}

Here’s an example that exercises the new Browser object. The example is written exactly the same in TypeScript or pure JavaScript.

Intellisense

Every time I discuss using some kind of object mechanism for TestComplete JavaScript, the next questions is “what about Intellisense?”. My not-very satisfactory answer is “The value of maintainability may outweigh ease-of-use”. I could say that real coders only use ones and zeroes, but no one likes a smart aleck. In a TypeScript IDE (Visual Studio Code in the screenshot) we get a list of all the properties and methods available for the TypeScript Browser object.

TS5

TypeScript IDEs plug-ins typically support the jsdoc standard. The screenshot below shows the Atom editor with the atom-typescript and docblockr plugin. Typing /** populates a comment skeleton that you finish by hand.

TS6

Inheritance

TypeScript uses the extends keyword to add properties and methods to an existing class. I can inherit from the Browser object and create a new SelfieBrowser class that knows how to take a screenshot of the current page:

class SelfieBrowser extends Browser {
  screenshot() {
    Log.Picture(this.CurrentPage.Picture(), "Screenshot of " + this.CurrentUrl);
    return this;
  }
}

The sample usage shows that we retain the existing functionality and gain a new screenshot method.

new SelfieBrowser(btChrome)
  .run()
  .navigate(url)
  .validate()
  .maximize()
  .screenshot()
  .close();

TypeScript Definition Files

Up to now, the examples declare TestComplete objects inside the same .ts file that consumes them. TypeScript has a better way. It supports JavaScript libraries through the use of definition files (.d.ts). Definition files describe external objects that TypeScript should accept. There are hundreds of these definition files on DefinitelyTyped.org, but alas, none for TestComplete. Fortunately, you can build a partial definition file that supports just the features you need. Better yet, perhaps we can persuade the folks at SmartBear to create a definition file.



Load more reviews
You've already submitted a review for this item
|
()

Copyright © 2018 Alain "Lino" Tadros
Using Sitefinity 11.1.6800