blob: deccf337df9a561aeb42174b0d21eb82951bcffc [file] [log] [blame]
/*
* 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 <Foundation/NSProgress.h>
#import <WebKit/WKBrowsingContextController.h>
#import <WebKit/WKNavigationDelegatePrivate.h>
#import <WebKit/WKProcessPoolPrivate.h>
#import <WebKit/WKWebViewConfiguration.h>
#import <WebKit/_WKDownload.h>
#import <WebKit/_WKDownloadDelegate.h>
#import <pal/spi/cocoa/NSProgressSPI.h>
#import <wtf/BlockPtr.h>
#import <wtf/FileSystem.h>
#import <wtf/RetainPtr.h>
#import <wtf/WeakObjCPtr.h>
@class DownloadProgressTestProtocol;
enum class DownloadStartType {
ConvertLoadToDownload,
StartFromNavigationAction,
StartInProcessPool,
};
@interface DownloadProgressTestRunner : NSObject <WKNavigationDelegate, _WKDownloadDelegate>
@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<NSURL> m_progressURL;
RetainPtr<TestWKWebView> m_webView;
RetainPtr<id> m_progressSubscriber;
RetainPtr<NSProgress> m_progress;
RetainPtr<_WKDownload> m_download;
RetainPtr<DownloadProgressTestProtocol> m_protocol;
BlockPtr<void(void)> 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<DownloadProgressTestRunner> { self }](NSProgress *progress) {
if (auto strongSelf = weakSelf.get()) {
[strongSelf.get() _didGetProgress:progress];
return strongSelf->m_unpublishingBlock.get();
}
return static_cast<NSProgressUnpublishingHandler>(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<const void*>(&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<NSKeyValueChangeKey, id> *)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: <WKNavigationDelegate> 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<NSString> 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];
}