Open9

playwrightでAPIへリクエスト送信

fuyufuyu

はじめに

APIを使う事例が少なく、実行後の確認にSlackへ送信するコードを書いておく必要がありました。

環境

Slack

Incomming Webhookを有効化したSlackアプリを利用しています。
無料版でもアプリは10個は作れるようなので、検証や履歴を気にしないログに使えます。

https://slack.com/intl/ja-jp/help/articles/115002422943-Slack-フリープランでのメッセージ、ファイルやアプリの制限

https://api.slack.com/apps

Incoming WebhooksをON

コード

example.spec.ts
import { test ,request} from '@playwright/test';
const SLACK_BASE_URL = "https://hooks.slack.com/"
const SLACK_INCOMING_WEBHOOK_PATH = "services/..."
test.describe("まとめて実行", () => {
  test('SlackのAPIへ通知するパターン1', async () => {
    const context = await request.newContext();
    await context.post(SLACK_BASE_URL + SLACK_INCOMING_WEBHOOK_PATH,{
      headers: {
        Accept : 'application/json',
      },
      data: {
        text : 'パターン1'
      }
    });
  });

  test('SlackのAPIへ通知するパターン2', async () => {
    const context = await request.newContext({
      baseURL: SLACK_BASE_URL,
    });
    await context.post(SLACK_INCOMING_WEBHOOK_PATH, {
      headers: {
        Accept : 'application/json',
      },
      data: {
        text : 'パターン2'
      }
    });
  });
})

さいごに

例を見ると認証情報を保持したり、二次利用できたり便利になっていました。

参考

https://playwright.dev/docs/test-api-testing#using-request-context

https://anandhik.medium.com/authentication-in-playwright-356f6638ed56

fuyufuyu

GitHub Actionsを使う

.github/workflows にplaywright.ymlを設定して
他のソースと一緒にGitHubに追加

playwright.yml
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
            fetch-depth: 2
      - run: git checkout HEAD^
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npx playwright test
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 1

テスト結果

artifactとして取得

課金は?

https://docs.github.com/ja/billing/managing-billing-for-github-actions/about-billing-for-github-actions
有料版を使ってるので50時間くらいはだいじょうぶか

fuyufuyu

.envファイルを使う時

理不尽なエラーに困る

型 'string | undefined' の引数を型 'string' のパラメーターに割り当てることはできません。

//環境変数に値がなければ.envファイルをよむ、あれば読み込まない
if (typeof process.env.SLACK_INCOMING_WEBHOOK_PATH == 'undefined') {
  require('dotenv').config();
}
//process.envはundefinedの型も含むのでstringにしておく
const SLACK_INCOMING_WEBHOOK_PATH :string = (process.env.SLACK_INCOMING_WEBHOOK_PATH as string);

参考

https://www.web-dev-qa-db-ja.com/ja/node.js/typescriptでprocessenvを使用する/833506694/

fuyufuyu

シナリオ

  • ファイルをダウンロード
  • Shift_jisからUTF8へ変換

コード生成

$ npx playwright codegen https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm

こちらのコードは今は動くが、今後動かないこともある。

test.describe("ファイルをダウンロード", () => {

  test('test', async ({ page }) => {
    // Go to https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm
    await page.goto('https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm');
    // Click text=CSV_1(7KB) >> nth=0
    const [download1] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('text=CSV_1(7KB)').first().click()
    ]);
    const st = await download1.createReadStream()
    if (st){
      await st
      .pipe(iconv.decodeStream('SHIFT_JIS'))
      .pipe(iconv.encodeStream('utf8'))
      .pipe(fs.createWriteStream('./data/data_utf8.csv'));   
    }
  });
})

参考

https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm
https://nodejs.org/api/stream.html#readable-streams
https://playwright.dev/docs/api/class-download
https://github.com/ashtuchkin/iconv-lite

fuyufuyu

