From 3fbf258473054fb1a90285572b811982ac66902e Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Wed, 14 Feb 2018 18:48:48 +0100 Subject: [PATCH] Adds twitch support Resolves #13139 Auditors: Test Plan: --- app/browser/api/ledger.js | 23 +- app/common/cache/ledgerVideoCache.js | 19 +- app/common/constants/ledgerMediaProviders.js | 3 +- app/common/lib/ledgerUtil.js | 211 +++++++++++- app/filtering.js | 18 +- .../preferences/payment/ledgerTable.js | 27 +- docs/state.md | 4 + img/mediaProviders/twitch.svg | 1 + img/mediaProviders/youtube.png | Bin 0 -> 348 bytes test/unit/app/browser/api/ledgerTest.js | 12 +- .../app/common/cache/ledgerVideoCacheTest.js | 41 ++- test/unit/app/common/lib/ledgerUtilTest.js | 316 +++++++++++++++++- 12 files changed, 615 insertions(+), 60 deletions(-) create mode 100644 img/mediaProviders/twitch.svg create mode 100644 img/mediaProviders/youtube.png diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 0662750c659..27e14967964 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -315,6 +315,7 @@ const getPublisherData = (result, scorekeeper) => { verified: result.options.verified || false, exclude: result.options.exclude || false, publisherKey: result.publisherKey, + providerName: result.providerName, siteName: result.publisherKey, views: result.visits, duration: duration, @@ -1387,7 +1388,7 @@ const roundtrip = (params, options, callback) => { : typeof params.server !== 'undefined' ? params.server : typeof options.server === 'string' ? urlParse(options.server) : options.server const binaryP = options.binaryP - const rawP = binaryP || options.rawP + const rawP = binaryP || options.rawP || options.scrapeP if (!params.method) params.method = 'GET' parts = underscore.extend(underscore.pick(parts, ['protocol', 'hostname', 'port']), @@ -2399,10 +2400,15 @@ const onMediaRequest = (state, xhr, type, tabId) => { const parsed = ledgerUtil.getMediaData(xhr, type) const mediaId = ledgerUtil.getMediaId(parsed, type) + + if (mediaId == null) { + return state + } + const mediaKey = ledgerUtil.getMediaKey(mediaId, type) - let duration = ledgerUtil.getMediaDuration(parsed, type) + let duration = ledgerUtil.getMediaDuration(state, parsed, mediaKey, type) - if (mediaId == null || duration == null || mediaKey == null) { + if (duration == null || mediaKey == null) { return state } @@ -2422,9 +2428,14 @@ const onMediaRequest = (state, xhr, type, tabId) => { currentMediaKey = mediaKey } + const stateData = ledgerUtil.generateMediaCacheData(parsed, type) const cache = ledgerVideoCache.getDataByVideoId(state, mediaKey) if (!cache.isEmpty()) { + if (!stateData.isEmpty()) { + state = ledgerVideoCache.mergeCacheByVideoId(state, mediaKey, stateData) + } + const publisherKey = cache.get('publisher') const publisher = ledgerState.getPublisher(state, publisherKey) if (!publisher.isEmpty() && publisher.has('providerName')) { @@ -2436,6 +2447,10 @@ const onMediaRequest = (state, xhr, type, tabId) => { } } + if (!stateData.isEmpty()) { + state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, stateData) + } + const options = underscore.extend({roundtrip: module.exports.roundtrip}, clientOptions) const mediaProps = { mediaId, @@ -2513,7 +2528,7 @@ const onMediaPublisher = (state, mediaKey, response, duration, revisited) => { .set('publisher', publisherKey) // Add to cache - state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, cacheObject) + state = ledgerVideoCache.mergeCacheByVideoId(state, mediaKey, cacheObject) state = module.exports.saveVisit(state, publisherKey, { duration, diff --git a/app/common/cache/ledgerVideoCache.js b/app/common/cache/ledgerVideoCache.js index c7f0ae3fcb1..adab8a5f561 100644 --- a/app/common/cache/ledgerVideoCache.js +++ b/app/common/cache/ledgerVideoCache.js @@ -33,7 +33,24 @@ const setCacheByVideoId = (state, key, data) => { return state.setIn(['cache', 'ledgerVideos', key], data) } +const mergeCacheByVideoId = (state, key, data) => { + state = validateState(state) + + if (key == null || data == null) { + return state + } + + data = makeImmutable(data) + + if (data.isEmpty()) { + return state + } + + return state.mergeIn(['cache', 'ledgerVideos', key], data) +} + module.exports = { getDataByVideoId, - setCacheByVideoId + setCacheByVideoId, + mergeCacheByVideoId } diff --git a/app/common/constants/ledgerMediaProviders.js b/app/common/constants/ledgerMediaProviders.js index 84ef2d1af80..b8d241eb3d2 100644 --- a/app/common/constants/ledgerMediaProviders.js +++ b/app/common/constants/ledgerMediaProviders.js @@ -3,7 +3,8 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const providers = { - YOUTUBE: 'youtube' + YOUTUBE: 'youtube', + TWITCH: 'twitch' } module.exports = providers diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index 6b02863b09a..d882ba31053 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -13,6 +13,7 @@ const queryString = require('querystring') // State const siteSettingsState = require('../state/siteSettingsState') const ledgerState = require('../state/ledgerState') +const ledgerVideoCache = require('../cache/ledgerVideoCache') // Constants const settings = require('../../../js/constants/settings') @@ -23,6 +24,7 @@ const {responseHasContent} = require('./httpUtil') const urlUtil = require('../../../js/lib/urlutil') const getSetting = require('../../../js/settings').getSetting const urlParse = require('../urlParse') + /** * Is page an actual page being viewed by the user? (not an error page, etc) * If the page is invalid, we don't want to collect usage info. @@ -221,6 +223,26 @@ const getMediaId = (data, type) => { id = data.docid break } + case ledgerMediaProviders.TWITCH: + { + if ( + ( + data.event === 'minute-watched' || + data.event === 'video-play' || + data.event === 'player_click_playpause' || + data.event === 'vod_seek' + ) && + data.properties + ) { + id = data.properties.channel + let vod = data.properties.vod + + if (vod) { + vod = vod.replace('v', '') + id += `_vod_${vod}` + } + } + } } return id @@ -241,16 +263,39 @@ const getMediaData = (xhr, type) => { return result } + const parsedUrl = urlParse(xhr) + const query = parsedUrl && parsedUrl.query + + if (!parsedUrl || !query) { + return null + } + switch (type) { case ledgerMediaProviders.YOUTUBE: { - const parsedUrl = urlParse(xhr) - let query = null + result = queryString.parse(parsedUrl.query) + break + } + case ledgerMediaProviders.TWITCH: + { + result = queryString.parse(parsedUrl.query) + if (!result.data) { + result = null + break + } + + let obj = Buffer.from(result.data, 'base64').toString('utf8') + if (obj == null) { + result = null + break + } - if (parsedUrl && parsedUrl.query) { - query = queryString.parse(parsedUrl.query) + try { + result = JSON.parse(obj) + } catch (error) { + result = null + console.error(error.toString()) } - result = query break } } @@ -258,18 +303,141 @@ const getMediaData = (xhr, type) => { return result } -const getMediaDuration = (data, type) => { +const getMediaDuration = (state, data, mediaKey, type) => { let duration = 0 + + if (data == null) { + return duration + } + switch (type) { - case ledgerMediaProviders.YOUTUBE: { - duration = getYouTubeDuration(data) - break - } + case ledgerMediaProviders.YOUTUBE: + { + duration = getYouTubeDuration(data) + break + } + case ledgerMediaProviders.TWITCH: + { + duration = getTwitchDuration(state, data, mediaKey) + break + } } return duration } +const generateMediaCacheData = (parsed, type) => { + let data = Immutable.Map() + + if (parsed == null) { + return data + } + + switch (type) { + case ledgerMediaProviders.TWITCH: + { + data = generateTwitchCacheData(parsed) + break + } + } + + return data +} + +const generateTwitchCacheData = (parsed) => { + if (parsed == null) { + return Immutable.Map() + } + + if (parsed.properties) { + return Immutable.fromJS({ + event: parsed.event, + time: parsed.properties.time + }) + } + + return Immutable.fromJS({ + event: parsed.event + }) +} + +const getMediaFavicon = (providerName) => { + let image = null + + if (!providerName) { + return image + } + + providerName = providerName.toLowerCase() + + switch (providerName) { + case ledgerMediaProviders.YOUTUBE: + { + image = require('../../../img/mediaProviders/youtube.png') + break + } + case ledgerMediaProviders.TWITCH: + { + image = require('../../../img/mediaProviders/twitch.svg') + break + } + } + + return image +} + +const getTwitchDuration = (state, data, mediaKey) => { + if (data == null || mediaKey == null) { + return 0 + } + + const previousData = ledgerVideoCache.getDataByVideoId(state, mediaKey) + + if (previousData.isEmpty() && data.event === 'video-play') { + return milliseconds.second * 10 + } + + if (!data.properties) { + return 0 + } + + const oldMedia = ledgerVideoCache.getDataByVideoId(state, mediaKey) + let time = 0 + const currentTime = parseFloat(data.properties.time) + const oldTime = parseFloat(previousData.get('time')) + + if ( + data.event === 'player_click_playpause' && + oldMedia.get('event') !== 'player_click_playpause' + ) { + // User paused a video + time = currentTime - oldTime + } else if (previousData.get('event') === 'video-play') { + // From video play event to x event + time = currentTime - oldTime - 10 + } else if (data.event === 'minute-watched') { + // Minute watched event + time = currentTime - oldTime + } + + if (isNaN(time)) { + return 0 + } + + if (time < 0) { + return 0 + } + + if (time > 120) { + time = 120 // 2 minutes + } + + // we get seconds back, so we need to convert it into ms + time = time * 1000 + + return Math.floor(time) +} + const getYouTubeDuration = (data) => { let time = 0 @@ -294,7 +462,7 @@ const getYouTubeDuration = (data) => { return parseInt(time) } -const getMediaProvider = (url) => { +const getMediaProvider = (url, firstPartyUrl, referrer) => { let provider = null if (url == null) { @@ -303,7 +471,18 @@ const getMediaProvider = (url) => { // Youtube if (url.startsWith('https://www.youtube.com/api/stats/watchtime?')) { - provider = ledgerMediaProviders.YOUTUBE + return ledgerMediaProviders.YOUTUBE + } + + // Twitch + if ( + ( + (firstPartyUrl && firstPartyUrl.startsWith('https://www.twitch.tv')) || + (referrer && referrer.startsWith('https://player.twitch.tv')) + ) && + url.startsWith('https://api.mixpanel.com') + ) { + return ledgerMediaProviders.TWITCH } return provider @@ -339,14 +518,18 @@ const getMethods = () => { getMediaData, getMediaKey, milliseconds, - defaultMonthlyAmounts + defaultMonthlyAmounts, + getMediaFavicon, + generateMediaCacheData } let privateMethods = {} if (process.env.NODE_ENV === 'test') { privateMethods = { - getYouTubeDuration + getYouTubeDuration, + getTwitchDuration, + generateTwitchCacheData } } diff --git a/app/filtering.js b/app/filtering.js index 5b99063a90f..fbf013939d5 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -115,12 +115,21 @@ function registerForBeforeRequest (session, partition) { } const firstPartyUrl = module.exports.getMainFrameUrl(details) + const url = details.url // this can happen if the tab is closed and the webContents is no longer available if (!firstPartyUrl) { muonCb({ cancel: true }) return } + if (module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { + // Ledger media + const provider = ledgerUtil.getMediaProvider(url, firstPartyUrl, details.referrer) + if (provider) { + appActions.onLedgerMediaData(url, provider, details.tabId) + } + } + for (let i = 0; i < beforeRequestFilteringFns.length; i++) { let results = beforeRequestFilteringFns[i](details, isPrivate) const isAdBlock = (results.resourceName === appConfig.resourceNames.ADBLOCK) || @@ -201,7 +210,6 @@ function registerForBeforeRequest (session, partition) { } } // Redirect to non-script version of DDG when it's blocked - const url = details.url if (details.resourceType === 'mainFrame' && url.startsWith('https://duckduckgo.com/?q') && module.exports.isResourceEnabled('noScript', url, isPrivate)) { @@ -209,14 +217,6 @@ function registerForBeforeRequest (session, partition) { } else { muonCb({}) } - - if (module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { - // Ledger media - const provider = ledgerUtil.getMediaProvider(url) - if (provider) { - appActions.onLedgerMediaData(url, provider, details.tabId) - } - } }) } diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index 5709a8af76a..c470e225e27 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -27,6 +27,7 @@ const aboutActions = require('../../../../../js/about/aboutActions') const urlUtil = require('../../../../../js/lib/urlutil') const {SettingCheckbox, SiteSettingCheckbox} = require('../../common/settings') const locale = require('../../../../../js/l10n') +const ledgerUtil = require('../../../../common/lib/ledgerUtil') class LedgerTable extends ImmutableComponent { get synopsis () { @@ -153,6 +154,25 @@ class LedgerTable extends ImmutableComponent { ] } + getImage (faviconURL, providerName, publisherKey) { + if (!faviconURL && providerName) { + faviconURL = ledgerUtil.getMediaFavicon(providerName) + } + + if (!faviconURL) { + return + + + } + + return + } + getRow (synopsis) { const faviconURL = synopsis.get('faviconURL') const views = synopsis.get('views') @@ -162,6 +182,7 @@ class LedgerTable extends ImmutableComponent { const publisherURL = synopsis.get('publisherURL') const percentage = pinned ? this.pinPercentageValue(synopsis) : synopsis.get('percentage') const publisherKey = synopsis.get('publisherKey') + const providerName = synopsis.get('providerName') const siteName = synopsis.get('siteName') const defaultAutoInclude = this.enabledForSite(synopsis) @@ -178,11 +199,7 @@ class LedgerTable extends ImmutableComponent { { html:
- { - faviconURL - ? - : - } + { this.getImage(faviconURL, providerName, publisherKey) } {siteName}
, diff --git a/docs/state.md b/docs/state.md index 565e7d91923..aa5f8bbc5b9 100644 --- a/docs/state.md +++ b/docs/state.md @@ -93,6 +93,7 @@ AppStore ledgerVideos: { [mediaKey]: { publisher: string // publisher key + beatData: object // data that we get from a heartbeat } } } @@ -346,6 +347,7 @@ AppStore publishers: { [publisherId]: { duration: number, + faviconName: string, faviconURL: string, options: { exclude: boolean, @@ -355,6 +357,8 @@ AppStore }, pinPercentage: number, protocol: string, + publisherURL: string, + providerName: string, scores: { concave: number, visits: number diff --git a/img/mediaProviders/twitch.svg b/img/mediaProviders/twitch.svg new file mode 100644 index 00000000000..905e364f500 --- /dev/null +++ b/img/mediaProviders/twitch.svg @@ -0,0 +1 @@ +Glitch \ No newline at end of file diff --git a/img/mediaProviders/youtube.png b/img/mediaProviders/youtube.png new file mode 100644 index 0000000000000000000000000000000000000000..b0c05d071697d4c4a6b336af175f8a4f773b0fa3 GIT binary patch literal 348 zcmV-i0i*tjP)R+!C|{OQ8fi5j(&RwgV|) zJDB?+D@48Ap|a~GOfn%MgqdXDoA=fvkrN+e#$q75fzP{d%?h9b%ANpj;=kMe+4b~D z;pQtz1ECVM0?ou$P>nUO#lwu^kzs6NY@kUX9@#`)y9AM0+C-9U=mN7cdHJ6U{nCch z46N6$uuwgo1N%L+EtF-(AX=@!d#LMYYe5Lyq$<9K_mXo{JHZWY_^g45-A*csNd~gd zf`5JlAxL%oXc{_6KTN~F;CO`X7P@YRar_%L8)%x>1frxE#izvO_k|SRWEh=sNGN}} u-H5U;B$-w+yL&TAxRly*HCqhiHt-EOYOtGIZXVSD0000 { @@ -380,7 +399,7 @@ describe('ledgerUtil unit test', function () { }) it('unknown type', function () { - const result = ledgerUtil.getMediaData({}, 'test') + const result = ledgerUtil.getMediaId({}, 'test') assert.equal(result, null) }) @@ -395,6 +414,78 @@ describe('ledgerUtil unit test', function () { assert.equal(result, 'kLiLOkzLetE') }) }) + + describe('Twitch', function () { + it('null case', function () { + const result = ledgerUtil.getMediaId(null, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('event is not correct', function () { + const result = ledgerUtil.getMediaId({ + event: 'wrong' + }, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('properties are missing', function () { + const result = ledgerUtil.getMediaId({ + event: 'minute-watched' + }, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('content is a live stream', function () { + const result = ledgerUtil.getMediaId({ + event: 'minute-watched', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + + it('content is a vod', function () { + const result = ledgerUtil.getMediaId({ + event: 'minute-watched', + properties: { + channel: 'tchannel', + vod: 'v12343234' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel_vod_12343234') + }) + + it('event is video-play', function () { + const result = ledgerUtil.getMediaId({ + event: 'video-play', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + + it('event is player_click_playpause', function () { + const result = ledgerUtil.getMediaId({ + event: 'player_click_playpause', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + + it('event is vod_seek', function () { + const result = ledgerUtil.getMediaId({ + event: 'vod_seek', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + }) }) describe('getMediaKey', function () { @@ -408,14 +499,16 @@ describe('ledgerUtil unit test', function () { assert.equal(result, null) }) - it('id is null', function () { - const result = ledgerUtil.getMediaKey(null, ledgerMediaProviders.YOUTUBE) - assert.equal(result, null) - }) + describe('YouTube', function () { + it('id is null', function () { + const result = ledgerUtil.getMediaKey(null, ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) - it('data is ok', function () { - const result = ledgerUtil.getMediaKey('kLiLOkzLetE', ledgerMediaProviders.YOUTUBE) - assert.equal(result, 'youtube_kLiLOkzLetE') + it('data is ok', function () { + const result = ledgerUtil.getMediaKey('kLiLOkzLetE', ledgerMediaProviders.YOUTUBE) + assert.equal(result, 'youtube_kLiLOkzLetE') + }) }) }) @@ -430,17 +523,17 @@ describe('ledgerUtil unit test', function () { assert.equal(result, null) }) + it('query is not present', function () { + const result = ledgerUtil.getMediaData('https://youtube.com', ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) + describe('Youtube', function () { it('null case', function () { const result = ledgerUtil.getMediaData(null, ledgerMediaProviders.YOUTUBE) assert.equal(result, null) }) - it('query is not present', function () { - const result = ledgerUtil.getMediaData('https://youtube.com', ledgerMediaProviders.YOUTUBE) - assert.equal(result, null) - }) - it('query is present', function () { const result = ledgerUtil.getMediaData('https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339', ledgerMediaProviders.YOUTUBE) assert.deepEqual(result, { @@ -450,6 +543,33 @@ describe('ledgerUtil unit test', function () { }) }) }) + + describe('Twitch', function () { + it('null case', function () { + const result = ledgerUtil.getMediaData(null, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('data is missing', function () { + const result = ledgerUtil.getMediaData('https://api.mixpanel.com', ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('data is empty string', function () { + const result = ledgerUtil.getMediaData('https://api.mixpanel.com?data=', ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('obj is parsed correctly', function () { + const result = ledgerUtil.getMediaData('https://api.mixpanel.com?data=eyJldmVudCI6Im1pbnV0ZS13YXRjaGVkIiwicHJvcGVydGllcyI6eyJjaGFubmVsIjoidHcifX0=', ledgerMediaProviders.TWITCH) + assert.deepEqual(result, { + event: 'minute-watched', + properties: { + channel: 'tw' + } + }) + }) + }) }) describe('getYouTubeDuration', function () { @@ -490,6 +610,30 @@ describe('ledgerUtil unit test', function () { const result = ledgerUtil.getMediaProvider('https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339') assert.equal(result, ledgerMediaProviders.YOUTUBE) }) + + describe('twitch', function () { + it('we only have url', function () { + const result = ledgerUtil.getMediaProvider('https://api.mixpanel.com?data=lll') + assert.equal(result, null) + }) + + it('video is on twitch.tv', function () { + const result = ledgerUtil.getMediaProvider( + 'https://api.mixpanel.com?data=lll', + 'https://www.twitch.tv' + ) + assert.equal(result, ledgerMediaProviders.TWITCH) + }) + + it('video is embeded', function () { + const result = ledgerUtil.getMediaProvider( + 'https://api.mixpanel.com?data=lll', + 'https://www.site.tv', + 'https://player.twitch.tv' + ) + assert.equal(result, ledgerMediaProviders.TWITCH) + }) + }) }) describe('milliseconds', function () { @@ -529,4 +673,150 @@ describe('ledgerUtil unit test', function () { assert.deepEqual(ledgerUtil.defaultMonthlyAmounts.toJS(), [5.0, 7.5, 10.0, 17.5, 25.0, 50.0, 75.0, 100.0]) }) }) + + describe('getMediaFavicon', function () { + it('null case', function () { + const result = ledgerUtil.getMediaFavicon() + assert.equal(result, null) + }) + + it('youtube', function () { + const result = ledgerUtil.getMediaFavicon('YouTube') + assert.equal(result, 'youtube.png') + }) + + it('twitch', function () { + const result = ledgerUtil.getMediaFavicon('Twitch') + assert.equal(result, 'twitch.svg') + }) + }) + + describe('generateTwitchCacheData', function () { + it('null check', function () { + const result = ledgerUtil.generateTwitchCacheData() + assert.deepEqual(result.toJS(), {}) + }) + + it('properties are missing', function () { + const result = ledgerUtil.generateTwitchCacheData({ + event: 'video-play', + channel: 'test' + }) + assert.deepEqual(result.toJS(), { + event: 'video-play' + }) + }) + + it('properties are present', function () { + const result = ledgerUtil.generateTwitchCacheData({ + event: 'video-play', + properties: { + time: 100, + minute_logged: 1 + }, + channel: 'test' + }) + assert.deepEqual(result.toJS(), { + event: 'video-play', + time: 100 + }) + }) + }) + + describe('getTwitchDuration', function () { + it('null case', function () { + const result = ledgerUtil.getTwitchDuration() + assert.deepEqual(result, 0) + }) + + it('we just video playing', function () { + const result = ledgerUtil.getTwitchDuration(baseState, { + event: 'video-play' + }, 'twitch_test') + assert.deepEqual(result, 10000) + }) + + it('properties are missing', function () { + const result = ledgerUtil.getTwitchDuration(baseState, { + event: 'minute-watched' + }, 'twitch_test') + assert.deepEqual(result, 0) + }) + + it('current time is not a number', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: '1223fa' + } + }, 'twitch_test') + assert.deepEqual(result, 0) + }) + + it('user paused a video', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'player_click_playpause', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 40000) + }) + + it('first minute watched', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 30000) + }) + + it('second minute watched', function () { + const state = stateWithData + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + + const result = ledgerUtil.getTwitchDuration(state, { + event: 'minute-watched', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 40000) + }) + + it('end time is negative', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: 1519249926 + } + }, 'twitch_test') + assert.deepEqual(result, 0) + }) + + it('end time is more then 2 minutes', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: 1519449926 + } + }, 'twitch_test') + assert.deepEqual(result, 120000) + }) + + it('we need to floor end time', function () { + const state = stateWithData + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + + const result = ledgerUtil.getTwitchDuration(state, { + event: 'minute-watched', + properties: { + time: 1519279926.74353453 + } + }, 'twitch_test') + assert.deepEqual(result, 40743) + }) + }) })