diff --git a/bsconfig.json b/bsconfig.json
index ca83c58c..4045d2b8 100644
--- a/bsconfig.json
+++ b/bsconfig.json
@@ -8,8 +8,12 @@
"locale/**/*.*",
"settings/*.*"
],
- "plugins": [ "@rokucommunity/bslint" ],
+ "plugins": [
+ "@rokucommunity/bslint"
+ ],
"diagnosticFilters": [
- "**/roku_modules/**/*"
+ "**/roku_modules/**/*",
+ "**/testFramework/*",
+ "**/tests/*"
]
}
\ No newline at end of file
diff --git a/components/ItemGrid/GridItem.brs b/components/ItemGrid/GridItem.brs
index 162db0b9..9c0ebec1 100644
--- a/components/ItemGrid/GridItem.brs
+++ b/components/ItemGrid/GridItem.brs
@@ -13,11 +13,11 @@ sub init()
m.itemText.translation = [0, m.itemPoster.height + 7]
- m.alwaysShowTitles = get_user_setting("itemgrid.alwaysShowTitles") = "true"
- m.itemText.visible = m.alwaysShowTitles
+ m.gridTitles = get_user_setting("itemgrid.gridTitles")
+ m.itemText.visible = m.gridTitles = "showalways"
' Add some padding space when Item Titles are always showing
- if m.alwaysShowTitles then m.itemText.maxWidth = 250
+ if m.itemText.visible then m.itemText.maxWidth = 250
'Parent is MarkupGrid and it's parent is the ItemGrid
m.topParent = m.top.GetParent().GetParent()
@@ -43,10 +43,12 @@ sub itemContentChanged()
m.itemIcon.uri = itemData.iconUrl
m.itemText.text = itemData.Title
else if itemData.type = "Series"
- if itemData?.json?.UserData?.UnplayedItemCount <> invalid
- if itemData.json.UserData.UnplayedItemCount > 0
- m.unplayedCount.visible = true
- m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+ if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
+ if itemData?.json?.UserData?.UnplayedItemCount <> invalid
+ if itemData.json.UserData.UnplayedItemCount > 0
+ m.unplayedCount.visible = true
+ m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+ end if
end if
end if
@@ -131,16 +133,17 @@ end sub
'Display or hide title Visibility on focus change
sub focusChanged()
if m.top.itemHasFocus = true
- m.itemText.visible = true
m.itemText.repeatCount = -1
m.posterMask.scale = [1, 1]
else
- m.itemText.visible = m.alwaysShowTitles
m.itemText.repeatCount = 0
if m.topParent.alphaActive = true
m.posterMask.scale = [0.85, 0.85]
end if
end if
+ if m.gridTitles = "showonhover"
+ m.itemText.visible = m.top.itemHasFocus
+ end if
end sub
'Hide backdrop and text when poster loaded
diff --git a/components/ItemGrid/MovieLibraryView.brs b/components/ItemGrid/MovieLibraryView.brs
index ba8326bd..883a5a57 100644
--- a/components/ItemGrid/MovieLibraryView.brs
+++ b/components/ItemGrid/MovieLibraryView.brs
@@ -196,7 +196,7 @@ sub loadInitialItems()
m.itemGrid.numRows = "3"
m.selectedMovieOverview.visible = false
m.infoGroup.visible = false
- m.top.showItemTitles = get_user_setting("itemgrid.movieGridTitles")
+ m.top.showItemTitles = get_user_setting("itemgrid.gridTitles")
if LCase(m.top.showItemTitles) = "hidealways"
m.itemGrid.itemSize = "[230, 315]"
m.itemGrid.rowHeights = "[315]"
diff --git a/components/ItemGrid/MusicArtistGridItem.brs b/components/ItemGrid/MusicArtistGridItem.brs
index 438d7321..8fafec4f 100644
--- a/components/ItemGrid/MusicArtistGridItem.brs
+++ b/components/ItemGrid/MusicArtistGridItem.brs
@@ -1,5 +1,6 @@
sub init()
m.itemPoster = m.top.findNode("itemPoster")
+ m.postTextBackground = m.top.findNode("postTextBackground")
m.posterText = m.top.findNode("posterText")
m.posterText.font.size = 30
m.backdrop = m.top.findNode("backdrop")
@@ -14,11 +15,25 @@ sub init()
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
end if
+ m.gridTitles = get_user_setting("itemgrid.gridTitles")
+ m.posterText.visible = false
+ m.postTextBackground.visible = false
+
end sub
sub itemContentChanged()
m.backdrop.blendColor = "#101010"
+ m.posterText.visible = false
+ m.postTextBackground.visible = false
+
+ if isValid(m.topParent.showItemTitles)
+ if LCase(m.topParent.showItemTitles) = "showalways"
+ m.posterText.visible = true
+ m.postTextBackground.visible = true
+ end if
+ end if
+
itemData = m.top.itemContent
if not isValid(itemData) then return
@@ -38,6 +53,23 @@ sub itemContentChanged()
if m.itemPoster.loadStatus <> "ready"
m.backdrop.visible = true
end if
+ if m.top.itemHasFocus then focusChanged()
+end sub
+
+'Display or hide title Visibility on focus change
+sub focusChanged()
+ if m.top.itemHasFocus = true
+ m.posterText.repeatCount = -1
+ else
+ m.posterText.repeatCount = 0
+ end if
+
+ if isValid(m.topParent.showItemTitles)
+ if LCase(m.topParent.showItemTitles) = "showonhover"
+ m.posterText.visible = m.top.itemHasFocus
+ m.postTextBackground.visible = m.posterText.visible
+ end if
+ end if
end sub
'Hide backdrop and text when poster loaded
diff --git a/components/ItemGrid/MusicLibraryView.brs b/components/ItemGrid/MusicLibraryView.brs
index a1f18b66..615df55c 100644
--- a/components/ItemGrid/MusicLibraryView.brs
+++ b/components/ItemGrid/MusicLibraryView.brs
@@ -135,6 +135,8 @@ sub loadInitialItems()
m.sortAscending = false
end if
+ m.top.showItemTitles = get_user_setting("itemgrid.gridTitles")
+
if LCase(m.top.parentItem.json.type) = "musicgenre"
m.itemGrid.translation = "[96, 60]"
m.loadItemsTask.itemType = "MusicAlbum"
@@ -143,6 +145,7 @@ sub loadInitialItems()
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
else if LCase(m.view) = "artistspresentation" or LCase(m.options.view) = "artistspresentation"
m.loadItemsTask.genreIds = ""
+ m.top.showItemTitles = "hidealways"
else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid"
m.loadItemsTask.genreIds = ""
else
diff --git a/components/ItemGrid/MusicLibraryView.xml b/components/ItemGrid/MusicLibraryView.xml
index 5f2ec51c..f887468f 100644
--- a/components/ItemGrid/MusicLibraryView.xml
+++ b/components/ItemGrid/MusicLibraryView.xml
@@ -33,13 +33,13 @@
-
+
diff --git a/components/JFVideo.brs b/components/JFVideo.brs
index a6fe67fc..1c011ed9 100644
--- a/components/JFVideo.brs
+++ b/components/JFVideo.brs
@@ -19,6 +19,7 @@ sub init()
m.nextEpisodeButton = m.top.findNode("nextEpisode")
m.nextEpisodeButton.text = tr("Next Episode")
m.nextEpisodeButton.setFocus(false)
+ m.nextupbuttonseconds = get_user_setting("playback.nextupbuttonseconds", "30")
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
@@ -64,7 +65,11 @@ end sub
'
'Update count down text
sub updateCount()
- m.nextEpisodeButton.text = tr("Next Episode") + " " + Int(m.top.runTime - m.top.position).toStr()
+ nextEpisodeCountdown = Int(m.top.runTime - m.top.position)
+ if nextEpisodeCountdown < 0
+ nextEpisodeCountdown = 0
+ end if
+ m.nextEpisodeButton.text = tr("Next Episode") + " " + nextEpisodeCountdown.toStr()
end sub
'
@@ -77,7 +82,13 @@ end sub
' Checks if we need to display the Next Episode button
sub checkTimeToDisplayNextEpisode()
- if int(m.top.position) >= (m.top.runTime - 30)
+ nextEpisodeCountdown = Int(m.top.runTime - m.top.position)
+ if nextEpisodeCountdown < 0
+ hideNextEpisodeButton()
+ return
+ end if
+
+ if int(m.top.position) >= (m.top.runTime - Val(m.nextupbuttonseconds))
showNextEpisodeButton()
updateCount()
return
diff --git a/components/ListPoster.brs b/components/ListPoster.brs
index ae04ef18..738d7402 100644
--- a/components/ListPoster.brs
+++ b/components/ListPoster.brs
@@ -57,10 +57,12 @@ sub itemContentChanged() as void
itemData = m.top.itemContent
m.title.text = itemData.title
- if itemData?.json?.UserData?.UnplayedItemCount <> invalid
- if itemData.json.UserData.UnplayedItemCount > 0
- m.unplayedCount.visible = true
- m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+ if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
+ if itemData?.json?.UserData?.UnplayedItemCount <> invalid
+ if itemData.json.UserData.UnplayedItemCount > 0
+ m.unplayedCount.visible = true
+ m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+ end if
end if
end if
diff --git a/components/home/HomeItem.brs b/components/home/HomeItem.brs
index 42830c01..98e501c1 100644
--- a/components/home/HomeItem.brs
+++ b/components/home/HomeItem.brs
@@ -38,10 +38,12 @@ sub itemContentChanged()
end if
if LCase(itemData.type) = "series"
- if itemData?.json?.UserData?.UnplayedItemCount <> invalid
- if itemData.json.UserData.UnplayedItemCount > 0
- m.unplayedCount.visible = true
- m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+ if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
+ if itemData?.json?.UserData?.UnplayedItemCount <> invalid
+ if itemData.json.UserData.UnplayedItemCount > 0
+ m.unplayedCount.visible = true
+ m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
+ end if
end if
end if
end if
diff --git a/components/settings/settings.brs b/components/settings/settings.brs
index 8f637292..9616130e 100644
--- a/components/settings/settings.brs
+++ b/components/settings/settings.brs
@@ -187,6 +187,11 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
end if
+ if key = "options"
+ m.global.sceneManager.callFunc("popScene")
+ return true
+ end if
+
if key = "right"
settingSelected()
end if
diff --git a/components/tvshows/TVEpisodes.brs b/components/tvshows/TVEpisodes.brs
index fb69f9eb..5cb412f9 100644
--- a/components/tvshows/TVEpisodes.brs
+++ b/components/tvshows/TVEpisodes.brs
@@ -17,10 +17,12 @@ sub setSeasonLoading()
end sub
sub updateSeason()
- if m.top.seasonData?.UserData?.UnplayedItemCount <> invalid
- if m.top.seasonData.UserData.UnplayedItemCount > 0
- m.unplayedCount.visible = true
- m.unplayedEpisodeCount.text = m.top.seasonData.UserData.UnplayedItemCount
+ if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
+ if m.top.seasonData?.UserData?.UnplayedItemCount <> invalid
+ if m.top.seasonData.UserData.UnplayedItemCount > 0
+ m.unplayedCount.visible = true
+ m.unplayedEpisodeCount.text = m.top.seasonData.UserData.UnplayedItemCount
+ end if
end if
end if
diff --git a/components/tvshows/TVShowDetails.brs b/components/tvshows/TVShowDetails.brs
index fc39755b..6e578814 100644
--- a/components/tvshows/TVShowDetails.brs
+++ b/components/tvshows/TVShowDetails.brs
@@ -15,10 +15,12 @@ sub itemContentChanged()
item = m.top.itemContent
itemData = item.json
- if itemData?.UserData?.UnplayedItemCount <> invalid
- if itemData.UserData.UnplayedItemCount > 0
- m.unplayedCount.visible = true
- m.unplayedEpisodeCount.text = itemData.UserData.UnplayedItemCount
+ if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
+ if itemData?.UserData?.UnplayedItemCount <> invalid
+ if itemData.UserData.UnplayedItemCount > 0
+ m.unplayedCount.visible = true
+ m.unplayedEpisodeCount.text = itemData.UserData.UnplayedItemCount
+ end if
end if
end if
diff --git a/components/tvshows/TVShowDetails.xml b/components/tvshows/TVShowDetails.xml
index 7bdc7d8b..cf058566 100644
--- a/components/tvshows/TVShowDetails.xml
+++ b/components/tvshows/TVShowDetails.xml
@@ -33,5 +33,6 @@
+
diff --git a/locale/de_DE/translations.ts b/locale/de_DE/translations.ts
index 9fd6fef5..9d597a2e 100644
--- a/locale/de_DE/translations.ts
+++ b/locale/de_DE/translations.ts
@@ -9522,5 +9522,432 @@
Deaktiviert
+
+
+ Standard Ansicht für Film Sammlungen.
+ Settings Menu - Description for option
+
+
+
+ Film Sammlung Grid Titel
+ Settings Menu - Title for option
+
+
+
+ Wähle aus wann Serientitel gezeigt werden sollen.
+ Settings Menu - Description for option
+
+
+
+ Beim Hovern anzeigen
+
+
+
+ Immer zeigen
+
+
+
+ Niemals anzeigen
+
+
+
+ Medien Anordnung
+ UI -> Media Grid section in user setting screen.
+
+
+
+ Verwenden Sie "Wiederholen", um langsam zum ersten Element im Ordner zu überblenden. (Wenn diese Funktion deaktiviert ist, wird der Ordner sofort zum ersten Element zurückgesetzt).
+ Description for option in Setting Screen
+
+
+
+ Wenn diese Option aktiviert ist, führt die Auswahl einer TV-Serie mit nur einer Staffel direkt zur Episodenliste und nicht zu den Showdetails und der Staffelliste.
+ Settings Menu - Description for option
+
+
+
+ Generierten Splashscreen als Jellyfin's Bildschirmschoner verwenden. Jellyfin muss neu geöffnet werden, damit die Änderung wirksam wird.
+
+
+
+ Startbildschirm als Hintergrund verwenden
+ Option Title in user setting screen
+
+
+
+ Generierten Startbildschirm als Jellyfins Hintergrund verwenden. Jellyfin muss neu geöffnet werden, damit die Änderung wirksam wird.
+ Description for option in Setting Screen
+
+
+
+ Nächste Folge
+
+
+
+ Legen Sie die maximale Anzahl von Tagen fest, die eine Serie in der Liste "Als Nächstes" verbleiben soll, ohne dass sie angesehen wird.
+ Settings Menu - Description for option
+
+
+
+ Nur Text Untertitel
+ Name of a setting - should we hide subtitles that might transcode
+
+
+
+ Zeige Was ist neu Popup nach einem Jellyfin update.
+ Settings Menu - Description for option
+
+
+
+ fortsetzen
+
+
+
+ Film Sammlung standard Ansicht
+ Settings Menu - Title for option
+
+
+
+ Wenn ausgewählt, wird das Sterne und Benutzerbewertungssystem entfernt. Dies ist um spoilern vorzubeugen.
+
+
+
+ Einstellungen für das Aussehen Jellyfins.
+ Description for Design Elements user settings.
+
+
+
+ Kinomodus
+ Settings Menu - Title for option
+
+
+
+ Der Kinomodus bringt das Kinoerlebnis direkt in Ihr Wohnzimmer und bietet die Möglichkeit, benutzerdefinierte Intros vor dem Hauptfilm abzuspielen.
+ Settings Menu - Description for option
+
+
+
+ Uhr ausblenden
+ Option Title in user setting screen
+
+
+
+ Blendet alle Uhren in Jellyfin aus. Jellyfin muss neu gestartet werden, damit die Änderung wirksam wird.
+ Settings Menu - Description for option
+
+
+
+ Trailer abspielen
+
+
+
+ H.264
+ Name of codec used in settings menu
+
+
+
+ HEVC
+ Name of codec used in settings menu
+
+
+
+ Transkodierung-Information
+
+
+
+ Video-Codec
+
+
+
+ Audio-Codec
+
+
+
+ direkt
+
+
+
+ Audiokanäle
+
+
+
+ Codec-Tag
+
+
+
+ Bitrate
+ Video streaming bit rate
+
+
+
+ Container
+ Video streaming container
+
+
+
+ Video-Spektrum
+
+
+
+ Pixelformat
+ Video pixel format
+
+
+
+ Es konnten keine Alben oder Lieder dieses Künstlers gefunden werden
+ Popup message when we find no audio data for an artist
+
+
+
+ Zeige nur Text Untertitel um Transkodierungen zu minimieren.
+ Description of a setting - should we hide subtitles that might transcode
+
+
+
+ Diashow aus
+
+
+
+ Diashow an
+
+
+
+ Diashow pausiert
+
+
+
+ Diashow fortgesetzt
+
+
+
+ MPEG-4 Unterstützung
+ Settings Menu - Title for option
+
+
+
+ Serie Was ist neu Popup
+ Settings Menu - Title for option
+
+
+
+ Serien-Einstellungen.
+ Description for TV Shows user settings.
+
+
+
+ Startseite
+
+
+
+ Max. Tage im "Als Nächstes" Bereich
+ Option Title in user setting screen
+
+
+
+ Wiedergabeinformation
+
+
+
+ Stream Informationen
+
+
+
+ Breite x Höhe
+ Video width x height
+
+
+
+ Alle
+ all will reset the searchTerm so all data will be availible
+
+
+
+ Ausgestrahlt
+ Aired date label
+
+
+
+ Künstlerinnenvorstellung
+
+
+
+ Musiktitel
+
+
+
+ Lieder
+
+
+
+ Album
+
+
+
+ Alben
+
+
+
+ Alle anzeigen
+
+
+
+ Deaktiviere Community Bewertungen für Episoden
+
+
+
+ Künstler (Gitter)
+
+
+
+ Dateien-Anzahl
+ UI -> Media Grid -> Item Count in user setting screen.
+
+
+
+ Medien Anordnung Optionen.
+
+
+
+ Designelemente
+
+
+
+ Einstellungen zum Aussehen des Programms.
+
+
+
+ Einstellungen für die Wiedergabe und unterstützte Codec- und Medientypen.
+
+
+
+ Grund
+
+
+
+ Gesamte Bitrate
+
+
+
+ Codec
+
+
+
+ Größe
+ Video size
+
+
+
+ Zufällig an
+
+
+
+ Zufällig aus
+
+
+
+ gespielt
+
+
+
+ nicht gespielt
+
+
+
+ Dateien-Titel
+ UI -> Media Grid -> Item Title in user setting screen.
+
+
+
+ Level
+ Video profile level
+
+
+
+ Einstellungen für die Startseite.
+ Description for Home Page user settings.
+
+
+
+ Abmelden
+
+
+
+ Löschen
+
+
+
+ Fehler bei der wiedergabe
+ Dialog title when error occurs during playback
+
+
+
+ Fehler beim laden der Daten vom Server
+ Dialog detail when unable to load Content from Server
+
+
+ Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)
+
+ Dieses %1 hat keine Inhalte
+
+
+
+ Laufzeit
+
+
+
+ Beim Abspielen ist ein Fehler aufgetreten.
+ Dialog detail when error occurs during playback
+
+
+
+ Schlüssel speichern?
+
+
+
+ Fehler beim laden des Inhalts
+ Dialog title when unable to load Content from Server
+
+
+ Title of Tab for switching "views" when looking at a library
+
+ Ansicht
+
+
+
+ Veröffentlichungsdatum
+
+
+
+ Server wechseln
+
+
+ Name or Title field of media item
+
+ Name
+
+
+
+ Kritiker Bewertung
+
+
+
+ Altersfreigabe
+
+
+
+ IMDb Bewertung
+
+
+
+ Wiedergabeanzahl
+
+
+
+ Zuletzt abgespielt
+
+
+
+ Datum
+
diff --git a/locale/en_GB/translations.ts b/locale/en_GB/translations.ts
index 50d8ad19..335d193f 100644
--- a/locale/en_GB/translations.ts
+++ b/locale/en_GB/translations.ts
@@ -3015,5 +3015,150 @@
Albums
+
+
+ Delete Saved
+
+
+
+ On Now
+
+
+
+ Age
+
+
+
+ Died
+
+
+
+ More Like This
+
+
+
+ Born
+
+
+
+ Special Features
+
+
+
+ Save Credentials?
+
+
+
+ Press 'OK' to Close
+
+
+
+ Cast & Crew
+
+
+
+ Select an available Jellyfin server from your local network:
+ Instructions on initial app launch when the user is asked to pick a server from a list
+
+
+
+ Enter the server name or IP address
+ Title of KeyboardDialog when manually entering a server URL
+
+
+
+ TV Shows
+
+
+
+ Cancel Recording
+
+
+
+ Additional Parts
+ Additional parts of a video
+
+
+
+ View Channel
+
+
+
+ Record
+
+
+
+ Cancel Series Recording
+
+
+
+ Close
+
+
+
+ Unknown
+ Title for a cast member for which we have no information for
+
+
+
+ Record Series
+
+
+
+ Delete Saved
+
+
+
+ Save Credentials?
+
+
+
+ On Now
+
+
+
+ TV Shows
+
+
+
+ Died
+
+
+
+ More Like This
+
+
+
+ Additional Parts
+ Additional parts of a video
+
+
+
+ Delete Saved
+
+
+
+ Born
+
+
+
+ Special Features
+
+
+
+ Save Credentials?
+
+
+
+ Press 'OK' to Close
+
+
+
+ Cast & Crew
+
+
+
+ Age
+
diff --git a/locale/en_US/translations.ts b/locale/en_US/translations.ts
index 12f95af5..cb5042b7 100644
--- a/locale/en_US/translations.ts
+++ b/locale/en_US/translations.ts
@@ -557,16 +557,6 @@
Media Grid options.
-
-
- Item Titles
- UI -> Media Grid -> Item Title in user setting screen.
-
-
-
- Always show the titles below the poster images. (If disabled, the title will be shown under the highlighted item only).
- Description for option in Setting Screen
- Item Count
@@ -660,15 +650,6 @@
Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).Description for option in Setting Screen
-
-
- Details Page
-
-
-
- Options for Details pages.
- Description for Details page user settings.
- Hide Taglines
@@ -712,8 +693,8 @@
Description for Screensaver user settings.
-
- Use Splashscreen as Screensaver Background
+
+ Use Splashscreen as ScreensaverOption Title in user setting screen
@@ -795,15 +776,6 @@
Settings relating to how the application looks.
-
-
- Home Page
-
-
-
- Options for Home Page.
- Description for Home Page user settings.
- Max Days Next Up
@@ -992,9 +964,9 @@
Movies (Grid)
-
- Movie Library Grid Titles
- Settings Menu - Title for option
+
+ Item Titles
+ Title of a setting - when should we show the title text of a grid item
@@ -1053,5 +1025,92 @@
Biographical information for this person is not currently available.
+
+
+ Playback Bitrate Limits
+
+
+
+ Set limits for how high playback bitrates are allowed to be.
+
+
+
+ Limits Enabled
+
+
+
+ If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.
+
+
+
+ Playback Bitrate Limit
+
+
+
+ Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.
+
+
+
+ Libraries
+
+
+
+ Settings relating to the appearance of Library pages
+
+
+
+ General
+
+
+
+ Settings relating to the appearance of the Home screen and the program in general.
+
+
+
+ Grid View Settings
+
+
+
+ Settings that apply when Grid views are enabled.
+
+
+
+ Settings relating to the appearance of pages in TV Libraries.
+
+
+
+ Settings relating to the appearance of pages in Movie Libraries.
+
+
+
+ Presentation
+ Title of an option - name of presentation view
+
+
+
+ Grid
+ Title of an option - name of grid view
+
+
+
+
+ Disable Unwatched Episode Count
+ Settings Menu - Title for option
+
+
+
+ If enabled, the number of unwatched episodes in a series/season will be removed.
+ Settings Menu - Description for option
+
+
+
+ Next Episode Button Time
+ Settings Menu - Title for option
+
+
+
+ Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.
+ Settings Menu - Description for option
+
diff --git a/locale/es_419/translations.ts b/locale/es_419/translations.ts
index 635100f3..27c9ad4c 100644
--- a/locale/es_419/translations.ts
+++ b/locale/es_419/translations.ts
@@ -1713,5 +1713,41 @@
DomingoDay of Week
+
+
+ %1 de %2
+ Item position and count. %1 = current item. %2 = total number of items
+
+
+
+ Detalles
+
+
+
+ Opciones que alteran el diseño de Jellyfin.
+ Description for Design Elements user settings.
+
+
+
+ Tamaño
+ Video size
+
+
+
+ Transmitido
+ Aired date label
+
+
+
+ Álbum
+
+
+
+ Álbumes
+
+
+
+ Canción
+
diff --git a/locale/es_AR/translations.ts b/locale/es_AR/translations.ts
index c17b4b6f..7168d112 100644
--- a/locale/es_AR/translations.ts
+++ b/locale/es_AR/translations.ts
@@ -1345,5 +1345,14 @@
Error al recuperar contenidoDialog title when unable to load Content from Server
+
+
+ Canción
+
+
+
+ Tamaño
+ Video size
+
diff --git a/locale/es_ES/translations.ts b/locale/es_ES/translations.ts
index b12e816d..1b619dd5 100644
--- a/locale/es_ES/translations.ts
+++ b/locale/es_ES/translations.ts
@@ -2203,5 +2203,543 @@
Cambiar Servidor
+
+
+ Borrar Credenciales
+
+
+
+ Puntuación de la crítica
+
+
+
+ Edad
+
+
+
+ hoy
+ Current day
+
+
+
+ Muerto/a
+
+
+
+ Puntuación de IMDb
+
+
+
+ Reparto y equipo
+
+
+
+ Sábado
+ Day of Week
+
+
+
+ Repetir
+ If TV Shows has previously been broadcasted
+
+
+
+ Cuadrícula de medios
+ UI -> Media Grid section in user setting screen.
+
+
+
+ Cerrar sesión
+
+
+
+ ¿Guardar credenciales?
+
+
+
+ Error durante la reproducción
+ Dialog title when error occurs during playback
+
+
+
+ Ha ocurrido un error al reproducir este contenido.
+ Dialog detail when error occurs during playback
+
+
+
+ Reproduciendo contenido del canal
+
+
+
+ Error de reproducción de contenido del canal
+
+
+ Name or Title field of media item
+
+ Nombre
+
+
+
+ Fecha en que se agregó
+
+
+
+ Fecha de reproducción
+
+
+
+ Control Parental
+
+
+
+ Fecha de estreno
+
+
+
+ Tiempo de duración
+
+
+ Title of Tab for switching "views" when looking at a library
+
+ Vista
+
+
+ Title of Tab for options to sort library content
+
+ Clasificar
+
+
+ Title of Tab for options to filter library content
+
+ Filtro
+
+
+
+ Funciones especiales
+
+
+
+ No se ha podido reproducir el contenido del canal de este servidor
+
+
+
+ Conteo de reproducción
+
+
+
+ En directo ahora
+
+
+
+ Error recuperando contenido
+ Dialog title when unable to load Content from Server
+
+
+
+ Ha ocurrido un error al tratar de recuperar la información de este contenido desde el servidor.
+ Dialog detail when unable to load Content from Server
+
+
+ Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)
+
+ Este %1 no contiene elementos
+
+
+
+ Nacido/a
+
+
+
+ Mas de este estilo
+
+
+
+ Se ha encontrado un error al reproducir este elemento. El servidor no proveyó la información necesaria para la transcodificación.
+ Content of message box when trying to play an item which requires transcoding, and the server did not provide transcode url
+
+
+
+ Utilizar la búsqueda remota por voz
+ Help text in search voice text box
+
+
+
+ Películas (presentación)
+
+
+
+ Películas (cuadrícula)
+
+
+
+ Programas de televisión
+
+
+
+ ayer
+ Previous day
+
+
+
+ mañana
+ Next day
+
+
+
+ Jueves
+ Day of Week
+
+
+
+ Viernes
+ Day of Week
+
+
+
+ Comenzó a las
+ (Past Tense) For defining time when a program started today (e.g. Started at 08:00)
+
+
+
+ Comienza a las
+ (Future Tense) For defining time when a program will start today (e.g. Starts at 08:00)
+
+
+
+ En vivo
+ If TV Show is being broadcast live (not pre-recorded)
+
+
+
+ Guía de televisión
+ Menu option for showing Live TV Guide / Schedule
+
+
+
+ Grabar serie
+
+
+
+ Cancelar la grabación
+
+
+
+ Cancelar la grabación de la serie
+
+
+
+ Cerrar
+
+
+
+ Conectando con el servidor
+ Message to display to user while client is attempting to connect to the server
+
+
+
+ No se ha encontrado
+ Title of message box when the requested content is not found on the server
+
+
+
+ Desconocido
+ Title for a cast member for which we have no information for
+
+
+
+ El contenido solicitado no existe en el servidor
+ Content of message box when the requested content is not found on the server
+
+
+
+ Agregar el nombre del servidor o su dirección de IP
+ Title of KeyboardDialog when manually entering a server URL
+
+
+
+ Elige un servidor Jellyfin disponible en la red local:
+ Instructions on initial app launch when the user is asked to pick a server from a list
+
+
+
+ Soporte de reproducción directa para contenido MPEG-2 (ej., televisión en vivo). Esto previene la transcodificación de contenido MPEG-2, pero a mayor uso de ancho de banda.
+ Settings Menu - Description for option
+
+
+
+ Soporte de reproducción directa para contenido MPEG-4. Esto podría requerir ser deshabilitado para poder reproducir los archivos de video con encodificación DIVX.
+ Settings Menu - Description for option
+
+
+
+ **EXPERIMENTAL** Soporte de reproducción directa para contenido AV1 si este dispositivo Roku es compatible.
+ Description of a setting - should we try to direct play experimental av1 codec
+
+
+
+ Siempre mostrar los títulos por debajo de las imágenes de cartelera. (Si se deshabilita, el título se mostrará debajo del elemento resaltado solamente).
+ Description for option in Setting Screen
+
+
+
+ Conteo de elementos
+ UI -> Media Grid -> Item Count in user setting screen.
+
+
+
+ Mostrar el conteo de elementos en la biblioteca y en el índice del elemento seleccionado.
+ Description for option in Setting Screen
+
+
+
+ Agregar a favoritos
+ Button Text - When pressed, sets item as Favorite
+
+
+
+ Marcar como visto
+ Button Text - When pressed, marks item as Warched
+
+
+
+ Ir a serie
+ Continue Watching Popup Menu - Navigate to the Series Detail Page
+
+
+
+ Ir a la temporada
+ Continue Watching Popup Menu - Navigate to the Season Page
+
+
+
+ Ir al episodio
+ Continue Watching Popup Menu - Navigate to the Episode Detail Page
+
+
+
+ Buscar ahora
+ Help text in search Box
+
+
+
+ Presiona 'OK' para cerrar
+
+
+
+ Partes adicionales
+ Additional parts of a video
+
+
+
+ Películas
+
+
+
+ Domingo
+ Day of Week
+
+
+
+ Lunes
+ Day of Week
+
+
+
+ Martes
+ Day of Week
+
+
+
+ Miércoles
+ Day of Week
+
+
+
+ Comienza a las
+ (Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00)
+
+
+
+ Terminó a las
+ (Past Tense) For defining time when a program will ended (e.g. Ended at 08:00)
+
+
+
+ Termina a las
+ (Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00)
+
+
+
+ Canales
+ Menu option for showing Live TV Channel List
+
+
+
+ Ver Canal
+
+
+
+ Grabar
+
+
+
+ Si no hay servidores disponibles, puedes agregar manualmente la URL:
+ Instructions on initial app launch when the user is asked to manually enter a server URL
+
+
+
+ Error obteniendo la Información de reproducción
+ Dialog Title: Received error from server when trying to get information about the selected item for playback
+
+
+
+ Versión
+
+
+
+ Reproducción
+ Title for Playback section in user setting screen.
+
+
+
+ Interfaz de usuario
+ Title for User Interface section in user setting screen.
+
+
+
+ Opciones de la cuadrícula de medios.
+
+
+
+ Soporte de Codec
+ Settings Menu - Title for settings group related to codec support
+
+
+
+ Habilitar o desactivar la reproducción directa para codecs opcionales
+ Settings Menu - Title for settings group related to codec support
+
+
+
+ MPEG-2
+ Name of codec used in settings menu
+
+
+
+ MPEG-4
+ Name of codec used in settings menu
+
+
+
+ AV1
+ Name of a setting - should we try to direct play experimental av1 codec
+
+
+
+ Títulos de elementos
+ UI -> Media Grid -> Item Title in user setting screen.
+
+
+
+ Comenzó a las
+ (Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00)
+
+
+
+ Activado
+
+
+
+ Desactivado
+
+
+
+ espectáculos
+
+
+
+ Conexión rápida
+
+
+
+ (El diálogo se cerrará automáticamente)
+
+
+
+ Vuelva a la parte superior
+ UI -> Media Grid -> Item Title in user setting screen.
+
+
+
+ Puede buscar títulos, personas, canales de TV en vivo y más
+ Help text in search results
+
+
+
+ Aquí está su código de conexión rápida:
+
+
+
+ Hubo un error al autenticarse a través de Quick Connect.
+
+
+
+ Redes
+
+
+
+ Estudios
+
+
+
+ Use el botón de reproducción para animar lentamente al primer elemento de la carpeta. (Si está deshabilitado, la carpeta se restablecerá al primer elemento inmediatamente).
+ Description for option in Setting Screen
+
+
+
+ Eliga un servidor Jellyfin disponible en la red local:
+ Instructions on initial app launch when the user is asked to pick a server from a list
+
+
+
+ %1 de %2
+ Item position and count. %1 = current item. %2 = total number of items
+
+
+
+ Opciones para el protector de pantalla de Jellyfin.
+ Description for Screensaver user settings.
+
+
+
+ Siguiente episodio
+
+
+
+ Tamaño
+ Video size
+
+
+
+ Canción
+
+
+
+ Reproducir Trailer
+
+
+
+ Protector de pantalla
+
+
+
+ Opciones que alteran el diseño de Jellyfin.
+ Description for Design Elements user settings.
+
+
+
+ Modo Cine
+ Settings Menu - Title for option
+
diff --git a/locale/fr_CA/translations.ts b/locale/fr_CA/translations.ts
index acba06f5..41a2008c 100644
--- a/locale/fr_CA/translations.ts
+++ b/locale/fr_CA/translations.ts
@@ -1124,5 +1124,53 @@
Changer de serveur
+
+
+ Chargement des données de la chaîne
+
+
+
+ Erreur lors du chargement des données de la chaîne
+
+
+
+ En ce moment
+
+
+
+ Erreur lors de la récupération du contenu
+ Dialog title when unable to load Content from Server
+
+
+
+ Une erreur s'est produite lors de la lecture de cet élément.
+ Dialog detail when error occurs during playback
+
+
+
+ Changer de serveur
+
+
+
+ Se déconnecter
+
+
+
+ Sauvegarder les informations d'authentification ?
+
+
+
+ Supprimer les valeurs enregistrées
+
+
+
+ Erreur lors de la lecture
+ Dialog title when error occurs during playback
+
+
+
+ Une erreur s'est produite lors de la récupération des données de cet élément depuis le serveur.
+ Dialog detail when unable to load Content from Server
+
diff --git a/locale/hu/translations.ts b/locale/hu/translations.ts
index 26e62dde..f2d7e577 100644
--- a/locale/hu/translations.ts
+++ b/locale/hu/translations.ts
@@ -10102,5 +10102,82 @@ elemeket
MPEG-4 TámogatásSettings Menu - Title for option
+
+
+ Album
+
+
+
+ Mutassa Mind
+
+
+
+ Alapértelmezett nézet Filmkönyvtáraknak.
+ Settings Menu - Description for option
+
+
+
+ Filmkönyvtár Rácscímek
+ Settings Menu - Title for option
+
+
+
+ Mutassa Rámutatáskor
+
+
+
+ Mindig Mutassa
+
+
+
+ Sose Mutassa
+
+
+
+ Albumok
+
+
+
+ Újdonságok felugró ablak mutatása miután a Jellyfin a legújabb verzióra frissült.
+ Settings Menu - Description for option
+
+
+
+ Filmkönyvtár Alapértelmezett Nézet
+ Settings Menu - Title for option
+
+
+
+ Válaszd ki mikor mutassa a címeket.
+ Settings Menu - Description for option
+
+
+
+ Ha bekapcsolja, akkor a csillagok és a közösség általi értékelések el lesznek rejtve a sorozatok epizódjainál, hogy elkerülhetőek legyenek a spoilerek a közelgő jó/rossz epizódoknál.
+
+
+
+ Zene
+
+
+
+ Zenék
+
+
+
+ Közösségi Minősítés Kikapcsolása az Epizódoknál
+
+
+
+ Lejátszott
+
+
+
+ Folytatható
+
+
+
+ Nem játszott
+
diff --git a/locale/it_IT/translations.ts b/locale/it_IT/translations.ts
index f8405190..62a944ec 100644
--- a/locale/it_IT/translations.ts
+++ b/locale/it_IT/translations.ts
@@ -1952,5 +1952,13 @@
Cambia server
+
+
+ Esci
+
+
+
+ Cambia Server
+
diff --git a/locale/pt_BR/translations.ts b/locale/pt_BR/translations.ts
index ad52f7d2..301100fb 100644
--- a/locale/pt_BR/translations.ts
+++ b/locale/pt_BR/translations.ts
@@ -2643,5 +2643,53 @@ não contém itens
Classificação Etária
+
+
+ Sair
+
+
+
+ Salvar Credenciais?
+
+
+
+ Deletar Salvos
+
+
+
+ Erro Durante a Reprodução
+ Dialog title when error occurs during playback
+
+
+
+ Mudar Servidor
+
+
+
+ Erro ao Carregar Conteúdo
+ Dialog title when unable to load Content from Server
+
+
+
+ Carregando Dados do Canal
+
+
+
+ Foi encontrado um erro na reprodução deste item.
+ Dialog detail when error occurs during playback
+
+
+
+ Em Exibição
+
+
+
+ Houve um erro ao recuperar os dados deste item do servidor.
+ Dialog detail when unable to load Content from Server
+
+
+
+ Erro ao carregar os Dados do Canal
+
diff --git a/locale/sk/translations.ts b/locale/sk/translations.ts
index 8e2024a2..4e72b54c 100644
--- a/locale/sk/translations.ts
+++ b/locale/sk/translations.ts
@@ -958,5 +958,169 @@
PrehrávanieTitle for Playback section in user setting screen.
+
+ Message displayed in Item Grid when no item to display. %1 is container type (e.g. Boxset, Collection, Folder, etc)
+
+ Tento %1 neobsahuje žiadne položky
+
+
+
+ utorok
+ Day of Week
+
+
+
+ piatok
+ Day of Week
+
+
+
+ Hodnotenie IMDb
+
+
+
+ Dátum vydania
+
+
+ Title of Tab for switching "views" when looking at a library
+
+ Zobrazenie
+
+
+
+ Uložiť prihlasovacie údaje?
+
+
+
+ Dátum prehrania
+
+
+
+ Obsadenie a štáb
+
+
+
+ Filmy
+
+
+ Name or Title field of media item
+
+ Meno
+
+
+
+ Odstrániť uložené
+
+
+
+ Dátum pridania
+
+
+
+ Rodičovské hodnotenie
+
+
+
+ Počet prehraní
+
+
+
+ Dĺžka
+
+
+ Title of Tab for options to sort library content
+
+ Zoradenie
+
+
+ Title of Tab for options to filter library content
+
+ Filter
+
+
+
+ Dátum narodenia
+
+
+
+ Dátum úmrtia
+
+
+
+ Vek
+
+
+
+ Viac podobných
+
+
+
+ Hodnotenie kritikov
+
+
+
+ Zatvorte stlačením tlačidla 'OK'
+
+
+
+ Špeciálne Funkcie
+
+
+
+ Dodatočné Časti
+ Additional parts of a video
+
+
+
+ včera
+ Previous day
+
+
+
+ nedeľa
+ Day of Week
+
+
+
+ pondelok
+ Day of Week
+
+
+
+ streda
+ Day of Week
+
+
+
+ štvrtok
+ Day of Week
+
+
+
+ sobota
+ Day of Week
+
+
+
+ Filmy (prezentácia)
+
+
+
+ Filmy (mriežka)
+
+
+
+ TV Seriály
+
+
+
+ dnes
+ Current day
+
+
+
+ zajtra
+ Next day
+
diff --git a/package-lock.json b/package-lock.json
index 0b7b2e69..7ad2e03b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1345,14 +1345,14 @@
}
},
"node_modules/jszip": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
- "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
- "set-immediate-shim": "~1.0.1"
+ "setimmediate": "^1.0.5"
}
},
"node_modules/latinize": {
@@ -2007,13 +2007,10 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
- "node_modules/set-immediate-shim": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
- "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
- "engines": {
- "node": ">=0.10.0"
- }
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/sob": {
"name": "slide-out-button",
@@ -3422,14 +3419,14 @@
}
},
"jszip": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
- "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
- "set-immediate-shim": "~1.0.1"
+ "setimmediate": "^1.0.5"
}
},
"latinize": {
@@ -3914,10 +3911,10 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
- "set-immediate-shim": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
- "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
+ "setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"sob": {
"version": "npm:slide-out-button@1.0.1",
diff --git a/settings/settings.json b/settings/settings.json
index 8dabaf52..50179bad 100644
--- a/settings/settings.json
+++ b/settings/settings.json
@@ -30,6 +30,26 @@
}
]
},
+ {
+ "title": "Playback Bitrate Limits",
+ "description": "Set limits for how high playback bitrates are allowed to be.",
+ "children": [
+ {
+ "title": "Limits Enabled",
+ "description": "If enabled, playback bitrates will be limited based on the 'Playback Bitrate Limit' setting.",
+ "settingName": "playback.bitrate.maxlimited",
+ "type": "bool",
+ "default": "true"
+ },
+ {
+ "title": "Playback Bitrate Limit",
+ "description": "Max bitrate (Mbps) allowed if limits are enabled. Set to 0 to use Roku's specifications.",
+ "settingName": "playback.bitrate.limit",
+ "type": "integer",
+ "default": "0"
+ }
+ ]
+ },
{
"title": "Profile Level Support",
"description": "Attempt Direct Play of potentially unsupported profile levels",
@@ -57,6 +77,13 @@
"type": "bool",
"default": "false"
},
+ {
+ "title": "Next Episode Button Time",
+ "description": "Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.",
+ "settingName": "playback.nextupbuttonseconds",
+ "type": "integer",
+ "default": "30"
+ },
{
"title": "Text Subtitles Only",
"description": "Only display text subtitles to minimize transcoding.",
@@ -66,21 +93,21 @@
}
]
},
- {
- "title": "Show What's New Popup",
- "description": "Show What's New popup when Jellyfin is updated to a new version.",
- "settingName": "load.allowwhatsnew",
- "type": "bool",
- "default": "true"
- },
{
"title": "User Interface",
"description": "Settings relating to how the application looks.",
"children": [
{
- "title": "Home Page",
- "description": "Options for Home page.",
+ "title": "General",
+ "description": "Settings relating to the appearance of the Home screen and the program in general.",
"children": [
+ {
+ "title": "Hide Clock",
+ "description": "Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.",
+ "settingName": "ui.design.hideclock",
+ "type": "bool",
+ "default": "false"
+ },
{
"title": "Max Days Next Up",
"description": "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.",
@@ -88,61 +115,22 @@
"type": "integer",
"default": "365"
},
+ {
+ "title": "Show What's New Popup",
+ "description": "Show What's New popup when Jellyfin is updated to a new version.",
+ "settingName": "load.allowwhatsnew",
+ "type": "bool",
+ "default": "true"
+ },
{
"title": "Use Splashscreen as Home Background",
"description": "Use generated splashscreen image as Jellyfin's home background. Jellyfin will need to be closed and reopened for change to take effect.",
"settingName": "ui.home.splashBackground",
"type": "bool",
"default": "false"
- }
- ]
- },
- {
- "title": "Details Page",
- "description": "Options for Details pages.",
- "children": [
- {
- "title": "Hide Taglines",
- "description": "Hides tagline text on details pages.",
- "settingName": "ui.details.hidetagline",
- "type": "bool",
- "default": "false"
- }
- ]
- },
- {
- "title": "TV Shows",
- "description": "Options for TV Shows.",
- "children": [
- {
- "title": "Blur Unwatched Episodes",
- "description": "If enabled, images of unwatched episodes will be blurred.",
- "settingName": "ui.tvshows.blurunwatched",
- "type": "bool",
- "default": "false"
},
{
- "title": "Skip Details for Single Seasons",
- "description": "If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.",
- "settingName": "ui.tvshows.goStraightToEpisodeListing",
- "type": "bool",
- "default": "false"
- },
- {
- "title":"Disable Community Rating for Episodes",
- "description": "If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.",
- "settingName": "ui.tvshows.disableCommunityRating",
- "type":"bool",
- "default":"false"
- }
- ]
- },
- {
- "title": "Screensaver",
- "description": "Options for Jellyfin's screensaver.",
- "children": [
- {
- "title": "Use Splashscreen as Screensaver Background",
+ "title": "Use Splashscreen as Screensaver",
"description": "Use generated splashscreen image as Jellyfin's screensaver background. Jellyfin will need to be closed and reopened for change to take effect.",
"settingName": "ui.screensaver.splashBackground",
"type": "bool",
@@ -151,80 +139,119 @@
]
},
{
- "title": "Design Elements",
- "description": "Options that alter the design of Jellyfin.",
+ "title": "Libraries",
+ "description": "Settings relating to the appearance of Library pages.",
"children": [
{
- "title": "Hide Clock",
- "description": "Hides all clocks in Jellyfin. Jellyfin will need to be closed and reopened for change to take effect.",
- "settingName": "ui.design.hideclock",
- "type": "bool",
- "default": "false"
- }
- ]
- },
- {
- "title": "Media Grid",
- "description": "Media Grid options.",
- "children": [
- {
- "title": "Movie Library Default View",
- "description": "Default view for Movie Libraries.",
- "settingName": "itemgrid.movieDefaultView",
- "type": "radio",
- "default": "movies",
- "options": [
+ "title": "General",
+ "description": "Settings relating to the appearance of pages in all Libraries.",
+ "children": [
{
- "title": "Movies (Presentation)",
- "id": "Movies"
+ "title": "Grid View Settings",
+ "description": "Settings that apply when Grid views are enabled.",
+ "children": [
+ {
+ "title": "Item Count",
+ "description": "Show item count in the library and index of selected item.",
+ "settingName": "itemgrid.showItemCount",
+ "type": "bool",
+ "default": "false"
+ },
+ {
+ "title": "Item Titles",
+ "description": "Select when to show titles.",
+ "settingName": "itemgrid.gridTitles",
+ "type": "radio",
+ "default": "showonhover",
+ "options": [
+ {
+ "title": "Show On Hover",
+ "id": "showonhover"
+ },
+ {
+ "title": "Always Show",
+ "id": "showalways"
+ },
+ {
+ "title": "Always Hide",
+ "id": "hidealways"
+ }
+ ]
+ }
+ ]
},
{
- "title": "Movies (Grid)",
- "id": "MoviesGrid"
+ "title": "Hide Taglines",
+ "description": "Hides tagline text on details pages.",
+ "settingName": "ui.details.hidetagline",
+ "type": "bool",
+ "default": "false"
+ },
+ {
+ "title": "Return to Top",
+ "description": "Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).",
+ "settingName": "itemgrid.reset",
+ "type": "bool",
+ "default": "true"
}
]
},
{
- "title": "Movie Library Grid Titles",
- "description": "Select when to show titles.",
- "settingName": "itemgrid.movieGridTitles",
- "type": "radio",
- "default": "showonhover",
- "options": [
+ "title": "Movies",
+ "description": "Settings relating to the appearance of pages in Movie Libraries.",
+ "children": [
{
- "title": "Show On Hover",
- "id": "showonhover"
- },
- {
- "title": "Always Show",
- "id": "showalways"
- },
- {
- "title": "Always Hide",
- "id": "hidealways"
+ "title": "Default View",
+ "description": "Default view for Movie Libraries.",
+ "settingName": "itemgrid.movieDefaultView",
+ "type": "radio",
+ "default": "movies",
+ "options": [
+ {
+ "title": "Movies (Presentation)",
+ "id": "Movies"
+ },
+ {
+ "title": "Movies (Grid)",
+ "id": "MoviesGrid"
+ }
+ ]
}
]
},
{
- "title": "Item Count",
- "description": "Show item count in the library and index of selected item.",
- "settingName": "itemgrid.showItemCount",
- "type": "bool",
- "default": "false"
- },
- {
- "title": "Item Titles",
- "description": "Always show the titles below the poster images. (If disabled, the title will be shown under the highlighted item only).",
- "settingName": "itemgrid.alwaysShowTitles",
- "type": "bool",
- "default": "false"
- },
- {
- "title": "Return to Top",
- "description": "Use the replay button to slowly animate to the first item in the folder. (If disabled, the folder will reset to the first item immediately).",
- "settingName": "itemgrid.reset",
- "type": "bool",
- "default": "true"
+ "title": "TV Shows",
+ "description": "Settings relating to the appearance of pages in TV Libraries.",
+ "children": [
+ {
+ "title": "Blur Unwatched Episodes",
+ "description": "If enabled, images of unwatched episodes will be blurred.",
+ "settingName": "ui.tvshows.blurunwatched",
+ "type": "bool",
+ "default": "false"
+ },
+ {
+ "title": "Disable Community Rating for Episodes",
+ "description": "If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.",
+ "settingName": "ui.tvshows.disableCommunityRating",
+ "type": "bool",
+ "default": "false"
+ },
+ {
+ "title": "Disable Unwatched Episode Count",
+ "description": "If enabled, the number of unwatched episodes in a series/season will be removed.",
+ "settingName": "ui.tvshows.disableUnwatchedEpisodeCount",
+ "type": "bool",
+ "default": "false"
+ },
+ {
+ "title": "Skip Details for Single Seasons",
+ "description": "If enabled, selecting a TV series with only one season will go straight to the episode list rather than the show details and season list.",
+ "settingName": "ui.tvshows.goStraightToEpisodeListing",
+ "type": "bool",
+ "default": "false"
+ }
+ ]
}
]
}
diff --git a/source/Main.brs b/source/Main.brs
index 8c3b5831..d006d12b 100644
--- a/source/Main.brs
+++ b/source/Main.brs
@@ -2,6 +2,22 @@ sub Main (args as dynamic) as void
appInfo = CreateObject("roAppInfo")
+ if appInfo.IsDev() and args.RunTests = "true" and TF_Utils__IsFunction(TestRunner)
+ ' POST to {ROKU ADDRESS}:8060/launch/dev?RunTests=true
+ Runner = TestRunner()
+
+ Runner.SetFunctions([
+ TestSuite__Misc
+ ])
+
+ Runner.Logger.SetVerbosity(1)
+ Runner.Logger.SetEcho(false)
+ Runner.Logger.SetJUnit(false)
+ Runner.SetFailFast(true)
+
+ Runner.Run()
+ end if
+
' The main function that runs when the application is launched.
m.screen = CreateObject("roSGScreen")
@@ -136,7 +152,7 @@ sub Main (args as dynamic) as void
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "Genre"
' User clicked on a genre folder
- if selectedItem.collectionType = "movies"
+ if selectedItem.json.MovieCount > 0
group = CreateMovieLibraryView(selectedItem)
else
group = CreateItemGrid(selectedItem)
diff --git a/source/testFramework/UnitTestFramework.brs b/source/testFramework/UnitTestFramework.brs
new file mode 100644
index 00000000..8989edef
--- /dev/null
+++ b/source/testFramework/UnitTestFramework.brs
@@ -0,0 +1,2867 @@
+'*****************************************************************
+'* Roku Unit Testing Framework
+'* Automating test suites for Roku channels.
+'*
+'* Build Version: 2.1.1
+'* Build Date: 05/06/2019
+'*
+'* Public Documentation is avaliable on GitHub:
+'* https://github.com/rokudev/unit-testing-framework
+'*
+'*****************************************************************
+'*****************************************************************
+'* Copyright Roku 2011-2019
+'* All Rights Reserved
+'*****************************************************************
+
+' Functions in this file:
+
+' BaseTestSuite
+' BTS__AddTest
+' BTS__CreateTest
+' BTS__Fail
+' BTS__AssertFalse
+' BTS__AssertTrue
+' BTS__AssertEqual
+' BTS__AssertNotEqual
+' BTS__AssertInvalid
+' BTS__AssertNotInvalid
+' BTS__AssertAAHasKey
+' BTS__AssertAANotHasKey
+' BTS__AssertAAHasKeys
+' BTS__AssertAANotHasKeys
+' BTS__AssertArrayContains
+' BTS__AssertArrayNotContains
+' BTS__AssertArrayContainsSubset
+' BTS__AssertArrayNotContainsSubset
+' BTS__AssertArrayCount
+' BTS__AssertArrayNotCount
+' BTS__AssertEmpty
+' BTS__AssertNotEmpty
+
+' ----------------------------------------------------------------
+' Main function. Create BaseTestSuite object.
+
+' @return A BaseTestSuite object.
+' ----------------------------------------------------------------
+function BaseTestSuite()
+ this = {}
+ this.Name = "BaseTestSuite"
+ this.SKIP_TEST_MESSAGE_PREFIX = "SKIP_TEST_MESSAGE_PREFIX__"
+ ' Test Cases methods
+ this.testCases = []
+ this.IS_NEW_APPROACH = false
+ this.addTest = BTS__AddTest
+ this.createTest = BTS__CreateTest
+ this.StorePerformanceData = BTS__StorePerformanceData
+
+ ' Assertion methods which determine test failure or skipping
+ this.skip = BTS__Skip
+ this.fail = BTS__Fail
+ this.assertFalse = BTS__AssertFalse
+ this.assertTrue = BTS__AssertTrue
+ this.assertEqual = BTS__AssertEqual
+ this.assertNotEqual = BTS__AssertNotEqual
+ this.assertInvalid = BTS__AssertInvalid
+ this.assertNotInvalid = BTS__AssertNotInvalid
+ this.assertAAHasKey = BTS__AssertAAHasKey
+ this.assertAANotHasKey = BTS__AssertAANotHasKey
+ this.assertAAHasKeys = BTS__AssertAAHasKeys
+ this.assertAANotHasKeys = BTS__AssertAANotHasKeys
+ this.assertArrayContains = BTS__AssertArrayContains
+ this.assertArrayNotContains = BTS__AssertArrayNotContains
+ this.assertArrayContainsSubset = BTS__AssertArrayContainsSubset
+ this.assertArrayNotContainsSubset = BTS__AssertArrayNotContainsSubset
+ this.assertArrayCount = BTS__AssertArrayCount
+ this.assertArrayNotCount = BTS__AssertArrayNotCount
+ this.assertEmpty = BTS__AssertEmpty
+ this.assertNotEmpty = BTS__AssertNotEmpty
+
+ ' Type Comparison Functionality
+ this.eqValues = TF_Utils__EqValues
+ this.eqAssocArrays = TF_Utils__EqAssocArray
+ this.eqArrays = TF_Utils__EqArray
+ this.baseComparator = TF_Utils__BaseComparator
+
+ return this
+end function
+
+' ----------------------------------------------------------------
+' Add a test to a suite's test cases array.
+
+' @param name (string) A test name.
+' @param func (object) A pointer to test function.
+' @param setup (object) A pointer to setup function.
+' @param teardown (object) A pointer to teardown function.
+' @param arg (dynamic) A test function arguments.
+' @param hasArgs (boolean) True if test function has parameters.
+' @param skip (boolean) Skip test run.
+' ----------------------------------------------------------------
+sub BTS__AddTest(name as string, func as object, setup = invalid as object, teardown = invalid as object, arg = invalid as dynamic, hasArgs = false as boolean, skip = false as boolean)
+ m.testCases.Push(m.createTest(name, func, setup, teardown, arg, hasArgs, skip))
+end sub
+
+' ----------------------------------------------------------------
+' Create a test object.
+
+' @param name (string) A test name.
+' @param func (object) A pointer to test function.
+' @param setup (object) A pointer to setup function.
+' @param teardown (object) A pointer to teardown function.
+' @param arg (dynamic) A test function arguments.
+' @param hasArgs (boolean) True if test function has parameters.
+' @param skip (boolean) Skip test run.
+'
+' @return TestCase object.
+' ----------------------------------------------------------------
+function BTS__CreateTest(name as string, func as object, setup = invalid as object, teardown = invalid as object, arg = invalid as dynamic, hasArgs = false as boolean, skip = false as boolean) as object
+ return {
+ Name: name
+ Func: func
+ SetUp: setup
+ TearDown: teardown
+
+ perfData: {}
+
+ hasArguments: hasArgs
+ arg: arg
+
+ skip: skip
+ }
+end function
+
+'----------------------------------------------------------------
+' Store performance data to current test instance.
+'
+' @param name (string) A property name.
+' @param value (Object) A value of data.
+'----------------------------------------------------------------
+sub BTS__StorePerformanceData(name as string, value as object)
+ timestamp = StrI(CreateObject("roDateTime").AsSeconds())
+ m.testInstance.perfData.Append({
+ name: {
+ "value": value
+ "timestamp": timestamp
+ }
+ })
+ ' print performance data to console
+ ? "PERF_DATA: " + m.testInstance.Name + ": " + timestamp + ": " + name + "|" + TF_Utils__AsString(value)
+end sub
+
+' ----------------------------------------------------------------
+' Assertion methods which determine test failure or skipping
+' ----------------------------------------------------------------
+
+' ----------------------------------------------------------------
+' Should be used to skip test cases. To skip test you must return the result of this method invocation.
+
+' @param message (string) Optional skip message.
+' Default value: "".
+
+' @return A skip message, with a specific prefix added, in order to runner know that this test should be skipped.
+' ----------------------------------------------------------------
+function BTS__Skip(message = "" as string) as string
+ ' add prefix so we know that this test is skipped, but not failed
+ return m.SKIP_TEST_MESSAGE_PREFIX + message
+end function
+
+' ----------------------------------------------------------------
+' Fail immediately, with the given message
+
+' @param msg (string) An error message.
+' Default value: "Error".
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__Fail(msg = "Error" as string) as string
+ return msg
+end function
+
+' ----------------------------------------------------------------
+' Fail the test if the expression is true.
+
+' @param expr (dynamic) An expression to evaluate.
+' @param msg (string) An error message.
+' Default value: "Expression evaluates to true"
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertFalse(expr as dynamic, msg = "Expression evaluates to true" as string) as string
+ if not TF_Utils__IsBoolean(expr) or expr
+ return BTS__Fail(msg)
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail the test unless the expression is true.
+
+' @param expr (dynamic) An expression to evaluate.
+' @param msg (string) An error message.
+' Default value: "Expression evaluates to false"
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertTrue(expr as dynamic, msg = "Expression evaluates to false" as string) as string
+ if not TF_Utils__IsBoolean(expr) or not expr then
+ return msg
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the two objects are unequal as determined by the '<>' operator.
+
+' @param first (dynamic) A first object to compare.
+' @param second (dynamic) A second object to compare.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertEqual(first as dynamic, second as dynamic, msg = "" as string) as string
+ if not TF_Utils__EqValues(first, second)
+ if msg = ""
+ first_as_string = TF_Utils__AsString(first)
+ second_as_string = TF_Utils__AsString(second)
+ msg = first_as_string + " != " + second_as_string
+ end if
+ return msg
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the two objects are equal as determined by the '=' operator.
+
+' @param first (dynamic) A first object to compare.
+' @param second (dynamic) A second object to compare.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertNotEqual(first as dynamic, second as dynamic, msg = "" as string) as string
+ if TF_Utils__EqValues(first, second)
+ if msg = ""
+ first_as_string = TF_Utils__AsString(first)
+ second_as_string = TF_Utils__AsString(second)
+ msg = first_as_string + " == " + second_as_string
+ end if
+ return msg
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the value is not invalid.
+
+' @param value (dynamic) A value to check.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertInvalid(value as dynamic, msg = "" as string) as string
+ if TF_Utils__IsValid(value)
+ if msg = ""
+ expr_as_string = TF_Utils__AsString(value)
+ msg = expr_as_string + " <> Invalid"
+ end if
+ return msg
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the value is invalid.
+
+' @param value (dynamic) A value to check.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertNotInvalid(value as dynamic, msg = "" as string) as string
+ if not TF_Utils__IsValid(value)
+ if msg = ""
+ if LCase(Type(value)) = "" then value = invalid
+ expr_as_string = TF_Utils__AsString(value)
+ msg = expr_as_string + " = Invalid"
+ end if
+ return msg
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array doesn't have the key.
+
+' @param array (dynamic) A target array.
+' @param key (string) A key name.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertAAHasKey(array as dynamic, key as dynamic, msg = "" as string) as string
+ if not TF_Utils__IsString(key)
+ return "Key value has invalid type."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array)
+ if not array.DoesExist(key)
+ if msg = ""
+ msg = "Array doesn't have the '" + key + "' key."
+ end if
+ return msg
+ end if
+ else
+ msg = "Input value is not an Associative Array."
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array has the key.
+
+' @param array (dynamic) A target array.
+' @param key (string) A key name.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertAANotHasKey(array as dynamic, key as dynamic, msg = "" as string) as string
+ if not TF_Utils__IsString(key)
+ return "Key value has invalid type."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array)
+ if array.DoesExist(key)
+ if msg = ""
+ msg = "Array has the '" + key + "' key."
+ end if
+ return msg
+ end if
+ else
+ msg = "Input value is not an Associative Array."
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array doesn't have the keys list.
+
+' @param array (dynamic) A target associative array.
+' @param keys (object) A key names array.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertAAHasKeys(array as dynamic, keys as object, msg = "" as string) as string
+ if not TF_Utils__IsAssociativeArray(array)
+ return "Input value is not an Associative Array."
+ end if
+
+ if not TF_Utils__IsArray(keys) or keys.Count() = 0
+ return "Keys value is not an Array or is empty."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array) and TF_Utils__IsArray(keys)
+ for each key in keys
+ if not TF_Utils__IsString(key)
+ return "Key value has invalid type."
+ end if
+
+ if not array.DoesExist(key)
+ if msg = ""
+ msg = "Array doesn't have the '" + key + "' key."
+ end if
+
+ return msg
+ end if
+ end for
+ else
+ msg = "Input value is not an Associative Array."
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array has the keys list.
+
+' @param array (dynamic) A target associative array.
+' @param keys (object) A key names array.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertAANotHasKeys(array as dynamic, keys as object, msg = "" as string) as string
+ if not TF_Utils__IsAssociativeArray(array)
+ return "Input value is not an Associative Array."
+ end if
+
+ if not TF_Utils__IsArray(keys) or keys.Count() = 0
+ return "Keys value is not an Array or is empty."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array) and TF_Utils__IsArray(keys)
+ for each key in keys
+ if not TF_Utils__IsString(key)
+ return "Key value has invalid type."
+ end if
+
+ if array.DoesExist(key)
+ if msg = ""
+ msg = "Array has the '" + key + "' key."
+ end if
+ return msg
+ end if
+ end for
+ else
+ msg = "Input value is not an Associative Array."
+ return msg
+ end if
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array doesn't have the item.
+
+' @param array (dynamic) A target array.
+' @param value (dynamic) A value to check.
+' @param key (object) A key name for associative array.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertArrayContains(array as dynamic, value as dynamic, key = invalid as dynamic, msg = "" as string) as string
+ if key <> invalid and not TF_Utils__IsString(key)
+ return "Key value has invalid type."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array) or TF_Utils__IsArray(array)
+ if not TF_Utils__ArrayContains(array, value, key)
+ msg = "Array doesn't have the '" + TF_Utils__AsString(value) + "' value."
+
+ return msg
+ end if
+ else
+ msg = "Input value is not an Array."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array has the item.
+
+' @param array (dynamic) A target array.
+' @param value (dynamic) A value to check.
+' @param key (object) A key name for associative array.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertArrayNotContains(array as dynamic, value as dynamic, key = invalid as dynamic, msg = "" as string) as string
+ if key <> invalid and not TF_Utils__IsString(key)
+ return "Key value has invalid type."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array) or TF_Utils__IsArray(array)
+ if TF_Utils__ArrayContains(array, value, key)
+ msg = "Array has the '" + TF_Utils__AsString(value) + "' value."
+
+ return msg
+ end if
+ else
+ msg = "Input value is not an Array."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array doesn't have the item subset.
+
+' @param array (dynamic) A target array.
+' @param subset (dynamic) An items array to check.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertArrayContainsSubset(array as dynamic, subset as dynamic, msg = "" as string) as string
+ if (TF_Utils__IsAssociativeArray(array) and TF_Utils__IsAssociativeArray(subset)) or (TF_Utils__IsArray(array) and TF_Utils__IsArray(subset))
+ isAA = TF_Utils__IsAssociativeArray(subset)
+ for each item in subset
+ key = invalid
+ value = item
+ if isAA
+ key = item
+ value = subset[key]
+ end if
+
+ if not TF_Utils__ArrayContains(array, value, key)
+ msg = "Array doesn't have the '" + TF_Utils__AsString(value) + "' value."
+
+ return msg
+ end if
+ end for
+ else
+ msg = "Input value is not an Array."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array have the item from subset.
+
+' @param array (dynamic) A target array.
+' @param subset (dynamic) A items array to check.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertArrayNotContainsSubset(array as dynamic, subset as dynamic, msg = "" as string) as string
+ if (TF_Utils__IsAssociativeArray(array) and TF_Utils__IsAssociativeArray(subset)) or (TF_Utils__IsArray(array) and TF_Utils__IsArray(subset))
+ isAA = TF_Utils__IsAssociativeArray(subset)
+ for each item in subset
+ key = invalid
+ value = item
+ if isAA
+ key = item
+ value = subset[key]
+ end if
+
+ if TF_Utils__ArrayContains(array, value, key)
+ msg = "Array has the '" + TF_Utils__AsString(value) + "' value."
+
+ return msg
+ end if
+ end for
+ else
+ msg = "Input value is not an Array."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array items count <> expected count
+
+' @param array (dynamic) A target array.
+' @param count (integer) An expected array items count.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertArrayCount(array as dynamic, count as dynamic, msg = "" as string) as string
+ if not TF_Utils__IsInteger(count)
+ return "Count value should be an integer."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array) or TF_Utils__IsArray(array)
+ if array.Count() <> count
+ msg = "Array items count <> " + TF_Utils__AsString(count) + "."
+
+ return msg
+ end if
+ else
+ msg = "Input value is not an Array."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the array items count = expected count.
+
+' @param array (dynamic) A target array.
+' @param count (integer) An expected array items count.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertArrayNotCount(array as dynamic, count as dynamic, msg = "" as string) as string
+ if not TF_Utils__IsInteger(count)
+ return "Count value should be an integer."
+ end if
+
+ if TF_Utils__IsAssociativeArray(array) or TF_Utils__IsArray(array)
+ if array.Count() = count
+ msg = "Array items count = " + TF_Utils__AsString(count) + "."
+
+ return msg
+ end if
+ else
+ msg = "Input value is not an Array."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the item is not empty array or string.
+
+' @param item (dynamic) An array or string to check.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertEmpty(item as dynamic, msg = "" as string) as string
+ if TF_Utils__IsAssociativeArray(item) or TF_Utils__IsArray(item)
+ if item.Count() > 0
+ msg = "Array is not empty."
+
+ return msg
+ end if
+ else if TF_Utils__IsString(item)
+ if Len(item) <> 0
+ msg = "Input value is not empty."
+
+ return msg
+ end if
+ else
+ msg = "Input value is not an Array, AssociativeArray or String."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+' ----------------------------------------------------------------
+' Fail if the item is empty array or string.
+
+' @param item (dynamic) An array or string to check.
+' @param msg (string) An error message.
+' Default value: ""
+
+' @return An error message.
+' ----------------------------------------------------------------
+function BTS__AssertNotEmpty(item as dynamic, msg = "" as string) as string
+ if TF_Utils__IsAssociativeArray(item) or TF_Utils__IsArray(item)
+ if item.Count() = 0
+ msg = "Array is empty."
+
+ return msg
+ end if
+ else if TF_Utils__IsString(item)
+ if Len(item) = 0
+ msg = "Input value is empty."
+
+ return msg
+ end if
+ else
+ msg = "Input value is not an Array, AssociativeArray or String."
+
+ return msg
+ end if
+
+ return ""
+end function
+
+'*****************************************************************
+'* Copyright Roku 2011-2019
+'* All Rights Reserved
+'*****************************************************************
+
+' Functions in this file:
+' ItemGenerator
+' IG_GetItem
+' IG_GetAssocArray
+' IG_GetArray
+' IG_GetSimpleType
+' IG_GetBoolean
+' IG_GetInteger
+' IG_GetFloat
+' IG_GetString
+
+' ----------------------------------------------------------------
+' Main function to generate object according to specified scheme.
+
+' @param scheme (object) A scheme with desired object structure. Can be
+' any simple type, array of types or associative array in form
+' { propertyName1 : "propertyType1"
+' propertyName2 : "propertyType2"
+' ...
+' propertyNameN : "propertyTypeN" }
+
+' @return An object according to specified scheme or invalid,
+' if scheme is not valid.
+' ----------------------------------------------------------------
+function ItemGenerator(scheme as object) as object
+ this = {}
+
+ this.getItem = IG_GetItem
+ this.getAssocArray = IG_GetAssocArray
+ this.getArray = IG_GetArray
+ this.getSimpleType = IG_GetSimpleType
+ this.getInteger = IG_GetInteger
+ this.getFloat = IG_GetFloat
+ this.getString = IG_GetString
+ this.getBoolean = IG_GetBoolean
+
+ if not TF_Utils__IsValid(scheme)
+ return invalid
+ end if
+
+ return this.getItem(scheme)
+end function
+
+' TODO: Create IG_GetInvalidItem function with random type fields
+
+' ----------------------------------------------------------------
+' Generate object according to specified scheme.
+
+' @param scheme (object) A scheme with desired object structure.
+' Can be any simple type, array of types or associative array.
+
+' @return An object according to specified scheme or invalid,
+' if scheme is not one of simple type, array or
+' associative array.
+' ----------------------------------------------------------------
+function IG_GetItem(scheme as object) as object
+ item = invalid
+
+ if TF_Utils__IsAssociativeArray(scheme)
+ item = IG_GetAssocArray(scheme)
+ else if TF_Utils__IsArray(scheme)
+ item = IG_GetArray(scheme)
+ else if TF_Utils__IsString(scheme)
+ item = IG_GetSimpleType(LCase(scheme))
+ end if
+
+ return item
+end function
+
+' ----------------------------------------------------------------
+' Generates associative array according to specified scheme.
+
+' @param scheme (object) An associative array with desired
+' object structure in form
+' { propertyName1 : "propertyType1"
+' propertyName2 : "propertyType2"
+' ...
+' propertyNameN : "propertyTypeN" }
+
+' @return An associative array according to specified scheme.
+' ----------------------------------------------------------------
+function IG_GetAssocArray(scheme as object) as object
+ item = {}
+
+ for each key in scheme
+ if not item.DoesExist(key)
+ item[key] = IG_GetItem(scheme[key])
+ end if
+ end for
+
+ return item
+end function
+
+' ----------------------------------------------------------------
+' Generates array according to specified scheme.
+
+' @param scheme (object) An array with desired object types.
+
+' @return An array according to specified scheme.
+' ----------------------------------------------------------------
+function IG_GetArray(scheme as object) as object
+ item = []
+
+ for each key in scheme
+ item.Push(IG_GetItem(key))
+ end for
+
+ return item
+end function
+
+' ----------------------------------------------------------------
+' Generates random value of specified type.
+
+' @param typeStr (string) A name of desired object type.
+
+' @return A simple type object or invalid if type is not supported.
+' ----------------------------------------------------------------
+function IG_GetSimpleType(typeStr as string) as object
+ item = invalid
+
+ if typeStr = "integer" or typeStr = "int" or typeStr = "roint"
+ item = IG_GetInteger()
+ else if typeStr = "float" or typeStr = "rofloat"
+ item = IG_GetFloat()
+ else if typeStr = "string" or typeStr = "rostring"
+ item = IG_GetString(10)
+ else if typeStr = "boolean" or typeStr = "roboolean"
+ item = IG_GetBoolean()
+ end if
+
+ return item
+end function
+
+' ----------------------------------------------------------------
+' Generates random boolean value.
+
+' @return A random boolean value.
+' ----------------------------------------------------------------
+function IG_GetBoolean() as boolean
+ return TF_Utils__AsBoolean(Rnd(2) \ Rnd(2))
+end function
+
+' ----------------------------------------------------------------
+' Generates random integer value from 1 to specified seed value.
+
+' @param seed (integer) A seed value for Rnd function.
+' Default value: 100.
+
+' @return A random integer value.
+' ----------------------------------------------------------------
+function IG_GetInteger(seed = 100 as integer) as integer
+ return Rnd(seed)
+end function
+
+' ----------------------------------------------------------------
+' Generates random float value.
+
+' @return A random float value.
+' ----------------------------------------------------------------
+function IG_GetFloat() as float
+ return Rnd(0)
+end function
+
+' ----------------------------------------------------------------
+' Generates random string with specified length.
+
+' @param seed (integer) A string length.
+
+' @return A random string value or empty string if seed is 0.
+' ----------------------------------------------------------------
+function IG_GetString(seed as integer) as string
+ item = ""
+ if seed > 0
+ stringLength = Rnd(seed)
+
+ for i = 0 to stringLength
+ chType = Rnd(3)
+
+ if chType = 1 ' Chr(48-57) - numbers
+ chNumber = 47 + Rnd(10)
+ else if chType = 2 ' Chr(65-90) - Uppercase Letters
+ chNumber = 64 + Rnd(26)
+ else ' Chr(97-122) - Lowercase Letters
+ chNumber = 96 + Rnd(26)
+ end if
+
+ item = item + Chr(chNumber)
+ end for
+ end if
+
+ return item
+end function
+'*****************************************************************
+'* Copyright Roku 2011-2019
+'* All Rights Reserved
+'*****************************************************************
+
+' Functions in this file:
+' Logger
+' Logger__SetVerbosity
+' Logger__SetEcho
+' Logger__SetServerURL
+' Logger__PrintStatistic
+' Logger__SendToServer
+' Logger__CreateTotalStatistic
+' Logger__CreateSuiteStatistic
+' Logger__CreateTestStatistic
+' Logger__AppendSuiteStatistic
+' Logger__AppendTestStatistic
+' Logger__PrintSuiteStatistic
+' Logger__PrintTestStatistic
+' Logger__PrintStart
+' Logger__PrintEnd
+' Logger__PrintSuiteSetUp
+' Logger__PrintSuiteStart
+' Logger__PrintSuiteEnd
+' Logger__PrintSuiteTearDown
+' Logger__PrintTestSetUp
+' Logger__PrintTestStart
+' Logger__PrintTestEnd
+' Logger__PrintTestTearDown
+
+' ----------------------------------------------------------------
+' Main function. Create Logger object.
+
+' @return A Logger object.
+' ----------------------------------------------------------------
+function Logger() as object
+ this = {}
+
+ this.verbosityLevel = {
+ basic: 0
+ normal: 1
+ verboseFailed: 2
+ verbose: 3
+ }
+
+ ' Internal properties
+ this.verbosity = this.verbosityLevel.normal
+ this.echoEnabled = false
+ this.serverURL = ""
+ this.jUnitEnabled = false
+
+ ' Interface
+ this.SetVerbosity = Logger__SetVerbosity
+ this.SetEcho = Logger__SetEcho
+ this.SetJUnit = Logger__SetJUnit
+ this.SetServer = Logger__SetServer
+ this.SetServerURL = Logger__SetServerURL ' Deprecated. Use Logger__SetServer instead.
+ this.PrintStatistic = Logger__PrintStatistic
+ this.SendToServer = Logger__SendToServer
+
+ this.CreateTotalStatistic = Logger__CreateTotalStatistic
+ this.CreateSuiteStatistic = Logger__CreateSuiteStatistic
+ this.CreateTestStatistic = Logger__CreateTestStatistic
+ this.AppendSuiteStatistic = Logger__AppendSuiteStatistic
+ this.AppendTestStatistic = Logger__AppendTestStatistic
+
+ ' Internal functions
+ this.PrintSuiteStatistic = Logger__PrintSuiteStatistic
+ this.PrintTestStatistic = Logger__PrintTestStatistic
+ this.PrintStart = Logger__PrintStart
+ this.PrintEnd = Logger__PrintEnd
+ this.PrintSuiteSetUp = Logger__PrintSuiteSetUp
+ this.PrintSuiteStart = Logger__PrintSuiteStart
+ this.PrintSuiteEnd = Logger__PrintSuiteEnd
+ this.PrintSuiteTearDown = Logger__PrintSuiteTearDown
+ this.PrintTestSetUp = Logger__PrintTestSetUp
+ this.PrintTestStart = Logger__PrintTestStart
+ this.PrintTestEnd = Logger__PrintTestEnd
+ this.PrintTestTearDown = Logger__PrintTestTearDown
+ this.PrintJUnitFormat = Logger__PrintJUnitFormat
+
+ return this
+end function
+
+' ----------------------------------------------------------------
+' Set logging verbosity parameter.
+
+' @param verbosity (integer) A verbosity level.
+' Posible values:
+' 0 - basic
+' 1 - normal
+' 2 - verbose failed tests
+' 3 - verbose
+' Default level: 1
+' ----------------------------------------------------------------
+sub Logger__SetVerbosity(verbosity = m.verbosityLevel.normal as integer)
+ if verbosity >= m.verbosityLevel.basic and verbosity <= m.verbosityLevel.verbose
+ m.verbosity = verbosity
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Set logging echo parameter.
+
+' @param enable (boolean) A echo trigger.
+' Posible values: true or false
+' Default value: false
+' ----------------------------------------------------------------
+sub Logger__SetEcho(enable = false as boolean)
+ m.echoEnabled = enable
+end sub
+
+' ----------------------------------------------------------------
+' Set logging JUnit output parameter.
+
+' @param enable (boolean) A JUnit output trigger.
+' Posible values: true or false
+' Default value: false
+' ----------------------------------------------------------------
+sub Logger__SetJUnit(enable = false as boolean)
+ m.jUnitEnabled = enable
+end sub
+
+' ----------------------------------------------------------------
+' Set storage server parameters.
+
+' @param url (string) Storage server host.
+' Default value: ""
+' @param port (string) Storage server port.
+' Default value: ""
+' ----------------------------------------------------------------
+sub Logger__SetServer(host = "" as string, port = "" as string)
+ if TF_Utils__IsNotEmptyString(host)
+ if TF_Utils__IsNotEmptyString(port)
+ m.serverURL = "http://" + host + ":" + port
+ else
+ m.serverURL = "http://" + host
+ end if
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Set storage server URL parameter.
+
+' @param url (string) A storage server URL.
+' Default value: ""
+' ----------------------------------------------------------------
+sub Logger__SetServerURL(url = "" as string)
+ ? "This function is deprecated. Please use Logger__SetServer(host, port)"
+end sub
+
+'----------------------------------------------------------------
+' Send test results as a POST json payload.
+'
+' @param statObj (object) stats of the test run.
+' Default value: invalid
+' ----------------------------------------------------------------
+sub Logger__SendToServer(statObj as object)
+ if TF_Utils__IsNotEmptyString(m.serverURL) and TF_Utils__IsValid(statObj)
+ ? "***"
+ ? "*** Sending statsObj to server: "; m.serverURL
+
+ request = CreateObject("roUrlTransfer")
+ request.SetUrl(m.serverURL)
+ statString = FormatJson(statObj)
+
+ ? "*** Response: "; request.postFromString(statString)
+ ? "***"
+ ? "******************************************************************"
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Print statistic object with specified verbosity.
+
+' @param statObj (object) A statistic object to print.
+' ----------------------------------------------------------------
+sub Logger__PrintStatistic(statObj as object)
+ if not m.echoEnabled
+ m.PrintStart()
+
+ if m.verbosity = m.verbosityLevel.normal or m.verbosity = m.verbosityLevel.verboseFailed
+ for each testSuite in statObj.Suites
+ for each testCase in testSuite.Tests
+ if m.verbosity = m.verbosityLevel.verboseFailed and testCase.result = "Fail"
+ m.printTestStatistic(testCase)
+ else
+ ? "*** "; testSuite.Name; ": "; testCase.Name; " - "; testCase.Result
+ end if
+ end for
+ end for
+ else if m.verbosity = m.verbosityLevel.verbose
+ for each testSuite in statObj.Suites
+ m.PrintSuiteStatistic(testSuite)
+ end for
+ end if
+ end if
+
+ ? "***"
+ ? "*** Total = "; TF_Utils__AsString(statObj.Total); " ; Passed = "; statObj.Correct; " ; Failed = "; statObj.Fail; " ; Skipped = "; statObj.skipped; " ; Crashes = "; statObj.Crash;
+ ? "*** Time spent: "; statObj.Time; "ms"
+ ? "***"
+
+ m.PrintEnd()
+
+ m.SendToServer(statObj)
+
+ if m.jUnitEnabled
+ m.printJUnitFormat(statObj)
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Create an empty statistic object for totals in output log.
+
+' @return An empty statistic object.
+' ----------------------------------------------------------------
+function Logger__CreateTotalStatistic() as object
+ statTotalItem = {
+ Suites: []
+ Time: 0
+ Total: 0
+ Correct: 0
+ Fail: 0
+ Skipped: 0
+ Crash: 0
+ }
+
+ if m.echoEnabled
+ m.PrintStart()
+ end if
+
+ return statTotalItem
+end function
+
+' ----------------------------------------------------------------
+' Create an empty statistic object for test suite with specified name.
+
+' @param name (string) A test suite name for statistic object.
+
+' @return An empty statistic object for test suite.
+' ----------------------------------------------------------------
+function Logger__CreateSuiteStatistic(name as string) as object
+ statSuiteItem = {
+ Name: name
+ Tests: []
+ Time: 0
+ Total: 0
+ Correct: 0
+ Fail: 0
+ Skipped: 0
+ Crash: 0
+ }
+
+ if m.echoEnabled
+ if m.verbosity = m.verbosityLevel.verbose
+ m.PrintSuiteStart(name)
+ end if
+ end if
+
+ return statSuiteItem
+end function
+
+' ----------------------------------------------------------------
+' Create statistic object for test with specified name.
+
+' @param name (string) A test name.
+' @param result (string) A result of test running.
+' Posible values: "Success", "Fail".
+' Default value: "Success"
+' @param time (integer) A test running time.
+' Default value: 0
+' @param errorCode (integer) An error code for failed test.
+' Posible values:
+' 252 (&hFC) : ERR_NORMAL_END
+' 226 (&hE2) : ERR_VALUE_RETURN
+' 233 (&hE9) : ERR_USE_OF_UNINIT_VAR
+' 020 (&h14) : ERR_DIV_ZERO
+' 024 (&h18) : ERR_TM
+' 244 (&hF4) : ERR_RO2
+' 236 (&hEC) : ERR_RO4
+' 002 (&h02) : ERR_SYNTAX
+' 241 (&hF1) : ERR_WRONG_NUM_PARAM
+' Default value: 0
+' @param errorMessage (string) An error message for failed test.
+
+' @return A statistic object for test.
+' ----------------------------------------------------------------
+function Logger__CreateTestStatistic(name as string, result = "Success" as string, time = 0 as integer, errorCode = 0 as integer, errorMessage = "" as string, isInit = false as boolean) as object
+ statTestItem = {
+ Name: name
+ Result: result
+ Time: time
+ PerfData: {}
+ Error: {
+ Code: errorCode
+ Message: errorMessage
+ }
+ }
+
+ if m.echoEnabled and not isInit
+ if m.verbosity = m.verbosityLevel.verbose
+ m.PrintTestStart(name)
+ end if
+ end if
+
+ return statTestItem
+end function
+
+' ----------------------------------------------------------------
+' Append test statistic to test suite statistic.
+
+' @param statSuiteObj (object) A target test suite object.
+' @param statTestObj (object) A test statistic to append.
+' ----------------------------------------------------------------
+sub Logger__AppendTestStatistic(statSuiteObj as object, statTestObj as object)
+ if TF_Utils__IsAssociativeArray(statSuiteObj) and TF_Utils__IsAssociativeArray(statTestObj)
+ statSuiteObj.Tests.Push(statTestObj)
+
+ if TF_Utils__IsInteger(statTestObj.time)
+ statSuiteObj.Time = statSuiteObj.Time + statTestObj.Time
+ end if
+
+ statSuiteObj.Total = statSuiteObj.Total + 1
+
+ if LCase(statTestObj.Result) = "success"
+ statSuiteObj.Correct = statSuiteObj.Correct + 1
+ else if LCase(statTestObj.result) = "fail"
+ statSuiteObj.Fail = statSuiteObj.Fail + 1
+ else if LCase(statTestObj.result) = "skipped"
+ statSuiteObj.skipped++
+ else
+ statSuiteObj.crash = statSuiteObj.crash + 1
+ end if
+
+ if m.echoEnabled
+ if m.verbosity = m.verbosityLevel.normal
+ ? "*** "; statSuiteObj.Name; ": "; statTestObj.Name; " - "; statTestObj.Result
+ else if m.verbosity = m.verbosityLevel.verbose
+ m.PrintTestStatistic(statTestObj)
+ end if
+ end if
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Append suite statistic to total statistic object.
+
+' @param statTotalObj (object) A target total statistic object.
+' @param statSuiteObj (object) A test suite statistic object to append.
+' ----------------------------------------------------------------
+sub Logger__AppendSuiteStatistic(statTotalObj as object, statSuiteObj as object)
+ if TF_Utils__IsAssociativeArray(statTotalObj) and TF_Utils__IsAssociativeArray(statSuiteObj)
+ statTotalObj.Suites.Push(statSuiteObj)
+ statTotalObj.Time = statTotalObj.Time + statSuiteObj.Time
+
+ if TF_Utils__IsInteger(statSuiteObj.Total)
+ statTotalObj.Total = statTotalObj.Total + statSuiteObj.Total
+ end if
+
+ if TF_Utils__IsInteger(statSuiteObj.Correct)
+ statTotalObj.Correct = statTotalObj.Correct + statSuiteObj.Correct
+ end if
+
+ if TF_Utils__IsInteger(statSuiteObj.Fail)
+ statTotalObj.Fail = statTotalObj.Fail + statSuiteObj.Fail
+ end if
+
+ if TF_Utils__IsInteger(statSuiteObj.skipped)
+ statTotalObj.skipped += statSuiteObj.skipped
+ end if
+
+ if TF_Utils__IsInteger(statSuiteObj.Crash)
+ statTotalObj.Crash = statTotalObj.Crash + statSuiteObj.Crash
+ end if
+
+ if m.echoEnabled
+ if m.verbosity = m.verbosityLevel.verbose
+ m.PrintSuiteStatistic(statSuiteObj)
+ end if
+ end if
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Print test suite statistic.
+
+' @param statSuiteObj (object) A target test suite object to print.
+' ----------------------------------------------------------------
+sub Logger__PrintSuiteStatistic(statSuiteObj as object)
+ if not m.echoEnabled
+ m.PrintSuiteStart(statSuiteObj.Name)
+
+ for each testCase in statSuiteObj.Tests
+ m.PrintTestStatistic(testCase)
+ end for
+ end if
+
+ ? "==="
+ ? "=== Total = "; TF_Utils__AsString(statSuiteObj.Total); " ; Passed = "; statSuiteObj.Correct; " ; Failed = "; statSuiteObj.Fail; " ; Skipped = "; statSuiteObj.skipped; " ; Crashes = "; statSuiteObj.Crash;
+ ? " Time spent: "; statSuiteObj.Time; "ms"
+ ? "==="
+
+ m.PrintSuiteEnd(statSuiteObj.Name)
+end sub
+
+' ----------------------------------------------------------------
+' Print test statistic.
+
+' @param statTestObj (object) A target test object to print.
+' ----------------------------------------------------------------
+sub Logger__PrintTestStatistic(statTestObj as object)
+ if not m.echoEnabled
+ m.PrintTestStart(statTestObj.Name)
+ end if
+
+ ? "--- Result: "; statTestObj.Result
+ ? "--- Time: "; statTestObj.Time
+
+ if LCase(statTestObj.result) = "skipped"
+ if Len(statTestObj.message) > 0
+ ? "--- Message: "; statTestObj.message
+ end if
+ else if LCase(statTestObj.Result) <> "success"
+ ? "--- Error Code: "; statTestObj.Error.Code
+ ? "--- Error Message: "; statTestObj.Error.Message
+ end if
+
+ m.PrintTestEnd(statTestObj.Name)
+end sub
+
+' ----------------------------------------------------------------
+' Print testting start message.
+' ----------------------------------------------------------------
+sub Logger__PrintStart()
+ ? ""
+ ? "******************************************************************"
+ ? "******************************************************************"
+ ? "************* Start testing *************"
+ ? "******************************************************************"
+end sub
+
+' ----------------------------------------------------------------
+' Print testing end message.
+' ----------------------------------------------------------------
+sub Logger__PrintEnd()
+ ? "******************************************************************"
+ ? "************* End testing *************"
+ ? "******************************************************************"
+ ? "******************************************************************"
+ ? ""
+end sub
+
+' ----------------------------------------------------------------
+' Print test suite SetUp message.
+' ----------------------------------------------------------------
+sub Logger__PrintSuiteSetUp(sName as string)
+ if m.verbosity = m.verbosityLevel.verbose
+ ? "================================================================="
+ ? "=== SetUp "; sName; " suite."
+ ? "================================================================="
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Print test suite start message.
+' ----------------------------------------------------------------
+sub Logger__PrintSuiteStart(sName as string)
+ ? "================================================================="
+ ? "=== Start "; sName; " suite:"
+ ? "==="
+end sub
+
+' ----------------------------------------------------------------
+' Print test suite end message.
+' ----------------------------------------------------------------
+sub Logger__PrintSuiteEnd(sName as string)
+ ? "==="
+ ? "=== End "; sName; " suite."
+ ? "================================================================="
+end sub
+
+' ----------------------------------------------------------------
+' Print test suite TearDown message.
+' ----------------------------------------------------------------
+sub Logger__PrintSuiteTearDown(sName as string)
+ if m.verbosity = m.verbosityLevel.verbose
+ ? "================================================================="
+ ? "=== TearDown "; sName; " suite."
+ ? "================================================================="
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Print test setUp message.
+' ----------------------------------------------------------------
+sub Logger__PrintTestSetUp(tName as string)
+ if m.verbosity = m.verbosityLevel.verbose
+ ? "----------------------------------------------------------------"
+ ? "--- SetUp "; tName; " test."
+ ? "----------------------------------------------------------------"
+ end if
+end sub
+
+' ----------------------------------------------------------------
+' Print test start message.
+' ----------------------------------------------------------------
+sub Logger__PrintTestStart(tName as string)
+ ? "----------------------------------------------------------------"
+ ? "--- Start "; tName; " test:"
+ ? "---"
+end sub
+
+' ----------------------------------------------------------------
+' Print test end message.
+' ----------------------------------------------------------------
+sub Logger__PrintTestEnd(tName as string)
+ ? "---"
+ ? "--- End "; tName; " test."
+ ? "----------------------------------------------------------------"
+end sub
+
+' ----------------------------------------------------------------
+' Print test TearDown message.
+' ----------------------------------------------------------------
+sub Logger__PrintTestTearDown(tName as string)
+ if m.verbosity = m.verbosityLevel.verbose
+ ? "----------------------------------------------------------------"
+ ? "--- TearDown "; tName; " test."
+ ? "----------------------------------------------------------------"
+ end if
+end sub
+
+sub Logger__PrintJUnitFormat(statObj as object)
+ ' TODO finish report
+ xml = CreateObject("roXMLElement")
+ xml.SetName("testsuites")
+ for each testSuiteAA in statObj.suites
+ testSuite = xml.AddElement("testsuite")
+ ' name="FeatureManagerTest" time="13.923" tests="2" errors="0" skipped="0" failures="0"
+ testSuite.AddAttribute("name", testSuiteAA.name)
+ testSuite.AddAttribute("time", testSuiteAA.time.toStr())
+ testSuite.AddAttribute("tests", testSuiteAA.Tests.count().toStr())
+
+ skippedNum = 0
+ failedNum = 0
+ for each testAA in testSuiteAA.Tests
+ test = testSuite.AddElement("testcase")
+ test.AddAttribute("name", testAA.name)
+ test.AddAttribute("time", testAA.time.toStr())
+
+ if LCase(testAA.result) = "skipped" then
+ test.AddElement("skipped")
+ skippedNum++
+ else if LCase(testAA.Result) <> "success"
+ failure = test.AddElement("failure")
+ failure.AddAttribute("message", testAA.error.message)
+ failure.AddAttribute("type", testAA.error.code.tostr())
+ failedNum++
+ end if
+ end for
+ testSuite.AddAttribute("errors", failedNum.tostr())
+ testSuite.AddAttribute("skipped", skippedNum.tostr())
+ end for
+ ? xml.GenXML(true)
+end sub
+'*****************************************************************
+'* Copyright Roku 2011-2019
+'* All Rights Reserved
+'*****************************************************************
+
+' Functions in this file:
+' TestRunner
+' TestRunner__Run
+' TestRunner__SetTestsDirectory
+' TestRunner__SetTestFilePrefix
+' TestRunner__SetTestSuitePrefix
+' TestRunner__SetTestSuiteName
+' TestRunner__SetTestCaseName
+' TestRunner__SetFailFast
+' TestRunner__GetTestSuitesList
+' TestRunner__GetTestSuiteNamesList
+' TestRunner__GetTestFilesList
+' TestRunner__GetTestNodesList
+' TestFramework__RunNodeTests
+
+' ----------------------------------------------------------------
+' Main function. Create TestRunner object.
+
+' @return A TestRunner object.
+' ----------------------------------------------------------------
+function TestRunner() as object
+ this = {}
+ GetGlobalAA().globalErrorsList = []
+ this.isNodeMode = GetGlobalAA().top <> invalid
+ this.Logger = Logger()
+
+ ' Internal properties
+ this.SKIP_TEST_MESSAGE_PREFIX = "SKIP_TEST_MESSAGE_PREFIX__"
+ this.nodesTestDirectory = "pkg:/components/tests"
+ if this.isNodeMode
+ this.testsDirectory = this.nodesTestDirectory
+ this.testFilePrefix = m.top.subtype()
+ else
+ this.testsDirectory = "pkg:/source/tests"
+ this.testFilePrefix = "Test__"
+ end if
+ this.testSuitePrefix = "TestSuite__"
+ this.testSuiteName = ""
+ this.testCaseName = ""
+ this.failFast = false
+
+ ' Interface
+ this.Run = TestRunner__Run
+ this.SetTestsDirectory = TestRunner__SetTestsDirectory
+ this.SetTestFilePrefix = TestRunner__SetTestFilePrefix
+ this.SetTestSuitePrefix = TestRunner__SetTestSuitePrefix
+ this.SetTestSuiteName = TestRunner__SetTestSuiteName ' Obsolete, will be removed in next versions
+ this.SetTestCaseName = TestRunner__SetTestCaseName ' Obsolete, will be removed in next versions
+ this.SetFailFast = TestRunner__SetFailFast
+ this.SetFunctions = TestRunner__SetFunctions
+ this.SetIncludeFilter = TestRunner__SetIncludeFilter
+ this.SetExcludeFilter = TestRunner__SetExcludeFilter
+
+ ' Internal functions
+ this.GetTestFilesList = TestRunner__GetTestFilesList
+ this.GetTestSuitesList = TestRunner__GetTestSuitesList
+ this.GetTestNodesList = TestRunner__GetTestNodesList
+ this.GetTestSuiteNamesList = TestRunner__GetTestSuiteNamesList
+ this.GetIncludeFilter = TestRunner__GetIncludeFilter
+ this.GetExcludeFilter = TestRunner__GetExcludeFilter
+
+ return this
+end function
+
+' ----------------------------------------------------------------
+' Run main test loop.
+
+' @param statObj (object, optional) statistic object to be used in tests
+' @param testSuiteNamesList (array, optional) array of test suite function names to be used in tests
+
+' @return Statistic object if run in node mode, invalid otherwise
+' ----------------------------------------------------------------
+function TestRunner__Run(statObj = m.Logger.CreateTotalStatistic() as object, testSuiteNamesList = [] as object) as object
+ alltestCount = 0
+ totalStatObj = statObj
+ testSuitesList = m.GetTestSuitesList(testSuiteNamesList)
+
+ globalErrorsList = GetGlobalAA().globalErrorsList
+ for each testSuite in testSuitesList
+ testCases = testSuite.testCases
+ testCount = testCases.Count()
+ alltestCount = alltestCount + testCount
+
+ IS_NEW_APPROACH = testSuite.IS_NEW_APPROACH
+ ' create dedicated env for each test, so that they will have not global m and don't rely on m.that is set in another suite
+ env = {}
+
+ if TF_Utils__IsFunction(testSuite.SetUp)
+ m.Logger.PrintSuiteSetUp(testSuite.Name)
+ if IS_NEW_APPROACH then
+ env.functionToCall = testSuite.SetUp
+ env.functionToCall()
+ else
+ testSuite.SetUp()
+ end if
+ end if
+
+ suiteStatObj = m.Logger.CreateSuiteStatistic(testSuite.Name)
+ ' Initiate empty test statistics object to print results if no tests was run
+ testStatObj = m.Logger.CreateTestStatistic("", "Success", 0, 0, "", true)
+ for each testCase in testCases
+ ' clear all existing errors
+ globalErrorsList.clear()
+
+ if m.testCaseName = "" or (m.testCaseName <> "" and LCase(testCase.Name) = LCase(m.testCaseName))
+ skipTest = TF_Utils__AsBoolean(testCase.skip)
+
+ if TF_Utils__IsFunction(testCase.SetUp) and not skipTest
+ m.Logger.PrintTestSetUp(testCase.Name)
+ if IS_NEW_APPROACH then
+ env.functionToCall = testCase.SetUp
+ env.functionToCall()
+ else
+ testCase.SetUp()
+ end if
+ end if
+
+ testTimer = CreateObject("roTimespan")
+ testStatObj = m.Logger.CreateTestStatistic(testCase.Name)
+
+ if skipTest
+ runResult = m.SKIP_TEST_MESSAGE_PREFIX + "Test was skipped according to specified filters"
+ else
+ testSuite.testInstance = testCase
+ testSuite.testCase = testCase.Func
+
+ runResult = ""
+ if IS_NEW_APPROACH then
+ env.functionToCall = testCase.Func
+
+ if GetInterface(env.functionToCall, "ifFunction") <> invalid
+ if testCase.hasArguments then
+ env.functionToCall(testCase.arg)
+ else
+ env.functionToCall()
+ end if
+ else
+ UTF_fail("Failed to execute test """ + testCase.Name + """ function pointer not found")
+ end if
+ else
+ runResult = testSuite.testCase()
+ end if
+ end if
+
+ if TF_Utils__IsFunction(testCase.TearDown) and not skipTest
+ m.Logger.PrintTestTearDown(testCase.Name)
+ if IS_NEW_APPROACH then
+ env.functionToCall = testCase.TearDown
+ env.functionToCall()
+ else
+ testCase.TearDown()
+ end if
+ end if
+
+ if IS_NEW_APPROACH then
+ if globalErrorsList.count() > 0
+ for each error in globalErrorsList
+ runResult += error + Chr(10) + string(10, "-") + Chr(10)
+ end for
+ end if
+ end if
+
+ if runResult <> ""
+ if InStr(0, runResult, m.SKIP_TEST_MESSAGE_PREFIX) = 1
+ testStatObj.result = "Skipped"
+ testStatObj.message = runResult.Mid(Len(m.SKIP_TEST_MESSAGE_PREFIX)) ' remove prefix from the message
+ else
+ testStatObj.Result = "Fail"
+ testStatObj.Error.Code = 1
+ testStatObj.Error.Message = runResult
+ end if
+ else
+ testStatObj.Result = "Success"
+ end if
+
+ testStatObj.Time = testTimer.TotalMilliseconds()
+ m.Logger.AppendTestStatistic(suiteStatObj, testStatObj)
+
+ if testStatObj.Result = "Fail" and m.failFast
+ suiteStatObj.Result = "Fail"
+ exit for
+ end if
+ end if
+ end for
+
+ m.Logger.AppendSuiteStatistic(totalStatObj, suiteStatObj)
+
+ if TF_Utils__IsFunction(testSuite.TearDown)
+ m.Logger.PrintSuiteTearDown(testSuite.Name)
+ testSuite.TearDown()
+ end if
+
+ if suiteStatObj.Result = "Fail" and m.failFast
+ exit for
+ end if
+ end for
+
+ gthis = GetGlobalAA()
+ msg = ""
+ if gthis.notFoundFunctionPointerList <> invalid then
+ msg = Chr(10) + string(40, "---") + Chr(10)
+ if m.isNodeMode
+ fileNamesString = ""
+
+ for each testSuiteObject in testSuiteNamesList
+ if GetInterface(testSuiteObject, "ifString") <> invalid then
+ fileNamesString += testSuiteObject + ".brs, "
+ else if GetInterface(testSuiteObject, "ifAssociativeArray") <> invalid then
+ if testSuiteObject.filePath <> invalid then
+ fileNamesString += testSuiteObject.filePath + ", "
+ end if
+ end if
+ end for
+
+ msg += Chr(10) + "Create this function below in one of these files"
+ msg += Chr(10) + fileNamesString + Chr(10)
+
+ msg += Chr(10) + "sub init()"
+ end if
+ msg += Chr(10) + "Runner.SetFunctions([" + Chr(10) + " testCase" + Chr(10) + "])"
+ msg += Chr(10) + "For example we think this might resolve your issue"
+ msg += Chr(10) + "Runner = TestRunner()"
+ msg += Chr(10) + "Runner.SetFunctions(["
+
+ tmpMap = {}
+ for each functionName in gthis.notFoundFunctionPointerList
+ if tmpMap[functionName] = invalid then
+ tmpMap[functionName] = ""
+ msg += Chr(10) + " " + functionName
+ end if
+ end for
+
+ msg += Chr(10) + "])"
+ if m.isNodeMode then
+ msg += Chr(10) + "end sub"
+ else
+ msg += Chr(10) + "Runner.Run()"
+ end if
+ end if
+
+ if m.isNodeMode
+ if msg.Len() > 0 then
+ if totalStatObj.notFoundFunctionsMessage = invalid then totalStatObj.notFoundFunctionsMessage = ""
+ totalStatObj.notFoundFunctionsMessage += msg
+ end if
+ return totalStatObj
+ else
+ testNodes = m.getTestNodesList()
+ for each testNodeName in testNodes
+ testNode = CreateObject("roSGNode", testNodeName)
+ if testNode <> invalid
+ testSuiteNamesList = m.GetTestSuiteNamesList(testNodeName)
+ if CreateObject("roSGScreen").CreateScene(testNodeName) <> invalid
+ ? "WARNING: Test cases cannot be run in main scene."
+ for each testSuiteName in testSuiteNamesList
+ suiteStatObj = m.Logger.CreateSuiteStatistic(testSuiteName)
+ suiteStatObj.fail = 1
+ suiteStatObj.total = 1
+ m.Logger.AppendSuiteStatistic(totalStatObj, suiteStatObj)
+ end for
+ else
+ params = [m, totalStatObj, testSuiteNamesList, m.GetIncludeFilter(), m.GetExcludeFilter()]
+ tmp = testNode.callFunc("TestFramework__RunNodeTests", params)
+ if tmp <> invalid then
+ totalStatObj = tmp
+ end if
+ end if
+ end if
+ end for
+
+ m.Logger.PrintStatistic(totalStatObj)
+ end if
+
+ if msg.Len() > 0 or totalStatObj.notFoundFunctionsMessage <> invalid then
+ title = ""
+ title += Chr(10) + "NOTE: If some your tests haven't been executed this might be due to outdated list of functions"
+ title += Chr(10) + "To resolve this issue please execute" + Chr(10) + Chr(10)
+
+ title += msg
+
+ if totalStatObj.notFoundFunctionsMessage <> invalid then
+ title += totalStatObj.notFoundFunctionsMessage
+ end if
+ ? title
+ end if
+end function
+
+' ----------------------------------------------------------------
+' Set testsDirectory property.
+' ----------------------------------------------------------------
+sub TestRunner__SetTestsDirectory(testsDirectory as string)
+ m.testsDirectory = testsDirectory
+end sub
+
+' ----------------------------------------------------------------
+' Set testFilePrefix property.
+' ----------------------------------------------------------------
+sub TestRunner__SetTestFilePrefix(testFilePrefix as string)
+ m.testFilePrefix = testFilePrefix
+end sub
+
+' ----------------------------------------------------------------
+' Set testSuitePrefix property.
+' ----------------------------------------------------------------
+sub TestRunner__SetTestSuitePrefix(testSuitePrefix as string)
+ m.testSuitePrefix = testSuitePrefix
+end sub
+
+' ----------------------------------------------------------------
+' Set testSuiteName property.
+' ----------------------------------------------------------------
+sub TestRunner__SetTestSuiteName(testSuiteName as string)
+ m.testSuiteName = testSuiteName
+end sub
+
+' ----------------------------------------------------------------
+' Set testCaseName property.
+' ----------------------------------------------------------------
+sub TestRunner__SetTestCaseName(testCaseName as string)
+ m.testCaseName = testCaseName
+end sub
+
+' ----------------------------------------------------------------
+' Set failFast property.
+' ----------------------------------------------------------------
+sub TestRunner__SetFailFast(failFast = false as boolean)
+ m.failFast = failFast
+end sub
+
+' ----------------------------------------------------------------
+' Builds an array of test suite objects.
+
+' @param testSuiteNamesList (string, optional) array of names of test suite functions. If not passed, scans all test files for test suites
+
+' @return An array of test suites.
+' ----------------------------------------------------------------
+function TestRunner__GetTestSuitesList(testSuiteNamesList = [] as object) as object
+ result = []
+
+ if testSuiteNamesList.count() > 0
+ for each value in testSuiteNamesList
+ if TF_Utils__IsString(value) then
+ tmpTestSuiteFunction = TestFramework__getFunctionPointer(value)
+ if tmpTestSuiteFunction <> invalid then
+ testSuite = tmpTestSuiteFunction()
+
+ if TF_Utils__IsAssociativeArray(testSuite)
+ result.Push(testSuite)
+ end if
+ end if
+ ' also we can get AA that will give source code and filePath
+ ' Please be aware this is executed in render thread
+ else if GetInterface(value, "ifAssociativeArray") <> invalid then
+ ' try to use new approach
+ testSuite = ScanFileForNewTests(value.code, value.filePath)
+ if testSuite <> invalid then
+ result.push(testSuite)
+ end if
+ else if GetInterface(value, "ifFunction") <> invalid then
+ result.Push(value)
+ end if
+ end for
+ else
+ testSuiteRegex = CreateObject("roRegex", "^(function|sub)\s(" + m.testSuitePrefix + m.testSuiteName + "[0-9a-z\_]*)\s*\(", "i")
+ testFilesList = m.GetTestFilesList()
+
+ for each filePath in testFilesList
+ code = TF_Utils__AsString(ReadAsciiFile(filePath))
+
+ if code <> ""
+ foundTestSuite = false
+ for each line in code.Tokenize(Chr(10))
+ line.Trim()
+
+ if testSuiteRegex.IsMatch(line)
+ testSuite = invalid
+ functionName = testSuiteRegex.Match(line).Peek()
+
+ tmpTestSuiteFunction = TestFramework__getFunctionPointer(functionName)
+ if tmpTestSuiteFunction <> invalid then
+ testSuite = tmpTestSuiteFunction()
+
+ if TF_Utils__IsAssociativeArray(testSuite)
+ result.Push(testSuite)
+ foundTestSuite = true
+ else
+ ' TODO check if we need this
+ ' using new mode
+ ' testSuite = ScanFileForNewTests(code, filePath)
+
+ ' exit for
+ end if
+ end if
+ end if
+ end for
+ if not foundTestSuite then
+ testSuite = ScanFileForNewTests(code, filePath)
+ if testSuite <> invalid then
+ result.push(testSuite)
+ end if
+ end if
+ end if
+ end for
+ end if
+
+ return result
+end function
+
+function ScanFileForNewTests(souceCode, filePath)
+ foundAnyTest = false
+ testSuite = BaseTestSuite()
+
+ allowedAnnotationsRegex = CreateObject("roRegex", "^'\s*@(test|beforeall|beforeeach|afterall|aftereach|repeatedtest|parameterizedtest|methodsource|ignore)\s*|\n", "i")
+ voidFunctionRegex = CreateObject("roRegex", "^(function|sub)\s([a-z0-9A-Z_]*)\(\)", "i")
+ anyArgsFunctionRegex = CreateObject("roRegex", "^(function|sub)\s([a-z0-9A-Z_]*)\(", "i")
+
+ processors = {
+ testSuite: testSuite
+ filePath: filePath
+ currentLine: ""
+ annotations: {}
+
+ functionName: ""
+
+ tests: []
+
+ beforeEachFunc: invalid
+ beforeAllFunc: invalid
+
+ AfterEachFunc: invalid
+ AfterAllFunc: invalid
+
+ isParameterizedTest: false
+ MethodForArguments: ""
+ executedParametrizedAdding: false
+
+ test: sub()
+ skipTest = m.doSkipTest(m.functionName)
+ funcPointer = m.getFunctionPointer(m.functionName)
+ m.tests.push({ name: m.functionName, pointer: funcPointer, skip: skipTest })
+ end sub
+
+ repeatedtest: sub()
+ allowedAnnotationsRegex = CreateObject("roRegex", "^'\s*@(repeatedtest)\((\d*)\)", "i")
+ annotationLine = m.annotations["repeatedtest"].line
+ if allowedAnnotationsRegex.IsMatch(annotationLine)
+ groups = allowedAnnotationsRegex.Match(annotationLine)
+ numberOfLoops = groups[2]
+ if numberOfLoops <> invalid and TF_Utils__AsInteger(numberOfLoops) > 0 then
+ numberOfLoops = TF_Utils__AsInteger(numberOfLoops)
+ funcPointer = m.getFunctionPointer(m.functionName)
+ for index = 1 to numberOfLoops
+ skipTest = m.doSkipTest(m.functionName)
+ text = " " + index.tostr() + " of " + numberOfLoops.tostr()
+ m.tests.push({ name: m.functionName + text, pointer: funcPointer, skip: skipTest })
+ end for
+ end if
+ else
+ ? "WARNING: Wrong format of repeatedTest(numberOfRuns) "annotationLine
+ end if
+ end sub
+
+ parameterizedTest: sub()
+ m.processParameterizedTests()
+ end sub
+
+ methodSource: sub()
+ m.processParameterizedTests()
+ end sub
+
+ processParameterizedTests: sub()
+ ' add test if it was not added already
+ if not m.executedParametrizedAdding
+ if m.annotations.methodSource <> invalid and m.annotations.parameterizedTest <> invalid then
+ methodAnottation = m.annotations.methodSource.line
+
+ allowedAnnotationsRegex = CreateObject("roRegex", "^'\s*@(methodsource)\(" + Chr(34) + "([A-Za-z0-9_]*)" + Chr(34) + "\)", "i")
+
+ if allowedAnnotationsRegex.IsMatch(methodAnottation)
+ groups = allowedAnnotationsRegex.Match(methodAnottation)
+ providerFunction = groups[2]
+
+ providerFunctionPointer = m.getFunctionPointer(providerFunction)
+
+ if providerFunctionPointer <> invalid then
+ funcPointer = m.getFunctionPointer(m.functionName)
+
+ args = providerFunctionPointer()
+
+ index = 1
+ for each arg in args
+ skipTest = m.doSkipTest(m.functionName)
+ text = " " + index.tostr() + " of " + args.count().tostr()
+ m.tests.push({ name: m.functionName + text, pointer: funcPointer, arg: arg, hasArgs: true, skip: skipTest })
+ index++
+ end for
+ else
+ ? "WARNING: Cannot find function [" providerFunction "]"
+ end if
+ end if
+ else
+ ? "WARNING: Wrong format of @ParameterizedTest \n @MethodSource(providerFunctionName)"
+ ? "m.executedParametrizedAdding = "m.executedParametrizedAdding
+ ? "m.annotations.methodSource = "m.annotations.methodSource
+ ? "m.annotations.parameterizedTest = "m.annotations.parameterizedTest
+ ? ""
+ end if
+ end if
+ end sub
+
+ beforeEach: sub()
+ m.beforeEachFunc = m.getFunctionPointer(m.functionName)
+ end sub
+
+ beforeAll: sub()
+ m.beforeAllFunc = m.getFunctionPointer(m.functionName)
+ end sub
+
+ AfterEach: sub()
+ m.AfterEachFunc = m.getFunctionPointer(m.functionName)
+ end sub
+
+ AfterAll: sub()
+ m.AfterAllFunc = m.getFunctionPointer(m.functionName)
+ end sub
+
+ ignore: sub()
+ funcPointer = m.getFunctionPointer(m.functionName)
+ m.tests.push({ name: m.functionName, pointer: funcPointer, skip: true })
+ end sub
+
+ doSkipTest: function(name as string)
+ includeFilter = []
+ excludeFilter = []
+
+ gthis = GetGlobalAA()
+ if gthis.IncludeFilter <> invalid then includeFilter.append(gthis.IncludeFilter)
+ if gthis.ExcludeFilter <> invalid then excludeFilter.append(gthis.ExcludeFilter)
+
+ ' apply test filters
+ skipTest = false
+ ' skip test if it is found in exclude filter
+ for each testName in excludeFilter
+ if TF_Utils__IsNotEmptyString(testName) and LCase(testName.Trim()) = LCase(name.Trim())
+ skipTest = true
+ exit for
+ end if
+ end for
+
+ ' skip test if it is not found in include filter
+ if not skipTest and includeFilter.Count() > 0
+ foundInIncludeFilter = false
+
+ for each testName in includeFilter
+ if TF_Utils__IsNotEmptyString(testName) and LCase(testName) = LCase(name)
+ foundInIncludeFilter = true
+ exit for
+ end if
+ end for
+
+ skipTest = not foundInIncludeFilter
+ end if
+
+ return skipTest
+ end function
+
+ buildTests: sub()
+ testSuite = m.testSuite
+ testSuite.Name = m.filePath
+ if m.beforeAllFunc <> invalid then testSuite.SetUp = m.beforeAllFunc
+ if m.AfterAllFunc <> invalid then testSuite.TearDown = m.AfterAllFunc
+ testSuite.IS_NEW_APPROACH = true
+
+ for each test in m.tests
+ ' Add tests to suite's tests collection
+ arg = invalid
+ hasArgs = false
+ if test.hasArgs <> invalid then
+ arg = test.arg
+ hasArgs = true
+ end if
+
+ testSuite.addTest(test.name, test.pointer, m.beforeEachFunc, m.AfterEachFunc, arg, hasArgs, test.skip)
+ end for
+ end sub
+
+ getFunctionPointer: TestFramework__getFunctionPointer
+ }
+
+ currentAnottations = []
+ index = 0
+
+ for each line in souceCode.Tokenize(Chr(10))
+ line = line.Trim()
+ if line <> "" ' skipping empty lines
+ if allowedAnnotationsRegex.IsMatch(line)
+ groups = allowedAnnotationsRegex.Match(line)
+ anottationType = groups[1]
+ if anottationType <> invalid and processors[anottationType] <> invalid then
+ currentAnottations.push(anottationType)
+ processors.annotations[anottationType] = { line: line, lineIndex: index }
+ end if
+ else
+ if currentAnottations.count() > 0 then
+ isParametrized = anyArgsFunctionRegex.IsMatch(line)
+ properMap = { parameterizedtest: "", methodsource: "" }
+ for each availableAnottation in currentAnottations
+ isParametrized = isParametrized or properMap[availableAnottation] <> invalid
+ end for
+
+ if voidFunctionRegex.IsMatch(line) or isParametrized then
+ groups = voidFunctionRegex.Match(line)
+
+ if isParametrized then
+ groups = anyArgsFunctionRegex.Match(line)
+ end if
+ if groups[2] <> invalid then
+ processors.functionName = groups[2]
+ processors.currentLine = line
+
+ ' process all handlers
+ if isParametrized then processors.executedParametrizedAdding = false
+ for each availableAnottation in currentAnottations
+ processors[availableAnottation]()
+ if isParametrized then processors.executedParametrizedAdding = true
+ end for
+ currentAnottations = []
+ processors.annotations = {}
+ foundAnyTest = true
+ end if
+ else
+ ' invalidating annotation
+ ' TODO print message here that we skipped annotation
+ ? "WARNING: annotation " currentAnottations " isparametrized=" isParametrized " skipped at line " index ":[" line "]"
+ processors.annotations = {}
+ currentAnottations = []
+ end if
+ end if
+ end if
+ end if
+ index++
+ end for
+
+ processors.buildTests()
+
+ if not foundAnyTest then
+ testSuite = invalid
+ end if
+ return testSuite
+end function
+
+function TestFramework__getFunctionPointer(functionName as string) as dynamic
+ result = invalid
+
+ gthis = GetGlobalAA()
+ if gthis.FunctionsList <> invalid then
+ for each value in gthis.FunctionsList
+ if Type(value) <> "" and LCase(Type(value)) <> "" and GetInterface(value, "ifFunction") <> invalid and LCase(value.tostr()) = "function: " + LCase(functionName) then
+ result = value
+ exit for
+ end if
+ end for
+ end if
+
+ if LCase(Type(result)) = "" then result = invalid
+ if result = invalid then
+ if gthis.notFoundFunctionPointerList = invalid then gthis.notFoundFunctionPointerList = []
+ gthis.notFoundFunctionPointerList.push(functionName)
+ end if
+ return result
+end function
+
+sub TestRunner__SetFunctions(listOfFunctions as dynamic)
+ gthis = GetGlobalAA()
+
+ if gthis.FunctionsList = invalid then
+ gthis.FunctionsList = []
+ end if
+ gthis.FunctionsList.append(listOfFunctions)
+end sub
+
+sub TestRunner__SetIncludeFilter(listOfFunctions as dynamic)
+ gthis = GetGlobalAA()
+
+ if gthis.IncludeFilter = invalid
+ gthis.IncludeFilter = []
+ end if
+
+ if TF_Utils__IsArray(listOfFunctions)
+ gthis.IncludeFilter.Append(listOfFunctions)
+ else if TF_Utils__IsNotEmptyString(listOfFunctions)
+ gthis.IncludeFilter.Append(listOfFunctions.Split(","))
+ else
+ ? "WARNING: Could not parse input parameters for Include Filter. Filter wont be applied."
+ end if
+end sub
+
+function TestRunner__GetIncludeFilter()
+ gthis = GetGlobalAA()
+
+ if gthis.IncludeFilter = invalid
+ gthis.IncludeFilter = []
+ end if
+
+ return gthis.IncludeFilter
+end function
+
+sub TestRunner__SetExcludeFilter(listOfFunctions as dynamic)
+ gthis = GetGlobalAA()
+
+ if gthis.ExcludeFilter = invalid
+ gthis.ExcludeFilter = []
+ end if
+
+ if TF_Utils__IsArray(listOfFunctions)
+ gthis.ExcludeFilter.Append(listOfFunctions)
+ else if TF_Utils__IsNotEmptyString(listOfFunctions)
+ gthis.ExcludeFilter.Append(listOfFunctions.Split(","))
+ else
+ ? "WARNING: Could not parse input parameters for Exclude Filter. Filter wont be applied."
+ end if
+end sub
+
+function TestRunner__GetExcludeFilter()
+ gthis = GetGlobalAA()
+
+ if gthis.ExcludeFilter = invalid
+ gthis.ExcludeFilter = []
+ end if
+
+ return gthis.ExcludeFilter
+end function
+
+' ----------------------------------------------------------------
+' Scans all test files for test suite function names for a given test node.
+
+' @param testNodeName (string) name of a test node, test suites for which are needed
+
+' @return An array of test suite names.
+' ----------------------------------------------------------------
+function TestRunner__GetTestSuiteNamesList(testNodeName as string) as object
+ result = []
+ testSuiteRegex = CreateObject("roRegex", "^(function|sub)\s(" + m.testSuitePrefix + m.testSuiteName + "[0-9a-z\_]*)\s*\(", "i")
+ testFilesList = m.GetTestFilesList(m.nodesTestDirectory, testNodeName)
+
+ for each filePath in testFilesList
+ code = TF_Utils__AsString(ReadAsciiFile(filePath))
+
+ if code <> ""
+ foundTestSuite = false
+ for each line in code.Tokenize(Chr(10))
+ line.Trim()
+
+ if testSuiteRegex.IsMatch(line)
+ functionName = testSuiteRegex.Match(line).Peek()
+ result.Push(functionName)
+ foundTestSuite = true
+ end if
+ end for
+
+ if not foundTestSuite then
+ ' we cannot scan for new tests as we are not in proper scope
+ ' so we need to pass some data so this can be executed in render thread
+ result.push({ filePath: filePath, code: code })
+ end if
+ end if
+ end for
+
+ return result
+end function
+
+' ----------------------------------------------------------------
+' Scan testsDirectory and all subdirectories for test files.
+
+' @param testsDirectory (string, optional) A target directory with test files.
+' @param testFilePrefix (string, optional) prefix, used by test files
+
+' @return An array of test files.
+' ----------------------------------------------------------------
+function TestRunner__GetTestFilesList(testsDirectory = m.testsDirectory as string, testFilePrefix = m.testFilePrefix as string) as object
+ result = []
+ testsFileRegex = CreateObject("roRegex", "^(" + testFilePrefix + ")[0-9a-z\_]*\.brs$", "i")
+
+ if testsDirectory <> ""
+ fileSystem = CreateObject("roFileSystem")
+
+ if m.isNodeMode
+ ? string(2, Chr(10))
+ ? string(10, "!!!")
+ ? "Note if you crash here this means that we are in render thread and searching for tests"
+ ? "Problem is that file naming is wrong"
+ ? "check brs file name they should match pattern ""Test_ExactComponentName_anything.brs"""
+ ? "In this case we were looking for "testFilePrefix
+ ? string(10, "!!!") string(2, Chr(10))
+ end if
+ listing = fileSystem.GetDirectoryListing(testsDirectory)
+
+ for each item in listing
+ itemPath = testsDirectory + "/" + item
+ itemStat = fileSystem.Stat(itemPath)
+
+ if itemStat.type = "directory" then
+ result.Append(m.getTestFilesList(itemPath, testFilePrefix))
+ else if testsFileRegex.IsMatch(item) then
+ result.Push(itemPath)
+ end if
+ end for
+ end if
+
+ return result
+end function
+
+' ----------------------------------------------------------------
+' Scan nodesTestDirectory and all subdirectories for test nodes.
+
+' @param nodesTestDirectory (string, optional) A target directory with test nodes.
+
+' @return An array of test node names.
+' ----------------------------------------------------------------
+function TestRunner__GetTestNodesList(testsDirectory = m.nodesTestDirectory as string) as object
+ result = []
+ testsFileRegex = CreateObject("roRegex", "^(" + m.testFilePrefix + ")[0-9a-z\_]*\.xml$", "i")
+
+ if testsDirectory <> ""
+ fileSystem = CreateObject("roFileSystem")
+ listing = fileSystem.GetDirectoryListing(testsDirectory)
+
+ for each item in listing
+ itemPath = testsDirectory + "/" + item
+ itemStat = fileSystem.Stat(itemPath)
+
+ if itemStat.type = "directory" then
+ result.Append(m.getTestNodesList(itemPath))
+ else if testsFileRegex.IsMatch(item) then
+ result.Push(item.replace(".xml", ""))
+ end if
+ end for
+ end if
+
+ return result
+end function
+
+' ----------------------------------------------------------------
+' Creates and runs test runner. Should be used ONLY within a node.
+
+' @param params (array) parameters, passed from main thread, used to setup new test runner
+
+' @return statistic object.
+' ----------------------------------------------------------------
+function TestFramework__RunNodeTests(params as object) as object
+ this = params[0]
+
+ statObj = params[1]
+ testSuiteNamesList = params[2]
+
+ Runner = TestRunner()
+
+ Runner.SetTestSuitePrefix(this.testSuitePrefix)
+ Runner.SetTestFilePrefix(this.testFilePrefix)
+ Runner.SetTestSuiteName(this.testSuiteName)
+ Runner.SetTestCaseName(this.testCaseName)
+ Runner.SetFailFast(this.failFast)
+
+ Runner.SetIncludeFilter(params[3])
+ Runner.SetExcludeFilter(params[4])
+
+ return Runner.Run(statObj, testSuiteNamesList)
+end function
+function UTF_skip(msg = "")
+ return UTF_PushErrorMessage(BTS__Skip(msg))
+end function
+
+function UTF_fail(msg = "")
+ return UTF_PushErrorMessage(BTS__Fail(msg))
+end function
+
+function UTF_assertFalse(expr, msg = "Expression evaluates to true")
+ return UTF_PushErrorMessage(BTS__AssertFalse(expr, msg))
+end function
+
+function UTF_assertTrue(expr, msg = "Expression evaluates to false")
+ return UTF_PushErrorMessage(BTS__AssertTrue(expr, msg))
+end function
+
+function UTF_assertEqual(first, second, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertEqual(first, second, msg))
+end function
+
+function UTF_assertNotEqual(first, second, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertNotEqual(first, second, msg))
+end function
+
+function UTF_assertInvalid(value, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertInvalid(value, msg))
+end function
+
+function UTF_assertNotInvalid(value, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertNotInvalid(value, msg))
+end function
+
+function UTF_assertAAHasKey(array, key, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertAAHasKey(array, key, msg))
+end function
+
+function UTF_assertAANotHasKey(array, key, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertAANotHasKey(array, key, msg))
+end function
+
+function UTF_assertAAHasKeys(array, keys, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertAAHasKeys(array, keys, msg))
+end function
+
+function UTF_assertAANotHasKeys(array, keys, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertAANotHasKeys(array, keys, msg))
+end function
+
+function UTF_assertArrayContains(array, value, key = invalid, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertArrayContains(array, value, key, msg))
+end function
+
+function UTF_assertArrayNotContains(array, value, key = invalid, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertArrayNotContains(array, value, key, msg))
+end function
+
+function UTF_assertArrayContainsSubset(array, subset, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertArrayContainsSubset(array, subset, msg))
+end function
+
+function UTF_assertArrayNotContainsSubset(array, subset, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertArrayNotContainsSubset(array, subset, msg))
+end function
+
+function UTF_assertArrayCount(array, count, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertArrayCount(array, count, msg))
+end function
+
+function UTF_assertArrayNotCount(array, count, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertArrayNotCount(array, count, msg))
+end function
+
+function UTF_assertEmpty(item, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertEmpty(item, msg))
+end function
+
+function UTF_assertNotEmpty(item, msg = "")
+ return UTF_PushErrorMessage(BTS__AssertNotEmpty(item, msg))
+end function
+
+function UTF_PushErrorMessage(message as string) as boolean
+ result = Len(message) <= 0
+ if not result then
+ m.globalErrorsList.push(message)
+ end if
+
+ return result
+end function'*****************************************************************
+'* Copyright Roku 2011-2019
+'* All Rights Reserved
+'*****************************************************************
+' Common framework utility functions
+' *****************************************************************
+
+' *************************************************
+' TF_Utils__IsXmlElement - check if value contains XMLElement interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains XMLElement interface, else return false
+' *************************************************
+function TF_Utils__IsXmlElement(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifXMLElement") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsFunction - check if value contains Function interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains Function interface, else return false
+' *************************************************
+function TF_Utils__IsFunction(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifFunction") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsBoolean - check if value contains Boolean interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains Boolean interface, else return false
+' *************************************************
+function TF_Utils__IsBoolean(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifBoolean") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsInteger - check if value type equals Integer
+' @param value As Dynamic
+' @return As Boolean - true if value type equals Integer, else return false
+' *************************************************
+function TF_Utils__IsInteger(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifInt") <> invalid and (Type(value) = "roInt" or Type(value) = "roInteger" or Type(value) = "Integer")
+end function
+
+' *************************************************
+' TF_Utils__IsFloat - check if value contains Float interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains Float interface, else return false
+' *************************************************
+function TF_Utils__IsFloat(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifFloat") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsDouble - check if value contains Double interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains Double interface, else return false
+' *************************************************
+function TF_Utils__IsDouble(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifDouble") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsLongInteger - check if value contains LongInteger interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains LongInteger interface, else return false
+' *************************************************
+function TF_Utils__IsLongInteger(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifLongInt") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsNumber - check if value contains LongInteger or Integer or Double or Float interface
+' @param value As Dynamic
+' @return As Boolean - true if value is number, else return false
+' *************************************************
+function TF_Utils__IsNumber(value as dynamic) as boolean
+ return TF_Utils__IsLongInteger(value) or TF_Utils__IsDouble(value) or TF_Utils__IsInteger(value) or TF_Utils__IsFloat(value)
+end function
+
+' *************************************************
+' TF_Utils__IsList - check if value contains List interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains List interface, else return false
+' *************************************************
+function TF_Utils__IsList(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifList") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsArray - check if value contains Array interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains Array interface, else return false
+' *************************************************
+function TF_Utils__IsArray(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifArray") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsAssociativeArray - check if value contains AssociativeArray interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains AssociativeArray interface, else return false
+' *************************************************
+function TF_Utils__IsAssociativeArray(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifAssociativeArray") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsSGNode - check if value contains SGNodeChildren interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains SGNodeChildren interface, else return false
+' *************************************************
+function TF_Utils__IsSGNode(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifSGNodeChildren") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsString - check if value contains String interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains String interface, else return false
+' *************************************************
+function TF_Utils__IsString(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and GetInterface(value, "ifString") <> invalid
+end function
+
+' *************************************************
+' TF_Utils__IsNotEmptyString - check if value contains String interface and length more 0
+' @param value As Dynamic
+' @return As Boolean - true if value contains String interface and length more 0, else return false
+' *************************************************
+function TF_Utils__IsNotEmptyString(value as dynamic) as boolean
+ return TF_Utils__IsString(value) and Len(value) > 0
+end function
+
+' *************************************************
+' TF_Utils__IsDateTime - check if value contains DateTime interface
+' @param value As Dynamic
+' @return As Boolean - true if value contains DateTime interface, else return false
+' *************************************************
+function TF_Utils__IsDateTime(value as dynamic) as boolean
+ return TF_Utils__IsValid(value) and (GetInterface(value, "ifDateTime") <> invalid or Type(value) = "roDateTime")
+end function
+
+' *************************************************
+' TF_Utils__IsValid - check if value initialized and not equal invalid
+' @param value As Dynamic
+' @return As Boolean - true if value initialized and not equal invalid, else return false
+' *************************************************
+function TF_Utils__IsValid(value as dynamic) as boolean
+ return Type(value) <> "" and value <> invalid
+end function
+
+' *************************************************
+' TF_Utils__ValidStr - return value if his contains String interface else return empty string
+' @param value As Object
+' @return As String - value if his contains String interface else return empty string
+' *************************************************
+function TF_Utils__ValidStr(obj as object) as string
+ if obj <> invalid and GetInterface(obj, "ifString") <> invalid
+ return obj
+ else
+ return ""
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsString - convert input to String if this possible, else return empty string
+' @param input As Dynamic
+' @return As String - return converted string
+' *************************************************
+function TF_Utils__AsString(input as dynamic) as string
+ if TF_Utils__IsValid(input) = false
+ return ""
+ else if TF_Utils__IsString(input)
+ return input
+ else if TF_Utils__IsInteger(input) or TF_Utils__IsLongInteger(input) or TF_Utils__IsBoolean(input)
+ return input.ToStr()
+ else if TF_Utils__IsFloat(input) or TF_Utils__IsDouble(input)
+ return Str(input).Trim()
+ else
+ return ""
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsInteger - convert input to Integer if this possible, else return 0
+' @param input As Dynamic
+' @return As Integer - return converted Integer
+' *************************************************
+function TF_Utils__AsInteger(input as dynamic) as integer
+ if TF_Utils__IsValid(input) = false
+ return 0
+ else if TF_Utils__IsString(input)
+ return input.ToInt()
+ else if TF_Utils__IsInteger(input)
+ return input
+ else if TF_Utils__IsFloat(input) or TF_Utils__IsDouble(input) or TF_Utils__IsLongInteger(input)
+ return Int(input)
+ else
+ return 0
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsLongInteger - convert input to LongInteger if this possible, else return 0
+' @param input As Dynamic
+' @return As Integer - return converted LongInteger
+' *************************************************
+function TF_Utils__AsLongInteger(input as dynamic) as longinteger
+ if TF_Utils__IsValid(input) = false
+ return 0
+ else if TF_Utils__IsString(input)
+ return TF_Utils__AsInteger(input)
+ else if TF_Utils__IsLongInteger(input) or TF_Utils__IsFloat(input) or TF_Utils__IsDouble(input) or TF_Utils__IsInteger(input)
+ return input
+ else
+ return 0
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsFloat - convert input to Float if this possible, else return 0.0
+' @param input As Dynamic
+' @return As Float - return converted Float
+' *************************************************
+function TF_Utils__AsFloat(input as dynamic) as float
+ if TF_Utils__IsValid(input) = false
+ return 0.0
+ else if TF_Utils__IsString(input)
+ return input.ToFloat()
+ else if TF_Utils__IsInteger(input)
+ return (input / 1)
+ else if TF_Utils__IsFloat(input) or TF_Utils__IsDouble(input) or TF_Utils__IsLongInteger(input)
+ return input
+ else
+ return 0.0
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsDouble - convert input to Double if this possible, else return 0.0
+' @param input As Dynamic
+' @return As Float - return converted Double
+' *************************************************
+function TF_Utils__AsDouble(input as dynamic) as double
+ if TF_Utils__IsValid(input) = false
+ return 0.0
+ else if TF_Utils__IsString(input)
+ return TF_Utils__AsFloat(input)
+ else if TF_Utils__IsInteger(input) or TF_Utils__IsLongInteger(input) or TF_Utils__IsFloat(input) or TF_Utils__IsDouble(input)
+ return input
+ else
+ return 0.0
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsBoolean - convert input to Boolean if this possible, else return False
+' @param input As Dynamic
+' @return As Boolean
+' *************************************************
+function TF_Utils__AsBoolean(input as dynamic) as boolean
+ if TF_Utils__IsValid(input) = false
+ return false
+ else if TF_Utils__IsString(input)
+ return LCase(input) = "true"
+ else if TF_Utils__IsInteger(input) or TF_Utils__IsFloat(input)
+ return input <> 0
+ else if TF_Utils__IsBoolean(input)
+ return input
+ else
+ return false
+ end if
+end function
+
+' *************************************************
+' TF_Utils__AsArray - if type of value equals array return value, else return array with one element [value]
+' @param value As Object
+' @return As Object - roArray
+' *************************************************
+function TF_Utils__AsArray(value as object) as object
+ if TF_Utils__IsValid(value)
+ if not TF_Utils__IsArray(value)
+ return [value]
+ else
+ return value
+ end if
+ end if
+ return []
+end function
+
+' =====================
+' Strings
+' =====================
+
+' *************************************************
+' TF_Utils__IsNullOrEmpty - check if value is invalid or empty
+' @param value As Dynamic
+' @return As Boolean - true if value is null or empty string, else return false
+' *************************************************
+function TF_Utils__IsNullOrEmpty(value as dynamic) as boolean
+ if TF_Utils__IsString(value)
+ return Len(value) = 0
+ else
+ return not TF_Utils__IsValid(value)
+ end if
+end function
+
+' =====================
+' Arrays
+' =====================
+
+' *************************************************
+' TF_Utils__FindElementIndexInArray - find an element index in array
+' @param array As Object
+' @param value As Object
+' @param compareAttribute As Dynamic
+' @param caseSensitive As Boolean
+' @return As Integer - element index if array contains a value, else return -1
+' *************************************************
+function TF_Utils__FindElementIndexInArray(array as object, value as object, compareAttribute = invalid as dynamic, caseSensitive = false as boolean) as integer
+ if TF_Utils__IsArray(array)
+ for i = 0 to TF_Utils__AsArray(array).Count() - 1
+ compareValue = array[i]
+
+ if compareAttribute <> invalid and TF_Utils__IsAssociativeArray(compareValue) and compareValue.DoesExist(compareAttribute)
+ compareValue = compareValue.LookupCI(compareAttribute)
+ end if
+
+ if TF_Utils__IsString(compareValue) and TF_Utils__IsString(value) and not caseSensitive
+ if LCase(compareValue) = LCase(value)
+ return i
+ end if
+ else if TF_Utils__BaseComparator(compareValue, value)
+ return i
+ end if
+
+ item = array[i]
+ next
+ end if
+
+ return -1
+end function
+
+' *************************************************
+' TF_Utils__ArrayContains - check if array contains specified value
+' @param array As Object
+' @param value As Object
+' @param compareAttribute As Dynamic
+' @return As Boolean - true if array contains a value, else return false
+' *************************************************
+function TF_Utils__ArrayContains(array as object, value as object, compareAttribute = invalid as dynamic) as boolean
+ return (TF_Utils__FindElementIndexInArray(array, value, compareAttribute) > -1)
+end function
+
+' ----------------------------------------------------------------
+' Type Comparison Functionality
+' ----------------------------------------------------------------
+
+' ----------------------------------------------------------------
+' Compare two arbitrary values to each other.
+
+' @param Value1 (dynamic) A first item to compare.
+' @param Value2 (dynamic) A second item to compare.
+' @param comparator (Function, optional) Function, to compare 2 values. Should take in 2 parameters and return either true or false.
+
+' @return True if values are equal or False in other case.
+' ----------------------------------------------------------------
+function TF_Utils__EqValues(Value1 as dynamic, Value2 as dynamic, comparator = invalid as object) as boolean
+ if comparator = invalid
+ return TF_Utils__BaseComparator(value1, value2)
+ else
+ return comparator(value1, value2)
+ end if
+end function
+
+' ----------------------------------------------------------------
+' Base comparator for comparing two values.
+
+' @param Value1 (dynamic) A first item to compare.
+' @param Value2 (dynamic) A second item to compare.
+
+' @return True if values are equal or False in other case.
+function TF_Utils__BaseComparator(value1 as dynamic, value2 as dynamic) as boolean
+ value1Type = Type(value1)
+ value2Type = Type(value2)
+
+ if (value1Type = "roList" or value1Type = "roArray") and (value2Type = "roList" or value2Type = "roArray")
+ return TF_Utils__EqArray(value1, value2)
+ else if value1Type = "roAssociativeArray" and value2Type = "roAssociativeArray"
+ return TF_Utils__EqAssocArray(value1, value2)
+ else if Type(box(value1), 3) = Type(box(value2), 3)
+ return value1 = value2
+ else
+ return false
+ end if
+end function
+
+' ----------------------------------------------------------------
+' Compare two roAssociativeArray objects for equality.
+
+' @param Value1 (object) A first associative array.
+' @param Value2 (object) A second associative array.
+
+' @return True if arrays are equal or False in other case.
+' ----------------------------------------------------------------
+function TF_Utils__EqAssocArray(Value1 as object, Value2 as object) as boolean
+ l1 = Value1.Count()
+ l2 = Value2.Count()
+
+ if not l1 = l2
+ return false
+ else
+ for each k in Value1
+ if not Value2.DoesExist(k)
+ return false
+ else
+ v1 = Value1[k]
+ v2 = Value2[k]
+ if not TF_Utils__EqValues(v1, v2)
+ return false
+ end if
+ end if
+ end for
+ return true
+ end if
+end function
+
+' ----------------------------------------------------------------
+' Compare two roArray objects for equality.
+
+' @param Value1 (object) A first array.
+' @param Value2 (object) A second array.
+
+' @return True if arrays are equal or False in other case.
+' ----------------------------------------------------------------
+function TF_Utils__EqArray(Value1 as object, Value2 as object) as boolean
+ l1 = Value1.Count()
+ l2 = Value2.Count()
+
+ if not l1 = l2
+ return false
+ else
+ for i = 0 to l1 - 1
+ v1 = Value1[i]
+ v2 = Value2[i]
+ if not TF_Utils__EqValues(v1, v2) then
+ return false
+ end if
+ end for
+ return true
+ end if
+end function
diff --git a/source/tests/Test__Misc.brs b/source/tests/Test__Misc.brs
new file mode 100644
index 00000000..922c0699
--- /dev/null
+++ b/source/tests/Test__Misc.brs
@@ -0,0 +1,81 @@
+function TestSuite__Misc() as object
+
+ ' Inherite test suite from BaseTestSuite
+ this = BaseTestSuite()
+
+ ' Test suite name for log statistics
+ this.Name = "MiscTestSuite"
+
+ this.SetUp = MiscTestSuite__SetUp
+ this.TearDown = MiscTestSuite__TearDown
+
+ ' Add tests to suite's tests collection
+ this.addTest("IsValid() true", TestCase__Misc_IsValid_True)
+ this.addTest("IsValid() false", TestCase__Misc_IsValid_False)
+ this.addTest("RoundNumber() Floor", TestCase__Misc_RoundNumber_Floor)
+ this.addTest("RoundNumber() Ceiling", TestCase__Misc_RoundNumber_Ceiling)
+
+ return this
+end function
+
+'----------------------------------------------------------------
+' This function called immediately before running tests of current suite.
+'----------------------------------------------------------------
+sub MiscTestSuite__SetUp()
+end sub
+
+'----------------------------------------------------------------
+' This function called immediately after running tests of current suite.
+'----------------------------------------------------------------
+sub MiscTestSuite__TearDown()
+end sub
+
+'----------------------------------------------------------------
+' Check if isValid() properly identifies valid items
+'
+' @return An empty string if test is success or error message if not.
+'----------------------------------------------------------------
+function TestCase__Misc_IsValid_True() as string
+ returnResults = ""
+ testData = [1, 2, [3, 4], { "key": invalid }, [1, 2, 3], CreateObject("roAppInfo")]
+
+ for each testItem in testData
+ returnResults = returnResults + m.AssertTrue(isValid(testItem))
+ end for
+
+ return m.AssertEmpty(returnResults)
+end function
+
+'----------------------------------------------------------------
+' Check if isValid() properly identifies invalid items
+'
+' @return An empty string if test is success or error message if not.
+'----------------------------------------------------------------
+function TestCase__Misc_IsValid_False() as string
+ returnResults = ""
+ testData = [invalid, CreateObject("nothing")]
+
+ for each testItem in testData
+ returnResults = m.AssertFalse(isValid(testItem))
+ end for
+
+ return m.AssertEmpty(returnResults)
+end function
+
+'----------------------------------------------------------------
+' Check if roundNumber() properly rounds down
+'
+' @return An empty string if test is success or error message if not.
+'----------------------------------------------------------------
+function TestCase__Misc_RoundNumber_Floor() as string
+ return m.AssertEqual(roundNumber(9.4), 9)
+end function
+
+'----------------------------------------------------------------
+' Check if roundNumber() properly rounds up
+'
+' @return An empty string if test is success or error message if not.
+'----------------------------------------------------------------
+function TestCase__Misc_RoundNumber_Ceiling() as string
+ return m.AssertEqual(roundNumber(9.6), 10)
+end function
diff --git a/source/utils/deviceCapabilities.brs b/source/utils/deviceCapabilities.brs
index 8df2374b..501b8910 100644
--- a/source/utils/deviceCapabilities.brs
+++ b/source/utils/deviceCapabilities.brs
@@ -171,13 +171,7 @@ function getDeviceProfile() as object
"Value": "41",
"IsRequired": false
},
- ' Roku only supports h264 up to 10Mpbs
- {
- "Condition": "LessThanEqual",
- "Property": "VideoBitrate",
- "Value": "10000000",
- IsRequired: true
- }
+ GetBitRateLimit("H264")
]
}
],
@@ -211,13 +205,7 @@ function getDeviceProfile() as object
"Value": av1VideoRangeTypes,
"IsRequired": false
},
- ' Roku only supports AVI up to 40Mpbs
- {
- "Condition": "LessThanEqual",
- "Property": "VideoBitrate",
- "Value": "40000000",
- IsRequired: true
- }
+ GetBitRateLimit("AV1")
]
})
end if
@@ -244,13 +232,7 @@ function getDeviceProfile() as object
"Value": (120 * 5.1).ToStr(),
"IsRequired": false
},
- ' Roku only supports h265 up to 40Mpbs
- {
- "Condition": "LessThanEqual",
- "Property": "VideoBitrate",
- "Value": "40000000",
- IsRequired: true
- }
+ GetBitRateLimit("H265")
]
})
end if
@@ -265,13 +247,7 @@ function getDeviceProfile() as object
"Value": vp9VideoRangeTypes,
"IsRequired": false
},
- ' Roku only supports VP9 up to 40Mpbs
- {
- "Condition": "LessThanEqual",
- "Property": "VideoBitrate",
- "Value": "40000000",
- IsRequired: true
- }
+ GetBitRateLimit("VP9")
]
})
end if
@@ -382,3 +358,56 @@ function GetDirectPlayProfiles() as object
]
end function
+
+function GetBitRateLimit(codec as string)
+ if get_user_setting("playback.bitrate.maxlimited") = "true"
+ userSetLimit = get_user_setting("playback.bitrate.limit").ToInt()
+ userSetLimit *= 1000000
+
+ if userSetLimit > 0
+ return {
+ "Condition": "LessThanEqual",
+ "Property": "VideoBitrate",
+ "Value": userSetLimit.ToStr(),
+ IsRequired: true
+ }
+ else
+ ' Some repeated values (e.g. same "40mbps" for several codecs)
+ ' but this makes it easy to update in the future if the bitrates start to deviate.
+ if codec = "H264"
+ ' Roku only supports h264 up to 10Mpbs
+ return {
+ "Condition": "LessThanEqual",
+ "Property": "VideoBitrate",
+ "Value": "10000000",
+ IsRequired: true
+ }
+ else if codec = "AV1"
+ ' Roku only supports AV1 up to 40Mpbs
+ return {
+ "Condition": "LessThanEqual",
+ "Property": "VideoBitrate",
+ "Value": "40000000",
+ IsRequired: true
+ }
+ else if codec = "H265"
+ ' Roku only supports h265 up to 40Mpbs
+ return {
+ "Condition": "LessThanEqual",
+ "Property": "VideoBitrate",
+ "Value": "40000000",
+ IsRequired: true
+ }
+ else if codec = "VP9"
+ ' Roku only supports VP9 up to 40Mpbs
+ return {
+ "Condition": "LessThanEqual",
+ "Property": "VideoBitrate",
+ "Value": "40000000",
+ IsRequired: true
+ }
+ end if
+ end if
+ end if
+ return {}
+end function