blob: 0343af46ed1598fd74624dfd70fc89d0d9c87b41 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../core/text_utils/text_utils.js';
import type * as Protocol from '../../generated/protocol.js';
import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js';
import {setupSettingsHooks} from '../../testing/SettingsHelpers.js';
import {encodeSourceMap} from '../../testing/SourceMapEncoder.js';
import {TestUniverse} from '../../testing/TestUniverse.js';
import * as Workspace from '../workspace/workspace.js';
import type * as Bindings from './bindings.js';
const {urlString} = Platform.DevToolsPath;
describe('CSSWorkspaceBinding', () => {
setupLocaleHooks();
setupSettingsHooks();
setupRuntimeHooks();
let universe: TestUniverse;
let cssWorkspaceBinding: Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding;
let workspace: Workspace.Workspace.WorkspaceImpl;
const styleSheetURL = urlString`http://example.com/example.css`;
const sourceURLString = urlString`http://example.com/example.scss`;
const scssContent = '/* Comment */\nline 2\nline 3\nline 4\nline 5';
const mappings = [
'1:0 => example.scss:1:0',
'2:4 => example.scss:2:2',
'2:6 => example.scss:2:5',
'2:9 => example.scss:2:7',
'3:7 => example.scss:2:10',
'4:8 => example.scss:4:2',
'4:10 => example.scss:4:2',
'4:11 => example.scss:4:11',
'4:15 => example.scss:4:13',
'4:20 => example.scss:4:17',
];
const sourceMapContent = encodeSourceMap(mappings, 'http://example.com');
beforeEach(() => {
sourceMapContent.sourcesContent = [scssContent];
universe = new TestUniverse({
pageResourceLoaderOptions: {
loadOverride: async (url: string) => {
if (url === 'http://example.com/example.css.map') {
return {
success: true,
content: JSON.stringify(sourceMapContent),
errorDescription: {message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true},
};
}
return {
success: false,
content: '',
errorDescription: {message: 'Not found', statusCode: 404, netError: 0, netErrorName: '', urlValid: true},
};
},
},
});
cssWorkspaceBinding = universe.cssWorkspaceBinding;
workspace = universe.workspace;
});
const waitForUISourceCodeAdded =
(url: Platform.DevToolsPath.UrlString): Promise<Workspace.UISourceCode.UISourceCode> => new Promise(resolve => {
const uiSourceCode = workspace.uiSourceCodeForURL(url);
if (uiSourceCode) {
resolve(uiSourceCode);
return;
}
const {eventType, listener} =
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, event => {
if (event.data.url() === url) {
workspace.removeEventListener(eventType, listener);
resolve(event.data);
}
});
});
it('correctly maps locations using source map', async () => {
const target = universe.createTarget();
const cssModel = target.model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const headerPayload: Protocol.CSS.CSSStyleSheetHeader = {
styleSheetId: 'stylesheet' as Protocol.DOM.StyleSheetId,
frameId: 'frame' as Protocol.Page.FrameId,
sourceURL: styleSheetURL,
origin: 'regular' as Protocol.CSS.StyleSheetOrigin,
title: 'example.css',
disabled: false,
isInline: false,
isMutable: false,
isConstructed: false,
loadingFailed: false,
startLine: 0,
startColumn: 0,
length: 100,
endLine: 10,
endColumn: 10,
sourceMapURL: 'http://example.com/example.css.map',
};
const cssUISourceCodePromise = waitForUISourceCodeAdded(styleSheetURL);
const scssUISourceCodePromise = waitForUISourceCodeAdded(sourceURLString);
cssModel.styleSheetAdded(headerPayload);
const cssUISourceCode = await cssUISourceCodePromise;
const scssUISourceCode = await scssUISourceCodePromise;
assert.exists(cssUISourceCode);
assert.exists(scssUISourceCode);
await cssWorkspaceBinding.pendingLiveLocationChangesPromise();
const header = cssModel.styleSheetHeaderForId(headerPayload.styleSheetId);
assert.exists(header);
const testLocation =
(line: number, column: number, expectedUISourceCode: Workspace.UISourceCode.UISourceCode, expectedLine: number,
expectedColumn: number, expectedReverseLine: number, expectedReverseColumn: number) => {
const rawLocation = new SDK.CSSModel.CSSLocation(header, line, column);
const uiLocation = cssWorkspaceBinding.rawLocationToUILocation(rawLocation);
assert.exists(uiLocation, `Could not map raw location ${line}:${column}`);
assert.strictEqual(uiLocation.uiSourceCode, expectedUISourceCode,
`Mismatched UISourceCode for ${line}:${column}`);
assert.strictEqual(uiLocation.lineNumber, expectedLine, `Mismatched line number for ${line}:${column}`);
assert.strictEqual(uiLocation.columnNumber, expectedColumn, `Mismatched column number for ${line}:${column}`);
const reverseRawLocations = cssWorkspaceBinding.uiLocationToRawLocations(uiLocation);
assert.isNotEmpty(reverseRawLocations,
`Could not reverse map UI location ${uiLocation.lineNumber}:${uiLocation.columnNumber}`);
const reverseRaw = reverseRawLocations[0];
assert.strictEqual(reverseRaw.lineNumber, expectedReverseLine,
`Mismatched reverse line number for ${line}:${column}`);
assert.strictEqual(reverseRaw.columnNumber, expectedReverseColumn,
`Mismatched reverse column number for ${line}:${column}`);
};
testLocation(0, 3, cssUISourceCode, 0, 3, 0, 3);
testLocation(1, 0, scssUISourceCode, 1, 0, 1, 0);
testLocation(2, 4, scssUISourceCode, 2, 2, 2, 4);
testLocation(2, 6, scssUISourceCode, 2, 5, 2, 6);
testLocation(2, 9, scssUISourceCode, 2, 7, 2, 9);
testLocation(3, 7, scssUISourceCode, 2, 10, 3, 7);
testLocation(4, 8, scssUISourceCode, 4, 2, 4, 8);
testLocation(4, 10, scssUISourceCode, 4, 2, 4, 8);
testLocation(4, 11, scssUISourceCode, 4, 11, 4, 11);
testLocation(4, 15, scssUISourceCode, 4, 13, 4, 15);
testLocation(4, 20, scssUISourceCode, 4, 17, 4, 20);
const contentData = await scssUISourceCode.requestContentData();
assert.instanceOf(contentData, TextUtils.ContentData.ContentData);
assert.isTrue(contentData.text.startsWith('/* Comment */'));
});
it('correctly maps property UI locations', async () => {
const target = universe.createTarget();
const cssModel = target.model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const headerPayload: Protocol.CSS.CSSStyleSheetHeader = {
styleSheetId: 'stylesheet' as Protocol.DOM.StyleSheetId,
frameId: 'frame' as Protocol.Page.FrameId,
sourceURL: styleSheetURL,
origin: 'regular' as Protocol.CSS.StyleSheetOrigin,
title: 'example.css',
disabled: false,
isInline: false,
isMutable: false,
isConstructed: false,
loadingFailed: false,
startLine: 0,
startColumn: 0,
length: 100,
endLine: 10,
endColumn: 10,
sourceMapURL: 'http://example.com/example.css.map',
};
const scssUISourceCodePromise = waitForUISourceCodeAdded(sourceURLString);
cssModel.styleSheetAdded(headerPayload);
const scssUISourceCode = await scssUISourceCodePromise;
assert.exists(scssUISourceCode);
await cssWorkspaceBinding.pendingLiveLocationChangesPromise();
const header = cssModel.styleSheetHeaderForId(headerPayload.styleSheetId);
assert.exists(header);
const mockOwnerStyle = {
type: SDK.CSSStyleDeclaration.Type.Regular,
styleSheetId: header.id,
cssModel: () => cssModel,
} as SDK.CSSStyleDeclaration.CSSStyleDeclaration;
const mockProperty = (forName: boolean, line: number, column: number) => {
const range = new TextUtils.TextRange.TextRange(line, column, line, column + 5);
return {
ownerStyle: mockOwnerStyle,
nameRange: () => forName ? range : null,
valueRange: () => !forName ? range : null,
} as SDK.CSSProperty.CSSProperty;
};
const testPropertyUILocation =
(forName: boolean, line: number, column: number, expectedUISourceCode: Workspace.UISourceCode.UISourceCode,
expectedLine: number, expectedColumn: number) => {
const uiLocation = cssWorkspaceBinding.propertyUILocation(mockProperty(forName, line, column), forName);
assert.exists(uiLocation, `Could not map property UI location ${line}:${column}`);
assert.strictEqual(uiLocation.uiSourceCode, expectedUISourceCode,
`Mismatched UISourceCode for ${line}:${column}`);
assert.strictEqual(uiLocation.lineNumber, expectedLine, `Mismatched line number for ${line}:${column}`);
assert.strictEqual(uiLocation.columnNumber, expectedColumn, `Mismatched column number for ${line}:${column}`);
};
// Value mapping (tests nameRange/valueRange extraction combined with rawLocationToUILocation)
testPropertyUILocation(true, 2, 4, scssUISourceCode, 2, 2);
testPropertyUILocation(false, 2, 6, scssUISourceCode, 2, 5);
});
});