Merge branch 'master' of github.com:cypress-io/cypress-monorepo

This commit is contained in:
Chris Breiding
2017-05-25 09:35:40 -04:00
13 changed files with 282 additions and 241 deletions

View File

@@ -1,12 +1,7 @@
import {log} from './log'
import {find, map} from 'lodash'
import cp = require('child_process')
import {BrowserNotFoundError} from './types'
type FoundBrowser = {
name: string,
path?: string
}
import {Browser, FoundBrowser, BrowserNotFoundError} from './types'
const browserNotFoundErr = (browsers: FoundBrowser[], name: string): BrowserNotFoundError => {
const available = map(browsers, 'name').join(', ')
@@ -17,6 +12,46 @@ const browserNotFoundErr = (browsers: FoundBrowser[], name: string): BrowserNotF
return err
}
const googleChromeStable: Browser = {
name: 'Google Chrome Stable',
versionRegex: /Google Chrome (\S+)/,
profile: true,
binary: 'google-chrome-stable'
}
const googleChromeAlias: Browser = {
name: 'Google Chrome',
versionRegex: /Google Chrome (\S+)/,
profile: true,
binary: 'chrome'
}
/** list of all browsers we can detect and use */
export const browsers: Browser[] = [
{
name: 'chrome',
displayName: 'Chrome',
versionRegex: /Google Chrome (\S+)/,
profile: true,
binary: 'google-chrome'
},{
name: 'chromium',
displayName: 'Chromium',
versionRegex: /Chromium (\S+)/,
profile: true,
binary: 'chromium-browser'
},{
name: 'canary',
displayName: 'Canary',
versionRegex: /Google Chrome Canary (\S+)/,
profile: true,
binary: 'google-chrome-canary'
},
// a couple of fallbacks
googleChromeStable,
googleChromeAlias
]
/** starts a browser by name and opens URL if given one */
export function launch (browsers: FoundBrowser[],
name: string, url?: string, args: string[] = []) {

View File

@@ -1,22 +0,0 @@
import {parse, find} from './util'
import path = require('path')
import Promise = require('bluebird')
const canary = {
version: (p: string) =>
parse(p, 'KSVersion'),
path: () => find('com.google.Chrome.canary'),
get (executable: string) {
return this.path()
.then (p => {
return Promise.props({
path: path.join(p, executable),
version: this.version(p)
})
})
}
}
export default canary

View File

@@ -1,28 +0,0 @@
import {log} from '../log'
import {parse, find} from './util'
import path = require('path')
import Promise = require('bluebird')
const chrome = {
version (p: string) {
return parse(p, 'KSVersion')
},
path () {
return find('com.google.Chrome')
},
get (executable: string) {
log('Looking for Chrome %s', executable)
return this.path()
.then(p => {
return Promise.props({
path: path.join(p, executable),
version: this.version(p)
})
})
}
}
export default chrome

View File

@@ -1,25 +0,0 @@
import {find, parse} from './util'
import path = require('path')
import Promise = require('bluebird')
const chromium = {
version (p: string) {
return parse(p, 'CFBundleShortVersionString')
},
path () {
return find('org.chromium.Chromium')
},
get (executable: string) {
return this.path()
.then(p =>
Promise.props({
path: path.join(p, executable),
version: this.version(p)
})
)
}
}
export default chromium

View File

@@ -1,11 +1,40 @@
import canary from './canary'
import chrome from './chrome'
import chromium from './chromium'
import {findApp} from './util'
import {Browser} from '../types'
import {detectBrowserLinux} from '../linux'
import {log} from '../log'
import {merge, partial} from 'ramda'
const browsers = {
chrome,
canary,
chromium
const detectCanary = partial(findApp,
['Contents/MacOS/Google Chrome Canary', 'com.google.Chrome.canary', 'KSVersion'])
const detectChrome = partial(findApp,
['Contents/MacOS/Google Chrome', 'com.google.Chrome', 'KSVersion'])
const detectChromium = partial(findApp,
['Contents/MacOS/Chromium', 'org.chromium.Chromium', 'CFBundleShortVersionString'])
type Detectors = {
[index: string]: Function
}
export default browsers
const browsers: Detectors = {
chrome: detectChrome,
canary: detectCanary,
chromium: detectChromium
}
export function detectBrowserDarwin (browser: Browser) {
let fn = browsers[browser.name]
if (!fn) {
// ok, maybe it is custom alias?
log('detecting custom browser %s on darwin', browser.name)
return detectBrowserLinux(browser)
}
return fn()
.then(merge({name: browser.name}))
.catch(() => {
log('could not detect %s using traditional Mac methods', browser.name)
log('trying linux search')
return detectBrowserLinux(browser)
})
}

View File

@@ -7,7 +7,8 @@ import fs = require('fs-extra')
import path = require('path')
import plist = require('plist')
export function parse (p: string, property: string) {
/** parses Info.plist file from given application and returns a property */
export function parse (p: string, property: string): Promise<string> {
const pl = path.join(p, 'Contents', 'Info.plist')
log('reading property file "%s"', pl)
@@ -26,7 +27,8 @@ export function parse (p: string, property: string) {
.catch(failed)
}
export function find (id: string): Promise<string> {
/** uses mdfind to find app using Ma app id like 'com.google.Chrome.canary' */
export function mdfind (id: string): Promise<string> {
const cmd = `mdfind 'kMDItemCFBundleIdentifier=="${id}"' | head -1`
log('looking for bundle id %s using command: %s', id, cmd)
@@ -47,3 +49,43 @@ export function find (id: string): Promise<string> {
.then(tap(logFound))
.catch(failedToFind)
}
export type AppInfo = {
path: string,
version: string
}
function formApplicationPath (executable: string) {
const parts = executable.split('/')
const name = parts[parts.length - 1]
const appName = `${name}.app`
return path.join('/Applications', appName)
}
/** finds an application and its version */
export function findApp (executable: string, appId: string, versionProperty: string): Promise<AppInfo> {
log('looking for app %s id %s', executable, appId)
const findVersion = (foundPath: string) =>
parse(foundPath, versionProperty)
.then((version) => {
return {
path: path.join(foundPath, executable),
version
}
})
const tryMdFind = () => {
return mdfind(appId)
.then(findVersion)
}
const tryFullApplicationFind = () => {
const applicationPath = formApplicationPath(executable)
log('looking for application %s', applicationPath)
return findVersion(applicationPath)
}
return tryMdFind()
.catch(tryFullApplicationFind)
}

View File

@@ -1,35 +1,13 @@
import {linuxBrowser} from './linux'
import darwin from './darwin'
import {detectBrowserLinux} from './linux'
import {detectBrowserDarwin} from './darwin'
import {log} from './log'
import {Browser, NotInstalledError} from './types'
import {browsers} from './browsers'
import * as Bluebird from 'bluebird'
import {merge, pick, tap} from 'ramda'
import {merge, pick, tap, uniqBy, prop} from 'ramda'
import _ = require('lodash')
import os = require('os')
// import Promise = require('bluebird')
const browsers: Browser[] = [
{
name: 'chrome',
re: /Google Chrome (\S+)/,
profile: true,
binary: 'google-chrome',
executable: 'Contents/MacOS/Google Chrome'
},{
name: 'chromium',
re: /Chromium (\S+)/,
profile: true,
binary: 'chromium-browser',
executable: 'Contents/MacOS/Chromium'
},{
name: 'canary',
re: /Google Chrome Canary (\S+)/,
profile: true,
binary: 'google-chrome-canary',
executable: 'Contents/MacOS/Google Chrome Canary'
}
]
const setMajorVersion = (obj: Browser) => {
if (obj.version) {
@@ -40,33 +18,29 @@ const setMajorVersion = (obj: Browser) => {
return obj
}
type MacBrowserName = 'chrome' | 'chromium' | 'canary'
type BrowserDetector = (browser: Browser) => Promise<Object>
type Detectors = {
[index: string]: BrowserDetector
}
const detectors: Detectors = {
darwin: detectBrowserDarwin,
linux: detectBrowserLinux
}
function lookup (platform: string, obj: Browser): Promise<Object> {
function lookup (platform: NodeJS.Platform, obj: Browser): Promise<Object> {
log('looking up %s on %s platform', obj.name, platform)
switch (platform) {
case 'darwin':
const browserName: MacBrowserName = obj.name as MacBrowserName
const fn = darwin[browserName]
if (fn) {
return fn.get(obj.executable) as any as Promise<Object>
}
const err: NotInstalledError =
new Error(`Browser not installed: ${obj.name}`) as NotInstalledError
err.notInstalled = true
throw err
case 'linux':
return linuxBrowser.get(obj.binary, obj.re) as any as Promise<Object>
default:
throw new Error(`Cannot lookup browser ${obj.name} on ${platform}`)
const detector = detectors[platform]
if (!detector) {
throw new Error(`Cannot lookup browser ${obj.name} on ${platform}`)
}
return detector(obj)
}
function checkOneBrowser (browser: Browser) {
const platform = os.platform()
const pickBrowserProps = pick(['name', 'type', 'version', 'path'])
const pickBrowserProps = pick(['name', 'displayName', 'type', 'version', 'path'])
const logBrowser = (props: object) => {
const logBrowser = (props: any) => {
log('setting major version for %j', props)
}
@@ -88,8 +62,12 @@ function checkOneBrowser (browser: Browser) {
/** returns list of detected browsers */
function detectBrowsers (): Bluebird<Browser[]> {
// we can detect same browser under different aliases
// tell them apart by the full version property
const removeDuplicates = uniqBy(prop('version'))
return Bluebird.mapSeries(browsers, checkOneBrowser)
.then(_.compact) as Bluebird<Browser[]>
.then(_.compact)
.then(removeDuplicates) as Bluebird<Browser[]>
}
export default detectBrowsers

View File

@@ -1,30 +1,40 @@
import cp = require('child_process')
import Promise = require('bluebird')
import {NotInstalledError} from '../types'
const execAsync = Promise.promisify(cp.exec)
import {log} from '../log'
import {prop, trim} from 'ramda'
import {FoundBrowser, Browser, NotInstalledError} from '../types'
import execa = require('execa')
const notInstalledErr = (name: string) => {
const err: NotInstalledError = new Error(`Browser not installed: ${name}`) as NotInstalledError
const err: NotInstalledError =
new Error(`Browser not installed: ${name}`) as NotInstalledError
err.notInstalled = true
throw err
}
export const linuxBrowser = {
get: (binary: string, re: RegExp): Promise<any> => {
return execAsync(`${binary} --version`)
.call('trim')
.then (stdout => {
const m = re.exec(stdout)
if (m) {
return {
path: binary,
version: m[1]
}
} else {
return notInstalledErr(binary)
}
})
.catch(() => notInstalledErr(binary))
function getLinuxBrowser (name: string, binary: string, versionRegex: RegExp): Promise<FoundBrowser> {
const getVersion = (stdout: string) => {
const m = versionRegex.exec(stdout)
if (m) {
return m[1]
}
return notInstalledErr(binary)
}
const cmd = `${binary} --version`
log('looking using command "%s"', cmd)
return execa.shell(cmd)
.then(prop('stdout'))
.then(trim)
.then(getVersion)
.then((version) => {
return {
name,
version,
path: binary
}
})
.catch(() => notInstalledErr(binary))
}
export function detectBrowserLinux (browser: Browser) {
return getLinuxBrowser(browser.name, browser.binary, browser.versionRegex)
}

View File

@@ -1,14 +1,27 @@
/** TODO this are typical browser names, not just Mac */
export type MacBrowserName = 'chrome' | 'chromium' | 'canary' | string
export type PlatformName = 'darwin' | 'linux'
export type Browser = {
name: string,
re: RegExp,
/** short browser name */
name: MacBrowserName,
/** Optional display name */
displayName?: string,
/** RegExp to use to extract version from something like "Google Chrome 58.0.3029.110" */
versionRegex: RegExp,
profile: boolean,
binary: string,
executable: string,
version?: string,
majorVersion?: string,
page?: string
}
export type FoundBrowser = {
name: string,
path?: string
}
interface ExtraLauncherMethods {
update: Function,
detect: Function

View File

@@ -32,7 +32,7 @@
"mocha": "^2.4.5",
"sinon": "^1.17.3",
"sinon-chai": "^2.8.0",
"tslint": "5.2.0",
"tslint": "5.3.2",
"tslint-config-standard": "5.0.2",
"typescript": "2.3.2"
},

View File

@@ -244,6 +244,35 @@ module.exports = {
openProject.launch(browser, spec, browserOpts)
listenForProjectEnd: (project) ->
new Promise (resolve) ->
## dont ever end if we're in 'gui' debugging mode
return if gui
onEarlyExit = (errMsg) ->
## probably should say we ended
## early too: (Ended Early: true)
## in the stats
obj = {
error: errors.stripAnsi(errMsg)
failures: 1
tests: 0
passes: 0
pending: 0
duration: 0
failingTests: []
}
resolve(obj)
onEnd = (obj) =>
resolve(obj)
## when our project fires its end event
## resolve the promise
project.once("end", onEnd)
project.once("exitEarlyWithErr", onEarlyExit)
waitForBrowserToConnect: (options = {}) ->
{ project, id, timeout } = options
@@ -295,65 +324,37 @@ module.exports = {
waitForTestsToFinishRunning: (options = {}) ->
{ project, gui, screenshots, started, end, name, cname, videoCompression } = options
new Promise (resolve, reject) =>
## dont ever end if we're in 'gui' debugging mode
return if gui
@listenForProjectEnd(project)
.then (obj) =>
finish = ->
project
.getConfig()
.then (cfg) ->
obj.config = cfg
.return(obj)
onFinish = (obj) =>
finish = ->
project
.getConfig()
.then (cfg) ->
obj.config = cfg
.finally ->
resolve(obj)
if end
obj.video = name
if end
obj.video = name
if screenshots
obj.screenshots = screenshots
if screenshots
obj.screenshots = screenshots
@displayStats(obj)
@displayStats(obj)
if screenshots and screenshots.length
@displayScreenshots(screenshots)
if screenshots and screenshots.length
@displayScreenshots(screenshots)
ft = obj.failingTests
ft = obj.failingTests
if ft and ft.length
obj.failingTests = Reporter.setVideoTimestamp(started, ft)
if ft and ft.length
obj.failingTests = Reporter.setVideoTimestamp(started, ft)
if end
@postProcessRecording(end, name, cname, videoCompression)
.then(finish)
## TODO: add a catch here
else
finish()
onEarlyExit = (errMsg) ->
## probably should say we ended
## early too: (Ended Early: true)
## in the stats
obj = {
error: errors.stripAnsi(errMsg)
failures: 1
tests: 0
passes: 0
pending: 0
duration: 0
failingTests: []
}
onFinish(obj)
onEnd = (obj) =>
onFinish(obj)
## when our project fires its end event
## resolve the promise
project.once("end", onEnd)
project.once("exitEarlyWithErr", onEarlyExit)
if end
@postProcessRecording(end, name, cname, videoCompression)
.then(finish)
## TODO: add a catch here
else
finish()
trashAssets: (options = {}) ->
if options.trashAssetsBeforeHeadlessRuns is true

View File

@@ -23,6 +23,8 @@ module.exports = {
ended = Promise.pending()
done = false
errored = false
written = false
logErrors = true
wantsWrite = true
skipped = 0
@@ -31,15 +33,20 @@ module.exports = {
})
end = ->
pt.end()
done = true
if errored
errored.recordingVideoFailed = true
Promise.reject(errored)
else
Promise.resolve(ended.promise)
if not written
## when no data has been written this will
## result in an 'pipe:0: End of file' error
## for ffmpeg so we need to account for that
## and not log errors to the console
logErrors = false
pt.end()
## return the ended promise which will eventually
## get resolve or rejected
return ended.promise
write = (data) ->
## make sure we haven't ended
@@ -48,6 +55,9 @@ module.exports = {
## finishing the actual video
return if done
## we have written at least 1 byte
written = true
if wantsWrite
if not wantsWrite = pt.write(data)
pt.once "drain", ->
@@ -64,28 +74,26 @@ module.exports = {
.inputOptions("-use_wallclock_as_timestamps 1")
.videoCodec("libx264")
.outputOptions("-preset ultrafast")
.save(name)
.on "start", (line) ->
# console.log "spawned ffmpeg", line
console.log "spawned ffmpeg", line
started.resolve(new Date)
# .on "codecData", (data) ->
# .on "codecData", (data) ->
# console.log "codec data", data
# .on("error", options.onError)
# .on("error", options.onError)
.on "error", (err, stdout, stderr) ->
options.onError(err, stdout, stderr)
## if we're supposed log errors then
## bubble them up
if logErrors
options.onError(err, stdout, stderr)
err.recordingVideoFailed = true
## reject the ended promise
ended.reject(err)
errored = err
# ended.reject(err)
# console.log "error occured here", arguments
## TODO: call into lib/errors here
# console.log "ffmpeg failed", err
# ended.reject(err)
.on "end", ->
ended.resolve()
# setTimeout ->
# cmd.kill()
# , 1000
.save(name)
return {
cmd: cmd

View File

@@ -199,7 +199,7 @@ describe "lib/cypress", ->
beforeEach ->
@sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync()
@sandbox.stub(headless, "waitForSocketConnection")
@sandbox.stub(headless, "waitForTestsToFinishRunning").resolves({failures: 0})
@sandbox.stub(headless, "listenForProjectEnd").resolves({failures: 0})
@sandbox.stub(browsers, "open")
@sandbox.stub(git, "_getRemoteOrigin").resolves("remoteOrigin")
@@ -234,7 +234,7 @@ describe "lib/cypress", ->
@expectExitWith(0)
it "runs project headlessly and exits with exit code 10", ->
headless.waitForTestsToFinishRunning.resolves({failures: 10})
headless.listenForProjectEnd.resolves({failures: 10})
Project.add(@todosPath)
.then =>
@@ -609,7 +609,7 @@ describe "lib/cypress", ->
describe "--port", ->
beforeEach ->
headless.waitForTestsToFinishRunning.resolves({failures: 0})
headless.listenForProjectEnd.resolves({failures: 0})
Project.add(@todosPath)
@@ -641,7 +641,7 @@ describe "lib/cypress", ->
process.env = _.omit(process.env, "CYPRESS_DEBUG")
headless.waitForTestsToFinishRunning.resolves({failures: 0})
headless.listenForProjectEnd.resolves({failures: 0})
Project.add(@todosPath)