在 iRecorder (Web IDE , Web UI 自

作者: 光剑书架上的书 | 来源:发表于2018-12-12 16:01 被阅读26次

    在 iRecorder (Web IDE , Web UI 自动化测试平台)中添加上传云端平台的功能

    后端代码(Kotlin):

    @Controller
    class UiTestCaseController {
        @Autowired
        lateinit var uiTestCaseMapper: UiTestCaseMapper
        @Autowired
        lateinit var employeeMapper: EmployeeMapper
    
        class IRecorderSideJsonData {
            lateinit var name: String
            lateinit var sideJson: String
            lateinit var token: String
        }
    
        @RequestMapping(value = ["/uitestcase/upload.api"], method = [RequestMethod.POST])
        @ResponseBody
        fun upload(@RequestBody irecorderSideJsonData: IRecorderSideJsonData): Boolean {
    
            val name = irecorderSideJsonData.name
            val sideJson = irecorderSideJsonData.sideJson
            val token = irecorderSideJsonData.token
    
            println("name=${name}")
            println("sideJson=${sideJson}")
            println("token=${token}")
    
            val uiTestCase: UiTestCase? = uiTestCaseMapper.findByToken(token)
            try {
                if (uiTestCase == null) {// 新记录
                    val newRecord = UiTestCase()
                    newRecord.gmtCreate = Date()
                    newRecord.gmtModified = Date()
                    newRecord.isDeleted = 0
                    newRecord.name = name
                    newRecord.sideJson = sideJson
                    newRecord.token = token
                    uiTestCaseMapper.insert(newRecord)
                    return true
                } else { // 更新记录
                    //  update 核心字段
                    uiTestCase.gmtModified = Date()
                    uiTestCase.name = name
                    uiTestCase.sideJson = sideJson
                    uiTestCaseMapper.updateByPrimaryKeyWithBLOBs(uiTestCase)
                    return true
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return false
        }
    
        @RequestMapping(value = ["/uitestcase/get.api"], method = [RequestMethod.GET])
        @ResponseBody
        fun get(token: String): UiTestCase {
            return uiTestCaseMapper.findByToken(token)
        }
    
        //    http://127.0.0.1:9000/getTestCase?token=aac0a0bc-9f06-402c-b44e-ecbfa29f1b90
        @RequestMapping(value = ["/getTestCase"], method = [RequestMethod.GET])
        fun getTestCase(token: String, model: Model, request: HttpServletRequest, response: HttpServletResponse): String {
            val loginUser = SimpleUserUtil.getBucSSOUser(request);
            val empId = loginUser.empId
    
            println("loginUser:${JSON.toJSONString(loginUser)}")
            val uiTestCase = uiTestCaseMapper.findByToken(token)
            println("uiTestCase===${JSON.toJSONString(uiTestCase)}")
            if (uiTestCase.owner == null) { // 首次带着该 token 打开链接,回写当前打开人的工号到 owner 字段
                uiTestCase.owner = empId
                uiTestCaseMapper.updateByPrimaryKeyWithBLOBs(uiTestCase)
            }
    
            model.addAttribute("uiTestCase", uiTestCase)
    
            val emp: Employee? = employeeMapper.findByWorkno(uiTestCase.owner)
            var ownerText = emp?.empName
            model.addAttribute("ownerText", ownerText)
    
            val sideJson = uiTestCase.sideJson
            println("sideJson=$sideJson")
            val sideJsonDto = JSON.parseObject<SideJsonDto>(sideJson, SideJsonDto::class.java)
            println("sideJsonDto = " + JSON.toJSONString(sideJsonDto))
            model.addAttribute("sideJsonDto", sideJsonDto)
            return "/testcase"
        }
    
        @RequestMapping(value = ["", "/", "/listTestCase"], method = [RequestMethod.GET])
        fun listTestCase(model: Model, request: HttpServletRequest, response: HttpServletResponse): String {
            val loginUser = SimpleUserUtil.getBucSSOUser(request);
            val empId = loginUser.empId
            println("loginUser:${JSON.toJSONString(loginUser)}")
            val list = uiTestCaseMapper.listTestCase(empId)
            println(JSON.toJSONString(list))
    
            model.addAttribute("list", list)
            return "/testcaselist"
        }
    
    }
    
    

    前端代码(React jsx):

    import browser from 'webextension-polyfill'
    import {js_beautify as beautify} from 'js-beautify'
    import UpgradeProject from './migrate'
    import {FileTypes, migrateProject, migrateTestCase, migrateUrls, verifyFile,} from './legacy/migrate'
    import TestCase from '../models/TestCase'
    import UiState from '../stores/view/UiState'
    import PlaybackState from '../stores/view/PlaybackState'
    import ModalState from '../stores/view/ModalState'
    import Selianize, {ParseError} from 'selianize'
    import Manager from '../../plugin/manager'
    import chromeGetFile from './filesystem/chrome'
    import firefoxGetFile from './filesystem/firefox'
    import {userAgent as parsedUA} from '../../common/utils'
    import uuidv4 from 'uuid/v4'
    
    export const supportedFileFormats = '.side, text/html'
    
    export function getFile(path) {
      const browserName = parsedUA.browser.name
      return (() => {
        if (browserName === 'Chrome') {
          return chromeGetFile(path)
        } else if (browserName === 'Firefox') {
          return firefoxGetFile(path)
        } else {
          return Promise.reject(
            new Error('Operation is not supported in this browser')
          )
        }
      })().then(blob => {
        return new Promise(res => {
          const reader = new FileReader()
          reader.addEventListener('load', () => {
            res(reader.result)
          })
          reader.readAsDataURL(blob)
        })
      })
    }
    
    export function loadAsText(blob) {
      return new Promise(res => {
        const fileReader = new FileReader()
        fileReader.onload = e => {
          res(e.target.result)
        }
    
        fileReader.readAsText(blob)
      })
    }
    
    export function saveProject(_project) {
      const project = _project.toJS()
      downloadProject(project)
      UiState.saved()
    }
    
    /**
     * 原生 js 的 Ajax 函数
     * @type {{get: Ajax.get, post: Ajax.post}}
     */
    const Ajax = {
      get: function (url, fn) {
        // XMLHttpRequest对象用于在后台与服务器交换数据
        var xhr = new XMLHttpRequest()
        xhr.open('GET', url, true)
        xhr.onreadystatechange = function () {
          // readyState == 4说明请求已完成
          if ((xhr.readyState == 4 && xhr.status == 200) || xhr.status == 304) {
            // 从服务器获得数据
            fn.call(this, xhr.responseText)
          }
        }
        xhr.send()
      },
      // data应为'a=a1&b=b1'这种字符串格式,在jq里如果data为对象会自动将对象转成这种字符串格式
      post: function (url, data, fn) {
        var xhr = new XMLHttpRequest()
        xhr.open('POST', url, true)
        // 添加http头,发送信息至服务器时内容编码类型
        xhr.setRequestHeader('Content-Type', 'application/json')
        xhr.onreadystatechange = function () {
          if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
            fn.call(this, xhr.responseText)
          }
        }
        xhr.send(data)
      },
    }
    
    export function uploadProject(_project) {
      const project = _project.toJS()
      const sideJson = JSON.stringify(project)
      const token = uuidv4()
      const host = 'http://localhost:9000'
    
      let data = {
        name: project.name,
        sideJson: sideJson,
        token: token
      }
    
      console.log(`data===>${JSON.stringify(data)}`)
    
      Ajax.post(`${host}/uitestcase/upload.api`, JSON.stringify(data), res => {
        console.log(`${host}/uitestcase/upload.api result: ${res}`)
        ModalState.showAlert({
          title: '上传云端',
          description: `上传成功,前往小U平台查看用例:  ${host}/getTestCase?token=${token}`,
          confirmLabel: '确定',
          cancelLabel: '取消',
        }).then(choseConfirm => {
          console.log(`choseConfirm=${choseConfirm}`)
          if (choseConfirm) {
            window.open(`${host}/getTestCase?token=${token}`, '_blank')
          }
        })
      })
    }
    
    function downloadProject(project) {
      return exportProject(project).then(snapshot => {
        if (snapshot) {
          project.snapshot = snapshot
          Object.assign(project, Manager.emitDependencies())
        }
        return browser.downloads.download({
          filename: project.name + '.side',
          url: createBlob(
            'application/json',
            beautify(JSON.stringify(project), {indent_size: 2})
          ),
          saveAs: true,
          conflictAction: 'overwrite',
        })
      })
    }
    
    function exportProject(project) {
      return Manager.validatePluginExport(project).then(() => {
        return Selianize(project, {
          silenceErrors: true,
          skipStdLibEmitting: true,
        }).catch(err => {
          const markdown = ParseError((err && err.message) || err)
          ModalState.showAlert({
            title: 'Error saving project',
            description: markdown,
            confirmLabel: 'Download log',
            cancelLabel: 'Close',
          }).then(choseDownload => {
            if (choseDownload) {
              browser.downloads.download({
                filename: project.name + '-logs.md',
                url: createBlob('text/markdown', markdown),
                saveAs: true,
                conflictAction: 'overwrite',
              })
            }
          })
          return Promise.reject()
        })
      })
    }
    
    let previousFile = null
    
    // eslint-disable-next-line
    function createBlob(mimeType, data) {
      const blob = new Blob([data], {
        type: 'text/plain',
      })
      // If we are replacing a previously generated file we need to
      // manually revoke the object URL to avoid memory leaks.
      if (previousFile !== null) {
        window.URL.revokeObjectURL(previousFile)
      }
      previousFile = window.URL.createObjectURL(blob)
      return previousFile
    }
    
    export function loadProject(project, file) {
      function displayError(error) {
        ModalState.showAlert({
          title: 'Error migrating project',
          description: error.message,
          confirmLabel: 'Close',
        })
      }
    
      loadAsText(file).then(contents => {
        if (/\.side$/.test(file.name)) {
          loadJSProject(project, UpgradeProject(JSON.parse(contents)))
        } else {
          try {
            const type = verifyFile(contents)
            if (type === FileTypes.Suite) {
              ModalState.importSuite(contents, files => {
                try {
                  loadJSProject(project, migrateProject(files))
                } catch (error) {
                  displayError(error)
                }
              })
            } else if (type === FileTypes.TestCase) {
              let {test, baseUrl} = migrateTestCase(contents)
              if (project.urls.length && !project.urls.includes(baseUrl)) {
                ModalState.showAlert({
                  title: 'Migrate test case',
                  description: `The test case you're trying to migrate has a different base URL (${baseUrl}) than the project's one.  \nIn order to migrate the test case URLs will be made absolute.`,
                  confirmLabel: 'Migrate',
                  cancelLabel: 'Discard',
                }).then(choseMigration => {
                  if (choseMigration) {
                    UiState.selectTest(
                      project.addTestCase(
                        TestCase.fromJS(migrateUrls(test, baseUrl))
                      )
                    )
                  }
                })
              } else {
                UiState.selectTest(
                  project.addTestCase(TestCase.fromJS(test, baseUrl))
                )
              }
            }
          } catch (error) {
            displayError(error)
          }
        }
      })
    }
    
    export function loadJSProject(project, data) {
      UiState.changeView('Tests')
      PlaybackState.clearPlayingCache()
      UiState.clearViewCache()
      project.fromJS(data)
      UiState.projectChanged()
      Manager.emitMessage({
        action: 'event',
        event: 'projectLoaded',
        options: {
          projectName: project.name,
          projectId: project.id,
        },
      })
    }
    
    
    

    side runner 代码

    #!/usr/bin/env node
    
    // Licensed to the Software Freedom Conservancy (SFC) under one
    // or more contributor license agreements.  See the NOTICE file
    // distributed with this work for additional information
    // regarding copyright ownership.  The SFC licenses this file
    // to you under the Apache License, Version 2.0 (the
    // "License"); you may not use this file except in compliance
    // with the License.  You may obtain a copy of the License at
    //
    //   http://www.apache.org/licenses/LICENSE-2.0
    //
    // Unless required by applicable law or agreed to in writing,
    // software distributed under the License is distributed on an
    // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    // KIND, either express or implied.  See the License for the
    // specific language governing permissions and limitations
    // under the License.
    
    import fs from 'fs'
    import path from 'path'
    import crypto from 'crypto'
    import util from 'util'
    import { fork } from 'child_process'
    import program from 'commander'
    import winston from 'winston'
    import glob from 'glob'
    import rimraf from 'rimraf'
    import { js_beautify as beautify } from 'js-beautify'
    import Selianize, { getUtilsFile } from 'selianize'
    import Capabilities from './capabilities'
    import ParseProxy from './proxy'
    import Config from './config'
    import Satisfies from './versioner'
    import metadata from '../package.json'
    
    const DEFAULT_TIMEOUT = 15000
    
    process.title = metadata.name
    
    program
      .usage('[options] project.side [project.side] [*.side]')
      .version(metadata.version)
      .option('-c, --capabilities [list]', 'Webdriver capabilities')
      .option('-s, --server [url]', 'Webdriver remote server')
      .option('-p, --params [list]', 'General parameters')
      .option('-f, --filter [string]', 'Run suites matching name')
      .option(
        '-w, --max-workers [number]',
        'Maximum amount of workers that will run your tests, defaults to number of cores'
      )
      .option('--base-url [url]', 'Override the base URL that was set in the IDE')
      .option(
        '--timeout [number | undefined]',
        `The maximimum amount of time, in milliseconds, to spend attempting to locate an element. (default: ${DEFAULT_TIMEOUT})`
      )
      .option(
        '--proxy-type [type]',
        'Type of proxy to use (one of: direct, manual, pac, socks, system)'
      )
      .option(
        '--proxy-options [list]',
        'Proxy options to pass, for use with manual, pac and socks proxies'
      )
      .option(
        '--configuration-file [filepath]',
        'Use specified YAML file for configuration. (default: .side.yml)'
      )
      .option(
        '--output-directory [directory]',
        'Write test results to files, results written in JSON'
      )
      .option(
        '--force',
        "Forcibly run the project, regardless of project's version"
      )
      .option('--debug', 'Print debug logs')
    
    if (process.env.NODE_ENV === 'development') {
      program.option(
        '-e, --extract',
        'Only extract the project file to code (this feature is for debugging purposes)'
      )
      program.option(
        '-r, --run [directory]',
        'Run the extracted project files (this feature is for debugging purposes)'
      )
    }
    
    program.parse(process.argv)
    
    if (!program.args.length && !program.run) {
      program.outputHelp()
      process.exit(1)
    }
    
    winston.cli()
    winston.level = program.debug ? 'debug' : 'info'
    
    if (program.extract || program.run) {
      winston.warn(
        "This feature is used by iRecorder IDE maintainers for debugging purposes, we hope you know what you're doing!"
      )
    }
    
    const configuration = {
      capabilities: {
        browserName: 'chrome',
      },
      params: {},
      runId: crypto.randomBytes(16).toString('hex'),
      path: path.join(__dirname, '../../'),
    }
    
    const confPath = program.configurationFile || '.side.yml'
    const configurationFilePath = path.isAbsolute(confPath)
      ? confPath
      : path.join(process.cwd(), confPath)
    try {
      Object.assign(configuration, Config.load(configurationFilePath))
    } catch (e) {
      winston.debug('Could not load ' + configurationFilePath)
    }
    
    program.filter = program.filter || '*'
    configuration.server = program.server ? program.server : configuration.server
    
    configuration.timeout = program.timeout
      ? +program.timeout
      : configuration.timeout
        ? +configuration.timeout
        : DEFAULT_TIMEOUT // eslint-disable-line indent
    
    if (configuration.timeout === 'undefined') configuration.timeout = undefined
    
    if (program.capabilities) {
      try {
        configuration.capabilities = Capabilities.parseString(program.capabilities)
      } catch (e) {
        winston.debug('Failed to parse inline capabilities')
      }
    }
    
    if (program.params) {
      try {
        configuration.params = Capabilities.parseString(program.params)
      } catch (e) {
        winston.debug('Failed to parse additional params')
      }
    }
    
    if (program.proxyType) {
      try {
        let opts = program.proxyOptions
        if (program.proxyType === 'manual' || program.proxyType === 'socks') {
          opts = Capabilities.parseString(opts)
        }
        const proxy = ParseProxy(program.proxyType, opts)
        Object.assign(configuration, proxy)
      } catch (e) {
        winston.error(e.message)
        process.exit(1)
      }
    } else if (configuration.proxyType) {
      try {
        const proxy = ParseProxy(
          configuration.proxyType,
          configuration.proxyOptions
        )
        Object.assign(configuration, proxy)
      } catch (e) {
        winston.error(e.message)
        process.exit(1)
      }
    }
    
    configuration.baseUrl = program.baseUrl
      ? program.baseUrl
      : configuration.baseUrl
    
    winston.debug(util.inspect(configuration))
    
    let projectPath
    
    function runProject(project) {
      winston.info(`Running ${project.path}`)
      if (!program.force) {
        let warning
        try {
          warning = Satisfies(project.version, '2.0')
        } catch (e) {
          return Promise.reject(e)
        }
        if (warning) {
          winston.warn(warning)
        }
      } else {
        winston.warn("--force is set, ignoring project's version")
      }
      if (!project.suites.length) {
        return Promise.reject(
          new Error(
            `The project ${
              project.name
            } has no test suites defined, create a suite using the IDE.`
          )
        )
      }
      projectPath = `side-suite-${project.id}`
      rimraf.sync(projectPath)
      fs.mkdirSync(projectPath)
      fs.writeFileSync(
        path.join(projectPath, 'package.json'),
        JSON.stringify(
          {
            name: project.name,
            version: '0.0.0',
            jest: {
              modulePaths: [path.join(__dirname, '../node_modules')],
              setupTestFrameworkScriptFile: require.resolve(
                'jest-environment-selenium/dist/setup.js'
              ),
              testEnvironment: 'jest-environment-selenium',
              testEnvironmentOptions: configuration,
            },
            dependencies: project.dependencies || {},
          },
          null,
          2
        )
      )
    
      return Selianize(project, { silenceErrors: true }, project.snapshot).then(
        code => {
          const tests = code.tests
            .reduce((tests, test) => {
              return (tests += test.code)
            }, 'const utils = require("./utils.js");const tests = {};')
            .concat('module.exports = tests;')
          writeJSFile(path.join(projectPath, 'commons'), tests, '.js')
          writeJSFile(path.join(projectPath, 'utils'), getUtilsFile(), '.js')
          code.suites.forEach(suite => {
            if (!suite.tests) {
              // not parallel
              const cleanup = suite.persistSession
                ? ''
                : 'beforeEach(() => {vars = {};});afterEach(async () => (cleanup()));'
              writeJSFile(
                path.join(projectPath, suite.name),
                `// This file was generated using iRecorder (Jason Chimpanzee).\nconst tests = require("./commons.js");${
                  code.globalConfig
                }${suite.code}${cleanup}`
              )
            } else if (suite.tests.length) {
              fs.mkdirSync(path.join(projectPath, suite.name))
              // parallel suite
              suite.tests.forEach(test => {
                writeJSFile(
                  path.join(projectPath, suite.name, test.name),
                  `// This file was generated using iRecorder (Jason Chimpanzee).\nconst tests = require("../commons.js");${
                    code.globalConfig
                  }${test.code}`
                )
              })
            }
          })
    
          return new Promise((resolve, reject) => {
            let npmInstall
            if (project.dependencies && Object.keys(project.dependencies).length) {
              npmInstall = new Promise((resolve, reject) => {
                const child = fork(require.resolve('./npm'), {
                  cwd: path.join(process.cwd(), projectPath),
                  stdio: 'inherit',
                })
                child.on('exit', code => {
                  if (code) {
                    reject()
                  } else {
                    resolve()
                  }
                })
              })
            } else {
              npmInstall = Promise.resolve()
            }
            npmInstall
              .then(() => {
                if (program.extract) {
                  resolve()
                } else {
                  runJest(project) // main run
                    .then(resolve)
                    .catch(reject)
                }
              })
              .catch(reject)
          })
        }
      )
    }
    
    /**
     * run the auto generated jest test project
     * @param project
     * @returns {Promise}
     */
    function runJest(project) {
      return new Promise((resolve, reject) => {
        const args = [
          '--testMatch',
          `{**/*${program.filter}*/*.test.js,**/*${program.filter}*.test.js}`,
        ]
          .concat(program.maxWorkers ? ['-w', program.maxWorkers] : [])
          .concat(
            program.outputDirectory
              ? [
                  '--json',
                  '--outputFile',
                  path.isAbsolute(program.outputDirectory)
                    ? path.join(program.outputDirectory, `${project.name}.json`)
                    : '../' +
                      path.join(program.outputDirectory, `${project.name}.json`),
                ]
              : []
          )
        const opts = {
          cwd: path.join(process.cwd(), projectPath),
          stdio: 'inherit',
        }
        winston.debug('jest worker args')
        winston.debug(args)
        winston.debug('jest work opts')
        winston.debug(opts)
        const child = fork(require.resolve('./child'), args, opts)
    
        child.on('exit', code => {
          console.log('') // eslint-disable-line no-console
          // when is not running
          if (!program.run) {
            // uncomment this: keep the jest project files
            // rimraf.sync(projectPath) // delete the project files
          }
          if (code) {
            reject()
          } else {
            resolve()
          }
        })
      })
    }
    
    function runAll(projects, index = 0) {
      if (index >= projects.length) return Promise.resolve()
      return runProject(projects[index])
        .then(() => {
          return runAll(projects, ++index)
        })
        .catch(error => {
          process.exitCode = 1
          error && winston.error(error.message + '\n')
          return runAll(projects, ++index)
        })
    }
    
    function writeJSFile(name, data, postfix = '.test.js') {
      fs.writeFileSync(`${name}${postfix}`, beautify(data, { indent_size: 2 }))
    }
    
    const projects = [
      ...program.args.reduce((projects, project) => {
        glob.sync(project).forEach(p => {
          projects.add(p)
        })
        return projects
      }, new Set()),
    ].map(p => {
      const project = JSON.parse(fs.readFileSync(p))
      project.path = p
      return project
    })
    
    function handleQuit(_signal, code) {
      if (!program.run) {
        rimraf.sync(projectPath)
      }
      process.exit(code)
    }
    
    process.on('SIGINT', handleQuit)
    process.on('SIGTERM', handleQuit)
    
    if (program.run) {
      projectPath = program.run
      runJest({
        name: 'test',
      }).catch(winston.error)
    } else {
      runAll(projects)
    }
    
    

    相关文章

      网友评论

        本文标题:在 iRecorder (Web IDE , Web UI 自

        本文链接:https://www.haomeiwen.com/subject/dyhphqtx.html