Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Closes #3424 - Check if session is installable
Browse files Browse the repository at this point in the history
  • Loading branch information
NotWoods committed Jun 19, 2019
1 parent abd88a8 commit 318e5d2
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package mozilla.components.concept.engine.manifest

import mozilla.components.support.ktx.android.org.json.asSequence
import mozilla.components.support.ktx.android.org.json.tryGetString
import org.json.JSONException
import org.json.JSONObject

Expand Down Expand Up @@ -37,9 +38,13 @@ class WebAppManifestParser {
*/
fun parse(json: JSONObject): Result {
return try {
val shortName = json.tryGetString("short_name")
val name = json.tryGetString("name") ?: shortName
?: return Result.Failure(JSONException("Missing manifest name"))

Result.Success(WebAppManifest(
name = json.getString("name"),
shortName = json.optString("short_name", null),
name = name,
shortName = shortName,
startUrl = json.getString("start_url"),
display = getDisplayMode(json),
backgroundColor = parseColor(json.optString("background_color", null)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ class WebAppManifestParserTest {
assertEquals(0, manifest.icons.size)
}

@Test
fun `Parsing manifest with no name`() {
val json = loadManifest("minimal_short_name.json")
val result = WebAppManifestParser().parse(json)
assertTrue(result is WebAppManifestParser.Result.Success)
val manifest = (result as WebAppManifestParser.Result.Success).manifest

assertNotNull(manifest)
assertEquals("Minimal with Short Name", manifest.name)
assertEquals("Minimal with Short Name", manifest.shortName)
assertEquals("/", manifest.startUrl)
assertEquals(WebAppManifest.DisplayMode.BROWSER, manifest.display)
assertNull(manifest.backgroundColor)
assertNull(manifest.description)
assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
assertNull(manifest.lang)
assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
assertNull(manifest.scope)
assertNull(manifest.themeColor)

assertEquals(0, manifest.icons.size)
}

@Test
fun `Parsing typical manifest from W3 spec`() {
val json = loadManifest("spec_typical.json")
Expand Down Expand Up @@ -245,6 +268,14 @@ class WebAppManifestParserTest {
assertTrue(result is WebAppManifestParser.Result.Failure)
}

@Test
fun `Parsing invalid JSON missing name fields`() {
val json = loadManifest("invalid_missing_name.json")
val result = WebAppManifestParser().parse(json)

assertTrue(result is WebAppManifestParser.Result.Failure)
}

@Test
fun `Parsing manifest with unusual values`() {
val json = loadManifest("unusual.json")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"start_url": "https://example.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"short_name": "Minimal with Short Name",
"start_url": "/"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.pwa.ext

import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.manifest.WebAppManifest.Icon.Purpose
import kotlin.math.min

private const val MIN_INSTALLABLE_ICON_SIZE = 192

/**
* Checks if the current session represents an installable web app.
*
* Websites are installable if:
* - The site is served over HTTPS
* - The site has a valid manifest with a name or short_name
* - The icons array in the manifest contains an icon of at least 192x192
*/
fun Session.isInstallable(): Boolean {
if (!securityInfo.secure) return false
val manifest = webAppManifest ?: return false

return manifest.icons.any { icon ->
(Purpose.ANY in icon.purpose || Purpose.MASKABLE in icon.purpose) &&
icon.sizes.any { size ->
min(size.width, size.height) >= MIN_INSTALLABLE_ICON_SIZE
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.pwa.ext

import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.manifest.Size
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class SessionKtTest {
private val demoManifest = WebAppManifest(name = "Demo", startUrl = "https://mozilla.com")
private val demoIcon = WebAppManifest.Icon(src = "https://mozilla.com/example.png")

@Test
fun `web app must be HTTPS to be installable`() {
val httpSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = false))
}
assertFalse(httpSession.isInstallable())
}

@Test
fun `web app must have manifest to be installable`() {
val noManifestSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(null)
}
assertFalse(noManifestSession.isInstallable())
}

@Test
fun `web app must have an icon to be installable`() {
val noIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(demoManifest)
}
assertFalse(noIconSession.isInstallable())

val noSizeIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(demoIcon))
)
}
assertFalse(noSizeIconSession.isInstallable())

val onlyBadgeIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(
demoIcon.copy(
sizes = listOf(Size(512, 512)),
purpose = setOf(WebAppManifest.Icon.Purpose.BADGE)
)
))
)
}
assertFalse(onlyBadgeIconSession.isInstallable())
}

@Test
fun `web app must have 192x192 icons to be installable`() {
val smallIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(
demoIcon.copy(sizes = listOf(Size(32, 32)))
))
)
}
assertFalse(smallIconSession.isInstallable())

val weirdSizeSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(
demoIcon.copy(sizes = listOf(Size(50, 200)))
))
)
}
assertFalse(weirdSizeSession.isInstallable())

val largeIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(
demoIcon.copy(sizes = listOf(Size(192, 192)))
))
)
}
assertTrue(largeIconSession.isInstallable())

val multiSizeIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(
demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512)))
))
)
}
assertTrue(multiSizeIconSession.isInstallable())

val multiIconSession = mock<Session>().also {
whenever(it.securityInfo).thenReturn(Session.SecurityInfo(secure = true))
whenever(it.webAppManifest).thenReturn(
demoManifest.copy(icons = listOf(
demoIcon.copy(sizes = listOf(Size(191, 193))),
demoIcon.copy(sizes = listOf(Size(512, 512))),
demoIcon.copy(
sizes = listOf(Size(192, 192)),
purpose = setOf(WebAppManifest.Icon.Purpose.BADGE)
)
))
)
}
assertTrue(multiIconSession.isInstallable())
}
}

0 comments on commit 318e5d2

Please sign in to comment.