| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/thumbnails/thumbnail_image.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/test/task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_unittest_util.h" |
| |
| namespace { |
| |
| constexpr int kTestBitmapWidth = 200; |
| constexpr int kTestBitmapHeight = 123; |
| |
| class CallbackWaiter { |
| public: |
| CallbackWaiter() { |
| callback_ = base::BindRepeating(&CallbackWaiter::HandleCallback, |
| weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| base::RepeatingClosure callback() { return callback_; } |
| |
| bool called() const { return called_; } |
| |
| void Reset() { called_ = false; } |
| |
| void Wait() { |
| if (called_) { |
| return; |
| } |
| |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| private: |
| void HandleCallback() { |
| if (quit_closure_) { |
| std::move(quit_closure_).Run(); |
| } |
| called_ = true; |
| } |
| |
| base::RepeatingClosure callback_; |
| base::OnceClosure quit_closure_; |
| bool called_ = false; |
| |
| base::WeakPtrFactory<CallbackWaiter> weak_ptr_factory_{this}; |
| }; |
| |
| class StubDelegate : public ThumbnailImage::Delegate { |
| public: |
| StubDelegate() = default; |
| ~StubDelegate() override = default; |
| |
| // ThumbnailImage::Delegate: |
| void ThumbnailImageBeingObservedChanged(bool is_being_observed) override {} |
| }; |
| |
| } // anonymous namespace |
| |
| class ThumbnailImageTest : public testing::Test, |
| public ThumbnailImage::Delegate { |
| public: |
| ThumbnailImageTest() = default; |
| |
| ThumbnailImageTest(const ThumbnailImageTest&) = delete; |
| ThumbnailImageTest& operator=(const ThumbnailImageTest&) = delete; |
| |
| protected: |
| std::vector<uint8_t> Compress(SkBitmap bitmap) const { |
| return ThumbnailImage::CompressBitmap(bitmap, std::nullopt); |
| } |
| |
| bool is_being_observed() const { return is_being_observed_; } |
| |
| private: |
| void ThumbnailImageBeingObservedChanged(bool is_being_observed) override { |
| is_being_observed_ = is_being_observed; |
| } |
| |
| bool is_being_observed_ = false; |
| base::test::TaskEnvironment task_environment_; |
| }; |
| |
| using Subscription = ThumbnailImage::Subscription; |
| |
| TEST_F(ThumbnailImageTest, AddRemoveSubscriber) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| EXPECT_FALSE(is_being_observed()); |
| |
| std::unique_ptr<Subscription> subscription = image->Subscribe(); |
| EXPECT_TRUE(is_being_observed()); |
| |
| subscription.reset(); |
| EXPECT_FALSE(is_being_observed()); |
| } |
| |
| TEST_F(ThumbnailImageTest, AddRemoveMultipleObservers) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| EXPECT_FALSE(is_being_observed()); |
| |
| std::unique_ptr<Subscription> subscription1 = image->Subscribe(); |
| EXPECT_TRUE(is_being_observed()); |
| |
| std::unique_ptr<Subscription> subscription2 = image->Subscribe(); |
| EXPECT_TRUE(is_being_observed()); |
| |
| subscription1.reset(); |
| EXPECT_TRUE(is_being_observed()); |
| |
| subscription2.reset(); |
| EXPECT_FALSE(is_being_observed()); |
| } |
| |
| TEST_F(ThumbnailImageTest, AssignSkBitmapNotifiesObservers) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription1 = image->Subscribe(); |
| std::unique_ptr<Subscription> subscription2 = image->Subscribe(); |
| |
| CallbackWaiter waiter1; |
| subscription1->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(waiter1.callback())); |
| |
| CallbackWaiter waiter2; |
| subscription2->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(waiter2.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, AssignSkBitmap_NotifiesObserversAgain) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription1 = image->Subscribe(); |
| std::unique_ptr<Subscription> subscription2 = image->Subscribe(); |
| |
| CallbackWaiter waiter1; |
| subscription1->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(waiter1.callback())); |
| |
| CallbackWaiter waiter2; |
| subscription2->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(waiter2.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(bitmap, std::nullopt); |
| |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| |
| waiter1.Reset(); |
| waiter2.Reset(); |
| |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, AssignSkBitmap_NotifiesCompressedObservers) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription1 = image->Subscribe(); |
| std::unique_ptr<Subscription> subscription2 = image->Subscribe(); |
| |
| CallbackWaiter waiter1; |
| subscription1->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| waiter1.callback())); |
| |
| CallbackWaiter waiter2; |
| subscription2->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| waiter2.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, AssignSkBitmap_NotifiesCompressedObserversAgain) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription1 = image->Subscribe(); |
| std::unique_ptr<Subscription> subscription2 = image->Subscribe(); |
| |
| CallbackWaiter waiter1; |
| subscription1->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| waiter1.callback())); |
| |
| CallbackWaiter waiter2; |
| subscription2->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| waiter2.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(bitmap, std::nullopt); |
| |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| |
| waiter1.Reset(); |
| waiter2.Reset(); |
| |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, RequestThumbnailImage) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription1 = image->Subscribe(); |
| |
| CallbackWaiter waiter1; |
| subscription1->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(waiter1.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| waiter1.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| waiter1.Reset(); |
| |
| std::unique_ptr<Subscription> subscription2 = image->Subscribe(); |
| |
| CallbackWaiter waiter2; |
| subscription2->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(waiter2.callback())); |
| |
| image->RequestThumbnailImage(); |
| waiter1.Wait(); |
| waiter2.Wait(); |
| EXPECT_TRUE(waiter1.called()); |
| EXPECT_TRUE(waiter2.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, RequestCompressedThumbnailData) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription = image->Subscribe(); |
| |
| CallbackWaiter waiter; |
| subscription->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| waiter.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| waiter.Wait(); |
| EXPECT_TRUE(waiter.called()); |
| waiter.Reset(); |
| |
| image->RequestCompressedThumbnailData(); |
| waiter.Wait(); |
| EXPECT_TRUE(waiter.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, ClearThumbnailAfterAssignBitmap) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription = image->Subscribe(); |
| |
| CallbackWaiter uncompressed_image_waiter; |
| subscription->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(uncompressed_image_waiter.callback())); |
| |
| CallbackWaiter compressed_image_waiter; |
| subscription->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| compressed_image_waiter.callback())); |
| |
| CallbackWaiter async_operation_finished_waiter; |
| image->set_async_operation_finished_callback_for_testing( |
| async_operation_finished_waiter.callback()); |
| |
| // No observers should be notified if the thumbnail is cleared just |
| // after assigning a bitmap. |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| image->ClearData(); |
| async_operation_finished_waiter.Wait(); |
| EXPECT_TRUE(async_operation_finished_waiter.called()); |
| EXPECT_FALSE(uncompressed_image_waiter.called()); |
| EXPECT_FALSE(compressed_image_waiter.called()); |
| } |
| |
| TEST_F(ThumbnailImageTest, ClearExistingThumbnailNotifiesObservers) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription = image->Subscribe(); |
| |
| CallbackWaiter uncompressed_image_waiter; |
| subscription->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(uncompressed_image_waiter.callback())); |
| |
| CallbackWaiter compressed_image_waiter; |
| subscription->SetCompressedImageCallback( |
| base::IgnoreArgs<ThumbnailImage::CompressedThumbnailData>( |
| compressed_image_waiter.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| compressed_image_waiter.Wait(); |
| uncompressed_image_waiter.Wait(); |
| EXPECT_TRUE(compressed_image_waiter.called()); |
| EXPECT_TRUE(uncompressed_image_waiter.called()); |
| compressed_image_waiter.Reset(); |
| uncompressed_image_waiter.Reset(); |
| |
| image->ClearData(); |
| compressed_image_waiter.Wait(); |
| uncompressed_image_waiter.Wait(); |
| EXPECT_TRUE(compressed_image_waiter.called()); |
| EXPECT_TRUE(uncompressed_image_waiter.called()); |
| } |
| |
| // Makes sure a null dereference does not happen. Regression test for |
| // crbug.com/1159701. |
| TEST_F(ThumbnailImageTest, UnsubscribeAfterDelegateDestroyed) { |
| auto delegate = std::make_unique<StubDelegate>(); |
| auto image = base::MakeRefCounted<ThumbnailImage>(delegate.get()); |
| |
| std::unique_ptr<Subscription> subscription = image->Subscribe(); |
| |
| // Normally |image| will notify its delegate when the last |
| // subscription is destroyed. When there is no delegate it shouldn't |
| // do anything. |
| delegate.reset(); |
| subscription.reset(); |
| } |
| |
| // Ensures subscribers with a size hint get notified correctly on |
| // thumbnail clear. Regression test for crbug.com/1168483 where |
| // CropPreviewImage was called on blank thumbnails resulting in a |
| // DCHECK. |
| TEST_F(ThumbnailImageTest, DoesNotCropBlankThumbnails) { |
| auto image = base::MakeRefCounted<ThumbnailImage>(this); |
| |
| std::unique_ptr<Subscription> subscription = image->Subscribe(); |
| subscription->SetSizeHint( |
| gfx::Size(kTestBitmapWidth / 2, kTestBitmapHeight / 2)); |
| |
| CallbackWaiter uncompressed_image_waiter; |
| subscription->SetUncompressedImageCallback( |
| base::IgnoreArgs<gfx::ImageSkia>(uncompressed_image_waiter.callback())); |
| |
| SkBitmap bitmap = |
| gfx::test::CreateBitmap(kTestBitmapWidth, kTestBitmapHeight); |
| image->AssignSkBitmap(std::move(bitmap), std::nullopt); |
| uncompressed_image_waiter.Wait(); |
| EXPECT_TRUE(uncompressed_image_waiter.called()); |
| uncompressed_image_waiter.Reset(); |
| |
| image->ClearData(); |
| uncompressed_image_waiter.Wait(); |
| EXPECT_TRUE(uncompressed_image_waiter.called()); |
| } |