@TestOn('browser') library; import 'dart:async'; import 'dart:js_interop'; import 'package:flutter_test/flutter_test.dart'; import 'package:wakelock_plus/src/wakelock_plus_web_plugin.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart'; import 'package:web/web.dart' as web; @JS('wakeLockTest.reset') external void resetWakeLockTest(); @JS('wakeLockTest.setVisibility') external void setTestVisibility(String value, bool dispatchEvent); @JS('wakeLockTest.releaseSentinel') external void releaseTestSentinel(int index); @JS('wakeLockTest.delayNextRequest') external void delayNextWakeLockRequest(); @JS('wakeLockTest.resolvePendingRequest') external void resolvePendingWakeLockRequest(); @JS('wakeLockTest.rejectPendingRequest') external void rejectPendingWakeLockRequest(); @JS('wakeLockTest.failNextRequest') external void failNextWakeLockRequest(); @JS('wakeLockTest.delayNextRelease') external void delayNextWakeLockRelease(); @JS('wakeLockTest.resolvePendingRelease') external void resolvePendingWakeLockRelease(); @JS('wakeLockTest.failNextRelease') external void failNextWakeLockRelease(); @JS('wakeLockTest.settleForCleanup') external void settleFakeWakeLockForCleanup(); @JS('wakeLockTest.requestCount') external int get wakeLockRequestCount; @JS('wakeLockTest.releaseCount') external int get wakeLockReleaseCount; void installFakeWakeLock() { final script = web.document.createElement('script') as web.HTMLScriptElement; script.text = r''' (() => { let visibility = 'visible'; let requests = 0; let releases = 0; let delayRequest = false; let delayRelease = false; let rejectRequest = false; let rejectRelease = false; let pendingRequest = null; let pendingRequestReject = null; let pendingRelease = null; let sentinels = []; class FakeWakeLockSentinel extends EventTarget { constructor() { super(); this.released = false; } release() { releases++; if (rejectRelease) { rejectRelease = false; return Promise.reject(new Error('release failed')); } if (delayRelease) { delayRelease = false; return new Promise((resolve) => { pendingRelease = () => { pendingRelease = null; this.released = true; this.dispatchEvent(new Event('release')); resolve(); }; }); } this.released = true; this.dispatchEvent(new Event('release')); return Promise.resolve(); } } Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => visibility, }); Object.defineProperty(navigator, 'wakeLock', { configurable: true, value: { request(type) { if (type !== 'screen') { return Promise.reject(new Error(`unexpected type: ${type}`)); } requests++; if (rejectRequest) { rejectRequest = false; return Promise.reject(new Error('request failed')); } const sentinel = new FakeWakeLockSentinel(); sentinels.push(sentinel); if (!delayRequest) { return Promise.resolve(sentinel); } delayRequest = false; return new Promise((resolve, reject) => { pendingRequest = () => { pendingRequest = null; pendingRequestReject = null; resolve(sentinel); }; pendingRequestReject = () => { pendingRequest = null; pendingRequestReject = null; reject(new Error('request failed')); }; }); }, }, }); window.wakeLockTest = { reset() { visibility = 'visible'; requests = 0; releases = 0; delayRequest = false; delayRelease = false; rejectRequest = false; rejectRelease = false; pendingRequest = null; pendingRequestReject = null; pendingRelease = null; sentinels = []; }, setVisibility(value, dispatchEvent) { visibility = value; if (dispatchEvent) { document.dispatchEvent(new Event('visibilitychange')); } }, releaseSentinel(index) { const sentinel = sentinels[index]; sentinel.released = true; sentinel.dispatchEvent(new Event('release')); }, delayNextRequest() { delayRequest = true; }, resolvePendingRequest() { if (pendingRequest === null) { throw new Error('no pending wake lock request'); } pendingRequest(); }, rejectPendingRequest() { if (pendingRequestReject === null) { throw new Error('no pending wake lock request'); } pendingRequestReject(); }, failNextRequest() { rejectRequest = true; }, delayNextRelease() { delayRelease = true; }, resolvePendingRelease() { if (pendingRelease === null) { throw new Error('no pending wake lock release'); } pendingRelease(); }, failNextRelease() { rejectRelease = true; }, settleForCleanup() { delayRequest = false; delayRelease = false; rejectRequest = false; rejectRelease = false; if (pendingRequest !== null) { pendingRequest(); } if (pendingRelease !== null) { pendingRelease(); } }, get requestCount() { return requests; }, get releaseCount() { return releases; }, }; })(); '''; web.document.head!.appendChild(script); } Future flushBrowserTasks() async { await Future.delayed(Duration.zero); await Future.delayed(Duration.zero); } Completer settlementOf(Future future) { final settlement = Completer(); unawaited( future.then((_) => settlement.complete(), onError: (Object _, StackTrace _) => settlement.complete()), ); return settlement; } Future cleanUpFakeWakeLock() async { settleFakeWakeLockForCleanup(); await flushBrowserTasks(); try { await WakelockPlus.disable(); } finally { resetWakeLockTest(); } } void main() { group('$WakelockPlusWebPlugin', () { setUpAll(() { installFakeWakeLock(); WakelockPlusPlatformInterface.instance = WakelockPlusWebPlugin(); }); tearDown(cleanUpFakeWakeLock); test('$WakelockPlusWebPlugin is the platform instance', () { expect(WakelockPlusPlatformInterface.instance, isA()); }); test('enable then disable releases a pending acquisition', () async { delayNextWakeLockRequest(); final enabling = WakelockPlus.enable(); await flushBrowserTasks(); expect(wakeLockRequestCount, 1); final disabling = WakelockPlus.disable(); await flushBrowserTasks(); resolvePendingWakeLockRequest(); await Future.wait([enabling, disabling]); expect(wakeLockReleaseCount, 1); expect(await WakelockPlus.enabled, isFalse); }); test('concurrent enables share a pending acquisition', () async { delayNextWakeLockRequest(); final firstEnable = WakelockPlus.enable(); final secondEnable = WakelockPlus.enable(); await flushBrowserTasks(); expect(wakeLockRequestCount, 1); resolvePendingWakeLockRequest(); await Future.wait([firstEnable, secondEnable]); expect(await WakelockPlus.enabled, isTrue); }); test('concurrent toggles await one replacement after a delayed release', () async { await WakelockPlus.enable(); delayNextWakeLockRelease(); delayNextWakeLockRequest(); final disabling = WakelockPlus.disable(); await flushBrowserTasks(); final firstEnable = WakelockPlus.enable(); final secondEnable = WakelockPlus.enable(); final firstSettlement = settlementOf(firstEnable); final secondSettlement = settlementOf(secondEnable); await flushBrowserTasks(); expect(wakeLockReleaseCount, 1); expect(wakeLockRequestCount, 1); expect(firstSettlement.isCompleted, isFalse); expect(secondSettlement.isCompleted, isFalse); resolvePendingWakeLockRelease(); await flushBrowserTasks(); expect(wakeLockRequestCount, 2); expect(firstSettlement.isCompleted, isFalse); expect(secondSettlement.isCompleted, isFalse); resolvePendingWakeLockRequest(); await Future.wait([disabling, firstEnable, secondEnable]); expect(wakeLockRequestCount, 2); expect(firstSettlement.isCompleted, isTrue); expect(secondSettlement.isCompleted, isTrue); expect(await WakelockPlus.enabled, isTrue); }); test('replacement rejection reaches enable and leaves wake lock disabled', () async { await WakelockPlus.enable(); delayNextWakeLockRelease(); delayNextWakeLockRequest(); final disabling = WakelockPlus.disable(); await flushBrowserTasks(); final enabling = WakelockPlus.enable(); final enableSettlement = settlementOf(enabling); await flushBrowserTasks(); expect(enableSettlement.isCompleted, isFalse); resolvePendingWakeLockRelease(); await flushBrowserTasks(); expect(wakeLockRequestCount, 2); expect(enableSettlement.isCompleted, isFalse); final failedDisable = expectLater(disabling, throwsA(contains('request failed'))); final failedEnable = expectLater(enabling, throwsA(contains('request failed'))); rejectPendingWakeLockRequest(); await Future.wait([failedDisable, failedEnable]); expect(enableSettlement.isCompleted, isTrue); expect(wakeLockRequestCount, 2); expect(await WakelockPlus.enabled, isFalse); }); test('concurrent disables share a pending release', () async { await WakelockPlus.enable(); delayNextWakeLockRelease(); final firstDisable = WakelockPlus.disable(); final secondDisable = WakelockPlus.disable(); await flushBrowserTasks(); expect(wakeLockReleaseCount, 1); resolvePendingWakeLockRelease(); await Future.wait([firstDisable, secondDisable]); expect(await WakelockPlus.enabled, isFalse); }); test('acquisition failure remains retryable', () async { failNextWakeLockRequest(); await expectLater(WakelockPlus.enable(), throwsA(anything)); expect(await WakelockPlus.enabled, isFalse); await WakelockPlus.enable(); expect(wakeLockRequestCount, 2); expect(await WakelockPlus.enabled, isTrue); }); test('release failure retains the sentinel for teardown retry', () async { await WakelockPlus.enable(); failNextWakeLockRelease(); await expectLater(WakelockPlus.disable(), throwsA(anything)); expect(await WakelockPlus.enabled, isTrue); await WakelockPlus.disable(); expect(wakeLockReleaseCount, 2); expect(await WakelockPlus.enabled, isFalse); }); test('failed acquisition rollback retains the sentinel for retry', () async { delayNextWakeLockRequest(); final enabling = WakelockPlus.enable(); await flushBrowserTasks(); final disabling = WakelockPlus.disable(); failNextWakeLockRelease(); final failedEnable = expectLater(enabling, throwsA(anything)); resolvePendingWakeLockRequest(); await Future.wait([failedEnable, disabling]); expect(wakeLockReleaseCount, 2); expect(await WakelockPlus.enabled, isFalse); }); test('reacquires after a browser release when still requested', () async { await WakelockPlus.enable(); expect(wakeLockRequestCount, 1); setTestVisibility('hidden', false); releaseTestSentinel(0); expect(await WakelockPlus.enabled, isFalse); setTestVisibility('visible', true); await flushBrowserTasks(); expect(wakeLockRequestCount, 2); expect(await WakelockPlus.enabled, isTrue); }); test('does not reacquire after explicit teardown', () async { await WakelockPlus.enable(); setTestVisibility('hidden', false); releaseTestSentinel(0); await WakelockPlus.disable(); setTestVisibility('visible', true); web.document.dispatchEvent(web.Event('fullscreenchange')); await flushBrowserTasks(); expect(wakeLockRequestCount, 1); expect(await WakelockPlus.enabled, isFalse); }); test('releases a reacquisition that resolves after disable', () async { await WakelockPlus.enable(); setTestVisibility('hidden', false); releaseTestSentinel(0); delayNextWakeLockRequest(); setTestVisibility('visible', true); await flushBrowserTasks(); expect(wakeLockRequestCount, 2); final disabling = WakelockPlus.disable(); await flushBrowserTasks(); resolvePendingWakeLockRequest(); await disabling; expect(wakeLockReleaseCount, 1); expect(await WakelockPlus.enabled, isFalse); setTestVisibility('visible', true); web.document.dispatchEvent(web.Event('fullscreenchange')); await flushBrowserTasks(); expect(wakeLockRequestCount, 2); }); test('an old sentinel release cannot clear its replacement', () async { await WakelockPlus.enable(); setTestVisibility('hidden', false); releaseTestSentinel(0); setTestVisibility('visible', true); await flushBrowserTasks(); expect(await WakelockPlus.enabled, isTrue); releaseTestSentinel(0); await flushBrowserTasks(); expect(wakeLockRequestCount, 2); expect(await WakelockPlus.enabled, isTrue); }); }); }