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
26 changes: 26 additions & 0 deletions src/pages/app-insights/release-insights-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from 'bun:test';
import {
observationBytes,
observationNumber,
observationPercent,
} from './release-insights-format';

describe('release insight missingness', () => {
test('missing is not zero', () => {
expect(observationNumber(null, 'missing')).toBe('missing');
expect(observationNumber(undefined, 'missing')).toBe('missing');
expect(observationNumber(0, 'missing')).toBe('0');
});
test('a real zero hit ratio remains visible', () => {
expect(observationPercent(0, 'missing')).toBe('0.0%');
expect(observationPercent(null, 'missing')).toBe('missing');
});
test('negative savings are not clamped', () => {
expect(observationPercent(-0.5, 'missing')).toBe('-50.0%');
});
test('invalid size is not represented as a free patch', () => {
expect(observationBytes(null, 'missing')).toBe('missing');
expect(observationBytes(0, 'missing')).toBe('missing');
expect(observationBytes(1024, 'missing')).toBe('1.0 KiB');
});
});
28 changes: 28 additions & 0 deletions src/pages/app-insights/release-insights-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Preserve missingness: an absent observation is not an observed zero.
export function observationNumber(
value: number | null | undefined,
missing: string,
): string {
return value == null || !Number.isFinite(value)
? missing
: value.toLocaleString();
}

export function observationPercent(
value: number | null | undefined,
missing: string,
): string {
return value == null || !Number.isFinite(value)
? missing
: `${(value * 100).toFixed(1)}%`;
}

export function observationBytes(
value: number | null | undefined,
missing: string,
): string {
if (value == null || !Number.isFinite(value) || value <= 0) return missing;
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`;
return `${(value / (1024 * 1024)).toFixed(2)} MiB`;
}
98 changes: 98 additions & 0 deletions src/pages/app-insights/release-insights-panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { afterEach, beforeEach, expect, test } from 'bun:test';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cleanup, render, screen } from '@testing-library/react';
import i18n from 'i18next';
import '@/i18n';
import { metricsKeys } from '@/utils/query-keys';
import { ReleaseInsightsPanel } from './release-insights-panel';
import type { ReleaseInsights } from './release-insights-types';

beforeEach(async () => {
global.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as any;
global.ShadowRoot = class {} as any;
await i18n.changeLanguage('en');
});
afterEach(cleanup);
function show(releaseInsights?: ReleaseInsights) {
const client = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity, retry: false } },
});
client.setQueryData(metricsKeys.appVersionFunnel('app', 3), {
days: 3,
versions: [],
releaseInsights,
});
render(
<QueryClientProvider client={client}>
<ReleaseInsightsPanel appKey="app" days={3} />
</QueryClientProvider>,
);
return client;
}
test('older backend renders an upgrade message', () => {
const client = show();
expect(
screen.getByText('This backend has not enabled release insights yet.'),
).not.toBeNull();
client.clear();
});
test('optional API failure renders without throwing', () => {
const client = show({
status: 'unavailable',
timezone: 'UTC',
retentionDays: 14,
artifactRetentionDays: 35,
days: [],
versions: [],
});
expect(
screen.getByText(
'Release insights are unavailable. Existing version statistics remain below.',
),
).not.toBeNull();
client.clear();
});
test('partial day and Hermes metadata render in Chinese with accessible day selector', async () => {
await i18n.changeLanguage('zh-CN');
const client = show({
status: 'available',
timezone: 'UTC',
retentionDays: 14,
artifactRetentionDays: 35,
days: [
{
date: '2026-09-19',
status: 'partial',
limited: true,
requests: 2,
cohorts: [],
deliveries: [],
},
],
versions: [
{
hash: 'target',
name: 'Build',
bytecodeVersion: 96,
baseVersionId: 3,
hermesBaseOutcome: 'used',
hermesBaseDetail: 'verified',
artifactStatus: 'unavailable',
artifactsLimited: false,
artifacts: [],
},
],
});
expect(screen.getByText('已采用')).not.toBeNull();
expect(
screen.getByText('当日曾触及采集上限或部分观测不完整,数量可能偏低。'),
).not.toBeNull();
expect(
screen.getByRole('combobox', { name: '观测日期(UTC)' }),
).not.toBeNull();
client.clear();
});
Loading
Loading