Merge pull request #32 from avalon-vanguard/fix/routes-panel-honesty
Let the Routes panel be clicked as soon as it is back, and stop it departing from elsewhere
This commit is contained in:
@@ -253,6 +253,65 @@ describe('HudDockComponent', () => {
|
|||||||
expect(tab('Readout').getAttribute('aria-selected')).toBe('true');
|
expect(tab('Readout').getAttribute('aria-selected')).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('will not plot from a departure that was typed but never chosen', () => {
|
||||||
|
setReadout();
|
||||||
|
fixture.componentRef.setInput('routing', true);
|
||||||
|
fixture.componentRef.setInput('currentStar', { id: 3, name: "Barnard's Star", subtitle: '1.8 pc' });
|
||||||
|
fixture.componentRef.setInput('routeOptions', [{ id: 7, name: 'Sirius', subtitle: '2.6 pc' }]);
|
||||||
|
fixture.componentRef.setInput('defaultTab', 'routes');
|
||||||
|
fixture.detectChanges();
|
||||||
|
const plot = () => [...host().querySelectorAll<HTMLButtonElement>('button')].find((button) => button.textContent?.includes('Plot route'))!;
|
||||||
|
const type = (field: string, value: string) => {
|
||||||
|
const input = host().querySelector<HTMLInputElement>(`#route-${field}`)!;
|
||||||
|
input.value = value;
|
||||||
|
input.dispatchEvent(new Event('input'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
type('to', 'Sir');
|
||||||
|
host().querySelector<HTMLButtonElement>('#dock-panel-routes ul button')!.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
// With the departure field empty, the view's own star stands in for it.
|
||||||
|
expect(plot().disabled).toBe(false);
|
||||||
|
|
||||||
|
// Text that names no chosen star is not a departure: plotting from the view's star instead
|
||||||
|
// would name one place and leave from another.
|
||||||
|
type('from', 'Sol');
|
||||||
|
expect(plot().disabled).toBe(true);
|
||||||
|
|
||||||
|
type('from', '');
|
||||||
|
expect(plot().disabled).toBe(false);
|
||||||
|
|
||||||
|
// A space is not text that names a star: the field looks empty, the scene offers nothing to
|
||||||
|
// choose for it, and the button going dead would have nothing on screen to explain it.
|
||||||
|
type('from', ' ');
|
||||||
|
expect(plot().disabled).toBe(false);
|
||||||
|
|
||||||
|
// The offer beside a refusal is the same request by another route, so it is held to the same
|
||||||
|
// test: moving the range with nothing to plot leaves the panel contradicting itself.
|
||||||
|
fixture.componentRef.setInput('routeResult', { stars: [], totalPc: 0, neededRangePc: 1.8, gaveUp: false, least: true });
|
||||||
|
fixture.detectChanges();
|
||||||
|
const offer = () => host().querySelector<HTMLButtonElement>('[data-testid="route-summary"] button')!;
|
||||||
|
expect(offer().disabled).toBe(false);
|
||||||
|
|
||||||
|
type('from', 'Sol');
|
||||||
|
expect(offer().disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not replay the acquire wipe over the Routes panel, whose entries survive the trip', () => {
|
||||||
|
setReadout();
|
||||||
|
fixture.componentRef.setInput('routing', true);
|
||||||
|
fixture.componentRef.setInput('display', DEFAULT_HUD_DISPLAY);
|
||||||
|
fixture.componentRef.setInput('defaultTab', 'routes');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// The wipe clips its panel for 380 ms, which swallows clicks on entries that are already there.
|
||||||
|
expect(host().querySelector('#dock-panel-routes')?.classList.contains('hud-acquire')).toBe(false);
|
||||||
|
tab('Display').click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(host().querySelector('#dock-panel-display')?.classList.contains('hud-acquire')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('says the search gave up rather than that there is no route, when that is what happened', () => {
|
it('says the search gave up rather than that there is no route, when that is what happened', () => {
|
||||||
fixture.componentRef.setInput('routing', true);
|
fixture.componentRef.setInput('routing', true);
|
||||||
fixture.componentRef.setInput('defaultTab', 'routes');
|
fixture.componentRef.setInput('defaultTab', 'routes');
|
||||||
|
|||||||
@@ -186,9 +186,11 @@ function isWideViewport(): boolean {
|
|||||||
}
|
}
|
||||||
<!-- Hidden rather than unmounted: the departure, destination and range it holds would
|
<!-- Hidden rather than unmounted: the departure, destination and range it holds would
|
||||||
otherwise reset on every trip to another tab, while the scene kept drawing the graph at
|
otherwise reset on every trip to another tab, while the scene kept drawing the graph at
|
||||||
the old range. Hiding still replays the acquire wipe when it is shown again. -->
|
the old range. No acquire wipe, unlike the panels around it: this is the one that comes
|
||||||
|
back with what it had, so it is not acquiring anything — and for the 380 ms the wipe
|
||||||
|
runs, its clip path swallows clicks on the suggestions it just brought back. -->
|
||||||
@if (routing()) {
|
@if (routing()) {
|
||||||
<section id="dock-panel-routes" role="tabpanel" aria-labelledby="dock-tab-routes" [hidden]="activeTab() !== 'routes'" class="hud-acquire hud-brackets hud-surface pointer-events-auto mb-2 w-full max-w-xl px-4 py-3">
|
<section id="dock-panel-routes" role="tabpanel" aria-labelledby="dock-tab-routes" [hidden]="activeTab() !== 'routes'" class="hud-brackets hud-surface pointer-events-auto mb-2 w-full max-w-xl px-4 py-3">
|
||||||
<app-routes-panel
|
<app-routes-panel
|
||||||
[result]="routeResult()"
|
[result]="routeResult()"
|
||||||
[pending]="routePending()"
|
[pending]="routePending()"
|
||||||
|
|||||||
@@ -125,8 +125,9 @@ type Field = 'from' | 'to';
|
|||||||
@if (plotted.neededRangePc !== null) {
|
@if (plotted.neededRangePc !== null) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
[disabled]="!canPlot() || pending()"
|
||||||
(click)="raiseTo(plotted.neededRangePc)"
|
(click)="raiseTo(plotted.neededRangePc)"
|
||||||
class="text-accent underline decoration-accent/40 underline-offset-2 hover:decoration-accent focus-visible:outline-1 focus-visible:outline-accent"
|
class="text-accent underline decoration-accent/40 underline-offset-2 disabled:opacity-40 disabled:no-underline enabled:hover:decoration-accent focus-visible:outline-1 focus-visible:outline-accent"
|
||||||
>
|
>
|
||||||
{{ format(plotted.neededRangePc) }} would reach.
|
{{ format(plotted.neededRangePc) }} would reach.
|
||||||
</button>
|
</button>
|
||||||
@@ -184,8 +185,14 @@ export class RoutesPanelComponent {
|
|||||||
private readonly chosen = signal<Record<Field, RouteStarOption | null>>({ from: null, to: null });
|
private readonly chosen = signal<Record<Field, RouteStarOption | null>>({ from: null, to: null });
|
||||||
private readonly typed = signal<Record<Field, string>>({ from: '', to: '' });
|
private readonly typed = signal<Record<Field, string>>({ from: '', to: '' });
|
||||||
|
|
||||||
/** Departure falls back to wherever the view already is, so one field is usually enough. */
|
/**
|
||||||
private readonly departure = computed(() => this.chosen().from ?? this.currentStar());
|
* Departure falls back to wherever the view already is, so one field is usually enough — but only
|
||||||
|
* while the field is empty. Text left in it that names no chosen star used to fall back all the
|
||||||
|
* same, so the panel read "Sol" and the route left from whatever the view had since flown to.
|
||||||
|
*/
|
||||||
|
// Trimmed, as the scene trims the same text before offering matches for it: a field holding one
|
||||||
|
// space looks empty, offers nothing to choose, and would otherwise count as a departure.
|
||||||
|
private readonly departure = computed(() => this.chosen().from ?? (this.typed().from.trim() ? null : this.currentStar()));
|
||||||
|
|
||||||
readonly canPlot = computed(() => this.departure() !== null && this.chosen().to !== null);
|
readonly canPlot = computed(() => this.departure() !== null && this.chosen().to !== null);
|
||||||
readonly rangeLabel = computed(() => formatParsecs(this.rangePc()));
|
readonly rangeLabel = computed(() => formatParsecs(this.rangePc()));
|
||||||
|
|||||||
Reference in New Issue
Block a user