Skip to content

Commit e9b60a5

Browse files
stefanhuberclaude
andcommitted
fix: default collectedPercentage to 0 for non-finite values
Guard StationService.getStation and the station-icon progress ring against NaN, null, or undefined collectedPercentage values that would otherwise produce NaN aria-valuenow/stroke-dashoffset attributes in the rendered SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d35b4ad commit e9b60a5

5 files changed

Lines changed: 62 additions & 1 deletion

File tree

packages/data/src/domain/station/station-service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ export class StationService {
2222
* Get Stations without resolving assets
2323
*/
2424
getUnresolvedStations(): Station[] {
25-
return this.storage.get(`stations-${this.getLanguage()}`) as Station[];
25+
if (this.storage.has(`stations-${this.getLanguage()}`)) {
26+
return this.storage.get(`stations-${this.getLanguage()}`) as Station[];
27+
} else {
28+
return [];
29+
}
2630
}
2731

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

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

53+
if (!Number.isFinite(station.collectedPercentage)) {
54+
station.collectedPercentage = 0;
55+
}
56+
4957
if (station && station.images) {
5058
for (let i = 0; i < station.images.length; i++) {
5159
station.images[i] = await this.assetService.getAsset(station.images[i] as string);

packages/data/test/domain/station/service.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ describe('test station service', () => {
5656
expect(resolveUrl.mock.calls.length).toEqual(0);
5757
});
5858

59+
test('should return empty array when unresolved stations storage entry does not exist', () => {
60+
memoryStorage.set('language', 'xy');
61+
62+
const stations = service.getUnresolvedStations();
63+
64+
expect(stations).toEqual([]);
65+
});
66+
5967
test('should store collected percentage for station', async () => {
6068
resolveUrl.mockReturnValue('http://internal.url');
6169
memoryStorage.set('language', 'xy');
@@ -77,6 +85,26 @@ describe('test station service', () => {
7785
await expect(service.updateCollectedPercentage("123", "123", 0.456)).rejects.toThrow();
7886
});
7987

88+
test('should default collectedPercentage to 0 when missing, null, or NaN in storage', async () => {
89+
memoryStorage.set('language', 'xy');
90+
resolveUrl.mockReturnValue('http://internal.url');
91+
92+
const undefinedStation = setStationToStorage(memoryStorage, 'xy');
93+
const nullStation = setStationToStorage(memoryStorage, 'xy');
94+
const nanStation = setStationToStorage(memoryStorage, 'xy');
95+
96+
(memoryStorage.get(`station-xy-${nullStation.id}`) as Station).collectedPercentage = null as unknown as number;
97+
(memoryStorage.get(`station-xy-${nanStation.id}`) as Station).collectedPercentage = NaN;
98+
99+
const loadedUndefined = await service.getStation(undefinedStation.id);
100+
const loadedNull = await service.getStation(nullStation.id);
101+
const loadedNaN = await service.getStation(nanStation.id);
102+
103+
expect(loadedUndefined.collectedPercentage).toBe(0);
104+
expect(loadedNull.collectedPercentage).toBe(0);
105+
expect(loadedNaN.collectedPercentage).toBe(0);
106+
});
107+
80108
test('should remove collected percentage for all stations in language', async () => {
81109
resolveUrl.mockReturnValue('http://internal.url');
82110
memoryStorage.set('language', 'xy');

packages/ui/src/components/station-icon/__snapshots__/station-icon.snapshot.tsx.snap

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,23 @@ exports[`render station icon just above lower limit starts showing progress 1`]
142142
</sc-station-icon>
143143
`;
144144

145+
exports[`render station icon with NaN collectedPercent falls back to 0% progress 1`] = `
146+
<sc-station-icon class="hydrated">
147+
<mock:shadow-root>
148+
<div class="station-icon-wrapper station-icon-size-normal">
149+
<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">
150+
<circle class="progress-ring-track" cx="15" cy="15" r="13.5" stroke-width="3" fill="none"></circle>
151+
<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>
152+
</svg>
153+
<div class="station-icon">
154+
<slot></slot>
155+
</div>
156+
</div>
157+
</mock:shadow-root>
158+
13
159+
</sc-station-icon>
160+
`;
161+
145162
exports[`render station icon with custom lowerLimitPercent above boundary shows progress 1`] = `
146163
<sc-station-icon class="hydrated">
147164
<mock:shadow-root>

packages/ui/src/components/station-icon/station-icon.snapshot.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ test('render station icon with custom lowerLimitPercent above boundary shows pro
5050
const { root } = await render(<sc-station-icon collectedPercent={15} lowerLimitPercent={10}>13</sc-station-icon>);
5151
expect(root).toMatchSnapshot();
5252
});
53+
54+
test('render station icon with NaN collectedPercent falls back to 0% progress', async () => {
55+
const { root } = await render(<sc-station-icon collectedPercent={NaN}>13</sc-station-icon>);
56+
expect(root).toMatchSnapshot();
57+
});

packages/ui/src/components/station-icon/station-icon.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export class StationIcon {
3232
@Prop() size: 'small' | 'normal' | 'large' = 'normal';
3333

3434
private calculatePercent() {
35+
if (!Number.isFinite(this.collectedPercent)) {
36+
return 0;
37+
}
3538
if (this.collectedPercent >= this.upperLimitPercent) {
3639
return 100;
3740
} else if (this.collectedPercent <= this.lowerLimitPercent) {

0 commit comments

Comments
 (0)