130 lines
5.4 KiB
TypeScript
130 lines
5.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { cleanup, render, screen, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { InstanceContext } from '../app-shell.js';
|
|
import { CallsModule, type CallsDataSource, type CallsSnapshot } from './calls-module.js';
|
|
|
|
afterEach(cleanup);
|
|
|
|
const owner: InstanceContext = {
|
|
id: 'alpha',
|
|
name: 'Alpha',
|
|
origin: 'https://alpha.example',
|
|
status: 'online',
|
|
authentication: 'authenticated',
|
|
freshness: 'fresh',
|
|
};
|
|
|
|
const snapshot: CallsSnapshot = {
|
|
observedAt: '2026-07-17T12:30:00Z',
|
|
calls: { state: 'active', active: 2, ringing: 1, held: 0 },
|
|
devices: { state: 'available', total: 4, online: 3, busy: 2 },
|
|
};
|
|
|
|
function deferredSource() {
|
|
let resolvePending!: (value: CallsSnapshot) => void;
|
|
const load = vi.fn<CallsDataSource['load']>(
|
|
(_instanceId, _signal) =>
|
|
new Promise((done) => {
|
|
void _instanceId;
|
|
void _signal;
|
|
resolvePending = done;
|
|
}),
|
|
);
|
|
return { source: { load }, load, resolve: (value: CallsSnapshot) => resolvePending(value) };
|
|
}
|
|
|
|
describe('isolated Calls read module', () => {
|
|
it('loads only through the injected source and renders aggregate call and device status', async () => {
|
|
const pending = deferredSource();
|
|
render(<CallsModule instance={owner} dataSource={pending.source} />);
|
|
expect(screen.getByRole('status', { name: '通话加载状态' })).toBeTruthy();
|
|
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
|
|
pending.resolve(snapshot);
|
|
|
|
const calls = await screen.findByRole('region', { name: '通话状态' });
|
|
const devices = screen.getByRole('region', { name: '设备状态' });
|
|
expect(within(calls).getByText('活跃')).toBeTruthy();
|
|
expect(within(calls).getByText('2')).toBeTruthy();
|
|
expect(within(devices).getByText('可用')).toBeTruthy();
|
|
expect(screen.getByText(/观测时间:2026-07-17T12:30:00Z/)).toBeTruthy();
|
|
});
|
|
|
|
it('strictly allowlists aggregate fields and exposes no phone numbers, audio, logs, or controls', async () => {
|
|
const unsafe = {
|
|
...snapshot,
|
|
phoneNumber: '+1-555-0100',
|
|
audio: 'private recording body',
|
|
logBody: 'private call log body',
|
|
calls: { ...snapshot.calls, caller: '+1-555-0101', transcript: 'private transcript' },
|
|
devices: { ...snapshot.devices, phoneNumber: '+1-555-0102', audioUrl: 'secret-audio-url' },
|
|
} as CallsSnapshot;
|
|
render(<CallsModule instance={owner} dataSource={{ load: async () => unsafe }} />);
|
|
expect(await screen.findByText('可用')).toBeTruthy();
|
|
expect(document.body.textContent).not.toMatch(/555|private|secret-audio/i);
|
|
expect(screen.queryByRole('button')).toBeNull();
|
|
});
|
|
|
|
it('fails closed without authentication or an injected source', async () => {
|
|
const load = vi.fn<CallsDataSource['load']>().mockResolvedValue(snapshot);
|
|
const { rerender } = render(
|
|
<CallsModule
|
|
instance={{ ...owner, authentication: 'auth-required' }}
|
|
dataSource={{ load }}
|
|
/>,
|
|
);
|
|
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
|
|
expect(load).not.toHaveBeenCalled();
|
|
|
|
rerender(<CallsModule instance={owner} />);
|
|
expect(screen.getByRole('status', { name: '通话不可用' }).textContent).toMatch(
|
|
/没有可用的安全通话只读数据源.*不会臆造.*生产端点/i,
|
|
);
|
|
});
|
|
|
|
it('uses fixed safe errors, retries, and retains the same owner snapshot on refresh failure', async () => {
|
|
const user = userEvent.setup();
|
|
const load = vi
|
|
.fn<CallsDataSource['load']>()
|
|
.mockResolvedValueOnce(snapshot)
|
|
.mockRejectedValueOnce(new Error('secret endpoint and telephone'))
|
|
.mockResolvedValueOnce({ ...snapshot, calls: { ...snapshot.calls, active: 3 } });
|
|
const source = { load };
|
|
const { rerender } = render(
|
|
<CallsModule instance={owner} dataSource={source} refreshSignal={0} />,
|
|
);
|
|
expect(await screen.findByText('可用')).toBeTruthy();
|
|
rerender(<CallsModule instance={owner} dataSource={source} refreshSignal={1} />);
|
|
|
|
const alert = await screen.findByRole('alert');
|
|
expect(alert.textContent).toMatch(/正在显示上次已知的通话数据/i);
|
|
expect(alert.textContent).not.toMatch(/secret endpoint|telephone/i);
|
|
expect(screen.getByText('可用')).toBeTruthy();
|
|
await user.click(screen.getByRole('button', { name: '重试加载通话' }));
|
|
expect(
|
|
await within(screen.getByRole('region', { name: '通话状态' })).findByText('3'),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it('aborts superseded reads and fences late data from another owner', async () => {
|
|
const alpha = deferredSource();
|
|
const bravo = deferredSource();
|
|
const load = vi.fn<CallsDataSource['load']>((id, signal) =>
|
|
id === 'alpha' ? alpha.source.load(id, signal) : bravo.source.load(id, signal),
|
|
);
|
|
const source = { load };
|
|
const { rerender } = render(<CallsModule instance={owner} dataSource={source} />);
|
|
const alphaSignal = load.mock.calls[0]?.[1];
|
|
rerender(
|
|
<CallsModule instance={{ ...owner, id: 'bravo', name: 'Bravo' }} dataSource={source} />,
|
|
);
|
|
expect(alphaSignal?.aborted).toBe(true);
|
|
alpha.resolve({ ...snapshot, calls: { state: 'late-alpha' } });
|
|
bravo.resolve({ ...snapshot, calls: { state: 'current-bravo' } });
|
|
expect(await screen.findByText('current-bravo')).toBeTruthy();
|
|
expect(screen.queryByText('late-alpha')).toBeNull();
|
|
});
|
|
});
|