Skip to content

Commit

Permalink
Merge pull request #6 from flowplayer/feat/PLAY-473/tests
Browse files Browse the repository at this point in the history
feat(PLAY-473): setup puppeteer
  • Loading branch information
ondreian authored Oct 8, 2024
2 parents eda40af + 5702318 commit b7f8c4c
Show file tree
Hide file tree
Showing 18 changed files with 2,087 additions and 46 deletions.
2 changes: 1 addition & 1 deletion flowplayer.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FlowplayerUMD} from "@flowplayer/player"
import type { FlowplayerUMD} from "@flowplayer/player"
declare global {
var flowplayer: FlowplayerUMD;
}
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"new:component": "tsx tooling/code-gen/bin.ts",
"cdn:manifest": "tsx tooling/cdn/bin.ts",
"build": "webpack"
"build": "webpack",
"test:puppeteer": "yarn ava --config test/puppeteer/_ava.config.mjs"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
Expand All @@ -19,6 +20,13 @@
"typescript": "latest",
"webpack": "^5.91.0",
"ts-loader": "^9.5.1",
"puppeteer": "^14.4.0",
"mkdirp": "^1.0.4",
"http-server": "^14.1.0",
"puppeteer-screen-recorder": "^2.0.2",
"ava": "^4.2.0",
"pug": "^3.0.3",
"dotenv": "^16.0.1",
"style-loader": "^4.0.0",
"css-loader": "^7.1.2",
"webpack-cli": "^5.1.4"
Expand Down
2 changes: 1 addition & 1 deletion packages/combined-menu-control/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ export default class CombinedMenuControl extends HTMLElement{
}
}

install("flowplayer-control", "combined-menu-controls", CombinedMenuControl)
install("flowplayer-control", "combined-menu-control", CombinedMenuControl)
2 changes: 1 addition & 1 deletion packages/combined-menu-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"main": "./index.ts",
"description": "A control bar with a single combined menu for all settings",
"flowplayer": {
"componentName": "combined-menu-controls",
"componentName": "combined-menu-control",
"overridenComponent": "flowplayer-control",
"className": "CombinedMenuControl"
}
Expand Down
Empty file removed packages/fancy-ui/fancy.css
Empty file.
11 changes: 0 additions & 11 deletions packages/fancy-ui/index.ts

This file was deleted.

9 changes: 0 additions & 9 deletions packages/fancy-ui/package.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/live-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,4 @@ export default class LiveUiMiddle extends HTMLElement{
}
}

