mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-26 13:11:53 +08:00
Enhance architecture documentation and update project dependencies. Introduce new features for browser recording, page callables, and transform capabilities. Improve build scripts and permissions for better functionality.
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { BrowserRecordingEvent } from '@/types/models';
|
||||
import {
|
||||
buildRecordingLinks,
|
||||
buildRecordingTraces,
|
||||
mergeRecordingEvents,
|
||||
nextRecordingSequence,
|
||||
} from './timeline';
|
||||
|
||||
function event(
|
||||
id: string,
|
||||
sequence: number,
|
||||
traceId: string,
|
||||
overrides: Partial<BrowserRecordingEvent> = {},
|
||||
): BrowserRecordingEvent {
|
||||
return {
|
||||
id,
|
||||
sequence,
|
||||
timestamp: 1_000 + sequence * 10,
|
||||
recordingId: 'session-1',
|
||||
traceId,
|
||||
kind: 'interaction',
|
||||
operation: 'click',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
sensitiveCaptured: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('browser recording timeline', () => {
|
||||
it('keeps traces and their steps in top-to-bottom chronological order', () => {
|
||||
const events = [
|
||||
event('request', 3, 'trace-login', { kind: 'fetch', operation: 'request', method: 'POST', url: 'https://example.test/login' }),
|
||||
event('second-click', 5, 'trace-search', { label: '查询' }),
|
||||
event('first-click', 1, 'trace-login', { label: '登录' }),
|
||||
event('crypto', 2, 'trace-login', { kind: 'crypto', operation: 'AES.encrypt', crypto: { adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt' } }),
|
||||
event('navigation', 4, 'trace-login', {
|
||||
kind: 'navigation',
|
||||
operation: 'navigation.document',
|
||||
navigation: { phase: 'completed', kind: 'document', toUrl: 'https://example.test/success', sameDocument: false },
|
||||
}),
|
||||
];
|
||||
const traces = buildRecordingTraces(events, []);
|
||||
|
||||
expect(traces.map((trace) => trace.label)).toEqual(['登录', '查询']);
|
||||
expect(traces[0].eventIds).toEqual(['first-click', 'crypto', 'request', 'navigation']);
|
||||
expect(traces[0]).toMatchObject({ requestCount: 1, cryptoCount: 1, navigationCount: 1 });
|
||||
});
|
||||
|
||||
it('updates one navigation boundary instead of duplicating its lifecycle phases', () => {
|
||||
const started = event('navigation', 2, 'trace-login', {
|
||||
kind: 'navigation',
|
||||
operation: 'navigation.document',
|
||||
navigation: { phase: 'started', kind: 'document', fromUrl: 'https://example.test/', toUrl: 'https://example.test/success', sameDocument: false },
|
||||
});
|
||||
const completed = {
|
||||
...started,
|
||||
durationMs: 84,
|
||||
navigation: { ...started.navigation!, phase: 'completed' as const, documentId: 'document-2' },
|
||||
};
|
||||
const merged = mergeRecordingEvents([[started], [completed]]);
|
||||
|
||||
expect(merged).toHaveLength(1);
|
||||
expect(merged[0].navigation?.phase).toBe('completed');
|
||||
expect(merged[0].durationMs).toBe(84);
|
||||
expect(nextRecordingSequence(merged)).toBe(3);
|
||||
expect(buildRecordingTraces(merged, [])[0]).toMatchObject({ startedAt: 1_020, endedAt: 1_104 });
|
||||
});
|
||||
|
||||
it('preserves sensitive previews and the most advanced navigation phase across status snapshots', () => {
|
||||
const sensitive = event('crypto', 1, 'trace-login', {
|
||||
kind: 'crypto',
|
||||
operation: 'AES.encrypt',
|
||||
crypto: { adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt' },
|
||||
sensitiveCaptured: true,
|
||||
inputPreview: 'plain-value',
|
||||
inputs: [{ path: '$input', fingerprint: 'plain', encoding: 'text', byteLength: 11, preview: 'plain-value' }],
|
||||
});
|
||||
const metadataOnly = {
|
||||
...sensitive,
|
||||
sensitiveCaptured: false,
|
||||
inputPreview: undefined,
|
||||
inputs: sensitive.inputs.map(({ preview: _preview, ...item }) => item),
|
||||
};
|
||||
const completedNavigation = event('navigation', 2, 'trace-login', {
|
||||
kind: 'navigation',
|
||||
operation: 'navigation.document',
|
||||
durationMs: 48,
|
||||
navigation: { phase: 'completed', kind: 'document', toUrl: 'https://example.test/success', sameDocument: false },
|
||||
});
|
||||
const pageNavigation = {
|
||||
...completedNavigation,
|
||||
durationMs: undefined,
|
||||
navigation: { ...completedNavigation.navigation!, phase: 'started' as const },
|
||||
};
|
||||
|
||||
const merged = mergeRecordingEvents([[sensitive, completedNavigation], [metadataOnly, pageNavigation]]);
|
||||
expect(merged[0]).toMatchObject({ sensitiveCaptured: true, inputPreview: 'plain-value' });
|
||||
expect(merged[0].inputs[0].preview).toBe('plain-value');
|
||||
expect(merged[1].navigation?.phase).toBe('completed');
|
||||
expect(merged[1].durationMs).toBe(48);
|
||||
});
|
||||
|
||||
it('keeps exact value links separate from chronological navigation edges', () => {
|
||||
const crypto = event('crypto', 1, 'trace-login', {
|
||||
kind: 'crypto',
|
||||
operation: 'AES.encrypt',
|
||||
crypto: { adapterId: 'cryptojs', providerKind: 'library', family: 'symmetric', operation: 'AES.encrypt' },
|
||||
outputs: [{ path: '$output', fingerprint: 'cipher', encoding: 'text', byteLength: 32 }],
|
||||
});
|
||||
const request = event('request', 2, 'trace-login', {
|
||||
kind: 'fetch',
|
||||
operation: 'request',
|
||||
inputs: [{ path: '$body.encryptedData', fingerprint: 'cipher', encoding: 'text', byteLength: 32 }],
|
||||
});
|
||||
const navigation = event('navigation', 3, 'trace-login', {
|
||||
kind: 'navigation',
|
||||
operation: 'navigation.document',
|
||||
navigation: { phase: 'completed', kind: 'document', toUrl: 'https://example.test/success', sameDocument: false },
|
||||
});
|
||||
|
||||
const links = buildRecordingLinks([crypto, request, navigation]);
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0]).toMatchObject({ fromEventId: 'crypto', toEventId: 'request' });
|
||||
});
|
||||
|
||||
it('correlates delayed Worker replies with the originating channel without claiming value equality', () => {
|
||||
const send = event('send', 1, 'trace-worker', {
|
||||
kind: 'worker', operation: 'worker.postMessage', direction: 'send', channelId: 'worker-1',
|
||||
});
|
||||
const receive = event('receive', 2, 'trace-worker', {
|
||||
kind: 'worker', operation: 'worker.message', direction: 'receive', channelId: 'worker-1',
|
||||
});
|
||||
const links = buildRecordingLinks([send, receive]);
|
||||
|
||||
expect(links).toEqual([expect.objectContaining({
|
||||
kind: 'channel', confidence: 'correlated', fromEventId: 'send', toEventId: 'receive',
|
||||
})]);
|
||||
expect(buildRecordingTraces([send, receive], links)[0]).toMatchObject({ messageCount: 2, linkedValueCount: 0 });
|
||||
});
|
||||
|
||||
it('links ordered constructor and session stages without claiming value equality', () => {
|
||||
const state = (phase: 'create' | 'update' | 'final') => ({
|
||||
adapterId: 'jsrsasign',
|
||||
providerKind: 'library' as const,
|
||||
family: 'signature' as const,
|
||||
operation: phase === 'final' ? 'sign' : phase,
|
||||
state: { model: 'session' as const, correlationId: 'signature-session-1', phase },
|
||||
});
|
||||
const create = event('create', 1, 'trace-sign', {
|
||||
kind: 'crypto', operation: 'Signature.create', crypto: state('create'),
|
||||
});
|
||||
const update = event('update', 2, 'trace-sign', {
|
||||
kind: 'crypto', operation: 'Signature.updateString', crypto: state('update'),
|
||||
});
|
||||
const final = event('final', 3, 'trace-sign', {
|
||||
kind: 'crypto', operation: 'Signature.sign', crypto: state('final'),
|
||||
outputs: [{ path: '$output', fingerprint: 'signature', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
const request = event('request', 4, 'trace-sign', {
|
||||
kind: 'fetch', operation: 'request',
|
||||
inputs: [{ path: '$headers.x-signature', fingerprint: 'signature', encoding: 'hex', byteLength: 64 }],
|
||||
});
|
||||
|
||||
const links = buildRecordingLinks([create, update, final, request]);
|
||||
expect(links.filter((link) => link.kind === 'state')).toEqual([
|
||||
expect.objectContaining({ fromEventId: 'create', toEventId: 'update', confidence: 'correlated' }),
|
||||
expect.objectContaining({ fromEventId: 'update', toEventId: 'final', confidence: 'correlated' }),
|
||||
]);
|
||||
expect(links.filter((link) => link.kind === 'value')).toEqual([
|
||||
expect.objectContaining({ fromEventId: 'final', toEventId: 'request', confidence: 'exact' }),
|
||||
]);
|
||||
expect(buildRecordingTraces([create, update, final, request], links)[0].linkedValueCount).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user