Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
201 lines
5.1 KiB
Dart
201 lines
5.1 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:qlyra/frontend/screens/webapp/web_app_bridge.dart';
|
|
|
|
void main() {
|
|
late List<(String, Map<String, dynamic>, bool)> sent;
|
|
late int closeCalls;
|
|
|
|
WebAppBridge buildBridge({
|
|
bool privateChannel = false,
|
|
String entryPoint = WebAppEntryPoint.webApp,
|
|
}) {
|
|
return WebAppBridge(
|
|
botId: 777,
|
|
entryPoint: entryPoint,
|
|
privateChannel: privateChannel,
|
|
contextResolver: () => null,
|
|
viewportResolver: () => const Size(420, 800),
|
|
onClose: () => closeCalls++,
|
|
emitter: (method, payload, private) => sent.add((
|
|
method,
|
|
jsonDecode(payload) as Map<String, dynamic>,
|
|
private,
|
|
)),
|
|
);
|
|
}
|
|
|
|
setUp(() {
|
|
sent = [];
|
|
closeCalls = 0;
|
|
});
|
|
|
|
test('reports the launch context it was created with', () async {
|
|
final bridge = buildBridge(entryPoint: WebAppEntryPoint.inlineButton);
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppGetLaunchContext',
|
|
'{"requestId":"r1"}',
|
|
false,
|
|
);
|
|
|
|
expect(sent, hasLength(1));
|
|
expect(sent.first.$1, 'WebAppGetLaunchContext');
|
|
expect(sent.first.$2, {'requestId': 'r1', 'entryPoint': 'inline_button'});
|
|
});
|
|
|
|
test('answers viewport requests with the current webview size', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppGetViewportSize',
|
|
'{"requestId":"r2"}',
|
|
false,
|
|
);
|
|
|
|
expect(sent.first.$2['width'], 420);
|
|
expect(sent.first.$2['height'], 800);
|
|
expect(sent.first.$2['isStateStable'], isTrue);
|
|
});
|
|
|
|
test('rejects an unknown method with the client error code', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppSomethingElse',
|
|
'{"requestId":"r3"}',
|
|
false,
|
|
);
|
|
|
|
expect(sent.first.$2['error'], {
|
|
'code': 'client.unsupported_method.unsupported_method',
|
|
});
|
|
});
|
|
|
|
test('stays silent for methods that never get an answer', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent('WebAppReady', '{}', false);
|
|
await bridge.handleEvent('WebAppStat', '{}', false);
|
|
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('drops gesture-gated methods until the user touches the page', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppShare',
|
|
'{"requestId":"r4","text":"hi"}',
|
|
false,
|
|
);
|
|
for (final method in [
|
|
'WebAppRequestPhone',
|
|
'WebAppDownloadFile',
|
|
'WebAppOpenCodeReader',
|
|
'WebAppBiometryRequestAccess',
|
|
'WebAppBiometryRequestAuth',
|
|
'WebAppBiometryUpdateToken',
|
|
]) {
|
|
await bridge.handleEvent(method, '{"requestId":"blocked"}', false);
|
|
}
|
|
expect(sent, isEmpty);
|
|
|
|
bridge.registerGesture();
|
|
await bridge.handleEvent('WebAppShare', '{"requestId":"r5"}', false);
|
|
|
|
expect(sent.single.$2['error'], {
|
|
'code': 'client.web_app_share.invalid_request',
|
|
});
|
|
});
|
|
|
|
test('ignores private-channel events when the channel is off', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppVerifyMobileId',
|
|
'{"requestId":"r6","url":"https://example.test/verify"}',
|
|
true,
|
|
);
|
|
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('reports malformed payloads as a decode error', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent('WebAppGetViewportSize', 'not-json', false);
|
|
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'tracks the back button and closing behaviour the app asked for',
|
|
() async {
|
|
final bridge = buildBridge();
|
|
|
|
expect(bridge.handlesBackButton, isFalse);
|
|
expect(bridge.needsCloseConfirmation, isFalse);
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppSetupBackButton',
|
|
'{"isVisible":true}',
|
|
false,
|
|
);
|
|
await bridge.handleEvent(
|
|
'WebAppSetupClosingBehavior',
|
|
'{"needConfirmation":true}',
|
|
false,
|
|
);
|
|
|
|
expect(bridge.handlesBackButton, isTrue);
|
|
expect(bridge.needsCloseConfirmation, isTrue);
|
|
|
|
bridge.notifyBackPressed();
|
|
expect(sent.single.$1, 'WebAppBackButtonPressed');
|
|
},
|
|
);
|
|
|
|
test('closes the screen when the app asks to', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent('WebAppClose', '{}', false);
|
|
|
|
expect(closeCalls, 1);
|
|
});
|
|
|
|
test('echoes the screen capture behaviour back', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent(
|
|
'WebAppSetupScreenCaptureBehavior',
|
|
'{"requestId":"r7","isScreenCaptureEnabled":true}',
|
|
false,
|
|
);
|
|
|
|
expect(sent.first.$2, {'requestId': 'r7', 'isScreenCaptureEnabled': true});
|
|
});
|
|
|
|
test('answers NFC availability without pretending to support it', () async {
|
|
final bridge = buildBridge();
|
|
|
|
await bridge.handleEvent('WebAppNfcGetInfo', '{"requestId":"r8"}', false);
|
|
await bridge.handleEvent(
|
|
'WebAppNfcEmulateNfcTag',
|
|
'{"requestId":"r9"}',
|
|
false,
|
|
);
|
|
|
|
expect(sent[0].$2, {
|
|
'requestId': 'r8',
|
|
'available': false,
|
|
'enabled': false,
|
|
});
|
|
expect(sent[1].$2['error'], {
|
|
'code': 'client.nfc_emulate_nfc_tag.not_supported',
|
|
});
|
|
});
|
|
}
|