blob: 8166dea71875dba4871fa7f84da43e42f3fadf47 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <utility>
#include <vector>
// spellcheck_per_process_browsertest.cc
#include "base/bind_helpers.h"
#include "base/feature_list.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/site_isolation/chrome_site_per_process_test.h"
#include "chrome/browser/spellchecker/spell_check_host_chrome_impl.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/common/spellcheck.mojom.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/browser_test.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#if BUILDFLAG(HAS_SPELLCHECK_PANEL)
#include "chrome/browser/spellchecker/test/spellcheck_panel_browsertest_helper.h"
#include "components/spellcheck/common/spellcheck_panel.mojom.h"
#include "mojo/public/cpp/bindings/remote.h"
#endif // BUILDFLAG(HAS_SPELLCHECK_PANEL)
// Class to sniff incoming spellcheck Mojo SpellCheckHost messages.
class MockSpellCheckHost : spellcheck::mojom::SpellCheckHost {
public:
explicit MockSpellCheckHost(content::RenderProcessHost* process_host)
: process_host_(process_host) {}
~MockSpellCheckHost() override {}
content::RenderProcessHost* process_host() const { return process_host_; }
const base::string16& text() const { return text_; }
bool HasReceivedText() const { return text_received_; }
void Wait() {
if (text_received_)
return;
base::RunLoop run_loop;
quit_ = run_loop.QuitClosure();
run_loop.Run();
}
void WaitUntilTimeout() {
if (text_received_)
return;
auto ui_task_runner = content::GetUIThreadTaskRunner({});
ui_task_runner->PostDelayedTask(
FROM_HERE,
base::BindOnce(&MockSpellCheckHost::Timeout, base::Unretained(this)),
base::TimeDelta::FromSeconds(1));
base::RunLoop run_loop;
quit_ = run_loop.QuitClosure();
run_loop.Run();
}
void SpellCheckHostReceiver(
mojo::PendingReceiver<spellcheck::mojom::SpellCheckHost> receiver) {
EXPECT_FALSE(receiver_.is_bound());
receiver_.Bind(std::move(receiver));
}
private:
void TextReceived(const base::string16& text) {
text_received_ = true;
text_ = text;
receiver_.reset();
if (quit_)
std::move(quit_).Run();
}
void Timeout() {
if (quit_)
std::move(quit_).Run();
}
// spellcheck::mojom::SpellCheckHost:
void RequestDictionary() override {}
void NotifyChecked(const base::string16& word, bool misspelled) override {}
#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
void CallSpellingService(const base::string16& text,
CallSpellingServiceCallback callback) override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::move(callback).Run(true, std::vector<SpellCheckResult>());
TextReceived(text);
}
#endif
#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
void RequestTextCheck(const base::string16& text,
int route_id,
RequestTextCheckCallback callback) override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::move(callback).Run(std::vector<SpellCheckResult>());
TextReceived(text);
}
void CheckSpelling(const base::string16& word,
int,
CheckSpellingCallback) override {}
void FillSuggestionList(const base::string16& word,
FillSuggestionListCallback) override {}
#if defined(OS_WIN)
void GetPerLanguageSuggestions(
const base::string16& word,
GetPerLanguageSuggestionsCallback callback) override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::move(callback).Run(std::vector<std::vector<base::string16>>());
}
void InitializeDictionaries(
InitializeDictionariesCallback callback) override {
if (base::FeatureList::IsEnabled(
spellcheck::kWinDelaySpellcheckServiceInit)) {
SpellcheckService* spellcheck = SpellcheckServiceFactory::GetForContext(
process_host()->GetBrowserContext());
if (!spellcheck) { // Teardown.
std::move(callback).Run(/*dictionaries=*/{}, /*custom_words=*/{},
/*enable=*/false);
return;
}
dictionaries_loaded_callback_ = std::move(callback);
spellcheck->InitializeDictionaries(
base::BindOnce(&MockSpellCheckHost::OnDictionariesInitialized,
base::Unretained(this)));
return;
}
NOTREACHED();
std::move(callback).Run(/*dictionaries=*/{}, /*custom_words=*/{},
/*enable=*/false);
}
void OnDictionariesInitialized() {
if (dictionaries_loaded_callback_) {
std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries;
dictionaries.push_back(spellcheck::mojom::SpellCheckBDictLanguage::New(
base::File(), "en-US"));
std::move(dictionaries_loaded_callback_)
.Run(std::move(dictionaries), {}, true);
}
}
// Callback passed as argument to InitializeDictionaries, and invoked when
// the dictionaries are loaded for the first time.
InitializeDictionariesCallback dictionaries_loaded_callback_;
#endif // defined(OS_WIN)
#endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#if defined(OS_ANDROID)
// spellcheck::mojom::SpellCheckHost:
void DisconnectSessionBridge() override {}
#endif
content::RenderProcessHost* process_host_;
bool text_received_ = false;
base::string16 text_;
mojo::Receiver<spellcheck::mojom::SpellCheckHost> receiver_{this};
base::OnceClosure quit_;
DISALLOW_COPY_AND_ASSIGN(MockSpellCheckHost);
};
class SpellCheckBrowserTestHelper {
public:
SpellCheckBrowserTestHelper() {
SpellCheckHostChromeImpl::OverrideBinderForTesting(
base::BindRepeating(&SpellCheckBrowserTestHelper::BindSpellCheckHost,
base::Unretained(this)));
}
~SpellCheckBrowserTestHelper() {
SpellCheckHostChromeImpl::OverrideBinderForTesting(base::NullCallback());
}
// Retrieves the registered MockSpellCheckHost for the given
// RenderProcessHost. It will return nullptr if the RenderProcessHost was
// initialized while a different instance of ContentBrowserClient was in
// action.
MockSpellCheckHost* GetSpellCheckHostForProcess(
content::RenderProcessHost* process_host) const {
for (auto& spell_check_host : spell_check_hosts_) {
if (spell_check_host->process_host() == process_host)
return spell_check_host.get();
}
return nullptr;
}
void RunUntilBind() {
if (!spell_check_hosts_.empty())
return;
base::RunLoop run_loop;
quit_on_bind_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
void RunUntilBindOrTimeout() {
if (!spell_check_hosts_.empty())
return;
auto ui_task_runner = content::GetUIThreadTaskRunner({});
ui_task_runner->PostDelayedTask(
FROM_HERE,
base::BindOnce(&SpellCheckBrowserTestHelper::Timeout,
base::Unretained(this)),
base::TimeDelta::FromSeconds(1));
base::RunLoop run_loop;
quit_on_bind_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
private:
void BindSpellCheckHost(
int render_process_id,
mojo::PendingReceiver<spellcheck::mojom::SpellCheckHost> receiver) {
content::RenderProcessHost* host =
content::RenderProcessHost::FromID(render_process_id);
auto spell_check_host = std::make_unique<MockSpellCheckHost>(host);
spell_check_host->SpellCheckHostReceiver(std::move(receiver));
spell_check_hosts_.push_back(std::move(spell_check_host));
if (quit_on_bind_closure_)
std::move(quit_on_bind_closure_).Run();
}
void Timeout() {
if (quit_on_bind_closure_)
std::move(quit_on_bind_closure_).Run();
}
base::OnceClosure quit_on_bind_closure_;
std::vector<std::unique_ptr<MockSpellCheckHost>> spell_check_hosts_;
DISALLOW_COPY_AND_ASSIGN(SpellCheckBrowserTestHelper);
};
class ChromeSitePerProcessSpellCheckTest : public ChromeSitePerProcessTest {
public:
ChromeSitePerProcessSpellCheckTest() = default;
void SetUp() override {
#if defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
// When delayed initialization of the spellcheck service is enabled by
// default, want to maintain test coverage for the older code path that
// initializes spellcheck on browser startup.
feature_list_.InitWithFeatures(
/*enabled_features=*/{spellcheck::kWinUseBrowserSpellChecker},
/*disabled_features=*/{spellcheck::kWinDelaySpellcheckServiceInit});
#endif // defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
ChromeSitePerProcessTest::SetUp();
}
protected:
// Tests that spelling in out-of-process subframes is checked.
// See crbug.com/638361 for details.
void RunOOPIFSpellCheckTest() {
SpellCheckBrowserTestHelper spell_check_helper;
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/page_with_contenteditable_in_cross_site_subframe.html"));
ui_test_utils::NavigateToURL(browser(), main_url);
spell_check_helper.RunUntilBind();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::RenderFrameHost* cross_site_subframe =
ChildFrameAt(web_contents->GetMainFrame(), 0);
MockSpellCheckHost* spell_check_host =
spell_check_helper.GetSpellCheckHostForProcess(
cross_site_subframe->GetProcess());
spell_check_host->Wait();
EXPECT_EQ(base::ASCIIToUTF16("zz."), spell_check_host->text());
}
// Tests that after disabling spellchecking, spelling in new out-of-process
// subframes is not checked. See crbug.com/789273 for details.
// https://crbug.com/944428
void RunOOPIFDisabledSpellCheckTest() {
SpellCheckBrowserTestHelper spell_check_helper;
content::BrowserContext* browser_context =
static_cast<content::BrowserContext*>(browser()->profile());
// Initiate a SpellcheckService
SpellcheckServiceFactory::GetForContext(browser_context);
// Disable spellcheck
PrefService* prefs = user_prefs::UserPrefs::Get(browser_context);
prefs->SetBoolean(spellcheck::prefs::kSpellCheckEnable, false);
base::RunLoop().RunUntilIdle();
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/page_with_contenteditable_in_cross_site_subframe.html"));
ui_test_utils::NavigateToURL(browser(), main_url);
spell_check_helper.RunUntilBindOrTimeout();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::RenderFrameHost* cross_site_subframe =
ChildFrameAt(web_contents->GetMainFrame(), 0);
MockSpellCheckHost* spell_check_host =
spell_check_helper.GetSpellCheckHostForProcess(
cross_site_subframe->GetProcess());
// The renderer makes no SpellCheckHostReceiver at all, in which case no
// SpellCheckHost is bound and no spellchecking will be done.
EXPECT_FALSE(spell_check_host);
prefs->SetBoolean(spellcheck::prefs::kSpellCheckEnable, true);
}
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessSpellCheckTest,
OOPIFSpellCheckTest) {
RunOOPIFSpellCheckTest();
}
IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessSpellCheckTest,
OOPIFDisabledSpellCheckTest) {
RunOOPIFDisabledSpellCheckTest();
}
#if BUILDFLAG(HAS_SPELLCHECK_PANEL)
// Tests that the OSX spell check panel can be opened from an out-of-process
// subframe, crbug.com/712395
#if defined(OS_MAC)
// https://crbug.com/1032617
#define MAYBE_OOPIFSpellCheckPanelTest DISABLED_OOPIFSpellCheckPanelTest
#else
#define MAYBE_OOPIFSpellCheckPanelTest OOPIFSpellCheckPanelTest
#endif
IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessSpellCheckTest,
MAYBE_OOPIFSpellCheckPanelTest) {
spellcheck::SpellCheckPanelBrowserTestHelper test_helper;
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/page_with_contenteditable_in_cross_site_subframe.html"));
ui_test_utils::NavigateToURL(browser(), main_url);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::RenderFrameHost* cross_site_subframe =
ChildFrameAt(web_contents->GetMainFrame(), 0);
EXPECT_TRUE(cross_site_subframe->IsCrossProcessSubframe());
mojo::Remote<spellcheck::mojom::SpellCheckPanel> spell_check_panel_client;
cross_site_subframe->GetRemoteInterfaces()->GetInterface(
spell_check_panel_client.BindNewPipeAndPassReceiver());
spell_check_panel_client->ToggleSpellPanel(false);
test_helper.RunUntilBind();
spellcheck::SpellCheckMockPanelHost* host =
test_helper.GetSpellCheckMockPanelHostForProcess(
cross_site_subframe->GetProcess());
EXPECT_TRUE(host->SpellingPanelVisible());
}
#endif // BUILDFLAG(HAS_SPELLCHECK_PANEL)
#if defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
class ChromeSitePerProcessSpellCheckTestDelayInit
: public ChromeSitePerProcessSpellCheckTest {
public:
ChromeSitePerProcessSpellCheckTestDelayInit() = default;
void SetUp() override {
// Don't initialize the SpellcheckService on browser launch.
feature_list_.InitWithFeatures(
/*enabled_features=*/{spellcheck::kWinUseBrowserSpellChecker,
spellcheck::kWinDelaySpellcheckServiceInit},
/*disabled_features=*/{});
ChromeSitePerProcessTest::SetUp();
}
};
IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessSpellCheckTestDelayInit,
OOPIFSpellCheckTest) {
RunOOPIFSpellCheckTest();
}
IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessSpellCheckTestDelayInit,
OOPIFDisabledSpellCheckTest) {
RunOOPIFDisabledSpellCheckTest();
}
#endif // defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)