Я хочу запустить модульные тесты (используя ava
и browser-env
) для загрузчика ресурсов, который поддерживает предварительную загрузку изображений, аудио и видео. Я хотел знать, поддерживает ли jsdom аудио и видео элементы. Когда я пытаюсь создать и вызвать video.load()
для элемента видео ( HTMLVideoElement
который сам является HTMLMediaElement
), jsdom возвращает эту ошибку:
Error: Not implemented: HTMLMediaElement.prototype.load
Я предполагаю, что нет поддержки видео и аудио элемента. Я ничего не нашел о поддержке видео и аудио в jsdom, может, она отсутствует?
jsdom
не поддерживает операции загрузки или воспроизведения мультимедиа. В качестве обходного пути вы можете добавить несколько заглушек в свою тестовую установку:
window.HTMLMediaElement.prototype.load = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.play = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.pause = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.addTextTrack = () => { /* do nothing */ };
Вот что мне здесь нужно: способ имитировать загрузку / выборку HTMLMediaElements
. Делая это, он не будет предварительно загружать аудио и видео, как в реальном браузере, верно?
Обычно для таких тестов загрузка не требуется. Эти заглушки подавляют исключения jsdom, и тогда вы сможете протестировать свою логику с вручную отправленными событиями из видеоэлемента (например, videoElement.dispatchEvent(new window.Event("loading"));
).
Хорошо, спасибо за помощь, я наконец исправил свои тесты. 👍
Это помогло мне начать работу, но в моем случае я хочу проверить, правильно ли влияют определенные условия на воспроизведение. Я заменил функции play
и pause
и пытаюсь установить переменную paused
медиа-элемента, но получаю сообщение об ошибке, что для этой переменной есть только геттер. Это делает издевательство над ним немного сложным.
Я новичок в JS. Есть ли способ имитировать такие переменные, доступные только для чтения?
@BenBergman Вы могли бы сделать:
Object.defineProperty(HTMLMediaElement.prototype, "paused", {
get() {
// Your own getter, where `this` refers to the HTMLMediaElement.
}
});
Отлично, спасибо! Для потомков мой геттер выглядит так, чтобы учесть значение по умолчанию:
get() {
if (this.mockPaused === undefined) {
return true;
}
return this.mockPaused;
}
Вы можете сделать это еще проще:
Object.defineProperty(mediaTag, "paused", {
writable: true,
value: true,
});
а затем просто измените mediaTag.paused = true
или mediaTag.paused = false
в своем тесте.
Преимущество этого подхода в том, что он безопасен по типу, если вы используете TypeScript. Вы не можете устанавливать свое фиктивное свойство как-то вроде (mediaTag as any).mockPaused = true
.
Еще лучше, спасибо!
Как я могу смоделировать воспроизведение видео?
Я отключил воспроизведение и загрузку, но я понятия не имею, как запустить воспроизведение видео (или думаю, что оно воспроизводится, все, что мне нужно, это то, что часто происходит).
Object.defineProperty(HTMLMediaElement.prototype, "play", { get() { document.getElementsByTagName('video')[0].dispatchEvent(new Event('play')); } });
jsdom не поддерживает операции загрузки или воспроизведения мультимедиа. В качестве обходного пути вы можете добавить несколько заглушек в свою тестовую установку:
Спасибо за обходной путь. Могу я спросить, почему это не поддерживается по умолчанию?
В jsdom еще никто не реализовал видео- или аудиоплеер.
Вот быстрая и грязная реализация (для jest и vue) методов play
и pause
которые также отправляют некоторые события, которые мне нужны для теста ( loadedmetadata
, play
, pause
):
// Jest's setup file, setup.js
// Mock data and helper methods
global.window.HTMLMediaElement.prototype._mock = {
paused: true,
duration: NaN,
_loaded: false,
// Emulates the audio file loading
_load: function audioInit(audio) {
// Note: we could actually load the file from this.src and get real duration
// and other metadata.
// See for example: https://github.com/59naga/mock-audio-element/blob/master/src/index.js
// For now, the 'duration' and other metadata has to be set manually in test code.
audio.dispatchEvent(new Event('loadedmetadata'))
audio.dispatchEvent(new Event('canplaythrough'))
},
// Reset audio object mock data to the initial state
_resetMock: function resetMock(audio) {
audio._mock = Object.assign(
{},
global.window.HTMLMediaElement.prototype._mock,
)
},
}
// Get "paused" value, it is automatically set to true / false when we play / pause the audio.
Object.defineProperty(global.window.HTMLMediaElement.prototype, 'paused', {
get() {
return this._mock.paused
},
})
// Get and set audio duration
Object.defineProperty(global.window.HTMLMediaElement.prototype, 'duration', {
get() {
return this._mock.duration
},
set(value) {
// Reset the mock state to initial (paused) when we set the duration.
this._mock._resetMock(this)
this._mock.duration = value
},
})
// Start the playback.
global.window.HTMLMediaElement.prototype.play = function playMock() {
if (!this._mock._loaded) {
// emulate the audio file load and metadata initialization
this._mock._load(this)
}
this._mock.paused = false
this.dispatchEvent(new Event('play'))
// Note: we could
}
// Pause the playback
global.window.HTMLMediaElement.prototype.pause = function pauseMock() {
this._mock.paused = true
this.dispatchEvent(new Event('pause'))
}
И пример теста (обратите внимание, что нам нужно вручную установить audio.duration
:
// Test
it('creates audio player', async () => {
// `page` is a wrapper for a page being tested, created in beforeEach
let player = page.player()
// Useful to see which properties are defined where.
// console.log(Object.getOwnPropertyDescriptors(HTMLMediaElement.prototype))
// console.log(Object.getOwnPropertyDescriptors(HTMLMediaElement))
// console.log(Object.getOwnPropertyDescriptors(audio))
let audio = player.find('audio').element as HTMLAudioElement
let audioEventReceived = false
audio.addEventListener('play', () => {
audioEventReceived = true
})
// @ts-ignore: error TS2540: Cannot assign to 'duration' because it is a read-only property.
audio.duration = 300
expect(audio.paused).toBe(true)
expect(audio.duration).toBe(300)
expect(audio.currentTime).toBe(0)
audio.play()
audio.currentTime += 30
expect(audioEventReceived).toBe(true)
expect(audio.paused).toBe(false)
expect(audio.duration).toBe(300)
expect(audio.currentTime).toBe(30.02)
})
Я рассматривал возможность использования этих обходных путей, но вместо того, чтобы заново реализовывать браузер, например функции воспроизведения, я решил использовать puppeteer
, то есть получить настоящий браузер для проведения тестирования. Это моя установка:
src / reviewer.tests.ts
jest.disableAutomock()
// Use this in a test to pause its execution, allowing you to open the chrome console
// and while keeping the express server running: chrome://inspect/#devices
// jest.setTimeout(2000000000);
// debugger; await new Promise(function(resolve) {});
test('renders test site', async function() {
let self: any = global;
let page = self.page;
let address = process.env.SERVER_ADDRESS;
console.log(`The server address is '${address}'.`);
await page.goto(`${address}/single_audio_file.html`);
await page.waitForSelector('[data-attibute]');
let is_paused = await page.evaluate(() => {
let audio = document.getElementById('silence1.mp3') as HTMLAudioElement;
return audio.paused;
});
expect(is_paused).toEqual(true);
});
testfiles / single_audio_file.html
<html>
<head>
<title>main webview</title>
<script src="importsomething.js"></script>
</head>
<body>
<div id="qa">
<audio id="silence1.mp3" src="silence1.mp3" data-attibute="some" controls></audio>
<script type="text/javascript">
// doSomething();
</script>
</div>
</body>
</html>
** globalTeardown.js **
module.exports = async () => {
global.server.close();
};
** globalSetup.js **const express = require('express');
module.exports = async () => {
let server;
const app = express();
await new Promise(function(resolve) {
server = app.listen(0, "127.0.0.1", function() {
let address = server.address();
process.env.SERVER_ADDRESS = `http://${address.address}:${address.port}`;
console.log(`Running static file server on '${process.env.SERVER_ADDRESS}'...`);
resolve();
});
});
global.server = server;
app.get('/favicon.ico', (req, res) => res.sendStatus(200));
app.use(express.static('./testfiles'));
};
** testEnvironment.js **const puppeteer = require('puppeteer');
// const TestEnvironment = require('jest-environment-node'); // for server node apps
const TestEnvironment = require('jest-environment-jsdom'); // for browser js apps
class ExpressEnvironment extends TestEnvironment {
constructor(config, context) {
let cloneconfig = Object.assign({}, config);
cloneconfig.testURL = process.env.SERVER_ADDRESS;
super(cloneconfig, context);
}
async setup() {
await super.setup();
let browser = await puppeteer.launch({
// headless: false, // show the Chrome window
// slowMo: 250, // slow things down by 250 ms
ignoreDefaultArgs: [
"--mute-audio",
],
args: [
"--autoplay-policy=no-user-gesture-required",
],
});
let [page] = await browser.pages(); // reuses/takes the default blank page
// let page = await this.global.browser.newPage();
page.on('console', async msg => console[msg._type](
...await Promise.all(msg.args().map(arg => arg.jsonValue()))
));
this.global.page = page;
this.global.browser = browser;
this.global.jsdom = this.dom;
}
async teardown() {
await this.global.browser.close();
await super.teardown();
}
runScript(script) {
return super.runScript(script);
}
}
module.exports = ExpressEnvironment;
** tsconfig.json **{
"compilerOptions": {
"target": "es2017"
},
"include": [
"src/**/*"
],
"exclude": [
"src/**/*.test.ts"
]
}
** package.json **{
"scripts": {
"test": "jest",
},
"jest": {
"testEnvironment": "./testEnvironment.js",
"globalSetup": "./globalSetup.js",
"globalTeardown": "./globalTeardown.js",
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
}
},
"jshintConfig": {
"esversion": 8
},
"dependencies": {
"typescript": "^3.7.3"
},
"devDependencies": {
"@types/express": "^4.17.6",
"@types/jest": "^25.2.1",
"@types/node": "^13.11.1",
"@types/puppeteer": "^2.0.1",
"express": "^4.17.1",
"jest": "^25.3.0",
"puppeteer": "^3.0.0",
"ts-jest": "^25.3.1"
}
}
Самый полезный комментарий
jsdom
не поддерживает операции загрузки или воспроизведения мультимедиа. В качестве обходного пути вы можете добавить несколько заглушек в свою тестовую установку: