blob: 52f6e1b7300477309012a84a14a1e488769dee46 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as Protocol from '../../generated/protocol.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {SnapshotTester} from '../../testing/SnapshotTester.js';
import {TraceLoader} from '../../testing/TraceLoader.js';
import * as Common from '../common/common.js';
import type {Message} from '../protocol_client/InspectorBackend.js';
import type {
RehydratingExecutionContext, RehydratingResource, RehydratingScript, RehydratingTarget, ServerMessage} from
'./RehydratingObject.js';
import * as SDK from './sdk.js';
const mockTarget1: RehydratingTarget = {
targetId: 'ABCDE' as Protocol.Target.TargetID,
type: 'page',
isolate: '12345',
url: 'example.com',
pid: 12345,
};
const mockExecutionContext1: RehydratingExecutionContext = {
id: 1 as Protocol.Runtime.ExecutionContextId,
origin: 'example.com',
v8Context: 'example context 1',
name: 'example context 1',
uniqueId: 'example context 1',
auxData: {
frameId: 'ABCDE' as Protocol.Page.FrameId,
isDefault: true,
type: 'type',
},
isolate: '12345',
};
const mockExecutionContext2: RehydratingExecutionContext = {
id: 2 as Protocol.Runtime.ExecutionContextId,
origin: 'example.com',
v8Context: 'example context 2',
name: 'example context 2',
uniqueId: 'example context 2',
auxData: {
frameId: 'ABCDE' as Protocol.Page.FrameId,
isDefault: true,
type: 'type',
},
isolate: '12345',
};
const mockScript1: RehydratingScript = {
scriptId: '1' as Protocol.Runtime.ScriptId,
isolate: '12345',
pid: 12345,
executionContextId: 1 as Protocol.Runtime.ExecutionContextId,
startLine: 0,
startColumn: 0,
endLine: 1,
endColumn: 10,
hash: '',
isModule: false,
url: 'example.com', // inline
hasSourceURL: false,
sourceMapURL: undefined,
length: 10,
sourceText: 'source text 1',
executionContextAuxData: {
frameId: 'ABCDE' as Protocol.Page.FrameId,
isDefault: true,
type: 'type',
},
buildId: ''
};
const mockScript2: RehydratingScript = {
scriptId: '2' as Protocol.Runtime.ScriptId,
isolate: '12345',
pid: 12345,
executionContextId: 2 as Protocol.Runtime.ExecutionContextId,
startLine: 0,
startColumn: 0,
endLine: 1,
endColumn: 10,
hash: '',
isModule: false,
url: 'example.com/script.js',
hasSourceURL: false,
sourceMapURL: undefined,
length: 10,
sourceText: 'source text 2',
executionContextAuxData: {
frameId: 'ABCDE' as Protocol.Page.FrameId,
isDefault: true,
type: 'type',
},
buildId: ''
};
const mockResource: RehydratingResource = {
url: 'example.com',
frame: 'ABCDE',
content: '<html>',
mimeType: 'text/html',
};
describe('RehydratingSession', () => {
const sessionId = 1;
const messageId = 1;
const target = mockTarget1;
let mockRehydratingConnection: MockRehydratingConnection;
let mockRehydratingSession: SDK.RehydratingConnection.RehydratingSession;
const executionContextsForTarget1 = [mockExecutionContext1, mockExecutionContext2];
const scriptsForTarget1 = [mockScript1, mockScript2];
const resourcesForTarget1 = [mockResource];
class MockRehydratingConnection implements SDK.RehydratingConnection.RehydratingConnectionInterface {
messageQueue: ServerMessage[] = [];
postToFrontend(arg: ServerMessage): void {
this.messageQueue.push(arg);
}
clearMessageQueue(): void {
this.messageQueue = [];
}
}
class RehydratingSessionForTest extends SDK.RehydratingConnection.RehydratingSession {
override sendMessageToFrontend(payload: ServerMessage): void {
this.connection?.postToFrontend(payload);
}
}
beforeEach(() => {
mockRehydratingConnection = new MockRehydratingConnection();
mockRehydratingSession = new RehydratingSessionForTest(
sessionId, target, executionContextsForTarget1, scriptsForTarget1, resourcesForTarget1,
mockRehydratingConnection);
mockRehydratingSession.declareSessionAttachedToTarget();
});
it('send attach to target on construction', async function() {
const attachToTargetMessage = mockRehydratingConnection.messageQueue[0];
assert.isNotNull(attachToTargetMessage);
assert.strictEqual(attachToTargetMessage.method, 'Target.attachedToTarget');
assert.strictEqual(
(attachToTargetMessage.params as Protocol.Target.AttachedToTargetEvent).sessionId.toString(),
sessionId.toString());
assert.strictEqual(
(attachToTargetMessage.params as Protocol.Target.AttachedToTargetEvent).targetInfo.targetId.toString(),
target.targetId.toString());
});
it('sends execution context created while handling runtime enable', async function() {
mockRehydratingConnection.clearMessageQueue();
mockRehydratingSession.handleFrontendMessageAsFakeCDPAgent({
id: messageId,
method: 'Runtime.enable',
sessionId,
});
assert.lengthOf(mockRehydratingConnection.messageQueue, 3);
const executionContextCreatedMessages = mockRehydratingConnection.messageQueue.slice(0, 2);
const resultMessage = mockRehydratingConnection.messageQueue.slice(2);
for (const executionContextCreatedMessage of executionContextCreatedMessages) {
assert.strictEqual(executionContextCreatedMessage.method, 'Runtime.executionContextCreated');
assert.strictEqual(
(executionContextCreatedMessage.params as Protocol.Runtime.ExecutionContextCreatedEvent)
.context.auxData.frameId,
target.targetId);
}
assert.isNotNull(resultMessage[0]);
assert.strictEqual(resultMessage[0].id, messageId);
});
it('sends script source text while handling get script source', async function() {
mockRehydratingConnection.clearMessageQueue();
mockRehydratingSession.handleFrontendMessageAsFakeCDPAgent({
id: messageId,
method: 'Debugger.getScriptSource',
sessionId,
params: {
scriptId: mockScript1.scriptId,
},
});
assert.lengthOf(mockRehydratingConnection.messageQueue, 1);
const scriptSourceTextMessage = mockRehydratingConnection.messageQueue[0];
assert.isNotNull(scriptSourceTextMessage);
assert.strictEqual(scriptSourceTextMessage.id, messageId);
assert.strictEqual(
(scriptSourceTextMessage.result as Protocol.Debugger.GetScriptSourceResponse).scriptSource,
mockScript1.sourceText);
});
});
describeWithEnvironment('RehydratingConnection emittance', function() {
const snapshotTester = new SnapshotTester(this, import.meta);
before(async () => {
// Create fake popup opener as rehydrating connection needs it.
window.opener = {
postMessage: sinon.stub(),
};
});
after(async () => {
delete window.opener;
});
it('emits the expected CDP data', async function() {
const contents = await TraceLoader.fixtureContents(this, 'enhanced-paul.json.gz');
const reveal = sinon.stub(Common.Revealer.RevealerRegistry.prototype, 'reveal').resolves();
const messageLog: Array<string|Message> = [];
const conn = new SDK.RehydratingConnection.RehydratingConnectionTransport((e: string) => {
throw new Error(`Connection lost: ${e}`);
});
// Impractical to invoke the real devtools frontend, so we fake the 3 CDP handlers that
// `RehydratingSession.handleFrontendMessageAsFakeCDPAgent` cares about
let id = 1;
const fakeDevToolsFrontend = (arg0: Message|string): void => {
const message = ((typeof arg0 === 'string') ? JSON.parse(arg0) : arg0) as Message;
messageLog.push('RehydratingConnection says:', message);
if (message.method === 'Target.attachedToTarget') {
const attachedParams = message.params as Protocol.Target.AttachedToTargetEvent;
const sessionId = attachedParams.sessionId;
conn.sendRawMessage({id: id++, sessionId, method: 'Runtime.enable'});
conn.sendRawMessage({id: id++, sessionId, method: 'Debugger.enable'});
}
if (message.method === 'Debugger.scriptParsed') {
const scriptParsedParams = message.params as Protocol.Debugger.ScriptParsedEvent;
const sessionId = message.sessionId;
conn.sendRawMessage(
{id: id++, sessionId, method: 'Debugger.getScriptSource', params: {scriptId: scriptParsedParams.scriptId}});
}
};
conn.setOnMessage(fakeDevToolsFrontend);
const oldSendRawMessage = conn.sendRawMessage;
conn.sendRawMessage = (message: Message) => {
messageLog.push('fakeDevToolsFrontend says:', message);
oldSendRawMessage.call(conn, message);
};
// Kick off the rehydration process
conn.onReceiveHostWindowPayload({
data: {type: 'REHYDRATING_TRACE_FILE', traceJson: JSON.stringify(contents)},
} as MessageEvent);
// Poll for rehydration complete
const poll = async () => {
const isRehydrated =
conn.rehydratingConnectionState === SDK.RehydratingConnection.RehydratingConnectionState.REHYDRATED;
const messageLogPopulated = messageLog.length > 100; // This trace ends up with 158.
if (isRehydrated && messageLogPopulated) {
return;
}
await new Promise<void>(res => setTimeout(res, 100));
void poll();
};
await poll();
/** Elide any script sources in front_end/core/sdk/RehydratingConnection.snapshot.txt **/
function sanitizeLog(m: string|Message): string {
if (typeof m === 'object' && m.params?.sourceText) {
m.params.sourceText = m.params.sourceText.slice(0, 20) + '…';
}
if (typeof m === 'object' && m.params?.sourceMapURL && m.params.sourceMapURL.length > 200) {
m.params.sourceMapURL = m.params.sourceMapURL.slice(0, 200) + '…';
}
// @ts-expect-error
if (typeof m === 'object' && m.result?.scriptSource) {
// @ts-expect-error
m.result.scriptSource = m.result.scriptSource.slice(0, 20) + '…';
}
return typeof m === 'string' ? `\n/* ${m} */` : JSON.stringify(m, null, 2);
}
const sanitizedLog = messageLog.map(sanitizeLog).join('\n');
snapshotTester.assert(this, sanitizedLog);
sinon.assert.calledOnce(reveal);
});
});