Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdc73edce | ||
|
|
3d254fa075 | ||
|
|
ed1e43015e | ||
|
|
e6cbbd73f0 | ||
|
|
67dff7b38c | ||
|
|
ced67176a3 | ||
|
|
00ac8afe64 | ||
|
|
a6964c4495 | ||
|
|
6541291e0d | ||
|
|
711d274398 | ||
|
|
6c5861b9fc | ||
|
|
f7ab27f9fd | ||
|
|
e4e789cc5b | ||
|
|
09b525fe58 | ||
|
|
9dabfc1367 | ||
|
|
bdb733352a | ||
|
|
75a4655a0f | ||
|
|
051c1516a0 | ||
|
|
62c5b78358 | ||
|
|
290913d07a | ||
|
|
77534d650a | ||
|
|
a4c715e3f6 | ||
|
|
7415d3cee5 | ||
|
|
e1ba9c89fe | ||
|
|
0fcbe7369a | ||
|
|
c8087b5b63 | ||
|
|
065faca8eb | ||
|
|
bcd6a38a05 | ||
|
|
fa67f9b82b | ||
|
|
39acd0bd47 | ||
|
|
a629f287f0 |
@@ -32,5 +32,6 @@
|
||||
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
|
||||
- Noam Okman (noamokman@gmail.com)
|
||||
- PurgingPanda (t3ch0wn3r@gmail.com)
|
||||
- Kai Curtis (morecode@kcurtis.com)
|
||||
|
||||
#### Generated by bin/update-authors.sh.
|
||||
|
||||
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
# WebTorrent Desktop Version History
|
||||
|
||||
## v0.16.0 - 2016-09-18
|
||||
|
||||
### Added
|
||||
- **Windows 64-bit support!** ([#931](https://github.com/feross/webtorrent-desktop/pull/931))
|
||||
- Existing 32-bit users will update to 64-bit automatically in next release
|
||||
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
|
||||
|
||||
## v0.15.0 - 2016-09-16
|
||||
|
||||
### Added
|
||||
- Option to start automatically on login
|
||||
- Add integration tests
|
||||
- Add more detailed telemetry to diagnose "buffer allocation failed"
|
||||
|
||||
### Changed
|
||||
- Disable playback controls while in external player (#909)
|
||||
|
||||
### Fixed
|
||||
- Fix several uncaught errors (#889, #891, #892)
|
||||
- Update to the latest webtorrent.js, fixing some more uncaught errors
|
||||
- Clicking on the "torrent finished" notification works again (#912)
|
||||
|
||||
## v0.14.0 - 2016-09-03
|
||||
|
||||
### Added
|
||||
|
||||
22
README.md
@@ -56,6 +56,28 @@ Restart the app automatically every time code changes. Useful during development
|
||||
$ npm run watch
|
||||
```
|
||||
|
||||
### Run linters
|
||||
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
### Run integration tests
|
||||
|
||||
```
|
||||
$ npm run integration-test
|
||||
```
|
||||
|
||||
The integration tests use Spectron and Tape. They click through the app, taking screenshots and comparing each one to a reference. Why screenshots?
|
||||
|
||||
* Ad-hoc checking makes the tests a lot more work to write
|
||||
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't catch an bug where hitting ESC from a video doesn't correctly restore window size.
|
||||
* Chrome's own integration tests use screenshot diffing iirc
|
||||
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending screenshots and running the tests, which will recreate them with the new look.
|
||||
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! Ses https://github.com/blog/817-behold-image-view-modes
|
||||
|
||||
For MacOS, you'll need a Retina screen for the integration tests to pass.
|
||||
|
||||
### Package the app
|
||||
|
||||
Builds app binaries for Mac, Linux, and Windows.
|
||||
|
||||
109
bin/package.js
@@ -112,7 +112,7 @@ var darwin = {
|
||||
// Build for Mac
|
||||
platform: 'darwin',
|
||||
|
||||
// Build 64 bit binaries only.
|
||||
// Build x64 binaries only.
|
||||
arch: 'x64',
|
||||
|
||||
// The bundle identifier to use in the application's plist (Mac only).
|
||||
@@ -133,8 +133,8 @@ var win32 = {
|
||||
// Build for Windows.
|
||||
platform: 'win32',
|
||||
|
||||
// Build 32 bit binaries only.
|
||||
arch: 'ia32',
|
||||
// Build ia32 and x64 binaries.
|
||||
arch: 'all',
|
||||
|
||||
// Object hash of application metadata to embed into the executable (Windows only)
|
||||
'version-string': {
|
||||
@@ -167,7 +167,7 @@ var linux = {
|
||||
// Build for Linux.
|
||||
platform: 'linux',
|
||||
|
||||
// Build 32 and 64 bit binaries.
|
||||
// Build ia32 and x64 binaries.
|
||||
arch: 'all'
|
||||
|
||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||
@@ -388,19 +388,25 @@ function buildWin32 (cb) {
|
||||
}
|
||||
|
||||
var tasks = []
|
||||
if (argv.package === 'exe' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageInstaller(cb))
|
||||
}
|
||||
if (argv.package === 'portable' || argv.package === 'all') {
|
||||
tasks.push((cb) => packagePortable(cb))
|
||||
}
|
||||
buildPath.forEach(function (filesPath) {
|
||||
var destArch = filesPath.split('-').pop()
|
||||
|
||||
if (argv.package === 'exe' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
|
||||
}
|
||||
if (argv.package === 'portable' || argv.package === 'all') {
|
||||
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
|
||||
}
|
||||
})
|
||||
series(tasks, cb)
|
||||
|
||||
function packageInstaller (cb) {
|
||||
console.log('Windows: Creating installer...')
|
||||
function packageInstaller (filesPath, destArch, cb) {
|
||||
console.log(`Windows: Creating ${destArch} installer...`)
|
||||
|
||||
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
installer.createWindowsInstaller({
|
||||
appDirectory: buildPath[0],
|
||||
appDirectory: filesPath,
|
||||
authors: config.APP_TEAM,
|
||||
description: config.APP_NAME,
|
||||
exe: config.APP_NAME + '.exe',
|
||||
@@ -410,8 +416,21 @@ function buildWin32 (cb) {
|
||||
noMsi: true,
|
||||
outputDirectory: DIST_PATH,
|
||||
productName: config.APP_NAME,
|
||||
remoteReleases: config.GITHUB_URL,
|
||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
||||
/**
|
||||
* Only create delta updates for the Windows x64 build because 90% of our
|
||||
* users have Windows x64 and the delta files take a *very* long time to
|
||||
* generate. Also, the ia32 files on GitHub have non-standard Squirrel
|
||||
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
|
||||
* find them unless we proxy the requests.
|
||||
*/
|
||||
remoteReleases: destArch === 'x64'
|
||||
? config.GITHUB_URL
|
||||
: undefined,
|
||||
/**
|
||||
* If you hit a "GitHub API rate limit exceeded" error, set this token!
|
||||
*/
|
||||
remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
|
||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
|
||||
setupIcon: config.APP_ICON + '.ico',
|
||||
signWithParams: signWithParams,
|
||||
title: config.APP_NAME,
|
||||
@@ -419,23 +438,65 @@ function buildWin32 (cb) {
|
||||
version: pkg.version
|
||||
})
|
||||
.then(function () {
|
||||
console.log('Windows: Created installer.')
|
||||
console.log(`Windows: Created ${destArch} installer.`)
|
||||
|
||||
/**
|
||||
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
|
||||
* versions of the app)
|
||||
*/
|
||||
fs.readdirSync(DIST_PATH)
|
||||
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
|
||||
.forEach((filename) => {
|
||||
fs.unlinkSync(path.join(DIST_PATH, filename))
|
||||
})
|
||||
|
||||
if (destArch === 'ia32') {
|
||||
console.log('Windows: Renaming ia32 installer files...')
|
||||
|
||||
// RELEASES -> RELEASES-ia32
|
||||
var relPath = path.join(DIST_PATH, 'RELEASES-ia32')
|
||||
fs.renameSync(
|
||||
path.join(DIST_PATH, 'RELEASES'),
|
||||
relPath
|
||||
)
|
||||
|
||||
// WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg
|
||||
fs.renameSync(
|
||||
path.join(DIST_PATH, `${BUILD_NAME}-full.nupkg`),
|
||||
path.join(DIST_PATH, `${BUILD_NAME}-ia32-full.nupkg`)
|
||||
)
|
||||
|
||||
// Change file name inside RELEASES-ia32 to match renamed file
|
||||
var relContent = fs.readFileSync(relPath, 'utf8')
|
||||
var relContent32 = relContent.replace(/full\.nupkg$/, '-ia32-full.nupkg')
|
||||
fs.writeFileSync(relPath, relContent32)
|
||||
|
||||
if (relContent === relContent32) {
|
||||
// Sanity check
|
||||
throw new Error('Fixing RELEASE-ia32 failed. Replacement did not modify the file.')
|
||||
}
|
||||
|
||||
console.log('Windows: Renamed ia32 installer files.')
|
||||
}
|
||||
|
||||
cb(null)
|
||||
})
|
||||
.catch(cb)
|
||||
}
|
||||
|
||||
function packagePortable (cb) {
|
||||
console.log('Windows: Creating portable app...')
|
||||
function packagePortable (filesPath, destArch, cb) {
|
||||
console.log(`Windows: Creating ${destArch} portable app...`)
|
||||
|
||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
||||
var portablePath = path.join(filesPath, 'Portable Settings')
|
||||
mkdirp.sync(portablePath)
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log('Windows: Created portable app.')
|
||||
console.log(`Windows: Created ${destArch} portable app.`)
|
||||
cb(null)
|
||||
}
|
||||
})
|
||||
@@ -500,8 +561,10 @@ function buildLinux (cb) {
|
||||
// Create .zip file for Linux
|
||||
console.log(`Linux: Creating ${destArch} zip...`)
|
||||
|
||||
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log(`Linux: Created ${destArch} zip.`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"author": {
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@webtorrent.io",
|
||||
@@ -18,12 +18,13 @@
|
||||
"application-config": "^1.0.0",
|
||||
"auto-launch": "^4.0.1",
|
||||
"bitfield": "^1.0.2",
|
||||
"capture-frame": "^1.0.0",
|
||||
"chromecasts": "^1.8.0",
|
||||
"create-torrent": "^3.24.5",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dlnacasts": "^0.1.0",
|
||||
"drag-drop": "^2.12.1",
|
||||
"electron": "1.3.5",
|
||||
"electron": "1.4.0",
|
||||
"es6-error": "^3.0.1",
|
||||
"fs-extra": "^0.30.0",
|
||||
"iso-639-1": "^1.2.1",
|
||||
@@ -65,6 +66,7 @@
|
||||
"nodemon": "^1.10.2",
|
||||
"open": "0.0.5",
|
||||
"plist": "^2.0.1",
|
||||
"pngjs": "^3.0.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"run-series": "^1.1.4",
|
||||
"spectron": "^3.3.0",
|
||||
|
||||
@@ -81,6 +81,8 @@ module.exports = {
|
||||
IS_PRODUCTION: IS_PRODUCTION,
|
||||
IS_TEST: IS_TEST,
|
||||
|
||||
OS_SYSARCH: is64BitOperatingSystem() ? 'x64' : 'ia32',
|
||||
|
||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||
ROOT_PATH: path.join(__dirname, '..'),
|
||||
STATIC_PATH: path.join(__dirname, '..', 'static'),
|
||||
@@ -149,3 +151,32 @@ function isProduction () {
|
||||
return !/\/electron$/.test(process.execPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operating system's CPU architecture. This is different than
|
||||
* `process.arch` which returns the architecture the binary was compiled for.
|
||||
*
|
||||
* On Windows, the most reliable way to detect a 64-bit OS from within a 32-bit
|
||||
* app is based on the presence of a WOW64 file: %SystemRoot%\SysNative.
|
||||
*
|
||||
* Background: https://twitter.com/feross/status/776949077208510464
|
||||
*/
|
||||
function is64BitOperatingSystem () {
|
||||
// This is a 64-bit binary, so the OS clearly supports 64-bit apps
|
||||
if (process.arch === 'x64') return true
|
||||
|
||||
let useEnv = false
|
||||
try {
|
||||
useEnv = !!(process.env.SYSTEMROOT && fs.statSync(process.env.SYSTEMROOT))
|
||||
} catch (err) {}
|
||||
|
||||
let sysRoot = useEnv ? process.env.SYSTEMROOT : 'C:\\Windows'
|
||||
|
||||
// If %SystemRoot%\SysNative exists, we are in a WOW64 FS Redirected application.
|
||||
let isWOW64 = false
|
||||
try {
|
||||
isWOW64 = !!fs.statSync(path.join(sysRoot, 'sysnative'))
|
||||
} catch (err) {}
|
||||
|
||||
return isWOW64
|
||||
}
|
||||
|
||||
@@ -21,12 +21,7 @@ function openSeedFile () {
|
||||
title: 'Select a file for the torrent.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
showOpenSeed(opts)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -46,12 +41,7 @@ function openSeedDirectory () {
|
||||
title: 'Select a folder for the torrent.',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
showOpenSeed(opts)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -119,3 +109,16 @@ function setTitle (title) {
|
||||
function resetTitle () {
|
||||
windows.main.dispatch('resetTitle')
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops up an Open File dialog with the given options.
|
||||
* After the user selects files / folders, shows the Create Torrent page.
|
||||
*/
|
||||
function showOpenSeed (opts) {
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,8 +147,14 @@ function onAppOpen (newArgv) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading args.
|
||||
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
|
||||
// Development: 2 args, eg: electron .
|
||||
// Test: 4 args, eg: electron -r .../mocks.js .
|
||||
function sliceArgv (argv) {
|
||||
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
|
||||
return argv.slice(config.IS_PRODUCTION ? 1
|
||||
: config.IS_TEST ? 4
|
||||
: 2)
|
||||
}
|
||||
|
||||
function processArgv (argv) {
|
||||
|
||||
@@ -98,14 +98,16 @@ function init () {
|
||||
/**
|
||||
* File handlers
|
||||
*/
|
||||
|
||||
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||
if (flag) handlers.install()
|
||||
else handlers.uninstall()
|
||||
})
|
||||
|
||||
/**
|
||||
* Startup
|
||||
* Auto start on login
|
||||
*/
|
||||
|
||||
ipc.on('setStartup', (e, flag) => {
|
||||
if (flag) startup.install()
|
||||
else startup.uninstall()
|
||||
@@ -143,7 +145,10 @@ function init () {
|
||||
|
||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||
|
||||
// Capture all events
|
||||
/**
|
||||
* Message passing
|
||||
*/
|
||||
|
||||
const oldEmit = ipc.emit
|
||||
ipc.emit = function (name, e, ...args) {
|
||||
// Relay messages between the main window and the WebTorrent hidden window
|
||||
|
||||
@@ -11,7 +11,8 @@ const windows = require('./windows')
|
||||
|
||||
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||
'?version=' + config.APP_VERSION +
|
||||
'&platform=' + process.platform
|
||||
'&platform=' + process.platform +
|
||||
'&sysarch=' + config.OS_SYSARCH
|
||||
|
||||
function init () {
|
||||
if (process.platform === 'linux') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react')
|
||||
|
||||
const FlatButton = require('material-ui/FlatButton').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
|
||||
class ShowMore extends React.Component {
|
||||
static get propTypes () {
|
||||
@@ -39,9 +39,10 @@ class ShowMore extends React.Component {
|
||||
? this.props.hideLabel
|
||||
: this.props.showLabel
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<div className='show-more' style={this.props.style}>
|
||||
{this.state.expanded ? this.props.children : null}
|
||||
<FlatButton
|
||||
<RaisedButton
|
||||
className='control'
|
||||
onClick={this.handleClick}
|
||||
label={label} />
|
||||
</div>
|
||||
|
||||
@@ -215,11 +215,41 @@ module.exports = class TorrentListController {
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Save Torrent File As...',
|
||||
click: () => saveTorrentFileAs(torrentSummary)
|
||||
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey)
|
||||
}))
|
||||
|
||||
menu.popup(electron.remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
// Takes a torrentSummary or torrentKey
|
||||
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
||||
saveTorrentFileAs (torrentKey) {
|
||||
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
||||
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
|
||||
const downloadPath = this.state.saved.prefs.downloadPath
|
||||
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||
const win = electron.remote.getCurrentWindow()
|
||||
const opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
}
|
||||
|
||||
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
||||
if (!savePath) return // They clicked Cancel
|
||||
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||
if (err) return dispatch('error', err)
|
||||
fs.writeFile(savePath, torrentFile, function (err) {
|
||||
if (err) return dispatch('error', err)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively finds {name, path, size} for all files in a folder
|
||||
@@ -280,27 +310,3 @@ function moveItemToTrash (torrentSummary) {
|
||||
function showItemInFolder (torrentSummary) {
|
||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||
}
|
||||
|
||||
function saveTorrentFileAs (torrentSummary) {
|
||||
const downloadPath = this.state.saved.prefs.downloadPath
|
||||
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||
const opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
}
|
||||
const win = electron.remote.getCurrentWindow()
|
||||
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||
if (!savePath) return // They clicked Cancel
|
||||
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||
if (err) return dispatch('error', err)
|
||||
fs.writeFile(savePath, torrentFile, function (err) {
|
||||
if (err) return dispatch('error', err)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
module.exports = captureVideoFrame
|
||||
|
||||
const {IllegalArgumentError} = require('./errors')
|
||||
|
||||
function captureVideoFrame (video, format) {
|
||||
if (typeof video === 'string') {
|
||||
video = document.querySelector(video)
|
||||
}
|
||||
|
||||
if (video == null || video.nodeName !== 'VIDEO') {
|
||||
throw new IllegalArgumentError('First argument must be a <video> element or selector')
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
format = 'png'
|
||||
}
|
||||
|
||||
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
||||
throw new IllegalArgumentError('Second argument must be one of "png", "jpg", or "webp"')
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
|
||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||
|
||||
const dataUri = canvas.toDataURL('image/' + format)
|
||||
const data = dataUri.split(',')[1]
|
||||
|
||||
return new Buffer(data, 'base64')
|
||||
}
|
||||
@@ -33,6 +33,15 @@ function init (appState, callback) {
|
||||
state = appState
|
||||
update = callback
|
||||
|
||||
// Don't actually cast during integration tests
|
||||
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
|
||||
if (config.IS_TEST) {
|
||||
state.devices.chromecast = testPlayer('chromecast')
|
||||
state.devices.airplay = testPlayer('airplay')
|
||||
state.devices.dlna = testPlayer('dlna')
|
||||
return
|
||||
}
|
||||
|
||||
// Load modules, scan the network for devices
|
||||
airplayer = require('airplayer')()
|
||||
chromecasts = require('chromecasts')()
|
||||
@@ -58,6 +67,32 @@ function init (appState, callback) {
|
||||
})
|
||||
}
|
||||
|
||||
// integration test player implementation
|
||||
function testPlayer (type) {
|
||||
return {
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
|
||||
function getDevices () {
|
||||
return [{name: type + '-1'}, {name: type + '-2'}]
|
||||
}
|
||||
|
||||
function open () {}
|
||||
function play () {}
|
||||
function pause () {}
|
||||
function stop () {}
|
||||
function status () {}
|
||||
function seek () {}
|
||||
function volume () {}
|
||||
}
|
||||
|
||||
// chromecast player implementation
|
||||
function chromecastPlayer () {
|
||||
const ret = {
|
||||
|
||||
@@ -35,10 +35,6 @@ class TorrentKeyNotFoundError extends TorrentError {
|
||||
|
||||
class InvalidTorrentError extends TorrentError {}
|
||||
|
||||
/* Miscellaneous */
|
||||
|
||||
class IllegalArgumentError extends ExtendableError {}
|
||||
|
||||
module.exports = {
|
||||
CastingError,
|
||||
PlaybackError,
|
||||
@@ -49,6 +45,5 @@ module.exports = {
|
||||
PlaybackTimedOutError,
|
||||
InvalidSoundNameError,
|
||||
TorrentKeyNotFoundError,
|
||||
InvalidTorrentError,
|
||||
IllegalArgumentError
|
||||
InvalidTorrentError
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ function getSystemInfo () {
|
||||
osPlatform: process.platform,
|
||||
osRelease: os.type() + ' ' + os.release(),
|
||||
architecture: os.arch(),
|
||||
systemArchitecture: config.OS_SYSARCH,
|
||||
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
||||
numCores: os.cpus().length
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = torrentPoster
|
||||
|
||||
const captureVideoFrame = require('./capture-video-frame')
|
||||
const captureFrame = require('capture-frame')
|
||||
const path = require('path')
|
||||
|
||||
function torrentPoster (torrent, cb) {
|
||||
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
||||
function onSeeked () {
|
||||
video.removeEventListener('seeked', onSeeked)
|
||||
|
||||
const buf = captureVideoFrame(video)
|
||||
const buf = captureFrame(video)
|
||||
|
||||
// unload video element
|
||||
video.pause()
|
||||
|
||||
@@ -194,6 +194,8 @@ const dispatchHandlers = {
|
||||
controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||
'startTorrentingSummary': (torrentKey) =>
|
||||
controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||
'saveTorrentFileAs': (torrentKey) =>
|
||||
controllers.torrentList.saveTorrentFileAs(torrentKey),
|
||||
|
||||
// Playback
|
||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||
|
||||
@@ -99,14 +99,14 @@ class CreateTorrentPage extends React.Component {
|
||||
</ShowMore>
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
className='control'
|
||||
className='control cancel'
|
||||
label='Cancel'
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
onClick={dispatcher('cancel')} />
|
||||
<RaisedButton
|
||||
className='control'
|
||||
className='control create-torrent'
|
||||
label='Create Torrent'
|
||||
primary
|
||||
onClick={this.handleSubmit} />
|
||||
|
||||
@@ -6,6 +6,7 @@ const zeroFill = require('zero-fill')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const Playlist = require('../lib/playlist')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||
module.exports = class Player extends React.Component {
|
||||
@@ -585,6 +586,8 @@ function renderPlayerControls (state) {
|
||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||
// can be 'spongey' / non-contiguous
|
||||
function renderLoadingBar (state) {
|
||||
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
|
||||
|
||||
const torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (!torrentSummary.progress) {
|
||||
return []
|
||||
|
||||
@@ -54,11 +54,18 @@ const VERSION_STR = VERSION.match(/([0-9]+)/g)
|
||||
*/
|
||||
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
||||
|
||||
/**
|
||||
* Generate an ephemeral peer ID each time.
|
||||
* Once there are around 2^24 = ~8 million WebTorrent Desktops online at the same time,
|
||||
* ID collisions will start happening. Birthday paradox.
|
||||
* This is fine, though. Bad peers can already clone someone else's peer ID.
|
||||
* The network is robust to occasional collisions.
|
||||
*/
|
||||
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
|
||||
|
||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||
// client, as explained here: https://webtorrent.io/faq
|
||||
const client = window.client = new WebTorrent({
|
||||
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
|
||||
})
|
||||
let client = window.client = new WebTorrent({ peerId: PEER_ID })
|
||||
|
||||
// WebTorrent-to-HTTP streaming sever
|
||||
let server = null
|
||||
@@ -69,8 +76,7 @@ let prevProgress = null
|
||||
init()
|
||||
|
||||
function init () {
|
||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||
listenToClientEvents()
|
||||
|
||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||
@@ -101,6 +107,11 @@ function init () {
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
function listenToClientEvents () {
|
||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||
}
|
||||
|
||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
||||
// Returns a WebTorrent object. See https://git.io/vik9M
|
||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||
@@ -157,7 +168,7 @@ function addTorrentEvents (torrent) {
|
||||
function torrentReady () {
|
||||
const info = getTorrentInfo(torrent)
|
||||
ipc.send('wt-ready', torrent.key, info)
|
||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
|
||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||
|
||||
updateTorrentProgress()
|
||||
}
|
||||
@@ -395,3 +406,17 @@ function getTorrent (torrentKey) {
|
||||
function onError (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
// TODO: remove this once the following bugs are fixed:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
|
||||
// https://github.com/electron/electron/issues/7212
|
||||
window.testOfflineMode = function () {
|
||||
console.log('Test, going OFFLINE')
|
||||
client = window.client = new WebTorrent({
|
||||
peerId: PEER_ID,
|
||||
tracker: false,
|
||||
dht: false,
|
||||
webSeeds: false
|
||||
})
|
||||
listenToClientEvents()
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<p>
|
||||
Version <script>document.write(require('../package.json').version)</script>
|
||||
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
||||
(<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
|
||||
</p>
|
||||
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||
</body>
|
||||
|
||||
14
test/config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const path = require('path')
|
||||
|
||||
const TEST_DIR = path.join(__dirname, 'tempTestData')
|
||||
const TEST_DIR_DOWNLOAD = path.join(TEST_DIR, 'Downloads')
|
||||
const TEST_DIR_DESKTOP = path.join(TEST_DIR, 'Desktop')
|
||||
|
||||
module.exports = {
|
||||
TORRENT_FILES: [path.join(__dirname, 'resources', '1.torrent')],
|
||||
SEED_FILES: [path.join(TEST_DIR_DESKTOP, 'tmp.jpg')],
|
||||
SAVED_TORRENT_FILE: path.join(TEST_DIR_DESKTOP, 'saved.torrent'),
|
||||
TEST_DIR,
|
||||
TEST_DIR_DOWNLOAD,
|
||||
TEST_DIR_DESKTOP
|
||||
}
|
||||
@@ -1,33 +1,20 @@
|
||||
const test = require('tape')
|
||||
const fs = require('fs-extra')
|
||||
const setup = require('./setup')
|
||||
|
||||
console.log('Creating download dir: ' + setup.TEST_DOWNLOAD_DIR)
|
||||
fs.mkdirpSync(setup.TEST_DOWNLOAD_DIR)
|
||||
|
||||
test.onFinish(function () {
|
||||
console.log('Removing test dir: ' + setup.TEST_DATA_DIR)
|
||||
fs.removeSync(setup.TEST_DATA_DIR) // includes download dir
|
||||
})
|
||||
test.onFinish(setup.deleteTestDataDir)
|
||||
|
||||
test('app runs', function (t) {
|
||||
t.timeoutAfter(10e3)
|
||||
setup.resetTestDataDir()
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-basic'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'app-basic'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
console.log('Testing the torrent list (home page)...')
|
||||
setup.wipeTestDataDir()
|
||||
require('./test-torrent-list')
|
||||
|
||||
// TODO:
|
||||
// require('./test-add-torrent')
|
||||
// require('./test-create-torrent')
|
||||
// require('./test-prefs')
|
||||
// require('./test-video')
|
||||
// require('./test-audio')
|
||||
// require('./test-cast')
|
||||
require('./test-add-torrent')
|
||||
require('./test-video')
|
||||
require('./test-audio')
|
||||
|
||||
15
test/mocks.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const electron = require('electron')
|
||||
const config = require('./config')
|
||||
|
||||
console.log('Mocking electron.dialog.showOpenDialog...')
|
||||
electron.dialog.showOpenDialog = function (win, opts, cb) {
|
||||
const ret = /select.*torrent file/i.test(opts.title)
|
||||
? config.TORRENT_FILES
|
||||
: config.SEED_FILES
|
||||
cb(ret)
|
||||
}
|
||||
|
||||
console.log('Mocking electron.remote.dialog.showSaveDialog...')
|
||||
electron.dialog.showSaveDialog = function (win, opts, cb) {
|
||||
cb(config.SAVED_TORRENT_FILE)
|
||||
}
|
||||
BIN
test/resources/1.torrent
Normal file
BIN
test/resources/expected-single-file.torrent
Normal file
BIN
test/resources/m3.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
test/resources/monitor-test.mp4
Normal file
BIN
test/screenshots/darwin/add-torrent-0-percent.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/add-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/create-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
test/screenshots/darwin/create-torrent-advanced.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
test/screenshots/darwin/create-torrent-simple.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
test/screenshots/darwin/play-torrent-bbb.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
test/screenshots/darwin/play-torrent-return.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-2.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-3.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-4.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-5.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-fullscreen.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/play-torrent-wired-list.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/play-torrent-wired.png
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-delete-data.png
Normal file
|
After Width: | Height: | Size: 701 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-deleted.png
Normal file
|
After Width: | Height: | Size: 932 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
101
test/setup.js
@@ -1,20 +1,21 @@
|
||||
const path = require('path')
|
||||
const Application = require('spectron').Application
|
||||
const fs = require('fs-extra')
|
||||
|
||||
const TEST_DATA_DIR = path.join(__dirname, 'tempTestData')
|
||||
const TEST_DOWNLOAD_DIR = path.join(TEST_DATA_DIR, 'Downloads')
|
||||
const parseTorrent = require('parse-torrent')
|
||||
const PNG = require('pngjs').PNG
|
||||
const config = require('./config')
|
||||
|
||||
module.exports = {
|
||||
TEST_DATA_DIR,
|
||||
TEST_DOWNLOAD_DIR,
|
||||
createApp,
|
||||
endTest,
|
||||
screenshotCreateOrCompare,
|
||||
compareDownloadFolder,
|
||||
compareFiles,
|
||||
compareTorrentFiles,
|
||||
waitForLoad,
|
||||
wait,
|
||||
wipeTestDataDir
|
||||
resetTestDataDir,
|
||||
deleteTestDataDir
|
||||
}
|
||||
|
||||
// Runs WebTorrent Desktop.
|
||||
@@ -24,7 +25,7 @@ function createApp (t) {
|
||||
return new Application({
|
||||
path: path.join(__dirname, '..', 'node_modules', '.bin',
|
||||
'electron' + (process.platform === 'win32' ? '.cmd' : '')),
|
||||
args: [path.join(__dirname, '..')],
|
||||
args: ['-r', path.join(__dirname, 'mocks.js'), path.join(__dirname, '..')],
|
||||
env: {NODE_ENV: 'test'}
|
||||
})
|
||||
}
|
||||
@@ -35,10 +36,8 @@ function waitForLoad (app, t, opts) {
|
||||
return app.start().then(function () {
|
||||
return app.client.waitUntilWindowLoaded()
|
||||
}).then(function () {
|
||||
// Offline mode? Disable internet in the webtorrent window
|
||||
// TODO. For now, just run integration tests with internet turned off.
|
||||
// Spectron is poorly documented, and contrary to the docs, webContents.session is missing
|
||||
// That is the correct API (in theory) to put the app in offline mode
|
||||
// Offline mode
|
||||
if (opts.offline) app.webContents.executeJavaScript('testOfflineMode()')
|
||||
}).then(function () {
|
||||
// Switch to the main window. Index 0 is apparently the hidden webtorrent window...
|
||||
return app.client.windowByIndex(1)
|
||||
@@ -52,9 +51,9 @@ function waitForLoad (app, t, opts) {
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a promise that resolves after 'ms' milliseconds. Default: 500
|
||||
// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
|
||||
function wait (ms) {
|
||||
if (ms === undefined) ms = 500 // Default: wait long enough for the UI to update
|
||||
if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
@@ -80,7 +79,7 @@ function screenshotCreateOrCompare (app, t, name) {
|
||||
console.log('Saving screenshot ' + ssPath)
|
||||
fs.writeFileSync(ssPath, buffer)
|
||||
} else {
|
||||
const match = Buffer.compare(buffer, ssBuf) === 0
|
||||
const match = compareIgnoringTransparency(buffer, ssBuf)
|
||||
t.ok(match, 'screenshot comparison ' + name)
|
||||
if (!match) {
|
||||
const ssFailedPath = path.join(ssDir, name + '-failed.png')
|
||||
@@ -91,21 +90,81 @@ function screenshotCreateOrCompare (app, t, name) {
|
||||
})
|
||||
}
|
||||
|
||||
// Resets the test directory, containing config.json, torrents, downloads, etc
|
||||
function wipeTestDataDir () {
|
||||
fs.removeSync(TEST_DATA_DIR)
|
||||
fs.mkdirpSync(TEST_DOWNLOAD_DIR) // Downloads/ is inside of TEST_DATA_DIR
|
||||
// Compares two PNGs, ignoring any transparent regions in bufExpected.
|
||||
// Returns true if they match.
|
||||
function compareIgnoringTransparency (bufActual, bufExpected) {
|
||||
// Common case: exact byte-for-byte match
|
||||
if (Buffer.compare(bufActual, bufExpected) === 0) return true
|
||||
|
||||
// Otherwise, compare pixel by pixel
|
||||
const pngA = PNG.sync.read(bufActual)
|
||||
const pngE = PNG.sync.read(bufExpected)
|
||||
if (pngA.width !== pngE.width || pngA.height !== pngE.height) return false
|
||||
const w = pngA.width
|
||||
const h = pngE.height
|
||||
const da = pngA.data
|
||||
const de = pngE.data
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4
|
||||
if (de[i + 3] === 0) continue // Skip transparent pixels
|
||||
if (da[i] !== de[i] || da[i + 1] !== de[i + 1] || da[i + 2] !== de[i + 2]) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Resets the test directory, containing config.json, torrents, downloads, etc
|
||||
function resetTestDataDir () {
|
||||
fs.removeSync(config.TEST_DIR)
|
||||
// Create TEST_DIR as well as /Downloads and /Desktop
|
||||
fs.mkdirpSync(config.TEST_DIR_DOWNLOAD)
|
||||
fs.mkdirpSync(config.TEST_DIR_DESKTOP)
|
||||
}
|
||||
|
||||
function deleteTestDataDir () {
|
||||
fs.removeSync(config.TEST_DIR)
|
||||
}
|
||||
|
||||
// Checks a given folder under Downloads.
|
||||
// Makes sure that the filenames match exactly.
|
||||
// If `filenames` is null, asserts that the folder doesn't exist.
|
||||
function compareDownloadFolder (t, dirname, filenames) {
|
||||
const dirpath = path.join(TEST_DOWNLOAD_DIR, dirname)
|
||||
const dirpath = path.join(config.TEST_DIR_DOWNLOAD, dirname)
|
||||
try {
|
||||
const actualFilenames = fs.readdirSync(dirpath)
|
||||
const expectedSorted = filenames.slice().sort()
|
||||
const actualSorted = actualFilenames.slice().sort()
|
||||
t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
t.equal(filenames, null, 'download folder missing: ' + dirname)
|
||||
if (e.code === 'ENOENT') {
|
||||
t.equal(filenames, null, 'download folder missing: ' + dirname)
|
||||
} else {
|
||||
console.error(e)
|
||||
t.fail('unexpected error getting download folder: ' + dirname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Makes sure two files have identical contents
|
||||
function compareFiles (t, pathActual, pathExpected) {
|
||||
const bufActual = fs.readFileSync(pathActual)
|
||||
const bufExpected = fs.readFileSync(pathExpected)
|
||||
const match = Buffer.compare(bufActual, bufExpected) === 0
|
||||
t.ok(match, 'correct contents: ' + pathActual)
|
||||
}
|
||||
|
||||
// Makes sure two torrents have the same infohash and flags
|
||||
function compareTorrentFiles (t, pathActual, pathExpected) {
|
||||
const bufActual = fs.readFileSync(pathActual)
|
||||
const bufExpected = fs.readFileSync(pathExpected)
|
||||
const fieldsActual = extractImportantFields(parseTorrent(bufActual))
|
||||
const fieldsExpected = extractImportantFields(parseTorrent(bufExpected))
|
||||
t.deepEqual(fieldsActual, fieldsExpected, 'torrent contents: ' + pathActual)
|
||||
}
|
||||
|
||||
function extractImportantFields (parsedTorrent) {
|
||||
const { infoHash, name, announce, urlList, comment } = parsedTorrent
|
||||
const priv = parsedTorrent.private // private is a reserved word in JS
|
||||
return { infoHash, name, announce, urlList, comment, 'private': priv }
|
||||
}
|
||||
|
||||
69
test/test-add-torrent.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const test = require('tape')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const setup = require('./setup')
|
||||
const config = require('./config')
|
||||
|
||||
test('add-torrent', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
t.timeoutAfter(30e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Add an existing torrent. The corresponding file is not present. Should be at 0%
|
||||
.then(() => app.electron.ipcRenderer.send('openTorrentFile'))
|
||||
// The call to dialog.openFiles() is mocked. See mocks.js
|
||||
.then(() => app.client.waitUntilTextExists('m3.jpg'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'add-torrent-0-percent'))
|
||||
// Delete the torrent.
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.icon.delete'))
|
||||
.then(() => app.client.waitUntilTextExists('REMOVE'))
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
// Add the same existing torrent, this time with the file present. Should be at 100%
|
||||
.then(() => fs.copySync(
|
||||
path.join(__dirname, 'resources', 'm3.jpg'),
|
||||
path.join(config.TEST_DIR_DOWNLOAD, 'm3.jpg')))
|
||||
.then(() => app.electron.ipcRenderer.send('openTorrentFile'))
|
||||
.then(() => app.client.waitUntilTextExists('m3.jpg'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'add-torrent-100-percent'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
test('create-torrent', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
// Set up the files to seed
|
||||
fs.copySync(path.join(__dirname, 'resources', 'm3.jpg'), config.SEED_FILES[0])
|
||||
|
||||
t.timeoutAfter(30e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Click the + button, open a non-torrent file to seed
|
||||
.then(() => app.client.click('.icon.add'))
|
||||
.then(() => app.client.waitUntilTextExists('Create'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-simple'))
|
||||
// Click to show advanced settings
|
||||
.then(() => app.client.click('.show-more .control'))
|
||||
.then(() => app.client.waitUntilTextExists('Comment'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-advanced'))
|
||||
// Click OK to create the torrent
|
||||
.then(() => app.client.click('.control.create-torrent'))
|
||||
.then(() => app.client.waitUntilTextExists('tmp.jpg'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-100-percent'))
|
||||
// Click "Save Torrent File As..." on the new torrent
|
||||
.then(() => app.webContents.executeJavaScript(
|
||||
'dispatch("saveTorrentFileAs", 6)'))
|
||||
.then(() => setup.wait())
|
||||
// Mock saves to <temp folder>/Desktop/saved.torrent
|
||||
.then(() => setup.compareTorrentFiles(t,
|
||||
config.SAVED_TORRENT_FILE,
|
||||
path.join(__dirname, 'resources', 'expected-single-file.torrent')))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
59
test/test-audio.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const test = require('tape')
|
||||
const setup = require('./setup')
|
||||
|
||||
test('audio-streaming', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
t.timeoutAfter(60e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {online: true})
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Play Wired CD. Wait for it to start streaming.
|
||||
.then(() => app.client.moveToObject('#torrent-wired'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('#torrent-wired .icon.play'))
|
||||
.then(() => app.client.waitUntilTextExists('The Wired CD'))
|
||||
// Pause. Skip to two seconds in. Wait another two seconds for it to load.
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.waitUntilTextExists('Artist'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired'))
|
||||
// Click next
|
||||
.then(() => app.client.click('.skip-next'))
|
||||
.then(() => app.client.waitUntilTextExists('The Wired CD'))
|
||||
.then(() => app.client.moveToObject('.letterbox'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-2'))
|
||||
// Play from end of song, let it advance on its own
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 206)'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => setup.wait(5e3)) // Let it play a few seconds, past the end of the song
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-3'))
|
||||
// Fullscreen
|
||||
.then(() => app.client.click('.fullscreen'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-fullscreen'))
|
||||
// Back to normal audio view. Give the player controls have had time to disappear.
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("escapeBack")'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-4'))
|
||||
// Back. Return to torrent list
|
||||
.then(() => app.client.click('.back'))
|
||||
.then(() => app.client.waitUntilTextExists('Big Buck Bunny'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-list'))
|
||||
// Forward. Should play again where we left off (should not stay paused)
|
||||
.then(() => app.client.click('.forward'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-5'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
const test = require('tape')
|
||||
const fs = require('fs-extra')
|
||||
const setup = require('./setup')
|
||||
const config = require('./config')
|
||||
|
||||
test.skip('torrent-list: show download path missing', function (t) {
|
||||
setup.wipeTestDataDir()
|
||||
fs.removeSync(setup.TEST_DOWNLOAD_DIR)
|
||||
test('torrent-list: show download path missing', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
fs.removeSync(config.TEST_DIR_DOWNLOAD)
|
||||
|
||||
t.timeoutAfter(10e3)
|
||||
const app = setup.createApp()
|
||||
@@ -23,8 +24,8 @@ test.skip('torrent-list: show download path missing', function (t) {
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
test.skip('torrent-list: start, stop, and delete torrents', function (t) {
|
||||
setup.wipeTestDataDir()
|
||||
test('torrent-list: start, stop, and delete torrents', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {offline: true})
|
||||
@@ -35,7 +36,7 @@ test.skip('torrent-list: start, stop, and delete torrents', function (t) {
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
|
||||
// Click download on the first torrent, start downloading
|
||||
.then(() => app.client.click('.icon.download'))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB'))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', '276MB'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download'))
|
||||
// Click download on the first torrent again, stop downloading
|
||||
.then(() => app.client.click('.icon.download'))
|
||||
@@ -62,7 +63,7 @@ test.skip('torrent-list: start, stop, and delete torrents', function (t) {
|
||||
})
|
||||
|
||||
test('torrent-list: expand torrent, unselect file', function (t) {
|
||||
setup.wipeTestDataDir()
|
||||
setup.resetTestDataDir()
|
||||
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {offline: true})
|
||||
@@ -81,7 +82,7 @@ test('torrent-list: expand torrent, unselect file', function (t) {
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-deselect'))
|
||||
// Start the torrent
|
||||
.then(() => app.client.click('#torrent-cosmos .icon.download'))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', '0%'))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'peers'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-start'))
|
||||
// Make sure that it creates all files EXCEPT the deslected one
|
||||
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', [
|
||||
@@ -97,6 +98,16 @@ test('torrent-list: expand torrent, unselect file', function (t) {
|
||||
'CosmosLaundromatFirstCycle_meta.sqlite',
|
||||
'CosmosLaundromatFirstCycle_meta.xml'
|
||||
]))
|
||||
// Delete torrent plus data
|
||||
// Spectron doesn't have proper support for menu clicks yet...
|
||||
.then(() => app.webContents.executeJavaScript(
|
||||
'dispatch("confirmDeleteTorrent", "6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5", true)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-delete-data'))
|
||||
// Click confirm
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-deleted'))
|
||||
// Make sure that all the files are gone
|
||||
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', null))
|
||||
.then(() => setup.endTest(app, t),
|
||||
|
||||
36
test/test-video.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const test = require('tape')
|
||||
const setup = require('./setup')
|
||||
|
||||
test('video-streaming', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
t.timeoutAfter(30e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {online: true})
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Play Big Buck Bunny. Wait for it to start streaming.
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.icon.play'))
|
||||
.then(() => setup.wait(10e3))
|
||||
// Pause. Skip to two seconds in. Wait another two seconds for it to load.
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait(5e3))
|
||||
// Take a screenshot to verify video playback
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-bbb'))
|
||||
// Hit escape
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("escapeBack")'))
|
||||
.then(() => setup.wait())
|
||||
// Delete Big Buck Bunny
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.icon.delete'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
.then(() => setup.wait())
|
||||
// Take another screenshot to verify that the window resized correctly
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-return'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||