Jsdom: Jsdom prend-il en charge les éléments vidéo et audio ?

Créé le 19 févr. 2018  ·  14Commentaires  ·  Source: jsdom/jsdom

Informations de base:

  • Version de Node.js : 8.94
  • version jsdom : 11.6.2

Informations

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 ?

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 :

window.HTMLMediaElement.prototype.load = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.play = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.pause = () => { /* do nothing */ };
window.HTMLMediaElement.prototype.addTextTrack = () => { /* do nothing */ };

Tous les 14 commentaires

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"
  }
}

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

kentmw picture kentmw  ·  3Commentaires

lehni picture lehni  ·  4Commentaires

JacksonGariety picture JacksonGariety  ·  4Commentaires

potapovDim picture potapovDim  ·  4Commentaires

mitar picture mitar  ·  4Commentaires