[OutSystems]Service CenterをCodeceptJSで操作してみる

CodeceptJS (オープンソースのE2Eテスト用のツール) を使ってService Centerを操作する処理を試しに書いてみました。

# 作ったもの
1. Service Centerにログイン
2. Administrationページを開く
3. Licensingページを開く
4. TableタグからActive Named UsersとApplication Objectsを抽出してファイルに出力
![qCodeceptJS1_ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/945bb6f1-51bc-a4c2-8ec9-4b3115ba184a.gif)

 (CodeceptJSのプラグインStepByStepReportで出力したスクリーンショットを、[ezgif](https://ezgif.com/)でアニメーションGIFに変換したもの）

ソース：https://github.com/jyunji-watanabe/ServiceCenterAutomation

実行方法（コマンド）：

```
npx codeceptjs run tests/licensingInfo_test.js
```

結果サンプル（ファイル。output/LicensingInfo_yyyyMMdd.txtに出力される）：

```
Active Named Users: 349
Application Objects: 873
```

# Service Centerにログイン
Service Centerに未ログインの状態でアクセスすると、ログイン画面にリダイレクトされるので、その画面でログインする処理を実装します。

ログインはあちこちのページで実行されので、steps_file.jsに実装することにしました。
steps_file.jsについては、[CodeceptJSに独自のStep Methodを追加する](https://qiita.com/tsuemura/items/3c95803382df85b42a9c)を参照。

``` javascript
module.exports = function() {
  return actor({

    // Define custom steps here, use 'this' to access default methods of I.
    // It is recommended to place a general 'login' function here.
    login: function(username, password) {
        this.see('Login');
        this.fillField({css: 'label[id$=Username]+input'}, username);
        this.fillField({css: 'label[id$=Password]+input'}, password);
        this.click('Login');
    },
  });
}
```

Service Centerのログイン画面では、Labelのforが空であるためか、Username/Passwordのlabelにあいまいにマッチさせる方法が機能しませんでした。
代わりに上記コードの通り、strict locatorの方法でCSSセレクタで指定しています。
（this.fillFieldの部分）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/970f57d6-cd31-70ed-34aa-84b0275849e3.png)

steps_file.jsに上記定義をしておくと、テストコード中から以下のようにI.を介して使えます。

```js
I.login(process.env.ACCOUNT, process.env.PASSWORD);
```

# Menuからページを開く
Service CenterではMenuか2階層になっています。
第1階層は、「.sc-header-menu」というクラスがついていて、第2階層には、「.sc-sub-header」というクラスがついています。
そこで、menu.jsというファイルを作成し、open(第1階層のmenuタイトル, 第2階層のmenuタイトル)で該当ページを開けるようにしました。

```js
const { I } = inject();

module.exports = {
    locators: {
        header: { css: '.sc-header-menu' },
        subHeader: { css: '.sc-sub-header' },
    },
    open(headerTitle, subHeaderTitle) {
        I.click(headerTitle, this.locators.header);
        if (subHeaderTitle)
            I.click(subHeaderTitle, this.locators.subHeader);
    },
}
```
I.clickは、第1引数でクリックしたいボタンやリンクのタイトルを、第2引数でそのボタンやリンクが所属する上位の要素を指定します。
つまり、
1. 第1階層にあるheaderTitleという文字列を持つリンクをクリック
2. (subHeaderTitleが指定されていたら) 第2階層にあるsubHeaderTitleという文字列を持つリンクをクリック

という処理です。

利用例（Licensingページを開く）

``` js
menu.open('Administration', 'Licensing');
```

# TableタグからActive Named UsersとApplication Objectsを抽出してファイルに出力
Licensingのページには、Tableタグがあり、以下のようになっています。
2列目各セルの上段がFeature名、4列目がその値という構造。
ロケータは「'tr>td:nth-child(2) span.NoWrap' }」（2番めの列にあるNoWrapクラスがついているspanタグ）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/ac1b26a7-69aa-771f-9973-c5e7feca492b.png)

2列目上段のみをfeatureTitles、4列目のみをfeatureValuesに取得。取得した値をループして戻り値（Feature名を指定して対応する値を取れるように）を準備しています。

ページから値を取得するI.grab**の関数は非同期実行されるのでawaitをつけることを忘れずに。従って関数自体もasyncです。

サンプルコード（Licensingページに対するPageObjectとして作成）

```js
const menu = require('../../utilities/menu');  // 「Menuからページを開く
」の処理を参照

const { I } = inject();
module.exports = {
    locators: {
        FeatureTitles: { css: 'tr>td:nth-child(2) span.NoWrap' }, // Title部分はNoWrapクラスがついているため（他に説明部分がある）
        FeatureValues: { css: 'tr>td:nth-child(4) span.NoWrap' }
    },
    open() {
        menu.open('Administration', 'Licensing');
    },
    async getFeatureValueSet() {
        let featureTitles = await I.grabTextFromAll(this.locators.FeatureTitles);
        let featureValues = await I.grabTextFromAll(this.locators.FeatureValues);
        if (featureTitles.length !== featureValues.length)
            throw new Exception("Failed to retrieve OutSystem Features. " +
                                "FeatureTitleCount, FeatureValueCount: " + FeatureTitles.length + ", " + FeatureValues.length);
        let featureTitleValueSet = {};
        for (var i = 0; i < featureTitles.length; i++)
            featureTitleValueSet[featureTitles[i]] = featureValues[i];
        return featureTitleValueSet;
    }
}
```

用意したコードを利用するテストファイルを作成。

UsernameとPasswordはdotenvモジュールを利用して、.envファイルから取得。
I.writeToFileでファイルを出力していますが、設定ファイル (codecept.conf.js) のhelpersにFileSystem: {}の設定が必要です。

``` js
Feature('LicensingInfo');
require('dotenv').config({ path: '.env' });
let utility = require('../utilities/utility.js')
Scenario('Retrieve Active Named Users and Application Objects count', async ({ I, licensingPage }) => {
    I.amOnPage('/');  // Service Centerにアクセス→未ログインなのでログイン画面へ遷移
    I.login(process.env.ACCOUNT, process.env.PASSWORD);
    licensingPage.open();
    let featureTitleValueSet = await licensingPage.getFeatureValueSet();
    let NEWLINE = '\r\n';
    let result = "Active Named Users: " + featureTitleValueSet["Active Named Users"] + NEWLINE + 
                 "Application Objects: " + featureTitleValueSet["Application Objects"];
    I.writeToFile("output/LicensingInfo" + utility.getFileSuffix() + ".txt", result);
});
```