Skip to content

Commit b5ad984

Browse files
authored
ref: Allow tests to specify the browsers they use in TestProperties. (#16313)
* ref: Allow tests to specify the browsers they use in TestProperties.
1 parent 81ce664 commit b5ad984

File tree

9 files changed

+570
-170
lines changed

9 files changed

+570
-170
lines changed

package-lock.json

Lines changed: 390 additions & 96 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"dayjs": "1.11.13",
6060
"dropbox": "10.7.0",
6161
"focus-visible": "5.1.0",
62+
"glob": "11.0.3",
6263
"grapheme-splitter": "1.0.4",
6364
"i18n-iso-countries": "6.8.0",
6465
"i18next": "17.0.6",

tests/helpers/TestProperties.ts

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,46 @@ export type ITestProperties = {
88
useJaas: boolean;
99
/** The test requires the webhook proxy. */
1010
useWebhookProxy: boolean;
11+
usesBrowsers?: string[];
1112
};
1213

1314
const defaultProperties: ITestProperties = {
1415
useIFrameApi: false,
1516
useWebhookProxy: false,
16-
useJaas: false
17+
useJaas: false,
18+
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
1719
};
1820

19-
const testProperties: Record<string, ITestProperties> = {};
21+
function getDefaultProperties(filename: string): ITestProperties {
22+
const properties = { ...defaultProperties };
23+
24+
properties.usesBrowsers = getDefaultBrowsers(filename);
25+
26+
return properties;
27+
}
28+
29+
function getDefaultBrowsers(filename: string): string[] {
30+
if (filename.includes('/alone/')) {
31+
return [ 'p1' ];
32+
}
33+
if (filename.includes('/2way/')) {
34+
return [ 'p1', 'p2' ];
35+
}
36+
if (filename.includes('/3way/')) {
37+
return [ 'p1', 'p2', 'p3' ];
38+
}
39+
if (filename.includes('/4way/')) {
40+
return [ 'p1', 'p2', 'p3', 'p4' ];
41+
}
42+
43+
// Tests outside /alone/, /2way/, /3way/, /4way/ will default to p1 only.
44+
return [ 'p1' ];
45+
}
46+
47+
/**
48+
* Maps a test filename to its registered properties.
49+
*/
50+
export const testProperties: Record<string, ITestProperties> = {};
2051

2152
/**
2253
* Set properties for a test file. This was needed because I couldn't find a hook that executes with describe() before
@@ -31,13 +62,72 @@ export function setTestProperties(filename: string, properties: Partial<ITestPro
3162
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
3263
}
3364

34-
testProperties[filename] = { ...defaultProperties, ...properties };
65+
testProperties[filename] = { ...getDefaultProperties(filename), ...properties };
66+
}
67+
68+
let testFilesLoaded = false;
69+
70+
/**
71+
* Loads test files to populate the testProperties registry. This function:
72+
* 1. Mocks test framework globals to prevent test registration
73+
* 2. require()s each file to trigger setTestProperties calls
74+
* 3. Restores original test framework functions
75+
*
76+
* @param files - Array of file names to load
77+
*/
78+
export function loadTestFiles(files: string[]): void {
79+
if (testFilesLoaded) {
80+
return;
81+
}
82+
83+
// Temporarily override test functions to prevent tests registering at this stage. We only want TestProperties to be
84+
// loaded.
85+
const originalTestFunctions: Record<string, any> = {};
86+
const testGlobals = [ 'describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'before', 'after', 'beforeAll',
87+
'afterAll', 'suite', 'setup', 'teardown' ];
88+
89+
testGlobals.forEach(fn => {
90+
originalTestFunctions[fn] = (global as any)[fn];
91+
(global as any)[fn] = () => {
92+
// do nothing
93+
};
94+
});
95+
96+
try {
97+
// Load all spec files to trigger setTestProperties calls
98+
files.forEach(file => {
99+
try {
100+
require(file);
101+
if (!testProperties[file]) {
102+
// If no properties were set, apply defaults
103+
setTestProperties(file, getDefaultProperties(file));
104+
}
105+
} catch (error) {
106+
console.warn(`Warning: Could not analyze ${file}:`, (error as Error).message);
107+
}
108+
});
109+
testFilesLoaded = true;
110+
111+
} finally {
112+
// Restore original functions
113+
testGlobals.forEach(fn => {
114+
if (originalTestFunctions[fn] !== undefined) {
115+
(global as any)[fn] = originalTestFunctions[fn];
116+
} else {
117+
delete (global as any)[fn];
118+
}
119+
});
120+
// Clear require cache for analyzed files so they can be loaded fresh by WebDriverIO
121+
files.forEach(file => {
122+
delete require.cache[file];
123+
});
124+
}
35125
}
36126

37127
/**
38128
* @param testFilePath - The absolute path to the test file
39129
* @returns Promise<ITestProperties> - The test properties with defaults applied
40130
*/
41131
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
42-
return testProperties[testFilePath] || { ...defaultProperties };
132+
return testProperties[testFilePath] || getDefaultProperties(testFilePath);
43133
}

