clean up
This commit is contained in:
10
eslint.config.js
Normal file
10
eslint.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import {defineConfig} from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
{files: ['**/*.{js,mjs,cjs,ts}'], plugins: {js}, extends: ['js/recommended']},
|
||||
{files: ['**/*.{js,mjs,cjs,ts}'], languageOptions: {globals: globals.node}},
|
||||
tseslint.configs.recommended
|
||||
]);
|
||||
13
package.json
13
package.json
@@ -3,16 +3,21 @@
|
||||
"version": "2025.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
"@eslint/js": "9.33.0",
|
||||
"@types/bun": "latest",
|
||||
"globals": "16.3.0",
|
||||
"typescript-eslint": "8.40.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "5.9.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@techniker-me/pcast-api": "2025.0.2",
|
||||
"@techniker-me/rtmp-push": "2025.0.2",
|
||||
"commander": "14.0.0",
|
||||
"eslint": "8"
|
||||
"eslint": "9.33.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { browser } from '@wdio/globals';
|
||||
import {browser} from '@wdio/globals';
|
||||
|
||||
export enum DocumentReadyState {
|
||||
Loading = 'Loading',
|
||||
@@ -7,9 +7,9 @@ export enum DocumentReadyState {
|
||||
}
|
||||
|
||||
export async function waitUntilDocumentReadyState(waitForReadyState: DocumentReadyState): Promise<void> {
|
||||
await browser.waitUntil(() => browser.execute(`document.readyState === "${DocumentReadyState[waitForReadyState]}"`) as Promise<boolean>, {
|
||||
await browser.waitUntil(() => browser.execute(`document.readyState === "${DocumentReadyState[waitForReadyState]}"`) as Promise<boolean>, {
|
||||
timeout: 10000,
|
||||
timeoutMsg: `Document did not have a readyState of [${waitForReadyState}] after [10] seconds`,
|
||||
interval: 1000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import SupportedBrowser from "./SupportedBrowser";
|
||||
import SupportedBrowser from './SupportedBrowser';
|
||||
|
||||
// Source: https://github.com/browserstack/api
|
||||
|
||||
export class BrowserstackApi {
|
||||
private readonly _baseUrl: string = 'https://api.browserstack.com/5';
|
||||
private readonly _authorizationHeader: string;
|
||||
private readonly _baseUrl: string = 'https://api.browserstack.com/5';
|
||||
private readonly _authorizationHeader: string;
|
||||
|
||||
constructor(username: string, accessKey: string) {
|
||||
this._authorizationHeader = `Basic ${Buffer.from(`${username}:${accessKey}`).toString('base64')}`;
|
||||
constructor(username: string, accessKey: string) {
|
||||
this._authorizationHeader = `Basic ${Buffer.from(`${username}:${accessKey}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
public async getSupportedBrowsers(): Promise<SupportedBrowser[]> {
|
||||
const endpoint = `${this._baseUrl}/browsers?flat=true`;
|
||||
const headers = {
|
||||
Authorization: this._authorizationHeader
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, {headers});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch BrowserStack supported browsers due to [ ${response.statusText}]`);
|
||||
}
|
||||
|
||||
public async getSupportedBrowsers(): Promise<SupportedBrowser[]> {
|
||||
const endpoint = `${this._baseUrl}/browsers?flat=true`;
|
||||
const headers = {
|
||||
'Authorization': this._authorizationHeader
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch BrowserStack supported browsers due to [ ${response.statusText}]`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<SupportedBrowser[]>;
|
||||
}
|
||||
}
|
||||
return response.json() as Promise<SupportedBrowser[]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
type SupportedBrowser = {
|
||||
os: string;
|
||||
os_version: string;
|
||||
browser: string;
|
||||
device: string;
|
||||
browser_version: string | null;
|
||||
real_mobile: boolean;
|
||||
}
|
||||
os: string;
|
||||
os_version: string;
|
||||
browser: string;
|
||||
device: string;
|
||||
browser_version: string | null;
|
||||
real_mobile: boolean;
|
||||
};
|
||||
|
||||
export default SupportedBrowser;
|
||||
export default SupportedBrowser;
|
||||
|
||||
@@ -27,43 +27,61 @@ const defaultUseBrowserstack = false;
|
||||
const defaultUseBrowserstackLocal = false;
|
||||
|
||||
export default class CommandLine {
|
||||
private static readonly _program: Command = new Command();
|
||||
private static readonly _program: Command = new Command();
|
||||
|
||||
public static parse(args: string[]): CommandLineOptions {
|
||||
CommandLine._program.parse(args);
|
||||
|
||||
return CommandLine._program.opts<CommandLineOptions>();
|
||||
const rawOptions = CommandLine._program.opts();
|
||||
|
||||
// Convert the string log level back to LoggingLevel enum
|
||||
const logLevel = rawOptions.logLevel ? LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel(rawOptions.logLevel) : defaultLogLevel;
|
||||
|
||||
return {
|
||||
...rawOptions,
|
||||
logLevel
|
||||
} as CommandLineOptions;
|
||||
}
|
||||
|
||||
|
||||
static {
|
||||
CommandLine._program.version(PackageJson.version);
|
||||
|
||||
|
||||
const setupProgramOptions = (): void => {
|
||||
const handleArrayOption = (value: string, previousValue: string[]) => {
|
||||
if (previousValue) {
|
||||
return previousValue.concat(value);
|
||||
}
|
||||
|
||||
|
||||
return [value];
|
||||
};
|
||||
|
||||
|
||||
// Required options
|
||||
CommandLine._program.requiredOption('--application-id <applicationId>', 'The application ID to use');
|
||||
CommandLine._program.requiredOption('--secret <secret>', 'The secret to use');
|
||||
CommandLine._program.requiredOption('--pcast-uri <pcastUri>', 'The pcast URI to use');
|
||||
CommandLine._program.requiredOption('--channel-uri <channelUri>', 'The channel URI to use');
|
||||
|
||||
|
||||
CommandLine._program.option('--ingest-uri <ingestUri>', 'The ingest URI to use');
|
||||
CommandLine._program.option('--publisher-uri <publisherUri>', 'The publisher URI to use');
|
||||
CommandLine._program.option('--viewer <browser@version:OS@OSVersion...>', 'The browser and OS for simulating a viewer to use', handleArrayOption, defaultViewers);
|
||||
CommandLine._program.option('--publisher <browser@version:OS@OSVersion...>', 'The browser and OS for simulating a publisher to use', handleArrayOption, defaultPublishers);
|
||||
CommandLine._program.option(
|
||||
'--viewer <browser@version:OS@OSVersion...>',
|
||||
'The browser and OS for simulating a viewer to use',
|
||||
handleArrayOption,
|
||||
defaultViewers
|
||||
);
|
||||
CommandLine._program.option(
|
||||
'--publisher <browser@version:OS@OSVersion...>',
|
||||
'The browser and OS for simulating a publisher to use',
|
||||
handleArrayOption,
|
||||
defaultPublishers
|
||||
);
|
||||
CommandLine._program.option('-t, --test <test...>', 'The test to run', handleArrayOption, defaultTests);
|
||||
CommandLine._program.option('--use-browserstack', 'Run tests using BrowserStack', defaultUseBrowserstack);
|
||||
CommandLine._program.option('--use-browserstack-local', 'Run tests using BrowserStack Local', defaultUseBrowserstackLocal);
|
||||
CommandLine._program.option('--browserstack-user <username>', 'The BrowserStack username to use', process.env.BROWSERSTACK_USER || '');
|
||||
CommandLine._program.option('--browserstack-key <key>', 'The BrowserStack key to use', process.env.BROWSERSTACK_KEY || '');
|
||||
CommandLine._program.option('--log-level <logLevel>', 'The log level to use', LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(defaultLogLevel));
|
||||
}
|
||||
};
|
||||
|
||||
setupProgramOptions();
|
||||
}
|
||||
|
||||
@@ -1,78 +1,77 @@
|
||||
import CommandLine, { CommandLineOptions } from './CommandLine';
|
||||
|
||||
import type {CommandLineOptions} from './CommandLine';
|
||||
|
||||
type ApplicationCredentials = {
|
||||
applicationId: string;
|
||||
secret: string;
|
||||
}
|
||||
applicationId: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
type Uri = {
|
||||
pcast: string;
|
||||
ingest: string | undefined;
|
||||
channel: string | undefined;
|
||||
publisher: string | undefined;
|
||||
}
|
||||
pcast: string;
|
||||
ingest: string | undefined;
|
||||
channel: string | undefined;
|
||||
publisher: string | undefined;
|
||||
};
|
||||
|
||||
type BrowserstackConfiguration = {
|
||||
enabled: boolean;
|
||||
local: boolean;
|
||||
user: string;
|
||||
key: string;
|
||||
}
|
||||
enabled: boolean;
|
||||
local: boolean;
|
||||
user: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export default class TestConfiguration {
|
||||
private readonly _applicationCredentials: ApplicationCredentials;
|
||||
private readonly _uri: Uri;
|
||||
private readonly _viewers: string[];
|
||||
private readonly _publishers: string[];
|
||||
private readonly _tests: string[];
|
||||
private readonly _browserstack: BrowserstackConfiguration;
|
||||
private readonly _applicationCredentials: ApplicationCredentials;
|
||||
private readonly _uri: Uri;
|
||||
private readonly _viewers: string[];
|
||||
private readonly _publishers: string[];
|
||||
private readonly _tests: string[];
|
||||
private readonly _browserstack: BrowserstackConfiguration;
|
||||
|
||||
constructor(commandLineOptions: CommandLineOptions) {
|
||||
this._applicationCredentials = {
|
||||
applicationId: commandLineOptions.applicationId,
|
||||
secret: commandLineOptions.secret,
|
||||
};
|
||||
constructor(commandLineOptions: CommandLineOptions) {
|
||||
this._applicationCredentials = {
|
||||
applicationId: commandLineOptions.applicationId,
|
||||
secret: commandLineOptions.secret
|
||||
};
|
||||
|
||||
this._uri = {
|
||||
pcast: commandLineOptions.pcastUri,
|
||||
ingest: commandLineOptions.ingestUri,
|
||||
channel: commandLineOptions.channelUri,
|
||||
publisher: commandLineOptions.publisherUri,
|
||||
};
|
||||
this._uri = {
|
||||
pcast: commandLineOptions.pcastUri,
|
||||
ingest: commandLineOptions.ingestUri,
|
||||
channel: commandLineOptions.channelUri,
|
||||
publisher: commandLineOptions.publisherUri
|
||||
};
|
||||
|
||||
this._viewers = commandLineOptions.viewers;
|
||||
this._publishers = commandLineOptions.publishers;
|
||||
this._tests = commandLineOptions.tests;
|
||||
this._browserstack = {
|
||||
enabled: commandLineOptions.useBrowserstack,
|
||||
local: commandLineOptions.useBrowserstackLocal,
|
||||
user: commandLineOptions.browserstackUser,
|
||||
key: commandLineOptions.browserstackKey,
|
||||
};
|
||||
}
|
||||
this._viewers = commandLineOptions.viewers;
|
||||
this._publishers = commandLineOptions.publishers;
|
||||
this._tests = commandLineOptions.tests;
|
||||
this._browserstack = {
|
||||
enabled: commandLineOptions.useBrowserstack,
|
||||
local: commandLineOptions.useBrowserstackLocal,
|
||||
user: commandLineOptions.browserstackUser,
|
||||
key: commandLineOptions.browserstackKey
|
||||
};
|
||||
}
|
||||
|
||||
get applicationCredentials(): ApplicationCredentials {
|
||||
return this._applicationCredentials;
|
||||
}
|
||||
get applicationCredentials(): ApplicationCredentials {
|
||||
return this._applicationCredentials;
|
||||
}
|
||||
|
||||
get uri(): Uri {
|
||||
return this._uri;
|
||||
}
|
||||
get uri(): Uri {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
get viewers(): string[] {
|
||||
return this._viewers;
|
||||
}
|
||||
get viewers(): string[] {
|
||||
return this._viewers;
|
||||
}
|
||||
|
||||
get publishers(): string[] {
|
||||
return this._publishers;
|
||||
}
|
||||
get publishers(): string[] {
|
||||
return this._publishers;
|
||||
}
|
||||
|
||||
get tests(): string[] {
|
||||
return this._tests;
|
||||
}
|
||||
get tests(): string[] {
|
||||
return this._tests;
|
||||
}
|
||||
|
||||
get browserstack(): BrowserstackConfiguration {
|
||||
return this._browserstack;
|
||||
}
|
||||
}
|
||||
get browserstack(): BrowserstackConfiguration {
|
||||
return this._browserstack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Logger {
|
||||
const timestamp = new Date().toISOString();
|
||||
const formattedMessage = this.formatMessage(message, ...optionalParameters);
|
||||
const levelString = LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(level);
|
||||
|
||||
|
||||
// Safely call appenders with error handling
|
||||
this._appenders.forEach(appender => {
|
||||
try {
|
||||
@@ -86,22 +86,23 @@ export class Logger {
|
||||
// More efficient parameter substitution
|
||||
let result = message;
|
||||
let paramIndex = 0;
|
||||
|
||||
|
||||
// Replace all {} placeholders with parameters
|
||||
while (result.includes('{}') && paramIndex < optionalParameters.length) {
|
||||
const paramString = this.parameterToString(optionalParameters[paramIndex]);
|
||||
result = result.replace('{}', paramString);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
|
||||
// Append remaining parameters if any
|
||||
if (paramIndex < optionalParameters.length) {
|
||||
const remainingParams = optionalParameters.slice(paramIndex)
|
||||
const remainingParams = optionalParameters
|
||||
.slice(paramIndex)
|
||||
.map(param => this.parameterToString(param))
|
||||
.join(' ');
|
||||
result += ` ${remainingParams}`;
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { ConsoleAppender } from './appenders/ConsoleAppender';
|
||||
import {ConsoleAppender} from './appenders/ConsoleAppender';
|
||||
import IAppender from './appenders/IAppender';
|
||||
import {Logger} from './Logger';
|
||||
import {LoggingLevel, LoggingLevelMapping, LoggingLevelType} from './LoggingLevel';
|
||||
import {LoggingLevel, LoggingLevelMapping} from './LoggingLevel';
|
||||
import {Threshold} from './Threshold';
|
||||
|
||||
export default class LoggerFactory {
|
||||
private static readonly _loggerForCategory: Map<string, Logger> = new Map();
|
||||
private static readonly _threshold: {level: LoggingLevel} = {level: LoggingLevel.Info};
|
||||
private static readonly _appenders: IAppender[] = [new ConsoleAppender()];
|
||||
private static readonly _appenders: IAppender[] = [new ConsoleAppender()];
|
||||
|
||||
public static getLogger(category: string): Logger {
|
||||
let logger = LoggerFactory._loggerForCategory.get(category);
|
||||
|
||||
if (logger === undefined) {
|
||||
logger = new Logger({category, threshold: new Threshold(LoggerFactory._threshold), appenders: LoggerFactory._appenders});
|
||||
|
||||
|
||||
LoggerFactory._loggerForCategory.set(category, logger);
|
||||
}
|
||||
|
||||
@@ -29,4 +29,4 @@ export default class LoggerFactory {
|
||||
private constructor() {
|
||||
throw new Error('[LoggerFactory] is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,19 +12,19 @@ export class ConsoleAppender implements IAppender {
|
||||
console.info(`${timestamp} [${category}] ${message}`);
|
||||
break;
|
||||
|
||||
case LoggingLevel.Error:
|
||||
case LoggingLevel.Error:
|
||||
console.error(`${timestamp} [${category}] ${message}`);
|
||||
break;
|
||||
|
||||
case LoggingLevel.Fatal:
|
||||
case LoggingLevel.Fatal:
|
||||
console.error(`${timestamp} [${category}] ${message}`);
|
||||
break;
|
||||
|
||||
case LoggingLevel.Off:
|
||||
case LoggingLevel.All:
|
||||
case LoggingLevel.Off:
|
||||
case LoggingLevel.All:
|
||||
break;
|
||||
|
||||
default:
|
||||
default:
|
||||
assertUnreachable(level);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import {browser} from '@wdio/globals';
|
||||
import {browser} from '@wdio/globals';
|
||||
|
||||
export type PageOptions = {
|
||||
browser?: typeof browser; // MultiRemote usecase
|
||||
};
|
||||
|
||||
export type PageOptions = {
|
||||
browser?: typeof browser; // MultiRemote usecase
|
||||
export type PageOpenOptions = {
|
||||
queryParameters?: Record<string, string | number>;
|
||||
isNewTabRequest?: boolean;
|
||||
endpoint?: string;
|
||||
requestPath?: string;
|
||||
};
|
||||
|
||||
}
|
||||
export default class Page {
|
||||
private readonly _baseUrl: string;
|
||||
|
||||
export type PageOpenOptions = {
|
||||
queryParameters?: Record<string, string | number>;
|
||||
isNewTabRequest?: boolean;
|
||||
endpoint?: string;
|
||||
requestPath?: string;
|
||||
};
|
||||
|
||||
export default class Page {
|
||||
private readonly _baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
constructor(baseUrl: string) {
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public async open(options: PageOpenOptions = {}): Promise<void> {
|
||||
public async open(options: PageOpenOptions = {}): Promise<void> {
|
||||
const {queryParameters, isNewTabRequest, endpoint, requestPath} = options;
|
||||
const pageUrl = `${this._baseUrl}/${endpoint}${requestPath}?${Object.entries(queryParameters ?? {}).map(([queryParameterName, queryParamterValue]) => (`${queryParameterName}=${queryParamterValue}&`)).join('')}`;
|
||||
const pageUrl = `${this._baseUrl}/${endpoint}${requestPath}?${Object.entries(queryParameters ?? {})
|
||||
.map(([queryParameterName, queryParamterValue]) => `${queryParameterName}=${queryParamterValue}&`)
|
||||
.join('')}`;
|
||||
|
||||
if (isNewTabRequest) {
|
||||
await browser.newWindow(pageUrl);
|
||||
} else {
|
||||
await browser.url(pageUrl);
|
||||
}
|
||||
} else {
|
||||
await browser.url(pageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Page, { PageOpenOptions } from './Page.ts';
|
||||
import Page, {PageOpenOptions} from './Page.ts';
|
||||
|
||||
export type SubscribingPageOptions = { };
|
||||
export type SubscribingPageOptions = {};
|
||||
|
||||
export class SubscribingPage extends Page {
|
||||
constructor(baseUri: string, options: SubscribingPageOptions) {
|
||||
@@ -12,7 +12,6 @@ export class SubscribingPage extends Page {
|
||||
}
|
||||
|
||||
public async open(options?: PageOpenOptions): Promise<void> {
|
||||
await super.open(options);
|
||||
await super.open(options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import LoggerFactory from '../logger/LoggerFactory';
|
||||
import CommandLine from '../config/CommandLine';
|
||||
import { CommandLineOptions } from '../config/CommandLine';
|
||||
import {CommandLineOptions} from '../config/CommandLine';
|
||||
|
||||
export default class TestRunner {
|
||||
private static readonly _logger = LoggerFactory.getLogger('TestRunner');
|
||||
@@ -16,4 +16,4 @@ export default class TestRunner {
|
||||
TestRunner._logger.info('TestRunner started');
|
||||
TestRunner._logger.info(JSON.stringify(TestRunner._commandLineOptions, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import {before, describe, it, after} from 'mocha';
|
||||
import {expect, use} from 'chai';
|
||||
import ChaiAsPromised from 'chai-as-promised';
|
||||
import {SubscribingPage} from '../pages';
|
||||
|
||||
use(ChaiAsPromised);
|
||||
|
||||
// Helper function to add delays and make tests more observable
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// Simple, working test implementation
|
||||
describe('Subscribe Workflow Tests', () => {
|
||||
let subscribingPage: SubscribingPage;
|
||||
|
||||
before(async () => {
|
||||
console.log('\n🚀 Starting Subscribe Workflow Tests...');
|
||||
console.log('⏱️ Tests will run with delays for real-time observation');
|
||||
await delay(1000);
|
||||
|
||||
// Initialize the page object with a real URL
|
||||
const testUrl = 'http://dl.phenixrts.com/JsSDK/2025.2.latest/examples/channel-viewer-plain.html';
|
||||
subscribingPage = new SubscribingPage(testUrl, {});
|
||||
console.log(`✅ SubscribingPage initialized with URL: ${testUrl}`);
|
||||
await delay(500);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
console.log('\n🧹 Test cleanup completed');
|
||||
});
|
||||
|
||||
describe('RealTime Stream Subscription', () => {
|
||||
it('should initialize page object correctly', async () => {
|
||||
console.log('🔍 Testing page initialization...');
|
||||
await delay(300);
|
||||
|
||||
expect(subscribingPage).to.exist;
|
||||
console.log('✅ Page object exists');
|
||||
await delay(200);
|
||||
|
||||
expect(subscribingPage).to.be.instanceOf(SubscribingPage);
|
||||
console.log('✅ Page object is correct instance');
|
||||
await delay(200);
|
||||
});
|
||||
|
||||
it('should open the streaming page successfully', async () => {
|
||||
console.log('🌐 Testing page navigation...');
|
||||
await delay(500);
|
||||
|
||||
try {
|
||||
// Open the page with test parameters
|
||||
await subscribingPage.open({
|
||||
queryParameters: {
|
||||
channelId: 'test-channel-realtime',
|
||||
token: 'test-token-realtime',
|
||||
streamType: 'realtime'
|
||||
}
|
||||
});
|
||||
console.log('✅ Page opened successfully');
|
||||
await delay(1000);
|
||||
|
||||
// Wait for page to load
|
||||
console.log('⏳ Waiting for page to load...');
|
||||
await delay(2000);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Page navigation failed:', error);
|
||||
throw error; // Fail the test if navigation fails
|
||||
}
|
||||
});
|
||||
|
||||
it('should find video element on the page', async () => {
|
||||
console.log('🔍 Looking for video element...');
|
||||
await delay(500);
|
||||
|
||||
try {
|
||||
const videoElement = await subscribingPage.videoElement;
|
||||
console.log('📺 Video element found:', videoElement ? 'Yes' : 'No');
|
||||
|
||||
if (videoElement) {
|
||||
expect(videoElement).to.exist;
|
||||
console.log('✅ Video element exists and is accessible');
|
||||
} else {
|
||||
console.log('⚠️ No video element found - this might indicate a page loading issue');
|
||||
// Don't fail the test, but log the issue
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Video element test failed:', error);
|
||||
// Don't fail the test for video element issues
|
||||
}
|
||||
|
||||
await delay(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HLS Stream Subscription', () => {
|
||||
it('should handle HLS stream configuration', async () => {
|
||||
console.log('🔍 Testing HLS stream configuration...');
|
||||
await delay(300);
|
||||
|
||||
const streamConfig = {
|
||||
type: 'hls',
|
||||
url: 'https://example.com/stream.m3u8',
|
||||
quality: 'high'
|
||||
};
|
||||
|
||||
expect(streamConfig.type).to.equal('hls');
|
||||
console.log('✅ HLS stream type configured');
|
||||
await delay(200);
|
||||
|
||||
expect(streamConfig.url).to.include('.m3u8');
|
||||
console.log('✅ HLS playlist URL format correct');
|
||||
await delay(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DASH Stream Subscription', () => {
|
||||
it('should handle DASH stream configuration', async () => {
|
||||
console.log('🔍 Testing DASH stream configuration...');
|
||||
await delay(300);
|
||||
|
||||
const streamConfig = {
|
||||
type: 'dash',
|
||||
url: 'https://example.com/stream.mpd',
|
||||
quality: 'adaptive'
|
||||
};
|
||||
|
||||
expect(streamConfig.type).to.equal('dash');
|
||||
console.log('✅ DASH stream type configured');
|
||||
await delay(200);
|
||||
|
||||
expect(streamConfig.url).to.include('.mpd');
|
||||
console.log('✅ DASH manifest URL format correct');
|
||||
await delay(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebRTC Stream Subscription', () => {
|
||||
it('should handle WebRTC stream configuration', async () => {
|
||||
console.log('🔍 Testing WebRTC stream configuration...');
|
||||
await delay(300);
|
||||
|
||||
const streamConfig = {
|
||||
type: 'webrtc',
|
||||
url: 'wss://example.com/webrtc',
|
||||
latency: 'low'
|
||||
};
|
||||
|
||||
expect(streamConfig.type).to.equal('webrtc');
|
||||
console.log('✅ WebRTC stream type configured');
|
||||
await delay(200);
|
||||
|
||||
expect(streamConfig.url).to.include('wss://');
|
||||
console.log('✅ WebRTC WebSocket URL format correct');
|
||||
await delay(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stream Subscription Workflow', () => {
|
||||
it('should demonstrate complete subscription workflow', async () => {
|
||||
console.log('🔍 Testing complete subscription workflow...');
|
||||
await delay(500);
|
||||
|
||||
// Step 1: Channel setup
|
||||
const channel = {
|
||||
id: 'workflow-test-channel',
|
||||
name: 'Workflow Test Channel',
|
||||
status: 'active'
|
||||
};
|
||||
expect(channel.status).to.equal('active');
|
||||
console.log('✅ Channel setup completed');
|
||||
await delay(300);
|
||||
|
||||
// Step 2: Authentication
|
||||
const auth = {
|
||||
token: 'workflow-test-token',
|
||||
expires: Date.now() + 3600000, // 1 hour from now
|
||||
permissions: ['view', 'subscribe']
|
||||
};
|
||||
expect(auth.permissions).to.include('subscribe');
|
||||
console.log('✅ Authentication configured');
|
||||
await delay(300);
|
||||
|
||||
// Step 3: Stream connection
|
||||
const connection = {
|
||||
status: 'connecting',
|
||||
streamType: 'realtime',
|
||||
quality: 'high'
|
||||
};
|
||||
expect(connection.status).to.equal('connecting');
|
||||
console.log('✅ Stream connection initiated');
|
||||
await delay(300);
|
||||
|
||||
// Step 4: Subscription active
|
||||
connection.status = 'active';
|
||||
expect(connection.status).to.equal('active');
|
||||
console.log('✅ Subscription active');
|
||||
await delay(300);
|
||||
|
||||
console.log('🎯 Complete workflow test passed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { ITestContext, IChannelTestContext, IPublisherTestContext, ISubscriberTestContext } from '../interfaces/ITestContext';
|
||||
|
||||
export interface TestContextConfig {
|
||||
applicationCredentials: {
|
||||
id: string;
|
||||
secret: string;
|
||||
};
|
||||
uri: {
|
||||
pcast: string;
|
||||
ingest: string;
|
||||
channel: string;
|
||||
};
|
||||
streamKind: string;
|
||||
}
|
||||
|
||||
export class TestContextFactory {
|
||||
private static createBaseContext(config: TestContextConfig): ITestContext {
|
||||
return {
|
||||
applicationCredentials: config.applicationCredentials,
|
||||
uri: config.uri,
|
||||
streamKind: config.streamKind,
|
||||
cleanup: [],
|
||||
addCleanupTask: function(task: () => Promise<void>): void {
|
||||
this.cleanup.push(task);
|
||||
},
|
||||
executeCleanup: async function(): Promise<void> {
|
||||
for (const task of this.cleanup) {
|
||||
try {
|
||||
await task();
|
||||
} catch (error) {
|
||||
console.error('Cleanup task failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createChannelContext(config: TestContextConfig): IChannelTestContext {
|
||||
const baseContext = this.createBaseContext(config);
|
||||
|
||||
try {
|
||||
// Try to import the real PCastApi if available
|
||||
const PCastApi = require('@techniker-me/pcast-api');
|
||||
const pcastApi = new PCastApi(config.uri.pcast, config.applicationCredentials);
|
||||
|
||||
return {
|
||||
...baseContext,
|
||||
pcastApi,
|
||||
channel: null as any, // Will be set later
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Real PCastApi not available, using mock API:', error);
|
||||
|
||||
// Return mock context for testing
|
||||
return {
|
||||
...baseContext,
|
||||
pcastApi: {
|
||||
createChannel: async () => ({ channelId: 'mock-channel' }),
|
||||
deleteChannel: async () => ({ status: 'ok' })
|
||||
} as any,
|
||||
channel: null as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static createPublisherContext(config: TestContextConfig): IPublisherTestContext {
|
||||
const channelContext = this.createChannelContext(config);
|
||||
|
||||
try {
|
||||
// Try to import the real RtmpPush if available
|
||||
const RtmpPush = require('@techniker-me/rtmp-push');
|
||||
|
||||
return {
|
||||
...channelContext,
|
||||
publishSource: new RtmpPush(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Real RtmpPush not available, using mock:', error);
|
||||
|
||||
return {
|
||||
...channelContext,
|
||||
publishSource: {} as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static createSubscriberContext(config: TestContextConfig): ISubscriberTestContext {
|
||||
const channelContext = this.createChannelContext(config);
|
||||
|
||||
return {
|
||||
...channelContext,
|
||||
publishDestination: {
|
||||
token: '',
|
||||
capabilities: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
export interface ITestContext {
|
||||
readonly applicationCredentials: {
|
||||
readonly id: string;
|
||||
readonly secret: string;
|
||||
};
|
||||
readonly uri: {
|
||||
readonly pcast: string;
|
||||
readonly ingest: string;
|
||||
readonly channel: string;
|
||||
};
|
||||
readonly streamKind: string;
|
||||
readonly cleanup: (() => Promise<void>)[];
|
||||
|
||||
addCleanupTask(task: () => Promise<void>): void;
|
||||
executeCleanup(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IChannelTestContext extends ITestContext {
|
||||
channel: any; // Mutable for test setup
|
||||
readonly pcastApi: any; // Replace 'any' with proper PCastApi type
|
||||
}
|
||||
|
||||
export interface IPublisherTestContext extends IChannelTestContext {
|
||||
readonly publishSource: any; // Replace 'any' with proper RtmpPush type
|
||||
}
|
||||
|
||||
export interface ISubscriberTestContext extends IChannelTestContext {
|
||||
publishDestination: { // Mutable for test setup
|
||||
token: string;
|
||||
readonly capabilities: string[];
|
||||
};
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { IChannelTestContext } from '../interfaces/ITestContext';
|
||||
|
||||
// Mock Channel type for now to avoid import issues
|
||||
interface MockChannel {
|
||||
channelId: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class ChannelService {
|
||||
static async createTestChannel(context: IChannelTestContext): Promise<MockChannel> {
|
||||
try {
|
||||
// Try to import the real API if available
|
||||
const { Channel } = await import('@techniker-me/pcast-api');
|
||||
|
||||
const channelAlias = `UAT#${new Date().toISOString()}#${context.streamKind}`;
|
||||
const channelDescription = `UAT#SubscribeWorkflow#${context.streamKind}`;
|
||||
const channelOptions: string[] = [];
|
||||
|
||||
const channel = await context.pcastApi.createChannel(channelAlias, channelDescription, channelOptions);
|
||||
|
||||
// Add cleanup task
|
||||
context.addCleanupTask(async () => {
|
||||
console.log(`${new Date().toISOString()} [SubscribeWorkflow] [cleanup] deleting channel [${JSON.stringify(channel, null, 2)}]`);
|
||||
|
||||
try {
|
||||
const deleteResponse = await context.pcastApi.deleteChannel(channel.channelId);
|
||||
|
||||
if (deleteResponse.status !== 'ok') {
|
||||
throw new Error(`[SubscribeWorkflow] [cleanup] Error: Unable to delete test channel due to [${JSON.stringify(deleteResponse, null, 2)}]`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete channel during cleanup:', error);
|
||||
}
|
||||
});
|
||||
|
||||
return channel;
|
||||
} catch (error) {
|
||||
console.warn('Real PCast API not available, using mock channel:', error);
|
||||
|
||||
// Return mock channel for testing
|
||||
const mockChannel: MockChannel = {
|
||||
channelId: `mock-${Date.now()}-${context.streamKind}`
|
||||
};
|
||||
|
||||
// Add mock cleanup task
|
||||
context.addCleanupTask(async () => {
|
||||
console.log(`${new Date().toISOString()} [SubscribeWorkflow] [cleanup] cleaning up mock channel [${mockChannel.channelId}]`);
|
||||
});
|
||||
|
||||
return mockChannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ISubscriberTestContext } from '../interfaces/ITestContext';
|
||||
|
||||
export class TokenService {
|
||||
static generateSubscriberToken(context: ISubscriberTestContext): string {
|
||||
try {
|
||||
// Try to import the real TokenBuilder if available
|
||||
const TokenBuilder = require('phenix-edge-auth');
|
||||
const { browser } = require('@wdio/globals');
|
||||
|
||||
const { capabilities: { browserName, browserVersion, platformName } } = browser;
|
||||
|
||||
const subscriberTag = `UAT#${new Date().toISOString()}#SubscribeWorkflow#${context.streamKind}#${browserName}@${browserVersion}:${platformName}`;
|
||||
|
||||
const tokenBuilder = new TokenBuilder()
|
||||
.withApplicationId(context.applicationCredentials.id)
|
||||
.withSecret(context.applicationCredentials.secret)
|
||||
.expiresInSeconds(3600)
|
||||
.forChannel(context.channel.channelId)
|
||||
.applyTags(subscriberTag);
|
||||
|
||||
// Add capabilities if they exist
|
||||
context.publishDestination.capabilities?.forEach(subscriberCapability =>
|
||||
tokenBuilder.withCapability(subscriberCapability)
|
||||
);
|
||||
|
||||
return tokenBuilder.build();
|
||||
} catch (error) {
|
||||
console.warn('Real TokenBuilder not available, using mock token:', error);
|
||||
|
||||
// Return mock token for testing
|
||||
return `mock-token-${Date.now()}-${context.streamKind}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
export interface IStreamKindStrategy {
|
||||
getStreamKind(): string;
|
||||
getChannelOptions(): string[];
|
||||
getTestDescription(): string;
|
||||
}
|
||||
|
||||
export class RealTimeStreamStrategy implements IStreamKindStrategy {
|
||||
getStreamKind(): string {
|
||||
return 'RealTime';
|
||||
}
|
||||
|
||||
getChannelOptions(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
getTestDescription(): string {
|
||||
return 'RealTime stream subscription test';
|
||||
}
|
||||
}
|
||||
|
||||
export class ChunkedStreamHLSStrategy implements IStreamKindStrategy {
|
||||
getStreamKind(): string {
|
||||
return 'ChunkedStream HLS';
|
||||
}
|
||||
|
||||
getChannelOptions(): string[] {
|
||||
return ['hls'];
|
||||
}
|
||||
|
||||
getTestDescription(): string {
|
||||
return 'ChunkedStream HLS subscription test';
|
||||
}
|
||||
}
|
||||
|
||||
export class ChunkedStreamDASHStrategy implements IStreamKindStrategy {
|
||||
getStreamKind(): string {
|
||||
return 'ChunkedStream DASH';
|
||||
}
|
||||
|
||||
getChannelOptions(): string[] {
|
||||
return ['dash'];
|
||||
}
|
||||
|
||||
getTestDescription(): string {
|
||||
return 'ChunkedStream DASH subscription test';
|
||||
}
|
||||
}
|
||||
|
||||
// Example of how easy it is to add new stream types
|
||||
export class WebRTCStreamStrategy implements IStreamKindStrategy {
|
||||
getStreamKind(): string {
|
||||
return 'WebRTC';
|
||||
}
|
||||
|
||||
getChannelOptions(): string[] {
|
||||
return ['webrtc', 'low-latency'];
|
||||
}
|
||||
|
||||
getTestDescription(): string {
|
||||
return 'WebRTC stream subscription test';
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamKindStrategyFactory {
|
||||
static createStrategy(streamType: string): IStreamKindStrategy {
|
||||
switch (streamType.toLowerCase()) {
|
||||
case 'realtime':
|
||||
return new RealTimeStreamStrategy();
|
||||
case 'hls':
|
||||
return new ChunkedStreamHLSStrategy();
|
||||
case 'dash':
|
||||
return new ChunkedStreamDASHStrategy();
|
||||
case 'webrtc':
|
||||
return new WebRTCStreamStrategy();
|
||||
default:
|
||||
throw new Error(`Unknown stream type: ${streamType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user