install("flowplayer-middle", "live-ui-middle", LiveUiMiddle)
install("flowplayer-middle", "live-ui", LiveUiMiddle)
2 changes: 1 addition & 1 deletion packages/live-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@flowplayer/components-live-ui",
"main": "./index.ts",
"description": "A screen rendered for live streams before playback starts, with two buttons: one for starting a stream from the beginning and another one for going going live",
"description": "A screen rendered for live streams before playback starts, with two buttons: one for starting a stream from the beginning and another one for going live",
"flowplayer": {
"componentName": "live-ui",
"overridenComponent": "flowplayer-middle",
Expand Down
2 changes: 1 addition & 1 deletion shared/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function install (flowplayerComponent : string, name : string, klass : Cu
return void window.flowplayer.customElements.set(flowplayerComponent, name)
}

window.addEventListener("flowplayer:umd" as any, (e : CustomEvent<typeof window.flowplayer>) => {
document.addEventListener("flowplayer:umd" as any, (e : CustomEvent<typeof window.flowplayer>) => {
const flowplayer = e.detail
flowplayer.customElements.set(flowplayerComponent, name)
})
Expand Down
6 changes: 6 additions & 0 deletions test/puppeteer/_ava.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
files: ["./test/puppeteer/**/*.spec.mjs"],
require: ["./test/puppeteer/_setup/helpers.mjs"],
timeout: "5m",
concurrency: 3
}
202 changes: 202 additions & 0 deletions test/puppeteer/_setup/helpers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import pug from "pug"
import os, {tmpdir} from "os"
import fs from "fs/promises"
import path from "path"
import puppeteer from "puppeteer"
import HttpServer from "http-server"
import "dotenv/config"
import mkdirp from "mkdirp"
import {PuppeteerScreenRecorder} from "puppeteer-screen-recorder"

const debug = (...args) => process.env.DEBUG && console.log(...args)
const _dirname = path.dirname(import.meta.url)
const tmpDir = path.join(os.tmpdir(), "puppeteer-native", "html")

export function compilePug(name) {
const fileName = path.join(_dirname, name + ".pug").slice(5)
return pug.compileFile(fileName, {})
}

const Templates = {local: compilePug("local")}

export async function createServer(t) {
return new Promise((resolve) => {
const server = HttpServer.createServer({root: tmpDir})
server.listen(0, "localhost", () => {
Object.assign(t.context, {server, ...server.server.address()})
Object.assign(t.context, {address: "localhost"})
debug("server / up / %s:%s", t.context.address, t.context.port)
resolve()
})
})
}

export async function destroyServer(t) {
if (!t.context.server) return
t.context.server.close()
t.context.server = void 0
}

export async function loadComponent(componentName) {
const component = await fs.readFile(
path.join("dist", `${componentName}.js`)
)

return component.toString()
}

export async function writeTempFile(fileName, contents) {
const fullName = path.join(tmpDir, fileName)
debug("created: %s", fullName)
await fs.writeFile(fullName, contents)
return fullName
}

export function titleToFile(title, suffix = ".html") {
const fileName = title
.toLowerCase()
.replaceAll(/\s+/g, "-")
.replaceAll(/[/]/g, "-")
.replaceAll(/,/g, "-")
.replaceAll(/-{2,}/g, "-")
.replaceAll(/=/g, "-")
.replaceAll(/_/g, "-")
return fileName + suffix
}

export async function compileLocalTest(t, {config, componentNames}) {
const components = await Promise.all(componentNames.map((name) => loadComponent(name)))

const compiled = Templates.local({
Test: {
components,
config: config
}
})
const fileName = titleToFile(t.title)
const filePath = await writeTempFile(fileName, compiled)
return {fileName, filePath}
}

const {
PUPPETEER_PRODUCT: puppeteerProduct,
//Applications/Google Chrome.app/Contents/MacOS/Google Chrome
CHROME_PATH: chromePath = "/usr/bin/google-chrome",
FIREFOX_PATH: firefoxPath = "/usr/bin/firefox"
} = process.env

const headless = !!process.env.CI || !!process.env.PUPPETEER_HEADLESS

const executablePath = puppeteerProduct === "firefox" ? firefoxPath : chromePath

export async function makePuppeteerSession(t, args) {
args = args || []
if (process.env.CI) args.push("--no-sandbox")

const browser = await puppeteer.launch({
headless,
executablePath,
slowMo: !headless ? 50 : 200,
userDataDir: path.join(tmpdir(), `puppeteer-ci-${Date.now()}`),
args
})
const page = await browser.newPage({timeout: 120 * 1000})
const recorder = new PuppeteerScreenRecorder(page)
page.on("console", (msg) => {
if (!process.env.DEBUG) return
for (let i = 0; i < msg.args().length; ++i)
debug(`console / ${t.title} / ${i}: ${msg.args()[i]}`)
})
return {browser, page, recorder}
}

async function getPlayer(
t,
componentNames,
config,
token,
puppeteer,
recorder,
setup,
host,
page
) {
const context = {host, page}
if (typeof setup == "function") {
await setup(context)
}
const recordingFile =
"/tmp/puppeteer-native/components/recordings/" + titleToFile(t.title, ".mp4")

if (!process.env.DEBUG) await recorder.start(recordingFile)
const {fileName, filePath} = await compileLocalTest(t, {
config: JSON.stringify(config).replaceAll(":host:", host),
componentNames,
})
const testDocument = `${context.host}/${fileName}` //host + fileName
await page.goto(testDocument, puppeteer || {waitUntil: "networkidle2", timeout: 120 * 1000}
)
const player = await page.$(".fp-engine")
const components = await Promise.all(componentNames.map((name) => page.$(name)))
return {components, player, filePath, recordingFile}
}

export function withComponents({componentNames, config, token, files, puppeteer, args}, setup) {
return async function (t, run) {
await mkdirp(tmpDir)
await mkdirp("/tmp/puppeteer-native/components/recordings/")
if (!t.context.address) await createServer(t)
const host = `http://localhost:${t.context.port}`
if (files)
await Promise.all(
Object.entries(files).map(([fileName, contents]) =>
writeTempFile(fileName, contents)
)
)

const {browser, page, recorder} = await makePuppeteerSession(t, args)

const {components, player, filePath, recordingFile} = await getPlayer(
t,
componentNames,
config,
token,
puppeteer,
recorder,
setup,
host,
page
)

//ensure we have player
t.assert(player)
//ensure the components are rendered
components.forEach((component, idx) => t.assert(component, componentNames[idx]))
try {
try {
await run(t, page, player, ...components)
} catch (err) {
// todo: open an issue with ava.js about this
t.fail(err.message)
}
} finally {
await destroyServer(t)
if (!process.env.DEBUG) await recorder.stop()
if (t.passed) {
const testFiles = Object.keys(files || {}).map((fileName) =>
path.join(tmpDir, fileName)
)
const rm = async (f) => {
if (process.env.KEEP) return
if (process.env.DEBUG) debug("cleaning up file %s", f)
if (await fs.stat(f)) {
return await fs.rm(f)
}
}
const removals = [rm(recordingFile), rm(filePath), ...testFiles.map(rm)]
await Promise.all(removals)
}
if (!process.env.KEEP) await browser.close()
}
}
}
21 changes: 21 additions & 0 deletions test/puppeteer/_setup/local.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
html(lang="en")
meta(charset="utf-8")
head
link(rel="stylesheet" href="https://cdn.flowplayer.com/releases/native/3/stable/style/flowplayer.css")
body
div(id = "player")
script(type= "module")
| import flowplayer from "https://cdn.flowplayer.com/releases/native/3/stable/esm/default/flowplayer.min.js"
| import ovp from "https://cdn.flowplayer.com/releases/native/3/stable/esm/plugins/ovp.min.js"
| import hls from "https://cdn.flowplayer.com/releases/native/3/stable/esm/plugins/hls.min.js"
| import qsel from "https://cdn.flowplayer.com/releases/native/3/stable/esm/plugins/qsel.min.js"
| import speed from "https://cdn.flowplayer.com/releases/native/3/stable/esm/plugins/speed.min.js"
| window.flowplayer = flowplayer
| flowplayer(ovp, hls, qsel, speed);
each component in Test.components
|!{component}
| localStorage["flowplayer/debug"] = ".*"
| window.Errors = []
| window.onerror = function(messageOrEvent, source, lineno, colno, error) { window.Errors.push([...arguments]) }
| window.__FLOWPLAYER_TOKEN = "eyJraWQiOiJiRDJrSnhuTkppT1AiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjIjoie1wiYWNsXCI6NixcImlkXCI6XCJiRDJrSnhuTkppT1BcIn0iLCJpc3MiOiJGbG93cGxheWVyIn0.6SS-jLJb338KVYAsj4SFVBzbah-auDeQjeBqjJL6SRa_vJKt4xW7-lSlZGjqsKAqhXFso2UF_BaBkJZ1S1SQFg"
| window.player = flowplayer("#player", !{Test.config})
44 changes: 44 additions & 0 deletions test/puppeteer/_setup/player.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export async function captureEvent(page, player, eventName, timeout = 20_000) {
return await page.evaluate(
(player, eventName, timeout) => {
return new Promise((ok, err) => {
const serialize = (o) => {
try {
return JSON.parse(JSON.stringify(o))
} catch (err) {
return o
}
}

const handle = (e) => {
ok({
type: e.type,
state: player.root.className,
detail: serialize(e.detail)
})
}
player.addEventListener(eventName, handle, {once: true})
setTimeout(
() =>
err(
new Error(`failed to detect event::${eventName} in ${timeout}ms`)
),
timeout
)
})
},
player,
eventName,
timeout
)
}

export async function togglePlay(page, player, flag) {
return await page.evaluate(
(player, flag) => {
return Promise.resolve(player.togglePlay(flag))
},
player,
flag
)
}
Loading

0 comments on commit b7f8c4c

Please sign in to comment.