File: extensions_interactive_uitest.cc

package info (click to toggle)
chromium 138.0.7204.157-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,864 kB
  • sloc: cpp: 34,936,859; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,967; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (314 lines) | stat: -rw-r--r-- 14,313 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <string>
#include <string_view>
#include <tuple>

#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_keybinding_registry.h"
#include "chrome/browser/extensions/install_verifier.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/supervised_user/supervised_user_test_util.h"
#include "chrome/browser/ui/extensions/extensions_dialogs.h"
#include "chrome/browser/ui/supervised_user/parent_permission_dialog.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "chrome/test/supervised_user/browser_user.h"
#include "chrome/test/supervised_user/family_live_test.h"
#include "components/prefs/pref_service.h"
#include "components/supervised_user/core/common/pref_names.h"
#include "components/supervised_user/test_support/family_link_settings_state_management.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/interaction/element_identifier.h"

namespace supervised_user {
namespace {

static constexpr std::string_view kChromeManageExternsionsUrl =
    "chrome://extensions/";
static constexpr std::string_view kExtensionSiteSettingsUrl =
    "chrome://settings/content/siteDetails?site=chrome-extension://";
static constexpr std::string_view kExtensionName = "An Extension";

// TODO(b/321242366): Consider moving to helper class.
// Checks if a page title matches the given regexp in ecma script dialect.
InteractiveBrowserTestApi::StateChange PageWithMatchingTitle(
    std::string_view title_regexp) {
  DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kStateChange);
  InteractiveBrowserTestApi::StateChange state_change;
  state_change.type =
      InteractiveBrowserTestApi::StateChange::Type::kConditionTrue;
  state_change.event = kStateChange;
  state_change.test_function = base::StringPrintf(R"js(
    () => /%s/.test(document.title)
  )js",
                                                  title_regexp.data());
  state_change.continue_across_navigation = true;
  return state_change;
}

