Skip to content

Commit

Permalink
dpwa: Add Subapp integration testing to framework
Browse files Browse the repository at this point in the history
Add a new app with a subapp to the desktop web app
integration testing framework, and a function
to install the subapp.

The API under test requires user activation.
For this, the app contains a button to press that
will trigger the API-call and an input field in which
to specify the specific sub-app to add.

Bug: 1313112
Test: browser_tests --gtest_filter=*WebAppIntegration.CheckSubappInstallation
Change-Id: I0100c16dde58e30a3332b6789403d1525bcffa21
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4142580
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Commit-Queue: Philipp Weiß <phweiss@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1095538}
  • Loading branch information
Philipp Weiß authored and Chromium LUCI CQ committed Jan 23, 2023
1 parent 24601f3 commit ed8d404
Show file tree
Hide file tree
Showing 28 changed files with 448 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ IN_PROC_BROWSER_TEST_F(WebAppIntegration, CheckBrowserNavigationFails) {
"webapps_integration/standalone/foo/basic.html");
}

IN_PROC_BROWSER_TEST_F(WebAppIntegration, CheckSubAppInstallation) {
helper_.InstallMenuOption(InstallableSite::kHasSubApps);
helper_.CheckNoSubApps();
helper_.InstallSubApp(Site::kHasSubApps, Site::kSubApp1,
SubAppInstallDialogOptions::kUserAllow);
helper_.CheckHasSubApp(Site::kSubApp1);
helper_.CheckNotHasSubApp(Site::kSubApp2);
helper_.CheckAppInListWindowed(Site::kSubApp1);
EXPECT_NONFATAL_FAILURE(helper_.CheckNoSubApps(),
"Expected equality of these values");
helper_.RemoveSubApp(Site::kHasSubApps, Site::kSubApp1);
helper_.CheckNoSubApps();
}

// Generated tests:

IN_PROC_BROWSER_TEST_F(
Expand Down
142 changes: 142 additions & 0 deletions chrome/browser/ui/views/web_apps/web_app_integration_test_driver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/accessibility/ax_action_data.h"
Expand Down Expand Up @@ -195,6 +196,12 @@ Site InstallableSiteToSite(InstallableSite site) {
return Site::kNotInstalled;
case InstallableSite::kScreenshots:
return Site::kScreenshots;
case InstallableSite::kHasSubApps:
return Site::kHasSubApps;
case InstallableSite::kSubApp1:
return Site::kSubApp1;
case InstallableSite::kSubApp2:
return Site::kSubApp2;
}
}

Expand Down Expand Up @@ -308,6 +315,26 @@ base::flat_map<Site, SiteConfig> g_site_configs = {
.app_name = "Site With Screenshots",
.wco_not_enabled_title = u"Site With Screenshots",
.icon_color = SK_ColorGREEN}},
{Site::kHasSubApps,
{.relative_url = "/webapps_integration/has_sub_apps/basic.html",
.relative_manifest_id = "webapps_integration/has_sub_apps/basic.html",
.app_name = "Site With Sub Apps",
.wco_not_enabled_title = u"Site With Sub Apps",
.icon_color = SK_ColorGREEN}},
{Site::kSubApp1,
{.relative_url = "/webapps_integration/has_sub_apps/sub_app1/basic.html",
.relative_manifest_id =
"webapps_integration/has_sub_apps/sub_app1/basic.html",
.app_name = "Sub App 1",
.wco_not_enabled_title = u"Sub App 1",
.icon_color = SK_ColorBLUE}},
{Site::kSubApp2,
{.relative_url = "/webapps_integration/has_sub_apps/sub_app2/basic.html",
.relative_manifest_id =
"webapps_integration/has_sub_apps/sub_app2/basic.html",
.app_name = "Sub App 2",
.wco_not_enabled_title = u"Sub App 2",
.icon_color = SK_ColorBLUE}},
};

