أرغب في إجراء اختبارات الوحدة (باستخدام 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 حتى الآن.
في ما يلي تنفيذ سريع وقذر (من أجل الدعابة والنظرة) لطريقتين 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
أي عمليات تحميل أو تشغيل للوسائط. كحل بديل ، يمكنك إضافة بعض التنويهات في إعداد الاختبار الخاص بك: