Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Models.Intercom;
4using CascadeIDE.Services;
5using CascadeIDE.Services.Intercom;
6using CascadeIDE.Views.Chat;
7using CascadeIDE.Views.Chat.Skia;
8using SkiaSharp;
9using Xunit;
10
11namespace CascadeIDE.Tests;
12
13public sealed class SkiaChatFeedAttachHitTests
14{
15 [Fact]
16 public void ComputeFeedLinkHitRect_is_narrower_than_full_segment()
17 {
18 var segment = new SKRect(12, 80, 400, 104);
19 var metrics = new SkiaChatBubbleMetrics(
20 [new SkiaMarkdownLine([new SkiaMarkdownRun("[TryResolveFile]", SkiaMarkdownStyle.Link)])],
21 Footer: null,
22 TitleHeight: 0,
23 FooterHeight: 0,
24 LineHeight: 15);
25
26 var feed = SkiaChatFeedLayout.For(forwardHost: false);
27 var hit = SkiaChatBubbleRenderer.ComputeFeedLinkHitRect(segment, "[TryResolveFile]", metrics, feed);
28
29 Assert.True(hit.Width < segment.Width - 40f);
30 Assert.True(hit.Left > segment.Left);
31 Assert.InRange(hit.Top, segment.Top, segment.Bottom);
32 }
33
34 [Fact]
35 public void HitRegistry_find_prefers_attach_reveal_over_later_row_hit()
36 {
37 var registry = new SkiaChatHitRegistry();
38 registry.RegisterControlRect(
39 new Avalonia.Rect(0, 0, 400, 80),
40 new SkiaChatHit(0, null, ResetDetailMode: false));
41 registry.RegisterControlRect(
42 new Avalonia.Rect(12, 20, 200, 24),
43 new SkiaChatHit(0, null, ResetDetailMode: false, RevealAttachment: new AttachmentAnchor { File = "a.cs" }));
44
45 var index = registry.FindIndex(new Avalonia.Point(20, 28));
46
47 Assert.Equal(1, index);
48 Assert.True(registry.TryGetHit(index, out var hit));
49 Assert.NotNull(hit.RevealAttachment);
50 Assert.True(SkiaChatHitRegistry.WantsHandCursor(hit));
51 }
52
53 [Fact]
54 public void HitRegistry_content_rect_scroll_maps_to_control_viewport()
55 {
56 const float chromeTop = 48f;
57 const float scrollOffset = 120f;
58 var scrollInContext = scrollOffset - chromeTop;
59 var registry = new SkiaChatHitRegistry();
60 registry.RegisterContentRect(
61 new SKRect(12, 200, 372, 220),
62 scrollInContext,
63 new SkiaChatHit(0, null, ResetDetailMode: false, RevealAttachment: new AttachmentAnchor { File = "a.cs" }));
64
65 var controlY = chromeTop - scrollOffset + 200f;
66 var controlPoint = new Avalonia.Point(20, controlY + 5);
67 Assert.Equal(0, registry.FindIndex(controlPoint));
68 Assert.True(registry.TryGetHit(0, out var hit));
69 Assert.NotNull(hit.RevealAttachment);
70 }
71
72 [Fact]
73 public void Attach_chip_classify_resolved_degraded_failed_pending()
74 {
75 var resolved = new AttachmentAnchor
76 {
77 File = "src/Foo.cs",
78 ResolveOutcome = IntercomAttachmentRevealPlan.OutcomeResolved,
79 };
80 Assert.Equal(
81 IntercomAttachLinkVisualStatus.Resolved,
82 SkiaIntercomAttachLinkChip.Classify(resolved, messagePending: false));
83
84 var degraded = resolved with { ResolveOutcome = IntercomAttachmentRevealPlan.OutcomeMemberNotFound };
85 Assert.Equal(
86 IntercomAttachLinkVisualStatus.Degraded,
87 SkiaIntercomAttachLinkChip.Classify(degraded, messagePending: false));
88
89 var failed = resolved with
90 {
91 File = "missing.cs",
92 ResolveOutcome = IntercomAttachmentRevealPlan.OutcomeFileMissing,
93 };
94 Assert.Equal(
95 IntercomAttachLinkVisualStatus.Failed,
96 SkiaIntercomAttachLinkChip.Classify(failed, messagePending: false));
97
98 Assert.Equal(
99 IntercomAttachLinkVisualStatus.Pending,
100 SkiaIntercomAttachLinkChip.Classify(resolved, messagePending: true));
101 }
102
103 [Fact]
104 public void RegisterFeedMarkdownLinkHits_registers_narrow_link_rects()
105 {
106 var registry = new SkiaChatHitRegistry();
107 var drawContext = new SkiaChatDrawContext
108 {
109 Canvas = null!,
110 Theme = SkiaChatTheme.DarkFallback,
111 ContentLeft = 12,
112 ContentWidth = 360,
113 ScrollOffset = 0,
114 ItemIndex = 0,
115 HoveredItemIndex = -1,
116 SelectedMessageIndex = -1,
117 HitRegistry = registry,
118 };
119
120 var segment = new SKRect(12, 40, 372, 80);
121 var metrics = new SkiaChatBubbleMetrics(
122 [
123 new SkiaMarkdownLine(
124 [
125 new SkiaMarkdownRun("see ", SkiaMarkdownStyle.Plain),
126 new SkiaMarkdownRun("Run", SkiaMarkdownStyle.Link),
127 new SkiaMarkdownRun(" ok", SkiaMarkdownStyle.Plain),
128 ]),
129 ],
130 Footer: null,
131 TitleHeight: 0,
132 FooterHeight: 0,
133 LineHeight: 15);
134
135 var anchor = new AttachmentAnchor { File = "Foo.cs", MemberKey = "Run", DisplayLabel = "Run" };
136 var feed = SkiaChatFeedLayout.For(forwardHost: false);
137 SkiaChatBubbleRenderer.RegisterFeedMarkdownLinkHits(
138 drawContext,
139 segment,
140 metrics,
141 feed,
142 messageIndex: 3,
143 linkText => string.Equals(linkText, "Run", StringComparison.Ordinal) ? anchor : null);
144
145 Assert.Equal(1, registry.Count);
146 var linkBaseline = feed.FirstLineBaselineY(segment.Top, metrics.TitleHeight);
147 Assert.Equal(0, registry.FindIndex(new Avalonia.Point(50, linkBaseline)));
148 Assert.Equal(-1, registry.FindIndex(new Avalonia.Point(12, segment.Top)));
149 Assert.True(registry.TryGetHit(0, out var hit));
150 Assert.Equal(3, hit.MessageIndex);
151 Assert.Equal("Run", hit.RevealAttachment?.MemberKey);
152 }
153
154 [Fact]
155 public void HitRegistry_find_prefers_chrome_action_over_row_hit()
156 {
157 var registry = new SkiaChatHitRegistry();
158 registry.RegisterControlRect(
159 new Avalonia.Rect(0, 0, 400, 80),
160 new SkiaChatHit(0, null, ResetDetailMode: false));
161 registry.RegisterControlRect(
162 new Avalonia.Rect(20, 20, 80, 24),
163 new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.OverviewToggle));
164
165 Assert.Equal(1, registry.FindIndex(new Avalonia.Point(40, 30)));
166 }
167
168 [Fact]
169 public void HitRegistry_contains_pointer_action_for_wheel_routing()
170 {
171 var registry = new SkiaChatHitRegistry();
172 registry.RegisterControlRect(
173 new Avalonia.Rect(0, 500, 800, 120),
174 new SkiaChatHit(null, null, ResetDetailMode: false, PointerAction: SkiaChatPointerAction.ComposerFocus));
175
176 Assert.True(registry.ContainsPointerAction(new Avalonia.Point(100, 550), SkiaChatPointerAction.ComposerFocus));
177 Assert.False(registry.ContainsPointerAction(new Avalonia.Point(100, 200), SkiaChatPointerAction.ComposerFocus));
178 }
179
180 [Fact]
181 public void Link_body_measure_uses_single_link_run_for_bracket_label()
182 {
183 var ctx = new SkiaChatMeasureContext(48, 360);
184 var spec = new SkiaChatBubbleSpec(
185 "",
186 "[TryResolveFile]",
187 null,
188 SkiaChatBubbleKind.Feed,
189 SkiaBubbleFillRole.MessageAssistant,
190 SkiaChatBodyTone.Link,
191 false,
192 false,
193 false,
194 0);
195
196 var metrics = SkiaChatBubbleRenderer.Measure(ctx, spec);
197 Assert.Single(metrics.ContentLines);
198 Assert.Single(metrics.ContentLines[0].Runs);
199 Assert.Equal(SkiaMarkdownStyle.Link, metrics.ContentLines[0].Runs[0].Style);
200 }
201}
202
View only · write via MCP/CIDE