Skip to content

Commit 3369d8b

Browse files
committed
fix tests
1 parent 9149f60 commit 3369d8b

File tree

17 files changed

+458
-184
lines changed

17 files changed

+458
-184
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ Ask first:
394394
### Dislikes
395395

396396
- backend creep in the standalone runtime
397+
- OBS-coupled runtime architecture or UI; `PrompterOne` must be the streaming system itself, not an OBS companion or Browser Source wrapper
397398
- hardcoded fallback reader/test fixtures such as inline `Ready` chunks, fake word models, or synthetic UI state embedded directly in tests when the same behavior can be exercised through shared script fixtures, builders, or production-owned constants
398399
- agent-started local servers taking shared user ports or using ports outside the reserved `5050-5070` agent range
399400
- brittle selectors without `data-testid`

docs/Features/ReaderRuntime.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ flowchart LR
5959
- `teleprompter` keeps TPS inline colors visible even when a phrase group is active or the active word is highlighted.
6060
- `teleprompter` keeps the active focus word calm: the active word may be brighter than its neighbors, but upcoming and read words stay gently dimmed and active-word glow stays restrained enough to avoid a bright moving patch.
6161
- `teleprompter` persists font scale, text width, focal point, and camera auto-start changes through `IUserSettingsStore` and restores them from stored `ReaderSettings` during bootstrap.
62-
- `teleprompter` prepositions the next card below the focal line before activation, so forward and backward block jumps both animate upward instead of alternating direction.
62+
- `teleprompter` keeps forward block jumps on the straight reference path, but backward block jumps reverse that motion so the returning previous block comes in from above while the outgoing current block drops away.
6363
- `teleprompter` uses one smooth paragraph realignment while words advance inside a card, but the first word of a newly entered card is already pre-centered so block changes do not trigger a second correction pass.
6464
- `teleprompter` loads its feature stylesheet from the initial host `<head>` instead of relying on route-time `HeadContent`, so direct opens and route transitions share the same first-paint styling.
6565

@@ -80,4 +80,4 @@ flowchart LR
8080
- Playwright verifies a dedicated reader-timing probe for both `learn` and `teleprompter`, recording emitted words in the browser and checking that sequence order and elapsed delays match the rendered timing contract word by word.
8181
- Playwright verifies the teleprompter stylesheet is already registered in `document.styleSheets` before the app navigates into the teleprompter route.
8282
- Playwright verifies custom TPS speed offsets change computed teleprompter `letter-spacing` while `[normal]` words reset back to neutral spacing and timing.
83-
- Playwright verifies teleprompter width and focal settings survive a real browser reload and that backward block jumps keep the outgoing card on the upward exit path during the transition.
83+
- Playwright verifies teleprompter width and focal settings survive a real browser reload and that backward block jumps reverse direction instead of reusing the forward upward-only path.

src/PrompterOne.App/wwwroot/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<link rel="stylesheet" href="_content/PrompterOne.Shared/design/tokens.css" />
1414
<link rel="stylesheet" href="_content/PrompterOne.Shared/design/components.css" />
1515
<link rel="stylesheet" href="_content/PrompterOne.Shared/design/styles.css" />
16+
<link rel="preload" href="_content/PrompterOne.Shared/design/learn.css" as="style" />
17+
<link rel="preload" href="_content/PrompterOne.Shared/design/modules/30-rsvp.css" as="style" />
1618
<link rel="stylesheet" href="_content/PrompterOne.Shared/design/teleprompter.css" />
1719
<link rel="stylesheet" href="_content/PrompterOne.Shared/app.css" />
1820
<link rel="stylesheet" href="PrompterOne.App.styles.css" />
@@ -46,7 +48,6 @@ <h2 id="app-shell-error-title">PrompterOne hit a shell error</h2>
4648
<script src="_content/PrompterOne.Shared/media/go-live-output-vdo-ninja.js"></script>
4749
<script src="_content/PrompterOne.Shared/media/go-live-output.js"></script>
4850
<script src="_content/PrompterOne.Shared/editor/editor-source-panel.js"></script>
49-
<script src="_content/PrompterOne.Shared/learn/learn-rsvp-layout.js"></script>
5051
<script src="_content/PrompterOne.Shared/teleprompter/teleprompter-reader.js"></script>
5152
<script src="_content/PrompterOne.Shared/app/cross-tab-message-bus.js"></script>
5253
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace PrompterOne.Shared.Pages;
2+
3+
public partial class LearnPage
4+
{
5+
private const string LayoutReadyFalseValue = "false";
6+
private const string LayoutReadyTrueValue = "true";
7+
8+
private bool _isFocusLayoutReady;
9+
private TaskCompletionSource<bool>? _pendingFocusLayoutSyncCompletionSource;
10+
11+
private string BuildLayoutReadyAttributeValue() => _isFocusLayoutReady
12+
? LayoutReadyTrueValue
13+
: LayoutReadyFalseValue;
14+
15+
private void MarkFocusLayoutDirty()
16+
{
17+
_isFocusLayoutReady = false;
18+
_pendingFocusLayoutSyncCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
19+
}
20+
21+
private async Task AwaitPendingFocusLayoutSyncAsync()
22+
{
23+
var pendingSync = _pendingFocusLayoutSyncCompletionSource;
24+
if (pendingSync is null)
25+
{
26+
return;
27+
}
28+
29+
await pendingSync.Task;
30+
}
31+
32+
private void CompletePendingFocusLayoutSync(bool isReady)
33+
{
34+
_isFocusLayoutReady = isReady;
35+
_pendingFocusLayoutSyncCompletionSource?.TrySetResult(isReady);
36+
}
37+
}

