Skip to content
Merged
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
24 changes: 23 additions & 1 deletion apps/escurel-explore/lib/crm/crm_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,33 @@ class _CollapsibleRegion extends StatelessWidget {
);

if (collapsed) {
// The whole collapsed rail is the expand target. A centered 16px icon
// is far too small to reliably hit — and when *both* panes collapse
// the right rail balloons to a wide blank area — so an InkWell fills
// the region and re-expands on a tap anywhere within it.
return Semantics(
label: label,
container: true,
explicitChildNodes: true,
child: Center(child: toggle),
child: Semantics(
label: '$label-expand',
button: true,
onTap: onToggle,
excludeSemantics: true,
child: Tooltip(
message: 'Expand',
child: InkWell(
onTap: onToggle,
child: Center(
child: Icon(
edge == _Edge.right ? Icons.chevron_right : Icons.chevron_left,
size: 16,
color: kOnSurfaceVariant,
),
),
),
),
),
);
}
return Semantics(
Expand Down
83 changes: 83 additions & 0 deletions apps/escurel-explore/test/crm/both_panes_collapse_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Reproduction for the live report: on an instance, collapsing BOTH panes
// leaves the expand chevrons unresponsive. Drives the real chevrons via
// the pointer path over the real fixture.

import 'package:escurel_explore/client/escurel_client.dart';
import 'package:escurel_explore/client/fixture_escurel_client.dart';
import 'package:escurel_explore/crm/crm_workspace.dart';
import 'package:escurel_explore/state/providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

EscurelClient _corpus() => FixtureEscurelClient.fromSources(
skillFiles: const {
'customer.md': '---\ntype: skill\nid: customer\ndescription: A buying org.\n---\n# customer\n',
},
instanceFiles: const {
'customer__acme.md':
'---\ntype: instance\nskill: customer\nid: acme\nname: Acme Ltd\n---\n# Acme Ltd\n',
},
);

Future<void> _pump(WidgetTester tester) async {
tester.view.physicalSize = const Size(1400, 900);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
ProviderScope(
overrides: [escurelClientProvider.overrideWithValue(_corpus())],
child: const MaterialApp(home: CrmWorkspace()),
),
);
await tester.pumpAndSettle();
}

void main() {
testWidgets('collapse both panes, then both expand chevrons work', (tester) async {
await _pump(tester);

// Collapse the left (events) pane.
await tester.tap(find.bySemanticsLabel('region-events-collapse'));
await tester.pumpAndSettle();
// Collapse the right (instance) pane.
await tester.tap(find.bySemanticsLabel('region-instance-collapse'));
await tester.pumpAndSettle();

// Both collapsed: two expand chevrons, no panes.
expect(find.bySemanticsLabel('region-events-expand'), findsOneWidget);
expect(find.bySemanticsLabel('region-instance-expand'), findsOneWidget);
expect(find.bySemanticsLabel('event-pane'), findsNothing);
expect(find.bySemanticsLabel('instance-pane'), findsNothing);

// Expand the right pane.
await tester.tap(find.bySemanticsLabel('region-instance-expand'));
await tester.pumpAndSettle();
expect(find.bySemanticsLabel('instance-pane'), findsOneWidget,
reason: 'right pane must re-expand');

// Expand the left pane.
await tester.tap(find.bySemanticsLabel('region-events-expand'));
await tester.pumpAndSettle();
expect(find.bySemanticsLabel('event-pane'), findsOneWidget,
reason: 'left pane must re-expand');
});

testWidgets('a tap anywhere in the collapsed rail re-expands it (not just the icon)', (tester) async {
await _pump(tester);

await tester.tap(find.bySemanticsLabel('region-events-collapse'));
await tester.pumpAndSettle();
expect(find.bySemanticsLabel('event-pane'), findsNothing);

// Tap near the TOP of the collapsed rail — far from the vertically
// centered chevron icon. Before the full-rail hit target, only the
// ~16px icon responded, so a real click that missed it did nothing.
final rail = tester.getRect(find.bySemanticsLabel('region-events'));
await tester.tapAt(Offset(rail.center.dx, rail.top + 24));
await tester.pumpAndSettle();
expect(find.bySemanticsLabel('event-pane'), findsOneWidget,
reason: 'tapping the rail (off the icon) must still expand it');
});
}
33 changes: 33 additions & 0 deletions docs/notes/discovered/2026-05-31-collapsed-rail-tiny-hit-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# A collapsed pane's expand chevron was a too-small hit target

**Symptom.** On the CRM workspace, collapsing a pane worked, but the
**expand** chevron in the resulting rail often did nothing for a real
mouse — "I can collapse, but afterwards expand is not possible." Worst
when *both* panes were collapsed (a near-blank screen with two tiny
chevrons that wouldn't respond).

**Cause.** The collapsed rail rendered `Center(child: toggle)` where
`toggle` was a 16px `IconButton`. Only that ~16–28px icon was
tappable. The collapse button, by contrast, lives in a full-width 28px
header bar (easy to hit) — hence "collapse works, expand doesn't." When
both panes collapse, the *right* rail also balloons to most of the
width (`leftW = rail`, `rightW = w - rail - divW`), so the chevron sits
alone in a large blank region that ignores clicks everywhere except the
icon.

**Why tests missed it.** `tester.tap(find.bySemanticsLabel(...))` taps
the widget's **center**, which is exactly where the icon is — so every
collapse/expand test passed while real off-icon clicks failed. The
regression test now uses `tester.tapAt(...)` at an offset *away* from the
centered icon (near the top of the rail) to exercise the real hit area.

**Fix.** Make the whole collapsed rail the tap target: wrap the rail in
an `InkWell(onTap: onToggle)` filling the region, with the chevron
centered for affordance. A tap anywhere in the rail re-expands it.

**How to recognise it.** A control that "doesn't work" for real users
but passes every widget test. Check whether the tappable area is a small
centered child while the test taps dead-center — `tester.tap` hits the
center regardless of size, so it cannot catch an undersized hit target.
Use `tapAt` with an offset, and prefer generous, full-region hit targets
for rails/toggles.
Loading