E2Eテスト(protractor)で行ったテストをエクセルにまとめるVBAを組んだ

そろそろスクショ職人から卒業すべく、protractorを使ってAngularの自動テスト技術を磨いています。

でも上司への提出用に、エクセルに証跡を残していかないといけないんだろーなー。
自動で取得したスクショをエクセルに貼って、テストの流れがわかるように矢印図形なんかもつけて…
ああ、嫌になりますね。やっぱりスクショ職人じゃないか！
こんな状態になる前に、テストコードとスクショファイルから自動で証跡を作ってくれるVBAを組みました。

##テスト対象のAngularコード
```html:app.component.html
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>

<input type="button" id="click1" value="クリック！" routerLink="./sub" />
<input type="button" id="click2" value="もどる" routerLink="./.." />
<br />
<router-outlet></router-outlet>
```

```html:sub.component.html
<h1>Angularです</h1>
<img src="../../assets/react.png" />
```

```ts:app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SubComponent } from './sub/sub.component';


const routes: Routes = [
  { path: 'sub', component: SubComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

```

大体こんな感じの超簡易なコードです。app.component.tsとかは省略。
これを動かすとこんな感じ。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480762/07292dfc-135e-2a28-c080-f0f44f539cab.png)
クリックボタンを押します。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480762/62a6c216-5cd1-43ab-5d75-d12f4019510d.png)
下にsubのコンポーネントが現れました。
このsubコンポーネントの「Angularです」の文字列が間違いなく表示されているかチェックすることを今回のテストの目的とします。
現実的じゃないけど許して

##テストコード
以下がテストコード。
app.po.tsは置いてきた。

```ts:app-e2e-spec.ts
import { browser, logging, by, element } from 'protractor';

describe('workspace-project App', () => {
  const fs = require('fs');

  beforeEach(() => {
    browser.get('');
  });

  it('テストケース1', () => {
    browser.takeScreenshot().then((png) => {
      saveScreenshot(png, 'スクショ1.png');
    })

    element(by.id('click1')).click();

    expect(element(by.css('app-sub h1')).getText()).toEqual('Angularです');

    browser.takeScreenshot().then((png) => {
      saveScreenshot(png, 'スクショ2.png');
    })
  });

  afterEach(async () => {
    const logs = await browser.manage().logs().get(logging.Type.BROWSER);
    expect(logs).not.toContain(jasmine.objectContaining({
      level: logging.Level.SEVERE,
    } as logging.Entry));
  });

  //スクショ保存
  function saveScreenshot(data: any, name: string) {
    const stream = fs.createWriteStream(name);
    stream.write(new Buffer(data, 'base64'));
    stream.end();
  }
});

```
itでテストケースのひとかたまりです。
そこで起動直後のスクショを「スクショ1.png」という名前で取得し、
その後クリックボタンを押下。
遷移したsubコンポーネントのh1テキストが「Angularです」だということを確認しています。
そしてまたスクショ保存。「スクショ2.png」という名前。

##VBAコード

これでテストは終わりですが、ここからが今回のキモ。
今回私が組んだVBAはこちら。