tests/specs/jaas/maxOccupants.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
33

44
setTestProperties(__filename, {
55
useJaas: true,
6-
useWebhookProxy: true
6+
useWebhookProxy: true,
7+
usesBrowsers: [ 'p1', 'p2', 'p3' ]
78
});
89

910
describe('MaxOccupants limit enforcement', () => {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ describe('Setting passcode through settings provisioning', () => {
1717
};
1818

1919
await joinWithPassword(ctx.roomName, 'p1', t({ room: ctx.roomName }));
20-
await joinWithPassword(ctx.roomName, 'p2', t({ room: ctx.roomName, moderator: true }));
21-
await joinWithPassword(ctx.roomName, 'p3', t({ room: ctx.roomName, visitor: true }));
20+
await joinWithPassword(ctx.roomName, 'p1', t({ room: ctx.roomName, moderator: true }));
21+
await joinWithPassword(ctx.roomName, 'p1', t({ room: ctx.roomName, visitor: true }));
2222
});
2323
it('With an invalid passcode', async () => {
2424
ctx.webhooksProxy.defaultMeetingSettings = {

tests/specs/jaas/visitors/participantsSoftLimit.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
33

44
setTestProperties(__filename, {
55
useJaas: true,
6-
useWebhookProxy: true
6+
useWebhookProxy: true,
7+
usesBrowsers: [ 'p1', 'p2', 'p3' ]
78
});
89

910
describe('Visitors triggered by reaching participantsSoftLimit', () => {

tests/specs/jaas/visitors/visitorTokens.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
33

44
setTestProperties(__filename, {
55
useJaas: true,
6-
useWebhookProxy: true
6+
useWebhookProxy: true,
7+
usesBrowsers: [ 'p1', 'p2', 'p3' ]
78
});
89

910
describe('Visitors triggered by visitor tokens', () => {
@@ -50,7 +51,7 @@ describe('Visitors triggered by visitor tokens', () => {
5051
// Joining with a participant token after visitors...:mindblown:
5152
const v2 = await joinMuc(
5253
ctx.roomName,
53-
'p4',
54+
'p2',
5455
t({ room: ctx.roomName, displayName: 'Visi Tor 2' }));
5556

5657
expect(await v2.isInMuc()).toBe(true);

tests/wdio.conf.ts

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import AllureReporter from '@wdio/allure-reporter';
22
import { multiremotebrowser } from '@wdio/globals';
33
import { Buffer } from 'buffer';
4+
import { glob } from 'glob';
45
import path from 'node:path';
56
import process from 'node:process';
67
import pretty from 'pretty';
78

8-
import { getTestProperties } from './helpers/TestProperties';
9+
import { getTestProperties, loadTestFiles } from './helpers/TestProperties';
910
import WebhookProxy from './helpers/WebhookProxy';
1011
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
1112
import { IContext } from './helpers/types';
@@ -55,15 +56,80 @@ const chromePreferences = {
5556
'intl.accept_languages': 'en-US'
5657
};
5758

59+
const specs = [
60+
'specs/**/*.spec.ts'
61+
];
62+
63+
/**
64+
* Analyzes test files at config construction time to determine browser requirements
65+
* and generate capabilities with appropriate exclusions.
66+
*/
67+
function generateCapabilitiesFromSpecs(): Record<string, any> {
68+
const allSpecFiles: string[] = [];
69+
const browsers = [ 'p1', 'p2', 'p3', 'p4' ];
70+
71+
for (const pattern of specs) {
72+
const matches = glob.sync(pattern, { cwd: path.join(__dirname) });
73+
74+
allSpecFiles.push(...matches.map(f => path.resolve(__dirname, f)));
75+
}
76+
77+
// Load test files to populate the testProperties registry
78+
loadTestFiles(allSpecFiles);
79+
80+
// Import TestProperties to access the populated registry
81+
const { testProperties } = require('./helpers/TestProperties');
82+
83+
// Determine which browsers need which exclusions
84+
const browserExclusions: Record<string, Set<string>> = {
85+
p1: new Set(),
86+
p2: new Set(),
87+
p3: new Set(),
88+
p4: new Set()
89+
};
90+
91+
for (const file of allSpecFiles) {
92+
const props = testProperties[file];
93+
const relativeFile = path.relative(__dirname, file);
94+
95+
// If a test doesn't use a particular browser, add it to exclusions for that browser
96+
if (props?.usesBrowsers) {
97+
browsers.forEach(browser => {
98+
if (!props.usesBrowsers!.includes(browser)) {
99+
browserExclusions[browser].add(relativeFile);
100+
}
101+
});
102+
}
103+
}
104+
105+
return Object.fromEntries(
106+
browsers.map(browser => [
107+
browser,
108+
{
109+
capabilities: {
110+
browserName: 'chrome',
111+
...(browser === 'p1' && process.env.BROWSER_CHROME_BETA ? { browserVersion: 'beta' } : {}),
112+
'goog:chromeOptions': {
113+
args: chromeArgs,
114+
prefs: chromePreferences
115+
},
116+
'wdio:exclude': Array.from(browserExclusions[browser] || [])
117+
}
118+
}
119+
])
120+
);
121+
}
122+
123+
const capabilities = generateCapabilitiesFromSpecs();
124+
58125
const TEST_RESULTS_DIR = 'test-results';
59126

60127
export const config: WebdriverIO.MultiremoteConfig = {
61128

62129
runner: 'local',
63130

64-
specs: [
65-
'specs/**/*.spec.ts'
66-
],
131+
specs,
132+
67133
maxInstances: parseInt(process.env.MAX_INSTANCES || '1', 10), // if changing check onWorkerStart logic
68134

69135
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/',
@@ -85,61 +151,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
85151
timeout: 180_000
86152
},
87153

88-
capabilities: {
89-
// participant1
90-
p1: {
91-
capabilities: {
92-
browserName: 'chrome',
93-
browserVersion: process.env.BROWSER_CHROME_BETA ? 'beta' : undefined,
94-
'goog:chromeOptions': {
95-
args: chromeArgs,
96-
prefs: chromePreferences
97-
}
98-
}
99-
},
100-
// participant2
101-
p2: {
102-
capabilities: {
103-
browserName: 'chrome',
104-
'goog:chromeOptions': {
105-
args: chromeArgs,
106-
prefs: chromePreferences
107-
},
108-
'wdio:exclude': [
109-
'specs/alone/**'
110-
]
111-
}
112-
},
113-
// participant3
114-
p3: {
115-
capabilities: {
116-
browserName: 'chrome',
117-
'goog:chromeOptions': {
118-
args: chromeArgs,
119-
prefs: chromePreferences
120-
},
121-
'wdio:exclude': [
122-
'specs/alone/**',
123-
'specs/2way/**'
124-
]
125-
}
126-
},
127-
// participant4
128-
p4: {
129-
capabilities: {
130-
browserName: 'chrome',
131-
'goog:chromeOptions': {
132-
args: chromeArgs,
133-
prefs: chromePreferences
134-
},
135-
'wdio:exclude': [
136-
'specs/alone/**',
137-
'specs/2way/**',
138-
'specs/3way/**'
139-
]
140-
}
141-
}
142-
},
154+
capabilities,
143155

144156
// Level of logging verbosity: trace | debug | info | warn | error | silent
145157
logLevel: 'trace',
@@ -176,12 +188,12 @@ export const config: WebdriverIO.MultiremoteConfig = {
176188
*
177189
* @returns {Promise<void>}
178190
*/
179-
async before(cid, _, specs) {
180-
if (specs.length !== 1) {
191+
async before(cid, _, files) {
192+
if (files.length !== 1) {
181193
console.warn('We expect to run a single suite, but got more than one');
182194
}
183195

184-
const testFilePath = specs[0].replace(/^file:\/\//, '');
196+
const testFilePath = files[0].replace(/^file:\/\//, '');
185197
const testName = path.relative('tests/specs', testFilePath)
186198
.replace(/.spec.ts$/, '')
187199
.replace(/\//g, '-');
@@ -267,7 +279,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
267279
ctx?.keepAlive?.forEach(clearInterval);
268280
},
269281

270-
beforeSession(c, capabilities, specs, cid) {
282+
beforeSession(c, capabilities_, specs_, cid) {
271283
const originalBefore = c.before;
272284

273285
if (!originalBefore || !Array.isArray(originalBefore) || originalBefore.length !== 1) {

tests/wdio.firefox.conf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const mergedConfig = merge(defaultConfig, {
2929
'specs/4way/desktopSharing.spec.ts',
3030
'specs/4way/lastN.spec.ts',
3131

32-
// when unmuting a participant, we see the presence in debug logs imidiately,
32+
// when unmuting a participant, we see the presence in debug logs immediately,
3333
// but for 15 seconds it is not received/processed by the client
3434
// (also the menu disappears after clicking one of the moderation options, does not happen manually)
3535
'specs/3way/audioVideoModeration.spec.ts'

0 commit comments

Comments
 (0)