src/PrompterOne.Shared/Learn/Pages/LearnPage.Playback.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ await PersistLearnSettingsAsync(settings => settings with
6767

6868
UpdateDisplayedState();
6969
UpdateShellState();
70+
await InvokeAsync(StateHasChanged);
71+
await AwaitPendingFocusLayoutSyncAsync();
7072
RestartPlaybackLoopIfActive();
7173
}
7274

@@ -81,6 +83,7 @@ private async Task StepRsvpWordAsync(int delta)
8183
UpdateDisplayedState();
8284
RestartPlaybackLoopIfActive();
8385
await InvokeAsync(StateHasChanged);
86+
await AwaitPendingFocusLayoutSyncAsync();
8487
}
8588

8689
private void RestartPlaybackLoopIfActive()

src/PrompterOne.Shared/Learn/Pages/LearnPage.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
<div class="rsvp-display"
2424
@ref="_displayRoot"
25-
data-rsvp-layout-ready="false"
25+
data-rsvp-layout-ready="@BuildLayoutReadyAttributeValue()"
2626
data-testid="@UiTestIds.Learn.Display">
2727
<div class="rsvp-crosshair">
2828
<svg width="16" height="16" viewBox="0 0 16 16">

src/PrompterOne.Shared/Learn/Pages/LearnPage.razor.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ protected override Task OnParametersSetAsync()
7272
StopPlaybackLoop();
7373
_loadState = true;
7474
_focusScreenAfterRender = true;
75+
MarkFocusLayoutDirty();
7576
return Task.CompletedTask;
7677
}
7778