// Test the behavior of handling extensions for supervised users.
class SupervisedUserExtensionsParentalControlsUiTest
    : public InteractiveFamilyLiveTest,
      public testing::WithParamInterface<
          std::tuple<FamilyLiveTest::RpcMode,
                     /*permissions_switch_state=*/FamilyLinkToggleState,
                     /*extensions_switch_state=*/FamilyLinkToggleState>> {
 public:
  SupervisedUserExtensionsParentalControlsUiTest()
      : InteractiveFamilyLiveTest(GetRpcMode()) {}

 protected:
  // Child tries to enable a disabled extension (which is pending parent
  // approval) by clicking at the extension's toggle.
  // When the Extensions toggle is ON and used to manage the extensions,
  // the extension should be already enabled.
  // In that case the method only verifies the enabled state.
  auto ChildClicksEnableExtensionIfExtensionDisabled(
      ui::ElementIdentifier kChildTab,
      bool expected_extension_enabled) {
    return Steps(
        ExecuteJs(kChildTab, base::StringPrintf(
                                 R"js(
                () => {
                  const view_manager =
                    document.querySelector("extensions-manager").shadowRoot
                      .querySelector("#container").querySelector("#viewManager");
                  if (!view_manager) {
                    throw Error("Path to view_manager element is invalid.");
                  }
                  const container = view_manager.querySelector("#items-list")
                    .shadowRoot.querySelector("#container");
                  if (!container) {
                    throw Error("Path to container element is invalid.");
                  }
                  const count = container.querySelectorAll("extensions-item").length;
                  if (count !== 1) {
                    throw Error("Encountered unexpected number of extensions: " + count);
                  }
                  const extn = container.querySelectorAll("extensions-item")[0];
                  if (!extn) {
                    throw Error("Path to extension element is invalid.");
                  }
                  const toggle = extn.shadowRoot.querySelector("#enableToggle");
                  if (!toggle) {
                    throw Error("Path to extension toggle is invalid.");
                  }
                  if (toggle.ariaPressed != "%s") {
                    throw Error("Extension toggle in unexpected state: " + toggle.ariaPressed);
                  }
                  if (toggle.ariaPressed == "false") {
                    toggle.click();
                  }
                }
              )js",
                                 base::ToString(expected_extension_enabled))),
        Log("Child inspected extension toggle."));
  }

  // Installs programmatically (not through the UI) an extension for the given
  // user.
  void InstallExtension(Profile* profile) {
    std::string extension_manifest = base::StringPrintf(
        R"({
            "name": "%s",
            "manifest_version": 3,
            "version": "0.1",
            "host_permissions": ["<all_urls>"],
            "permissions": [ "geolocation" ]
          })",
        kExtensionName.data());
    extensions::TestExtensionDir extension_dir;
    extension_dir.WriteManifest(extension_manifest);

    extensions::ChromeTestExtensionLoader extension_loader(profile);
    extension_loader.set_ignore_manifest_warnings(true);
    extension_loader.LoadExtension(extension_dir.Pack());
  }

  ui::ElementIdentifier GetTargetUIElement() {
    CHECK(GetExtensionsSwitchTargetState() == FamilyLinkToggleState::kDisabled);
    // Parent approval dialog should appear.
    return ParentPermissionDialog::kDialogViewIdForTesting;
  }

  // Navigates to the `Settings` page for the installed extension under test
  // and inspects the permissions granted to the `Location` setting.
  // Checks if the `Locations` attribute is editable or not (html attribute
  // should be disabled), respecting the configuration of the "Permissions"
  // switch in Family Link.
  auto CheckExtensionLocationPermissions(ui::ElementIdentifier kChildElementId,
                                         Profile* profile) {
    extensions::ExtensionId installed_extension_id;
    const auto& installed_extensions =
        extensions::ExtensionRegistry::Get(profile)
            ->GenerateInstalledExtensionsSet();
    for (const auto& extension : installed_extensions) {
      if (extension->name() == kExtensionName) {
        installed_extension_id = extension->id();
        break;
      }
    }
    CHECK(installed_extension_id.size() > 0)
        << "There must be an installed extension.";

    // When the Permissions FL switch is Off, the Location permissions button
    // should be disabled (unmodifiable).
    bool permissions_button_greyed_out =
        GetPermissionsSwitchTargetState() == FamilyLinkToggleState::kDisabled;
    return Steps(
        Log("With installed extension : " + installed_extension_id),
        NavigateWebContents(kChildElementId,
                            GURL(std::string(kExtensionSiteSettingsUrl) +
                                 std::string(installed_extension_id))),
        WaitForStateChange(kChildElementId, PageWithMatchingTitle("Settings")),
        Log("With extension settings page open."),
        // Detect the Location permission and check whether it's user
        // modifiable.
        ExecuteJs(kChildElementId,
                  base::StringPrintf(
                      R"js(
          () => { const location_permission = document.querySelector("body > settings-ui")
                .shadowRoot.querySelector("#main")
                .shadowRoot.querySelector("settings-basic-page")
                .shadowRoot.querySelector("#basicPage > settings-section.expanded > settings-privacy-page")
                .shadowRoot.querySelector("#pages > settings-subpage > site-details")
                .shadowRoot.querySelector('[label="Location"]')
                .shadowRoot.querySelector("#permission");
                if (!location_permission) {
                  throw Error('No location permission menu was found.');
                }
                if (location_permission.disabled === "%s") {
                  throw Error('Unexpected Location Permission state: ' + permission_drop.disabled);
                }
              }
          )js",
                      base::ToString(permissions_button_greyed_out))),
        Log("Child inspected Location Permission button."));
  }

  auto CheckForParentDialogIfExtensionDisabled(
      bool is_expected_extension_enabled) {
    if (is_expected_extension_enabled) {
      // No dialog appears in this case.
      return Steps(Log("No dialog check is done, the extension is enabled."));
    }
    auto target_ui_element_id = GetTargetUIElement();
    return Steps(
        Log(base::StringPrintf("Waiting for the %s to appear.",
                               (target_ui_element_id ==
                                ParentPermissionDialog::kDialogViewIdForTesting)
                                   ? "parent approval dialog"
                                   : "blocked extension message")),
        WaitForShow(target_ui_element_id),
        Log(base::StringPrintf("The %s appears.",
                               (target_ui_element_id ==
                                ParentPermissionDialog::kDialogViewIdForTesting)
                                   ? "parent approval dialog"
                                   : "blocked extension message")));
  }

  static FamilyLiveTest::RpcMode GetRpcMode() {
    return std::get<0>(GetParam());
  }

  static FamilyLinkToggleState GetPermissionsSwitchTargetState() {
    return std::get<1>(GetParam());
  }

  static FamilyLinkToggleState GetExtensionsSwitchTargetState() {
    return std::get<2>(GetParam());
  }
};