シナリオ

  • ファイルをダウンロード
  • Shift_jisからUTF8へ変換
  • 内容を精査

コード

import {createInterface} from 'node:readline';
import {once} from 'node:events'
import iconv from 'iconv-lite'
import {test ,request} from '@playwright/test';
const SLACK_BASE_URL = "https://hooks.slack.com/"

test.describe("ファイルをダウンロード", () => {
  test('test', async ({ page }) => {
    // Go to https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm
    await page.goto('https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm');
    // Click text=CSV_1(7KB) >> nth=0
    const [download1] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('text=CSV_1(7KB)').first().click()
    ]);
    const st = await download1.createReadStream()
    if (st){
      const readline = createInterface(
        await st
        .pipe(iconv.decodeStream('SHIFT_JIS'))
        // .pipe(iconv.encodeStream('utf8'))
      )
      readline.on('line', (line: string) => {
        // Process the line.
        console.log(line.split(',')[0])
      });
      await once(readline, 'close');
      console.log('File processed.');
    }
  });
})

気になったところ

  • CJSとESM

importの書き方を揃えるところから。あんまり分かってない。

参考

https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/

https://nodejs.org/docs/latest-v17.x/api/readline.html#readlinepromisescreateinterfaceoptions

https://playwright.dev/docs/test-typescript

fuyufuyu

シナリオ

  • ファイルをダウンロード
  • Shift_jisからUTF8へ変換
  • 内容を精査
  • Slackへ通知
import {createInterface} from 'node:readline';
import {once} from 'node:events'
import iconv from 'iconv-lite'
import {test ,request} from '@playwright/test';
const SLACK_BASE_URL = "https://hooks.slack.com/"

test.describe("ファイルをダウンロード", () => {
  test('test', async ({ page }) => {
    // Go to https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm
    await page.goto('https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm');
    // Click text=CSV_1(7KB) >> nth=0
    const [download1] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('text=CSV_1(7KB)').first().click()
    ]);
    const st = await download1.createReadStream()
    if (st){
      const readline = createInterface(
        await st
        .pipe(iconv.decodeStream('SHIFT_JIS'))
        // .pipe(iconv.encodeStream('utf8'))
      )
      readline.on('line', (line: string) => {
        // Process the line.
        let arr = line.split(',')
        if (arr[3] == '足立区'){
          sendSlack(arr[3])
        }
      });
      await once(readline, 'close');
      console.log('File processed.');
    }
  });
})

//環境変数に値がなければ.envファイルをよむ、あれば読み込まない
if (typeof process.env.SLACK_INCOMING_WEBHOOK_PATH == 'undefined') {
  require('dotenv').config();
}

async function sendSlack(str: string) {
  const context = await request.newContext({
    baseURL: SLACK_BASE_URL,
  });
  await context.post(SLACK_INCOMING_WEBHOOK_PATH, {
    headers: {
      Accept : 'application/json',
    },
    data: {
      text : `${str}`
    }
  });
}
fuyufuyu

不具合対応

具体的にはファイルから読み込んだファイルを一行ずつ処理しているタイミングで
Slackへ通知をすると同期周りがうまくいかず。
取得したデータは配列に持って、まとめてSlackへ通知するように修正。

import {createInterface} from 'node:readline';
import {once} from 'node:events'
import iconv from 'iconv-lite'
import {test ,request, type Page, expect} from '@playwright/test';
const SLACK_BASE_URL = "https://hooks.slack.com/"
let apiContext;

test.beforeAll(async ({ playwright }) => {
  apiContext = await playwright.request.newContext({
    // All requests we send go to this API endpoint.
    baseURL: SLACK_BASE_URL
  });
})

test.afterAll(async ({ }) => {
  // Dispose all responses.
  await apiContext.dispose();
});

