Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
- Added a `--pageHost` CLI option to `bedrock-auto`. This can be useful to set `host.docker.internal` when the browser is in a docker container. #TINYMCE-14879

### Fixed
- The grunt task passed config values to bedrock without type coercion, so string values for `Number` or `Boolean` options (e.g. `--webdriverPort=14444` or `--useSelenium=false` via `grunt.option`) were rejected or misread. #TINYMCE-14879
- Extended test start pings to every page load, allowing updates during a chunk or fail-retry reload. This should reduce how often the server's no-updates watchdog is tripped. #TINYMCE-14879
- Safari 26 failed to load the test page through the LambdaTest tunnel on `localhost`. The LT tunnel URL now uses LambdaTest's `localhost.lambdatest.com` DNS alias for all browsers. #TINYMCE-14879

## 17.0.1 - 2026-08-28

### Fixed
Expand Down
7 changes: 5 additions & 2 deletions modules/runner/src/main/ts/reporter/Reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const Reporter = (params: UrlParams, callbacks: Callbacks, ui: ReporterUi
let skipCount = 0;
let failCount = 0;
let finished = false;
let sentPageStart = false;

// A list of test results we are going to send as a batch to the server
const testResults: TestReport[] = [];
Expand Down Expand Up @@ -123,8 +124,10 @@ export const Reporter = (params: UrlParams, callbacks: Callbacks, ui: ReporterUi
const testUi = ui.test();

const sendStart = (): Promise<void> => {
if (currentCount === 1) {
// we need to send test start once to establish the session
if (!sentPageStart) {
// We don't want one start notification per test, these are blocking which is very slow over remote connections.
// This is a balance; once every page load (initial and chunk/retry loads).
sentPageStart = true;
requestsInFlight.push(callbacks.sendTestStart(params.session, currentCount, totalNumTests, file, name, takeResults()));
return Promise.resolve();
} else if (mouse.hasMoved()) {
Expand Down
43 changes: 35 additions & 8 deletions modules/runner/src/test/ts/reporter/ReporterTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('Reporter.test', () => {
let reporter: Reporter, startTestData: StartTestData[], endTestData: EndTestData[];
let doneCalled: boolean, doneError: string | undefined, offset: number;
let mouseMoved: boolean, startFails: boolean;
let startGate: Promise<void> | null;
const mouse: MouseWatch = {
hasMoved: () => mouseMoved,
clear: () => {
Expand All @@ -67,7 +68,7 @@ describe('Reporter.test', () => {
}
startTestData.push({ session, currentCount, totalTests, file, name, results });
results.forEach(r => endTestData.push({ session, ...r }));
return Promise.resolve();
return startGate !== null ? startGate : Promise.resolve();
},
sendTestResults: (session, results) => {
results.forEach(r => endTestData.push({session, ...r}));
Expand All @@ -84,6 +85,7 @@ describe('Reporter.test', () => {
offset = newOffset;
mouseMoved = false;
startFails = false;
startGate = null;
reporter = Reporter({ ...params, offset }, callbacks, ui, mouse);
startTestData = [];
endTestData = [];
Expand Down Expand Up @@ -130,8 +132,9 @@ describe('Reporter.test', () => {
mouseMoved = true;
return reporter.test('SomeTest.ts', 'second', 10).start();
}).then(() => {
assert.equal(startTestData.length, 1, 'Checking start test data was sent');
assert.deepEqual(startTestData[0].results.map((r) => r.name), [ 'first' ], 'Checking the batch rode along');
assert.equal(startTestData.length, 2, 'Checking both test starts were sent');
assert.deepEqual(startTestData[0].results, [], 'Checking the page load start had nothing to send');
assert.deepEqual(startTestData[1].results.map((r) => r.name), [ 'first' ], 'Checking the batch rode along');
assert.equal(endTestData.length, 1, 'Checking the result was recorded');
return reporter.waitForResults().then(() => {
assert.equal(endTestData.length, 1, 'Checking the result was not also sent separately');
Expand All @@ -148,7 +151,7 @@ describe('Reporter.test', () => {
startFails = true;
return reporter.test('SomeTest.ts', 'second', 10).start();
}).then(() => {
assert.equal(startTestData.length, 0, 'Checking the start was not recorded');
assert.equal(startTestData.length, 1, 'Checking only the page load start was recorded');
assert.isTrue(mouseMoved, 'Checking the mouse watch was not cleared');
return reporter.waitForResults().then(() => {
assert.deepEqual(endTestData.map((r) => r.name), [ 'first' ], 'Checking the result was sent later instead of lost');
Expand All @@ -160,16 +163,40 @@ describe('Reporter.test', () => {
reset(5);
const first = reporter.test('SomeTest.ts', 'first', 10);
return first.start().then(() => {
assert.equal(startTestData.length, 0, 'Checking no start test data was sent');
mouseMoved = true;
assert.equal(startTestData.length, 1, 'Checking only the page load start was sent');
return reporter.test('SomeTest.ts', 'second', 10).start();
}).then(() => {
assert.equal(startTestData.length, 1, 'Checking start test data was sent');
assert.equal(startTestData[0].currentCount, 7, 'Checking the test number');
assert.equal(startTestData.length, 1, 'Checking no start was sent while the mouse was still');
mouseMoved = true;
return reporter.test('SomeTest.ts', 'third', 10).start();
}).then(() => {
assert.equal(startTestData.length, 2, 'Checking start test data was sent');
assert.equal(startTestData[1].currentCount, 8, 'Checking the test number');
assert.isFalse(mouseMoved, 'Checking the mouse watch was cleared');
});
});

it('TINYMCE-14879: posts a start for the first test after a page reload without awaiting it', () => {
reset(5);
let releaseStart: () => void = noop;
startGate = new Promise((resolve) => {
releaseStart = resolve;
});
return reporter.test('SomeTest.ts', 'first', 10).start().then(() => {
assert.equal(startTestData.length, 1, 'Checking the start was posted before its request resolved');
assert.deepEqual(startTestData[0], {
currentCount: offset + 1,
session: sessionId,
totalTests: 10,
file: 'SomeTest.ts',
name: 'first',
results: []
}, 'Checking start test data contents');
releaseStart();
return reporter.waitForResults();
});
});

it('should report the session id, file, name, passed state and time on a skipped test', () => {
return fc.assert(fc.asyncProperty(fc.string(), fc.string(), fc.string(), fc.integer({ max: offset }), (fileName, testName, skippedMessage, testCount) => {
reset();
Expand Down
6 changes: 6 additions & 0 deletions modules/sample/src/test/ts/client/pass/SecureContextTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Assert, UnitTest } from '@ephox/bedrock-client';

// We now use secure contexts a lot in TinyMCE. Verify that bedrock remote (AWS and LT) both offer secure contexts.
UnitTest.test('Secure context Test', () => {
Comment thread
TheSpyder marked this conversation as resolved.
Resolved
Assert.eq('bedrock should be served on a secure context', true, window.isSecureContext);
});
2 changes: 1 addition & 1 deletion modules/server/src/main/ts/BedrockAuto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function makeWebDriver(settings: BedrockAutoSettings, servicePort: number,
key: accesskey
};

const tunnel = await Tunnel.prepareConnection(servicePort, remoteWebdriver, sishDomain, tunnelCredentials);
const tunnel = await Tunnel.prepareConnection(servicePort, remoteWebdriver, sishDomain, tunnelCredentials, settings.pageHost);
shutdownServices.push(tunnel.shutdown);
const location = tunnel.url.href;

Expand Down
28 changes: 21 additions & 7 deletions modules/server/src/main/ts/bedrock/auto/Tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,14 @@ const createSSH = async (port: number | string, domain: string): Promise<Tunnel>
}
};

// Safari 26 cannot load `localhost` through the LambdaTest tunnel. LambdaTest publish
// `localhost.lambdatest.com` as a public DNS alias for 127.0.0.1.
const LAMBDA_LOCALHOST_ALIAS = 'localhost.lambdatest.com';

// LambdaTest supplied tunnel
// @lambdatest/nodetunnel has some weird quasi-overriden promise-based versions of functions
// and no proper typing for it. Excuse the hard type casting
const createLambda = async (port: number | string, credentials: LambdaCredentials): Promise<Tunnel> => {
const createLambda = async (port: number | string, credentials: LambdaCredentials, pageHost: string): Promise<Tunnel> => {
const tunnel = new LambdaTunnel();
const suffix = crypto.randomUUID();
const tunnelName = 'bedrock-tunnel-' + suffix;
Expand All @@ -101,8 +105,11 @@ const createLambda = async (port: number | string, credentials: LambdaCredential
return tunnel.stop(null as unknown as ((_: boolean) => void));
};

const url = pageHost === 'localhost'
? new URL('https://' + LAMBDA_LOCALHOST_ALIAS + ':' + port)
: new URL('http://' + pageHost + ':' + port);
const result: Tunnel = {
url: new URL('http://localhost:' + port),
url,
name: tunnelName,
shutdown
};
Expand All @@ -117,8 +124,8 @@ const createLambda = async (port: number | string, credentials: LambdaCredential
});
};

const createTunnel = async (port: number, domain: string | undefined, credentials: LambdaCredentials): Promise<Tunnel> => {
return domain ? createSSH(port, domain) : createLambda(port, credentials);
const createTunnel = async (port: number, domain: string | undefined, credentials: LambdaCredentials, pageHost: string): Promise<Tunnel> => {
return domain ? createSSH(port, domain) : createLambda(port, credentials, pageHost);
};

/**
Expand All @@ -127,11 +134,18 @@ const createTunnel = async (port: number, domain: string | undefined, credential
* @param port Dev server connection port
* @param remote Name of remote service or undefined
* @param domain Domain for sish connection or undefined
* @param pageHost Hostname the browser uses to reach the bedrock server
* @returns Connection tunnel
*/
export const prepareConnection = async (port: number, remote: string | undefined, domain: string | undefined, credentials: LambdaCredentials ): Promise<Tunnel> => {
return remote ? createTunnel(port, domain, credentials) : Promise.resolve({
url: new URL('http://localhost:' + port),
export const prepareConnection = async (port: number, remote: string | undefined, domain: string | undefined, credentials: LambdaCredentials, pageHost: string): Promise<Tunnel> => {
if (remote) {
if (domain && pageHost !== 'localhost') {
console.warn('--pageHost is ignored with a sish tunnel; the tunnel determines the page URL.');
}
return createTunnel(port, domain, credentials, pageHost);
}
return Promise.resolve({
url: new URL('http://' + pageHost + ':' + port),
shutdown: () => Promise.resolve()
});
};
9 changes: 9 additions & 0 deletions modules/server/src/main/ts/bedrock/cli/ClOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,12 @@ export const webdriverPort: ClOption = {
defaultValue: 4444,
uncommon: true
};

export const pageHost: ClOption = {
name: 'pageHost',
type: String,
defaultValue: 'localhost',
description: 'Hostname the browser uses to reach the bedrock server. Useful when the browser runs in a container or VM (e.g. host.docker.internal for a dockerized selenium grid). With the LambdaTest tunnel, localhost is replaced by the localhost.lambdatest.com alias. Ignored with a sish tunnel.',
validate: Extraction.any,
uncommon: true
};
1 change: 1 addition & 0 deletions modules/server/src/main/ts/bedrock/cli/Clis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const forAuto = (directories: Directories, argv: string[] = process.argv)
ClOptions.wipeBrowserCache,
ClOptions.remote,
ClOptions.webdriverPort,
ClOptions.pageHost,
ClOptions.sishDomain,
ClOptions.username,
ClOptions.accesskey,
Expand Down
1 change: 1 addition & 0 deletions modules/server/src/main/ts/bedrock/core/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export interface BedrockAutoSettings extends BedrockSettings {
readonly devicefarmArn: string;
readonly browserVersion: string;
readonly platformName?: string;
readonly pageHost: string;
}
2 changes: 1 addition & 1 deletion modules/server/src/main/ts/bedrock/server/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const create = (stickyFirstSession: boolean, overallTimeout: number, test
session.totalTests = totalTests;
session.currentTest = currentCount;
session.done = false;
// a bit of a lie, but we only ever get 1 start now
// after a start this names the test about to run, not the last one that completed
session.previous = {
name,
file,
Expand Down
62 changes: 56 additions & 6 deletions modules/server/src/test/ts/ClisTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ describe('Clis.forAuto', () => {
skipResetMousePosition: false,
wipeBrowserCache: false,
remote: '',
webdriverPort: 4444
webdriverPort: 4444,
pageHost: 'localhost'
}, cleanResult(actual));
});

Expand Down Expand Up @@ -106,7 +107,8 @@ describe('Clis.forAuto', () => {
skipResetMousePosition: false,
wipeBrowserCache: false,
remote: '',
webdriverPort: 4444
webdriverPort: 4444,
pageHost: 'localhost'
}, cleanResult(actual));
});

Expand Down Expand Up @@ -149,7 +151,8 @@ describe('Clis.forAuto', () => {
skipResetMousePosition: false,
wipeBrowserCache: false,
remote: '',
webdriverPort: 4444
webdriverPort: 4444,
pageHost: 'localhost'
}, cleanResult(actual));
});

Expand Down Expand Up @@ -191,7 +194,8 @@ describe('Clis.forAuto', () => {
skipResetMousePosition: false,
wipeBrowserCache: false,
remote: '',
webdriverPort: 4444
webdriverPort: 4444,
pageHost: 'localhost'
}, cleanResult(actual));
});

Expand Down Expand Up @@ -239,6 +243,7 @@ describe('Clis.forAuto', () => {
username: 'lt_username',
accesskey: 'lt_access_key',
webdriverPort: 4444,
pageHost: 'localhost',
platformName: 'macOS Catalina'
}, cleanResult(actual));
});
Expand Down Expand Up @@ -286,7 +291,8 @@ describe('Clis.forAuto', () => {
sishDomain: 'sish.osu.tiny.work',
devicefarmRegion: 'us-west-2',
devicefarmArn: 'arn:aws:devicefarm:us-west-2:123123123123:project-name:123a123b-123c-123d-1234a-123a123b123c',
webdriverPort: 4444
webdriverPort: 4444,
pageHost: 'localhost'
}, cleanResult(actual));
});

Expand Down Expand Up @@ -347,7 +353,51 @@ describe('Clis.forAuto', () => {
skipResetMousePosition: false,
wipeBrowserCache: false,
remote: '',
webdriverPort: 4444
webdriverPort: 4444,
pageHost: 'localhost'
}, cleanResult(actual));
});

it('TINYMCE-14879: parses a custom page host', () => {
const args = [
'--browser', 'chrome-headless',
'--files', 'src/test/resources/test.file1',
'--config', 'src/test/resources/tsconfig.sample.json',
'--pageHost', 'host.docker.internal'
];
const actual = Clis.forAuto(directories, args);
AttemptUtils.assertResult({
browser: 'chrome-headless',
browserVersion: 'latest',
bundler: 'rspack',
config: 'src/test/resources/tsconfig.sample.json',
name: 'bedrock-run',
output: 'scratch',
help: false,
testfiles: [
'src/test/resources/test.file1'
],
delayExit: false,
singleTimeout: 30000,
stopOnFailure: false,
overallTimeout: 600000,
loglevel: 'advanced',
version: false,
chunk: 2000,
retries: 0,
polyfills: [ 'Symbol' ],
verbose: false,
bucket: 1,
buckets: 1,
skipTypecheck: false,
useSandboxForHeadless: false,
useSelenium: false,
extraBrowserCapabilities: '',
skipResetMousePosition: false,
wipeBrowserCache: false,
remote: '',
webdriverPort: 4444,
pageHost: 'host.docker.internal'
}, cleanResult(actual));
});

Expand Down
13 changes: 12 additions & 1 deletion modules/server/tasks/bedrock.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,28 @@ const bedrockAuto = require('../lib/main/ts/BedrockAuto');

module.exports = function(grunt) {

// The CLI path coerces values via each option's declared type, but grunt
// options arrive as strings. Note Boolean('false') is true, so values
// are compared against the string 'true' instead.
const coerceSetting = function (clo, value) {
if (clo === undefined || typeof value !== 'string') return value;
if (clo.type === Boolean) return value === 'true';
return clo.type(value);
};

const enrichSettings = function (settings) {
const newSettings = { };
const optionsByKey = { };

for (const j in cloptions) {
const clo = cloptions[j];
const outputKey = clo.output !== undefined ? clo.output : clo.name;
optionsByKey[outputKey] = clo;
if (clo.defaultValue !== undefined) newSettings[outputKey] = clo.defaultValue;
}

for (const k in settings) {
newSettings[k] = settings[k];
newSettings[k] = coerceSetting(optionsByKey[k], settings[k]);
}

const testfiles = getFiles(settings.testfiles);
Expand Down
Loading