import { beforeEach, afterEach, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import type { ComponentProps } from 'svelte';
import { writable, type Writable } from 'svelte/store';
import { page } from '$app/stores';
import ActivityLog from '../../src/lib/ui/components/ActivityLog.svelte';
import ActivityPage from '../../src/routes/activity/+page.svelte';
import {
activityApiHref,
activityHistoryHref,
activityQuery,
activityTimeLabel
} from '../../src/lib/ui/activity-history.js';
import type { ActivityPage as ActivityReport, ActivityRow } from '../../src/lib/activity.js';
vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/activity') }) }));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$ui/stores/fleet.js', () => ({
fleet: writable({ servers: [], operations: [], receivedAt: 0 })
}));
vi.mock('$ui/stores/servers.js', () => ({ uiNow: writable(Date.parse('2026-10-07T18:00:00Z')) }));
const row: ActivityRow = {
id: 'operation-1',
at: '2026-10-07T17:59:45Z',
type: 'restart',
source: 'maintenance',
sources: ['maintenance', 'campaign'],
state: 'succeeded',
rawState: 'done',
title: 'Restart server',
detail: 'Verified game readiness.',
server: 't6_zombie_town_1',
serverDeletedAt: null,
hostId: 'host-a',
hostName: 'Dallas 1',
href: '/provisioning/operation-1'
};
const report = (): ActivityReport => ({
rows: [{ ...row }],
total: 52,
limit: 25,
offset: 0,
query: { limit: 25, offset: 0 }
});
function scope(path = '/activity') {
(page as unknown as Writable<{ url: URL }>).set({ url: new URL(`http://localhost${path}`) });
}
beforeEach(() => {
scope();
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-10-07T18:00:00Z'));
});
afterEach(() => vi.useRealTimers());
it.each([
['', '0', 'All retained history'],
['0', '0', 'All retained history'],
['1', '1', 'Last 24 hours'],
['7', '7', 'Last 7 days'],
['30', '30', 'Last 30 days'],
['90', '90', 'Last 90 days'],
['14', '14', 'Last 14 days']
])('selects the current date range for days=%s on desktop and mobile', (days, value, label) => {
scope(days ? `/activity?days=${days}` : '/activity');
const body = render(ActivityLog, { props: { initial: report() } }).body;
for (const suffix of ['', '-mobile']) {
const select = body.match(
new RegExp(``)
)?.[1];
expect(select).toBeDefined();
expect(select).toContain(``);
expect(select?.match(/\bselected=""/g)).toHaveLength(1);
}
});
it('sends only supported normalized filters and enforces scoped identities', () => {
const url = new URL(
'https://example.test/servers/town?tab=history&server=wrong&source=host&state=failed&offset=50&returnTo=%2F'
);
const href = activityApiHref(url, { server: 't6_zombie_town_1', source: 'maintenance' });
const params = new URL(href, url).searchParams;
expect(params.get('server')).toBe('t6_zombie_town_1');
expect(params.get('source')).toBe('maintenance');
expect(params.get('state')).toBe('failed');
expect(params.get('offset')).toBe('50');
expect(params.get('limit')).toBe('25');
expect(params.has('tab')).toBe(false);
expect(params.has('returnTo')).toBe(false);
expect(
activityQuery(new URL('https://example.test/activity?type=unknown&days=0')).type
).toBeUndefined();
});
it('resets pagination when filtering and preserves the containing page, hash, and return context', () => {
const current = new URL(
'https://example.test/servers/town?tab=history&offset=50&source=campaign&returnTo=%2F%3Ffilter%3Dattention#history'
);
const filtered = new URL(
activityHistoryHref(current, { state: 'failed', q: 'Dallas lobby' }),
current
);
expect(filtered.searchParams.get('offset')).toBeNull();
expect(filtered.searchParams.get('tab')).toBe('history');
expect(filtered.searchParams.get('source')).toBe('campaign');
expect(filtered.searchParams.get('returnTo')).toBe('/?filter=attention');
expect(filtered.hash).toBe('#history');
expect(filtered.searchParams.get('q')).toBe('Dallas lobby');
expect(
new URL(activityHistoryHref(filtered, { offset: '25' }), current).searchParams.get('state')
).toBe('failed');
});
it('keeps provisioning excluded from log polling after filtering or clearing filters', () => {
const url = new URL('http://localhost/activity?type=create&excludeProvisioning=false');
const scope = { excludeProvisioning: true };
expect(new URL(activityApiHref(url, scope), url).searchParams.get('excludeProvisioning')).toBe(
'true'
);
const cleared = new URL(activityHistoryHref(url, { type: '', source: '', offset: '' }), url);
expect(activityQuery(cleared, scope).excludeProvisioning).toBe(true);
expect(
activityQuery(new URL('http://localhost/servers/town?tab=history')).excludeProvisioning
).toBeUndefined();
});
it('keeps maintenance excluded from log polling while preserving the Maintenance history scope', () => {
const url = new URL('http://localhost/activity?source=maintenance&excludeMaintenance=false');
const logScope = { excludeProvisioning: true, excludeMaintenance: true };
expect(
new URL(activityApiHref(url, logScope), url).searchParams.get('excludeMaintenance')
).toBe('true');
const cleared = new URL(activityHistoryHref(url, { type: '', source: '', offset: '' }), url);
expect(activityQuery(cleared, logScope).excludeMaintenance).toBe(true);
const maintenance = new URL('http://localhost/maintenance?view=activity');
const query = new URL(
activityApiHref(maintenance, { source: 'maintenance', excludeMaintenance: false }),
maintenance
).searchParams;
expect(query.get('source')).toBe('maintenance');
expect(query.has('excludeMaintenance')).toBe(false);
});
it('shows relative recent timestamps and preserves accurate older dates and invalid evidence', () => {
const now = Date.parse('2026-10-07T18:00:00Z');
expect(activityTimeLabel('2026-10-07 17:59:45', 'America/New_York', now)).toBe('15 s ago');
expect(activityTimeLabel('2026-10-07T17:50:00Z', 'America/New_York', now)).toBe('10 min ago');
expect(activityTimeLabel('2026-10-07T16:00:00Z', 'America/New_York', now)).toBe('2 h ago');
expect(activityTimeLabel('2026-10-06T17:00:00Z', 'America/New_York', now)).toBe(
'Yesterday, 1:00 PM'
);
expect(activityTimeLabel('not a date', 'UTC', now)).toBe('Time unavailable');
expect(activityTimeLabel('2026-10-08T18:00:00Z', 'America/New_York', now)).toBe(
'Oct 8, 2026, 2:00 PM EDT'
);
});
it('renders one page heading with provisioning management and filters removed', () => {
const body = render(ActivityPage, {
props: {
data: {
activity: report(),
user: { role: 'admin' },
setupRequired: false
} as ComponentProps['data']
}
}).body;
expect(body.match(/]/g)).toHaveLength(1);
expect(body).not.toContain('Manage servers');
expect(body).not.toContain('href="/provisioning"');
expect(body).not.toContain('