Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(PLAY-473): setup puppeteer #6

Merged
merged 13 commits into from
Oct 8, 2024
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
Loading