```visualbasic

Type TestStruct
    testCaseIt As String
    testCaseSc() As String
End Type

Sub subLfFileRead()
    
    Dim buf As String
    Dim arrOneLine() As String
    Dim i As Long
    Dim RE, strPattern As String, reMatch
    Dim RESc, strPatternSc As String, reMatchSc
    Dim testSet() As TestStruct, intTestSet As Integer: intTestSet = 0
    Dim intTestCaseScCnt As Integer: intTestCaseScCnt = 0
    Dim strPath As String: strPath = Cells(4, 1).Value
    
    Set RE = CreateObject("VBScript.RegExp")
    Set RESc = CreateObject("VBScript.RegExp")
    
    strPattern = "it\(\'.+\'"
    strPatternSc = "savescreenshot.+\'.+\'"
    
    RE.Pattern = strPattern
    RE.IgnoreCase = True
    RE.Global = False
    
    RESc.Pattern = strPatternSc
    RESc.IgnoreCase = True
    RESc.Global = False

    'テストコードオープン
    With CreateObject("ADODB.Stream")
        .Charset = "UTF-8"
        .Open
        .LoadFromFile Cells(2, 1).Value
        buf = .ReadText
        .Close
        arrOneLine = Split(buf, vbLf)
    End With
    
    'テストコード読み込み
    For i = 0 To UBound(arrOneLine) - 1
        'コメント行は除外
        If InStr(arrOneLine(i), "//") = 0 Then
            '正規表現の検索・テストケース
            Set reMatch = RE.Execute(arrOneLine(i))
            If reMatch.Count > 0 Then
                ReDim Preserve testSet(intTestSet)
                testSet(intTestSet).testCaseIt = fncReturnCommaExclude(reMatch(0))
                intTestSet = intTestSet + 1
            End If
            
            '正規表現の検索・スクショ名
            Set reMatchSc = RESc.Execute(arrOneLine(i))
            If reMatchSc.Count > 0 Then
                ReDim Preserve testSet(intTestSet - 1).testCaseSc(intTestCaseScCnt)
                testSet(intTestSet - 1).testCaseSc(intTestCaseScCnt) = fncReturnCommaExclude(reMatchSc(0))
                intTestCaseScCnt = intTestCaseScCnt + 1
            End If
        End If
    Next i
    
    Call subFilePasteSheet(testSet, strPath)
    
    
End Sub

Sub subFilePasteSheet(ByRef structType() As TestStruct, ByVal strScreenshotPath As String)

    Dim myFileName As String
    Dim sglLastPictureHeight As Single
    Dim sglLastPictureWidth As Single
    Dim sglAllowHeight As Single
    Dim myShape As Shape
    Dim newSheet As Worksheet

    sglLastPictureHeight = 0
    sglAllowHeight = 0
    sglLastPictureWidth = 0
    
    For i = 0 To UBound(structType)
        Set newSheet = ThisWorkbook.Worksheets.Add
        newSheet.Name = structType(i).testCaseIt
        newSheet.Cells(i + 1, 1).Value = structType(i).testCaseIt
        
        For j = 0 To UBound(structType(i).testCaseSc())
            myFileName = strScreenshotPath & "\" & structType(i).testCaseSc(j)
        
            Set myShape = newSheet.Shapes.AddPicture( _
                  Filename:=myFileName, _
                  LinkToFile:=True, _
                  SaveWithDocument:=False, _
                  Left:=54, _
                  Top:=13.5 + sglLastPictureHeight * j + sglAllowHeight * j, _
                  Width:=0, _
                  Height:=0)
            
            With myShape
                .ScaleHeight 1, msoTrue
                .ScaleWidth 1, msoTrue
            End With
            
            sglLastPictureHeight = myShape.Height
            sglLastPictureWidth = myShape.Width
            
            If j <> UBound(structType(i).testCaseSc()) Then
            
                Set myShape = newSheet.Shapes.AddShape( _
                                    Type:=msoShapeDownArrow, _
                                    Left:=(54 + sglLastPictureWidth) / 2 - 100, _
                                    Top:=13.5 + sglLastPictureHeight * (j + 1) + sglAllowHeight * j, _
                                    Width:=200, _
                                    Height:=45)
                sglAllowHeight = myShape.Height
            Else
                sglAllowHeight = 0
            End If
            
        Next j
        
        sglAllowHeight = 13.5
    Next i

End Sub

'コンマに挟まれている文字列を返す関数
Function fncReturnCommaExclude(ByVal strData As String)

    fncReturnCommaExclude = ""
    
    intfrontcomma = InStr(strData, "'") + 1
    intBackComma = InStr(intfrontcomma, strData, "'")
    
    fncReturnCommaExclude = Mid(strData, intfrontcomma, intBackComma - intfrontcomma)

End Function


```

ざっくり言うと、テストコード内のitやsavescreenshotを検索、その中から文字列抽出、
抽出した文字列からスクショを拾いエクセルのシートに貼る。スクショのサイズも見ながら調節する。

こんな感じです。
これをこんな感じで使う。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480762/3222f9f6-baa7-9d5d-6d25-779a4a0a124f.png)
vscodeの改行コードがLfになってたので、そっちのみ対応してます。CrLfまでやるのは気が向かなかった。

実行ボタンを押すと新しくこんなシートができあがります。（倍率を低くする処理はありません。）
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480762/f96e90b0-a56c-252f-f841-a9acc68aaa16.png)

よく見るエビデンスの出来上がりですね。
これでスクショ職人は卒業だ！
でも今回の記事に載せる画像作るためにいくつかスクショ撮ったな…
やっぱりスクショ職人じゃないか！