Skip to content
This repository has been archived by the owner on Oct 28, 2024. It is now read-only.

Fix internal CI test failures #35

Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## 1.1.2-dev

- Require Dart 2.17
- Log errors from chrome
- Allow tests to detect headless-only environment (for CI).

## 1.1.1

Expand Down
16 changes: 13 additions & 3 deletions lib/src/chrome.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

Expand Down Expand Up @@ -46,6 +47,8 @@ String get _executable {

/// Manager for an instance of Chrome.
class Chrome {
static final _logger = Logger('BROWSER_LAUNCHER.CHROME');

Chrome._(
this.debugPort,
this.chromeConnection, {
Expand Down Expand Up @@ -119,16 +122,23 @@ class Chrome {
// Wait until the DevTools are listening before trying to connect.
final errorLines = <String>[];
try {
await process.stderr
final stderr = process.stderr.asBroadcastStream();
stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(_logger.fine);

await stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.firstWhere((line) {
errorLines.add(line);
return line.startsWith('DevTools listening');
}).timeout(const Duration(seconds: 60));
} catch (_) {
} on TimeoutException catch (e, s) {
_logger.severe('Unable to connect to Chrome DevTools', e, s);
throw Exception(
'Unable to connect to Chrome DevTools.\n\n'
'Unable to connect to Chrome DevTools: $e.\n\n'
'Chrome STDERR:\n${errorLines.join('\n')}',
);
}
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ environment:
sdk: '>=2.17.0 <3.0.0'

dependencies:
logging: ^1.0.0
path: ^1.8.0
webkit_inspection_protocol: ^1.0.0

Expand Down
254 changes: 152 additions & 102 deletions test/chrome_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,120 +7,91 @@ import 'dart:async';
import 'dart:io';

import 'package:browser_launcher/src/chrome.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

const _headlessOnlyEnvironment = 'HEADLESS_ONLY';

bool get headlessOnlyEnvironment =>
Platform.environment[_headlessOnlyEnvironment] == 'true';

void _configureLogging(bool verbose) {
Logger.root.level = verbose ? Level.ALL : Level.INFO;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
});
}

void main() {
Chrome? chrome;

// Pass 'true' for debugging.
_configureLogging(false);

Future<ChromeTab?> getTab(String url) => chrome!.chromeConnection.getTab(
(t) => t.url.contains(url),
retryFor: const Duration(seconds: 5),
);

Future<List<ChromeTab>?> getTabs() => chrome!.chromeConnection.getTabs(
retryFor: const Duration(seconds: 5),
);

Future<WipConnection> connectToTab(String url) async {
final tab =
await chrome!.chromeConnection.getTab((t) => t.url.contains(url));
final tab = await getTab(url);
expect(tab, isNotNull);
return tab!.connect();
}

Future<void> openTab(String url) async {
await chrome!.chromeConnection.getUrl(_openTabUrl(url));
}
Future<HttpClientResponse> openTab(String url) =>
chrome!.chromeConnection.getUrl(_openTabUrl(url));

Future<void> launchChromeWithDebugPort({
int port = 0,
String? userDataDir,
bool signIn = false,
bool headless = false,
}) async {
chrome = await Chrome.startWithDebugPort(
[_googleUrl],
debugPort: port,
userDataDir: userDataDir,
signIn: signIn,
headless: headless,
);
}

Future<void> launchChrome() async {
await Chrome.start([_googleUrl]);
Future<void> launchChrome({bool headless = false}) async {
await Chrome.start([_googleUrl], args: [if (headless) '--headless']);
}

group('chrome with temp data dir', () {
tearDown(() async {
await chrome?.close();
chrome = null;
});

test('can launch chrome', () async {
await launchChrome();
expect(chrome, isNull);
});

test('can launch chrome with debug port', () async {
await launchChromeWithDebugPort();
expect(chrome, isNotNull);
});

test('has a working debugger', () async {
await launchChromeWithDebugPort();
final tabs = await chrome!.chromeConnection.getTabs();
expect(
tabs,
contains(
const TypeMatcher<ChromeTab>()
.having((t) => t.url, 'url', _googleUrl),
),
);
});

test('uses open debug port if provided port is 0', () async {
await launchChromeWithDebugPort();
expect(chrome!.debugPort, isNot(equals(0)));
});

test('can provide a specific debug port', () async {
final port = await findUnusedPort();
await launchChromeWithDebugPort(port: port);
expect(chrome!.debugPort, port);
});
});

group('chrome with user data dir', () {
late Directory dataDir;

for (var signIn in [false, true]) {
group('and signIn = $signIn', () {
setUp(() {
dataDir = Directory.systemTemp.createTempSync(_userDataDirName);
});
final headlessModes = [
true,
if (!headlessOnlyEnvironment) false,
];

for (var headless in headlessModes) {
group('(headless: $headless)', () {
group('chrome with temp data dir', () {
tearDown(() async {
await chrome?.close();
chrome = null;
});

var attempts = 0;
while (true) {
try {
attempts++;
await Future<void>.delayed(const Duration(milliseconds: 100));
dataDir.deleteSync(recursive: true);
break;
} catch (_) {
if (attempts > 3) rethrow;
}
}
test('can launch chrome', () async {
await launchChrome(headless: headless);
expect(chrome, isNull);
});

test('can launch with debug port', () async {
await launchChromeWithDebugPort(
userDataDir: dataDir.path,
signIn: signIn,
);
test('can launch chrome with debug port', () async {
await launchChromeWithDebugPort(headless: headless);
expect(chrome, isNotNull);
});

test('has a working debugger', () async {
await launchChromeWithDebugPort(
userDataDir: dataDir.path,
signIn: signIn,
);
final tabs = await chrome!.chromeConnection.getTabs();
await launchChromeWithDebugPort(headless: headless);
final tabs = await getTabs();
expect(
tabs,
contains(
Expand All @@ -130,39 +101,118 @@ void main() {
);
});

test('has correct profile path', () async {
await launchChromeWithDebugPort(
userDataDir: dataDir.path,
signIn: signIn,
);
await openTab(_chromeVersionUrl);

final wipConnection = await connectToTab(_chromeVersionUrl);
final result = await _evaluateExpression(
wipConnection.page,
"document.getElementById('profile_path').textContent",
);
test('uses open debug port if provided port is 0', () async {
await launchChromeWithDebugPort(headless: headless);
expect(chrome!.debugPort, isNot(equals(0)));
});

expect(result, contains(_userDataDirName));
test('can provide a specific debug port', () async {
final port = await findUnusedPort();
await launchChromeWithDebugPort(port: port, headless: headless);
expect(chrome!.debugPort, port);
});
});
}
});

group('chrome with user data dir', () {
late Directory dataDir;
const waitMilliseconds = Duration(milliseconds: 100);

for (var signIn in [false, true]) {
group('and signIn = $signIn', () {
setUp(() {
dataDir = Directory.systemTemp.createTempSync(_userDataDirName);
});

tearDown(() async {
await chrome?.close();
chrome = null;

var attempts = 0;
while (true) {
try {
attempts++;
await Future<dynamic>.delayed(waitMilliseconds);
dataDir.deleteSync(recursive: true);
break;
} catch (_) {
if (attempts > 3) rethrow;
}
}
});

test('can launch with debug port', () async {
await launchChromeWithDebugPort(
userDataDir: dataDir.path,
signIn: signIn,
headless: headless,
);
expect(chrome, isNotNull);
});

test('has a working debugger', () async {
await launchChromeWithDebugPort(
userDataDir: dataDir.path,
signIn: signIn,
headless: headless,
);
final tabs = await getTabs();
expect(
tabs,
contains(
const TypeMatcher<ChromeTab>()
.having((t) => t.url, 'url', _googleUrl),
),
);
});

test(
'has correct profile path',
() async {
await launchChromeWithDebugPort(
userDataDir: dataDir.path,
signIn: signIn,
headless: headless,
);
await openTab(_chromeVersionUrl);
final wipConnection = await connectToTab(_chromeVersionUrl);
await wipConnection.debugger.enable();
await wipConnection.runtime.enable();
final result = await _evaluate(
wipConnection.page,
"document.getElementById('profile_path').textContent",
);
expect(result, contains(_userDataDirName));
},
skip: headless, // headless mode does not allow chrome: urls.
);
});
}
});
});
}
}

String _openTabUrl(String url) => '/json/new?$url';

Future<String> _evaluateExpression(WipPage page, String expression) async {
var result = '';
while (result.isEmpty) {
await Future<void>.delayed(const Duration(milliseconds: 100));
final wipResponse = await page.sendCommand(
'Runtime.evaluate',
params: {'expression': expression},
);
final response = wipResponse.json['result'] as Map<String, dynamic>;
final value = (response['result'] as Map<String, dynamic>)['value'];
result = (value != null && value is String) ? value : '';
Future<String?> _evaluate(WipPage page, String expression) async {
String? result;
const stopInSeconds = Duration(seconds: 5);
const waitMilliseconds = Duration(milliseconds: 100);
final stopTime = DateTime.now().add(stopInSeconds);

while (result == null && DateTime.now().isBefore(stopTime)) {
await Future<dynamic>.delayed(waitMilliseconds);
try {
final wipResponse = await page.sendCommand(
'Runtime.evaluate',
params: {'expression': expression},
);
final response = wipResponse.json['result'] as Map<String, dynamic>;
final value = (response['result'] as Map<String, dynamic>)['value'];
result = value?.toString();
} catch (_) {
return null;
}
}
return result;
}
Expand Down