video要素のテストをしよう
前提
前提となるライブラリは次です。
- react
- @testing-library/react
- jest
これらの説明はしません。ご了承ください。
はじめに
video要素のテストのついてまとめました。
調べた感じ、個々のテスト手法はStack Overflowだったり、githubのissueに載ってたりしましたが、まとまったものはなかったのでまとめておきます。
video要素の取得方法
video要素はgetByRoleじゃ取得できません。
おそらく、それはaria roleとしてvideoが定義されていないためです。(参考)
なので、aria-labelをつけるなりして、取得するための目印をつけましょう。
import { Video } from "./Video"
import { render, screen } from "@testing-library/react"
describe("Video",()=>{
it("video要素が取得できる", () => {
render(
// biome-ignore lint/a11y/useMediaCaption:
<video
src={staticPath.test_video.video_mp4}
controls
aria-label="The Video"
/>,
);
expect(screen.getByLabelText("The Video")).toBeInTheDocument();
});
})
動画の再生・一時停止
controls属性を設定していた場合、ブラウザ上では再生ボタンが描画されますが、これはテスト環境では取得できません。
ではどうするかというと、video要素を取得してplay関数を実行するのですが、jestのjsdomにはplay関数が実装されていません。
つまり結論としてどうするかというと次のことをします。
- play関数をモックする
- playイベントを発行する
これによって動画の再生を模倣することができます。
import { Video } from "./Video"
import { render, screen,act, fireEvent } from "@testing-library/react"
describe("Video",()=>{
it("動画が再生できる", async () => {
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(function (
this: HTMLMediaElement,
) {
fireEvent(this, new Event("play"));
return Promise.resolve();
});
const handlePlay = jest.fn();
render(
// biome-ignore lint/a11y/useMediaCaption:
<video
src={staticPath.test_video.video_mp4}
controls
aria-label="The Video"
onPlay={handlePlay}
/>,
);
const video = screen.getByLabelText<HTMLVideoElement>("The Video");
await video.play();
expect(handlePlay).toHaveBeenCalled();
});
})
一時停止も同様の方法でテストできます。
シークバーを移動させる
動画の再生と同様にシークバーもモックする必要があります。
シークバーの移動はvideo要素のcurrentTimeを変更することで実現されるので、curerntTimeをモックして挙動を模倣します。
import { staticPath } from "@/lib/$path";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { Video } from "./Video";
describe("Video", () => {
it("動画をシークできる", async () => {
jest
.spyOn(HTMLMediaElement.prototype, "currentTime", "set")
.mockImplementation(function (
this: HTMLMediaElement & { _currentTime: number },
time: number,
) {
this._currentTime = time;
fireEvent(this, new Event("seeking"));
fireEvent(this, new Event("seeked"));
});
jest
.spyOn(HTMLMediaElement.prototype, "currentTime", "get")
.mockImplementation(function (
this: HTMLMediaElement & { _currentTime: number },
) {
return this._currentTime ?? 0;
});
const handleSeeked = jest.fn();
const handleSeeking = jest.fn();
render(
// biome-ignore lint/a11y/useMediaCaption:
<video
src={staticPath.test_video.video_mp4}
controls
aria-label="The Video"
onSeeked={handleSeeked}
onSeeking={handleSeeking}
/>,
);
const video = screen.getByLabelText<HTMLVideoElement>("The Video");
expect(video.currentTime).toBe(0);
video.currentTime = 10;
expect(handleSeeked).toHaveBeenCalled();
expect(handleSeeking).toHaveBeenCalled();
expect(video.currentTime).toBe(10);
});
});
動画の再生が終了する
これはシンプルにendedイベントを発行するだけです。
describe("Video", () => {
it("動画の再生が終わる", async () => {
const handleEnded = jest.fn();
render(
// biome-ignore lint/a11y/useMediaCaption:
<video
src={staticPath.test_video.video_mp4}
controls
aria-label="The Video"
onEnded={handleEnded}
/>,
);
const video = screen.getByLabelText<HTMLVideoElement>("The Video");
fireEvent(video, new Event("ended"));
expect(handleEnded).toHaveBeenCalled();
});
});
すべてのテストでvideo要素のモックを適用する
これまでモックしてきたものをすべてまとめてモックする場合は次のようになります。
新たにdurationを設定していること、play関数内でsetIntervalでcurrentTimeを進めていることに注意してください。
import { fireEvent } from "@testing-library/dom";
Object.defineProperties(HTMLMediaElement.prototype, {
play: {
value() {
if(Number.isNaN(this.duration)) {
this.duration = 10;
}
this._timer = setInterval(() => {
if(this._currentTime === undefined) this._currentTime = 0;
this._currentTime += 0.01
if (this.currentTime >= this.duration) {
fireEvent(this, new Event("ended"));
clearInterval(this._timer);
}
}, 10);
fireEvent(this, new Event("play"));
return Promise.resolve();
},
},
pause: {
value() {
clearInterval(this._timer);
fireEvent(this, new Event("pause"));
},
},
currentTime: {
get() {
return this._currentTime ?? 0;
},
set(time) {
this._currentTime = time;
fireEvent(this, new Event("seeking"));
fireEvent(this, new Event("seeked"));
},
},
duration:{
get() {
return this._duration ?? NaN;
},
set(time) {
this._duration = time;
}
}
});
上記をsetupのファイルの中に置くと各テストファイルでモックする必要がなくなります。
これが動いてるかどうかをテストする関数は次です。
it("すべてのテストでvideo要素のモックを適用する", async () => {
jest.useFakeTimers({
advanceTimers:true
})
const handleEnded = jest.fn();
render(
// biome-ignore lint/a11y/useMediaCaption:
<video
src={staticPath.test_video.video_mp4}
controls
aria-label="The Video"
onEnded={handleEnded}
/>,
);
const video = screen.getByLabelText<HTMLVideoElement>("The Video");
video.play();
await waitFor(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 11 * 1000);
})
},{
timeout: 12*1000,
});
expect(handleEnded).toHaveBeenCalled();
});
まとめ
これでvideo要素をいろいろカスタムしても通常のDOMと同様の挙動が模倣されるのでテストが書けます。
次回はcanvasのテストを書きたいと思います。
まだ調査中なのでできるかわかりませんが...
Discussion