struct DisplayConfig {
Expand Down Expand Up @@ -831,6 +858,13 @@ void WebAppIntegrationTestDriver::TearDownOnMainThread() {
for (auto& app_id : app_ids) {
LOG(INFO) << "TearDownOnMainThread: Uninstalling " << app_id << ".";
const WebApp* app = provider->registrar_unsafe().GetAppById(app_id);
if (!app) {
// This might happen if |app_id| was a sub-app of a previously
// uninstalled app.
LOG(INFO) << "TearDownOnMainThread: " << app_id
<< " was already removed.";
continue;
}
if (app->IsPolicyInstalledApp())
UninstallPolicyAppById(app_id);
if (provider->registrar_unsafe().IsInstalled(app_id)) {
Expand Down Expand Up @@ -1114,6 +1148,58 @@ void WebAppIntegrationTestDriver::InstallPolicyApp(Site site,
AfterStateChangeAction();
}

void WebAppIntegrationTestDriver::InstallSubApp(
Site parentapp,
Site subapp,
SubAppInstallDialogOptions option) {
if (!BeforeStateChangeAction(__FUNCTION__)) {
return;
}
MaybeNavigateTabbedBrowserInScope(parentapp);
content::WebContents* web_contents = GetCurrentTab(browser());

std::string sub_url = GetSiteConfiguration(subapp).relative_url;
// The argument of add() is a dictionary-valued dictionary:
// { $unhashed_app_id : {'install_url' : $install_url} }
// In our case, both $unhashed_app_id and $install_url are sub_url.
base::Value::Dict inner_dict;
inner_dict.Set("install_url", sub_url);
base::Value::Dict outer_dict;
outer_dict.Set(sub_url, std::move(inner_dict));

const base::Value& add_result =
content::EvalJs(web_contents,
content::JsReplace("navigator.subApps.add($1)",
std::move(outer_dict)))
.value;

base::Value::Dict expected_output;
expected_output.Set(sub_url, "success");
EXPECT_EQ(expected_output, add_result);
// TODO: Use |option| after the dialog was implemented.
AfterStateChangeAction();
}

void WebAppIntegrationTestDriver::RemoveSubApp(Site parentapp, Site subapp) {
if (!BeforeStateChangeAction(__FUNCTION__)) {
return;
}
MaybeNavigateTabbedBrowserInScope(parentapp);
content::WebContents* web_contents = GetCurrentTab(browser());
std::string sub_url = GetSiteConfiguration(subapp).relative_url;

const base::Value& remove_result =
content::EvalJs(
web_contents,
content::JsReplace("navigator.subApps.remove($1)", sub_url))
.value;

// remove() returns void.
EXPECT_TRUE(remove_result.is_none());

AfterStateChangeAction();
}

void WebAppIntegrationTestDriver::EnableWindowControlsOverlay(Site site) {
if (!BeforeStateChangeAction(__FUNCTION__))
return;
Expand Down Expand Up @@ -2710,6 +2796,61 @@ void WebAppIntegrationTestDriver::CheckWindowDisplayStandalone() {
AfterStateCheckAction();
}

void WebAppIntegrationTestDriver::CheckHasSubApp(Site subapp) {
if (!BeforeStateCheckAction(__FUNCTION__)) {
return;
}
content::WebContents* web_contents =
app_browser()->tab_strip_model()->GetActiveWebContents();

auto subapp_url = GetSiteConfiguration(subapp).relative_url;

const base::Value& list_result =
content::EvalJs(web_contents, "navigator.subApps.list()").value;

const base::Value::Dict& list_result_dict = list_result.GetDict();

// Check that list() contained the subapp_url key.
EXPECT_NE(nullptr, list_result_dict.FindDict(subapp_url));

AfterStateCheckAction();
}

void WebAppIntegrationTestDriver::CheckNotHasSubApp(Site subapp) {
if (!BeforeStateCheckAction(__FUNCTION__)) {
return;
}
content::WebContents* web_contents =
app_browser()->tab_strip_model()->GetActiveWebContents();

auto subapp_url = GetSiteConfiguration(subapp).relative_url;

const base::Value& list_result =
content::EvalJs(web_contents, "navigator.subApps.list()").value;

const base::Value::Dict& list_result_dict = list_result.GetDict();

// Check that list() did not contain the subapp_url key.
EXPECT_EQ(nullptr, list_result_dict.FindDict(subapp_url));

AfterStateCheckAction();
}

void WebAppIntegrationTestDriver::CheckNoSubApps() {
if (!BeforeStateCheckAction(__FUNCTION__)) {
return;
}
content::WebContents* web_contents =
app_browser()->tab_strip_model()->GetActiveWebContents();

const base::Value& result =
content::EvalJs(web_contents, "navigator.subApps.list()").value;

// Check that list() returned an empty dictionary.
EXPECT_EQ(base::Value(base::Value::Type::DICT), result);
AfterStateCheckAction();
}

void WebAppIntegrationTestDriver::OnWebAppManifestUpdated(
const AppId& app_id,
base::StringPiece old_name) {
Expand Down Expand Up @@ -3420,6 +3561,7 @@ WebAppIntegrationTest::WebAppIntegrationTest() : helper_(this) {
enabled_features.push_back(features::kPwaUpdateDialogForName);
enabled_features.push_back(features::kDesktopPWAsEnforceWebAppSettingsPolicy);
enabled_features.push_back(features::kRecordWebAppDebugInfo);
enabled_features.push_back(blink::features::kDesktopPWAsSubApps);
#if BUILDFLAG(IS_CHROMEOS_ASH)
disabled_features.push_back(features::kWebAppsCrosapi);
disabled_features.push_back(ash::features::kLacrosPrimary);
Expand Down
23 changes: 21 additions & 2 deletions chrome/browser/ui/views/web_apps/web_app_integration_test_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ enum class Site {
kFileHandler,
kNoServiceWorker,
kNotInstalled,
kScreenshots
kScreenshots,
kHasSubApps,
kSubApp1,
kSubApp2,
};

enum class InstallableSite {
Expand All @@ -69,7 +72,10 @@ enum class InstallableSite {
kFileHandler,
kNoServiceWorker,
kNotInstalled,
kScreenshots
kScreenshots,
kHasSubApps,
kSubApp1,
kSubApp2,
};

enum class Title { kStandaloneOriginal, kStandaloneUpdated };
Expand Down Expand Up @@ -114,6 +120,12 @@ enum class UpdateDialogResponse {
kSkipUpdate
};

enum class SubAppInstallDialogOptions {
kUserAllow,
kUserDeny,
kPolicyOverride
};

// These structs are used to store the current state of the world before & after
// each state-change action.

Expand Down Expand Up @@ -242,6 +254,10 @@ class WebAppIntegrationTestDriver : WebAppInstallManagerObserver {
ShortcutOptions shortcut,
WindowOptions window,
InstallMode mode);
void InstallSubApp(Site parentapp,
Site subapp,
SubAppInstallDialogOptions option);
void RemoveSubApp(Site parentapp, Site subapp);
// These functions install apps which are tabbed and creates shortcuts.
void ApplyRunOnOsLoginPolicyAllowed(Site site);
void ApplyRunOnOsLoginPolicyBlocked(Site site);
Expand Down Expand Up @@ -330,6 +346,9 @@ class WebAppIntegrationTestDriver : WebAppInstallManagerObserver {
void CheckWindowDisplayBrowser();
void CheckWindowDisplayMinimal();
void CheckWindowDisplayStandalone();
void CheckNotHasSubApp(Site subapp);
void CheckHasSubApp(Site subapp);
void CheckNoSubApps();

protected:
// WebAppInstallManagerObserver:
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions chrome/test/data/webapps_integration/has_sub_apps/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

function doPromise(promise) {
console.log('promise: ', promise);
promise.then(
value => { console.log('promise resolved: ', JSON.stringify(value)); },
err => { console.log('promise rejected: ', JSON.stringify(err)); });
}

function callAdd() {
var arg = {};
var app_id = document.getElementById('subAppUrl').value;
arg[app_id] = {'install_url': app_id};
doPromise(navigator.subApps.add(arg));
}

function callRemove() {
arg = document.getElementById('subAppUrl').value;
doPromise(navigator.subApps.remove(arg));
}

function callList() {
navigator.subApps.list().then(
output => {
document.getElementById('listOutput').value =
JSON.stringify(output);
},
err => {
document.getElementById('listOutput').value =
"Error: " + JSON.stringify(err);
}
)
}
44 changes: 44 additions & 0 deletions chrome/test/data/webapps_integration/has_sub_apps/basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Site With Sub Apps</title>
<script src="/web_apps/test_utils.js"></script>
<script src="/webapps_integration/has_sub_apps/app.js"></script>
<script>
// If a "manifest=/path/to/manifest.json" query argument is provided to
// the URL accessing this page, that path is injected as the manifest tag.
// Otherwise, "basic.json" is used as the manifest tag.
addManifestLinkTag();
</script>
</head>
<body onload="startWorker('/webapps_integration/has_sub_apps/service_worker.js', { scope: '/webapps_integration/has_sub_apps/' })">
<h1>Site With Sub Apps</h1>
<div>
<p>
This site is used for dPWA integration tests, and is subject to modification to support that framework. See
<a
href="https://chromium.googlesource.com/chromium/src/+/main/docs/webapps/integration-testing-framework.md">https://chromium.googlesource.com/chromium/src/+/main/docs/webapps/integration-testing-framework.md</a>
</p>
<p>
These buttons below are for manual debugging, the framework is
calling the APIs-under-test directly.
</p>
<p>
Subapp to add/remove: <input id="subAppUrl" value="/sub_app_url"></input>
</p>
<p>
<button id="addButton" onclick="callAdd()">add()</button>
</p>
<p>
<button id="removeButton" onclick="callRemove()">remove()</button>
</p>
<p>
<button id="list" onclick="callList()">list()</button>
</p>
<p>
list() output:
<input id="listOutput" value="list wasn't called yet"></input>
</p>
</div>
</body>
</html>
28 changes: 28 additions & 0 deletions chrome/test/data/webapps_integration/has_sub_apps/basic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "Site With Sub Apps",
"icons": [
{
"src": "48x48-green.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "128x128-green.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "192x192-green.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "256x256-green.png",
"sizes": "256x256",
"type": "image/png"
}
],
"start_url": "/webapps_integration/has_sub_apps/basic.html",
"scope": "/webapps_integration/has_sub_apps/",
"display": "standalone"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request).catch(_ => {
return new Response('Offline test.');
}));
});
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ed8d404

Please sign in to comment.