既存文化をぶっ壊す!Selenium+Node.jsで業務ハックに入門

アイスタイルAdvent Calender2019の20日目を担当させていただきます。

長めの前置きがあります。技術の話は後半からしてます。
前半は、弊社の文化の話をします。

弊社でUnipos使ってます

しかし、Slack連携させてません。
連携させることによって、以下のことができるようになるらしいです。

投稿を送る、残りのポイントの確認、Slackで通知を受け取れる(投稿受信、ポイントリセットなど)

https://support.unipos.me/hc/ja/articles/360031704191-Slack連携を行う方法について

割と連携させてほしいので、Slackの管理者に突撃しました。

アイスタイル グループでは、年間で最も多く社員からの『Like!』を集めた社員にLike!Awardという賞が贈られます。
このLike!(感謝)を集めるためにUniposを使ってます。

社員同士が日々の感謝を伝え合い、それが他の社員にも見える形で評価されるのは素敵な制度ですよね

ちなみに、これは僕がLike!Awardを取った時の写真です

ちなみに、これは僕がLike!Awardを取った時の写真です

また、社内のコミュニケーションツールがいくつかの派閥に分かれているという現状があります

  • エンジニア → Slack
  • 非エンジニア → Workchat(ビジネス版Facebook)

会社の評価制度に関わる仕組みなので、一部の社員にえこひいきできないというのが人事の判断だと思います。

普段こんな感じでUniposを使ってます


この制度・文化の課題

弊社のUniposを使った社員の相互評価制度には課題があると思っています

  • 自分の所属する組織にUniposを使う文化がないとUniposで感謝を伝えるモチベーションが続かない

例えば、普段お仕事で関わらない人とランチに行って、Uniposでお礼をしてもリアクションが帰ってこないなんてことがあったら、また同じことがあってもUniposで感謝を伝えなくなりますよね?(その場で感謝はすると思いますが)

これが社内でたまに会う人なら、まだモチベーションを保てる人もいそうですが、自分の所属する組織が全くUniposを使わない人たちで構成されていたら、どうでしょうか?結構キツいですよね

別の課題として、社内ツールがめちゃくちゃ多いことがUniposへのハードルを上げているような気がします。軽く挙げて見たんですが書ききれないので一旦この辺で

ツール種別 ツール名
コミュニケーションツール(システム系) Slack
コミュニケーションツール(全社) Workchat, Gmail
ドキュメント管理 Confluence
タスク、プロジェクト管理 Jira、Redmine
予定管理、施設予約管理 サイボウズGaroon
社内用管理画面 各サービスごとにある
etc

ここから技術の話

ツールが多いことによる社員の負担をなるべく軽減して、楽しいUniposライフを送りたいですね

今回は、Uniposを普段から積極的にウォッチしてない人でも、誰からメッセージを貰ったらSlackに通知するアプリケーションを作成します

要件

  • 全員の投稿がタイムラインとして見れること
  • 送ったり貰ったりしたらメンション付きで通知すること

手順

普段よく触っているNode.jsを使って作ろうと思います。

  1. webdriverのダウンロード(パスも通しておく)
  2. npm install selenium-webdriver node-cache request
  3. Uniposにログインして、送り主のID、メッセージボディ、受け取り主のIDを取得するスクリプトを組む
  4. 3.のスクリプトを定期実行させて新規投稿をチャンネルに送る

Seleniumを使ってUniposにログインするコードです

const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const { Builder, By, until } = webdriver;

// headless modeでchromeを起動
const driver = await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(new chrome.Options().headless())
    .build();

// URLを指定して画面遷移
await driver.get('https://unipos.me/login');

// class名が"login_btn"のDOMが表示されるまで待つ
await driver.wait(until.elementsLocated(By.className('login_btn')), 10000);

