Je souhaite exécuter des tests unitaires (en utilisant ava
et browser-env
) pour un asset-loader qui prend en charge le pré-chargement d'images, audio et vidéo. Je voulais savoir si jsdom prend en charge les éléments audio et vidéo. Lorsque j'essaie de créer et d'appeler video.load()
sur un élément vidéo ( HTMLVideoElement
qui est lui-même un HTMLMediaElement
), jsdom renvoie cette erreur :
Error: Not implemented: HTMLMediaElement.prototype.load
Je suppose qu'il n'y a pas de support pour les éléments vidéo et audio. Je n'ai rien trouvé sur le support vidéo et audio dans jsdom, peut-être manque-t-il ?
jsdom
ne prend en charge aucune opération de chargement ou de lecture multimédia. Pour contourner ce problème, vous pouvez ajouter quelques stubs dans votre configuration de test :
window.HTMLMediaElement.prototype.load = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.play = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.pause = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.addTextTrack = () => { /* do nothing */ };
C'est ce dont j'ai besoin ici : un moyen de simuler le chargement/récupération de HTMLMediaElements
. En faisant cela, il ne préchargera pas l'audio et la vidéo comme dans un vrai navigateur, n'est-ce pas ?
En général, aucune récupération n'est requise pour de tels tests. Ces stubs suppriment les exceptions jsdom, et vous pourrez alors tester votre logique avec des événements envoyés manuellement à partir de l'élément vidéo (par exemple videoElement.dispatchEvent(new window.Event("loading"));
).
D'accord, merci pour l'aide, j'ai enfin corrigé mes tests. ??
Cela m'a aidé à démarrer, mais dans mon cas, je veux tester si certaines conditions affectent correctement la lecture. J'ai créé des fonctions de remplacement play
et pause
et j'essaie de définir la variable paused
de l'élément multimédia mais j'obtiens une erreur indiquant qu'il n'y a qu'un getter pour cette variable. Cela rend la moquerie un peu difficile.
Je suis un peu nouveau sur JS. Existe-t-il un moyen de simuler des variables en lecture seule comme celle-ci ?
@BenBergman Vous pourriez faire :
Object.defineProperty(HTMLMediaElement.prototype, "paused", {
get() {
// Your own getter, where `this` refers to the HTMLMediaElement.
}
});
Excellent merci! Pour la postérité, mon getter ressemble à ceci pour tenir compte d'une valeur par défaut :
get() {
if (this.mockPaused === undefined) {
return true;
}
return this.mockPaused;
}
Vous pouvez le rendre encore plus simple de cette façon :
Object.defineProperty(mediaTag, "paused", {
writable: true,
value: true,
});
puis changez simplement mediaTag.paused = true
ou mediaTag.paused = false
dans votre test.
L'avantage de cette approche est qu'elle est sécurisée si vous utilisez TypeScript. Vous ne devez pas définir votre propriété fictive comme (mediaTag as any).mockPaused = true
.
Encore mieux, merci !
Comment puis-je simuler la lecture vidéo ?
J'ai bloqué la lecture et le chargement, mais je ne sais pas comment lancer la lecture de la vidéo (ou je pense qu'elle est en train de jouer, tout ce dont j'ai besoin, c'est de ce qui se passe souvent).
Object.defineProperty(HTMLMediaElement.prototype, "play", { get() { document.getElementsByTagName('video')[0].dispatchEvent(new Event('play')); } });
jsdom ne prend en charge aucune opération de chargement ou de lecture multimédia. Pour contourner ce problème, vous pouvez ajouter quelques stubs dans votre configuration de test :
Merci pour la solution de contournement. Puis-je demander pourquoi cela n'est pas pris en charge par défaut, cependant?
Personne n'a encore implémenté de lecteur vidéo ou audio dans jsdom.
Voici une implémentation rapide et sale (pour jest et vue) des méthodes play
et pause
qui envoie également certains des événements dont j'avais besoin pour un test ( 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'))
}
Et l'exemple du test (notez qu'il faut régler manuellement 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)
})
J'ai envisagé d'utiliser ces solutions de contournement, mais au lieu de réimplémenter un navigateur comme des fonctionnalités de jeu, j'ai décidé d'utiliser puppeteer
, c'est-à-dire d'avoir un vrai navigateur pour faire les tests. Voici ma configuration :
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"
}
}
Commentaire le plus utile
jsdom
ne prend en charge aucune opération de chargement ou de lecture multimédia. Pour contourner ce problème, vous pouvez ajouter quelques stubs dans votre configuration de test :