Skip to content
Closed
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
12 changes: 8 additions & 4 deletions tests/PrompterOne.App.UITests/Editor/EditorLayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions tests/PrompterOne.App.UITests/Learn/LearnStartupAlignmentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down Expand Up @@ -98,13 +99,28 @@ private static string BuildStartupTraceScript()
});
};

let animationFramePasses = 0;
const captureOnAnimationFrame = () => {
capture();

if (window.__learnStartupTrace.length >= maxSamples || animationFramePasses >= maxAnimationFramePasses) {
return;
}

animationFramePasses += 1;
Comment on lines +104 to +110
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

animationFramePasses is checked before it is incremented, so captureOnAnimationFrame will run one extra requestAnimationFrame callback beyond maxAnimationFramePasses (it still captures once more before returning). If the intent is to cap RAF sampling to exactly maxAnimationFramePasses, increment before the check or adjust the comparison so the maximum number of scheduled passes matches the constant.

Suggested change
capture();
if (window.__learnStartupTrace.length >= maxSamples || animationFramePasses >= maxAnimationFramePasses) {
return;
}
animationFramePasses += 1;
if (window.__learnStartupTrace.length >= maxSamples || animationFramePasses >= maxAnimationFramePasses) {
return;
}
animationFramePasses += 1;
capture();
if (window.__learnStartupTrace.length >= maxSamples || animationFramePasses >= maxAnimationFramePasses) {
return;
}

Copilot uses AI. Check for mistakes.
window.requestAnimationFrame(captureOnAnimationFrame);
};

new MutationObserver(capture).observe(document, {
attributeFilter: ['style', layoutReadyAttributeName],
attributes: true,
characterData: true,
childList: true,
subtree: true
});

capture();
window.requestAnimationFrame(captureOnAnimationFrame);
})();
""";
}
Expand Down
34 changes: 33 additions & 1 deletion tests/PrompterOne.App.UITests/Media/recording-file-harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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));
}
Comment on lines +120 to +127
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waitForNextVideoFrame can hang indefinitely when requestVideoFrameCallback never fires (e.g., if videoElement.play() fails or the video never advances frames). Because this await has no timeout, detectVisibleVideoAcrossFrames may never reach its deadline and the harness can stall the test run. Consider racing requestVideoFrameCallback with a setTimeout fallback (or check paused/ended and fall back to polling) so the probe always makes progress and respects visibleVideoProbeTimeoutMs.

Copilot uses AI. Check for mistakes.

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;
Expand All @@ -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());

Expand Down
Loading