1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/modules/webshare/navigator_share.h"
6 
7 #include <memory>
8 #include <utility>
9 #include "testing/gtest/include/gtest/gtest.h"
10 #include "third_party/blink/public/common/browser_interface_broker_proxy.h"
11 #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
12 #include "third_party/blink/renderer/bindings/core/v8/v8_file_property_bag.h"
13 #include "third_party/blink/renderer/bindings/modules/v8/v8_share_data.h"
14 #include "third_party/blink/renderer/core/fileapi/file.h"
15 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
16 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
17 #include "third_party/blink/renderer/core/frame/local_frame.h"
18 #include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
19 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
20 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
21 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
22 
23 namespace blink {
24 
25 using mojom::blink::SharedFile;
26 using mojom::blink::SharedFilePtr;
27 using mojom::blink::ShareService;
28 
29 // A mock ShareService used to intercept calls to the mojo methods.
30 class MockShareService : public ShareService {
31  public:
MockShareService()32   MockShareService() : error_(mojom::ShareError::OK) {}
33   ~MockShareService() override = default;
34 
Bind(mojo::ScopedMessagePipeHandle handle)35   void Bind(mojo::ScopedMessagePipeHandle handle) {
36     receiver_.Bind(
37         mojo::PendingReceiver<mojom::blink::ShareService>(std::move(handle)));
38   }
39 
set_error(mojom::ShareError value)40   void set_error(mojom::ShareError value) { error_ = value; }
41 
title() const42   const WTF::String& title() const { return title_; }
text() const43   const WTF::String& text() const { return text_; }
url() const44   const KURL& url() const { return url_; }
files() const45   const WTF::Vector<SharedFilePtr>& files() const { return files_; }
error() const46   mojom::ShareError error() const { return error_; }
47 
48  private:
Share(const WTF::String & title,const WTF::String & text,const KURL & url,WTF::Vector<SharedFilePtr> files,ShareCallback callback)49   void Share(const WTF::String& title,
50              const WTF::String& text,
51              const KURL& url,
52              WTF::Vector<SharedFilePtr> files,
53              ShareCallback callback) override {
54     title_ = title;
55     text_ = text;
56     url_ = url;
57 
58     files_.clear();
59     files_.ReserveInitialCapacity(files.size());
60     for (const auto& entry : files) {
61       files_.push_back(entry->Clone());
62     }
63 
64     std::move(callback).Run(error_);
65   }
66 
67   mojo::Receiver<ShareService> receiver_{this};
68   WTF::String title_;
69   WTF::String text_;
70   KURL url_;
71   WTF::Vector<SharedFilePtr> files_;
72   mojom::ShareError error_;
73 };
74 
75 class NavigatorShareTest : public testing::Test {
76  public:
NavigatorShareTest()77   NavigatorShareTest()
78       : holder_(std::make_unique<DummyPageHolder>()),
79         handle_scope_(GetScriptState()->GetIsolate()),
80         context_(GetScriptState()->GetContext()),
81         context_scope_(context_) {}
82 
GetDocument()83   Document& GetDocument() { return holder_->GetDocument(); }
84 
GetFrame()85   LocalFrame& GetFrame() { return holder_->GetFrame(); }
86 
GetScriptState() const87   ScriptState* GetScriptState() const {
88     return ToScriptStateForMainWorld(&holder_->GetFrame());
89   }
90 
Share(const ShareData & share_data)91   void Share(const ShareData& share_data) {
92     LocalFrame::NotifyUserActivation(&GetFrame());
93     Navigator* navigator = GetFrame().DomWindow()->navigator();
94     NonThrowableExceptionState exception_state;
95     ScriptPromise promise = NavigatorShare::share(GetScriptState(), *navigator,
96                                                   &share_data, exception_state);
97     test::RunPendingTasks();
98     EXPECT_EQ(mock_share_service_.error() == mojom::ShareError::OK
99                   ? v8::Promise::kFulfilled
100                   : v8::Promise::kRejected,
101               promise.V8Value().As<v8::Promise>()->State());
102   }
103 
mock_share_service()104   MockShareService& mock_share_service() { return mock_share_service_; }
105 
106  protected:
SetUp()107   void SetUp() override {
108     GetFrame().Loader().CommitNavigation(
109         WebNavigationParams::CreateWithHTMLBuffer(SharedBuffer::Create(),
110                                                   KURL("https://example.com")),
111         nullptr /* extra_data */);
112     test::RunPendingTasks();
113 
114     GetFrame().GetBrowserInterfaceBroker().SetBinderForTesting(
115         ShareService::Name_,
116         WTF::BindRepeating(&MockShareService::Bind,
117                            WTF::Unretained(&mock_share_service_)));
118   }
119 
TearDown()120   void TearDown() override {
121     // Remove the testing binder to avoid crashes between tests caused by
122     // MockShareService rebinding an already-bound Binding.
123     // See https://crbug.com/1010116 for more information.
124     GetFrame().GetBrowserInterfaceBroker().SetBinderForTesting(
125         ShareService::Name_, {});
126   }
127 
128  public:
129   MockShareService mock_share_service_;
130 
131   std::unique_ptr<DummyPageHolder> holder_;
132   v8::HandleScope handle_scope_;
133   v8::Local<v8::Context> context_;
134   v8::Context::Scope context_scope_;
135 };
136 
TEST_F(NavigatorShareTest,ShareText)137 TEST_F(NavigatorShareTest, ShareText) {
138   const String title = "Subject";
139   const String message = "Body";
140   const String url = "https://example.com/path?query#fragment";
141 
142   ShareData share_data;
143   share_data.setTitle(title);
144   share_data.setText(message);
145   share_data.setUrl(url);
146   Share(share_data);
147 
148   EXPECT_EQ(mock_share_service().title(), title);
149   EXPECT_EQ(mock_share_service().text(), message);
150   EXPECT_EQ(mock_share_service().url(), KURL(url));
151   EXPECT_EQ(mock_share_service().files().size(), 0U);
152   EXPECT_TRUE(
153       GetDocument().IsUseCounted(WebFeature::kWebShareSuccessfulWithoutFiles));
154 }
155 
CreateSampleFile(ExecutionContext * context,const String & file_name,const String & content_type,const String & file_contents)156 File* CreateSampleFile(ExecutionContext* context,
157                        const String& file_name,
158                        const String& content_type,
159                        const String& file_contents) {
160   HeapVector<ArrayBufferOrArrayBufferViewOrBlobOrUSVString> blob_parts;
161   blob_parts.push_back(ArrayBufferOrArrayBufferViewOrBlobOrUSVString());
162   blob_parts.back().SetUSVString(file_contents);
163 
164   FilePropertyBag file_property_bag;
165   file_property_bag.setType(content_type);
166   return File::Create(context, blob_parts, file_name, &file_property_bag);
167 }
168 
TEST_F(NavigatorShareTest,ShareFile)169 TEST_F(NavigatorShareTest, ShareFile) {
170   const String file_name = "chart.svg";
171   const String content_type = "image/svg+xml";
172   const String file_contents = "<svg></svg>";
173 
174   HeapVector<Member<File>> files;
175   files.push_back(CreateSampleFile(ExecutionContext::From(GetScriptState()),
176                                    file_name, content_type, file_contents));
177 
178   ShareData share_data;
179   share_data.setFiles(files);
180   Share(share_data);
181 
182   EXPECT_EQ(mock_share_service().files().size(), 1U);
183   EXPECT_EQ(mock_share_service().files()[0]->name, file_name);
184   EXPECT_EQ(mock_share_service().files()[0]->blob->GetType(), content_type);
185   EXPECT_EQ(mock_share_service().files()[0]->blob->size(),
186             file_contents.length());
187   EXPECT_TRUE(GetDocument().IsUseCounted(
188       WebFeature::kWebShareSuccessfulContainingFiles));
189 }
190 
TEST_F(NavigatorShareTest,CancelShare)191 TEST_F(NavigatorShareTest, CancelShare) {
192   const String title = "Subject";
193   ShareData share_data;
194   share_data.setTitle(title);
195 
196   mock_share_service().set_error(mojom::blink::ShareError::CANCELED);
197   Share(share_data);
198   EXPECT_TRUE(GetDocument().IsUseCounted(
199       WebFeature::kWebShareUnsuccessfulWithoutFiles));
200 }
201 
TEST_F(NavigatorShareTest,CancelShareWithFile)202 TEST_F(NavigatorShareTest, CancelShareWithFile) {
203   const String file_name = "counts.csv";
204   const String content_type = "text/csv";
205   const String file_contents = "1,2,3";
206 
207   HeapVector<Member<File>> files;
208   files.push_back(CreateSampleFile(ExecutionContext::From(GetScriptState()),
209                                    file_name, content_type, file_contents));
210 
211   ShareData share_data;
212   share_data.setFiles(files);
213 
214   mock_share_service().set_error(mojom::blink::ShareError::CANCELED);
215   Share(share_data);
216   EXPECT_TRUE(GetDocument().IsUseCounted(
217       WebFeature::kWebShareUnsuccessfulContainingFiles));
218 }
219 
220 }  // namespace blink
221