From 506b325e527a848cbafdc875db3c2666aeb9e62e Mon Sep 17 00:00:00 2001 From: Isao Yoshikoshi Date: Wed, 7 Aug 2019 08:06:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=E5=87=BA=E3=81=97=E3=81=AE=E3=82=B5=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/download-utils.js | 41 ++++++++++++++++++++++++++ lib/export-csv.js | 65 +++++++++++++++++++++++++++++++++++++++++ lib/url-utils.js | 7 ++++- test/export-csv.test.js | 36 +++++++++++++++++++++++ test/settings/setup.js | 2 +- 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 lib/download-utils.js create mode 100644 lib/export-csv.js create mode 100644 test/export-csv.test.js diff --git a/lib/download-utils.js b/lib/download-utils.js new file mode 100644 index 0000000..1e6c55c --- /dev/null +++ b/lib/download-utils.js @@ -0,0 +1,41 @@ +const fs = require('fs') +const util = require('util') +const path = require('path') + +const downloadFileAndGetName = async (page, downloadPath, downloadSelector) => { + // テンポラリのディレクトリを作成(ファイルのダウンロードの監視に空のディレクトリが都合がよいため) + const tempPath = path.resolve(downloadPath, require('uuid/v4')()) + await util.promisify(fs.mkdir)(tempPath) + // パスを指定してダウンロード + await page._client.send('Page.setDownloadBehavior', { + behavior: 'allow', + downloadPath: tempPath, + }) + await page.waitForSelector(downloadSelector, { visible: true }) + await page.click(downloadSelector) + // ダウンロード完了の待機 + const fileName = await waitFileDownloadAndGetName(tempPath) + // ダウンロードしたファイルを移動、テンポラリのダウンロードパスを削除 + await util.promisify(fs.rename)(path.resolve(tempPath, fileName), path.resolve(downloadPath, fileName)) + await util.promisify(fs.rmdir)(tempPath) + return fileName +} + +const waitFileDownloadAndGetName = async downloadPath => { + const MAX_RETRY = 300 + let retry = 0 + do { + const dirents = await util.promisify(fs.readdir)(downloadPath, { withFileTypes: true }) + const found = dirents.find(dirent => { + // ダウンロード中のファイルと隠しファイルを除外 + return dirent.isFile() && !dirent.name.match(/\.crdownload$|(^|\/)\.[^/.]/) + }) + if (found) return found.name + await new Promise(resolve => setTimeout(resolve, 200)) + } while ((retry += 1) < MAX_RETRY) + return null +} + +module.exports = { + downloadFileAndGetName, +} diff --git a/lib/export-csv.js b/lib/export-csv.js new file mode 100644 index 0000000..891098e --- /dev/null +++ b/lib/export-csv.js @@ -0,0 +1,65 @@ +const { getDownloadFileUrl } = require('./url-utils') +const { downloadFileAndGetName } = require('./download-utils') + +const exportToCsv = async (page, domain, appId, options = {}) => { + const url = getDownloadFileUrl(domain, appId) + const response = await page.goto(url, { waitUntil: 'networkidle2' }) + if (response.status === 520) { + await page.close() + throw new Error(`Got 520 error while goto export screen. \ + Maybe export file is not allowed to the test user.`) + } + + if (options.additionalColumns) await addExportColumns(page, options.additionalColumns) + + await Promise.all([page.waitForNavigation('.downloadlist-content-header-gaia'), page.click('#export-submit-gaia')]) + let processed = false + let retries = 120 // 2 min + do { + await page.waitFor(1000) + // 再読み込みボタンだとクリック直後にダウンロードリンクが有効にならないため、ブラウザの再読み込みをつかう + await page.reload({ waitUntil: 'networkidle0' }) + processed = await page.evaluate(() => { + const td = document.querySelector('#view-list-data-processing-gaia td > span') + return td && td.textContent === 'データがありません。' + }) + } while (!processed && (retries -= 1)) + return downloadFileAndGetName(page, options.path, 'a.download-image-gaia') +} + +const addExportColumns = async (page, columnLabels) => { + for (const columnLabel of columnLabels) { + await addExportColumn(page, columnLabel) + } +} + +const addExportColumn = async (page, columnLabel) => { + const selector = '#fm-field-toolitems-cybozu .fm-toolitem-gaia' + const index = await page.evaluate( + ({ columnLabel, selector }) => { + const nodes = [...document.querySelectorAll(selector)] + return nodes.reduce((acc, node, index) => { + return node.textContent === columnLabel ? index : acc + }, -1) + }, + { columnLabel, selector } + ) + const item = (await page.$$(selector))[index] + if (item) { + const itemBox = await item.boundingBox() + const x = itemBox.x + itemBox.width / 2 + const y = itemBox.y + itemBox.height / 2 + const columnsView = await page.$('.fm-row-gaia') + const columnsViewBox = await columnsView.boundingBox() + + await page.mouse.move(x, y) + await page.mouse.down() + await page.waitFor(100) + await page.mouse.move(columnsViewBox.x + 5, y) + await page.waitFor(100) + await page.mouse.up() + await page.waitFor(2000) + } +} + +module.exports = { exportToCsv } diff --git a/lib/url-utils.js b/lib/url-utils.js index fe37f0b..bf8bc50 100644 --- a/lib/url-utils.js +++ b/lib/url-utils.js @@ -10,6 +10,11 @@ const getIndexUrl = (domain, appId, options = {}) => { return `https://${domain}/k/${appId}/${queryString}` } +const getDownloadFileUrl = (domain, appId, options = {}) => { + const queryString = toQueryString({ view: 20 }) + return `https://${domain}/k/${appId}/exportRecord${queryString}` +} + const toQueryString = obj => qs.stringify(obj, { addQueryPrefix: true }) -module.exports = { getCreateUrl, getIndexUrl } +module.exports = { getCreateUrl, getIndexUrl, getDownloadFileUrl } diff --git a/test/export-csv.test.js b/test/export-csv.test.js new file mode 100644 index 0000000..97cef9b --- /dev/null +++ b/test/export-csv.test.js @@ -0,0 +1,36 @@ +const lib = require('../lib') +const path = require('path') +const fs = require('fs') +const util = require('util') +const { exportToCsv } = require('../lib/export-csv') +const { app, domain, username, password } = require('./config') + +jest.setTimeout(60 * 1000) +/* eslint-disable no-console */ +process.on('unhandledRejection', console.dir) + +describe('#downloadFile', () => { + let page, appId, fileName, downloadPath + + beforeAll(async () => { + appId = app['jinzo-ningen-test'] + downloadPath = path.resolve() + page = await global.__BROWSER__.newPage() + await page.setViewport({ width: 1920, height: 980 }) + lib.setConsole(page) + await lib.login(page, { domain, username, password }) + }) + + it('ファイルが正常にダウンロードされたこと', async () => { + fileName = await exportToCsv(page, domain, appId, { + path: downloadPath, + additionalColumns: ['作成者', '更新者'], + }) + expect(fileName).toBeTruthy() + }) + + afterAll(async () => { + const fullPath = `${downloadPath}/${fileName}` + await util.promisify(fs.unlink)(fullPath) + }) +}) diff --git a/test/settings/setup.js b/test/settings/setup.js index 8350ed8..5bf653a 100644 --- a/test/settings/setup.js +++ b/test/settings/setup.js @@ -8,7 +8,7 @@ const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup') module.exports = async function() { console.info(chalk.green('Setup Puppeteer')) - const browser = await puppeteer.launch({ headless: true }) + const browser = await puppeteer.launch({ headless: false }) global.__BROWSER__ = browser mkdirp.sync(DIR) fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint())