feat(web): announce a newly discovered route

This commit is contained in:
Bastian Wagner
2026-08-23 16:56:48 +02:00
parent 06721f24e0
commit 40d69693ba
6 changed files with 96 additions and 4 deletions

View File

@@ -89,6 +89,13 @@ export interface LocationInteractionResult {
title: string;
text: string;
img?: string;
/**
* Set only the first time an interaction reveals a route. Optional on the
* client although the API always sends it: every existing test fixture
* builds this object literally, and a required field would break them all
* for no gain.
*/
discoveredLocation?: { key: string; name: string } | null;
}
export interface CurrentLocationResponse {

View File

@@ -122,6 +122,38 @@ describe('LocalLocationStore', () => {
expect(store.interactionResult()).toBeNull();
});
it('reloads the location when the interaction reveals a route', async () => {
const load = vi.fn().mockResolvedValue(undefined);
const store = setup(
{
runLocationInteraction: vi.fn().mockReturnValue(
of({ ...trackResult, discoveredLocation: { key: 'ash-pit', name: 'Ash Pit' } }),
),
},
{ load },
);
await store.runInteraction('inspect-tracks');
expect(load).toHaveBeenCalledTimes(1);
});
it('does not reload the location when nothing new was revealed', async () => {
const load = vi.fn().mockResolvedValue(undefined);
const store = setup(
{
runLocationInteraction: vi.fn().mockReturnValue(
of({ ...trackResult, discoveredLocation: null }),
),
},
{ load },
);
await store.runInteraction('inspect-tracks');
expect(load).not.toHaveBeenCalled();
});
it('resolves the hotspot an action mirrors', () => {
const store = setup();

View File

@@ -74,9 +74,14 @@ export class LocalLocationStore {
this.interactionErrorState.set(null);
try {
this.interactionResultState.set(
await firstValueFrom(this.api.runLocationInteraction(interactionKey)),
);
const result = await firstValueFrom(this.api.runLocationInteraction(interactionKey));
this.interactionResultState.set(result);
if (result.discoveredLocation) {
// The connection list is server-filtered, so a fresh reveal only
// shows up after the location is re-read.
await this.load();
}
} catch (error) {
this.interactionErrorState.set(this.toErrorMessage(error));
} finally {

View File

@@ -20,6 +20,11 @@
<div class="interaction-panel__copy">
<h2 class="interaction-panel__title">{{ result.title }}</h2>
<p class="interaction-panel__text">{{ result.text }}</p>
@if (result.discoveredLocation; as discovered) {
<p class="interaction-panel__discovery" data-discovered-location role="status">
New route discovered: {{ discovered.name }}.
</p>
}
</div>
</div>
} @else {

View File

@@ -63,6 +63,14 @@
color: var(--ar-danger);
}
.interaction-panel__discovery {
margin: 0;
color: var(--ar-success);
font-size: 0.95rem;
font-weight: 700;
line-height: 1.55;
}
.interaction-panel__close {
justify-self: end;
padding: var(--ar-space-2) var(--ar-space-5);

View File

@@ -2,7 +2,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LocationInteractionPanelComponent } from './location-interaction-panel.component';
async function setup(inputs: {
result?: { interactionKey: string; title: string; text: string; img?: string } | null;
result?:
| {
interactionKey: string;
title: string;
text: string;
img?: string;
discoveredLocation?: { key: string; name: string } | null;
}
| null;
error?: string | null;
}): Promise<{
fixture: ComponentFixture<LocationInteractionPanelComponent>;
@@ -110,4 +118,31 @@ describe('LocationInteractionPanelComponent', () => {
expect(element.querySelector('[data-interaction-panel]')).toBeNull();
});
it('says so when the interaction revealed a route', async () => {
const { element } = await setup({
result: {
interactionKey: 'inspect-watchpost',
title: 'The Watchpost',
text: 'Fresh tracks lead east.',
discoveredLocation: { key: 'ash-pit', name: 'Ash Pit' },
},
});
const banner = element.querySelector('[data-discovered-location]');
expect(banner?.textContent).toContain('Ash Pit');
});
it('stays quiet when nothing new was revealed', async () => {
const { element } = await setup({
result: {
interactionKey: 'search-guard-quarters',
title: 'Guard Quarters',
text: 'Nothing but ash.',
discoveredLocation: null,
},
});
expect(element.querySelector('[data-discovered-location]')).toBeNull();
});
});