/* * Copyright (C) 2018 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "TestWKWebView.h" #import "Utilities.h" #import #import #import #import #import #import #import #import #import #import #import #import @class DownloadProgressTestProtocol; enum class DownloadStartType { ConvertLoadToDownload, StartFromNavigationAction, StartInProcessPool, }; @interface DownloadProgressTestRunner : NSObject @property (nonatomic, readonly) _WKDownload *download; @property (nonatomic, readonly) NSProgress *progress; - (void)startLoadingWithProtocol:(DownloadProgressTestProtocol *)protocol; @end @interface DownloadProgressTestProtocol : NSURLProtocol @end @implementation DownloadProgressTestProtocol static DownloadProgressTestRunner *currentTestRunner; + (void)registerProtocolForTestRunner:(DownloadProgressTestRunner *)testRunner { currentTestRunner = testRunner; [NSURLProtocol registerClass:self]; [WKBrowsingContextController registerSchemeForCustomProtocol:@"http"]; } + (void)unregisterProtocol { currentTestRunner = nullptr; [WKBrowsingContextController unregisterSchemeForCustomProtocol:@"http"]; [NSURLProtocol unregisterClass:self]; } // MARK: NSURLProtocol Methods + (BOOL)canInitWithRequest:(NSURLRequest *)request { return [request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return NO; } - (void)startLoading { [currentTestRunner startLoadingWithProtocol:self]; } - (void)stopLoading { } @end static void* progressObservingContext = &progressObservingContext; @implementation DownloadProgressTestRunner { RetainPtr m_progressURL; RetainPtr m_webView; RetainPtr m_progressSubscriber; RetainPtr m_progress; RetainPtr<_WKDownload> m_download; RetainPtr m_protocol; BlockPtr m_unpublishingBlock; DownloadStartType m_startType; NSInteger m_expectedLength; bool m_hasProgress; bool m_lostProgress; bool m_downloadStarted; bool m_downloadDidCreateDestination; bool m_downloadFinished; bool m_downloadCanceled; bool m_downloadFailed; bool m_hasUpdatedCompletedUnitCount; } - (instancetype)init { self = [super init]; [DownloadProgressTestProtocol registerProtocolForTestRunner:self]; NSString *fileName = [NSString stringWithFormat:@"download-progress-%@", [NSUUID UUID].UUIDString]; m_progressURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName] isDirectory:NO]; currentTestRunner = self; m_unpublishingBlock = makeBlockPtr([self] { [self _didLoseProgress]; }).get(); return self; } - (_WKDownload *)download { return m_download.get(); } - (NSProgress *)progress { return m_progress.get(); } - (void)startLoadingWithProtocol:(DownloadProgressTestProtocol *)protocol { m_protocol = protocol; auto response = adoptNS([[NSURLResponse alloc] initWithURL:protocol.request.URL MIMEType:@"application/x-test-file" expectedContentLength:m_expectedLength textEncodingName:nullptr]); [m_protocol.get().client URLProtocol:m_protocol.get() didReceiveResponse:response.get() cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } - (void)tearDown { if (m_webView) { m_webView.get().configuration.processPool._downloadDelegate = nullptr; [m_webView.get() removeFromSuperview]; m_webView = nullptr; } if (m_progressSubscriber) { #if HAVE(NSPROGRESS_PUBLISHING_SPI) [NSProgress _removeSubscriber:m_progressSubscriber.get()]; #else [NSProgress removeSubscriber:m_progressSubscriber.get()]; #endif m_progressSubscriber = nullptr; } m_progress = nullptr; m_download = nullptr; m_protocol = nullptr; m_unpublishingBlock = nullptr; [DownloadProgressTestProtocol unregisterProtocol]; } - (void)_didGetProgress:(NSProgress *)progress { ASSERT(!m_progress); m_progress = progress; [progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:progressObservingContext]; m_hasProgress = true; } - (void)_didLoseProgress { ASSERT(m_progress); [m_progress.get() removeObserver:self forKeyPath:@"completedUnitCount"]; m_progress = nullptr; m_lostProgress = true; } - (void)subscribeAndWaitForProgress { if (!m_progressSubscriber) { auto publishingHandler = makeBlockPtr([weakSelf = WeakObjCPtr { self }](NSProgress *progress) { if (auto strongSelf = weakSelf.get()) { [strongSelf.get() _didGetProgress:progress]; return strongSelf->m_unpublishingBlock.get(); } return static_cast(nil); }); #if HAVE(NSPROGRESS_PUBLISHING_SPI) m_progressSubscriber = [NSProgress _addSubscriberForFileURL:m_progressURL.get() withPublishingHandler:publishingHandler.get()]; #else m_progressSubscriber = [NSProgress addSubscriberForFileURL:m_progressURL.get() withPublishingHandler:publishingHandler.get()]; #endif } TestWebKitAPI::Util::run(&m_hasProgress); } - (void)waitToLoseProgress { TestWebKitAPI::Util::run(&m_lostProgress); } - (void)startDownload:(DownloadStartType)startType expectedLength:(NSInteger)expectedLength { m_startType = startType; m_expectedLength = expectedLength; auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); m_webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]); m_webView.get().navigationDelegate = self; m_webView.get().configuration.processPool._downloadDelegate = self; auto request = adoptNS([[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://file"]]); switch (startType) { case DownloadStartType::ConvertLoadToDownload: case DownloadStartType::StartFromNavigationAction: [m_webView loadRequest:request.get()]; break; case DownloadStartType::StartInProcessPool: [m_webView.get().configuration.processPool _downloadURLRequest:request.get() websiteDataStore:[WKWebsiteDataStore defaultDataStore] originatingWebView:nullptr]; break; } TestWebKitAPI::Util::run(&m_downloadStarted); } - (void)publishProgress { ASSERT(m_download); [m_download.get() publishProgressAtURL:m_progressURL.get()]; } - (void)receiveData:(NSInteger)length { auto data = adoptNS([[NSMutableData alloc] init]); while (length-- > 0) { const char byte = 'A'; [data.get() appendBytes:static_cast(&byte) length:1]; } [m_protocol.get().client URLProtocol:m_protocol.get() didLoadData:data.get()]; } - (void)finishDownloadTask { [m_protocol.get().client URLProtocolDidFinishLoading:m_protocol.get()]; } - (void)failDownloadTask { [m_protocol.get().client URLProtocol:m_protocol.get() didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorNetworkConnectionLost userInfo:nullptr]]; } - (void)waitForDownloadDidCreateDestination { TestWebKitAPI::Util::run(&m_downloadDidCreateDestination); } - (void)waitForDownloadFinished { TestWebKitAPI::Util::run(&m_downloadFinished); } - (void)waitForDownloadCanceled { TestWebKitAPI::Util::run(&m_downloadCanceled); } - (void)waitForDownloadFailed { TestWebKitAPI::Util::run(&m_downloadFailed); } - (int64_t)waitForUpdatedCompletedUnitCount { TestWebKitAPI::Util::run(&m_hasUpdatedCompletedUnitCount); m_hasUpdatedCompletedUnitCount = false; return m_progress.get().completedUnitCount; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == progressObservingContext) { EXPECT_EQ(object, m_progress.get()); EXPECT_STREQ(keyPath.UTF8String, "completedUnitCount"); m_hasUpdatedCompletedUnitCount = true; } else [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } // MARK: Methods - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { if (m_startType == DownloadStartType::ConvertLoadToDownload) decisionHandler(WKNavigationResponsePolicyDownload); else decisionHandler(WKNavigationResponsePolicyAllow); } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { if (m_startType == DownloadStartType::StartFromNavigationAction) decisionHandler(_WKNavigationActionPolicyDownload); else decisionHandler(WKNavigationActionPolicyAllow); } // MARK: <_WKDownloadDelegate> Methods - (void)_downloadDidStart:(_WKDownload *)download { ASSERT(!m_downloadStarted); ASSERT(!m_download); m_download = download; m_downloadStarted = true; } - (void)_download:(_WKDownload *)download didCreateDestination:(NSString *)destination { EXPECT_EQ(download, m_download.get()); m_downloadDidCreateDestination = true; } - (void)_downloadDidFinish:(_WKDownload *)download { EXPECT_EQ(download, m_download.get()); m_downloadFinished = true; } - (void)_downloadDidCancel:(_WKDownload *)download { EXPECT_EQ(download, m_download.get()); m_downloadCanceled = true; } - (void)_download:(_WKDownload *)download didFailWithError:(NSError *)error { EXPECT_EQ(download, m_download.get()); m_downloadFailed = true; } - (void)_download:(_WKDownload *)download decideDestinationWithSuggestedFilename:(NSString *)filename completionHandler:(void (^)(BOOL allowOverwrite, NSString *destination))completionHandler { EXPECT_EQ(download, m_download.get()); FileSystem::PlatformFileHandle fileHandle; RetainPtr path = (NSString *)FileSystem::openTemporaryFile("TestWebKitAPI"_s, fileHandle); EXPECT_TRUE(fileHandle != FileSystem::invalidPlatformFileHandle); FileSystem::closeFile(fileHandle); completionHandler(YES, path.get()); } @end // End-to-end test of subscribing to progress on a successful download. The client // should be able to receive an NSProgress that is updated as the download makes // progress, and the NSProgress should be unpublished when the download finishes. TEST(DownloadProgress, BasicSubscriptionAndProgressUpdates) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:50]; EXPECT_EQ([testRunner.get() waitForUpdatedCompletedUnitCount], 50); EXPECT_EQ(testRunner.get().progress.fractionCompleted, .5); [testRunner.get() receiveData:50]; EXPECT_EQ([testRunner.get() waitForUpdatedCompletedUnitCount], 100); EXPECT_EQ(testRunner.get().progress.fractionCompleted, 1); [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // Similar test as before, but initiating the download before receiving its response. TEST(DownloadProgress, StartDownloadFromNavigationAction) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::StartFromNavigationAction expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:100]; [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } TEST(DownloadProgress, StartDownloadInProcessPool) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::StartInProcessPool expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:100]; [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // If the download is canceled, the progress should be unpublished. TEST(DownloadProgress, LoseProgressWhenDownloadIsCanceled) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:50]; [testRunner.get().download cancel]; [testRunner.get() waitForDownloadCanceled]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // If the download fails, the progress should be unpublished. TEST(DownloadProgress, LoseProgressWhenDownloadFails) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:50]; [testRunner.get() failDownloadTask]; [testRunner.get() waitForDownloadFailed]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // Canceling the progress should cancel the download. TEST(DownloadProgress, CancelDownloadWhenProgressIsCanceled) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:50]; [testRunner.get().progress cancel]; [testRunner.get() waitForDownloadFailed]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // Publishing progress on a download after it has finished should be a safe no-op. TEST(DownloadProgress, PublishProgressAfterDownloadFinished) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() receiveData:100]; [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() publishProgress]; [testRunner.get() tearDown]; } // Test the behavior of a download of unknown length. TEST(DownloadProgress, IndeterminateDownloadSize) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:NSURLResponseUnknownLength]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; EXPECT_EQ(testRunner.get().progress.totalUnitCount, -1); [testRunner.get() receiveData:50]; EXPECT_EQ([testRunner.get() waitForUpdatedCompletedUnitCount], 50); EXPECT_EQ(testRunner.get().progress.fractionCompleted, 0); EXPECT_EQ(testRunner.get().progress.totalUnitCount, -1); [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // Test the behavior when a download continues returning data beyond its expected length. TEST(DownloadProgress, ExtraData) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; [testRunner.get() receiveData:150]; EXPECT_EQ([testRunner.get() waitForUpdatedCompletedUnitCount], 150); EXPECT_EQ(testRunner.get().progress.fractionCompleted, 1.5); [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; } // Clients should be able to publish progress on a download that has already started. TEST(DownloadProgress, PublishProgressOnPartialDownload) { auto testRunner = adoptNS([[DownloadProgressTestRunner alloc] init]); [testRunner.get() startDownload:DownloadStartType::ConvertLoadToDownload expectedLength:100]; [testRunner.get() receiveData:50]; // Ensure that the the data task has become a download task so that we test // telling a live Download, not a PendingDownload, to publish its progress. [testRunner.get() waitForDownloadDidCreateDestination]; [testRunner.get() publishProgress]; [testRunner.get() subscribeAndWaitForProgress]; EXPECT_EQ(testRunner.get().progress.completedUnitCount, 50); [testRunner.get() receiveData:50]; EXPECT_EQ([testRunner.get() waitForUpdatedCompletedUnitCount], 100); [testRunner.get() finishDownloadTask]; [testRunner.get() waitForDownloadFinished]; [testRunner.get() waitToLoseProgress]; [testRunner.get() tearDown]; }