test(embedded-mpv): harden packaged Linux smoke

This commit is contained in:
4gray committed 2026-07-17 22:42:37 +02:00
1 parent 5c49deb77a
commit 22079a5986
7 files changed
+698 -115

No files matched your search

@@ -0,0 +1,27 @@
import { defineConfig } from '@playwright/test';
import baseConfig from './playwright.config';
export default defineConfig({
...baseConfig,
outputDir:
'../../dist/test-results/electron-backend-e2e/packaged-frame-copy-smoke',
reporter: [
['list'],
[
'html',
{
outputFolder:
'../../dist/playwright-report/electron-backend-e2e/packaged-frame-copy-smoke',
},
],
[
'json',
{
outputFile:
'../../dist/test-results/electron-backend-e2e/packaged-frame-copy-smoke/results.json',
},
],
],
timeout: 120000,
webServer: [],
});
+19
View File
@@ -12,6 +12,25 @@
"e2e": {
"dependsOn": ["electron-backend:build-e2e"]
},
"packaged-frame-copy-smoke": {
"executor": "nx:run-commands",
"cache": false,
"outputs": [
"{workspaceRoot}/dist/playwright-report/electron-backend-e2e/packaged-frame-copy-smoke",
"{workspaceRoot}/dist/test-results/electron-backend-e2e/packaged-frame-copy-smoke"
],
"options": {
"cwd": "apps/electron-backend-e2e",
"command": "pnpm exec playwright test --config=playwright.packaged.config.ts src/embedded-mpv-frame-copy-packaged.e2e.ts"
}
},
"test-packaged-frame-copy-fixtures": {
"executor": "nx:run-commands",
"options": {
"cwd": "apps/electron-backend-e2e",
"command": "pnpm exec tsx --test src/embedded-mpv-frame-copy-packaged-fixtures.spec.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
@@ -242,6 +242,14 @@ export function getPackagedLinuxNativeDir(executablePath: string): string {
);
}
export function resolvePackagedElectronLaunchArgs(
getuid: (() => number) | undefined
): string[] {
return typeof getuid === 'function' && getuid() === 0
? ['--no-sandbox']
: [];
}
/**
* Launch a real packaged Linux executable. Unlike the regular source E2E
* launcher, this deliberately keeps Chromium's GPU path enabled: the
@@ -273,17 +281,9 @@ export async function launchPackagedElectronApp(
);
}
const args: string[] = [];
if (
process.env['CI'] ||
(typeof process.getuid === 'function' && process.getuid() === 0)
) {
args.push('--no-sandbox');
}
const electronApp = await electron.launch({
executablePath: resolvedExecutablePath,
args,
args: resolvePackagedElectronLaunchArgs(process.getuid),
env: {
...process.env,
IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS:
@@ -0,0 +1,203 @@
import {
chmodSync,
copyFileSync,
existsSync,
linkSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
readlinkSync,
renameSync,
rmSync,
statSync,
symlinkSync,
} from 'fs';
import { tmpdir } from 'os';
import { basename, dirname, join, resolve } from 'path';
import { getPackagedLinuxNativeDir } from './electron-test-fixtures';
export type DisposablePackagedLinuxApp = {
cleanup: () => void;
executablePath: string;
nativeDir: string;
packageRoot: string;
temporaryRoot: string;
};
export type DisposablePackagedLinuxAppOptions = {
linkFile?: (existingPath: string, newPath: string) => void;
};
export type PackagedRuntimeIdentity = {
arch: string;
platform: string;
profile: string;
runtimeMode: string;
};
export type RuntimeManifestGuard = {
hide: () => void;
restore: () => void;
};
const HARDLINK_COPY_FALLBACK_CODES = new Set([
'EACCES',
'EMLINK',
'ENOSYS',
'ENOTSUP',
'EPERM',
'EXDEV',
]);
function clonePackagedEntry(
sourcePath: string,
destinationPath: string,
linkFile: (existingPath: string, newPath: string) => void
): void {
const sourceStats = lstatSync(sourcePath);
if (sourceStats.isDirectory()) {
mkdirSync(destinationPath);
for (const entry of readdirSync(sourcePath)) {
clonePackagedEntry(
join(sourcePath, entry),
join(destinationPath, entry),
linkFile
);
}
chmodSync(destinationPath, sourceStats.mode & 0o7777);
return;
}
if (sourceStats.isSymbolicLink()) {
symlinkSync(readlinkSync(sourcePath), destinationPath);
return;
}
if (!sourceStats.isFile()) {
throw new Error(
`Unsupported packaged runtime entry type at ${sourcePath}.`
);
}
try {
linkFile(sourcePath, destinationPath);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (!code || !HARDLINK_COPY_FALLBACK_CODES.has(code)) {
throw error;
}
copyFileSync(sourcePath, destinationPath);
chmodSync(destinationPath, sourceStats.mode & 0o7777);
}
}
export function createDisposablePackagedLinuxApp(
executablePath: string,
options: DisposablePackagedLinuxAppOptions = {}
): DisposablePackagedLinuxApp {
const sourceExecutablePath = resolve(executablePath);
const sourcePackageRoot = dirname(sourceExecutablePath);
const temporaryRoot = mkdtempSync(
join(tmpdir(), 'iptvnator-packaged-frame-copy-')
);
const packageRoot = join(temporaryRoot, basename(sourcePackageRoot));
let cleaned = false;
try {
clonePackagedEntry(
sourcePackageRoot,
packageRoot,
options.linkFile ?? linkSync
);
} catch (error) {
rmSync(temporaryRoot, { force: true, recursive: true });
throw error;
}
const clonedExecutablePath = join(
packageRoot,
basename(sourceExecutablePath)
);
return {
cleanup() {
if (cleaned) {
return;
}
rmSync(temporaryRoot, { force: true, recursive: true });
cleaned = true;
},
executablePath: clonedExecutablePath,
nativeDir: getPackagedLinuxNativeDir(clonedExecutablePath),
packageRoot,
temporaryRoot,
};
}
export function createRuntimeManifestGuard(
nativeDir: string
): RuntimeManifestGuard {
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
const hiddenRuntimeManifestPath = `${runtimeManifestPath}.e2e-hidden-${process.pid}`;
let hidden = false;
return {
hide() {
if (!statSync(runtimeManifestPath).isFile()) {
throw new Error(
`Packaged runtime manifest is not a regular file: ${runtimeManifestPath}`
);
}
if (existsSync(hiddenRuntimeManifestPath)) {
throw new Error(
`Stale hidden runtime manifest exists: ${hiddenRuntimeManifestPath}`
);
}
renameSync(runtimeManifestPath, hiddenRuntimeManifestPath);
hidden = true;
},
restore() {
if (!hidden) {
return;
}
if (!existsSync(hiddenRuntimeManifestPath)) {
throw new Error(
`Hidden runtime manifest disappeared before restore: ${hiddenRuntimeManifestPath}`
);
}
if (existsSync(runtimeManifestPath)) {
throw new Error(
`Refusing to overwrite runtime manifest during restore: ${runtimeManifestPath}`
);
}
renameSync(hiddenRuntimeManifestPath, runtimeManifestPath);
hidden = false;
},
};
}
export function readPackagedRuntimeIdentity(
nativeDir: string
): PackagedRuntimeIdentity {
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
const parsed = JSON.parse(
readFileSync(runtimeManifestPath, 'utf8')
) as Partial<PackagedRuntimeIdentity>;
for (const field of [
'arch',
'platform',
'profile',
'runtimeMode',
] as const) {
if (typeof parsed[field] !== 'string' || !parsed[field]) {
throw new Error(
`Packaged runtime manifest ${field} is invalid at ${runtimeManifestPath}.`
);
}
}
return parsed as PackagedRuntimeIdentity;
}
@@ -0,0 +1,318 @@
import assert = require('node:assert/strict');
import {
chmodSync,
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
readFileSync,
readlinkSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { afterEach, describe, it } from 'node:test';
import { isMeaningfulNativePlaybackSnapshot } from './embedded-mpv-frame-copy-packaged-fixtures';
import {
createDisposablePackagedLinuxApp,
createRuntimeManifestGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
import { resolvePackagedElectronLaunchArgs } from './electron-test-fixtures';
import packagedPlaywrightConfig from '../playwright.packaged.config';
const projectRoot = resolve(__dirname, '..');
const temporaryDirectories = new Set<string>();
type PackageFixture = {
executablePath: string;
nativeDir: string;
packageRoot: string;
payloadPath: string;
runtimeManifestPath: string;
};
function createPackageFixture(): PackageFixture {
const packageRoot = mkdtempSync(
join(tmpdir(), 'iptvnator-packaged-fixture-source-')
);
temporaryDirectories.add(packageRoot);
const executablePath = join(packageRoot, 'IPTVnator');
const nativeDir = join(
packageRoot,
'resources',
'app.asar.unpacked',
'electron-backend',
'native'
);
const payloadPath = join(packageRoot, 'resources', 'payload.bin');
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
mkdirSync(nativeDir, { recursive: true });
writeFileSync(executablePath, '#!/bin/sh\nexit 0\n');
chmodSync(executablePath, 0o755);
writeFileSync(payloadPath, 'packaged payload');
chmodSync(payloadPath, 0o640);
writeFileSync(
runtimeManifestPath,
JSON.stringify({
arch: 'x64',
platform: 'linux',
profile: 'portable',
runtimeMode: 'bundled',
})
);
if (process.platform !== 'win32') {
symlinkSync(
'payload.bin',
join(packageRoot, 'resources', 'payload-link')
);
}
return {
executablePath,
nativeDir,
packageRoot,
payloadPath,
runtimeManifestPath,
};
}
afterEach(() => {
for (const directory of temporaryDirectories) {
rmSync(directory, { force: true, recursive: true });
}
temporaryDirectories.clear();
});
describe('packaged Electron launch arguments', () => {
it('disables the Chromium sandbox only when running as root', () => {
const originalCi = process.env['CI'];
process.env['CI'] = 'true';
try {
assert.deepEqual(
resolvePackagedElectronLaunchArgs(() => 1000),
[]
);
assert.deepEqual(resolvePackagedElectronLaunchArgs(undefined), []);
assert.deepEqual(
resolvePackagedElectronLaunchArgs(() => 0),
['--no-sandbox']
);
} finally {
if (originalCi === undefined) {
delete process.env['CI'];
} else {
process.env['CI'] = originalCi;
}
}
});
});
describe('disposable unpacked package clone', () => {
it('hardlinks regular files, preserves modes and symlinks, and never hides the source manifest', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath);
temporaryDirectories.add(clone.temporaryRoot);
try {
assert.notEqual(clone.packageRoot, source.packageRoot);
assert.equal(
statSync(clone.executablePath).mode & 0o777,
statSync(source.executablePath).mode & 0o777
);
const clonedPayloadPath = join(
clone.packageRoot,
'resources',
'payload.bin'
);
assert.equal(
statSync(clonedPayloadPath).ino,
statSync(source.payloadPath).ino
);
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
if (process.platform !== 'win32') {
const clonedLinkPath = join(
clone.packageRoot,
'resources',
'payload-link'
);
assert.equal(lstatSync(clonedLinkPath).isSymbolicLink(), true);
assert.equal(readlinkSync(clonedLinkPath), 'payload.bin');
}
const clonedRuntimeManifestPath = join(
clone.nativeDir,
'embedded-mpv-runtime.json'
);
const guard = createRuntimeManifestGuard(clone.nativeDir);
guard.hide();
assert.equal(existsSync(clonedRuntimeManifestPath), false);
assert.equal(existsSync(source.runtimeManifestPath), true);
guard.restore();
} finally {
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
assert.equal(existsSync(clone.temporaryRoot), false);
assert.equal(existsSync(source.runtimeManifestPath), true);
});
it('copies a regular file when hardlinking is unavailable', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath, {
linkFile() {
throw Object.assign(new Error('cross-device hardlink'), {
code: 'EXDEV',
});
},
});
temporaryDirectories.add(clone.temporaryRoot);
try {
assert.equal(statSync(clone.executablePath).mode & 0o777, 0o755);
const clonedPayloadPath = join(
clone.packageRoot,
'resources',
'payload.bin'
);
assert.equal(
readFileSync(clonedPayloadPath, 'utf8'),
readFileSync(source.payloadPath, 'utf8')
);
assert.notEqual(
statSync(clonedPayloadPath).ino,
statSync(source.payloadPath).ino
);
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
} finally {
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
});
});
describe('native-view playback proof', () => {
it('requires the loaded URL, a playing or paused state, and positive duration', () => {
const expectedUrl = 'http://127.0.0.1:3210/fixture.y4m';
const baseSnapshot = {
durationSeconds: 2,
status: 'playing',
streamUrl: expectedUrl,
};
assert.equal(
isMeaningfulNativePlaybackSnapshot(baseSnapshot, expectedUrl),
true
);
assert.equal(
isMeaningfulNativePlaybackSnapshot(
{ ...baseSnapshot, status: 'paused' },
`${expectedUrl}#ignored`
),
true
);
assert.equal(
isMeaningfulNativePlaybackSnapshot(
{ ...baseSnapshot, durationSeconds: null },
expectedUrl
),
false
);
assert.equal(
isMeaningfulNativePlaybackSnapshot(
{ ...baseSnapshot, status: 'idle' },
expectedUrl
),
false
);
assert.equal(
isMeaningfulNativePlaybackSnapshot(
{ ...baseSnapshot, streamUrl: `${expectedUrl}?other=1` },
expectedUrl
),
false
);
});
it('loads the fixture and observes playback before disposing the native session', () => {
const source = readFileSync(
join(projectRoot, 'src', 'embedded-mpv-frame-copy-packaged.e2e.ts'),
'utf8'
);
const fallbackStart = source.indexOf('const launchedFallbackApp');
const captureIndex = source.indexOf(
'installEmbeddedMpvSessionCapture',
fallbackStart
);
const loadIndex = source.indexOf(
'loadEmbeddedMpvPlayback',
fallbackStart
);
const proofIndex = source.indexOf(
'isMeaningfulNativePlaybackSnapshot',
fallbackStart
);
const exitCodeIndex = source.indexOf(
'electronApp.process().exitCode',
fallbackStart
);
const disposeIndex = source.indexOf(
'disposeEmbeddedMpvSession',
fallbackStart
);
assert.ok(fallbackStart >= 0);
assert.ok(captureIndex > fallbackStart);
assert.ok(loadIndex > captureIndex);
assert.ok(proofIndex > loadIndex);
assert.ok(exitCodeIndex > proofIndex);
assert.ok(disposeIndex > exitCodeIndex);
});
});
describe('dedicated packaged smoke target', () => {
it('does not build the backend or start portal mock servers', () => {
const project = JSON.parse(
readFileSync(join(projectRoot, 'project.json'), 'utf8')
) as {
targets?: Record<
string,
{
cache?: boolean;
dependsOn?: unknown;
options?: { command?: string };
}
>;
};
const target = project.targets?.['packaged-frame-copy-smoke'];
const packagedConfig = readFileSync(
join(projectRoot, 'playwright.packaged.config.ts'),
'utf8'
);
if (!target) {
throw new Error('The packaged frame-copy smoke target is missing.');
}
assert.equal(target.cache, false);
assert.equal(target.dependsOn, undefined);
assert.match(
target.options?.command ?? '',
/playwright\.packaged\.config\.ts/
);
assert.match(
target.options?.command ?? '',
/embedded-mpv-frame-copy-packaged\.e2e\.ts/
);
assert.match(packagedConfig, /timeout:\s*120000/);
assert.match(packagedConfig, /webServer:\s*\[\]/);
assert.deepEqual(packagedPlaywrightConfig.webServer, []);
});
});
@@ -4,14 +4,16 @@ import type {
} from '@iptvnator/shared/interfaces';
import { spawnSync } from 'child_process';
import { createServer, type Server } from 'http';
import { existsSync, readFileSync, renameSync, statSync } from 'fs';
import { join } from 'path';
import sharp = require('sharp');
import {
closeElectronApp,
expect,
type LaunchedElectronApp,
} from './electron-test-fixtures';
import type {
DisposablePackagedLinuxApp,
RuntimeManifestGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
declare global {
interface Window {
@@ -25,18 +27,6 @@ export type LocalMediaServer = {
url: string;
};
export type RuntimeManifestGuard = {
hide: () => void;
restore: () => void;
};
export type PackagedRuntimeIdentity = {
arch: string;
platform: string;
profile: string;
runtimeMode: string;
};
function createTwoSecondY4mFixture(): Buffer {
const width = 64;
const height = 36;
@@ -157,72 +147,6 @@ export async function createLocalMediaServer(): Promise<LocalMediaServer> {
};
}
export function createRuntimeManifestGuard(
nativeDir: string
): RuntimeManifestGuard {
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
const hiddenRuntimeManifestPath = `${runtimeManifestPath}.e2e-hidden-${process.pid}`;
let hidden = false;
return {
hide() {
if (!statSync(runtimeManifestPath).isFile()) {
throw new Error(
`Packaged runtime manifest is not a regular file: ${runtimeManifestPath}`
);
}
if (existsSync(hiddenRuntimeManifestPath)) {
throw new Error(
`Stale hidden runtime manifest exists: ${hiddenRuntimeManifestPath}`
);
}
renameSync(runtimeManifestPath, hiddenRuntimeManifestPath);
hidden = true;
},
restore() {
if (!hidden) {
return;
}
if (!existsSync(hiddenRuntimeManifestPath)) {
throw new Error(
`Hidden runtime manifest disappeared before restore: ${hiddenRuntimeManifestPath}`
);
}
if (existsSync(runtimeManifestPath)) {
throw new Error(
`Refusing to overwrite runtime manifest during restore: ${runtimeManifestPath}`
);
}
renameSync(hiddenRuntimeManifestPath, runtimeManifestPath);
hidden = false;
},
};
}
export function readPackagedRuntimeIdentity(
nativeDir: string
): PackagedRuntimeIdentity {
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
const parsed = JSON.parse(
readFileSync(runtimeManifestPath, 'utf8')
) as Partial<PackagedRuntimeIdentity>;
for (const field of [
'arch',
'platform',
'profile',
'runtimeMode',
] as const) {
if (typeof parsed[field] !== 'string' || !parsed[field]) {
throw new Error(
`Packaged runtime manifest ${field} is invalid at ${runtimeManifestPath}.`
);
}
}
return parsed as PackagedRuntimeIdentity;
}
export function assertNativeFallbackPrerequisites(): void {
if (!process.env['DISPLAY']) {
throw new Error(
@@ -241,7 +165,7 @@ export function assertNativeFallbackPrerequisites(): void {
}
}
export async function installFrameCanvasAndSessionCapture(
export async function installEmbeddedMpvSessionCapture(
app: LaunchedElectronApp
): Promise<void> {
await app.mainWindow.evaluate(() => {
@@ -251,7 +175,14 @@ export async function installFrameCanvasAndSessionCapture(
window.electron.onEmbeddedMpvSessionUpdate?.((session) => {
window.__packagedEmbeddedMpvSessions?.push(session);
});
});
}
export async function installFrameCanvasAndSessionCapture(
app: LaunchedElectronApp
): Promise<void> {
await installEmbeddedMpvSessionCapture(app);
await app.mainWindow.evaluate(() => {
document.querySelector('canvas[data-embedded-mpv-frame]')?.remove();
const canvas = document.createElement('canvas');
canvas.dataset['embeddedMpvFrame'] = '';
@@ -290,6 +221,39 @@ export async function getLatestSession(
}, sessionId);
}
export function isMeaningfulNativePlaybackSnapshot(
snapshot: {
durationSeconds: number | null;
error?: string;
status: string;
streamUrl: string;
} | null,
expectedUrl: string
): boolean {
if (
!snapshot ||
snapshot.error ||
!['paused', 'playing'].includes(snapshot.status) ||
typeof snapshot.durationSeconds !== 'number' ||
!Number.isFinite(snapshot.durationSeconds) ||
snapshot.durationSeconds <= 0
) {
return false;
}
const normalizeUrl = (value: string): string => {
try {
const url = new URL(value);
url.hash = '';
return url.href;
} catch {
return value.trim();
}
};
return normalizeUrl(snapshot.streamUrl) === normalizeUrl(expectedUrl);
}
export async function renderedFrameSignal(
app: LaunchedElectronApp
): Promise<number> {
@@ -327,8 +291,9 @@ export async function closeAndWaitForExit(
export async function cleanupPackagedFrameCopySmoke(options: {
apps: Array<LaunchedElectronApp | undefined>;
media: LocalMediaServer;
runtimeManifest: RuntimeManifestGuard;
media?: LocalMediaServer;
packageClone?: DisposablePackagedLinuxApp;
runtimeManifest?: RuntimeManifestGuard;
}): Promise<void> {
const errors: unknown[] = [];
@@ -343,16 +308,28 @@ export async function cleanupPackagedFrameCopySmoke(options: {
}
}
try {
options.runtimeManifest.restore();
} catch (error) {
errors.push(error);
if (options.runtimeManifest) {
try {
options.runtimeManifest.restore();
} catch (error) {
errors.push(error);
}
}
try {
await options.media.close();
} catch (error) {
errors.push(error);
if (options.media) {
try {
await options.media.close();
} catch (error) {
errors.push(error);
}
}
if (options.packageClone) {
try {
options.packageClone.cleanup();
} catch (error) {
errors.push(error);
}
}
if (errors.length > 0) {
@@ -1,6 +1,5 @@
import {
expect,
getPackagedLinuxNativeDir,
launchPackagedElectronApp,
resolvePackagedLinuxExecutable,
test,
@@ -11,13 +10,21 @@ import {
cleanupPackagedFrameCopySmoke,
closeAndWaitForExit,
createLocalMediaServer,
createRuntimeManifestGuard,
getEmbeddedMpvSupport,
getLatestSession,
installEmbeddedMpvSessionCapture,
installFrameCanvasAndSessionCapture,
readPackagedRuntimeIdentity,
isMeaningfulNativePlaybackSnapshot,
renderedFrameSignal,
type LocalMediaServer,
} from './embedded-mpv-frame-copy-packaged-fixtures';
import {
createDisposablePackagedLinuxApp,
createRuntimeManifestGuard,
readPackagedRuntimeIdentity,
type DisposablePackagedLinuxApp,
type RuntimeManifestGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
const PACKAGED_FRAME_COPY_REQUIRED_ENV =
'IPTVNATOR_E2E_REQUIRE_PACKAGED_FRAME_COPY';
@@ -58,16 +65,24 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
`The dedicated packaged-runtime job must provide a real unpacked x64 executable with IPTVNATOR_E2E_PACKAGED_EXECUTABLE when ${PACKAGED_FRAME_COPY_REQUIRED_ENV}=1.`
).toBeTruthy();
const executablePath = packagedExecutable as string;
const sourceExecutablePath = packagedExecutable as string;
assertNativeFallbackPrerequisites();
const nativeDir = getPackagedLinuxNativeDir(executablePath);
const runtimeIdentity = readPackagedRuntimeIdentity(nativeDir);
const runtimeManifest = createRuntimeManifestGuard(nativeDir);
const media = await createLocalMediaServer();
let packageClone: DisposablePackagedLinuxApp | undefined;
let runtimeManifest: RuntimeManifestGuard | undefined;
let media: LocalMediaServer | undefined;
let frameCopyApp: LaunchedElectronApp | undefined;
let fallbackApp: LaunchedElectronApp | undefined;
try {
packageClone =
createDisposablePackagedLinuxApp(sourceExecutablePath);
const executablePath = packageClone.executablePath;
const nativeDir = packageClone.nativeDir;
const runtimeIdentity = readPackagedRuntimeIdentity(nativeDir);
runtimeManifest = createRuntimeManifestGuard(nativeDir);
const mediaServer = await createLocalMediaServer();
media = mediaServer;
expect(runtimeIdentity).toMatchObject({
arch: 'x64',
platform: 'linux',
@@ -116,7 +131,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
isLive: false,
});
},
{ sessionId: created.id, streamUrl: media.url }
{ sessionId: created.id, streamUrl: mediaServer.url }
);
await expect
@@ -128,7 +143,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
)
.toMatchObject({
status: 'paused',
streamUrl: media.url,
streamUrl: mediaServer.url,
videoHeight: 36,
videoWidth: 64,
});
@@ -229,6 +244,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
supported: true,
});
await installEmbeddedMpvSessionCapture(launchedFallbackApp);
const nativeSession = await launchedFallbackApp.mainWindow.evaluate(
async () => {
return window.electron.createEmbeddedMpvSession(
@@ -240,23 +256,46 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
);
expect(nativeSession.id).toMatch(/^embedded-mpv-/);
await launchedFallbackApp.mainWindow.evaluate(
(sessionId) =>
window.electron.disposeEmbeddedMpvSession(sessionId),
nativeSession.id
async ({ sessionId, streamUrl }) => {
await window.electron.loadEmbeddedMpvPlayback(sessionId, {
streamUrl,
title: 'Native-view generated Y4M fixture',
isLive: false,
});
},
{ sessionId: nativeSession.id, streamUrl: mediaServer.url }
);
await expect
.poll(() => launchedFallbackApp.mainWindow.title())
.toContain('IPTVnator');
.poll(
async () =>
isMeaningfulNativePlaybackSnapshot(
await getLatestSession(
launchedFallbackApp,
nativeSession.id
),
mediaServer.url
),
{ timeout: 15000 }
)
.toBe(true);
expect(
launchedFallbackApp.electronApp.process().exitCode
).toBeNull();
expect(
launchedFallbackApp.electronApp.process().signalCode
).toBeNull();
await launchedFallbackApp.mainWindow.evaluate(async (sessionId) => {
await window.electron.disposeEmbeddedMpvSession(sessionId);
window.__packagedEmbeddedMpvUnsubscribe?.();
}, nativeSession.id);
await expect
.poll(() => launchedFallbackApp.mainWindow.title())
.toContain('IPTVnator');
} finally {
await cleanupPackagedFrameCopySmoke({
apps: [frameCopyApp, fallbackApp],
media,
packageClone,
runtimeManifest,
});
}