blob: 4356549c7ab9d4ead89cb8f0f4c82000873ebea9 [file] [log] [blame]
// Copyright 2017 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 "third_party/blink/renderer/core/html/media/html_media_element.h"
#include <algorithm>
#include <memory>
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/web_fullscreen_video_status.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/html/media/media_controls.h"
#include "third_party/blink/renderer/core/html/media/media_custom_controls_fullscreen_detector.h"
#include "third_party/blink/renderer/core/html/track/text_track.h"
#include "third_party/blink/renderer/core/html/track/text_track_cue_list.h"
#include "third_party/blink/renderer/core/html/track/vtt/vtt_cue.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/bindings/microtask.h"
#include "third_party/blink/renderer/platform/testing/empty_web_media_player.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
namespace blink {
namespace {
// Most methods are faked rather than mocked. Faking avoids naggy warnings
// about unexpected calls. HTMLMediaElement <-> WebMediaplayer interface is
// highly complex and not generally the focus these tests (with the
// exception of the mocked methods).
class FakeWebMediaPlayer final : public EmptyWebMediaPlayer {
public:
FakeWebMediaPlayer(WebMediaPlayerClient* client,
ExecutionContext* context,
double duration)
: client_(client), context_(context), duration_(duration) {}
MOCK_METHOD1(SetIsEffectivelyFullscreen,
void(blink::WebFullscreenVideoStatus));
double CurrentTime() const override {
return current_time_;
}
// Establish a large so tests can attempt seeking.
double Duration() const override { return duration_; }
WebTimeRanges Seekable() const override {
WebTimeRange single_range[] = {WebTimeRange(0, Duration())};
return WebTimeRanges(single_range, 1);
}
void Seek(double seconds) override { last_seek_time_ = seconds; }
void Play() override {
playing_ = true;
ScheduleTimeIncrement();
}
void Pause() override { playing_ = false; }
bool Paused() const override { return !playing_; }
bool IsEnded() const override { return current_time_ == duration_; }
void FinishSeek() {
ASSERT_GE(last_seek_time_, 0);
current_time_ = last_seek_time_;
last_seek_time_ = -1;
client_->TimeChanged();
if (playing_)
ScheduleTimeIncrement();
}
void SetAutoIncrementTimeDelta(base::Optional<base::TimeDelta> delta) {
auto_time_increment_delta_ = delta;
ScheduleTimeIncrement();
}
private:
void ScheduleTimeIncrement() {
if (scheduled_time_increment_) {
return;
}
if (!auto_time_increment_delta_.has_value()) {
return;
}
context_->GetTaskRunner(TaskType::kInternalMediaRealTime)
->PostDelayedTask(FROM_HERE,
base::BindOnce(&FakeWebMediaPlayer::AutoTimeIncrement,
base::Unretained(this),
auto_time_increment_delta_.value()),
auto_time_increment_delta_.value());
scheduled_time_increment_ = true;
}
void AutoTimeIncrement(base::TimeDelta time_delta) {
// If time increments have been disabled since posting the task, bail out
if (!auto_time_increment_delta_.has_value() || !playing_) {
return;
}
scheduled_time_increment_ = false;
current_time_ += time_delta.InSecondsF();
// Notify the client if we've reached the end of the set duration
if (current_time_ >= duration_) {
current_time_ = duration_;
client_->TimeChanged();
} else {
ScheduleTimeIncrement();
}
// Run V8 Microtasks (update OfficialPlaybackPosition)
Microtask::PerformCheckpoint(context_->GetIsolate());
}
WebMediaPlayerClient* client_;
WeakPersistent<ExecutionContext> context_;
mutable double current_time_ = 0;
bool playing_ = false;
base::Optional<base::TimeDelta> auto_time_increment_delta_ =
base::TimeDelta::FromMilliseconds(33);
bool scheduled_time_increment_ = false;
double last_seek_time_ = -1;
const double duration_;
};
class MediaStubLocalFrameClient : public EmptyLocalFrameClient {
public:
std::unique_ptr<WebMediaPlayer> CreateWebMediaPlayer(
HTMLMediaElement& element,
const WebMediaPlayerSource&,
WebMediaPlayerClient* client) override {
return std::make_unique<FakeWebMediaPlayer>(
client, element.GetExecutionContext(), media_duration_);
}
void SetMediaDuration(double media_duration) {
media_duration_ = media_duration;
}
private:
double media_duration_ = 1000000;
};
using testing::_;
using testing::AtLeast;
using testing::Return;
} // anonymous namespace
class HTMLMediaElementEventListenersTest : public PageTestBase {
protected:
void SetUp() override {
SetupPageWithClients(nullptr,
MakeGarbageCollected<MediaStubLocalFrameClient>());
}
void DestroyDocument() { PageTestBase::TearDown(); }
HTMLVideoElement* Video() {
return To<HTMLVideoElement>(GetDocument().QuerySelector("video"));
}
FakeWebMediaPlayer* WebMediaPlayer() {
return static_cast<FakeWebMediaPlayer*>(Video()->GetWebMediaPlayer());
}
MediaStubLocalFrameClient* LocalFrameClient() {
return static_cast<MediaStubLocalFrameClient*>(GetFrame().Client());
}
void SetMediaDuration(double duration) {
LocalFrameClient()->SetMediaDuration(duration);
}
MediaControls* Controls() { return Video()->GetMediaControls(); }
void SimulateReadyState(HTMLMediaElement::ReadyState state) {
Video()->SetReadyState(state);
}
void SimulateNetworkState(HTMLMediaElement::NetworkState state) {
Video()->SetNetworkState(state);
}
MediaCustomControlsFullscreenDetector* FullscreenDetector() {
return Video()->custom_controls_fullscreen_detector_;
}
};
TEST_F(HTMLMediaElementEventListenersTest, RemovingFromDocumentCollectsAll) {
EXPECT_EQ(Video(), nullptr);
GetDocument().body()->setInnerHTML("<video controls></video>");
EXPECT_NE(Video(), nullptr);
EXPECT_TRUE(Video()->HasEventListeners());
EXPECT_NE(Controls(), nullptr);
EXPECT_TRUE(GetDocument().HasEventListeners());
WeakPersistent<HTMLVideoElement> weak_persistent_video = Video();
WeakPersistent<MediaControls> weak_persistent_controls = Controls();
{
Persistent<HTMLVideoElement> persistent_video = Video();
GetDocument().body()->setInnerHTML("");
// When removed from the document, the event listeners should have been
// dropped.
EXPECT_FALSE(GetDocument().HasEventListeners());
// The video element should still have some event listeners.
EXPECT_TRUE(persistent_video->HasEventListeners());
}
test::RunPendingTasks();
ThreadState::Current()->CollectAllGarbageForTesting();
// They have been GC'd.
EXPECT_EQ(weak_persistent_video, nullptr);
EXPECT_EQ(weak_persistent_controls, nullptr);
}
TEST_F(HTMLMediaElementEventListenersTest,
ReInsertingInDocumentCollectsControls) {
EXPECT_EQ(Video(), nullptr);
GetDocument().body()->setInnerHTML("<video controls></video>");
EXPECT_NE(Video(), nullptr);
EXPECT_TRUE(Video()->HasEventListeners());
EXPECT_NE(Controls(), nullptr);
EXPECT_TRUE(GetDocument().HasEventListeners());
// This should be a no-op. We keep a reference on the VideoElement to avoid an
// unexpected GC.
{
Persistent<HTMLVideoElement> video_holder = Video();
GetDocument().body()->RemoveChild(Video());
GetDocument().body()->AppendChild(video_holder.Get());
}
EXPECT_TRUE(GetDocument().HasEventListeners());
EXPECT_TRUE(Video()->HasEventListeners());
test::RunPendingTasks();
ThreadState::Current()->CollectAllGarbageForTesting();
EXPECT_NE(Video(), nullptr);
EXPECT_NE(Controls(), nullptr);
EXPECT_EQ(Controls(), Video()->GetMediaControls());
}
TEST_F(HTMLMediaElementEventListenersTest,
FullscreenDetectorTimerCancelledOnContextDestroy) {
EXPECT_EQ(Video(), nullptr);
GetDocument().body()->setInnerHTML("<video></video>");
Video()->SetSrc("http://example.com");
test::RunPendingTasks();
EXPECT_NE(WebMediaPlayer(), nullptr);
// Set ReadyState as HaveMetadata and go fullscreen, so the timer is fired.
EXPECT_NE(Video(), nullptr);
SimulateReadyState(HTMLMediaElement::kHaveMetadata);
LocalFrame::NotifyUserActivation(
GetDocument().GetFrame(), mojom::UserActivationNotificationType::kTest);
Fullscreen::RequestFullscreen(*Video());
Fullscreen::DidResolveEnterFullscreenRequest(GetDocument(),
true /* granted */);
test::RunPendingTasks();
Persistent<Document> persistent_document = &GetDocument();
Persistent<MediaCustomControlsFullscreenDetector> detector =
FullscreenDetector();
Vector<blink::WebFullscreenVideoStatus> observed_results;
ON_CALL(*WebMediaPlayer(), SetIsEffectivelyFullscreen(_))
.WillByDefault(testing::Invoke(
[&](blink::WebFullscreenVideoStatus fullscreen_video_status) {
observed_results.push_back(fullscreen_video_status);
}));
DestroyDocument();
test::RunPendingTasks();
// Document should not have listeners as the ExecutionContext is destroyed.
EXPECT_FALSE(persistent_document->HasEventListeners());
// Should only notify the kNotEffectivelyFullscreen value when
// ExecutionContext is destroyed.
EXPECT_EQ(1u, observed_results.size());
EXPECT_EQ(blink::WebFullscreenVideoStatus::kNotEffectivelyFullscreen,
observed_results[0]);
}
class MockEventListener final : public NativeEventListener {
public:
MOCK_METHOD2(Invoke, void(ExecutionContext* executionContext, Event*));
};
class HTMLMediaElementWithMockSchedulerTest
: public HTMLMediaElementEventListenersTest {
protected:
void SetUp() override {
EnablePlatform();
// We want total control over when to advance the clock. This also allows
// us to call platform()->RunUntilIdle() to run all pending tasks without
// fear of looping forever.
platform()->SetAutoAdvanceNowToPendingTasks(false);
// DocumentParserTiming has DCHECKS to make sure time > 0.0.
platform()->AdvanceClockSeconds(1);
HTMLMediaElementEventListenersTest::SetUp();
}
};
TEST_F(HTMLMediaElementWithMockSchedulerTest, OneTimeupdatePerSeek) {
testing::InSequence dummy;
GetDocument().body()->setInnerHTML("<video></video>");
// Set a src to trigger WebMediaPlayer creation.
Video()->SetSrc("http://example.com");
platform()->RunUntilIdle();
ASSERT_NE(WebMediaPlayer(), nullptr);
auto* timeupdate_handler = MakeGarbageCollected<MockEventListener>();
Video()->addEventListener(event_type_names::kTimeupdate, timeupdate_handler);
// Simulate conditions where playback is possible.
SimulateNetworkState(HTMLMediaElement::kNetworkIdle);
SimulateReadyState(HTMLMediaElement::kHaveFutureData);
// Simulate advancing playback time.
WebMediaPlayer()->SetAutoIncrementTimeDelta(
base::TimeDelta::FromMilliseconds(33));
Video()->Play();
// While playing, timeupdate should fire every 250 ms -> 4x per second as long
// as media player's CurrentTime continues to advance.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(4);
platform()->RunForPeriodSeconds(1);
// If media playback time is fixed, periodic timeupdate's should not continue
// to fire.
WebMediaPlayer()->SetAutoIncrementTimeDelta(base::nullopt);
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(0);
platform()->RunForPeriodSeconds(1);
// Per spec, pausing should fire `timeupdate`
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
Video()->pause();
platform()->RunUntilIdle();
// Seek to some time in the past. A completed seek while paused should trigger
// a *single* timeupdate.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
// The WebMediaPlayer current time should have progressed to almost 1 second
// (Actually 0.99 due to |kFakeMediaPlayerAutoIncrementTimeDelta|).
ASSERT_GE(WebMediaPlayer()->CurrentTime(), 0.95);
Video()->setCurrentTime(0.5);
// Fake the callback from WebMediaPlayer to complete the seek.
WebMediaPlayer()->FinishSeek();
// Give the scheduled timeupdate a chance to fire.
platform()->RunUntilIdle();
}
TEST_F(HTMLMediaElementWithMockSchedulerTest, PeriodicTimeupdateAfterSeek) {
testing::InSequence dummy;
GetDocument().body()->setInnerHTML("<video></video>");
// Set a src to trigger WebMediaPlayer creation.
Video()->SetSrc("http://example.com");
platform()->RunUntilIdle();
EXPECT_NE(WebMediaPlayer(), nullptr);
auto* timeupdate_handler = MakeGarbageCollected<MockEventListener>();
Video()->addEventListener(event_type_names::kTimeupdate, timeupdate_handler);
// Simulate conditions where playback is possible.
SimulateNetworkState(HTMLMediaElement::kNetworkIdle);
SimulateReadyState(HTMLMediaElement::kHaveFutureData);
// Simulate advancing playback time to enable periodic timeupdates.
WebMediaPlayer()->SetAutoIncrementTimeDelta(
base::TimeDelta::FromMilliseconds(8));
Video()->Play();
// Advance a full periodic timeupdate interval (250 ms) and expect a single
// timeupdate.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
platform()->RunForPeriodSeconds(.250);
// The event is scheduled, but needs one more scheduler cycle to fire.
platform()->RunUntilIdle();
// Now advance 125 ms to reach the middle of the periodic timeupdate interval.
// no additional timeupdate should trigger.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(0);
platform()->RunForPeriodSeconds(.125);
platform()->RunUntilIdle();
// While still in the middle of the periodic timeupdate interval, start and
// complete a seek and verify that a *non-periodic* timeupdate is fired.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
ASSERT_GE(WebMediaPlayer()->CurrentTime(), 0.3);
Video()->setCurrentTime(0.2);
WebMediaPlayer()->FinishSeek();
// Expect another timeupdate after FinishSeek due to
// seeking -> begin scrubbing -> pause -> timeupdate.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
platform()->RunUntilIdle();
// Advancing the remainder of the last periodic timeupdate interval should be
// insufficient to trigger a new timeupdate event because the seek's
// timeupdate occurred only 125ms ago. We desire to fire periodic timeupdates
// exactly every 250ms from the last timeupdate, and the seek's timeupdate
// should reset that 250ms ms countdown.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(0);
platform()->RunForPeriodSeconds(.125);
platform()->RunUntilIdle();
// Advancing another 125ms, we should expect a new timeupdate because we are
// now 250ms from the seek's timeupdate.
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
platform()->RunForPeriodSeconds(.125);
platform()->RunUntilIdle();
// Advancing 250ms further, we should expect yet another timeupdate because
// this represents a full periodic timeupdate interval with no interruptions
// (e.g. no-seeks).
EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1);
platform()->RunForPeriodSeconds(.250);
platform()->RunUntilIdle();
}
TEST_F(HTMLMediaElementWithMockSchedulerTest, ShowPosterFlag_FalseAfterLoop) {
testing::InSequence dummy;
// Adjust the duration of the media to something we can reasonably loop
SetMediaDuration(10.0);
// Create a looping video with a source
GetDocument().body()->setInnerHTML(
"<video loop src=\"http://example.com\"></video>");
platform()->RunUntilIdle();
EXPECT_NE(WebMediaPlayer(), nullptr);
EXPECT_EQ(WebMediaPlayer()->Duration(), 10.0);
EXPECT_TRUE(Video()->Loop());
SimulateNetworkState(HTMLMediaElement::kNetworkIdle);
SimulateReadyState(HTMLMediaElement::kHaveEnoughData);
// Simulate advancing playback time to enable periodic timeupdates.
WebMediaPlayer()->SetAutoIncrementTimeDelta(
base::TimeDelta::FromMilliseconds(8));
Video()->Play();
// Ensure the 'seeking' and 'seeked' events are fired, so we know a loop
// occurred
auto* seeking_handler = MakeGarbageCollected<MockEventListener>();
EXPECT_CALL(*seeking_handler, Invoke(_, _)).Times(1);
Video()->addEventListener(event_type_names::kSeeking, seeking_handler);
platform()->RunForPeriodSeconds(15);
testing::Mock::VerifyAndClearExpectations(seeking_handler);
auto* seeked_handler = MakeGarbageCollected<MockEventListener>();
EXPECT_CALL(*seeked_handler, Invoke(_, _)).Times(1);
Video()->addEventListener(event_type_names::kSeeked, seeked_handler);
WebMediaPlayer()->FinishSeek();
platform()->RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(seeked_handler);
// ShowPosterFlag should be false after looping
EXPECT_FALSE(Video()->IsShowPosterFlagSet());
}
TEST_F(HTMLMediaElementWithMockSchedulerTest, ShowPosterFlag_FalseAfterEnded) {
testing::InSequence dummy;
// Adjust the duration of the media to something we can reach the end of
SetMediaDuration(10.0);
// Create a video with a source
GetDocument().body()->setInnerHTML(
"<video src=\"http://example.com\"></video>");
platform()->RunUntilIdle();
EXPECT_NE(WebMediaPlayer(), nullptr);
EXPECT_EQ(WebMediaPlayer()->Duration(), 10.0);
SimulateNetworkState(HTMLMediaElement::kNetworkIdle);
SimulateReadyState(HTMLMediaElement::kHaveEnoughData);
// Simulate advancing playback time to enable periodic timeupdates.
WebMediaPlayer()->SetAutoIncrementTimeDelta(
base::TimeDelta::FromMilliseconds(8));
Video()->Play();
// Ensure the 'ended' event is fired
auto* ended_handler = MakeGarbageCollected<MockEventListener>();
Video()->addEventListener(event_type_names::kEnded, ended_handler);
EXPECT_CALL(*ended_handler, Invoke(_, _)).Times(1);
platform()->RunForPeriodSeconds(15);
testing::Mock::VerifyAndClearExpectations(ended_handler);
// ShowPosterFlag should be false even after ending
EXPECT_FALSE(Video()->IsShowPosterFlagSet());
}
struct TestCue {
double start_time;
double end_time;
char const* text;
};
constexpr TestCue kTestCueData[] = {
{15.000, 17.950, "At the left we can see..."},
{18.160, 20.080, "At the right we can see the..."},
{20.110, 21.960, "...the head-snarlers"},
{21.990, 24.360, "Everything is safe.\nPerfectly safe."},
{24.580, 27.030, "Emo?"},
{28.200, 29.990, "Watch out!"},
{47.030, 48.490, "Are you hurt?"},
{51.990, 53.940, "I don't think so.\nYou?"},
{55.160, 56.980, "I'm Ok."},
{57.110, 61.110, "Get up.\nEmo, it's not safe here."},
{62.030, 63.570, "Let's go."},
};
constexpr base::TimeDelta kTestCueDataLength =
base::TimeDelta::FromSecondsD(65);
class CueEventListener final : public NativeEventListener {
public:
void Invoke(ExecutionContext* ctx, Event* event) override {
if (event->type() == event_type_names::kEnter) {
EXPECT_TRUE(event->target()->GetWrapperTypeInfo()->Equals(
VTTCue::GetStaticWrapperTypeInfo()));
auto* const cue = static_cast<VTTCue*>(event->target());
auto* const media_element = cue->track()->MediaElement();
OnCueEnter(media_element, cue);
return;
} else if (event->type() == event_type_names::kExit) {
EXPECT_TRUE(event->target()->GetWrapperTypeInfo()->Equals(
VTTCue::GetStaticWrapperTypeInfo()));
auto* const cue = static_cast<VTTCue*>(event->target());
auto* const media_element = cue->track()->MediaElement();
OnCueExit(media_element, cue);
return;
}
// The above checks should be exhaustive
FAIL();
}
void ExpectAllEventsFiredWithinMargin(base::TimeDelta margin) const {
for (auto const& delta : cue_event_deltas_) {
EXPECT_TRUE(delta.enter_time_delta.has_value());
EXPECT_LE(delta.enter_time_delta.value(), margin);
EXPECT_GE(delta.enter_time_delta.value(), base::TimeDelta());
EXPECT_TRUE(delta.exit_time_delta.has_value());
EXPECT_GE(delta.exit_time_delta.value(), base::TimeDelta());
EXPECT_LE(delta.exit_time_delta.value(), margin);
}
}
private:
struct CueChangeEventTimeDelta {
// The difference between when the cue was scheduled to begin and when the
// |kEnter| event was fired. The optional will be empty if the |kEnter|
// event was never fired.
base::Optional<base::TimeDelta> enter_time_delta;
// The difference between when the cue was scheduled to end and when the
// |kExit| event fired. The optional will be empty if the |kExit| event
// was never fired.
base::Optional<base::TimeDelta> exit_time_delta;
};
void OnCueEnter(HTMLMediaElement* media_element, VTTCue* cue) {
auto const cue_index = cue->CueIndex();
EXPECT_LE(cue_index, cue_event_deltas_.size());
EXPECT_FALSE(cue_event_deltas_[cue_index].enter_time_delta.has_value());
// Get the start time delta
double const diff_seconds = media_element->currentTime() - cue->startTime();
cue_event_deltas_[cue_index].enter_time_delta =
base::TimeDelta::FromSecondsD(diff_seconds);
}
void OnCueExit(HTMLMediaElement* media_element, VTTCue* cue) {
auto const cue_index = cue->CueIndex();
EXPECT_LE(cue_index, cue_event_deltas_.size());
EXPECT_FALSE(cue_event_deltas_[cue_index].exit_time_delta.has_value());
// Get the end time delta
double const diff_seconds =
std::fabs(media_element->currentTime() - cue->endTime());
cue_event_deltas_[cue_index].exit_time_delta =
base::TimeDelta::FromSecondsD(diff_seconds);
}
std::array<CueChangeEventTimeDelta, base::size(kTestCueData)>
cue_event_deltas_;
};
TEST_F(HTMLMediaElementWithMockSchedulerTest, CueEnterExitEventLatency) {
testing::InSequence dummy;
GetDocument().body()->setInnerHTML("<video></video>");
// Set a src to trigger WebMediaPlayer creation.
Video()->SetSrc("http://example.com");
platform()->RunUntilIdle();
ASSERT_NE(WebMediaPlayer(), nullptr);
// Create a text track, and fill it with cue data
auto* text_track =
Video()->addTextTrack("subtitles", "", "", ASSERT_NO_EXCEPTION);
auto* listener = MakeGarbageCollected<CueEventListener>();
for (auto cue_data : kTestCueData) {
VTTCue* cue = MakeGarbageCollected<VTTCue>(
GetDocument(), cue_data.start_time, cue_data.end_time, cue_data.text);
text_track->addCue(cue);
cue->setOnenter(listener);
cue->setOnexit(listener);
}
// Simulate conditions where playback is possible.
SimulateNetworkState(HTMLMediaElement::kNetworkIdle);
SimulateReadyState(HTMLMediaElement::kHaveFutureData);
// Simulate advancing playback time to enable periodic timeupdates.
WebMediaPlayer()->SetAutoIncrementTimeDelta(
base::TimeDelta::FromMilliseconds(8));
Video()->Play();
platform()->RunForPeriod(kTestCueDataLength);
platform()->RunUntilIdle();
// Ensure all cue events fired when expected with a 20ms tolerance
// As suggested by the spec:
// https://html.spec.whatwg.org/multipage/media.html#playing-the-media-resource:current-playback-position-13
listener->ExpectAllEventsFiredWithinMargin(
base::TimeDelta::FromMilliseconds(20));
}
} // namespace blink