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
10 changes: 9 additions & 1 deletion packages/data/src/domain/station/station-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export class StationService {
* Get Stations without resolving assets
*/
getUnresolvedStations(): Station[] {
return this.storage.get(`stations-${this.getLanguage()}`) as Station[];
if (this.storage.has(`stations-${this.getLanguage()}`)) {
return this.storage.get(`stations-${this.getLanguage()}`) as Station[];
} else {
return [];
}
}

async getStations(): Promise<Station[]> {
Expand All @@ -46,6 +50,10 @@ export class StationService {

const station = this.storage.get(storageKey) as Station;

if (!Number.isFinite(station.collectedPercentage)) {
station.collectedPercentage = 0;
}

if (station && station.images) {
for (let i = 0; i < station.images.length; i++) {
station.images[i] = await this.assetService.getAsset(station.images[i] as string);
Expand Down
28 changes: 28 additions & 0 deletions packages/data/test/domain/station/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ describe('test station service', () => {
expect(resolveUrl.mock.calls.length).toEqual(0);
});

test('should return empty array when unresolved stations storage entry does not exist', () => {
memoryStorage.set('language', 'xy');

const stations = service.getUnresolvedStations();

expect(stations).toEqual([]);
});

test('should store collected percentage for station', async () => {
resolveUrl.mockReturnValue('http://internal.url');
memoryStorage.set('language', 'xy');
Expand All @@ -77,6 +85,26 @@ describe('test station service', () => {
await expect(service.updateCollectedPercentage("123", "123", 0.456)).rejects.toThrow();
});

test('should default collectedPercentage to 0 when missing, null, or NaN in storage', async () => {
memoryStorage.set('language', 'xy');
resolveUrl.mockReturnValue('http://internal.url');

const undefinedStation = setStationToStorage(memoryStorage, 'xy');
const nullStation = setStationToStorage(memoryStorage, 'xy');
const nanStation = setStationToStorage(memoryStorage, 'xy');

(memoryStorage.get(`station-xy-${nullStation.id}`) as Station).collectedPercentage = null as unknown as number;
(memoryStorage.get(`station-xy-${nanStation.id}`) as Station).collectedPercentage = NaN;

const loadedUndefined = await service.getStation(undefinedStation.id);
const loadedNull = await service.getStation(nullStation.id);
const loadedNaN = await service.getStation(nanStation.id);

expect(loadedUndefined.collectedPercentage).toBe(0);
expect(loadedNull.collectedPercentage).toBe(0);
expect(loadedNaN.collectedPercentage).toBe(0);
});

test('should remove collected percentage for all stations in language', async () => {
resolveUrl.mockReturnValue('http://internal.url');
memoryStorage.set('language', 'xy');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,23 @@ exports[`render station icon just above lower limit starts showing progress 1`]
</sc-station-icon>
`;

exports[`render station icon with NaN collectedPercent falls back to 0% progress 1`] = `
<sc-station-icon class="hydrated">
<mock:shadow-root>
<div class="station-icon-wrapper station-icon-size-normal">
<svg class="progress-ring" width="30" height="30" viewBox="0 0 30 30" role="progressbar" aria-label="Collection progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" focusable="false">
<circle class="progress-ring-track" cx="15" cy="15" r="13.5" stroke-width="3" fill="none"></circle>
<circle class="progress-ring-fill" cx="15" cy="15" r="13.5" stroke-width="3" fill="none" stroke-linecap="round" stroke-dasharray="84.82300164692441" stroke-dashoffset="84.82300164692441" transform="rotate(-90 15 15)"></circle>
</svg>
<div class="station-icon">
<slot></slot>
</div>
</div>
</mock:shadow-root>
13
</sc-station-icon>
`;

exports[`render station icon with custom lowerLimitPercent above boundary shows progress 1`] = `
<sc-station-icon class="hydrated">
<mock:shadow-root>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ test('render station icon with custom lowerLimitPercent above boundary shows pro
const { root } = await render(<sc-station-icon collectedPercent={15} lowerLimitPercent={10}>13</sc-station-icon>);
expect(root).toMatchSnapshot();
});

test('render station icon with NaN collectedPercent falls back to 0% progress', async () => {
const { root } = await render(<sc-station-icon collectedPercent={NaN}>13</sc-station-icon>);
expect(root).toMatchSnapshot();
});
3 changes: 3 additions & 0 deletions packages/ui/src/components/station-icon/station-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export class StationIcon {
@Prop() size: 'small' | 'normal' | 'large' = 'normal';

private calculatePercent() {
if (!Number.isFinite(this.collectedPercent)) {
return 0;
}
Comment on lines +35 to +37

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculatePercent() now normalizes non-finite collectedPercent to 0, but the collected/checkmark rendering condition later in render() still uses the raw this.collectedPercent >= this.upperLimitPercent. For values like Infinity, this will show the collected icon while the progress ring renders as 0%. Consider reusing the normalized percent value (or applying the same non-finite guard) for the collected/checkmark condition to keep UI consistent for all non-finite inputs.

Copilot uses AI. Check for mistakes.
if (this.collectedPercent >= this.upperLimitPercent) {
return 100;
} else if (this.collectedPercent <= this.lowerLimitPercent) {
Expand Down
Loading