const [emailInput, passwdInput, loginButton] = await Promise.all([
    // XPathでElementの指定してDOMを取得
    driver.findElement(By.xpath('/html/body/div[1]/div/div/div/div[2]/input[1]')),
    driver.findElement(By.xpath('/html/body/div[1]/div/div/div/div[2]/input[2]')),
    // Class名でElementの指定してDOMを取得
    driver.findElement(By.className('login_btn'))
]);
// Inputに文字を入力する
await emailInput.sendKeys(process.env.EMAIL);
await passwdInput.sendKeys(process.env.PASSWORD);
// ログインボタンをクリックして画面遷移(ホーム画面へ)
await loginButton.click();

定期的にUniposのタイムラインを取得して、新着が投稿が存在したらチャンネルに投稿するようにシンプルなメモリキャッシュ(node-cache)を使っています。

詰まったところ

ホーム画面に送り主の名前は表示されているがIDが表示されていません
弊社では各社内ツールのIDを統一しているので、このIDをSlackに飛ばすことでその人にメンションが届きます。そのためこのIDを取得する方法として

受け取り主のプロフィール画像をクリック(ホーム画面)→受け取り主のプロフィール画面にてIDを取得(プロフィール画面)→戻る(ホーム画面)→以降繰り返し

をすることで実現したいと思います

const cards = await driver.findElements(By.className('card'));
const toIds = [];
const fromIds = await Promise.all(cards.map(async (card) => {
    await driver.wait(until.elementsLocated(By.className('card_id')), 10000);
    const fromId = await card.findElement(By.className('card_id')).getText();

    await driver.wait(until.elementsLocated(By.className('card_picTo')), 10000);
    await driver.findElement(By.className('card_picTo')).click();

    await driver.wait(until.elementsLocated(By.className('ownProfile_uname')), 10000);
    const toId = await driver.findElement(By.className('ownProfile_uname')).getText();
    toIds.push(toId);

    // ブラウザバック
    await driver.navigate().back();
    await driver.wait(until.elementsLocated(By.className('card_id'), 10000));

    return fromId;
}));

このように書いて実行すると、
取得したDOMが画面遷移により参照が破棄され再度参照された時にStaleElementReferenceExceptionが発生します。
そのため、mapで繰り返すのではなく、愚直にfor文で要素の数分繰り返して取得しています

const cards = await driver.findElements(By.className('card'));
const toIds = [];
for(let i=0; i<cards.length; i++) {
    await driver.wait(until.elementsLocated(By.className('card_picTo')), 10000);
    const pics = await driver.findElements(By.className('card_picTo'));
    await pics[i].click();

    await driver.wait(until.elementsLocated(By.className('ownProfile_uname')), 10000);
    const toId = await driver.findElement(By.className('ownProfile_uname')).getText();
    toIds.push(toId);

    //ブラウザバック
    await driver.navigate().back();
    await driver.wait(until.elementsLocated(By.className('card_id'), 10000));
}

しかし、こう書いてしまうことで、プロフィール画面に遷移→ブラウザバックを繰り返してIDを取得いる間に、タイムラインに新着投稿があった場合に、取得したIDにズレが発生する可能性があります。

今後改善できればと思います。

最後に

ここまで書ききりましたが、実はこのアプリケーションをSlackチームへの導入をまだしてません。
関係各所と議論を重ねたのちに導入しようと思います。

文化は個人が作っていくのもではなく、みんなの当たり前が文化になると思うので、来期からLike!Awardは個人ではなく組織・チームを評価してほしいな~と個人的に思ってます。
そうなった暁には、自分が所属するメディア開発統括部で受賞しましょう!

今回初めてSeleniumを触ってみましたが、1回覚えると色んなところで応用ができるので積極的に勉強していきたいと思います(自動化、UIテストなどなど)

成果物のソースはこちらから
https://github.com/chilitreat/unipos_notify

書いた人:白田
サムネ作った人:白田
Like!Award受賞した人:白田

2019/12/25 20:32 追記

LikeAward!を取ってしまった関係で、テレビ出演をしました(news zeroさん)

今年の受賞者 白田光さん

アイスタイルエンジニアの広告塔として社内外ともに活発に活動していきたいと思います。
よろしくお願いします!!!!!!!!!!

メディア開発統括部 ウェブ開発第2グループ Node.js, TypeScriptあたりが好きでよく書いてます 何もしてないのに後輩にもいじられる日々が続いています