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; title: string;
text: string; text: string;
img?: 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 { export interface CurrentLocationResponse {

View File

@@ -122,6 +122,38 @@ describe('LocalLocationStore', () => {
expect(store.interactionResult()).toBeNull(); 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', () => { it('resolves the hotspot an action mirrors', () => {
const store = setup(); const store = setup();

View File

@@ -74,9 +74,14 @@ export class LocalLocationStore {
this.interactionErrorState.set(null); this.interactionErrorState.set(null);
try { try {
this.interactionResultState.set( const result = await firstValueFrom(this.api.runLocationInteraction(interactionKey));
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) { } catch (error) {
this.interactionErrorState.set(this.toErrorMessage(error)); this.interactionErrorState.set(this.toErrorMessage(error));
} finally { } finally {

View File

@@ -20,6 +20,11 @@
<div class="interaction-panel__copy"> <div class="interaction-panel__copy">
<h2 class="interaction-panel__title">{{ result.title }}</h2> <h2 class="interaction-panel__title">{{ result.title }}</h2>
<p class="interaction-panel__text">{{ result.text }}</p> <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>
</div> </div>
} @else { } @else {

View File

@@ -63,6 +63,14 @@
color: var(--ar-danger); 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 { .interaction-panel__close {
justify-self: end; justify-self: end;
padding: var(--ar-space-2) var(--ar-space-5); 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'; import { LocationInteractionPanelComponent } from './location-interaction-panel.component';
async function setup(inputs: { 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; error?: string | null;
}): Promise<{ }): Promise<{
fixture: ComponentFixture<LocationInteractionPanelComponent>; fixture: ComponentFixture<LocationInteractionPanelComponent>;
@@ -110,4 +118,31 @@ describe('LocationInteractionPanelComponent', () => {
expect(element.querySelector('[data-interaction-panel]')).toBeNull(); 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();
});
}); });