diff --git a/tests/PrompterOne.App.UITests/Editor/EditorLayoutTests.cs b/tests/PrompterOne.App.UITests/Editor/EditorLayoutTests.cs index 61e97f7..cafcf3b 100644 --- a/tests/PrompterOne.App.UITests/Editor/EditorLayoutTests.cs +++ b/tests/PrompterOne.App.UITests/Editor/EditorLayoutTests.cs @@ -20,8 +20,10 @@ public async Task EditorScreen_MetadataRailStaysDockedToRightOfMainPanel() var mainPanel = page.GetByTestId(UiTestIds.Editor.MainPanel); var metadataRail = page.GetByTestId(UiTestIds.Editor.MetadataRail); - await Expect(mainPanel).ToBeVisibleAsync(); - await Expect(metadataRail).ToBeVisibleAsync(); + await Expect(mainPanel) + .ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs }); + await Expect(metadataRail) + .ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs }); var mainBounds = await GetRequiredBoundingBoxAsync(mainPanel); var railBounds = await GetRequiredBoundingBoxAsync(metadataRail); @@ -59,8 +61,10 @@ public async Task EditorScreen_SourceEditorUsesSingleVerticalScrollSurface() var sourceInput = page.GetByTestId(UiTestIds.Editor.SourceInput); var sourceScrollHost = page.GetByTestId(UiTestIds.Editor.SourceScrollHost); - await Expect(sourceInput).ToBeVisibleAsync(); - await Expect(sourceScrollHost).ToBeVisibleAsync(); + await Expect(sourceInput) + .ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs }); + await Expect(sourceScrollHost) + .ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs }); await sourceInput.EvaluateAsync( """ diff --git a/tests/PrompterOne.App.UITests/Editor/EditorOverlayInteractionTests.cs b/tests/PrompterOne.App.UITests/Editor/EditorOverlayInteractionTests.cs index 0754d8d..0eb44d0 100644 --- a/tests/PrompterOne.App.UITests/Editor/EditorOverlayInteractionTests.cs +++ b/tests/PrompterOne.App.UITests/Editor/EditorOverlayInteractionTests.cs @@ -20,7 +20,8 @@ public async Task EditorScreen_HidesFloatingBarWhileToolbarDropdownIsOpen() var floatingBar = page.GetByTestId(UiTestIds.Editor.FloatingBar); var colorMenu = page.GetByTestId(UiTestIds.Editor.MenuColor); - await Expect(sourceInput).ToBeVisibleAsync(); + await Expect(sourceInput) + .ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs }); await sourceInput.EvaluateAsync( "(element, target) => { const start = element.value.indexOf(target); element.focus(); element.setSelectionRange(start, start + target.length); element.dispatchEvent(new Event('select', { bubbles: true })); element.dispatchEvent(new Event('keyup', { bubbles: true })); }", BrowserTestConstants.Editor.Welcome); diff --git a/tests/PrompterOne.App.UITests/Learn/LearnStartupAlignmentTests.cs b/tests/PrompterOne.App.UITests/Learn/LearnStartupAlignmentTests.cs index b82038a..bcbc089 100644 --- a/tests/PrompterOne.App.UITests/Learn/LearnStartupAlignmentTests.cs +++ b/tests/PrompterOne.App.UITests/Learn/LearnStartupAlignmentTests.cs @@ -68,6 +68,7 @@ private static string BuildStartupTraceScript() const learnLineTestId = {{ToJsString(UiTestIds.Learn.OrpLine)}}; const learnWordTestId = {{ToJsString(UiTestIds.Learn.Word)}}; const maxSamples = 16; + const maxAnimationFramePasses = maxSamples; window.__learnStartupTrace = []; @@ -98,6 +99,18 @@ private static string BuildStartupTraceScript() }); }; + let animationFramePasses = 0; + const captureOnAnimationFrame = () => { + capture(); + + if (window.__learnStartupTrace.length >= maxSamples || animationFramePasses >= maxAnimationFramePasses) { + return; + } + + animationFramePasses += 1; + window.requestAnimationFrame(captureOnAnimationFrame); + }; + new MutationObserver(capture).observe(document, { attributeFilter: ['style', layoutReadyAttributeName], attributes: true, @@ -105,6 +118,9 @@ private static string BuildStartupTraceScript() childList: true, subtree: true }); + + capture(); + window.requestAnimationFrame(captureOnAnimationFrame); })(); """; } diff --git a/tests/PrompterOne.App.UITests/Media/recording-file-harness.js b/tests/PrompterOne.App.UITests/Media/recording-file-harness.js index c874fa8..17386f8 100644 --- a/tests/PrompterOne.App.UITests/Media/recording-file-harness.js +++ b/tests/PrompterOne.App.UITests/Media/recording-file-harness.js @@ -6,6 +6,8 @@ const minimumAudibleFrequencyValue = 8; const minimumVisibleChannelValue = 12; const minimumVisiblePixelCount = 16; + const visibleVideoProbeTimeoutMs = 1500; + const visibleVideoPollDelayMs = 100; const readyStateHaveCurrentData = 2; if (typeof window[harnessGlobalName] === "object" && window[harnessGlobalName] !== null) { @@ -115,6 +117,36 @@ }; } + async function waitForNextVideoFrame(videoElement) { + if (typeof videoElement.requestVideoFrameCallback === "function") { + await new Promise(resolve => videoElement.requestVideoFrameCallback(() => resolve())); + return; + } + + await new Promise(resolve => window.setTimeout(resolve, visibleVideoPollDelayMs)); + } + + async function detectVisibleVideoAcrossFrames(videoElement) { + const deadline = Date.now() + visibleVideoProbeTimeoutMs; + let highestVisiblePixelCount = 0; + + while (Date.now() <= deadline) { + const sample = detectVisibleVideo(videoElement); + highestVisiblePixelCount = Math.max(highestVisiblePixelCount, sample.nonBlackPixelCount); + + if (sample.hasVisibleVideo) { + return sample; + } + + await waitForNextVideoFrame(videoElement); + } + + return { + hasVisibleVideo: highestVisiblePixelCount >= minimumVisiblePixelCount, + nonBlackPixelCount: highestVisiblePixelCount + }; + } + async function analyzeSavedRecording() { if (!(savedBlob instanceof Blob)) { return null; @@ -139,7 +171,7 @@ : null; const hasAudioTrack = Boolean(captureStream?.getAudioTracks?.().length); const hasAudibleAudio = await detectAudibleAudio(captureStream); - const visibleVideo = detectVisibleVideo(videoElement); + const visibleVideo = await detectVisibleVideoAcrossFrames(videoElement); captureStream?.getTracks?.().forEach(track => track.stop());