@@ -102,7 +103,9 @@ await Diagnostics.RunAsync(
102103
if (_syncFocusLayoutAfterRender)
103104
{
104105
_syncFocusLayoutAfterRender = false;
105-
await LearnRsvpLayoutInterop.SyncLayoutAsync(_displayRoot, _focusRow, _focusWord, _focusOrp);
106+
var didSync = await LearnRsvpLayoutInterop.SyncLayoutAsync(_displayRoot, _focusRow, _focusWord, _focusOrp);
107+
CompletePendingFocusLayoutSync(didSync);
108+
await InvokeAsync(StateHasChanged);
106109
}
107110

108111
if (_startPlaybackAfterLayoutSync)
@@ -164,6 +167,7 @@ private void UpdateDisplayedState()
164167
{
165168
if (_timeline.Count == 0)
166169
{
170+
MarkFocusLayoutDirty();
167171
_currentWordLeading = string.Empty;
168172
_currentWordOrp = ReadyWord;
169173
_currentWordTrailing = string.Empty;
@@ -178,6 +182,7 @@ private void UpdateDisplayedState()
178182

179183
_currentIndex = Math.Clamp(_currentIndex, 0, _timeline.Count - 1);
180184
var entry = _timeline[_currentIndex];
185+
MarkFocusLayoutDirty();
181186
var displayWord = NormalizeDisplayWord(entry.Word);
182187
var focusWord = BuildFocusWord(string.IsNullOrWhiteSpace(displayWord) ? entry.Word : displayWord);
183188
var sentenceRange = ResolveSentenceRange(_timeline, _currentIndex);

src/PrompterOne.Shared/Learn/Services/LearnRsvpLayoutInterop.cs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@
33

44
namespace PrompterOne.Shared.Services;
55

6-
public sealed class LearnRsvpLayoutInterop(IJSRuntime jsRuntime)
6+
public sealed class LearnRsvpLayoutInterop(IJSRuntime jsRuntime) : IAsyncDisposable
77
{
88
private readonly IJSRuntime _jsRuntime = jsRuntime;
9+
private Task<IJSObjectReference?>? _moduleTask;
910

10-
public ValueTask SyncLayoutAsync(
11+
public async ValueTask<bool> SyncLayoutAsync(
1112
ElementReference display,
1213
ElementReference row,
1314
ElementReference focusWord,
14-
ElementReference focusOrp) =>
15-
_jsRuntime.InvokeVoidAsync(
15+
ElementReference focusOrp)
16+
{
17+
var module = await GetModuleAsync();
18+
if (module is null)
19+
{
20+
return false;
21+
}
22+
23+
return await module.InvokeAsync<bool>(
1624
LearnRsvpLayoutInteropMethodNames.SyncLayout,
1725
display,
1826
row,
@@ -22,4 +30,40 @@ public ValueTask SyncLayoutAsync(
2230
LearnRsvpLayoutContract.FocusRightExtentCssCustomProperty,
2331
LearnRsvpLayoutContract.LayoutReadyAttributeName,
2432
LearnRsvpLayoutContract.FontSyncReadyAttributeName);
33+
}
34+
35+
public async ValueTask DisposeAsync()
36+
{
37+
if (_moduleTask is null)
38+
{
39+
return;
40+
}
41+
42+
var module = await _moduleTask;
43+
if (module is not null)
44+
{
45+
await module.DisposeAsync();
46+
}
47+
}
48+
49+
private Task<IJSObjectReference?> GetModuleAsync() =>
50+
_moduleTask ??= ImportModuleAsync();
51+
52+
private async Task<IJSObjectReference?> ImportModuleAsync()
53+
{
54+
try
55+
{
56+
return await _jsRuntime.InvokeAsync<IJSObjectReference>(
57+
LearnRsvpLayoutInteropMethodNames.JSImportMethodName,
58+
LearnRsvpLayoutInteropMethodNames.ModulePath);
59+
}
60+
catch (InvalidOperationException)
61+
{
62+
return null;
63+
}
64+
catch (JSException)
65+
{
66+
return null;
67+
}
68+
}
2569
}

src/PrompterOne.Shared/Learn/Services/LearnRsvpLayoutInteropMethodNames.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace PrompterOne.Shared.Services;
22

33
internal static class LearnRsvpLayoutInteropMethodNames
44
{
5-
private const string NamespacePrefix = "LearnRsvpLayoutInterop";
6-
7-
public const string SyncLayout = NamespacePrefix + ".syncLayout";
5+
public const string JSImportMethodName = "import";
6+
public const string ModulePath = "./_content/PrompterOne.Shared/learn/learn-rsvp-layout.js";
7+
public const string SyncLayout = "syncLayout";
88
}

src/PrompterOne.Shared/Teleprompter/Pages/TeleprompterPage.ReaderPlayback.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,9 @@ private async Task<int> AdvanceReaderPlaybackAsync(CancellationToken cancellatio
331331

332332
private async Task AdvanceToCardAsync(int nextCardIndex, CancellationToken cancellationToken)
333333
{
334+
await CancelPendingReaderCardTransitionAsync();
334335
var previousCardIndex = _activeReaderCardIndex;
336+
var transitionCts = BeginReaderCardTransitionScope(cancellationToken);
335337
await PrepareReaderCardTransitionAsync(nextCardIndex);
336338
await PrepareReaderCardAlignmentAsync(nextCardIndex, 0);
337339
_readerTransitionSourceCardIndex = previousCardIndex;
@@ -343,16 +345,21 @@ private async Task AdvanceToCardAsync(int nextCardIndex, CancellationToken cance
343345

344346
try
345347
{
346-
await Task.Delay(ReaderCardTransitionMilliseconds, cancellationToken);
348+
await Task.Delay(ReaderCardTransitionMilliseconds, transitionCts.Token);
347349

348-
if (cancellationToken.IsCancellationRequested)
350+
if (transitionCts.IsCancellationRequested)
349351
{
350352
return;
351353
}
354+
352355
await ActivateReaderWordAsync(0, alignBeforeActivation: false);
353356
}
357+
catch (OperationCanceledException) when (transitionCts.IsCancellationRequested)
358+
{
359+
}
354360
finally
355361
{
362+
CompleteReaderCardTransitionScope(transitionCts);
356363
await FinalizeReaderCardTransitionAsync(previousCardIndex);
357364
}
358365
}

0 commit comments

Comments
 (0)