The Store's unpackaged EXE path would require Authenticode-signing the installer and every PE file inside it. MSIX submissions are re-signed by the Store instead, so this route needs no code-signing certificate. build-msix.ps1 mirrors build-installer.ps1 and consumes the same per-architecture build artifacts, leaving the installer, portable archives and WinSparkle appcast untouched. One template generates the manifest for both architectures, carrying the identity reserved in Partner Center. check_windows_msix.py recomputes the package family name from the publisher DN, so a mistyped identity fails CI rather than a submission, and it parses the script rather than running it because root CI is Linux. Qualified logo assets are indexed into resources.pri; without the altform-unplated variants the shell draws the taskbar icon on an accent-coloured plate. PlatformDetector.isPackagedInstall gates the in-app updater and the Liberapay tile, which the read-only package directory and Store commerce policy respectively rule out. Gating at runtime keeps one Windows build feeding both the installer and the Store package.
180 lines
6.1 KiB
Dart
180 lines
6.1 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/utils/platform_detector.dart';
|
|
|
|
void main() {
|
|
setUp(() {
|
|
TvDetectionService.debugReset();
|
|
addTearDown(TvDetectionService.debugReset);
|
|
});
|
|
|
|
test('concurrent callers wait for TV detection', () async {
|
|
final detection = Completer<void>();
|
|
TvDetectionService.debugDetectionGate = detection.future;
|
|
|
|
final first = TvDetectionService.getInstance(forceTv: true);
|
|
var secondCompleted = false;
|
|
final second = TvDetectionService.getInstance();
|
|
unawaited(second.then((_) => secondCompleted = true));
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(secondCompleted, isFalse);
|
|
detection.complete();
|
|
|
|
final instances = await Future.wait([first, second]);
|
|
expect(identical(instances.first, instances.last), isTrue);
|
|
expect(instances.first.isTV, isTrue);
|
|
});
|
|
|
|
group('detectAndroidTvFromSystemFeatures', () {
|
|
test('detects leanback devices', () {
|
|
final detection = detectAndroidTvFromSystemFeatures([
|
|
'android.software.leanback',
|
|
'android.hardware.touchscreen',
|
|
]);
|
|
|
|
expect(detection.isTv, isTrue);
|
|
expect(detection.reasons, contains('leanback'));
|
|
expect(detection.reasons, isNot(contains('no_touchscreen')));
|
|
});
|
|
|
|
test('detects Fire TV even when touchscreen is present', () {
|
|
final detection = detectAndroidTvFromSystemFeatures(['amazon.hardware.fire_tv', 'android.hardware.touchscreen']);
|
|
|
|
expect(detection.isTv, isTrue);
|
|
expect(detection.reasons, contains('fire_tv'));
|
|
expect(detection.reasons, isNot(contains('no_touchscreen')));
|
|
});
|
|
|
|
test('detects devices without real touchscreen capability', () {
|
|
final detection = detectAndroidTvFromSystemFeatures(['android.hardware.faketouch']);
|
|
|
|
expect(detection.isTv, isTrue);
|
|
expect(detection.reasons, contains('no_touchscreen'));
|
|
});
|
|
|
|
test('detects television feature', () {
|
|
final detection = detectAndroidTvFromSystemFeatures([
|
|
'android.hardware.type.television',
|
|
'android.hardware.touchscreen',
|
|
]);
|
|
|
|
expect(detection.isTv, isTrue);
|
|
expect(detection.reasons, contains('television_feature'));
|
|
});
|
|
|
|
test('does not classify touchscreen-only devices as TV', () {
|
|
final detection = detectAndroidTvFromSystemFeatures(['android.hardware.touchscreen']);
|
|
|
|
expect(detection.isTv, isFalse);
|
|
expect(detection.reasons, isEmpty);
|
|
});
|
|
|
|
test('does not classify empty feature lists as no-touchscreen TVs', () {
|
|
final detection = detectAndroidTvFromSystemFeatures(const []);
|
|
|
|
expect(detection.isTv, isFalse);
|
|
expect(detection.reasons, isEmpty);
|
|
});
|
|
|
|
test('classifies automotive head units as cars, not TVs', () {
|
|
final detection = detectAndroidTvFromSystemFeatures([
|
|
'android.hardware.type.automotive',
|
|
'android.hardware.touchscreen',
|
|
]);
|
|
|
|
expect(detection.isAutomotive, isTrue);
|
|
expect(detection.isTv, isFalse);
|
|
});
|
|
|
|
test('rotary-only head units are cars despite reporting no touchscreen', () {
|
|
final detection = detectAndroidTvFromSystemFeatures(['android.hardware.type.automotive']);
|
|
|
|
expect(detection.isAutomotive, isTrue);
|
|
expect(detection.isTv, isFalse);
|
|
expect(detection.reasons, contains('no_touchscreen'));
|
|
});
|
|
|
|
test('automotive vetoes a stray leanback flag from an OEM image', () {
|
|
final detection = detectAndroidTvFromSystemFeatures([
|
|
'android.hardware.type.automotive',
|
|
'android.software.leanback',
|
|
'android.hardware.touchscreen',
|
|
]);
|
|
|
|
expect(detection.isAutomotive, isTrue);
|
|
expect(detection.isTv, isFalse);
|
|
});
|
|
|
|
test('ordinary devices are not automotive', () {
|
|
final detection = detectAndroidTvFromSystemFeatures(['android.hardware.touchscreen']);
|
|
|
|
expect(detection.isAutomotive, isFalse);
|
|
});
|
|
});
|
|
|
|
group('pictureInPictureAllowed', () {
|
|
bool allowed({bool host = true, bool appleTv = false, bool tv = false, bool automotive = false}) =>
|
|
pictureInPictureAllowed(
|
|
hostSupportsPictureInPicture: host,
|
|
isAppleTv: appleTv,
|
|
isTv: tv,
|
|
isAutomotive: automotive,
|
|
);
|
|
|
|
test('a plain handheld host may float a player', () {
|
|
expect(allowed(), isTrue);
|
|
});
|
|
|
|
test('automotive vetoes a host that otherwise supports PiP', () {
|
|
expect(allowed(automotive: true), isFalse);
|
|
});
|
|
|
|
test('TV form factors veto a host that otherwise supports PiP', () {
|
|
expect(allowed(tv: true), isFalse);
|
|
expect(allowed(appleTv: true), isFalse);
|
|
});
|
|
|
|
test('a host without PiP is never allowed, whatever the form factor', () {
|
|
expect(allowed(host: false), isFalse);
|
|
expect(allowed(host: false, automotive: true), isFalse);
|
|
});
|
|
});
|
|
|
|
group('isPackagedExecutablePath', () {
|
|
test('a WindowsApps executable path is a packaged install', () {
|
|
expect(
|
|
PlatformDetector.isPackagedExecutablePath(
|
|
r'C:\Program Files\WindowsApps\edde746.Plezy_2.11.0.0_x64__13q3sv6jzathm\plezy.exe',
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('the package directory is matched however it is cased', () {
|
|
expect(
|
|
PlatformDetector.isPackagedExecutablePath(
|
|
r'c:\program files\windowsapps\edde746.Plezy_2.11.0.0_x64__13q3sv6jzathm\plezy.exe',
|
|
),
|
|
isTrue,
|
|
reason:
|
|
'Windows paths are case-insensitive; a casing difference must not restore '
|
|
'the updater and donation link inside a read-only package',
|
|
);
|
|
});
|
|
|
|
test('installed and portable executable paths are not packaged installs', () {
|
|
expect(PlatformDetector.isPackagedExecutablePath(r'C:\Program Files\Plezy\plezy.exe'), isFalse);
|
|
expect(PlatformDetector.isPackagedExecutablePath(r'D:\portable\plezy-windows-x64\plezy.exe'), isFalse);
|
|
});
|
|
|
|
test('a directory whose name merely starts the same is not a package', () {
|
|
expect(
|
|
PlatformDetector.isPackagedExecutablePath(r'C:\Users\someone\Downloads\WindowsApps-backup\plezy.exe'),
|
|
isFalse,
|
|
);
|
|
});
|
|
});
|
|
}
|