IN_PROC_BROWSER_TEST_P(SupervisedUserExtensionsParentalControlsUiTest,
                       ChildTogglesExtensionMissingParentApproval) {
  extensions::ScopedInstallVerifierBypassForTest install_verifier_bypass;

  DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kChildElementId);
  DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(InIntendedStateObserver,
                                      kDefineStateObserverId);
  DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(InIntendedStateObserver,
                                      kResetStateObserverId);
  const int child_tab_index = 0;

  // The extensions should be disabled (pending parent approval) in all cases,
  // expect when the new "Extensions" FL switch is ON.
  const bool should_be_enabled =
      GetExtensionsSwitchTargetState() == FamilyLinkToggleState::kEnabled;

  TurnOnSync();

  // Set the FL switch in the value that require parent approvals for
  // extension installation.
  RunTestSequence(Log("Set config that requires parental approvals."),
                  WaitForStateSeeding(
                      kResetStateObserverId, child(),
                      FamilyLinkSettingsState::SetAdvancedSettingsDefault()));

  InstallExtension(&child().profile());

  RunTestSequence(InAnyContext(
      Log("Given an installed disabled extension."),
      // Parent sets both the FL Permissions and Extensions switches.
      // Only one of them impacts the handling of supervised user extensions.
      WaitForStateSeeding(
          kDefineStateObserverId, child(),
          FamilyLinkSettingsState::AdvancedSettingsToggles(
              {FamilyLinkToggleConfiguration(
                   {.type = FamilyLinkToggleType::kExtensionsToggle,
                    .state = GetExtensionsSwitchTargetState()}),
               FamilyLinkToggleConfiguration(
                   {.type = FamilyLinkToggleType::kPermissionsToggle,
                    .state = GetPermissionsSwitchTargetState()})})),
      // Child navigates to the extensions page and tries to enable the
      // extension, if it is disabled.
      Log("When child visits the extensions management page."),
      InstrumentTab(kChildElementId, child_tab_index, &child().browser()),
      NavigateWebContents(kChildElementId, GURL(kChromeManageExternsionsUrl)),
      WaitForStateChange(kChildElementId, PageWithMatchingTitle("Extensions")),
      Log("When child tries to enable the extension."),
      ChildClicksEnableExtensionIfExtensionDisabled(kChildElementId,
                                                    should_be_enabled),
      // If the extension is not already enabled, check that the expect UI
      // dialog appears.
      CheckForParentDialogIfExtensionDisabled(should_be_enabled),
      CheckExtensionLocationPermissions(kChildElementId, &child().profile())));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    SupervisedUserExtensionsParentalControlsUiTest,
    testing::Combine(
        testing::Values(FamilyLiveTest::RpcMode::kProd,
                        FamilyLiveTest::RpcMode::kTestImpersonation),
        /*permissions_switch_target_value=*/
        testing::Values(FamilyLinkToggleState::kEnabled,
                        FamilyLinkToggleState::kDisabled),
        /*extensions_switch_target_value==*/
        testing::Values(FamilyLinkToggleState::kEnabled,
                        FamilyLinkToggleState::kDisabled)),
    [](const auto& info) {
      return ToString(std::get<0>(info.param)) +
             std::string(
                 (std::get<1>(info.param) == FamilyLinkToggleState::kEnabled
                      ? "WithPermissionsOn"
                      : "WithPermissionsOff")) +
             std::string(
                 (std::get<2>(info.param) == FamilyLinkToggleState::kEnabled
                      ? "WithExtensionsOn"
                      : "WithExtensionsOff"));
    });

}  // namespace
}  // namespace supervised_user