├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bin ├── clean ├── clean-appium ├── create-appium ├── install ├── login ├── project_files │ ├── SpotifyPlugin-Prefix.pch │ └── xcode │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── SpotifyPlugin.xcscheme ├── shared │ └── env.js ├── test-all ├── test-appium ├── test-ios └── test-js ├── install.sh ├── package.json ├── plugin.xml ├── src └── ios │ ├── Spotify.framework │ └── .gitkeep │ ├── SpotifyAudioPlayer.h │ ├── SpotifyAudioPlayer.m │ ├── SpotifyPlugin.h │ └── SpotifyPlugin.m ├── test ├── appium │ ├── specs │ │ ├── audio-player.js │ │ ├── specs.js │ │ └── support │ │ │ ├── appium-servers.js │ │ │ ├── logging.js │ │ │ └── setup.js │ ├── src │ │ └── ios │ │ │ ├── AutomationCoreAudioController.h │ │ │ ├── AutomationCoreAudioController.m │ │ │ ├── SpotifyAudioPlayer+Testing.h │ │ │ └── SpotifyAudioPlayer+Testing.m │ └── www │ │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ └── main.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ │ └── index.html ├── ios │ ├── MockCommandDelegate.h │ ├── MockCommandDelegate.m │ ├── SpotifyAudioPlayer+Mock.h │ ├── SpotifyAudioPlayer+Mock.m │ ├── SpotifyPluginTests-Info.plist │ ├── SpotifyPluginTests.m │ ├── TestData │ │ ├── profile.json │ │ ├── refresh.json │ │ ├── refresh_failed.json │ │ └── session.json │ ├── testShared.h │ └── testShared.m └── js │ ├── index.html │ └── spec │ ├── lib │ ├── event-dispatcher.test.js │ ├── remote.test.js │ └── utils.test.js │ ├── shared │ └── paginate.js │ ├── spotify │ ├── audio-player.test.js │ └── auth.test.js │ └── support │ ├── android │ └── .gitkeep │ ├── ios │ └── mock-exec.js │ └── test-helper.js └── www ├── lib ├── event-dispatcher.js ├── remote.js └── utils.js ├── spotify ├── audio-player.js ├── auth.js └── request.js └── vendors └── reqwest ├── reqwest.js └── reqwest.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | *.xcodeproj 20 | 21 | # Node.js 22 | node_modules 23 | 24 | # Dev Dependencies 25 | .env 26 | .tmp 27 | /SpotifyPlugin-Prefix.pch 28 | development/ 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | before_install: 3 | - gem install xcpretty --no-rdoc --no-ri --no-document --quiet 4 | install: 5 | - make prepare 6 | script: 7 | - make test-ios 8 | - make test-js 9 | after_script: 10 | - make clean 11 | env: 12 | global: 13 | - secure: bELi+wLvamcuB1w7IPY6k0cT4puuHjXA0qbNF3O7HKkk76XJFYMSGNcUnkncNJwvLL4/sF3yZROVjWj344yaaNLD6vSa0qMyNukiHEZ2KpTdv1IyfAjTdYJZ/58lJL6RW61ZJzQERRTF2zh2cE7gMlgZxqqHCTwoUOqd/wHMHEk= 14 | - secure: F7N5pyuRObdwDaS220E9/4nOjJPuXsrOkwdNa9Xy8VGHGOK4weoL2siv4KtzPGGoYqq36b9guiN4B5WOlDRBbuM3xmCsFFlE9GcN2uExAWoXaUKV9xzEGT/l/VVpdWu+bXOtvDRw/6uloWOX4Ti+gBPyREbEyIcgk5E7/gZ2wEs= 15 | - secure: anNBboQbuuPoGQgsbEex8y7ZqSryKnc2dGVpb36RA72xzye0YnmR6bfwM2k9nwqeWtDXONjyVjcfpM3WeHtw5xlwXo0GNDlZhEjvRWqKpxm7c10zjlLAd1IFTcICfGHo7ZoLjjWYDZQhoFMXEHw0otGxY9wVB2cWN8mrEqk/whI= 16 | - secure: DYsS+fkR3z+uPifoICq64cP3RtDZT/A07/aTzjqCM/fTm5fZuZ7YEAsYlGZNA0l4ym+lAi0xeizr5vI0zKK2UVnSKoqXQiKkD8TTycSdJyfJlNpHFxpYEAwEkNVy7LqArvNOclqMFkNBKcUK+nHCQEcmZ+pQgmHJnHN0VW+tcxc= 17 | - secure: gs4UxLMyBg+wiYR8idsgyHHNv+qH/HmPzUcAarxKRXiqvanf+PU8HACcYvigBvSLtjbugKSZGITF2M/RvqtVPOaY8/Syc92UwqWSawfylkMH4qZGIjslPs1tOMC6aDXv3ilIn5/NySpY0h5JYnLOQNpduqvwH4s7+Fpaw7v8eTI= 18 | - SAUCE=1 19 | - SPOTIFY_SCOPE="playlist-read-private playlist-modify-public playlist-modify-private streaming user-library-read user-library-modify user-read-private user-read-email" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tim Flapper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @export PLUGIN_ENV='test'; bin/test-all 3 | 4 | test-ios: 5 | @bin/test-ios 6 | 7 | test-js: 8 | @export PLUGIN_ENV='test'; bin/test-js 9 | 10 | test-appium: 11 | @bin/test-appium 12 | 13 | prepare: npm development 14 | 15 | clean: 16 | @bin/clean 17 | @bin/clean-appium 18 | 19 | npm: 20 | @npm install 21 | 22 | appium: 23 | @bin/create-appium 24 | 25 | development: 26 | @bin/install 27 | 28 | .PHONY: test 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordova Spotify Plugin 2 | 3 | [![Build Status](https://travis-ci.org/timflapper/cordova-spotify-plugin.svg?branch=master)](https://travis-ci.org/timflapper/cordova-spotify-plugin) 4 | 5 | This plugin provides a javascript API to Spotify's iOS SDK for Cordova applications. 6 | 7 | _Android integration is planned for a future release_ 8 | 9 | ## Installation 10 | 11 | 1. Install the plugin: 12 | 13 | `cordova plugin add com.timflapper.spotify` 14 | 15 | 2. Add the iOS platform to your project (if needed): 16 | 17 | `cordova platform add ios` 18 | 19 | 3. The install script will start automatically. It will do two things: 20 | - Ask you for a [custom URL scheme](http://bit.ly/1u11ZUz). 21 | - Download and extract the Spotify iOS SDK. 22 | 23 | That's it! 24 | 25 | ## API 26 | 27 | Documentation can be found [here](https://github.com/timflapper/cordova-plugin-spotify/wiki/API) 28 | 29 | ## Setting up a token exchange service 30 | 31 | You can use the Ruby script that is included in the Spotify iOS SDK Demo Projects for development: 32 | 33 | - [Download the Spotify iOS SDK](https://github.com/spotify/ios-sdk/releases) 34 | - Follow the instructions from the [Spotify iOS SDK beginner's tutorial](https://developer.spotify.com/technologies/spotify-ios-sdk/tutorial/). 35 | 36 | 37 | ## Non-interactive installation 38 | 39 | To avoid being prompted for the [custom URL scheme](http://bit.ly/1u11ZUz), 40 | you can alternatively provide it in an environment variable: 41 | ``` 42 | export CORDOVA_SPOTIFY_URL_SCHEME=somecustomscheme 43 | cordova plugin add com.timflapper.spotify 44 | ``` 45 | 46 | 47 | ## License 48 | 49 | [MIT](LICENSE) 50 | -------------------------------------------------------------------------------- /bin/clean: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | , path = require('path') 5 | , shell = require('shelljs'); 6 | 7 | var projectDir = path.resolve(__dirname, '..'); 8 | var devDir = path.join(projectDir, 'development'); 9 | var xcodeProjTarget = path.join(projectDir, 'SpotifyPlugin.xcodeproj'); 10 | 11 | shell.rm('-rf', devDir); 12 | shell.rm('-rf', xcodeProjTarget); 13 | shell.rm(path.join(projectDir, 'SpotifyPlugin-Prefix.pch')); 14 | -------------------------------------------------------------------------------- /bin/clean-appium: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | , path = require('path') 5 | , shell = require('shelljs'); 6 | 7 | var tmpDir = path.join(__dirname, '..', 'tmp'); 8 | 9 | shell.rm('-rf', tmpDir); 10 | -------------------------------------------------------------------------------- /bin/create-appium: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cordova = require('cordova') 4 | , env = require('./shared/env') 5 | , fs = require('fs') 6 | , path = require('path') 7 | , shell = require('shelljs'); 8 | 9 | env.loadEnvVariables(path.join(process.cwd(), '.env')); 10 | 11 | var workingDir = process.cwd() 12 | , pluginDir = path.join(__dirname, '..') 13 | , testPluginDir = path.join(pluginDir, 'test', 'appium') 14 | , tmpDir = path.join(pluginDir, '.tmp') 15 | , tmpAppDir = path.join(tmpDir, 'TestApp') 16 | , tmpPluginDir = path.join(tmpDir, 'SpotifyPlugin') 17 | , wwwDir = path.join(tmpAppDir, 'www') 18 | , zipFilePath = path.join(tmpDir, 'AcceptanceTest.zip'); 19 | 20 | var accessToken = shell.exec('SPOTIFY_USERNAME='+process.env.SPOTIFY_USERNAME + ' SPOTIFY_PASSWORD=' + process.env.SPOTIFY_PASSWORD + ' SPOTIFY_CLIENTID=' + process.env.SPOTIFY_CLIENTID + ' ' + path.join(__dirname, 'login'), {silent:true}).output; 21 | 22 | var expirationDate = (new Date().getTime()+3600).toString() 23 | , origIndexHTML = fs.readFileSync(path.join(testPluginDir, 'www', 'index.html'), {encoding: 'utf8'}) 24 | , testIndexHTML = origIndexHTML 25 | .replace('###USERNAME###', process.env.SPOTIFY_USERNAME) 26 | .replace('###CREDENTIAL###', accessToken) 27 | .replace('###EXPIRATION_DATE###', expirationDate); 28 | 29 | var testOnlyPluginXMLTags = ' \n' + 30 | ' \n' + 31 | ' \n' + 32 | ' \n' 33 | , originalPluginXML = fs.readFileSync(path.join(pluginDir, 'plugin.xml'), {encoding: 'utf8'}) 34 | , testPluginXML = originalPluginXML.replace( 35 | '\n', 36 | '\n' + 37 | testOnlyPluginXMLTags); 38 | 39 | shell.rm('-rf', tmpDir); 40 | shell.mkdir('-p', tmpAppDir); 41 | shell.mkdir('-p', tmpPluginDir); 42 | 43 | fs.writeFileSync(path.join(tmpPluginDir, 'plugin.xml'), testPluginXML); 44 | 45 | shell.cp('-rf', path.join(pluginDir, 'www'), tmpPluginDir); 46 | shell.cp('-rf', path.join(pluginDir, 'src'), tmpPluginDir); 47 | shell.cp('-f', path.join(testPluginDir, 'src', 'ios', '*'), path.join(tmpPluginDir, 'src', 'ios')); 48 | 49 | cordova.raw.create(tmpAppDir, 'com.timflapper.spotify.acceptance-test', 'AcceptanceTest') 50 | .then(function() { 51 | shell.rm('-rf', path.join(wwwDir, '*')); 52 | shell.cp('-rf', path.join(testPluginDir, 'www', '*'), wwwDir); 53 | 54 | fs.writeFileSync(path.join(wwwDir, 'index.html'), testIndexHTML); 55 | shell.cd(tmpAppDir); 56 | }) 57 | .then(function() { 58 | return cordova.raw.plugin('add', tmpPluginDir); 59 | }) 60 | .then(function() { 61 | shell.cp('-rf', path.join(pluginDir, 'development', 'Spotify.framework'), path.join(tmpAppDir, 'plugins', 'com.timflapper.spotify', 'src', 'ios')); 62 | return cordova.raw.platform('add', 'ios'); 63 | }) 64 | .then(function() { 65 | return cordova.raw.build({platforms: ['ios']}); 66 | }) 67 | .then(function() { 68 | if (process.env.SAUCE) { 69 | shell.cd('platforms/ios/build/emulator'); 70 | shell.exec('zip -r '+zipFilePath+' AcceptanceTest.app/', {silent: true}); 71 | shell.exec('curl -u '+process.env.SAUCE_USERNAME+':'+process.env.SAUCE_ACCESS_KEY+' -X POST "http://saucelabs.com/rest/v1/storage/'+process.env.SAUCE_USERNAME+'/AcceptanceTest.zip?overwrite=true" -H "Content-Type: application/octet-stream" --data-binary @'+zipFilePath, { silent: true }); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | , fs = require('fs') 5 | , request = require('request') 6 | , shell = require('shelljs') 7 | , zlib = require('zlib') 8 | , tar = require('tar'); 9 | 10 | var projectDir = path.resolve(__dirname, '..'); 11 | 12 | var devDir = path.join(projectDir, 'development'); 13 | var cordovaDir = path.join(devDir, 'CordovaLib'); 14 | var spotifyDir = path.join(devDir, 'Spotify.framework'); 15 | var OHHTTPStubsDir = path.join(devDir, 'OHHTTPStubs'); 16 | 17 | var tmpDir = path.join(devDir, 'tmp'); 18 | var cordovaTmpDir = path.join(tmpDir, 'cordova-ios-3.7.0/CordovaLib'); 19 | var spotifyTmpDir = path.join(tmpDir, 'ios-sdk-beta-6/Spotify.framework'); 20 | var OHHTTPStubsTmpDir = path.join(tmpDir, 'OHHTTPStubs-3.1.6/OHHTTPStubs'); 21 | 22 | var pchFile = path.join(__dirname, 'project_files', 'SpotifyPlugin-Prefix.pch'); 23 | var xcodeProj = path.join(__dirname, 'project_files', 'xcode', '*'); 24 | var xcodeProjTarget = path.join(projectDir, 'SpotifyPlugin.xcodeproj'); 25 | 26 | shell.rm('-rf', devDir); 27 | shell.mkdir('-p', tmpDir); 28 | 29 | downloadCordovaLib(); 30 | 31 | function downloadCordovaLib() { 32 | var size; 33 | if (fs.existsSync(cordovaDir)) { 34 | console.log('CordovaLib exists'); 35 | 36 | return downloadSpotifyFramework(); 37 | } 38 | 39 | console.log('Downloading CordovaLib'); 40 | 41 | var req = request.get({ 42 | url: 'https://github.com/apache/cordova-ios/archive/3.7.0.tar.gz' 43 | }, function(err, res, body) { 44 | if (err || res.statusCode != 200) { 45 | shell.rm('-rf', tmpDir); 46 | } else { 47 | size = body.length; 48 | } 49 | }); 50 | 51 | req.pipe(zlib.createUnzip()) 52 | .pipe(tar.Extract({path:tmpDir})) 53 | .on('error', function(err) { 54 | shell.rm('-rf', tmpDir); 55 | }) 56 | .on('end', function() { 57 | shell.mv('-f', cordovaTmpDir, devDir); 58 | downloadSpotifyFramework(); 59 | }); 60 | } 61 | 62 | function downloadSpotifyFramework() { 63 | var size; 64 | if (fs.existsSync(spotifyDir)) { 65 | console.log('Spotify.framework exists'); 66 | 67 | return downloadOHHTTPStubs(); 68 | } 69 | 70 | console.log('Downloading Spotify.framework'); 71 | 72 | var req = request.get({ 73 | url: 'https://github.com/spotify/ios-sdk/archive/beta-6.tar.gz' 74 | }, function(err, res, body) { 75 | if (err || res.statusCode != 200) { 76 | shell.rm('-rf', tmpDir); 77 | } else { 78 | size = body.length; 79 | } 80 | }); 81 | 82 | req.pipe(zlib.createUnzip()) 83 | .pipe(tar.Extract({path:tmpDir})) 84 | .on('error', function(err) { 85 | shell.rm('-rf', tmpDir); 86 | }) 87 | .on('end', function() { 88 | shell.mv('-f', spotifyTmpDir, devDir); 89 | downloadOHHTTPStubs(); 90 | }); 91 | } 92 | 93 | function downloadOHHTTPStubs() { 94 | var size; 95 | if (fs.existsSync(OHHTTPStubsDir)) { 96 | console.log('OHHTTPStubs exists'); 97 | 98 | return createXcodeProj(); 99 | } 100 | 101 | console.log('Downloading OHHTTPStubs'); 102 | 103 | var req = request.get({ 104 | url: 'https://github.com/AliSoftware/OHHTTPStubs/archive/3.1.6.tar.gz' 105 | }, function(err, res, body) { 106 | if (err || res.statusCode != 200) { 107 | shell.rm('-rf', tmpDir); 108 | } else { 109 | size = body.length; 110 | } 111 | }); 112 | 113 | req.pipe(zlib.createUnzip()) 114 | .pipe(tar.Extract({path:tmpDir})) 115 | .on('error', function(err) { 116 | shell.rm('-rf', tmpDir); 117 | }) 118 | .on('end', function() { 119 | shell.mv('-f', OHHTTPStubsTmpDir, devDir); 120 | createXcodeProj(); 121 | }); 122 | } 123 | 124 | function createXcodeProj() { 125 | shell.rm('-rf', tmpDir); 126 | shell.cp('-f', pchFile, projectDir); 127 | shell.cp('-rf', xcodeProj, xcodeProjTarget); 128 | 129 | console.log('done'); 130 | } 131 | -------------------------------------------------------------------------------- /bin/login: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var https = require('https') 4 | , http = require('http'); 5 | 6 | var scope = encodeURIComponent(process.env.SPOTIFY_SCOPE || 'streaming') 7 | , redirectUri = process.env.SPOTIFY_REDIRECT_URI || 'http://localhost:1337/' 8 | , encodedRedirectUri = encodeURIComponent(redirectUri); 9 | 10 | var spAc, spDc, csrfToken; 11 | 12 | var url = 'https://accounts.spotify.com/nl/login?continue=https:%2F%2Faccounts.spotify.com%2Fnl%2Fauthorize%3Fclient_id%3D' + process.env.SPOTIFY_CLIENTID + '%26response_type%3Dtoken%26redirect_uri%3D'+encodedRedirectUri+'%26scope%3D'+scope; 13 | 14 | var options = { 15 | hostname: 'accounts.spotify.com', 16 | port: 443, 17 | path: '/nl/login?continue=https:%2F%2Faccounts.spotify.com%2Fnl%2Fauthorize%3Fclient_id%3D' + process.env.SPOTIFY_CLIENTID + '%26response_type%3Dtoken%26redirect_uri%3D'+encodedRedirectUri+'%26scope%3D'+scope, 18 | method: 'GET', 19 | headers: { 20 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 21 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36' 22 | } 23 | }; 24 | 25 | var req = https.request(options, onUrlResult); 26 | 27 | req.on('error', function(e) { 28 | console.error(e); 29 | }); 30 | 31 | req.end(); 32 | 33 | function onUrlResult(res) { 34 | res.headers['set-cookie'].forEach(function(item, index) { 35 | if ((matches = /^csrf_token\=([^;]+)/.exec(item)) !== null) { 36 | csrfToken = matches[1]; 37 | 38 | doLogin(csrfToken); 39 | } 40 | }); 41 | 42 | res.on('data', function(d) {}); 43 | } 44 | 45 | function doLogin(csrfToken) { 46 | var data = 'username='+process.env.SPOTIFY_USERNAME+'&password='+process.env.SPOTIFY_PASSWORD+'&csrf_token='+csrfToken; 47 | 48 | var options = { 49 | hostname: 'accounts.spotify.com', 50 | port: 443, 51 | path: '/api/login', 52 | method: 'POST', 53 | headers: { 54 | 'Accept': 'application/json, text/plain, */*', 55 | 'Host': 'accounts.spotify.com', 56 | 'Origin': 'https://accounts.spotify.com', 57 | 'Referer': 'https://accounts.spotify.com/nl/login?continue=https:%2F%2Faccounts.spotify.com%2Fnl%2Fauthorize%3Fclient_id%3D' + process.env.SPOTIFY_CLIENTID + '%26response_type%3Dtoken%26redirect_uri%3D'+encodedRedirectUri+'%26scope%3D'+scope, 58 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36', 59 | 'Cookie': 'csrf_token='+csrfToken+'; fb_continue=https%3A%2F%2Faccounts.spotify.com%2Fnl%2Fauthorize%3Fclient_id%3D' + process.env.SPOTIFY_CLIENTID + '%26response_type%3Dtoken%26redirect_uri%3D'+encodedRedirectUri+'%26scope%3D'+scope+'%26show_dialog%3Dtrue; _ga=GA1.2.1205302991.1415300731; _gat=1', 60 | 'Connection': 'keep-alive', 61 | 'Content-Type': 'application/x-www-form-urlencoded' 62 | } 63 | }; 64 | 65 | var loginReq = https.request(options, onLoginResult); 66 | 67 | loginReq.on('error', function(e) { 68 | console.log(e); 69 | }); 70 | 71 | loginReq.write(data); 72 | loginReq.end(); 73 | } 74 | 75 | function onLoginResult(res) { 76 | res.headers['set-cookie'].forEach(function(item, index) { 77 | if ((matches = /^sp_ac\=([^;]+)/.exec(item)) !== null) { 78 | spAc = matches[1]; 79 | } 80 | 81 | if ((matches = /^sp_dc\=([^;]+)/.exec(item)) !== null) { 82 | spDc = matches[1]; 83 | } 84 | 85 | if ((matches = /^csrf_token\=([^;]+)/.exec(item)) !== null) { 86 | csrfToken = matches[1]; 87 | } 88 | }); 89 | 90 | res.on('data', function(d) {}); 91 | 92 | doAuthorize(); 93 | } 94 | 95 | function doAuthorize() { 96 | var options = { 97 | hostname: 'accounts.spotify.com', 98 | port: 443, 99 | path: '/nl/authorize?client_id=' + process.env.SPOTIFY_CLIENTID + '&response_type=token&redirect_uri='+redirectUri+'&scope='+scope, 100 | method: 'GET', 101 | headers: { 102 | 'Accept': 'application/json, text/plain, */*', 103 | 'Host': 'accounts.spotify.com', 104 | 'Origin': 'https://accounts.spotify.com', 105 | 'Referer': 'https://accounts.spotify.com/nl/login?continue=https:%2F%2Faccounts.spotify.com%2Fnl%2Fauthorize%3Fclient_id%3D' + process.env.SPOTIFY_CLIENTID + '%26response_type%3Dtoken%26redirect_uri%3D'+encodedRedirectUri+'%26scope%3D'+scope, 106 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36', 107 | 'Cookie': '_gat=1; sp_ac='+spAc+'; sp_dc='+spDc+'; _ga=GA1.2.1293684910.1415403317; csrf_token='+csrfToken, 108 | 'Connection': 'keep-alive', 109 | } 110 | }; 111 | 112 | var loginReq = https.request(options, onAuthorize); 113 | 114 | loginReq.on('error', function(e) { 115 | console.log(e); 116 | }); 117 | 118 | loginReq.end(); 119 | }; 120 | 121 | function onAuthorize(res) { 122 | var data = ''; 123 | 124 | res.headers['set-cookie'].forEach(function(item, index) { 125 | if ((matches = /^csrf_token\=([^;]+)/.exec(item)) !== null) { 126 | csrfToken = matches[1]; 127 | } 128 | }); 129 | 130 | res.setEncoding('utf8'); 131 | res.on('data', function(d) { 132 | data += d; 133 | }); 134 | 135 | res.on('end', function() { 136 | onAuthorizeResponse(data); 137 | }); 138 | } 139 | 140 | function onAuthorizeResponse(data) { 141 | if (extractTokenFromData(data)) return; 142 | 143 | var cookies = [ 144 | '_gat=1', 145 | 'sp_ac='+spAc, 146 | 'sp_dc='+spDc, 147 | 'csrf_token='+csrfToken, 148 | '_ga=GA1.2.1596465940.1415422830' 149 | ].join('; '); 150 | 151 | acceptApplicationAccess(cookies); 152 | } 153 | 154 | function acceptApplicationAccess(cookies) { 155 | var data = 'client_id=' + process.env.SPOTIFY_CLIENTID + '&response_type=token&redirect_uri='+encodedRedirectUri+'&scope='+scope+'&csrf_token=' + csrfToken; 156 | 157 | var options = { 158 | hostname: 'accounts.spotify.com', 159 | port: 443, 160 | path: 'https://accounts.spotify.com/nl/authorize/accept', 161 | method: 'POST', 162 | headers: { 163 | 'Accept': 'application/json, text/plain, */*', 164 | 'Host': 'accounts.spotify.com', 165 | 'Origin': 'https://accounts.spotify.com', 166 | 'Referer': 'https://accounts.spotify.com/nl/authorize?client_id=' + process.env.SPOTIFY_CLIENTID + '&response_type=token&redirect_uri=' + encodedRedirectUri + '&scope=' + scope, 167 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36', 168 | 'Cookie': cookies, 169 | 'Connection': 'keep-alive', 170 | 'Content-Type': 'application/x-www-form-urlencoded' 171 | } 172 | }; 173 | 174 | var acceptReq = https.request(options, onAccept); 175 | 176 | acceptReq.on('error', function(e) { 177 | console.log(e); 178 | }); 179 | 180 | acceptReq.write(data); 181 | acceptReq.end(); 182 | } 183 | 184 | function onAccept(res) { 185 | var data = ''; 186 | 187 | res.setEncoding('utf8'); 188 | res.on('data', function(d) { 189 | data += d; 190 | }); 191 | 192 | res.on('end', function() { 193 | onAcceptResponse(data); 194 | }); 195 | } 196 | 197 | function onAcceptResponse(data) { 198 | if (extractTokenFromData(data)) return; 199 | 200 | process.stdout.write('Unable to grab token'); 201 | } 202 | 203 | function extractTokenFromData(data) { 204 | var obj = JSON.parse(data); 205 | 206 | if (obj.hasOwnProperty('redirect')) { 207 | var token = /#access_token\=([^&]+)/.exec(obj.redirect)[1]; 208 | 209 | registerToken(token); 210 | return true; 211 | } 212 | 213 | return false; 214 | } 215 | 216 | function registerToken(token) { 217 | process.stdout.write(token); 218 | } 219 | -------------------------------------------------------------------------------- /bin/project_files/SpotifyPlugin-Prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #endif 4 | -------------------------------------------------------------------------------- /bin/project_files/xcode/xcshareddata/xcschemes/SpotifyPlugin.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /bin/shared/env.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | loadEnvVariables: function(fn) { 5 | if (fs.existsSync(fn)) { 6 | fs.readFileSync(fn, {encoding: 'utf8'}) 7 | .split('\n') 8 | .forEach(function(item) { 9 | var env = item.split('=') 10 | , key = env[0], value = env[1]; 11 | process.env[key] = process.env[key] || value; 12 | }); 13 | } 14 | }, 15 | setupEnvVariablesForTestScript: function(keys) { 16 | var enabledVars = []; 17 | 18 | if (process.env.SAUCE) 19 | keys = keys.concat(['SAUCE_USERNAME', 'SAUCE_ACCESS_KEY']); 20 | 21 | keys.forEach(function(key, index, arr) { 22 | if (process.env[key]) 23 | enabledVars.push(key+'='+process.env[key]); 24 | }); 25 | 26 | return enabledVars; 27 | }, 28 | setupTestCommand: function(env, framework, script) { 29 | return env.concat([framework, script]).join(' '); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bin/test-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEST_EXIT_CODE=0 4 | 5 | ./bin/test-ios 6 | 7 | EC=$? 8 | if [ $EC -ne 0 ]; then 9 | TEST_EXIT_CODE=$(($TEST_EXIT_CODE + $EC)) 10 | fi 11 | 12 | ./bin/test-js 13 | EC=$? 14 | if [ $EC -ne 0 ]; then 15 | TEST_EXIT_CODE=$(($TEST_EXIT_CODE + $EC)) 16 | fi 17 | 18 | exit $TEST_EXIT_CODE 19 | -------------------------------------------------------------------------------- /bin/test-appium: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.info("\n\n\x1b[32m\x1b[1m==Appium Acceptance Tests==\x1b[0m\n"); 4 | 5 | var exitCode = 1; 6 | 7 | var env = require('./shared/env') 8 | , path = require('path') 9 | , shell = require('shelljs'); 10 | 11 | env.loadEnvVariables(path.join(process.cwd(), '.env')); 12 | 13 | var testEnv = env.setupEnvVariablesForTestScript(['DEV', 'VM', 'SAUCE']) 14 | , testFramework = 'node_modules/.bin/mocha' 15 | , testScript = 'test/appium/specs/specs.js' 16 | , testCommand = env.setupTestCommand(testEnv, testFramework, testScript); 17 | 18 | exitCode = shell.exec(testCommand).code; 19 | 20 | process.exit(exitCode); 21 | -------------------------------------------------------------------------------- /bin/test-ios: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "\n\n\x1b[32m\x1b[1m==XCode Unit tests==\x1b[0m\n" 4 | 5 | xcodebuild test -project SpotifyPlugin.xcodeproj -scheme SpotifyPlugin -destination 'platform=iOS Simulator,name=iPhone 6,OS=8.1' | xcpretty -tc 6 | exit ${PIPESTATUS[0]} 7 | -------------------------------------------------------------------------------- /bin/test-js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.info("\n\n\x1b[32m\x1b[1m==JavaScript Unit tests==\x1b[0m\n"); 4 | 5 | var cordova = require('cordova') 6 | , fs = require('fs') 7 | , path = require('path') 8 | , shell = require('shelljs') 9 | , temp = require('temp'); 10 | 11 | temp.track(); 12 | 13 | var old_log = console.log; 14 | 15 | var pluginDir = path.join(__dirname, '..') 16 | , tmpDir = temp.mkdirSync() 17 | , wwwDir = path.join(tmpDir, 'www'); 18 | 19 | cordova.raw.create(tmpDir) 20 | .then(function() { 21 | 22 | shell.rm('-rf', path.join(wwwDir, '*')); 23 | shell.cp('-rf', path.join(pluginDir, 'test', 'js', '*'), wwwDir); 24 | shell.mkdir('-p', path.join(wwwDir, 'support', '{chai,mocha}')); 25 | shell.cp('-rf', path.join(pluginDir, 'node_modules', 'mocha', '*'), path.join(wwwDir, 'support', 'mocha')); 26 | shell.cp('-rf', path.join(pluginDir, 'node_modules', 'chai', '*'), path.join(wwwDir, 'support', 'chai')); 27 | shell.cp('-rf', path.join(pluginDir, 'node_modules', 'sinon-chai', 'lib', '*'), path.join(wwwDir, 'support', 'sinon-chai')); 28 | shell.cp('-rf', path.join(pluginDir, 'node_modules', 'sinon', 'pkg', 'sinon.js'), path.join(wwwDir, 'support', 'sinon')); 29 | shell.cd(tmpDir); 30 | }) 31 | .then(function() { 32 | return cordova.raw.plugin('add', pluginDir); 33 | }) 34 | .then(function() { 35 | shell.cp('-rf', path.join(pluginDir, 'development', 'Spotify.framework'), path.join(tmpDir, 'plugins', 'com.timflapper.spotify', 'src', 'ios')); 36 | return cordova.raw.platform('add', 'ios'); 37 | }) 38 | .then(function() { 39 | console.log = function() { }; 40 | return cordova.raw.serve(); 41 | }) 42 | .then(function(server) { 43 | shell.cd(pluginDir); 44 | shell.exec('./node_modules/.bin/mocha-phantomjs -R dot -A "Mozilla/5.0 (iPhone; CPU iPhone OS 10_9_5 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B411 (140430689179968)" http://localhost:8000/ios/www/', function(code, output) { 45 | console.log = old_log; 46 | 47 | server.close(); 48 | 49 | process.exit(code); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$PLUGIN_ENV" == "test" ]; then 4 | exit 5 | fi 6 | 7 | if [ "$CORDOVA_SPOTIFY_URL_SCHEME" == "" ] 8 | then 9 | DEFAULT_SCHEME="spotify-cordova" 10 | 11 | echo "The Spotify SDK Plugin needs a URL scheme for authentication." 12 | echo "See http://bit.ly/1u11ZUz for more information" 13 | printf "Specify your URL scheme [$DEFAULT_SCHEME]: " 14 | 15 | read CORDOVA_SPOTIFY_URL_SCHEME 16 | if [ "$CORDOVA_SPOTIFY_URL_SCHEME" == "" ] 17 | then 18 | CORDOVA_SPOTIFY_URL_SCHEME="$DEFAULT_SCHEME" 19 | fi 20 | fi 21 | 22 | echo "Writing URL scheme to plugin.xml" 23 | 24 | mv plugins/com.timflapper.spotify/plugin.xml plugins/com.timflapper.spotify/plugin.bak.xml 25 | sed "s/{{URL_SCHEME}}/$CORDOVA_SPOTIFY_URL_SCHEME/g" plugins/com.timflapper.spotify/plugin.bak.xml > plugins/com.timflapper.spotify/plugin.xml 26 | rm plugins/com.timflapper.spotify/plugin.bak.xml 27 | 28 | echo "Removing placeholder" 29 | rm -rf plugins/com.timflapper.spotify/src/ios/Spotify.framework 30 | 31 | echo "Downloading Spotify Framework" 32 | mkdir plugins/com.timflapper.spotify/src/ios/tmp 33 | cd plugins/com.timflapper.spotify/src/ios/tmp 34 | curl -OL "https://github.com/spotify/ios-sdk/archive/beta-6.tar.gz" 35 | 36 | echo "Extracting" 37 | tar xzvf beta-6.tar.gz 38 | cd .. 39 | mv tmp/ios-sdk-beta-6/Spotify.framework . 40 | rm -rf tmp 41 | 42 | echo "Finished!" 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SpotifyPlugin", 3 | "version": "0.0.0", 4 | "description": "", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/timflapper/cordova-spotify-plugin.git" 8 | }, 9 | "author": "Tim Flapper", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/timflapper/cordova-spotify-plugin/issues" 13 | }, 14 | "homepage": "https://github.com/timflapper/cordova-spotify-plugin", 15 | "devDependencies": { 16 | "chai": "^1.9.2", 17 | "chai-as-promised": "^4.1.1", 18 | "colors": "^1.0.3", 19 | "cordova": "^4.0.0", 20 | "mocha": "^1.21.5", 21 | "mocha-phantomjs": "^3.5.1", 22 | "node-phantom": "^0.2.5", 23 | "phantomjs": "^1.9.11", 24 | "request": "^2.34.0", 25 | "shelljs": "^0.3.0", 26 | "sinon": "^1.10.3", 27 | "sinon-chai": "^2.6.0", 28 | "socket.io": "^0.9.17", 29 | "tar": "^0.1.19", 30 | "temp": "^0.8.1", 31 | "wd": "^0.3.9" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spotify Cordova Plugin 4 | This plugin provides a JavaScript API for Spotify's iOS SDK. 5 | Tim Flapper 6 | MIT 7 | spotify 8 | https://github.com/timflapper/cordova-spotify-plugin 9 | https://github.com/timflapper/cordova-spotify-plugin/issues 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | CFBundleTypeRole 48 | Editor 49 | CFBundleURLName 50 | Spotify Auth 51 | CFBundleURLSchemes 52 | 53 | {{URL_SCHEME}} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/ios/Spotify.framework/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timflapper/cordova-spotify-plugin/424f3a900c4915a0cdf0e20fccd51c7185349bb7/src/ios/Spotify.framework/.gitkeep -------------------------------------------------------------------------------- /src/ios/SpotifyAudioPlayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyAudioPlayer.h 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 08/05/14. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface SpotifyAudioPlayer : SPTAudioStreamingController 12 | + (instancetype)getInstanceByID:(NSString *)ID; 13 | @property NSString *instanceID; 14 | @end 15 | -------------------------------------------------------------------------------- /src/ios/SpotifyAudioPlayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyAudioPlayer.m 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 08/05/14. 6 | // 7 | // 8 | 9 | #import "SpotifyAudioPlayer.h" 10 | 11 | @interface SpotifyAudioPlayer() 12 | @end 13 | 14 | @implementation SpotifyAudioPlayer 15 | static NSMutableDictionary *instances; 16 | 17 | @synthesize delegate, playbackDelegate; 18 | 19 | + (void)initialize 20 | { 21 | if (self == [SpotifyAudioPlayer class]) { 22 | instances = [NSMutableDictionary new]; 23 | } 24 | } 25 | 26 | + (instancetype)getInstanceByID:(NSString *)ID 27 | { 28 | return [instances objectForKey: ID]; 29 | } 30 | 31 | - (id)initWithClientId:(NSString *)clientId audioController:(SPTCoreAudioController *)audioController 32 | { 33 | self = [super initWithClientId:clientId audioController:audioController]; 34 | 35 | if (self) { 36 | delegate = self; 37 | playbackDelegate = self; 38 | 39 | _instanceID = [NSString stringWithFormat:@"%d", (int)instances.count+1]; 40 | [instances setObject:self 41 | forKey: _instanceID]; 42 | } 43 | 44 | return self; 45 | } 46 | 47 | - (void)dispatchEvent:(NSString *)type 48 | { 49 | [self dispatchEvent:type withArguments:@[]]; 50 | } 51 | 52 | - (void)dispatchEvent:(NSString *)type withArguments:(NSArray *)args 53 | { 54 | 55 | NSDictionary *info = @{@"type": type, 56 | @"args": args}; 57 | 58 | NSNotification *note = [NSNotification notificationWithName:@"event" object:self userInfo:info]; 59 | 60 | [[NSNotificationCenter defaultCenter] postNotification:note]; 61 | } 62 | 63 | -(void)audioStreamingDidLogin:(SPTAudioStreamingController *)audioStreaming 64 | { 65 | [self dispatchEvent:@"login"]; 66 | } 67 | 68 | -(void)audioStreamingDidLogout:(SPTAudioStreamingController *)audioStreaming 69 | { 70 | [self dispatchEvent:@"logout"]; 71 | } 72 | 73 | -(void)audioStreamingDidLosePermissionForPlayback:(SPTAudioStreamingController *)audioStreaming 74 | { 75 | [self dispatchEvent:@"permissionLost"]; 76 | } 77 | 78 | -(void)audioStreamingDidBecomeActivePlaybackDevice:(SPTAudioStreamingController *)audioStreaming 79 | { 80 | [self dispatchEvent:@"active"]; 81 | } 82 | 83 | -(void)audioStreamingDidBecomeInactivePlaybackDevice:(SPTAudioStreamingController *)audioStreaming 84 | { 85 | [self dispatchEvent:@"inactive"]; 86 | } 87 | 88 | -(void)audioStreamingDidEncounterTemporaryConnectionError:(SPTAudioStreamingController *)audioStreaming 89 | { 90 | [self dispatchEvent:@"temporaryConnectionError"]; 91 | } 92 | 93 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didChangePlaybackStatus:(BOOL)isPlaying 94 | { 95 | [self dispatchEvent:@"playbackStatus" withArguments:@[[NSNumber numberWithBool:isPlaying]]]; 96 | } 97 | 98 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didChangeRepeatStatus:(BOOL)isRepeated 99 | { 100 | [self dispatchEvent:@"repeatStatus" withArguments:@[[NSNumber numberWithBool:isRepeated]]]; 101 | } 102 | 103 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didChangeShuffleStatus:(BOOL)isShuffled 104 | { 105 | [self dispatchEvent:@"shuffleStatus" withArguments:@[[NSNumber numberWithBool:isShuffled]]]; 106 | } 107 | 108 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didChangeToTrack:(NSDictionary *)trackMetadata 109 | { 110 | if(trackMetadata != nil){ 111 | [self dispatchEvent:@"trackChanged" withArguments:@[trackMetadata]]; 112 | } 113 | } 114 | 115 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didChangeVolume:(SPTVolume)volume 116 | { 117 | [self dispatchEvent:@"volumeChanged" withArguments:@[[NSNumber numberWithDouble:volume]]]; 118 | } 119 | 120 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didEncounterError:(NSError *)error 121 | { 122 | [self dispatchEvent:@"error" withArguments:@[error.localizedDescription]]; 123 | } 124 | 125 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didReceiveMessage:(NSString *)message 126 | { 127 | [self dispatchEvent:@"message" withArguments:@[message]]; 128 | } 129 | 130 | -(void)audioStreaming:(SPTAudioStreamingController *)audioStreaming didSeekToOffset:(NSTimeInterval)offset 131 | { 132 | [self dispatchEvent:@"seekToOffset" withArguments:@[[NSNumber numberWithDouble:offset]]]; 133 | } 134 | 135 | -(void)audioStreamingDidSkipToNextTrack:(SPTAudioStreamingController *)audioStreaming 136 | { 137 | [self dispatchEvent:@"skippedToNextTrack"]; 138 | } 139 | 140 | -(void)audioStreamingDidSkipToPreviousTrack:(SPTAudioStreamingController *)audioStreaming 141 | { 142 | [self dispatchEvent:@"skippedToPreviousTrack"]; 143 | } 144 | 145 | @end 146 | -------------------------------------------------------------------------------- /src/ios/SpotifyPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyPlugin.h 3 | // 4 | 5 | #import "Cordova/CDV.h" 6 | #import 7 | #import "SpotifyAudioPlayer.h" 8 | 9 | NSString *dateToString(NSDate *date); 10 | NSDate *stringToDate(NSString *dateString); 11 | 12 | @interface SpotifyPlugin : CDVPlugin 13 | 14 | /* Linked to SPTAuth */ 15 | - (void)authenticate:(CDVInvokedUrlCommand*)command; 16 | - (void)isSessionValid:(CDVInvokedUrlCommand*)command; 17 | - (void)renewSession:(CDVInvokedUrlCommand*)command; 18 | 19 | /* Linked to SpotifyAudioPlayer */ 20 | - (void)createAudioPlayerAndLogin:(CDVInvokedUrlCommand*)command; 21 | - (void)audioPlayerLogout:(CDVInvokedUrlCommand*)command; 22 | - (void)addAudioPlayerEventListener:(CDVInvokedUrlCommand*)command; 23 | - (void)play:(CDVInvokedUrlCommand*)command; 24 | - (void)setURIs:(CDVInvokedUrlCommand*)command; 25 | - (void)playURIsFromIndex:(CDVInvokedUrlCommand*)command; 26 | - (void)queue:(CDVInvokedUrlCommand*)command; 27 | - (void)queuePlay:(CDVInvokedUrlCommand*)command; 28 | - (void)queueClear:(CDVInvokedUrlCommand*)command; 29 | - (void)stop:(CDVInvokedUrlCommand*)command; 30 | - (void)seekToOffset:(CDVInvokedUrlCommand*)command; 31 | - (void)skipNext:(CDVInvokedUrlCommand*)command; 32 | - (void)skipPrevious:(CDVInvokedUrlCommand*)command; 33 | - (void)getIsPlaying:(CDVInvokedUrlCommand*)command; 34 | - (void)setIsPlaying:(CDVInvokedUrlCommand*)command; 35 | - (void)getTargetBitrate:(CDVInvokedUrlCommand*)command; 36 | - (void)setTargetBitrate:(CDVInvokedUrlCommand*)command; 37 | - (void)getDiskCacheSizeLimit:(CDVInvokedUrlCommand*)command; 38 | - (void)setDiskCacheSizeLimit:(CDVInvokedUrlCommand*)command; 39 | - (void)getVolume:(CDVInvokedUrlCommand*)command; 40 | - (void)setVolume:(CDVInvokedUrlCommand*)command; 41 | - (void)getRepeat:(CDVInvokedUrlCommand*)command; 42 | - (void)setRepeat:(CDVInvokedUrlCommand*)command; 43 | - (void)getShuffle:(CDVInvokedUrlCommand*)command; 44 | - (void)setShuffle:(CDVInvokedUrlCommand*)command; 45 | - (void)getLoggedIn:(CDVInvokedUrlCommand*)command; 46 | - (void)getQueueSize:(CDVInvokedUrlCommand*)command; 47 | - (void)getTrackListSize:(CDVInvokedUrlCommand*)command; 48 | - (void)getTrackMetadata:(CDVInvokedUrlCommand*)command; 49 | - (void)getCurrentPlaybackPosition:(CDVInvokedUrlCommand*)command; 50 | @end 51 | -------------------------------------------------------------------------------- /test/appium/specs/audio-player.js: -------------------------------------------------------------------------------- 1 | module.exports = function(driver) { 2 | 3 | before(function() { 4 | return driver 5 | .elementById('loginPlayerLink') 6 | .click() 7 | .waitForConditionInBrowser("document.getElementById('loginPlayerStatus').style.display !== 'none';", 30000) 8 | }); 9 | 10 | it('should have a logged in player', function() { 11 | return driver 12 | .elementById('loginPlayerStatus') 13 | .text() 14 | .should.eventually.equal('success'); 15 | }); 16 | 17 | context('playing a song', function() { 18 | before(function() { 19 | return driver 20 | .elementById('playSongLink') 21 | .click() 22 | .waitForConditionInBrowser("document.getElementById('playSongStatus').style.display !== 'none';", 30000); 23 | }); 24 | 25 | it('should be able to play a song', function() { 26 | return driver 27 | .elementById('playSongStatus') 28 | .text() 29 | .should.eventually.equal('success'); 30 | }); 31 | 32 | context('volume', function() { 33 | before(function() { 34 | return driver 35 | .elementById('muteLink') 36 | .click() 37 | .waitForConditionInBrowser("document.getElementById('muteStatus').style.display !== 'none';", 30000) 38 | }); 39 | 40 | it('should be able to change the volume', function() { 41 | return driver 42 | .elementById('muteStatus') 43 | .text() 44 | .should.eventually.equal('success'); 45 | }); 46 | }); 47 | 48 | context('playback position', function() { 49 | before(function() { 50 | return driver 51 | .sleep(5000) 52 | .elementById('playbackPositionLink') 53 | .click() 54 | .waitForConditionInBrowser("document.getElementById('playbackPosition').style.display !== 'none';", 30000) 55 | }); 56 | 57 | it('should be able to get the playback position', function() { 58 | return driver 59 | .elementById('playbackPosition') 60 | .text() 61 | .should.eventually.match(/^\d+\.\d+$/); 62 | }); 63 | }) 64 | }); 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /test/appium/specs/specs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var wd = require('wd') 4 | , Asserter = wd.Asserter; 5 | 6 | var setup = require('./support/setup') 7 | , driver = setup.driver 8 | , endOfTestSuite = setup.endOfTestSuite 9 | , updateAllPassed = setup.updateAllPassed; 10 | 11 | describe("SpotifyPlugin", function () { 12 | this.timeout(600000); 13 | 14 | before(function () { 15 | var waitForWebViewContext = new Asserter( 16 | function(driver, cb) { 17 | return driver 18 | .contexts() 19 | .then(function(contexts) { 20 | var ret = false; 21 | if (contexts.length > 1) { 22 | ret = contexts[1]; 23 | } 24 | 25 | cb(null, (ret !== false), ret); 26 | }) 27 | } 28 | ); 29 | 30 | return driver 31 | .setAsyncScriptTimeout(300000) 32 | .waitFor(waitForWebViewContext, 300000) 33 | .then(function(context) { 34 | if (! context) 35 | throw new Error('Context not found.'); 36 | 37 | return driver.context(context); 38 | }) 39 | .fail(function(error) { 40 | updateAllPassed(false); 41 | 42 | return endOfTestSuite(); 43 | }) 44 | .waitForConditionInBrowser("document.getElementById('page').style.display === 'block';", 30000) 45 | }); 46 | 47 | after(function () { 48 | return endOfTestSuite(); 49 | }); 50 | 51 | afterEach(function () { 52 | return updateAllPassed(this.currentTest.state === 'passed'); 53 | }); 54 | 55 | require('./audio-player')(driver); 56 | }); 57 | -------------------------------------------------------------------------------- /test/appium/specs/support/appium-servers.js: -------------------------------------------------------------------------------- 1 | exports.local = { 2 | host: 'localhost', 3 | port: 4723 4 | }; 5 | 6 | exports.sauce = { 7 | host: 'ondemand.saucelabs.com', 8 | port: 80, 9 | username: process.env.SAUCE_USERNAME, 10 | password: process.env.SAUCE_ACCESS_KEY 11 | }; 12 | -------------------------------------------------------------------------------- /test/appium/specs/support/logging.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.configure = function (driver) { 4 | // See whats going on 5 | driver.on('status', function (info) { 6 | console.log(info.cyan); 7 | }); 8 | driver.on('command', function (meth, path, data) { 9 | console.log(' > ' + meth.yellow, path.grey, data || ''); 10 | }); 11 | driver.on('http', function (meth, path, data) { 12 | console.log(' > ' + meth.magenta, path, (data || '').grey); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /test/appium/specs/support/setup.js: -------------------------------------------------------------------------------- 1 | var wd = require('wd') 2 | , path = require('path') 3 | 4 | require('colors'); 5 | 6 | var chai = require('chai') 7 | , chaiAsPromised = require('chai-as-promised') 8 | , should = chai.should(); 9 | 10 | chai.use(chaiAsPromised); 11 | chaiAsPromised.transferPromiseness = wd.transferPromiseness; 12 | 13 | var allPassed = true; 14 | 15 | var driver = setupDriver(); 16 | 17 | function setupDriver() { 18 | var appiumServers = require('./appium-servers'); 19 | 20 | var serverConfig = process.env.SAUCE ? 21 | appiumServers.sauce : appiumServers.local; 22 | var driver = wd.promiseChainRemote(serverConfig); 23 | 24 | var desired = { 25 | name: 'SpotifyPlugin Test', 26 | browserName: '', 27 | 'appium-version': '1.3.1', 28 | platformName: 'iOS', 29 | platformVersion: '8.1', 30 | deviceName: 'iPhone 6', 31 | app: undefined // will be set later 32 | }; 33 | 34 | if (process.env.SAUCE) { 35 | desired.app = 'sauce-storage:AcceptanceTest.zip'; 36 | } else { 37 | desired.app = path.join(process.cwd(), '.tmp/TestApp/platforms/ios/build/emulator/AcceptanceTest.app'); 38 | } 39 | 40 | return driver.init(desired); 41 | } 42 | 43 | function endOfTestSuite() { 44 | return driver 45 | .quit() 46 | .finally(function () { 47 | if (process.env.SAUCE) { 48 | return driver.sauceJobStatus(allPassed); 49 | } 50 | }); 51 | } 52 | 53 | function updateAllPassed(state) { 54 | allPassed = allPassed && state; 55 | } 56 | 57 | exports.should = should; 58 | 59 | exports.driver = driver; 60 | 61 | exports.endOfTestSuite = endOfTestSuite; 62 | 63 | exports.updateAllPassed = updateAllPassed; 64 | 65 | 66 | -------------------------------------------------------------------------------- /test/appium/src/ios/AutomationCoreAudioController.h: -------------------------------------------------------------------------------- 1 | // 2 | // AutomationCoreAudioController.h 3 | // 4 | 5 | #import 6 | 7 | @interface AutomationCoreAudioController : SPTCoreAudioController 8 | - (NSInteger)attemptToDeliverAudioFrames:(const void *)audioFrames ofCount:(NSInteger)frameCount streamDescription:(AudioStreamBasicDescription)audioDescription; 9 | @end 10 | -------------------------------------------------------------------------------- /test/appium/src/ios/AutomationCoreAudioController.m: -------------------------------------------------------------------------------- 1 | // 2 | // AutomationCoreAudioController.m 3 | // 4 | 5 | #import "AutomationCoreAudioController.h" 6 | 7 | @interface AutomationCoreAudioController () 8 | 9 | @property SPTCircularBuffer *circularBuffer; 10 | @property BOOL buffering; 11 | 12 | @property NSInteger framesSinceLastTimeUpdate; 13 | @end 14 | 15 | @implementation AutomationCoreAudioController 16 | 17 | - (id)init { 18 | self = [super init]; 19 | 20 | if (self) { 21 | _framesSinceLastTimeUpdate = 0; 22 | _buffering = false; 23 | } 24 | 25 | return self; 26 | } 27 | 28 | - (NSInteger)attemptToDeliverAudioFrames:(const void *)audioFrames ofCount:(NSInteger)frameCount streamDescription:(AudioStreamBasicDescription)audioDescription 29 | { 30 | if (! self.circularBuffer) { 31 | self.circularBuffer = [[SPTCircularBuffer alloc] initWithMaximumLength:(audioDescription.mBytesPerFrame * audioDescription.mSampleRate) * 0.5]; 32 | self.buffering = true; 33 | } 34 | 35 | NSUInteger bytesToAdd = audioDescription.mBytesPerPacket * frameCount; 36 | 37 | NSUInteger bytesAdded = [self.circularBuffer attemptAppendData:audioFrames ofLength:bytesToAdd chunkSize:audioDescription.mBytesPerPacket]; 38 | 39 | NSUInteger framesAdded = bytesAdded / audioDescription.mBytesPerPacket; 40 | 41 | self.framesSinceLastTimeUpdate += framesAdded; 42 | 43 | if (self.framesSinceLastTimeUpdate >= 8820) { 44 | [[self delegate] coreAudioController:self didOutputAudioOfDuration:self.framesSinceLastTimeUpdate/audioDescription.mSampleRate]; 45 | 46 | self.framesSinceLastTimeUpdate = 0; 47 | } 48 | 49 | if (self.buffering) { 50 | [self grabDataFromBufferOnInterval]; 51 | self.buffering = false; 52 | } 53 | 54 | return framesAdded; 55 | } 56 | 57 | -(void)grabDataFromBufferOnInterval 58 | { 59 | [NSTimer scheduledTimerWithTimeInterval:0.50f 60 | target:self selector:@selector(grabDataFromBuffer:) userInfo:nil repeats:YES]; 61 | } 62 | 63 | -(void)grabDataFromBuffer:(NSTimer *)timer 64 | { 65 | [self.circularBuffer clear]; 66 | } 67 | @end 68 | -------------------------------------------------------------------------------- /test/appium/src/ios/SpotifyAudioPlayer+Testing.h: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyAudioPlayer+Testing.h 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 08/11/14. 6 | // 7 | // 8 | 9 | #import "SpotifyAudioPlayer.h" 10 | 11 | @interface SpotifyAudioPlayer (Testing) 12 | @end 13 | -------------------------------------------------------------------------------- /test/appium/src/ios/SpotifyAudioPlayer+Testing.m: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyAudioPlayer+Testing.m 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 08/11/14. 6 | // 7 | // 8 | 9 | #import "SpotifyAudioPlayer+Testing.h" 10 | #import "AutomationCoreAudioController.h" 11 | #import 12 | 13 | static IMP __initWithClientId_Imp; 14 | id __Swizzle_initWithClientId(SpotifyAudioPlayer *self, SEL _cmd, NSString *clientId, SPTCoreAudioController *audioController) 15 | { 16 | AutomationCoreAudioController *controller = [AutomationCoreAudioController new]; 17 | 18 | return __initWithClientId_Imp(self, _cmd, clientId, controller); 19 | } 20 | 21 | @implementation SpotifyAudioPlayer (Testing) 22 | + (void)load 23 | { 24 | static dispatch_once_t onceToken; 25 | dispatch_once(&onceToken, ^{ 26 | Method originalMethod = class_getInstanceMethod([self class], @selector(initWithClientId:audioController:)); 27 | __initWithClientId_Imp = method_setImplementation(originalMethod, (IMP)__Swizzle_initWithClientId); 28 | }); 29 | } 30 | @end 31 | -------------------------------------------------------------------------------- /test/appium/www/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=109fa5b75f9e34ab14e2) 9 | * Config saved to config.json and https://gist.github.com/109fa5b75f9e34ab14e2 10 | */ 11 | .btn-default, 12 | .btn-primary, 13 | .btn-success, 14 | .btn-info, 15 | .btn-warning, 16 | .btn-danger { 17 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); 18 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 19 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 20 | } 21 | .btn-default:active, 22 | .btn-primary:active, 23 | .btn-success:active, 24 | .btn-info:active, 25 | .btn-warning:active, 26 | .btn-danger:active, 27 | .btn-default.active, 28 | .btn-primary.active, 29 | .btn-success.active, 30 | .btn-info.active, 31 | .btn-warning.active, 32 | .btn-danger.active { 33 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 34 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 35 | } 36 | .btn-default .badge, 37 | .btn-primary .badge, 38 | .btn-success .badge, 39 | .btn-info .badge, 40 | .btn-warning .badge, 41 | .btn-danger .badge { 42 | text-shadow: none; 43 | } 44 | .btn:active, 45 | .btn.active { 46 | background-image: none; 47 | } 48 | .btn-default { 49 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); 50 | background-image: -o-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); 51 | background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%); 52 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 53 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 54 | background-repeat: repeat-x; 55 | border-color: #dbdbdb; 56 | text-shadow: 0 1px 0 #fff; 57 | border-color: #ccc; 58 | } 59 | .btn-default:hover, 60 | .btn-default:focus { 61 | background-color: #e0e0e0; 62 | background-position: 0 -15px; 63 | } 64 | .btn-default:active, 65 | .btn-default.active { 66 | background-color: #e0e0e0; 67 | border-color: #dbdbdb; 68 | } 69 | .btn-default:disabled, 70 | .btn-default[disabled] { 71 | background-color: #e0e0e0; 72 | background-image: none; 73 | } 74 | .btn-primary { 75 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 76 | background-image: -o-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 77 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 78 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 79 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 80 | background-repeat: repeat-x; 81 | border-color: #2b669a; 82 | } 83 | .btn-primary:hover, 84 | .btn-primary:focus { 85 | background-color: #2d6ca2; 86 | background-position: 0 -15px; 87 | } 88 | .btn-primary:active, 89 | .btn-primary.active { 90 | background-color: #2d6ca2; 91 | border-color: #2b669a; 92 | } 93 | .btn-primary:disabled, 94 | .btn-primary[disabled] { 95 | background-color: #2d6ca2; 96 | background-image: none; 97 | } 98 | .btn-success { 99 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 100 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 101 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 102 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 103 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 104 | background-repeat: repeat-x; 105 | border-color: #3e8f3e; 106 | } 107 | .btn-success:hover, 108 | .btn-success:focus { 109 | background-color: #419641; 110 | background-position: 0 -15px; 111 | } 112 | .btn-success:active, 113 | .btn-success.active { 114 | background-color: #419641; 115 | border-color: #3e8f3e; 116 | } 117 | .btn-success:disabled, 118 | .btn-success[disabled] { 119 | background-color: #419641; 120 | background-image: none; 121 | } 122 | .btn-info { 123 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 124 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 125 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 126 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 127 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 128 | background-repeat: repeat-x; 129 | border-color: #28a4c9; 130 | } 131 | .btn-info:hover, 132 | .btn-info:focus { 133 | background-color: #2aabd2; 134 | background-position: 0 -15px; 135 | } 136 | .btn-info:active, 137 | .btn-info.active { 138 | background-color: #2aabd2; 139 | border-color: #28a4c9; 140 | } 141 | .btn-info:disabled, 142 | .btn-info[disabled] { 143 | background-color: #2aabd2; 144 | background-image: none; 145 | } 146 | .btn-warning { 147 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 148 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 149 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 150 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 151 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 152 | background-repeat: repeat-x; 153 | border-color: #e38d13; 154 | } 155 | .btn-warning:hover, 156 | .btn-warning:focus { 157 | background-color: #eb9316; 158 | background-position: 0 -15px; 159 | } 160 | .btn-warning:active, 161 | .btn-warning.active { 162 | background-color: #eb9316; 163 | border-color: #e38d13; 164 | } 165 | .btn-warning:disabled, 166 | .btn-warning[disabled] { 167 | background-color: #eb9316; 168 | background-image: none; 169 | } 170 | .btn-danger { 171 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 172 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 173 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 174 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 175 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 176 | background-repeat: repeat-x; 177 | border-color: #b92c28; 178 | } 179 | .btn-danger:hover, 180 | .btn-danger:focus { 181 | background-color: #c12e2a; 182 | background-position: 0 -15px; 183 | } 184 | .btn-danger:active, 185 | .btn-danger.active { 186 | background-color: #c12e2a; 187 | border-color: #b92c28; 188 | } 189 | .btn-danger:disabled, 190 | .btn-danger[disabled] { 191 | background-color: #c12e2a; 192 | background-image: none; 193 | } 194 | .thumbnail, 195 | .img-thumbnail { 196 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 197 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 198 | } 199 | .dropdown-menu > li > a:hover, 200 | .dropdown-menu > li > a:focus { 201 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 202 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 203 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 204 | background-repeat: repeat-x; 205 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 206 | background-color: #e8e8e8; 207 | } 208 | .dropdown-menu > .active > a, 209 | .dropdown-menu > .active > a:hover, 210 | .dropdown-menu > .active > a:focus { 211 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 212 | background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%); 213 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 214 | background-repeat: repeat-x; 215 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 216 | background-color: #357ebd; 217 | } 218 | .navbar-default { 219 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); 220 | background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); 221 | background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); 222 | background-repeat: repeat-x; 223 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 224 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 225 | border-radius: 4px; 226 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 227 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 228 | } 229 | .navbar-default .navbar-nav > .open > a, 230 | .navbar-default .navbar-nav > .active > a { 231 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 232 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 233 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 234 | background-repeat: repeat-x; 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 236 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); 237 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); 238 | } 239 | .navbar-brand, 240 | .navbar-nav > li > a { 241 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); 242 | } 243 | .navbar-inverse { 244 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%); 245 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222222 100%); 246 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%); 247 | background-repeat: repeat-x; 248 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 249 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 250 | } 251 | .navbar-inverse .navbar-nav > .open > a, 252 | .navbar-inverse .navbar-nav > .active > a { 253 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 254 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 255 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 256 | background-repeat: repeat-x; 257 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 258 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); 259 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); 260 | } 261 | .navbar-inverse .navbar-brand, 262 | .navbar-inverse .navbar-nav > li > a { 263 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 264 | } 265 | .navbar-static-top, 266 | .navbar-fixed-top, 267 | .navbar-fixed-bottom { 268 | border-radius: 0; 269 | } 270 | .alert { 271 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); 272 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 273 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 274 | } 275 | .alert-success { 276 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 277 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 278 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 279 | background-repeat: repeat-x; 280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 281 | border-color: #b2dba1; 282 | } 283 | .alert-info { 284 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 285 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 286 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 287 | background-repeat: repeat-x; 288 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 289 | border-color: #9acfea; 290 | } 291 | .alert-warning { 292 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 293 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 294 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 295 | background-repeat: repeat-x; 296 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 297 | border-color: #f5e79e; 298 | } 299 | .alert-danger { 300 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 301 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 302 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 303 | background-repeat: repeat-x; 304 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 305 | border-color: #dca7a7; 306 | } 307 | .progress { 308 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 309 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 310 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 311 | background-repeat: repeat-x; 312 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 313 | } 314 | .progress-bar { 315 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 316 | background-image: -o-linear-gradient(top, #428bca 0%, #3071a9 100%); 317 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 318 | background-repeat: repeat-x; 319 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 320 | } 321 | .progress-bar-success { 322 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 323 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 324 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 325 | background-repeat: repeat-x; 326 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 327 | } 328 | .progress-bar-info { 329 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 330 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 331 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 332 | background-repeat: repeat-x; 333 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 334 | } 335 | .progress-bar-warning { 336 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 337 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 338 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 339 | background-repeat: repeat-x; 340 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 341 | } 342 | .progress-bar-danger { 343 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 344 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 345 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 346 | background-repeat: repeat-x; 347 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 348 | } 349 | .progress-bar-striped { 350 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); 351 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); 352 | background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); 353 | } 354 | .list-group { 355 | border-radius: 4px; 356 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 357 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 358 | } 359 | .list-group-item.active, 360 | .list-group-item.active:hover, 361 | .list-group-item.active:focus { 362 | text-shadow: 0 -1px 0 #3071a9; 363 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 364 | background-image: -o-linear-gradient(top, #428bca 0%, #3278b3 100%); 365 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 366 | background-repeat: repeat-x; 367 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 368 | border-color: #3278b3; 369 | } 370 | .panel { 371 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 372 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 373 | } 374 | .panel-default > .panel-heading { 375 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 376 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 377 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 378 | background-repeat: repeat-x; 379 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 380 | } 381 | .panel-primary > .panel-heading { 382 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 383 | background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%); 384 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 385 | background-repeat: repeat-x; 386 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 387 | } 388 | .panel-success > .panel-heading { 389 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 390 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 391 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 392 | background-repeat: repeat-x; 393 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 394 | } 395 | .panel-info > .panel-heading { 396 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 397 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 398 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 399 | background-repeat: repeat-x; 400 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 401 | } 402 | .panel-warning > .panel-heading { 403 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 404 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 405 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 406 | background-repeat: repeat-x; 407 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 408 | } 409 | .panel-danger > .panel-heading { 410 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 411 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 412 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 413 | background-repeat: repeat-x; 414 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 415 | } 416 | .well { 417 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 418 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 419 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 420 | background-repeat: repeat-x; 421 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 422 | border-color: #dcdcdc; 423 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 424 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 425 | } 426 | -------------------------------------------------------------------------------- /test/appium/www/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=109fa5b75f9e34ab14e2) 9 | * Config saved to config.json and https://gist.github.com/109fa5b75f9e34ab14e2 10 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-default .badge,.btn-primary .badge,.btn-success .badge,.btn-info .badge,.btn-warning .badge,.btn-danger .badge{text-shadow:none}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:-o-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:linear-gradient(to bottom, #fff 0, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top, #428bca 0, #2d6ca2 100%);background-image:-o-linear-gradient(top, #428bca 0, #2d6ca2 100%);background-image:linear-gradient(to bottom, #428bca 0, #2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:-o-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:-o-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:-o-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:-o-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:-o-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:-o-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:-o-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:linear-gradient(to bottom, #fff 0, #f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #dbdbdb 0, #e2e2e2 100%);background-image:-o-linear-gradient(top, #dbdbdb 0, #e2e2e2 100%);background-image:linear-gradient(to bottom, #dbdbdb 0, #e2e2e2 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:-o-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:linear-gradient(to bottom, #3c3c3c 0, #222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #080808 0, #0f0f0f 100%);background-image:-o-linear-gradient(top, #080808 0, #0f0f0f 100%);background-image:linear-gradient(to bottom, #080808 0, #0f0f0f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:-o-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:-o-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:-o-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:-o-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:-o-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top, #428bca 0, #3071a9 100%);background-image:-o-linear-gradient(top, #428bca 0, #3071a9 100%);background-image:linear-gradient(to bottom, #428bca 0, #3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:-o-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:-o-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:-o-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:-o-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top, #428bca 0, #3278b3 100%);background-image:-o-linear-gradient(top, #428bca 0, #3278b3 100%);background-image:linear-gradient(to bottom, #428bca 0, #3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:-o-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:-o-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:-o-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:-o-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:-o-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:-o-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:linear-gradient(to bottom, #f2dede 0, #ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:-o-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /test/appium/www/css/main.css: -------------------------------------------------------------------------------- 1 | .main { 2 | border-top: 24px solid #a7a7a7; 3 | } 4 | 5 | .btn { 6 | margin: 10px 0; 7 | } 8 | 9 | .status { 10 | padding: 4px; 11 | border-radius: 4px; 12 | color: #fff; 13 | text-align: center; 14 | font-size: 90%; 15 | } 16 | 17 | .main p { 18 | margin: 0; 19 | } 20 | 21 | .pass { 22 | background: #789048; 23 | } 24 | 25 | .fail { 26 | background: #7D1A0C; 27 | } 28 | -------------------------------------------------------------------------------- /test/appium/www/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timflapper/cordova-spotify-plugin/424f3a900c4915a0cdf0e20fccd51c7185349bb7/test/appium/www/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /test/appium/www/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timflapper/cordova-spotify-plugin/424f3a900c4915a0cdf0e20fccd51c7185349bb7/test/appium/www/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /test/appium/www/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timflapper/cordova-spotify-plugin/424f3a900c4915a0cdf0e20fccd51c7185349bb7/test/appium/www/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /test/appium/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 81 |
82 | 92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /test/ios/MockCommandDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // MockCommandDelegate.h 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 08/05/14. 6 | // 7 | // 8 | 9 | #import "Cordova/CDV.h" 10 | 11 | typedef void (^mockPluginResultCallback)(CDVPluginResult *result, NSString *callbackId); 12 | 13 | @interface MockCommandDelegate : NSObject 14 | 15 | - (void)mockPluginResult:(mockPluginResultCallback)callback; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /test/ios/MockCommandDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // MockCommandDelegate.m 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 08/05/14. 6 | // 7 | // 8 | 9 | #import "MockCommandDelegate.h" 10 | 11 | @interface MockCommandDelegate() 12 | @property (copy, nonatomic) mockPluginResultCallback callback; 13 | @end 14 | 15 | @implementation MockCommandDelegate 16 | 17 | - (id)init 18 | { 19 | self = [super init]; 20 | 21 | if (self) { 22 | 23 | } 24 | 25 | return self; 26 | } 27 | 28 | - (void)mockPluginResult:(mockPluginResultCallback)callback 29 | { 30 | self.callback = callback; 31 | } 32 | 33 | - (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId 34 | { 35 | 36 | double delayInSeconds = 0.01; 37 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); 38 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 39 | if (self.callback) 40 | self.callback(result, callbackId); 41 | }); 42 | } 43 | 44 | #pragma mark neccesary but unused protocol methods 45 | 46 | - (NSString*)pathForResource:(NSString*)resourcepath 47 | { 48 | return @""; 49 | } 50 | 51 | - (id)getCommandInstance:(NSString*)pluginName 52 | { 53 | return nil; 54 | } 55 | 56 | - (BOOL)execute:(CDVInvokedUrlCommand*)command 57 | { 58 | return YES; 59 | } 60 | 61 | - (void)evalJs:(NSString*)js 62 | { 63 | 64 | } 65 | 66 | - (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop 67 | { 68 | 69 | } 70 | 71 | - (void)runInBackground:(void (^)())block 72 | { 73 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); 74 | } 75 | 76 | - (NSString*)userAgent 77 | { 78 | return @""; 79 | } 80 | 81 | - (BOOL)URLIsWhitelisted:(NSURL*)url 82 | { 83 | return YES; 84 | } 85 | 86 | - (NSDictionary*)settings 87 | { 88 | return @{}; 89 | } 90 | @end 91 | -------------------------------------------------------------------------------- /test/ios/SpotifyAudioPlayer+Mock.h: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyAudioPlayer+Mock.h 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 09/05/14. 6 | // 7 | // 8 | 9 | #import "SpotifyAudioPlayer.h" 10 | 11 | typedef void (^mockResultCallback)(id callback); 12 | 13 | @interface SpotifyAudioPlayer (Mock) 14 | + (void)clearTestValues; 15 | 16 | + (void)setNextCallback:(mockResultCallback)block; 17 | 18 | + (void)setNextCallback:(mockResultCallback)block afterDelayInSeconds:(NSTimeInterval)delayInSeconds; 19 | 20 | + (void)setNextMethodReturn:(id)returnValue; 21 | 22 | + (void)setNextEvent:(NSDictionary *)event; 23 | @end 24 | -------------------------------------------------------------------------------- /test/ios/SpotifyAudioPlayer+Mock.m: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyAudioPlayer+Mock.m 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 09/05/14. 6 | // 7 | // 8 | 9 | #import "SpotifyAudioPlayer+Mock.h" 10 | #import 11 | 12 | static char const * const nextCallbackKey = "__nextCallbackForTesting"; 13 | static char const * const delayInSecondsKey = "__delayInSecondsTesting"; 14 | static char const * const nextReturnKey = "__nextReturnForTesting"; 15 | static char const * const nextEventKey = "__nextEventForTesting"; 16 | 17 | 18 | void runBlockAfterDelayInSeconds(NSTimeInterval delayInSeconds, dispatch_block_t block) { 19 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); 20 | dispatch_after(popTime, dispatch_get_main_queue(), block); 21 | } 22 | 23 | @implementation SpotifyAudioPlayer (Mock) 24 | + (void)load 25 | { 26 | static dispatch_once_t onceToken; 27 | dispatch_once(&onceToken, ^{ 28 | [self clearTestValues]; 29 | }); 30 | 31 | [super load]; 32 | } 33 | 34 | + (void)clearTestValues 35 | { 36 | objc_setAssociatedObject([self class], nextCallbackKey, nil, OBJC_ASSOCIATION_RETAIN); 37 | objc_setAssociatedObject([self class], delayInSecondsKey, @0, OBJC_ASSOCIATION_RETAIN); 38 | objc_setAssociatedObject([self class], nextReturnKey, nil, OBJC_ASSOCIATION_RETAIN); 39 | objc_setAssociatedObject([self class], nextEventKey, nil, OBJC_ASSOCIATION_RETAIN); 40 | } 41 | 42 | + (void)setNextCallback:(mockResultCallback)block 43 | { 44 | objc_setAssociatedObject([self class], nextCallbackKey, block, OBJC_ASSOCIATION_COPY); 45 | } 46 | 47 | + (void)setNextCallback:(mockResultCallback)block afterDelayInSeconds:(NSTimeInterval)delayInSeconds 48 | { 49 | objc_setAssociatedObject([self class], delayInSecondsKey, [NSNumber numberWithDouble:delayInSeconds], OBJC_ASSOCIATION_RETAIN); 50 | objc_setAssociatedObject([self class], nextCallbackKey, block, OBJC_ASSOCIATION_COPY); 51 | } 52 | 53 | + (void)setNextMethodReturn:(id)returnValue 54 | { 55 | objc_setAssociatedObject([self class], nextReturnKey, returnValue, OBJC_ASSOCIATION_RETAIN); 56 | } 57 | 58 | + (void)setNextEvent:(NSDictionary *)event 59 | { 60 | objc_setAssociatedObject([self class], nextEventKey, event, OBJC_ASSOCIATION_RETAIN); 61 | } 62 | 63 | + (void)invokeNextCallback:(id)block 64 | { 65 | mockResultCallback callback = objc_getAssociatedObject([self class], nextCallbackKey); 66 | NSTimeInterval delayInSeconds = ((NSNumber *)objc_getAssociatedObject([self class], delayInSecondsKey)).doubleValue; 67 | 68 | if (! callback) 69 | return; 70 | 71 | if (delayInSeconds > 0) { 72 | runBlockAfterDelayInSeconds(delayInSeconds, ^{ 73 | callback(block); 74 | }); 75 | } else { 76 | callback(block); 77 | } 78 | } 79 | 80 | + (id)getNextMethodReturn 81 | { 82 | return objc_getAssociatedObject([self class], nextReturnKey); 83 | } 84 | 85 | + (NSDictionary *)getNextEvent 86 | { 87 | return objc_getAssociatedObject([self class], nextEventKey); 88 | } 89 | 90 | - (void)loginWithSession:(SPTSession *)session callback:(SPTErrorableOperationCallback)block 91 | { 92 | [[self class] invokeNextCallback:block]; 93 | 94 | [self audioStreamingDidLogin:self]; 95 | } 96 | 97 | - (void)playURI:(NSURL *)uri callback:(SPTErrorableOperationCallback)block 98 | { 99 | [[self class] invokeNextCallback:block]; 100 | 101 | if ([[self class] getNextMethodReturn] != nil) 102 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 103 | 104 | [self audioStreaming:self didChangePlaybackStatus:YES]; 105 | } 106 | 107 | - (void)playURI:(NSURL *)uri fromIndex:(int)index callback:(SPTErrorableOperationCallback)block 108 | { 109 | [self playURI:uri callback:block]; 110 | } 111 | 112 | - (void)playURIs:(NSArray *)uris fromIndex:(int)index callback:(SPTErrorableOperationCallback)block 113 | { 114 | [[self class] invokeNextCallback:block]; 115 | 116 | if ([[self class] getNextMethodReturn] != nil) 117 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 118 | 119 | [self audioStreaming:self didChangePlaybackStatus:YES]; 120 | } 121 | 122 | - (void)setURIs:(NSArray *)uris callback:(SPTErrorableOperationCallback)block 123 | { 124 | [[self class] invokeNextCallback:block]; 125 | } 126 | 127 | - (void)playURIsFromIndex:(int)index callback:(SPTErrorableOperationCallback)block 128 | { 129 | [[self class] invokeNextCallback:block]; 130 | 131 | if ([[self class] getNextMethodReturn] != nil) 132 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 133 | 134 | [self audioStreaming:self didChangePlaybackStatus:YES]; 135 | } 136 | 137 | - (void)queueURI:(NSURL *)uri callback:(SPTErrorableOperationCallback)block 138 | { 139 | [[self class] invokeNextCallback:block]; 140 | } 141 | 142 | - (void)queueURI:(NSURL *)uri clearQueue:(BOOL)clear callback:(SPTErrorableOperationCallback)block 143 | { 144 | [[self class] invokeNextCallback:block]; 145 | } 146 | 147 | - (void)queueURIs:(NSArray *)uris clearQueue:(BOOL)clear callback:(SPTErrorableOperationCallback)block 148 | { 149 | [[self class] invokeNextCallback:block]; 150 | } 151 | 152 | - (void)queuePlay:(SPTErrorableOperationCallback)block 153 | { 154 | [[self class] invokeNextCallback:block]; 155 | 156 | if ([[self class] getNextMethodReturn] != nil) 157 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 158 | 159 | [self audioStreaming:self didChangePlaybackStatus:YES]; 160 | } 161 | 162 | - (void)queueClear:(SPTErrorableOperationCallback)block 163 | { 164 | [[self class] invokeNextCallback:block]; 165 | } 166 | 167 | - (void)stop:(SPTErrorableOperationCallback)block 168 | { 169 | [[self class] invokeNextCallback:block]; 170 | 171 | if ([[self class] getNextMethodReturn] != nil) 172 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 173 | 174 | [self audioStreaming:self didChangePlaybackStatus:YES]; 175 | } 176 | 177 | - (void)skipNext:(SPTErrorableOperationCallback)block 178 | { 179 | [[self class] invokeNextCallback:block]; 180 | 181 | if ([[self class] getNextMethodReturn] != nil) 182 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 183 | 184 | [self audioStreaming:self didChangePlaybackStatus:YES]; 185 | 186 | } 187 | 188 | - (void)skipPrevious:(SPTErrorableOperationCallback)block 189 | { 190 | [[self class] invokeNextCallback:block]; 191 | 192 | if ([[self class] getNextMethodReturn] != nil) 193 | [self audioStreaming:self didChangeToTrack:[[self class] getNextMethodReturn]]; 194 | 195 | [self audioStreaming:self didChangePlaybackStatus:YES]; 196 | 197 | } 198 | 199 | - (void)seekToOffset:(NSTimeInterval)offset callback:(SPTErrorableOperationCallback)block 200 | { 201 | [[self class] invokeNextCallback:block]; 202 | } 203 | 204 | - (BOOL)isPlaying 205 | { 206 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 207 | 208 | return methodReturn.boolValue; 209 | } 210 | 211 | - (void)setIsPlaying:(BOOL)playing callback:(SPTErrorableOperationCallback)block 212 | { 213 | [self audioStreaming:self didChangePlaybackStatus:playing]; 214 | 215 | [[self class] invokeNextCallback:block]; 216 | } 217 | 218 | - (SPTBitrate)targetBitrate 219 | { 220 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 221 | 222 | return methodReturn.intValue; 223 | } 224 | 225 | - (void)setTargetBitrate:(SPTBitrate)bitrate callback:(SPTErrorableOperationCallback)block 226 | { 227 | [[self class] invokeNextCallback:block]; 228 | } 229 | 230 | - (NSUInteger)diskCacheSizeLimit 231 | { 232 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 233 | 234 | return methodReturn.intValue; 235 | } 236 | 237 | - (void)setDiskCacheSizeLimit:(NSUInteger)diskCacheSizeLimit 238 | { 239 | return; 240 | } 241 | 242 | - (SPTVolume)volume 243 | { 244 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 245 | 246 | return methodReturn.doubleValue; 247 | } 248 | 249 | - (void)setVolume:(SPTVolume)volume callback:(SPTErrorableOperationCallback)block 250 | { 251 | [[self class] invokeNextCallback:block]; 252 | [self audioStreaming:self didChangeVolume:volume]; 253 | } 254 | 255 | - (BOOL)repeat 256 | { 257 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 258 | 259 | return methodReturn.boolValue; 260 | } 261 | 262 | - (void)setRepeat:(BOOL)repeat 263 | { 264 | [self audioStreaming:self didChangeRepeatStatus:repeat]; 265 | } 266 | 267 | - (BOOL)shuffle 268 | { 269 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 270 | 271 | return methodReturn.boolValue; 272 | } 273 | 274 | - (void)setShuffle:(BOOL)shuffle 275 | { 276 | [self audioStreaming:self didChangeShuffleStatus:shuffle]; 277 | } 278 | 279 | - (int)queueSize 280 | { 281 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 282 | 283 | return methodReturn.intValue; 284 | } 285 | 286 | - (int)trackListSize 287 | { 288 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 289 | 290 | return methodReturn.intValue; 291 | } 292 | 293 | - (NSDictionary *)currentTrackMetadata 294 | { 295 | return [[self class] getNextMethodReturn]; 296 | } 297 | 298 | - (void)getRelativeTrackMetadata:(int)index callback:(void (^)(NSDictionary *))block 299 | { 300 | [[self class] invokeNextCallback:block]; 301 | } 302 | 303 | - (void)getAbsoluteTrackMetadata:(int)index callback:(void (^)(NSDictionary *))block 304 | { 305 | [[self class] invokeNextCallback:block]; 306 | } 307 | 308 | - (NSTimeInterval)currentPlaybackPosition 309 | { 310 | NSNumber *methodReturn = [[self class] getNextMethodReturn]; 311 | 312 | return methodReturn.doubleValue; 313 | } 314 | 315 | @end 316 | -------------------------------------------------------------------------------- /test/ios/SpotifyPluginTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.timflapper.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/ios/TestData/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "external_urls" : { 3 | "spotify" : "https://open.spotify.com/user/awesomeuser" 4 | }, 5 | "href" : "https://api.spotify.com/v1/users/awesomeuser", 6 | "id" : "awesomeuser", 7 | "type" : "user", 8 | "uri" : "spotify:user:awesomeuser" 9 | } 10 | -------------------------------------------------------------------------------- /test/ios/TestData/refresh.json: -------------------------------------------------------------------------------- 1 | {"access_token":"Ab4wVEt-33dSMLfD_Nd_GLjBGX5TQc0VrQRIXW2WVP69Z5I_bIw4YIumYsOPXNA-1-6HLdS_XdfX9FXrtezc-ltUAT6cj69scFrqxJPWV12mK-224W0ekpsi-WQe_T1OYSZbyv00abgBopOzx9AOH5sd","token_type":"Bearer","expires_in":3600} 2 | -------------------------------------------------------------------------------- /test/ios/TestData/refresh_failed.json: -------------------------------------------------------------------------------- 1 | {"error":"unauthorized"} 2 | -------------------------------------------------------------------------------- /test/ios/TestData/session.json: -------------------------------------------------------------------------------- 1 | {"access_token":"Ab4wVEt-33dSMLfD_Nd_GLjBGX5TQc0VrQRIXW2WVP69Z5I_bIw4YIumYsOPXNA-1-6HLdS_XdfX9FXrtezc-ltUAT6cj69scFrqxJPWV12mK-224W0ekpsi-WQe_T1OYSZbyv00abgBopOzx9AOH5sd","token_type":"Bearer","expires_in":3600,"refresh_token":"XXXX_qPeacCRHWujLagqGV0khtZ_jaF_Ek8VI80g7HpAzjmbQZHz1j5_0YbcpSvi31mE7AMipJcYGQ9_p_65elCf_OS6vIhhNJRCmlOPc3RJVjuNdadQTR9sucB413X4Xx"} 2 | -------------------------------------------------------------------------------- /test/ios/testShared.h: -------------------------------------------------------------------------------- 1 | // 2 | // shared.h 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 07/05/14. 6 | // 7 | // 8 | 9 | #import 10 | 11 | NSData *getDataFromTestDataFile(NSString *filename); 12 | 13 | void waitForSecondsOrDone(NSTimeInterval noOfSeconds, BOOL *done); 14 | 15 | NSError *errorForTesting(); -------------------------------------------------------------------------------- /test/ios/testShared.m: -------------------------------------------------------------------------------- 1 | // 2 | // shared.m 3 | // SpotifyPlugin 4 | // 5 | // Created by Tim Flapper on 07/05/14. 6 | // 7 | // 8 | 9 | #import "SpotifyPlugin.h" 10 | #import 11 | 12 | NSData *getDataFromTestDataFile(NSString *filename) { 13 | NSString *path = [[NSBundle bundleForClass: [SpotifyPlugin class]] pathForResource:filename ofType:@"" inDirectory:@"TestData"]; 14 | 15 | return [NSData dataWithContentsOfFile:path]; 16 | } 17 | 18 | void waitForSecondsOrDone(NSTimeInterval noOfSeconds, BOOL *done) { 19 | NSDate* timeoutDate = [NSDate dateWithTimeIntervalSinceNow:noOfSeconds]; 20 | while (!*done && ([timeoutDate timeIntervalSinceNow]>0)) 21 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.005, YES); 22 | } 23 | 24 | 25 | NSError *errorForTesting() { 26 | return [NSError errorWithDomain:@"for.testing.ErrorDomain" code:42 userInfo:@{NSLocalizedDescriptionKey: @"Nope"}]; 27 | } -------------------------------------------------------------------------------- /test/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /test/js/spec/lib/event-dispatcher.test.js: -------------------------------------------------------------------------------- 1 | describe('EventDispatcher', function() { 2 | var EventDispatcher; 3 | 4 | beforeEach(function() { 5 | EventDispatcher = EventDispatcher || require('com.timflapper.spotify.event-dispatcher'); 6 | 7 | this.eventCallback = sinon.spy(); 8 | 9 | this.eventDispatcher = new EventDispatcher(); 10 | this.eventDispatcher.addEventListener('test', this.eventCallback); 11 | }); 12 | 13 | describe('listening to event', function() { 14 | beforeEach(function() { 15 | this.eventDispatcher.dispatchEvent('test'); 16 | }); 17 | 18 | it('should have been called', function() { 19 | expect(this.eventCallback).to.have.been.calledWith(); 20 | }); 21 | }); 22 | 23 | describe('removing event', function() { 24 | beforeEach(function() { 25 | this.eventDispatcher.removeEventListener('test', this.eventCallback); 26 | this.eventDispatcher.dispatchEvent('test'); 27 | }); 28 | 29 | it('should not have been called', function() { 30 | expect(this.eventCallback).to.not.have.been.called; 31 | }); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/js/spec/lib/remote.test.js: -------------------------------------------------------------------------------- 1 | function createExpirationDate(date) { 2 | date = date || new Date(); 3 | 4 | return date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + date.getUTCDate() + " " + date.getUTCHours() + ":" + date.getUTCMinutes() + ":" + date.getUTCSeconds() + " GMT"; 5 | } 6 | 7 | var session = {canonicalUsername: 'antman', accessToken: 'Xcd4r234234fdfa_dfsadf3', encryptedRefreshToken: 'sdfsdfds724dfsdf234dsf', expirationDate: createExpirationDate()}; 8 | 9 | function sharedRemoteTests() { 10 | it('should load the request', function() { 11 | expect(this.callback).to.have.been.calledWith(null, this.request.response); 12 | }); 13 | } 14 | 15 | function sharedHooks() { 16 | beforeEach(function() { 17 | var self = this; 18 | this.server.respondWith(function(xhr, id) { 19 | var request = self.request; 20 | 21 | if (xhr.method === request.method && xhr.url === request.url) { 22 | self.requestHeaders = xhr.requestHeaders; 23 | self.requestBody = xhr.requestBody; 24 | xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(request.response)); 25 | } 26 | }); 27 | }); 28 | } 29 | 30 | describe('remote', function() { 31 | var remote; 32 | 33 | before(function() { remote = require('com.timflapper.spotify.remote'); }); 34 | 35 | beforeEach(function() { 36 | this.callback = sinon.spy(); 37 | this.server = sinon.fakeServer.create(); 38 | }); 39 | 40 | afterEach(function () { this.server.restore(); }); 41 | 42 | describe('empty options', function() { 43 | it('should throw an error', function() { 44 | var subject = function() { remote(); } 45 | 46 | expect(subject).to.throw('This method requires two arguments (options, callback)'); 47 | }); 48 | }); 49 | 50 | describe('basic options', function() { 51 | sharedHooks(); 52 | 53 | beforeEach(function() { 54 | this.request = { method: 'GET', url: 'https://api.spotify.com/v1/bla', response: {test: 'get'} }; 55 | remote({uri: '/bla'}, this.callback); 56 | this.server.respond(); 57 | }); 58 | 59 | sharedRemoteTests(); 60 | }); 61 | 62 | describe('different method', function() { 63 | sharedHooks(); 64 | 65 | beforeEach(function() { 66 | this.request = { method: 'POST', url: 'https://api.spotify.com/v1/blabla', response: {test: 'post'} }; 67 | remote({uri: '/blabla', method: 'post'}, this.callback); 68 | this.server.respond(); 69 | }); 70 | 71 | sharedRemoteTests(); 72 | }); 73 | 74 | describe('url instead of uri', function() { 75 | sharedHooks(); 76 | 77 | beforeEach(function() { 78 | this.request = { method: 'GET', url: 'http://google.com/blabla', response: {test: 'url'} }; 79 | remote({url: 'http://google.com/blabla'}, this.callback); 80 | this.server.respond(); 81 | }); 82 | 83 | sharedRemoteTests(); 84 | }); 85 | 86 | describe('with session', function() { 87 | sharedHooks(); 88 | 89 | beforeEach(function() { 90 | this.request = { method: 'GET', url: 'https://api.spotify.com/v1/session', response: {test: 'session'} }; 91 | remote({uri: '/session', session: session}, this.callback); 92 | this.server.respond(); 93 | }); 94 | 95 | sharedRemoteTests(); 96 | 97 | it('should send the Bearer token as part of the request', function() { 98 | expect(this.requestHeaders).to.have.property('Authorization', 'Bearer '+session.accessToken); 99 | }); 100 | }); 101 | 102 | describe('with data', function() { 103 | sharedHooks(); 104 | 105 | beforeEach(function() { 106 | this.request = { method: 'POST', url: 'https://api.spotify.com/v1/data', response: {test: 'data'} }; 107 | remote({uri: '/data', method: 'POST', data: 'DATA'}, this.callback); 108 | this.server.respond(); 109 | }); 110 | 111 | sharedRemoteTests(); 112 | 113 | it('should send the Bearer token as part of the request', function() { 114 | expect(this.requestBody).to.equal('DATA'); 115 | }); 116 | }); 117 | 118 | describe('pagination', function() { 119 | beforeEach(function() { 120 | this.nextUrl = 'http://next.url'; 121 | this.prevUrl ='http://previous.url'; 122 | }); 123 | 124 | describe('on root object', function() { 125 | beforeEach(function() { 126 | this.responseBody = { next: this.nextUrl, prev: this.prevUrl }; 127 | this.findPaginated = function(data) { return data; }; 128 | }); 129 | 130 | shared.paginatedTests(); 131 | }); 132 | 133 | describe('artists', function() { 134 | beforeEach(function() { 135 | this.responseBody = { artists: { next: this.nextUrl, prev: this.prevUrl } }; 136 | this.findPaginated = function(data) { return data.artists; }; 137 | }); 138 | 139 | shared.paginatedTests(); 140 | }); 141 | 142 | describe('albums', function() { 143 | beforeEach(function() { 144 | this.responseBody = { albums: { next: this.nextUrl, prev: this.prevUrl } }; 145 | this.findPaginated = function(data) { return data.albums; }; 146 | }); 147 | 148 | shared.paginatedTests(); 149 | }); 150 | 151 | describe('tracks', function() { 152 | beforeEach(function() { 153 | this.responseBody = { tracks: { next: this.nextUrl, prev: this.prevUrl } }; 154 | this.findPaginated = function(data) { return data.tracks; }; 155 | }); 156 | 157 | shared.paginatedTests(); 158 | }); 159 | 160 | describe('tracks in an array of albums', function() { 161 | beforeEach(function() { 162 | var self = this; 163 | this.responseBody = { albums: [{ tracks: { next: this.nextUrl, prev: this.prevUrl } }, { tracks: {next: null, prev: null} }, { tracks: { next: this.nextUrl, prev: this.prevUrl } }] }; 164 | }); 165 | 166 | describe('first album', function() { 167 | beforeEach(function() { this.findPaginated = function(data) { return data.albums[0].tracks; }; }); 168 | shared.paginatedTests(); 169 | }); 170 | 171 | describe('second album', function() { 172 | beforeEach(function() { this.findPaginated = function(data) { return data.albums[1].tracks; }; }); 173 | 174 | shared.hooksForPagination(); 175 | 176 | it('should not modify the previous and next objects', function(done) { 177 | var findPaginated = this.findPaginated; 178 | remote({uri: '/paginate'}, function(err, data) { 179 | data = findPaginated(data); 180 | expect(data.next).to.be.null; 181 | expect(data.prev).to.be.null; 182 | done(); 183 | }); 184 | this.server.respond(); 185 | }); 186 | }); 187 | 188 | describe('third album', function() { 189 | beforeEach(function() { this.findPaginated = function(data) { return data.albums[2].tracks; }; }); 190 | shared.paginatedTests(); 191 | }); 192 | }); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/js/spec/lib/utils.test.js: -------------------------------------------------------------------------------- 1 | describe('utils', function() { 2 | var utils; 3 | 4 | before(function() { 5 | utils = require('com.timflapper.spotify.utils'); 6 | }); 7 | 8 | describe('#exec', function() { 9 | context('no parameters', function() { 10 | it('should call xhr and return a message', function() { 11 | var onRequest = sinon.spy() 12 | , callback = sinon.spy(); 13 | 14 | mockExec(1, true, onRequest); 15 | utils.exec('test', callback); 16 | 17 | expect(onRequest).to.have.been.calledWith(['SpotifyPlugin', 'test', []]); 18 | 19 | expect(callback).to.have.been.calledWith(null, true); 20 | }); 21 | }); 22 | 23 | context('with parameters', function() { 24 | it('should call xhr and return a message', function() { 25 | var onRequest = sinon.spy() 26 | , callback = sinon.spy(); 27 | 28 | mockExec(1, 'message', onRequest); 29 | utils.exec('test', 'something', callback); 30 | 31 | expect(onRequest).to.have.been.calledWith(['SpotifyPlugin', 'test', ['something']]); 32 | 33 | expect(callback).to.have.been.calledWith(null, 'message'); 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/js/spec/shared/paginate.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | 3 | var shared = window.shared = window.shared || {}; 4 | 5 | shared.hooksForPagination = function() { 6 | beforeEach(function() { 7 | var body = JSON.stringify(this.responseBody); 8 | this.server.respondWith('GET', 'https://api.spotify.com/v1/paginate', 9 | [200, { 'Content-Type': 'application/json' }, 10 | body]); 11 | }); 12 | } 13 | 14 | shared.paginatedTests = function() { 15 | var remote; 16 | 17 | before(function() { remote = require('com.timflapper.spotify.remote'); }); 18 | 19 | beforeEach(function() { 20 | this.server.respondWith('GET', this.prevUrl, 21 | [200, { 'Content-Type': 'application/json' }, 22 | JSON.stringify({page: 'previous'})]); 23 | 24 | this.server.respondWith('GET', this.nextUrl, 25 | [200, { 'Content-Type': 'application/json' }, 26 | JSON.stringify({page: 'next'})]); 27 | }); 28 | 29 | shared.hooksForPagination(); 30 | 31 | it('should modify the previous and next objects', function(done) { 32 | var findPaginated = this.findPaginated; 33 | remote({uri: '/paginate'}, function(err, data) { 34 | data = findPaginated(data); 35 | expect(data.next).to.be.a('function'); 36 | expect(data.prev).to.be.a('function'); 37 | done(); 38 | }); 39 | this.server.respond(); 40 | }); 41 | 42 | it('should be calling the correct previous url', function(done) { 43 | var self = this; 44 | var findPaginated = this.findPaginated; 45 | 46 | remote({uri: '/paginate'}, function(err, data) { 47 | data = findPaginated(data); 48 | data.prev(self.callback); 49 | self.server.respond(); 50 | expect(self.callback).to.have.been.calledWith(null, {page: 'previous'}); 51 | done(); 52 | }); 53 | this.server.respond(); 54 | }); 55 | 56 | it('should be calling the correct next url', function(done) { 57 | var self = this; 58 | var findPaginated = this.findPaginated; 59 | 60 | remote({uri: '/paginate'}, function(err, data) { 61 | data = findPaginated(data); 62 | data.next(self.callback); 63 | self.server.respond(); 64 | expect(self.callback).to.have.been.calledWith(null, {page: 'next'}); 65 | done(); 66 | }); 67 | this.server.respond(); 68 | }); 69 | } 70 | 71 | })(window); 72 | -------------------------------------------------------------------------------- /test/js/spec/spotify/audio-player.test.js: -------------------------------------------------------------------------------- 1 | describe('AudioPlayer', function() { 2 | function createExpirationDate(date) { 3 | date = date || new Date(); 4 | 5 | return date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + date.getUTCDate() + " " + date.getUTCHours() + ":" + date.getUTCMinutes() + ":" + date.getUTCSeconds() + " GMT"; 6 | } 7 | 8 | var session = {canonicalUsername: 'antman', accessToken: 'Xcd4r234234fdfa_dfsadf3', encryptedRefreshToken: 'sdfsdfds724dfsdf234dsf', expirationDate: createExpirationDate()}; 9 | 10 | beforeEach(function() { 11 | this.player = spotify.createAudioPlayer('randomClientId'); 12 | this.callback = sinon.spy(); 13 | this.onRequest = sinon.spy(); 14 | }); 15 | 16 | afterEach(function() { 17 | restoreMockExec(); 18 | }); 19 | 20 | describe('spotify.createAudioPlayer', function() { 21 | var EventDispatcher; 22 | 23 | before(function() { 24 | EventDispatcher = require('com.timflapper.spotify.event-dispatcher'); 25 | }); 26 | 27 | it('should be an EventDispatcher', function() { 28 | expect(this.player).to.be.an.instanceof(EventDispatcher); 29 | }); 30 | }); 31 | 32 | describe('#login', function() { 33 | describe('with callback', function() { 34 | beforeEach(function() { 35 | this.eventCallback = sinon.spy(); 36 | this.onMessageResult = sinon.spy(); 37 | this.player.addEventListener(spotify.AudioPlayer.EVENT_MESSAGE, this.eventCallback); 38 | mockExec(1, 3, this.onRequest); 39 | mockExec(1, {type: spotify.AudioPlayer.EVENT_MESSAGE, args: ['Test Message']}, this.onMessageResult); 40 | this.player.login(session, this.callback); 41 | }); 42 | 43 | it('should send the correct parameters to native', function() { 44 | expect(this.onRequest).to.have.been.calledWith([ 45 | 'SpotifyPlugin', 'createAudioPlayerAndLogin', ['randomClientId', session] 46 | ]); 47 | }); 48 | 49 | it('should login successfully', function() { 50 | expect(this.callback).to.have.been.calledWith(null); 51 | }); 52 | 53 | it('should subscribe to native events', function() { 54 | expect(this.onMessageResult).to.have.been.calledWith([ 55 | 'SpotifyPlugin', 'addAudioPlayerEventListener', [3] 56 | ]); 57 | }); 58 | 59 | it('should be able to dispatch events', function() { 60 | expect(this.eventCallback).to.have.been.calledWith('Test Message'); 61 | }); 62 | }); 63 | 64 | describe('without callback', function() { 65 | beforeEach(function() { 66 | this.loginEventCallback = sinon.spy(); 67 | this.messageEventCallback = sinon.spy(); 68 | this.onMessageResult = sinon.spy(); 69 | this.player.addEventListener(spotify.AudioPlayer.EVENT_LOGIN, this.loginEventCallback); 70 | this.player.addEventListener(spotify.AudioPlayer.EVENT_MESSAGE, this.messageEventCallback); 71 | mockExec(1, 3, this.onRequest); 72 | mockExec(1, {type: spotify.AudioPlayer.EVENT_MESSAGE, args: ['Test Message']}, this.onMessageResult); 73 | this.player.login(session); 74 | }); 75 | 76 | it('should send the correct parameters to native', function() { 77 | expect(this.onRequest).to.have.been.calledWith([ 78 | 'SpotifyPlugin', 'createAudioPlayerAndLogin', ['randomClientId', session] 79 | ]); 80 | }); 81 | 82 | it('should login successfully', function() { 83 | expect(this.loginEventCallback).to.have.been.calledWith(); 84 | }); 85 | 86 | it('should subscribe to native events', function() { 87 | expect(this.onMessageResult).to.have.been.calledWith([ 88 | 'SpotifyPlugin', 'addAudioPlayerEventListener', [3] 89 | ]); 90 | }); 91 | 92 | it('should be able to dispatch events', function() { 93 | expect(this.messageEventCallback).to.have.been.calledWith('Test Message'); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('#logout', function() { 99 | beforeEach(function() { 100 | this.eventCallback = sinon.spy(); 101 | mockExec(1, null, this.onRequest); 102 | this.player.id = 1; 103 | }); 104 | 105 | describe('with callback', function() { 106 | beforeEach(function() { 107 | this.player.logout(this.callback); 108 | }); 109 | 110 | it('should send the correct parameters to native', function() { 111 | expect(this.onRequest).to.have.been.calledWith([ 112 | 'SpotifyPlugin', 'audioPlayerLogout', [1] 113 | ]); 114 | }); 115 | 116 | it('should logout successfully', function() { 117 | expect(this.callback).to.have.been.calledWith(null); 118 | }); 119 | }); 120 | 121 | describe('without callback', function() { 122 | beforeEach(function() { 123 | this.player.addEventListener(spotify.AudioPlayer.EVENT_LOGOUT, this.eventCallback); 124 | this.player.logout(); 125 | }); 126 | 127 | it('should send the correct parameters to native', function() { 128 | expect(this.onRequest).to.have.been.calledWith([ 129 | 'SpotifyPlugin', 'audioPlayerLogout', [1] 130 | ]); 131 | }); 132 | 133 | it('should logout successfully', function() { 134 | expect(this.eventCallback).to.have.been.calledWith(); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('#play', function() { 140 | beforeEach(function() { 141 | mockExec(1, null, this.onRequest); 142 | this.player.id = 1; 143 | }); 144 | 145 | describe('playing single object', function() { 146 | beforeEach(function() { 147 | this.player.play('spotify:track:3XpXhVtZwqh2eM5d9ieXT5', this.callback); 148 | }); 149 | 150 | it('should send the correct parameters to native', function() { 151 | expect(this.onRequest).to.have.been.calledWith([ 152 | 'SpotifyPlugin', 'play', [1, 'spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 0] 153 | ]); 154 | }); 155 | 156 | it('should play successfully', function() { 157 | expect(this.callback).to.have.been.calledWith(null); 158 | }); 159 | }); 160 | 161 | describe('playing single object with index', function() { 162 | beforeEach(function() { 163 | this.player.play('spotify:album:36k5aXpxffjVGcNce12GLZ', 4, this.callback); 164 | }); 165 | 166 | it('should send the correct parameters to native', function() { 167 | expect(this.onRequest).to.have.been.calledWith([ 168 | 'SpotifyPlugin', 'play', [1, 'spotify:album:36k5aXpxffjVGcNce12GLZ', 4] 169 | ]); 170 | }); 171 | 172 | it('should play successfully', function() { 173 | expect(this.callback).to.have.been.calledWith(null); 174 | }); 175 | }); 176 | 177 | describe('playing array of objects', function() { 178 | beforeEach(function() { 179 | this.player.play(['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], this.callback); 180 | }); 181 | 182 | it('should send the correct parameters to native', function() { 183 | expect(this.onRequest).to.have.been.calledWith([ 184 | 'SpotifyPlugin', 'play', [1, ['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], 0] 185 | ]); 186 | }); 187 | 188 | it('should play successfully', function() { 189 | expect(this.callback).to.have.been.calledWith(null); 190 | }); 191 | }); 192 | 193 | describe('playing array of objects with index', function() { 194 | beforeEach(function() { 195 | this.player.play(['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], 1, this.callback); 196 | }); 197 | 198 | it('should send the correct parameters to native', function() { 199 | expect(this.onRequest).to.have.been.calledWith([ 200 | 'SpotifyPlugin', 'play', [1, ['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], 1] 201 | ]); 202 | }); 203 | 204 | it('should play successfully', function() { 205 | expect(this.callback).to.have.been.calledWith(null); 206 | }); 207 | }); 208 | }); 209 | 210 | describe('#setURIs', function() { 211 | beforeEach(function() { 212 | mockExec(1, null, this.onRequest); 213 | this.player.id = 1; 214 | this.player.setURIs(['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], this.callback); 215 | }); 216 | 217 | it('should send the correct parameters to native', function() { 218 | expect(this.onRequest).to.have.been.calledWith([ 219 | 'SpotifyPlugin', 'setURIs', [1, ['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs']] 220 | ]); 221 | }); 222 | 223 | it('should setURIs successfully', function() { 224 | expect(this.callback).to.have.been.calledWith(null); 225 | }); 226 | }); 227 | 228 | describe('#playURIsFromIndex', function() { 229 | beforeEach(function() { 230 | mockExec(1, null, this.onRequest); 231 | this.player.id = 1; 232 | this.player.playURIsFromIndex(65, this.callback); 233 | }); 234 | 235 | it('should send the correct parameters to native', function() { 236 | expect(this.onRequest).to.have.been.calledWith([ 237 | 'SpotifyPlugin', 'playURIsFromIndex', [1, 65] 238 | ]); 239 | }); 240 | 241 | it('should playURIsFromIndex successfully', function() { 242 | expect(this.callback).to.have.been.calledWith(null); 243 | }); 244 | }); 245 | 246 | describe('#queue', function() { 247 | describe('without clearQueue', function() { 248 | beforeEach(function() { 249 | mockExec(1, null, this.onRequest); 250 | this.player.id = 1; 251 | this.player.queue(['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], this.callback); 252 | }); 253 | 254 | it('should send the correct parameters to native', function() { 255 | expect(this.onRequest).to.have.been.calledWith([ 256 | 'SpotifyPlugin', 'queue', [1, ['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], false] 257 | ]); 258 | }); 259 | 260 | it('should queue successfully', function() { 261 | expect(this.callback).to.have.been.calledWith(null); 262 | }); 263 | }); 264 | 265 | describe('with clearQueue', function() { 266 | beforeEach(function() { 267 | mockExec(1, null, this.onRequest); 268 | this.player.id = 1; 269 | this.player.queue(['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], true, this.callback); 270 | }); 271 | 272 | it('should send the correct parameters to native', function() { 273 | expect(this.onRequest).to.have.been.calledWith([ 274 | 'SpotifyPlugin', 'queue', [1, ['spotify:track:3XpXhVtZwqh2eM5d9ieXT5', 'spotify:track:0IqKeD8ZSP72KbGYyzEcAs'], true] 275 | ]); 276 | }); 277 | 278 | it('should queue successfully', function() { 279 | expect(this.callback).to.have.been.calledWith(null); 280 | }); 281 | }); 282 | }); 283 | 284 | describe('#queuePlay', function() { 285 | beforeEach(function() { 286 | mockExec(1, null, this.onRequest); 287 | this.player.id = 1; 288 | this.player.queuePlay(this.callback); 289 | }); 290 | 291 | it('should send the correct parameters to native', function() { 292 | expect(this.onRequest).to.have.been.calledWith([ 293 | 'SpotifyPlugin', 'queuePlay', [1] 294 | ]); 295 | }); 296 | 297 | it('should queuePlay successfully', function() { 298 | expect(this.callback).to.have.been.calledWith(null); 299 | }); 300 | }); 301 | 302 | describe('#queueClear', function() { 303 | beforeEach(function() { 304 | mockExec(1, null, this.onRequest); 305 | this.player.id = 1; 306 | this.player.queueClear(this.callback); 307 | }); 308 | 309 | it('should send the correct parameters to native', function() { 310 | expect(this.onRequest).to.have.been.calledWith([ 311 | 'SpotifyPlugin', 'queueClear', [1] 312 | ]); 313 | }); 314 | 315 | it('should queuePlay successfully', function() { 316 | expect(this.callback).to.have.been.calledWith(null); 317 | }); 318 | }); 319 | 320 | describe('#stop', function() { 321 | beforeEach(function() { 322 | mockExec(1, null, this.onRequest); 323 | this.player.id = 1; 324 | this.player.stop(this.callback); 325 | }); 326 | 327 | it('should send the correct parameters to native', function() { 328 | expect(this.onRequest).to.have.been.calledWith([ 329 | 'SpotifyPlugin', 'stop', [1] 330 | ]); 331 | }); 332 | 333 | it('should stop successfully', function() { 334 | expect(this.callback).to.have.been.calledWith(null); 335 | }); 336 | }); 337 | 338 | describe('#skipNext', function() { 339 | beforeEach(function() { 340 | mockExec(1, null, this.onRequest); 341 | this.player.id = 1; 342 | this.player.skipNext(this.callback); 343 | }); 344 | 345 | it('should send the correct parameters to native', function() { 346 | expect(this.onRequest).to.have.been.calledWith([ 347 | 'SpotifyPlugin', 'skipNext', [1] 348 | ]); 349 | }); 350 | 351 | it('should skipNext successfully', function() { 352 | expect(this.callback).to.have.been.calledWith(null); 353 | }); 354 | }); 355 | 356 | describe('#skipPrevious', function() { 357 | beforeEach(function() { 358 | mockExec(1, null, this.onRequest); 359 | this.player.id = 1; 360 | this.player.skipPrevious(this.callback); 361 | }); 362 | 363 | it('should send the correct parameters to native', function() { 364 | expect(this.onRequest).to.have.been.calledWith([ 365 | 'SpotifyPlugin', 'skipPrevious', [1] 366 | ]); 367 | }); 368 | 369 | it('should skipPrevious successfully', function() { 370 | expect(this.callback).to.have.been.calledWith(null); 371 | }); 372 | }); 373 | 374 | describe('#seekToOffset', function() { 375 | beforeEach(function() { 376 | mockExec(1, null, this.onRequest); 377 | this.player.id = 1; 378 | this.player.seekToOffset(3, this.callback); 379 | }); 380 | 381 | it('should send the correct parameters to native', function() { 382 | expect(this.onRequest).to.have.been.calledWith([ 383 | 'SpotifyPlugin', 'seekToOffset', [1, 3] 384 | ]); 385 | }); 386 | 387 | it('should seekToOffset successfully', function() { 388 | expect(this.callback).to.have.been.calledWith(null); 389 | }); 390 | }); 391 | 392 | describe('#getIsPlaying', function() { 393 | beforeEach(function() { 394 | mockExec(1, true, this.onRequest); 395 | this.player.id = 1; 396 | this.player.getIsPlaying(this.callback); 397 | }); 398 | 399 | it('should send the correct parameters to native', function() { 400 | expect(this.onRequest).to.have.been.calledWith([ 401 | 'SpotifyPlugin', 'getIsPlaying', [1] 402 | ]); 403 | }); 404 | 405 | it('should getIsPlaying successfully', function() { 406 | expect(this.callback).to.have.been.calledWith(null, true); 407 | }); 408 | }); 409 | 410 | describe('#setIsPlaying', function() { 411 | beforeEach(function() { 412 | mockExec(1, null, this.onRequest); 413 | this.player.id = 1; 414 | this.player.setIsPlaying(true, this.callback); 415 | }); 416 | 417 | it('should send the correct parameters to native', function() { 418 | expect(this.onRequest).to.have.been.calledWith([ 419 | 'SpotifyPlugin', 'setIsPlaying', [1, true] 420 | ]); 421 | }); 422 | 423 | it('should setIsPlaying successfully', function() { 424 | expect(this.callback).to.have.been.calledWith(null); 425 | }); 426 | }); 427 | 428 | describe('#getVolume', function() { 429 | beforeEach(function() { 430 | mockExec(1, 0.5, this.onRequest); 431 | this.player.id = 1; 432 | this.player.getVolume(this.callback); 433 | }); 434 | 435 | it('should send the correct parameters to native', function() { 436 | expect(this.onRequest).to.have.been.calledWith([ 437 | 'SpotifyPlugin', 'getVolume', [1] 438 | ]); 439 | }); 440 | 441 | it('should getVolume successfully', function() { 442 | expect(this.callback).to.have.been.calledWith(null, 0.5); 443 | }); 444 | }); 445 | 446 | describe('#setVolume', function() { 447 | beforeEach(function() { 448 | mockExec(1, null, this.onRequest); 449 | this.player.id = 1; 450 | this.player.setVolume(0.75, this.callback); 451 | }); 452 | 453 | it('should send the correct parameters to native', function() { 454 | expect(this.onRequest).to.have.been.calledWith([ 455 | 'SpotifyPlugin', 'setVolume', [1, 0.75] 456 | ]); 457 | }); 458 | 459 | it('should setVolume successfully', function() { 460 | expect(this.callback).to.have.been.calledWith(null); 461 | }); 462 | }); 463 | 464 | describe('#getRepeat', function() { 465 | beforeEach(function() { 466 | mockExec(1, true, this.onRequest); 467 | this.player.id = 1; 468 | this.player.getRepeat(this.callback); 469 | }); 470 | 471 | it('should send the correct parameters to native', function() { 472 | expect(this.onRequest).to.have.been.calledWith([ 473 | 'SpotifyPlugin', 'getRepeat', [1] 474 | ]); 475 | }); 476 | 477 | it('should getRepeat successfully', function() { 478 | expect(this.callback).to.have.been.calledWith(null, true); 479 | }); 480 | }); 481 | 482 | describe('#setRepeat', function() { 483 | beforeEach(function() { 484 | mockExec(1, null, this.onRequest); 485 | this.player.id = 1; 486 | this.player.setRepeat(true, this.callback); 487 | }); 488 | 489 | it('should send the correct parameters to native', function() { 490 | expect(this.onRequest).to.have.been.calledWith([ 491 | 'SpotifyPlugin', 'setRepeat', [1, true] 492 | ]); 493 | }); 494 | 495 | it('should setRepeat successfully', function() { 496 | expect(this.callback).to.have.been.calledWith(null); 497 | }); 498 | }); 499 | 500 | describe('#getShuffle', function() { 501 | beforeEach(function() { 502 | mockExec(1, true, this.onRequest); 503 | this.player.id = 1; 504 | this.player.getShuffle(this.callback); 505 | }); 506 | 507 | it('should send the correct parameters to native', function() { 508 | expect(this.onRequest).to.have.been.calledWith([ 509 | 'SpotifyPlugin', 'getShuffle', [1] 510 | ]); 511 | }); 512 | 513 | it('should getShuffle successfully', function() { 514 | expect(this.callback).to.have.been.calledWith(null, true); 515 | }); 516 | }); 517 | 518 | describe('#setShuffle', function() { 519 | beforeEach(function() { 520 | mockExec(1, null, this.onRequest); 521 | this.player.id = 1; 522 | this.player.setShuffle(true, this.callback); 523 | }); 524 | 525 | it('should send the correct parameters to native', function() { 526 | expect(this.onRequest).to.have.been.calledWith([ 527 | 'SpotifyPlugin', 'setShuffle', [1, true] 528 | ]); 529 | }); 530 | 531 | it('should setShuffle successfully', function() { 532 | expect(this.callback).to.have.been.calledWith(null); 533 | }); 534 | }); 535 | 536 | describe('#getDiskCacheSizeLimit', function() { 537 | beforeEach(function() { 538 | mockExec(1, 3000, this.onRequest); 539 | this.player.id = 1; 540 | this.player.getDiskCacheSizeLimit(this.callback); 541 | }); 542 | 543 | it('should send the correct parameters to native', function() { 544 | expect(this.onRequest).to.have.been.calledWith([ 545 | 'SpotifyPlugin', 'getDiskCacheSizeLimit', [1] 546 | ]); 547 | }); 548 | 549 | it('should getDiskCacheSizeLimit successfully', function() { 550 | expect(this.callback).to.have.been.calledWith(null, 3000); 551 | }); 552 | }); 553 | 554 | describe('#setDiskCacheSizeLimit', function() { 555 | beforeEach(function() { 556 | mockExec(1, null, this.onRequest); 557 | this.player.id = 1; 558 | this.player.setDiskCacheSizeLimit(2524, this.callback); 559 | }); 560 | 561 | it('should send the correct parameters to native', function() { 562 | expect(this.onRequest).to.have.been.calledWith([ 563 | 'SpotifyPlugin', 'setDiskCacheSizeLimit', [1, 2524] 564 | ]); 565 | }); 566 | 567 | it('should setDiskCacheSizeLimit successfully', function() { 568 | expect(this.callback).to.have.been.calledWith(null); 569 | }); 570 | }); 571 | 572 | describe('#getTargetBitrate', function() { 573 | beforeEach(function() { 574 | mockExec(1, 2, this.onRequest); 575 | this.player.id = 1; 576 | this.player.getTargetBitrate(this.callback); 577 | }); 578 | 579 | it('should send the correct parameters to native', function() { 580 | expect(this.onRequest).to.have.been.calledWith([ 581 | 'SpotifyPlugin', 'getTargetBitrate', [1] 582 | ]); 583 | }); 584 | 585 | it('should getTargetBitrate successfully', function() { 586 | expect(this.callback).to.have.been.calledWith(null, 2); 587 | }); 588 | }); 589 | 590 | describe('#setTargetBitrate', function() { 591 | beforeEach(function() { 592 | mockExec(1, null, this.onRequest); 593 | this.player.id = 1; 594 | this.player.setTargetBitrate(1, this.callback); 595 | }); 596 | 597 | it('should send the correct parameters to native', function() { 598 | expect(this.onRequest).to.have.been.calledWith([ 599 | 'SpotifyPlugin', 'setTargetBitrate', [1, 1] 600 | ]); 601 | }); 602 | 603 | it('should setTargetBitrate successfully', function() { 604 | expect(this.callback).to.have.been.calledWith(null); 605 | }); 606 | }); 607 | 608 | describe('#getLoggedIn', function() { 609 | beforeEach(function() { 610 | mockExec(1, false, this.onRequest); 611 | this.player.id = 1; 612 | this.player.getLoggedIn(this.callback); 613 | }); 614 | 615 | it('should send the correct parameters to native', function() { 616 | expect(this.onRequest).to.have.been.calledWith([ 617 | 'SpotifyPlugin', 'getLoggedIn', [1] 618 | ]); 619 | }); 620 | 621 | it('should getLoggedIn successfully', function() { 622 | expect(this.callback).to.have.been.calledWith(null, false); 623 | }); 624 | }); 625 | 626 | describe('#getQueueSize', function() { 627 | beforeEach(function() { 628 | mockExec(1, 25, this.onRequest); 629 | this.player.id = 1; 630 | this.player.getQueueSize(this.callback); 631 | }); 632 | 633 | it('should send the correct parameters to native', function() { 634 | expect(this.onRequest).to.have.been.calledWith([ 635 | 'SpotifyPlugin', 'getQueueSize', [1] 636 | ]); 637 | }); 638 | 639 | it('should getQueueSize successfully', function() { 640 | expect(this.callback).to.have.been.calledWith(null, 25); 641 | }); 642 | }); 643 | 644 | describe('#getTrackListSize', function() { 645 | beforeEach(function() { 646 | mockExec(1, 5, this.onRequest); 647 | this.player.id = 1; 648 | this.player.getTrackListSize(this.callback); 649 | }); 650 | 651 | it('should send the correct parameters to native', function() { 652 | expect(this.onRequest).to.have.been.calledWith([ 653 | 'SpotifyPlugin', 'getTrackListSize', [1] 654 | ]); 655 | }); 656 | 657 | it('should getQueueSize successfully', function() { 658 | expect(this.callback).to.have.been.calledWith(null, 5); 659 | }); 660 | }); 661 | 662 | describe('#getTrackMetadata', function() { 663 | beforeEach(function() { 664 | this.currentTrack = { 665 | name: 'Song 2', 666 | uri: 'spotify:track:3GfOAdcoc3X5GPiiXmpBjK', 667 | artist: { 668 | name: 'Blur', 669 | uri: 'spotify:artist:7MhMgCo0Bl0Kukl93PZbYS', 670 | }, 671 | album: { 672 | name: 'Blur: The Best Of', 673 | uri: 'spotify:album:1bgkxe4t0HNeLn9rhrx79x', 674 | }, 675 | duration: 122 676 | }; 677 | 678 | this.player.id = 1; 679 | 680 | mockExec(1, this.currentTrack, this.onRequest); 681 | }); 682 | 683 | describe('current track', function() { 684 | beforeEach(function() { 685 | this.player.getTrackMetadata(this.callback); 686 | }); 687 | 688 | it('should send the correct parameters to native', function() { 689 | expect(this.onRequest).to.have.been.calledWith([ 690 | 'SpotifyPlugin', 'getTrackMetadata', [1] 691 | ]); 692 | }); 693 | 694 | it('should getTrackMetadata successfully', function() { 695 | expect(this.callback).to.have.been.calledWith(null, this.currentTrack); 696 | }); 697 | }); 698 | 699 | describe('with trackID (absolute)', function() { 700 | beforeEach(function() { 701 | this.player.getTrackMetadata(3, this.callback); 702 | }); 703 | 704 | it('should send the correct parameters to native', function() { 705 | expect(this.onRequest).to.have.been.calledWith([ 706 | 'SpotifyPlugin', 'getTrackMetadata', [1, 3] 707 | ]); 708 | }); 709 | 710 | it('should getTrackMetadata successfully', function() { 711 | expect(this.callback).to.have.been.calledWith(null, this.currentTrack); 712 | }); 713 | }); 714 | 715 | describe('with trackID (relative)', function() { 716 | beforeEach(function() { 717 | this.player.getTrackMetadata(2, true, this.callback); 718 | }); 719 | 720 | it('should send the correct parameters to native', function() { 721 | expect(this.onRequest).to.have.been.calledWith([ 722 | 'SpotifyPlugin', 'getTrackMetadata', [1, 2, true] 723 | ]); 724 | }); 725 | 726 | it('should getTrackMetadata successfully', function() { 727 | expect(this.callback).to.have.been.calledWith(null, this.currentTrack); 728 | }); 729 | }); 730 | }); 731 | 732 | describe('#getCurrentPlaybackPosition', function() { 733 | beforeEach(function() { 734 | mockExec(1, 139, this.onRequest); 735 | this.player.id = 1; 736 | this.player.getCurrentPlaybackPosition(this.callback); 737 | }); 738 | 739 | it('should send the correct parameters to native', function() { 740 | expect(this.onRequest).to.have.been.calledWith([ 741 | 'SpotifyPlugin', 'getCurrentPlaybackPosition', [1] 742 | ]); 743 | }); 744 | 745 | it('should getCurrentPlaybackPosition successfully', function() { 746 | expect(this.callback).to.have.been.calledWith(null, 139); 747 | }); 748 | }); 749 | }); 750 | -------------------------------------------------------------------------------- /test/js/spec/spotify/auth.test.js: -------------------------------------------------------------------------------- 1 | describe('auth', function() { 2 | function createExpirationDate(date) { 3 | date = date || new Date(); 4 | 5 | return date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + date.getUTCDate() + " " + date.getUTCHours() + ":" + date.getUTCMinutes() + ":" + date.getUTCSeconds() + " GMT"; 6 | } 7 | 8 | var session = {canonicalUsername: 'antman', accessToken: 'Xcd4r234234fdfa_dfsadf3', encryptedRefreshToken: 'sdfsdfds724dfsdf234dsf', expirationDate: createExpirationDate()} 9 | , newSession = {canonicalUsername: 'antman', accessToken: 'XASDFasdfd4r234234fdfa_dfsdsadfsdf3', encryptedRefreshToken: '4334fsdfdSDF234', expirationDate: createExpirationDate()} 10 | 11 | var auth; 12 | 13 | before(function() { 14 | auth = require('com.timflapper.spotify.auth'); 15 | }); 16 | 17 | beforeEach(function() { 18 | this.callback = sinon.spy(); 19 | this.onRequest = sinon.spy(); 20 | }); 21 | 22 | afterEach(function() { 23 | restoreMockExec(); 24 | }); 25 | 26 | describe('#authenticate', function() { 27 | describe('succesful authentication', function() { 28 | beforeEach(function() { 29 | mockExec(1, session, this.onRequest); 30 | }); 31 | 32 | describe('without scope', function() { 33 | beforeEach(function() { 34 | spotify.authenticate('test-scheme', 'aRandomClientId1234', 'code', 'http://tok.en', this.callback); 35 | }); 36 | 37 | it('should send the "streaming" scope to native', function() { 38 | expect(this.onRequest).to.have.been.calledWith([ 39 | 'SpotifyPlugin', 'authenticate', ['test-scheme', 'aRandomClientId1234', 'code', 'http://tok.en', ['streaming']] 40 | ]); 41 | }); 42 | 43 | it('should call callback with the session', function() { 44 | expect(this.callback).to.have.been.calledWith(null, session); 45 | }) 46 | }); 47 | 48 | describe('with scope', function() { 49 | beforeEach(function() { 50 | spotify.authenticate('test-scheme', 'aRandomClientId1234', 'code', 'http://tok.en', ['somescope'], this.callback); 51 | }); 52 | 53 | it('should send the "somescope" scope to native', function() { 54 | expect(this.onRequest).to.have.been.calledWith([ 55 | 'SpotifyPlugin', 'authenticate', ['test-scheme', 'aRandomClientId1234', 'code', 'http://tok.en', ['somescope']] 56 | ]); 57 | }); 58 | 59 | it('should call callback with the session', function() { 60 | expect(this.callback).to.have.been.calledWith(null, session); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('failed authentication', function() { 66 | beforeEach(function() { 67 | mockExec(9, 'Login to Spotify failed because of invalid credentials.', this.onRequest); 68 | spotify.authenticate('test-scheme', 'aRandomClientId1234', 'code', 'http://tok.en', this.callback); 69 | }); 70 | 71 | it('should send back "Login to Spotify failed because of invalid credentials."', function() { 72 | expect(this.callback).to.have.been.calledWith('Login to Spotify failed because of invalid credentials.'); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('#isSessionValid', function() { 78 | describe('session is valid', function() { 79 | beforeEach(function() { 80 | mockExec(1, true, this.onRequest); 81 | spotify.isSessionValid(session, this.callback); 82 | }); 83 | 84 | it('should send the session to native', function() { 85 | expect(this.onRequest).to.have.been.calledWith([ 86 | 'SpotifyPlugin', 'isSessionValid', [session] 87 | ]); 88 | }); 89 | 90 | it('should send back true', function() { 91 | expect(this.callback).to.have.been.calledWith(null, true); 92 | }); 93 | }); 94 | 95 | describe('session is not valid', function() { 96 | beforeEach(function() { 97 | mockExec(1, false, this.onRequest); 98 | spotify.isSessionValid(session, this.callback); 99 | }); 100 | 101 | it('should send the session to native', function() { 102 | expect(this.onRequest).to.have.been.calledWith([ 103 | 'SpotifyPlugin', 'isSessionValid', [session] 104 | ]); 105 | }); 106 | 107 | it('should send back true', function() { 108 | expect(this.callback).to.have.been.calledWith(null, false); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('#renewSession', function() { 114 | describe('when session is still valid', function() { 115 | beforeEach(function() { 116 | sinon.stub(auth, "isSessionValid", function(session, callback) { 117 | callback(null, true); 118 | }); 119 | mockExec(1, session, this.onRequest); 120 | spotify.renewSession(session, 'http://tok.en/refresh', this.callback); 121 | }); 122 | 123 | afterEach(function() { 124 | auth.isSessionValid.restore(); 125 | }); 126 | 127 | it('should not send the session and tokenRefreshURL to native', function() { 128 | expect(this.onRequest).to.not.have.been.calledWith([ 129 | 'SpotifyPlugin', 'renewSession', [session, 'http://tok.en/refresh'] 130 | ]); 131 | }); 132 | 133 | it('should send back the same session', function() { 134 | expect(this.callback).to.have.been.calledWith(null, session); 135 | }); 136 | }); 137 | 138 | describe('when session is invalid', function() { 139 | beforeEach(function() { 140 | sinon.stub(auth, "isSessionValid", function(session, callback) { 141 | callback(null, false); 142 | }); 143 | mockExec(1, newSession, this.onRequest); 144 | spotify.renewSession(session, 'http://tok.en/refresh', this.callback); 145 | }); 146 | 147 | afterEach(function() { 148 | auth.isSessionValid.restore(); 149 | }); 150 | 151 | it('should send the session and tokenRefreshURL to native', function() { 152 | expect(this.onRequest).to.have.been.calledWith([ 153 | 'SpotifyPlugin', 'renewSession', [session, 'http://tok.en/refresh'] 154 | ]); 155 | }); 156 | 157 | it('should send back a different session', function() { 158 | expect(this.callback).to.have.been.calledWith(null, newSession); 159 | }); 160 | }); 161 | 162 | 163 | describe('force renew session', function() { 164 | beforeEach(function() { 165 | mockExec(1, newSession, this.onRequest); 166 | spotify.renewSession(session, 'http://tok.en/refresh', true, this.callback); 167 | }); 168 | 169 | it('should send the session and tokenRefreshURL to native', function() { 170 | expect(this.onRequest).to.have.been.calledWith([ 171 | 'SpotifyPlugin', 'renewSession', [session, 'http://tok.en/refresh'] 172 | ]); 173 | }); 174 | 175 | it('should send back a different session', function() { 176 | expect(this.callback).to.have.been.calledWith(null, newSession); 177 | }); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/js/spec/support/android/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timflapper/cordova-spotify-plugin/424f3a900c4915a0cdf0e20fccd51c7185349bb7/test/js/spec/support/android/.gitkeep -------------------------------------------------------------------------------- /test/js/spec/support/ios/mock-exec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var exec = require('cordova/exec'); 3 | 4 | var xhr, mockResults = [], first = true; 5 | 6 | window.mockExec = function(status, result, onRequest, noCallback) { 7 | noCallback = noCallback || false; 8 | if (first) { 9 | first = false; 10 | exec.setJsToNativeBridgeMode(exec.jsToNativeModes.XHR_NO_PAYLOAD); 11 | } 12 | 13 | mockResults.push({ 14 | status: status, 15 | result: result, 16 | onRequest: onRequest, 17 | noCallback: noCallback 18 | }); 19 | 20 | if (! xhr) { 21 | xhr = sinon.useFakeXMLHttpRequest(); 22 | 23 | xhr.onCreate = function(req) { 24 | var payloads = JSON.parse(exec.nativeFetchMessages()); 25 | 26 | while (mockResults.length > 0) { 27 | var mockResult = mockResults.shift() 28 | , payload = payloads.shift() 29 | , callbackId = payload.shift(); 30 | 31 | if (mockResult.onRequest) mockResult.onRequest(payload); 32 | if (! mockResult.noCallback) { 33 | var more = exec.nativeCallback(callbackId, mockResult.status, mockResult.result, false); 34 | 35 | if (more) { 36 | payloads = payloads.concat(JSON.parse(more)); 37 | } 38 | } 39 | } 40 | 41 | xhr.restore(); 42 | xhr = null; 43 | }; 44 | } 45 | }; 46 | 47 | window.restoreMockExec = function() { 48 | if (xhr) { 49 | xhr.restore(); 50 | xhr = null; 51 | } 52 | 53 | mockResults = []; 54 | } 55 | })(); 56 | -------------------------------------------------------------------------------- /test/js/spec/support/test-helper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (cordova.platformId === 'ios') { 3 | loadScript('spec/support/ios/mock-exec.js'); 4 | } else if (cordova.platformId === 'android') { 5 | console.error('Platform ' + cordova.platformId + ' is not yet supported.'); 6 | } else { 7 | console.error('Platform ' + cordova.platformId + ' is not a valid platform.'); 8 | } 9 | 10 | function onScriptError(err) { 11 | console.error('The script ' + err.target.src + ' is not accessible.'); 12 | } 13 | 14 | function loadScript(url, callback) { 15 | var script = document.createElement("script"); 16 | script.onload = callback; 17 | script.onerror = onScriptError; 18 | script.src = url; 19 | document.head.appendChild(script); 20 | } 21 | })(); 22 | -------------------------------------------------------------------------------- /www/lib/event-dispatcher.js: -------------------------------------------------------------------------------- 1 | function EventDispatcher() { 2 | var self = this 3 | , events = {}; 4 | 5 | this.dispatchEvent = function(event, args) { 6 | var i, listeners; 7 | 8 | args = args || []; 9 | 10 | if ((event in events) === false) { 11 | if (event === 'error') 12 | throw new Error(args[0]); 13 | 14 | if (event === 'message') 15 | alert(args[0]); 16 | 17 | return; 18 | } 19 | 20 | listeners = events[event]; 21 | 22 | if (listeners.length === 1) { 23 | listeners[0].apply(self, args); 24 | } else if (listeners.length > 1) { 25 | listeners = events[event].slice(); 26 | 27 | listeners.forEach(function(item) { 28 | item.apply(self, args); 29 | }); 30 | } 31 | }; 32 | 33 | this.addEventListener = function(event, listener) { 34 | if (typeof listener !== 'function') 35 | throw new Error('listener must be a function'); 36 | 37 | if ((event in events) === false) 38 | events[event] = []; 39 | 40 | events[event].push(listener); 41 | }; 42 | 43 | this.removeEventListener = function(event, listener) { 44 | if (typeof listener !== 'function') 45 | throw new Error('listener must be a function'); 46 | 47 | if ((event in events) === false) 48 | return; 49 | 50 | var updatedArray = []; 51 | 52 | events[event].forEach(function(func, index) { 53 | if (func === listener) 54 | return; 55 | 56 | updatedArray.push(func); 57 | }); 58 | 59 | events[event] = updatedArray; 60 | }; 61 | } 62 | module.exports = EventDispatcher; 63 | -------------------------------------------------------------------------------- /www/lib/remote.js: -------------------------------------------------------------------------------- 1 | var reqwest = require('./vendors/reqwest'); 2 | 3 | var apiUrl = 'https://api.spotify.com/v1'; 4 | 5 | module.exports = remote; 6 | 7 | function remote(options, callback) { 8 | if (options === undefined) 9 | throw new Error('This method requires two arguments (options, callback)'); 10 | 11 | var req = { 12 | type: 'json', 13 | contentType: 'application/json', 14 | method: options.method || 'get', 15 | crossOrigin: true, 16 | headers: {} 17 | }; 18 | 19 | if (options.uri) { 20 | req.url = apiUrl + options.uri; 21 | } else if (options.url) { 22 | req.url = options.url; 23 | } else { 24 | return callback('No URL or URI set'); 25 | } 26 | 27 | if (options.session) 28 | req.headers.Authorization = 'Bearer ' + options.session.accessToken; 29 | 30 | if (options.data) 31 | req.data = options.data; 32 | 33 | reqwest(req) 34 | .then(function (data) { 35 | paginate(data, session); 36 | 37 | callback(null, data); 38 | }) 39 | .fail(function (err, msg) { 40 | if (err) return callback(err.statusText); 41 | if (msg) return callback(msg); 42 | callback("An unkown error occurred"); 43 | }); 44 | } 45 | 46 | function paginate(data, session) { 47 | if (Array.isArray(data)) { 48 | data.forEach(function(item) { 49 | paginate(item, session); 50 | }); 51 | 52 | return; 53 | } 54 | 55 | if (data.next) { 56 | var nextOpts = {url: data.next}; 57 | 58 | if (session) 59 | nextOpts.session = session; 60 | 61 | data.next = function(callback) { 62 | remote(nextOpts, onRemoteResult); 63 | 64 | function onRemoteResult(err, data) { 65 | if (err) return callback(err); 66 | 67 | paginate(data, session); 68 | 69 | callback(null, data); 70 | } 71 | } 72 | } 73 | 74 | if (data.prev) { 75 | var prevOpts = {url: data.prev}; 76 | 77 | if (session) 78 | prevOpts.session = session; 79 | 80 | data.prev = function(callback) { 81 | remote(prevOpts, onRemoteResult); 82 | 83 | function onRemoteResult(err, data) { 84 | if (err) return callback(err); 85 | 86 | paginate(data, session); 87 | 88 | callback(null, data); 89 | } 90 | } 91 | } 92 | 93 | if (data.artists) paginate(data.artists, session); 94 | if (data.tracks) paginate(data.tracks, session); 95 | if (data.albums) paginate(data.albums, session); 96 | } 97 | -------------------------------------------------------------------------------- /www/lib/utils.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec'); 2 | 3 | var noop = function() {}; 4 | 5 | var utils = module.exports = { 6 | noop: noop, 7 | exec: function(/*action, [params], [callback]*/) { 8 | var action, args = [], callback = noop; 9 | var i, argsLength = arguments.length; 10 | 11 | if (arguments.length === 0) throw new Error('No arguments received 1 or more expected.'); 12 | action = arguments[0]; 13 | 14 | for (i=1;i < argsLength;i++) args[i] = arguments[i]; 15 | 16 | if (args.length > 0 && typeof args.slice(-1)[0] === 'function') { 17 | callback = args.splice(-1, 1)[0]; 18 | } 19 | 20 | function onSuccess(result) { callback(null, result); } 21 | function onError(error) { callback(error); } 22 | 23 | exec(onSuccess, onError, 'SpotifyPlugin', action, args); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /www/spotify/audio-player.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | , EventDispatcher = require('./event-dispatcher'); 3 | 4 | var exec = utils.exec 5 | , noop = utils.noop; 6 | 7 | function AudioPlayer(clientId) { 8 | EventDispatcher.call(this); 9 | 10 | this.id = undefined; 11 | this.clientId = clientId; 12 | } 13 | 14 | AudioPlayer.prototype = Object.create(EventDispatcher.prototype); 15 | AudioPlayer.prototype.constructor = AudioPlayer; 16 | 17 | module.exports.createAudioPlayer = function(clientId) { 18 | return new AudioPlayer(clientId); 19 | } 20 | 21 | var events = module.exports.AudioPlayer = { 22 | EVENT_LOGIN: 'login', 23 | EVENT_LOGOUT: 'logout', 24 | EVENT_PERMISSION_LOST: 'permissionLost', 25 | EVENT_ERROR: 'error', 26 | EVENT_MESSAGE: 'message', 27 | EVENT_PLAYBACK_STATUS: 'playbackStatus', 28 | EVENT_SEEK_TO_OFFSET: 'seekToOffset' 29 | }; 30 | 31 | AudioPlayer.prototype.login = function(session, callback) { 32 | var self = this; 33 | 34 | exec('createAudioPlayerAndLogin', self.clientId, session, loginCallback); 35 | 36 | function loginCallback(error, id) { 37 | if (error) { 38 | if (! callback) 39 | return self.dispatchEvent(events.EVENT_ERROR, [error]); 40 | 41 | return callback(error); 42 | } 43 | 44 | self.id = id; 45 | 46 | exec('addAudioPlayerEventListener', self.id, onEventCallback); 47 | 48 | if (! callback) 49 | return self.dispatchEvent(events.EVENT_LOGIN); 50 | 51 | callback(null); 52 | }; 53 | 54 | function onEventCallback(error, result) { 55 | if (error) 56 | return self.dispatchEvent.call(self, events.EVENT_ERROR, [error]); 57 | 58 | self.dispatchEvent.call(self, result.type, result.args); 59 | } 60 | }; 61 | 62 | AudioPlayer.prototype.logout = function(callback) { 63 | var self = this; 64 | 65 | exec('audioPlayerLogout', self.id, logoutCallback); 66 | 67 | function logoutCallback(error) { 68 | if (error) { 69 | if (! callback) 70 | return self.dispatchEvent.call(self, events.EVENT_ERROR, [error]); 71 | 72 | return callback(error); 73 | } 74 | 75 | if (! callback) 76 | return self.dispatchEvent.call(self, events.EVENT_LOGOUT); 77 | 78 | callback(null); 79 | } 80 | }; 81 | 82 | AudioPlayer.prototype.play = function(data, fromIndex, callback) { 83 | if (callback === undefined && typeof fromIndex === 'function') 84 | callback = fromIndex, fromIndex = null; 85 | 86 | if (typeof fromIndex !== 'number') fromIndex = 0; 87 | 88 | exec('play', this.id, data, fromIndex, callback); 89 | }; 90 | 91 | AudioPlayer.prototype.setURIs = function(data, callback) { 92 | exec('setURIs', this.id, data, callback); 93 | }; 94 | 95 | AudioPlayer.prototype.playURIsFromIndex = function(fromIndex, callback) { 96 | exec('playURIsFromIndex', this.id, fromIndex, callback); 97 | } 98 | 99 | AudioPlayer.prototype.queue = function(data, clearQueue, callback) { 100 | if (callback === undefined && typeof clearQueue === 'function') 101 | callback = clearQueue, clearQueue = false; 102 | 103 | exec('queue', this.id, data, clearQueue, callback); 104 | }; 105 | 106 | AudioPlayer.prototype.queuePlay = function(callback) { 107 | exec('queuePlay', this.id, callback); 108 | } 109 | 110 | AudioPlayer.prototype.queueClear = function(callback) { 111 | exec('queueClear', this.id, callback); 112 | } 113 | 114 | AudioPlayer.prototype.stop = function(callback) { 115 | exec('stop', this.id, callback); 116 | } 117 | 118 | AudioPlayer.prototype.skipNext = function(callback) { 119 | exec('skipNext', this.id, callback); 120 | }; 121 | 122 | AudioPlayer.prototype.skipPrevious = function(callback) { 123 | exec('skipPrevious', this.id, callback); 124 | }; 125 | 126 | AudioPlayer.prototype.seekToOffset = function(offset, callback) { 127 | exec('seekToOffset', this.id, offset, callback); 128 | }; 129 | 130 | AudioPlayer.prototype.getIsPlaying = function(callback) { 131 | exec('getIsPlaying', this.id, callback); 132 | }; 133 | 134 | AudioPlayer.prototype.setIsPlaying = function(status, callback) { 135 | exec('setIsPlaying', this.id, status, callback); 136 | }; 137 | 138 | AudioPlayer.prototype.getVolume = function(callback) { 139 | exec('getVolume', this.id, callback); 140 | }; 141 | 142 | AudioPlayer.prototype.setVolume = function(volume, callback) { 143 | exec('setVolume', this.id, volume, callback); 144 | }; 145 | 146 | AudioPlayer.prototype.getRepeat = function(callback) { 147 | exec('getRepeat', this.id, callback); 148 | }; 149 | 150 | AudioPlayer.prototype.setRepeat = function(repeat, callback) { 151 | exec('setRepeat', this.id, repeat, callback); 152 | }; 153 | 154 | AudioPlayer.prototype.getShuffle = function(callback) { 155 | exec('getShuffle', this.id, callback); 156 | }; 157 | 158 | AudioPlayer.prototype.setShuffle = function(shuffle, callback) { 159 | exec('setShuffle', this.id, shuffle, callback); 160 | }; 161 | 162 | AudioPlayer.prototype.getDiskCacheSizeLimit = function(callback) { 163 | exec('getDiskCacheSizeLimit', this.id, callback); 164 | }; 165 | 166 | AudioPlayer.prototype.setDiskCacheSizeLimit = function(diskCacheSizeLimit, callback) { 167 | exec('setDiskCacheSizeLimit', this.id, diskCacheSizeLimit, callback); 168 | }; 169 | 170 | AudioPlayer.prototype.getTargetBitrate = function(callback) { 171 | exec('getTargetBitrate', this.id, callback); 172 | } 173 | 174 | AudioPlayer.prototype.setTargetBitrate = function(bitrate, callback) { 175 | exec('setTargetBitrate', this.id, bitrate, callback); 176 | } 177 | 178 | AudioPlayer.prototype.getLoggedIn = function(callback) { 179 | exec('getLoggedIn', this.id, callback); 180 | }; 181 | 182 | AudioPlayer.prototype.getQueueSize = function(callback) { 183 | exec('getQueueSize', this.id, callback); 184 | } 185 | 186 | AudioPlayer.prototype.getTrackListSize = function(callback) { 187 | exec('getTrackListSize', this.id, callback); 188 | } 189 | 190 | AudioPlayer.prototype.getTrackMetadata = function(trackID, relative, callback) { 191 | if (callback === undefined) { 192 | if (relative && typeof relative === 'function') 193 | callback = relative, relative = null; 194 | else if (trackID && typeof trackID === 'function') 195 | callback = trackID, trackID = null; 196 | } 197 | 198 | var args = ['getTrackMetadata', this.id]; 199 | 200 | if (trackID) { 201 | args.push(trackID); 202 | 203 | if (relative) args.push(relative); 204 | } 205 | 206 | args.push(callback); 207 | 208 | exec.apply(this, args); 209 | }; 210 | 211 | AudioPlayer.prototype.getCurrentPlaybackPosition = function(callback) { 212 | exec('getCurrentPlaybackPosition', this.id, callback); 213 | }; 214 | -------------------------------------------------------------------------------- /www/spotify/auth.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | , exec = utils.exec 3 | , noop = utils.noop; 4 | 5 | var auth = exports; 6 | 7 | auth.authenticate = function(urlScheme, clientId, responseType, tokenExchangeURL, scopes, callback) { 8 | if (callback === undefined) { 9 | if (scopes === undefined) { 10 | callback = tokenExchangeURL; 11 | tokenExchangeURL = null; 12 | } else { 13 | callback = scopes; 14 | } 15 | 16 | scopes = ['streaming']; 17 | } 18 | 19 | args = ['authenticate', urlScheme, clientId, responseType]; 20 | 21 | if(responseType == 'code') args.push(tokenExchangeURL); 22 | 23 | args.push(scopes); 24 | args.push(callback); 25 | 26 | exec.apply(this, args); 27 | }; 28 | 29 | auth.renewSession = function(session, tokenRefreshURL, forceRenew, callback) { 30 | if (callback === undefined && typeof forceRenew === 'function') 31 | callback = forceRenew, forceRenew = false; 32 | 33 | if (forceRenew === true) 34 | return renewSession(session, tokenRefreshURL, callback); 35 | 36 | auth.isSessionValid(session, function(err, valid) { 37 | if (err) return callback(err); 38 | if (valid) return callback(null, session); 39 | 40 | renewSession(session, tokenRefreshURL, callback); 41 | }); 42 | }; 43 | 44 | auth.isSessionValid = function(session, callback) { 45 | exec('isSessionValid', session, callback); 46 | }; 47 | 48 | /* Private methods */ 49 | function renewSession(session, tokenRefreshURL, callback) { 50 | exec('renewSession', session, tokenRefreshURL, callback); 51 | }; 52 | -------------------------------------------------------------------------------- /www/spotify/request.js: -------------------------------------------------------------------------------- 1 | var remote = require('./remote'); 2 | 3 | var REMOTE_IDS = 0x0001 4 | , REMOTE_ID_IN_URL = 0x0002 5 | , REMOTE_USERNAME_IN_URL = 0x0004 6 | , REMOTE_DATA = 0x0008 7 | , REMOTE_DATA_JSON = REMOTE_DATA + 0x0010 8 | , REMOTE_SESSION = 0x0100 9 | , REMOTE_POST = 0x1000 10 | , REMOTE_PUT = 0x2000 11 | , REMOTE_DELETE = 0x4000; 12 | 13 | var requestMethods = { 14 | search: ['/search', REMOTE_DATA], 15 | getAlbum: ['/albums/$I', REMOTE_ID_IN_URL], 16 | getAlbums: ['/albums', REMOTE_IDS], 17 | getArtist: ['/artists/$I', REMOTE_ID_IN_URL], 18 | getArtists: ['/artists', REMOTE_IDS], 19 | getAlbumsOfArtist: ['/artists/$I/albums', REMOTE_ID_IN_URL], 20 | getTrack: ['/tracks/$I', REMOTE_ID_IN_URL], 21 | getTracks: ['/tracks', REMOTE_IDS], 22 | getProfile: ['/me', REMOTE_SESSION], 23 | getSavedTracks: ['/me/tracks', REMOTE_SESSION], 24 | getSavedTracksContain: ['/me/tracks/contains', REMOTE_SESSION + REMOTE_IDS], 25 | saveTracks: ['/me/tracks', REMOTE_SESSION + REMOTE_IDS + REMOTE_PUT], 26 | removeTracks: ['/me/tracks', REMOTE_SESSION + REMOTE_IDS + REMOTE_DELETE], 27 | getStarred: ['/users/$U/starred', REMOTE_SESSION], 28 | getPlaylists: ['/users/$U/playlists', REMOTE_SESSION], 29 | getPlaylist: ['/users/$U/playlists/$I', REMOTE_SESSION + REMOTE_ID_IN_URL], 30 | createPlaylist: ['/users/$U/playlists', REMOTE_SESSION + REMOTE_DATA_JSON + REMOTE_POST], 31 | changePlaylistDetails: ['/users/$U/playlists/$I', REMOTE_SESSION + REMOTE_DATA_JSON + REMOTE_PUT + REMOTE_ID_IN_URL], 32 | addTracksToPlaylist: ['/users/$U/playlists/$I/tracks', REMOTE_SESSION + REMOTE_DATA_JSON + REMOTE_POST + REMOTE_ID_IN_URL, ['uris']], 33 | replaceTracksOnPlaylist: ['/users/$U/playlists/$I/tracks', REMOTE_SESSION + REMOTE_DATA_JSON + REMOTE_PUT + REMOTE_ID_IN_URL, ['uris']], 34 | removeTracksFromPlaylist: ['/users/$U/playlists/$I/tracks', REMOTE_SESSION + REMOTE_DATA_JSON + REMOTE_DELETE + REMOTE_ID_IN_URL, ['tracks']] 35 | }; 36 | 37 | Object.keys(requestMethods).forEach(function(key) { 38 | module.exports[key] = createRemoteMethod.apply(null, requestMethods[key]); 39 | }); 40 | 41 | function createRemoteMethod(uri, type, dataKeys) { 42 | return function() { 43 | var options = {uri: uri} 44 | , args = Array.prototype.slice.call(arguments) 45 | , callback = args.pop(); 46 | 47 | if (type & REMOTE_POST) { 48 | options.method = 'post'; 49 | } else if (type & REMOTE_PUT) { 50 | options.method = 'put'; 51 | } else if (type & REMOTE_DELETE) { 52 | options.method = 'delete'; 53 | } 54 | 55 | if (type & REMOTE_SESSION) { 56 | options.session = args.pop(); 57 | options.uri = options.uri.replace('$U', session.canonicalUsername); 58 | } 59 | 60 | if (type & REMOTE_ID_IN_URL) 61 | options.uri = options.uri.replace('$I', spotifyUriToId(args.shift())); 62 | 63 | if (type & REMOTE_IDS) 64 | options.uri = uriWithIds(options.uri, args.shift()); 65 | 66 | if (type & REMOTE_DATA) { 67 | if (dataKeys) { 68 | options.data = {}; 69 | 70 | dataKeys.forEach(function(key) { 71 | options.data[key] = args.shift(); 72 | }); 73 | } else { 74 | options.data = args.shift(); 75 | } 76 | 77 | if (type & REMOTE_DATA_JSON) 78 | options.data = JSON.stringify(options.data); 79 | } 80 | 81 | remote(options, callback); 82 | }; 83 | } 84 | 85 | function uriWithIds(uri, ids) { 86 | return uri + "?ids=" + spotifyUriToId(ids).join(',');; 87 | } 88 | 89 | function spotifyUriToId(uri) { 90 | if (Array.isArray(uri)) { 91 | var id, result = []; 92 | 93 | items.forEach(function(uri) { 94 | if (id = spotifyUriToId(uri)) 95 | result.push(id); 96 | }); 97 | 98 | return result; 99 | } 100 | 101 | if (matches = /^spotify:[^:]*:(.*)$/.exec(uri)) 102 | return matches[1]; 103 | 104 | return uri; 105 | } 106 | -------------------------------------------------------------------------------- /www/vendors/reqwest/reqwest.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Reqwest! A general purpose XHR connection manager 3 | * license MIT (c) Dustin Diaz 2014 4 | * https://github.com/ded/reqwest 5 | */ 6 | 7 | !function (name, context, definition) { 8 | if (typeof module != 'undefined' && module.exports) module.exports = definition() 9 | else if (typeof define == 'function' && define.amd) define(definition) 10 | else context[name] = definition() 11 | }('reqwest', this, function () { 12 | 13 | var win = window 14 | , doc = document 15 | , httpsRe = /^http/ 16 | , twoHundo = /^(20\d|1223)$/ 17 | , byTag = 'getElementsByTagName' 18 | , readyState = 'readyState' 19 | , contentType = 'Content-Type' 20 | , requestedWith = 'X-Requested-With' 21 | , head = doc[byTag]('head')[0] 22 | , uniqid = 0 23 | , callbackPrefix = 'reqwest_' + (+new Date()) 24 | , lastValue // data stored by the most recent JSONP callback 25 | , xmlHttpRequest = 'XMLHttpRequest' 26 | , xDomainRequest = 'XDomainRequest' 27 | , noop = function () {} 28 | 29 | , isArray = typeof Array.isArray == 'function' 30 | ? Array.isArray 31 | : function (a) { 32 | return a instanceof Array 33 | } 34 | 35 | , defaultHeaders = { 36 | 'contentType': 'application/x-www-form-urlencoded' 37 | , 'requestedWith': xmlHttpRequest 38 | , 'accept': { 39 | '*': 'text/javascript, text/html, application/xml, text/xml, */*' 40 | , 'xml': 'application/xml, text/xml' 41 | , 'html': 'text/html' 42 | , 'text': 'text/plain' 43 | , 'json': 'application/json, text/javascript' 44 | , 'js': 'application/javascript, text/javascript' 45 | } 46 | } 47 | 48 | , xhr = function(o) { 49 | // is it x-domain 50 | if (o['crossOrigin'] === true) { 51 | var xhr = win[xmlHttpRequest] ? new XMLHttpRequest() : null 52 | if (xhr && 'withCredentials' in xhr) { 53 | return xhr 54 | } else if (win[xDomainRequest]) { 55 | return new XDomainRequest() 56 | } else { 57 | throw new Error('Browser does not support cross-origin requests') 58 | } 59 | } else if (win[xmlHttpRequest]) { 60 | return new XMLHttpRequest() 61 | } else { 62 | return new ActiveXObject('Microsoft.XMLHTTP') 63 | } 64 | } 65 | , globalSetupOptions = { 66 | dataFilter: function (data) { 67 | return data 68 | } 69 | } 70 | 71 | function succeed(request) { 72 | return httpsRe.test(window.location.protocol) ? twoHundo.test(request.status) : !!request.response; 73 | } 74 | 75 | function handleReadyState(r, success, error) { 76 | return function () { 77 | // use _aborted to mitigate against IE err c00c023f 78 | // (can't read props on aborted request objects) 79 | if (r._aborted) return error(r.request) 80 | if (r.request && r.request[readyState] == 4) { 81 | r.request.onreadystatechange = noop 82 | if (succeed(r.request)) success(r.request) 83 | else 84 | error(r.request) 85 | } 86 | } 87 | } 88 | 89 | function setHeaders(http, o) { 90 | var headers = o['headers'] || {} 91 | , h 92 | 93 | headers['Accept'] = headers['Accept'] 94 | || defaultHeaders['accept'][o['type']] 95 | || defaultHeaders['accept']['*'] 96 | 97 | var isAFormData = typeof FormData === "function" && (o['data'] instanceof FormData); 98 | // breaks cross-origin requests with legacy browsers 99 | if (!o['crossOrigin'] && !headers[requestedWith]) headers[requestedWith] = defaultHeaders['requestedWith'] 100 | if (!headers[contentType] && !isAFormData) headers[contentType] = o['contentType'] || defaultHeaders['contentType'] 101 | for (h in headers) 102 | headers.hasOwnProperty(h) && 'setRequestHeader' in http && http.setRequestHeader(h, headers[h]) 103 | } 104 | 105 | function setCredentials(http, o) { 106 | if (typeof o['withCredentials'] !== 'undefined' && typeof http.withCredentials !== 'undefined') { 107 | http.withCredentials = !!o['withCredentials'] 108 | } 109 | } 110 | 111 | function generalCallback(data) { 112 | lastValue = data 113 | } 114 | 115 | function urlappend (url, s) { 116 | return url + (/\?/.test(url) ? '&' : '?') + s 117 | } 118 | 119 | function handleJsonp(o, fn, err, url) { 120 | var reqId = uniqid++ 121 | , cbkey = o['jsonpCallback'] || 'callback' // the 'callback' key 122 | , cbval = o['jsonpCallbackName'] || reqwest.getcallbackPrefix(reqId) 123 | , cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)') 124 | , match = url.match(cbreg) 125 | , script = doc.createElement('script') 126 | , loaded = 0 127 | , isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1 128 | 129 | if (match) { 130 | if (match[3] === '?') { 131 | url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name 132 | } else { 133 | cbval = match[3] // provided callback func name 134 | } 135 | } else { 136 | url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em 137 | } 138 | 139 | win[cbval] = generalCallback 140 | 141 | script.type = 'text/javascript' 142 | script.src = url 143 | script.async = true 144 | if (typeof script.onreadystatechange !== 'undefined' && !isIE10) { 145 | // need this for IE due to out-of-order onreadystatechange(), binding script 146 | // execution to an event listener gives us control over when the script 147 | // is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html 148 | script.htmlFor = script.id = '_reqwest_' + reqId 149 | } 150 | 151 | script.onload = script.onreadystatechange = function () { 152 | if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) { 153 | return false 154 | } 155 | script.onload = script.onreadystatechange = null 156 | script.onclick && script.onclick() 157 | // Call the user callback with the last value stored and clean up values and scripts. 158 | fn(lastValue) 159 | lastValue = undefined 160 | head.removeChild(script) 161 | loaded = 1 162 | } 163 | 164 | // Add the script to the DOM head 165 | head.appendChild(script) 166 | 167 | // Enable JSONP timeout 168 | return { 169 | abort: function () { 170 | script.onload = script.onreadystatechange = null 171 | err({}, 'Request is aborted: timeout', {}) 172 | lastValue = undefined 173 | head.removeChild(script) 174 | loaded = 1 175 | } 176 | } 177 | } 178 | 179 | function getRequest(fn, err) { 180 | var o = this.o 181 | , method = (o['method'] || 'GET').toUpperCase() 182 | , url = typeof o === 'string' ? o : o['url'] 183 | // convert non-string objects to query-string form unless o['processData'] is false 184 | , data = (o['processData'] !== false && o['data'] && typeof o['data'] !== 'string') 185 | ? reqwest.toQueryString(o['data']) 186 | : (o['data'] || null) 187 | , http 188 | , sendWait = false 189 | 190 | // if we're working on a GET request and we have data then we should append 191 | // query string to end of URL and not post data 192 | if ((o['type'] == 'jsonp' || method == 'GET') && data) { 193 | url = urlappend(url, data) 194 | data = null 195 | } 196 | 197 | if (o['type'] == 'jsonp') return handleJsonp(o, fn, err, url) 198 | 199 | // get the xhr from the factory if passed 200 | // if the factory returns null, fall-back to ours 201 | http = (o.xhr && o.xhr(o)) || xhr(o) 202 | 203 | http.open(method, url, o['async'] === false ? false : true) 204 | setHeaders(http, o) 205 | setCredentials(http, o) 206 | if (win[xDomainRequest] && http instanceof win[xDomainRequest]) { 207 | http.onload = fn 208 | http.onerror = err 209 | // NOTE: see 210 | // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/30ef3add-767c-4436-b8a9-f1ca19b4812e 211 | http.onprogress = function() {} 212 | sendWait = true 213 | } else { 214 | http.onreadystatechange = handleReadyState(this, fn, err) 215 | } 216 | o['before'] && o['before'](http) 217 | if (sendWait) { 218 | setTimeout(function () { 219 | http.send(data) 220 | }, 200) 221 | } else { 222 | http.send(data) 223 | } 224 | return http 225 | } 226 | 227 | function Reqwest(o, fn) { 228 | this.o = o 229 | this.fn = fn 230 | 231 | init.apply(this, arguments) 232 | } 233 | 234 | function setType(header) { 235 | // json, javascript, text/plain, text/html, xml 236 | if (header.match('json')) return 'json' 237 | if (header.match('javascript')) return 'js' 238 | if (header.match('text')) return 'html' 239 | if (header.match('xml')) return 'xml' 240 | } 241 | 242 | function init(o, fn) { 243 | 244 | this.url = typeof o == 'string' ? o : o['url'] 245 | this.timeout = null 246 | 247 | // whether request has been fulfilled for purpose 248 | // of tracking the Promises 249 | this._fulfilled = false 250 | // success handlers 251 | this._successHandler = function(){} 252 | this._fulfillmentHandlers = [] 253 | // error handlers 254 | this._errorHandlers = [] 255 | // complete (both success and fail) handlers 256 | this._completeHandlers = [] 257 | this._erred = false 258 | this._responseArgs = {} 259 | 260 | var self = this 261 | 262 | fn = fn || function () {} 263 | 264 | if (o['timeout']) { 265 | this.timeout = setTimeout(function () { 266 | self.abort() 267 | }, o['timeout']) 268 | } 269 | 270 | if (o['success']) { 271 | this._successHandler = function () { 272 | o['success'].apply(o, arguments) 273 | } 274 | } 275 | 276 | if (o['error']) { 277 | this._errorHandlers.push(function () { 278 | o['error'].apply(o, arguments) 279 | }) 280 | } 281 | 282 | if (o['complete']) { 283 | this._completeHandlers.push(function () { 284 | o['complete'].apply(o, arguments) 285 | }) 286 | } 287 | 288 | function complete (resp) { 289 | o['timeout'] && clearTimeout(self.timeout) 290 | self.timeout = null 291 | while (self._completeHandlers.length > 0) { 292 | self._completeHandlers.shift()(resp) 293 | } 294 | } 295 | 296 | function success (resp) { 297 | var type = o['type'] || setType(resp.getResponseHeader('Content-Type')) 298 | resp = (type !== 'jsonp') ? self.request : resp 299 | // use global data filter on response text 300 | var filteredResponse = globalSetupOptions.dataFilter(resp.responseText, type) 301 | , r = filteredResponse 302 | try { 303 | resp.responseText = r 304 | } catch (e) { 305 | // can't assign this in IE<=8, just ignore 306 | } 307 | if (r) { 308 | switch (type) { 309 | case 'json': 310 | try { 311 | resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')') 312 | } catch (err) { 313 | return error(resp, 'Could not parse JSON in response', err) 314 | } 315 | break 316 | case 'js': 317 | resp = eval(r) 318 | break 319 | case 'html': 320 | resp = r 321 | break 322 | case 'xml': 323 | resp = resp.responseXML 324 | && resp.responseXML.parseError // IE trololo 325 | && resp.responseXML.parseError.errorCode 326 | && resp.responseXML.parseError.reason 327 | ? null 328 | : resp.responseXML 329 | break 330 | } 331 | } 332 | 333 | self._responseArgs.resp = resp 334 | self._fulfilled = true 335 | fn(resp) 336 | self._successHandler(resp) 337 | while (self._fulfillmentHandlers.length > 0) { 338 | resp = self._fulfillmentHandlers.shift()(resp) 339 | } 340 | 341 | complete(resp) 342 | } 343 | 344 | function error(resp, msg, t) { 345 | resp = self.request 346 | self._responseArgs.resp = resp 347 | self._responseArgs.msg = msg 348 | self._responseArgs.t = t 349 | self._erred = true 350 | while (self._errorHandlers.length > 0) { 351 | self._errorHandlers.shift()(resp, msg, t) 352 | } 353 | complete(resp) 354 | } 355 | 356 | this.request = getRequest.call(this, success, error) 357 | } 358 | 359 | Reqwest.prototype = { 360 | abort: function () { 361 | this._aborted = true 362 | this.request.abort() 363 | } 364 | 365 | , retry: function () { 366 | init.call(this, this.o, this.fn) 367 | } 368 | 369 | /** 370 | * Small deviation from the Promises A CommonJs specification 371 | * http://wiki.commonjs.org/wiki/Promises/A 372 | */ 373 | 374 | /** 375 | * `then` will execute upon successful requests 376 | */ 377 | , then: function (success, fail) { 378 | success = success || function () {} 379 | fail = fail || function () {} 380 | if (this._fulfilled) { 381 | this._responseArgs.resp = success(this._responseArgs.resp) 382 | } else if (this._erred) { 383 | fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t) 384 | } else { 385 | this._fulfillmentHandlers.push(success) 386 | this._errorHandlers.push(fail) 387 | } 388 | return this 389 | } 390 | 391 | /** 392 | * `always` will execute whether the request succeeds or fails 393 | */ 394 | , always: function (fn) { 395 | if (this._fulfilled || this._erred) { 396 | fn(this._responseArgs.resp) 397 | } else { 398 | this._completeHandlers.push(fn) 399 | } 400 | return this 401 | } 402 | 403 | /** 404 | * `fail` will execute when the request fails 405 | */ 406 | , fail: function (fn) { 407 | if (this._erred) { 408 | fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t) 409 | } else { 410 | this._errorHandlers.push(fn) 411 | } 412 | return this 413 | } 414 | , catch: function (fn) { 415 | return this.fail(fn) 416 | } 417 | } 418 | 419 | function reqwest(o, fn) { 420 | return new Reqwest(o, fn) 421 | } 422 | 423 | // normalize newline variants according to spec -> CRLF 424 | function normalize(s) { 425 | return s ? s.replace(/\r?\n/g, '\r\n') : '' 426 | } 427 | 428 | function serial(el, cb) { 429 | var n = el.name 430 | , t = el.tagName.toLowerCase() 431 | , optCb = function (o) { 432 | // IE gives value="" even where there is no value attribute 433 | // 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273 434 | if (o && !o['disabled']) 435 | cb(n, normalize(o['attributes']['value'] && o['attributes']['value']['specified'] ? o['value'] : o['text'])) 436 | } 437 | , ch, ra, val, i 438 | 439 | // don't serialize elements that are disabled or without a name 440 | if (el.disabled || !n) return 441 | 442 | switch (t) { 443 | case 'input': 444 | if (!/reset|button|image|file/i.test(el.type)) { 445 | ch = /checkbox/i.test(el.type) 446 | ra = /radio/i.test(el.type) 447 | val = el.value 448 | // WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here 449 | ;(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val)) 450 | } 451 | break 452 | case 'textarea': 453 | cb(n, normalize(el.value)) 454 | break 455 | case 'select': 456 | if (el.type.toLowerCase() === 'select-one') { 457 | optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null) 458 | } else { 459 | for (i = 0; el.length && i < el.length; i++) { 460 | el.options[i].selected && optCb(el.options[i]) 461 | } 462 | } 463 | break 464 | } 465 | } 466 | 467 | // collect up all form elements found from the passed argument elements all 468 | // the way down to child elements; pass a '
' or form fields. 469 | // called with 'this'=callback to use for serial() on each element 470 | function eachFormElement() { 471 | var cb = this 472 | , e, i 473 | , serializeSubtags = function (e, tags) { 474 | var i, j, fa 475 | for (i = 0; i < tags.length; i++) { 476 | fa = e[byTag](tags[i]) 477 | for (j = 0; j < fa.length; j++) serial(fa[j], cb) 478 | } 479 | } 480 | 481 | for (i = 0; i < arguments.length; i++) { 482 | e = arguments[i] 483 | if (/input|select|textarea/i.test(e.tagName)) serial(e, cb) 484 | serializeSubtags(e, [ 'input', 'select', 'textarea' ]) 485 | } 486 | } 487 | 488 | // standard query string style serialization 489 | function serializeQueryString() { 490 | return reqwest.toQueryString(reqwest.serializeArray.apply(null, arguments)) 491 | } 492 | 493 | // { 'name': 'value', ... } style serialization 494 | function serializeHash() { 495 | var hash = {} 496 | eachFormElement.apply(function (name, value) { 497 | if (name in hash) { 498 | hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]]) 499 | hash[name].push(value) 500 | } else hash[name] = value 501 | }, arguments) 502 | return hash 503 | } 504 | 505 | // [ { name: 'name', value: 'value' }, ... ] style serialization 506 | reqwest.serializeArray = function () { 507 | var arr = [] 508 | eachFormElement.apply(function (name, value) { 509 | arr.push({name: name, value: value}) 510 | }, arguments) 511 | return arr 512 | } 513 | 514 | reqwest.serialize = function () { 515 | if (arguments.length === 0) return '' 516 | var opt, fn 517 | , args = Array.prototype.slice.call(arguments, 0) 518 | 519 | opt = args.pop() 520 | opt && opt.nodeType && args.push(opt) && (opt = null) 521 | opt && (opt = opt.type) 522 | 523 | if (opt == 'map') fn = serializeHash 524 | else if (opt == 'array') fn = reqwest.serializeArray 525 | else fn = serializeQueryString 526 | 527 | return fn.apply(null, args) 528 | } 529 | 530 | reqwest.toQueryString = function (o, trad) { 531 | var prefix, i 532 | , traditional = trad || false 533 | , s = [] 534 | , enc = encodeURIComponent 535 | , add = function (key, value) { 536 | // If value is a function, invoke it and return its value 537 | value = ('function' === typeof value) ? value() : (value == null ? '' : value) 538 | s[s.length] = enc(key) + '=' + enc(value) 539 | } 540 | // If an array was passed in, assume that it is an array of form elements. 541 | if (isArray(o)) { 542 | for (i = 0; o && i < o.length; i++) add(o[i]['name'], o[i]['value']) 543 | } else { 544 | // If traditional, encode the "old" way (the way 1.3.2 or older 545 | // did it), otherwise encode params recursively. 546 | for (prefix in o) { 547 | if (o.hasOwnProperty(prefix)) buildParams(prefix, o[prefix], traditional, add) 548 | } 549 | } 550 | 551 | // spaces should be + according to spec 552 | return s.join('&').replace(/%20/g, '+') 553 | } 554 | 555 | function buildParams(prefix, obj, traditional, add) { 556 | var name, i, v 557 | , rbracket = /\[\]$/ 558 | 559 | if (isArray(obj)) { 560 | // Serialize array item. 561 | for (i = 0; obj && i < obj.length; i++) { 562 | v = obj[i] 563 | if (traditional || rbracket.test(prefix)) { 564 | // Treat each array item as a scalar. 565 | add(prefix, v) 566 | } else { 567 | buildParams(prefix + '[' + (typeof v === 'object' ? i : '') + ']', v, traditional, add) 568 | } 569 | } 570 | } else if (obj && obj.toString() === '[object Object]') { 571 | // Serialize object item. 572 | for (name in obj) { 573 | buildParams(prefix + '[' + name + ']', obj[name], traditional, add) 574 | } 575 | 576 | } else { 577 | // Serialize scalar item. 578 | add(prefix, obj) 579 | } 580 | } 581 | 582 | reqwest.getcallbackPrefix = function () { 583 | return callbackPrefix 584 | } 585 | 586 | // jQuery and Zepto compatibility, differences can be remapped here so you can call 587 | // .ajax.compat(options, callback) 588 | reqwest.compat = function (o, fn) { 589 | if (o) { 590 | o['type'] && (o['method'] = o['type']) && delete o['type'] 591 | o['dataType'] && (o['type'] = o['dataType']) 592 | o['jsonpCallback'] && (o['jsonpCallbackName'] = o['jsonpCallback']) && delete o['jsonpCallback'] 593 | o['jsonp'] && (o['jsonpCallback'] = o['jsonp']) 594 | } 595 | return new Reqwest(o, fn) 596 | } 597 | 598 | reqwest.ajaxSetup = function (options) { 599 | options = options || {} 600 | for (var k in options) { 601 | globalSetupOptions[k] = options[k] 602 | } 603 | } 604 | 605 | return reqwest 606 | }); 607 | -------------------------------------------------------------------------------- /www/vendors/reqwest/reqwest.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Reqwest! A general purpose XHR connection manager 3 | * license MIT (c) Dustin Diaz 2014 4 | * https://github.com/ded/reqwest 5 | */ 6 | !function(e,t,n){typeof module!="undefined"&&module.exports?module.exports=n():typeof define=="function"&&define.amd?define(n):t[e]=n()}("reqwest",this,function(){function succeed(e){return httpsRe.test(window.location.protocol)?twoHundo.test(e.status):!!e.response}function handleReadyState(e,t,n){return function(){if(e._aborted)return n(e.request);e.request&&e.request[readyState]==4&&(e.request.onreadystatechange=noop,succeed(e.request)?t(e.request):n(e.request))}}function setHeaders(e,t){var n=t.headers||{},r;n.Accept=n.Accept||defaultHeaders.accept[t.type]||defaultHeaders.accept["*"];var i=typeof FormData=="function"&&t.data instanceof FormData;!t.crossOrigin&&!n[requestedWith]&&(n[requestedWith]=defaultHeaders.requestedWith),!n[contentType]&&!i&&(n[contentType]=t.contentType||defaultHeaders.contentType);for(r in n)n.hasOwnProperty(r)&&"setRequestHeader"in e&&e.setRequestHeader(r,n[r])}function setCredentials(e,t){typeof t.withCredentials!="undefined"&&typeof e.withCredentials!="undefined"&&(e.withCredentials=!!t.withCredentials)}function generalCallback(e){lastValue=e}function urlappend(e,t){return e+(/\?/.test(e)?"&":"?")+t}function handleJsonp(e,t,n,r){var i=uniqid++,s=e.jsonpCallback||"callback",o=e.jsonpCallbackName||reqwest.getcallbackPrefix(i),u=new RegExp("((^|\\?|&)"+s+")=([^&]+)"),a=r.match(u),f=doc.createElement("script"),l=0,c=navigator.userAgent.indexOf("MSIE 10.0")!==-1;return a?a[3]==="?"?r=r.replace(u,"$1="+o):o=a[3]:r=urlappend(r,s+"="+o),win[o]=generalCallback,f.type="text/javascript",f.src=r,f.async=!0,typeof f.onreadystatechange!="undefined"&&!c&&(f.htmlFor=f.id="_reqwest_"+i),f.onload=f.onreadystatechange=function(){if(f[readyState]&&f[readyState]!=="complete"&&f[readyState]!=="loaded"||l)return!1;f.onload=f.onreadystatechange=null,f.onclick&&f.onclick(),t(lastValue),lastValue=undefined,head.removeChild(f),l=1},head.appendChild(f),{abort:function(){f.onload=f.onreadystatechange=null,n({},"Request is aborted: timeout",{}),lastValue=undefined,head.removeChild(f),l=1}}}function getRequest(e,t){var n=this.o,r=(n.method||"GET").toUpperCase(),i=typeof n=="string"?n:n.url,s=n.processData!==!1&&n.data&&typeof n.data!="string"?reqwest.toQueryString(n.data):n.data||null,o,u=!1;return(n["type"]=="jsonp"||r=="GET")&&s&&(i=urlappend(i,s),s=null),n["type"]=="jsonp"?handleJsonp(n,e,t,i):(o=n.xhr&&n.xhr(n)||xhr(n),o.open(r,i,n.async===!1?!1:!0),setHeaders(o,n),setCredentials(o,n),win[xDomainRequest]&&o instanceof win[xDomainRequest]?(o.onload=e,o.onerror=t,o.onprogress=function(){},u=!0):o.onreadystatechange=handleReadyState(this,e,t),n.before&&n.before(o),u?setTimeout(function(){o.send(s)},200):o.send(s),o)}function Reqwest(e,t){this.o=e,this.fn=t,init.apply(this,arguments)}function setType(e){if(e.match("json"))return"json";if(e.match("javascript"))return"js";if(e.match("text"))return"html";if(e.match("xml"))return"xml"}function init(o,fn){function complete(e){o.timeout&&clearTimeout(self.timeout),self.timeout=null;while(self._completeHandlers.length>0)self._completeHandlers.shift()(e)}function success(resp){var type=o.type||setType(resp.getResponseHeader("Content-Type"));resp=type!=="jsonp"?self.request:resp;var filteredResponse=globalSetupOptions.dataFilter(resp.responseText,type),r=filteredResponse;try{resp.responseText=r}catch(e){}if(r)switch(type){case"json":try{resp=win.JSON?win.JSON.parse(r):eval("("+r+")")}catch(err){return error(resp,"Could not parse JSON in response",err)}break;case"js":resp=eval(r);break;case"html":resp=r;break;case"xml":resp=resp.responseXML&&resp.responseXML.parseError&&resp.responseXML.parseError.errorCode&&resp.responseXML.parseError.reason?null:resp.responseXML}self._responseArgs.resp=resp,self._fulfilled=!0,fn(resp),self._successHandler(resp);while(self._fulfillmentHandlers.length>0)resp=self._fulfillmentHandlers.shift()(resp);complete(resp)}function error(e,t,n){e=self.request,self._responseArgs.resp=e,self._responseArgs.msg=t,self._responseArgs.t=n,self._erred=!0;while(self._errorHandlers.length>0)self._errorHandlers.shift()(e,t,n);complete(e)}this.url=typeof o=="string"?o:o.url,this.timeout=null,this._fulfilled=!1,this._successHandler=function(){},this._fulfillmentHandlers=[],this._errorHandlers=[],this._completeHandlers=[],this._erred=!1,this._responseArgs={};var self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){self.abort()},o.timeout)),o.success&&(this._successHandler=function(){o.success.apply(o,arguments)}),o.error&&this._errorHandlers.push(function(){o.error.apply(o,arguments)}),o.complete&&this._completeHandlers.push(function(){o.complete.apply(o,arguments)}),this.request=getRequest.call(this,success,error)}function reqwest(e,t){return new Reqwest(e,t)}function normalize(e){return e?e.replace(/\r?\n/g,"\r\n"):""}function serial(e,t){var n=e.name,r=e.tagName.toLowerCase(),i=function(e){e&&!e.disabled&&t(n,normalize(e.attributes.value&&e.attributes.value.specified?e.value:e.text))},s,o,u,a;if(e.disabled||!n)return;switch(r){case"input":/reset|button|image|file/i.test(e.type)||(s=/checkbox/i.test(e.type),o=/radio/i.test(e.type),u=e.value,(!s&&!o||e.checked)&&t(n,normalize(s&&u===""?"on":u)));break;case"textarea":t(n,normalize(e.value));break;case"select":if(e.type.toLowerCase()==="select-one")i(e.selectedIndex>=0?e.options[e.selectedIndex]:null);else for(a=0;e.length&&a