test.describe("ファイルをダウンロード", () => {
  test('ダウンロードチェック', async ({ page }) => {
    // Go to https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm
    await page.goto('https://www.toukei.metro.tokyo.lg.jp/jsuikei/js-index.htm');
    // Click text=CSV_1(7KB) >> nth=0
    const [download1] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('text=CSV_1(7KB)').first().click()
    ]);
    let senddata:Array<string> = new Array()

    const st = await download1.createReadStream()
    if (st){
      const readline = createInterface(
        await st
        .pipe(iconv.decodeStream('SHIFT_JIS'))
        // .pipe(iconv.encodeStream('utf8'))
      )

      readline.on('line',async (line:string) => {
        // Process the line.
        let arr = line.split(',')
        if (arr[3] === '足立区' || arr[3] === '荒川区'){
          senddata.push(`${arr[0]}番目${arr[3]}`)
        }
      });
      await once(readline, 'close');
      console.log('File processed.');


      // for await (const line of readline) {
      //   // Each line in input.txt will be successively available here as `line`.
      //   // console.log(`Line from file: ${line}`);
      //   let arr = line.split(',')
      //   if (arr[3] === '足立区'){
      //     senddata.push(`${arr[0]}番目${arr[3]}`)
      //   }
      // }

      await sendSlack(senddata.join("\r\n"))
    }
  });
})

//環境変数に値がなければ.envファイルをよむ、あれば読み込まない
if (typeof process.env.SLACK_INCOMING_WEBHOOK_PATH == 'undefined') {
  require('dotenv').config();
}
//process.envはundifineの型も含むのでstringにしておく
const SLACK_INCOMING_WEBHOOK_PATH :string = (process.env.SLACK_INCOMING_WEBHOOK_PATH as string);
// test.describe("まとめて実行", () => {
//   test('SlackのAPIへ通知するパターン1', async () => {
//     const context = await request.newContext();
//     await context.post(SLACK_BASE_URL + SLACK_INCOMING_WEBHOOK_PATH,{
//       headers: {
//         Accept : 'application/json',
//       },
//       data: {
//         text : 'パターン1'
//       }
//     });
//   });

  // test('SlackのAPIへ通知するパターン2', async () => {
  //   const context = await request.newContext({
  //     baseURL: SLACK_BASE_URL,
  //   });
  //   await context.post(SLACK_INCOMING_WEBHOOK_PATH, {
  //     headers: {
  //       Accept : 'application/json',
  //     },
  //     data: {
  //       text : 'パターン2'
  //     }
  //   });
  // });
// })

async function sendSlack(str: string) {
  const res = await apiContext.post(SLACK_INCOMING_WEBHOOK_PATH, {
    headers: {
      Accept : 'application/json',
    },
    data: {
      text : str
    }
  });
  expect(res.ok()).toBeTruthy();
}

参考

https://playwright.dev/docs/test-api-testing#sending-api-requests-from-ui-tests

fuyufuyu

同期・非同期とファイル

ファイルの処理が終了してからSlackで通知するようにと

      readline.on('line',async (line:string) => {
        // Process the line.
        let arr = line.split(',')
        if (arr[3] === '足立区' || arr[3] === '荒川区'|| arr[3] === '千代田区'){
          senddata.push(`${arr[0]}番目${arr[3]}`)
        }
      });
      await once(readline, 'close');
      // console.log('File processed.');
      //ファイルの処理が終わってから実行する
      await sendSlack(senddata.join("\r\n"))

コードを書いて後から理解するフローから抜けられないのね・・・

fuyufuyu

GitHub Actionsを使う - self-hosted

自前のraspberryPi(ubuntuのv22系)で実行させるように環境構築

  • raspberryPiの環境そのまま
  • docker

runner

https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github-ae

  • プロジェクト
  • 組織単位(個人用では設定表示されない)

プロジェクト用で設定。
https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository

New self-hosted runner を 押す。

Runner image を Linux Architecture を ARM64

configure

期限切れになったら、同じページを再読み込みすることで新しいtokenを確認できます。
一度認証されれば再び使うことはなし。