blob: 346a162f71569715af58ef312f71233c01aba135 [file] [log] [blame]
// Copyright 2020 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 "content/browser/attribution_reporting/attribution_internals_ui.h"
#include <stdint.h>
#include <limits>
#include <utility>
#include <vector>
#include "base/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "content/browser/attribution_reporting/attribution_manager.h"
#include "content/browser/attribution_reporting/attribution_storage.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/event_attribution_report.h"
#include "content/browser/attribution_reporting/send_result.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace content {
namespace {
using CreateReportStatus =
::content::AttributionStorage::CreateReportResult::Status;
using DeactivatedSource = ::content::AttributionStorage::DeactivatedSource;
using ::testing::_;
using ::testing::IsNull;
using ::testing::Return;
const char kAttributionInternalsUrl[] = "chrome://conversion-internals/";
const std::u16string kCompleteTitle = u"Complete";
const std::u16string kCompleteTitle2 = u"Complete2";
const std::u16string kCompleteTitle3 = u"Complete3";
const std::u16string kMaxInt64String = u"9223372036854775807";
const std::u16string kMaxUint64String = u"18446744073709551615";
template <typename T>
auto InvokeCallback(T value) {
return [value = std::move(value)](base::OnceCallback<void(T)> callback) {
std::move(callback).Run(std::move(value));
};
}
} // namespace
class AttributionInternalsWebUiBrowserTest : public ContentBrowserTest {
public:
AttributionInternalsWebUiBrowserTest() {
ON_CALL(manager_, GetActiveSourcesForWebUI)
.WillByDefault(InvokeCallback<std::vector<StorableSource>>({}));
ON_CALL(manager_, GetPendingReportsForWebUI)
.WillByDefault(InvokeCallback<std::vector<EventAttributionReport>>({}));
}
void ClickRefreshButton() {
EXPECT_TRUE(ExecJsInWebUI("document.getElementById('refresh').click();"));
}
// Executing javascript in the WebUI requires using an isolated world in which
// to execute the script because WebUI has a default CSP policy denying
// "eval()", which is what EvalJs uses under the hood.
bool ExecJsInWebUI(const std::string& script) {
return ExecJs(shell()->web_contents()->GetMainFrame(), script,
EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1);
}
void OverrideWebUIAttributionManager() {
content::WebUI* web_ui = shell()->web_contents()->GetWebUI();
// Performs a safe downcast to the concrete AttributionInternalsUI subclass.
AttributionInternalsUI* attribution_internals_ui =
web_ui ? web_ui->GetController()->GetAs<AttributionInternalsUI>()
: nullptr;
EXPECT_TRUE(attribution_internals_ui);
attribution_internals_ui->SetAttributionManagerProviderForTesting(
std::make_unique<TestManagerProvider>(&manager_));
}
// Registers a mutation observer that sets the window title to |title| when
// the report table is empty.
void SetTitleOnReportsTableEmpty(const std::u16string& title) {
static constexpr char kObserveEmptyReportsTableScript[] = R"(
let table = document.querySelector("#report-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText === "No sent or pending reports.") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, title)));
}
protected:
// The manager must outlive the `AttributionInternalsHandler` so that the
// latter can remove itself as an observer of the former on the latter's
// destruction.
MockAttributionManager manager_;
};
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
NavigationUrl_ResolvedToWebUI) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
// Execute script to ensure the page has loaded correctly, executing similarly
// to ExecJsInWebUI().
EXPECT_EQ(true, EvalJs(shell()->web_contents()->GetMainFrame(),
"document.body.innerHTML.search('Attribution "
"Reporting API Internals') >= 0;",
EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1));
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_MeasurementConsideredEnabled) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("feature-status-content");
let obs = new MutationObserver(() => {
if (status.innerText.trim() === "enabled") {
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
DisabledByEmbedder_MeasurementConsideredDisabled) {
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(browser_client,
IsConversionMeasurementOperationAllowed(
_, ContentBrowserClient::ConversionMeasurementOperation::kAny,
IsNull(), IsNull(), IsNull()))
.WillRepeatedly(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("feature-status-content");
let obs = new MutationObserver(() => {
if (status.innerText.trim() === "disabled") {
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(
AttributionInternalsWebUiBrowserTest,
WebUIShownWithNoActiveImpression_NoImpressionsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
static constexpr char wait_script[] = R"(
let table = document.querySelector("#source-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText ===
"No sources.") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithActiveImpression_ImpressionsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
const base::Time now = base::Time::Now();
// We use the max values of `uint64_t` and `int64_t` here to ensure that they
// are properly handled as `bigint` values in JS and don't run into issues
// with `Number.MAX_SAFE_INTEGER`.
ON_CALL(manager_, GetActiveSourcesForWebUI)
.WillByDefault(InvokeCallback<std::vector<StorableSource>>(
{SourceBuilder(now)
.SetSourceEventId(std::numeric_limits<uint64_t>::max())
.SetAttributionLogic(StorableSource::AttributionLogic::kNever)
.Build(),
SourceBuilder(now + base::Hours(1))
.SetSourceType(StorableSource::SourceType::kEvent)
.SetPriority(std::numeric_limits<int64_t>::max())
.SetDedupKeys({13, 17})
.Build()}));
manager_.NotifySourceDeactivated(
DeactivatedSource(SourceBuilder(now + base::Hours(2)).Build(),
DeactivatedSource::Reason::kReplacedByNewerSource));
manager_.NotifySourceDeactivated(
DeactivatedSource(SourceBuilder(now + base::Hours(3)).Build(),
DeactivatedSource::Reason::kReachedAttributionLimit));
static constexpr char wait_script[] = R"(
let table = document.querySelector("#source-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 4 &&
table.children[0].children[0].innerText === $1 &&
table.children[0].children[6].innerText === "Navigation" &&
table.children[1].children[6].innerText === "Event" &&
table.children[0].children[7].innerText === "0" &&
table.children[1].children[7].innerText === $2 &&
table.children[0].children[8].innerText === "" &&
table.children[1].children[8].innerText === "13, 17" &&
table.children[0].children[9].innerText === "Unattributable: noised" &&
table.children[1].children[9].innerText === "Attributable" &&
table.children[2].children[9].innerText === "Unattributable: replaced by newer source" &&
table.children[3].children[9].innerText === "Unattributable: reached attribution limit") {
document.title = $3;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kMaxUint64String,
kMaxInt64String, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithNoReports_NoReportsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
SetTitleOnReportsTableEmpty(kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_DebugModeDisabled) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("debug-mode-content");
let obs = new MutationObserver(() => {
if (status.innerText.trim() === "") {
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_DebugModeEnabled) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kConversionsDebugMode);
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("debug-mode-content");
let obs = new MutationObserver(() => {
if (status.innerText.trim() !== "") {
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithPendingReports_ReportsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
OverrideWebUIAttributionManager();
manager_.NotifyReportSent(ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now + base::Hours(3))
.Build(),
SendResult(SendResult::Status::kSent,
/*http_response_code=*/200));
manager_.NotifyReportSent(ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now + base::Hours(4))
.SetPriority(-1)
.Build(),
SendResult(SendResult::Status::kDropped,
/*http_response_code=*/0));
manager_.NotifyReportSent(ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now + base::Hours(5))
.SetPriority(-2)
.Build(),
SendResult(SendResult::Status::kFailure,
/*http_response_code=*/0));
ON_CALL(manager_, GetPendingReportsForWebUI)
.WillByDefault(InvokeCallback<std::vector<EventAttributionReport>>(
{ReportBuilder(SourceBuilder(now)
.SetSourceType(StorableSource::SourceType::kEvent)
.SetAttributionLogic(
StorableSource::AttributionLogic::kFalsely)
.Build())
.SetReportTime(now)
.SetPriority(13)
.Build()}));
manager_.NotifyReportDropped(AttributionStorage::CreateReportResult(
CreateReportStatus::kPriorityTooLow,
ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now + base::Hours(1))
.SetPriority(11)
.Build()));
manager_.NotifyReportDropped(AttributionStorage::CreateReportResult(
CreateReportStatus::kDroppedForNoise,
ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now + base::Hours(2))
.SetPriority(12)
.Build()));
manager_.NotifyReportDropped(AttributionStorage::CreateReportResult(
CreateReportStatus::kRateLimited,
ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now + base::Hours(6))
.SetPriority(-3)
.Build()));
{
static constexpr char wait_script[] = R"(
let table = document.querySelector("#report-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 7 &&
table.children[0].children[1].innerText === "https://conversion.test" &&
table.children[0].children[2].innerText ===
"https://report.test/.well-known/attribution-reporting/report-attribution" &&
table.children[0].children[5].innerText === "13" &&
table.children[0].children[6].innerText === "yes" &&
table.children[0].children[7].innerText === "Pending" &&
table.children[1].children[5].innerText === "11" &&
table.children[1].children[7].innerText === "Dropped due to low priority" &&
table.children[2].children[5].innerText === "12" &&
table.children[2].children[7].innerText === "Dropped for noise" &&
table.children[3].children[5].innerText === "0" &&
table.children[3].children[6].innerText === "no" &&
table.children[3].children[7].innerText === "Sent: HTTP 200" &&
table.children[4].children[7].innerText === "Prohibited by browser policy" &&
table.children[5].children[7].innerText === "Network error" &&
table.children[6].children[7].innerText === "Dropped due to rate-limiting") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
{
static constexpr char wait_script[] = R"(
let table = document.querySelector("#report-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 7 &&
table.children[6].children[1].innerText === "https://conversion.test" &&
table.children[6].children[2].innerText ===
"https://report.test/.well-known/attribution-reporting/report-attribution" &&
table.children[6].children[5].innerText === "13" &&
table.children[6].children[6].innerText === "yes" &&
table.children[6].children[7].innerText === "Pending" &&
table.children[5].children[5].innerText === "12" &&
table.children[5].children[7].innerText === "Dropped for noise" &&
table.children[4].children[5].innerText === "11" &&
table.children[4].children[7].innerText === "Dropped due to low priority" &&
table.children[3].children[5].innerText === "0" &&
table.children[3].children[6].innerText === "no" &&
table.children[3].children[7].innerText === "Sent: HTTP 200" &&
table.children[2].children[7].innerText === "Prohibited by browser policy" &&
table.children[1].children[7].innerText === "Network error" &&
table.children[0].children[7].innerText === "Dropped due to rate-limiting") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle2)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle2);
// Sort by priority ascending.
EXPECT_TRUE(ExecJsInWebUI(
"document.querySelectorAll('#report-table-wrapper th')[5].click();"));
EXPECT_EQ(kCompleteTitle2, title_watcher.WaitAndGetTitle());
}
{
static constexpr char wait_script[] = R"(
let table = document.querySelector("#report-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 7 &&
table.children[0].children[1].innerText === "https://conversion.test" &&
table.children[0].children[2].innerText ===
"https://report.test/.well-known/attribution-reporting/report-attribution" &&
table.children[0].children[5].innerText === "13" &&
table.children[0].children[6].innerText === "yes" &&
table.children[0].children[7].innerText === "Pending" &&
table.children[1].children[5].innerText === "12" &&
table.children[1].children[7].innerText === "Dropped for noise" &&
table.children[2].children[5].innerText === "11" &&
table.children[2].children[7].innerText === "Dropped due to low priority" &&
table.children[3].children[5].innerText === "0" &&
table.children[3].children[6].innerText === "no" &&
table.children[3].children[7].innerText === "Sent: HTTP 200" &&
table.children[4].children[7].innerText === "Prohibited by browser policy" &&
table.children[5].children[7].innerText === "Network error" &&
table.children[6].children[7].innerText === "Dropped due to rate-limiting") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle3)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle3);
// Sort by priority descending.
EXPECT_TRUE(ExecJsInWebUI(
"document.querySelectorAll('#report-table-wrapper th')[5].click();"));
EXPECT_EQ(kCompleteTitle3, title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIWithPendingReportsClearStorage_ReportsRemoved) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
OverrideWebUIAttributionManager();
EventAttributionReport report = ReportBuilder(SourceBuilder(now).Build())
.SetReportTime(now)
.SetPriority(7)
.Build();
EXPECT_CALL(manager_, GetPendingReportsForWebUI)
.WillOnce(InvokeCallback<std::vector<EventAttributionReport>>({report}));
report.set_report_time(report.report_time() + base::Hours(1));
manager_.NotifyReportSent(report, SendResult(SendResult::Status::kSent,
/*http_response_code=*/200));
EXPECT_CALL(manager_, ClearData)
.WillOnce([](base::Time delete_begin, base::Time delete_end,
base::RepeatingCallback<bool(const url::Origin&)> filter,
base::OnceClosure done) { std::move(done).Run(); });
// Verify both rows get rendered.
static constexpr char wait_script[] = R"(
let table = document.querySelector("#report-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 2 &&
table.children[0].children[5].innerText === "7" &&
table.children[1].children[7].innerText === "Sent: HTTP 200") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the clear storage button and expect that the report table is emptied.
const std::u16string kDeleteTitle = u"Delete";
TitleWatcher delete_title_watcher(shell()->web_contents(), kDeleteTitle);
SetTitleOnReportsTableEmpty(kDeleteTitle);
// Click the button.
EXPECT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
ClearButton_ClearsSourceTable) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
OverrideWebUIAttributionManager();
base::Time now = base::Time::Now();
ON_CALL(manager_, GetActiveSourcesForWebUI)
.WillByDefault(InvokeCallback<std::vector<StorableSource>>(
{SourceBuilder(now).SetSourceEventId(5).Build()}));
manager_.NotifySourceDeactivated(DeactivatedSource(
SourceBuilder(now + base::Hours(2)).SetSourceEventId(6).Build(),
DeactivatedSource::Reason::kReplacedByNewerSource));
EXPECT_CALL(manager_, ClearData)
.WillOnce([](base::Time delete_begin, base::Time delete_end,
base::RepeatingCallback<bool(const url::Origin&)> filter,
base::OnceClosure done) { std::move(done).Run(); });
// Verify both rows get rendered.
static constexpr char wait_script[] = R"(
let table = document.querySelector("#source-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 2 &&
table.children[0].children[0].innerText === "5" &&
table.children[1].children[0].innerText === "6") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the clear storage button and expect that the source table is emptied.
const std::u16string kDeleteTitle = u"Delete";
TitleWatcher delete_title_watcher(shell()->web_contents(), kDeleteTitle);
static constexpr char kObserveEmptySourcesTableScript[] = R"(
let table = document.querySelector("#source-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText === "No sources.") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptySourcesTableScript, kDeleteTitle)));
// Click the button.
EXPECT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUISendReports_ReportsRemoved) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
EXPECT_CALL(manager_, GetPendingReportsForWebUI)
.WillOnce(InvokeCallback<std::vector<EventAttributionReport>>(
{ReportBuilder(SourceBuilder().Build()).SetPriority(7).Build()}))
.WillOnce(InvokeCallback<std::vector<EventAttributionReport>>({}));
EXPECT_CALL(manager_, SendReportsForWebUI)
.WillOnce([](base::OnceClosure done) { std::move(done).Run(); });
OverrideWebUIAttributionManager();
static constexpr char wait_script[] = R"(
let table = document.querySelector("#report-table-wrapper tbody");
let obs = new MutationObserver(() => {
if (table.children.length === 1 &&
table.children[0].children[5].innerText === "7") {
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the send reports button and expect that the report table is emptied.
const std::u16string kSentTitle = u"Sent";
TitleWatcher sent_title_watcher(shell()->web_contents(), kSentTitle);
SetTitleOnReportsTableEmpty(kSentTitle);
EXPECT_TRUE(
ExecJsInWebUI("document.getElementById('send-reports').click();"));
// The real manager would do this itself, but the test manager requires manual
// triggering.
manager_.NotifyReportsChanged();
EXPECT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
MojoJsBindingsCorrectlyScoped) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const std::u16string passed_title = u"passed";
{
TitleWatcher sent_title_watcher(shell()->web_contents(), passed_title);
EXPECT_TRUE(
ExecJsInWebUI("document.title = window.Mojo? 'passed' : 'failed';"));
EXPECT_EQ(passed_title, sent_title_watcher.WaitAndGetTitle());
}
EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
{
TitleWatcher sent_title_watcher(shell()->web_contents(), passed_title);
EXPECT_TRUE(
ExecJsInWebUI("document.title = window.Mojo? 'failed' : 'passed';"));
EXPECT_EQ(passed_title, sent_title_watcher.WaitAndGetTitle());
}
}
} // namespace content