blob: a9fb74af7743a089368b198c61efbaabffc12bf4 [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 sinon from 'sinon';
import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as TextUtils from '../../core/text_utils/text_utils.js';
import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
import {MockDebuggerBackend} from '../../testing/MockScopeChain.js';
import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js';
import {
createContentProviderUISourceCode,
createContentProviderUISourceCodes,
createFileSystemUISourceCode,
} from '../../testing/UISourceCodeHelpers.js';
import * as Workspace from '../workspace/workspace.js';
import * as Persistence from './persistence.js';
const {urlString} = Platform.DevToolsPath;
describe('Automapping', () => {
setupLocaleHooks();
setupRuntimeHooks();
let backend: MockDebuggerBackend;
beforeEach(() => {
backend = new MockDebuggerBackend();
// Eagerly instantiate persistence so it registers listeners on the workspace before we add files.
void backend.universe.persistence;
});
it('correctly maps sourcemap sources even when compiled URL matches one of the source URLs', async () => {
const url = urlString`http://example.com/out.js`;
const compiledContent = 'console.log("compiled");';
const sourceContent = 'console.log("source");';
// 1. Create network UISourceCode for compiled script (Script)
const {uiSourceCode: networkScript} = createContentProviderUISourceCode({
url,
mimeType: 'text/javascript',
content: compiledContent,
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-script-project',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
compiledContent.length,
),
universe: backend.universe,
});
// 2. Create network UISourceCode for clashing source (SourceMapScript)
const {
uiSourceCodes: [networkSource],
} = createContentProviderUISourceCodes({
items: [
{
url,
mimeType: 'text/javascript',
content: sourceContent,
resourceType: Common.ResourceType.resourceTypes.SourceMapScript,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
sourceContent.length,
),
},
],
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'sourcemap-project',
universe: backend.universe,
});
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const persistence = backend.universe.persistence;
const bindingCreatedPromise = new Promise<void>(resolve => {
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
if (bindings.length === 2) {
resolve();
}
},
);
});
// 3. Create filesystem UISourceCodes
// Path `/var/www/out.js` (compiled)
const {uiSourceCode: fileSystemScript} = createFileSystemUISourceCode({
url: urlString`file:///var/www/out.js`,
content: compiledContent,
fileSystemPath: 'file:///var/www',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
compiledContent.length,
),
autoMapping: true,
universe: backend.universe,
});
// Path `/var/www/src/out.js` (source)
const {uiSourceCode: fileSystemSource} = createFileSystemUISourceCode({
url: urlString`file:///var/www/src/out.js`,
content: sourceContent,
fileSystemPath: 'file:///var/www',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
sourceContent.length,
),
autoMapping: true,
universe: backend.universe,
});
await bindingCreatedPromise;
assert.lengthOf(bindings, 2);
const scriptBinding = bindings.find(b => b.network === networkScript);
assert.exists(scriptBinding);
assert.strictEqual(scriptBinding?.fileSystem, fileSystemScript);
const sourceBinding = bindings.find(b => b.network === networkSource);
assert.exists(sourceBinding);
assert.strictEqual(sourceBinding?.fileSystem, fileSystemSource);
});
it('is able to map ambiguous resources based on the selected project folder', async () => {
const resetCssContent = '* { margin: 0 }';
const jqueryJsContent = 'window.superb = 1;';
const logo1Content = 'AAAA';
const logo2Content = 'BBBBBBBB';
// 1. Create network UISourceCodes
const {uiSourceCodes: networkSourceCodes} = createContentProviderUISourceCodes({
items: [
{
url: urlString`http://example.com/reset.css`,
mimeType: 'text/css',
content: resetCssContent,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
resetCssContent.length,
),
},
{
url: urlString`http://example.com/jquery.js`,
mimeType: 'text/javascript',
content: jqueryJsContent,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
jqueryJsContent.length,
),
},
{
url: urlString`http://example.com/logo.png`,
mimeType: 'image/png',
content: logo2Content,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
logo2Content.length,
),
},
],
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
universe: backend.universe,
});
const networkResetCss = networkSourceCodes[0];
const networkJqueryJs = networkSourceCodes[1];
const networkLogo = networkSourceCodes[2];
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const persistence = backend.universe.persistence;
const bindingsCreatedPromise = new Promise<void>(resolve => {
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
if (bindings.length === 3) {
resolve();
}
},
);
});
class MyTestFileSystem extends Persistence.FileSystemWorkspaceBinding.FileSystem {
contentMap = new Map<Workspace.UISourceCode.UISourceCode, string>();
constructor(fileSystemPath: string) {
const isolatedFileSystemManager = backend.universe.isolatedFileSystemManager;
const fileSystemWorkspaceBinding = new Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding(
isolatedFileSystemManager,
backend.universe.workspace,
);
class MyTestPlatformFileSystem extends Persistence.PlatformFileSystem.PlatformFileSystem {
constructor() {
super(
urlString`${fileSystemPath}`,
Persistence.PlatformFileSystem.PlatformFileSystemType.WORKSPACE_PROJECT,
false,
);
}
override supportsAutomapping(): boolean {
return true;
}
}
super(
fileSystemWorkspaceBinding,
new MyTestPlatformFileSystem(),
backend.universe.workspace,
);
}
override requestFileContent(
uiSourceCode: Workspace.UISourceCode.UISourceCode,
): Promise<TextUtils.ContentData.ContentDataOrError> {
return Promise.resolve(
new TextUtils.ContentData.ContentData(
this.contentMap.get(uiSourceCode) || '',
false,
'text/plain',
),
);
}
override requestMetadata(
uiSourceCode: Workspace.UISourceCode.UISourceCode,
): Promise<Workspace.UISourceCode.UISourceCodeMetadata|null> {
const length = (this.contentMap.get(uiSourceCode) || '').length;
return Promise.resolve(
new Workspace.UISourceCode.UISourceCodeMetadata(null, length),
);
}
addFileToMap(
url: string,
content: string,
mimeType: string,
): Workspace.UISourceCode.UISourceCode {
const uiSourceCode = this.createUISourceCode(
urlString`${url}`,
Common.ResourceType.ResourceType.fromMimeType(mimeType),
);
this.contentMap.set(uiSourceCode, content);
this.addUISourceCode(uiSourceCode);
return uiSourceCode;
}
}
// 2. Create filesystem UISourceCodes for proj1
const fsProj1 = new MyTestFileSystem('file:///var/www/code/proj1');
fsProj1.addFileToMap(
'file:///var/www/code/proj1/reset.css',
resetCssContent,
'text/css',
);
fsProj1.addFileToMap(
'file:///var/www/code/proj1/jquery.js',
jqueryJsContent,
'text/javascript',
);
fsProj1.addFileToMap(
'file:///var/www/code/proj1/logo.png',
logo1Content,
'image/png',
);
// 3. Create filesystem UISourceCodes for proj2
const fsProj2 = new MyTestFileSystem('file:///var/www/code/proj2');
const fsResetCss2 = fsProj2.addFileToMap(
'file:///var/www/code/proj2/reset.css',
resetCssContent,
'text/css',
);
const fsJqueryJs2 = fsProj2.addFileToMap(
'file:///var/www/code/proj2/jquery.js',
jqueryJsContent,
'text/javascript',
);
const fsLogo2 = fsProj2.addFileToMap(
'file:///var/www/code/proj2/logo.png',
logo2Content,
'image/png',
);
await bindingsCreatedPromise;
assert.lengthOf(bindings, 3);
const logoBinding = bindings.find(b => b.network === networkLogo);
assert.exists(logoBinding);
assert.strictEqual(logoBinding?.fileSystem, fsLogo2);
const resetCssBinding = bindings.find(b => b.network === networkResetCss);
assert.exists(resetCssBinding);
assert.strictEqual(resetCssBinding?.fileSystem, fsResetCss2);
const jqueryJsBinding = bindings.find(b => b.network === networkJqueryJs);
assert.exists(jqueryJsBinding);
assert.strictEqual(jqueryJsBinding?.fileSystem, fsJqueryJs2);
});
it('verify that automapping is sane', async () => {
const clock = sinon.useFakeTimers({toFake: ['setTimeout']});
try {
const timestamp = new Date('December 1, 1989');
const bazContent = 'alert(1);';
// Network resources
const networkResources = [
{
url: urlString`http://example.com`,
mimeType: 'text/html',
content: '<body>this is main resource</body>',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
null,
),
},
{
url: urlString`http://example.com/path/foo.js`,
mimeType: 'text/javascript',
content: 'console.log(\'foo.js!\');',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
{
url: urlString`http://example.com/bar.css?12341234`,
mimeType: 'text/css',
content: '* { box-sizing: border-box }',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
null,
),
},
{
url: urlString`http://example.com/baz.js`,
mimeType: 'text/javascript',
content: bazContent,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
new Date('December 3, 1989'),
null,
),
},
{
url: urlString`http://example.com/images/image.png`,
mimeType: 'image/png',
content: '012345',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
6,
),
},
{
url: urlString`http://example.com/elements/module.json`,
mimeType: 'application/json',
content: 'module descriptor 1',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
{
url: urlString`http://example.com/sources/module.json`,
mimeType: 'application/json',
content: 'module descriptor 2',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
];
const {uiSourceCodes: networkUISourceCodes} = createContentProviderUISourceCodes({
items: networkResources,
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
universe: backend.universe,
});
const [networkIndexHtml,
networkFooJs,
networkBarCss,
networkBazJs,
networkImagePng,
networkElementsJson,
networkSourcesJson,
] = networkUISourceCodes;
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const persistence = backend.universe.persistence;
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
},
);
// Filesystem resources
const fileSystemResources = [
{
url: urlString`file:///var/www/index.html`,
mimeType: 'text/html',
content: '<body>this is main resource</body>',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
null,
),
},
{
url: urlString`file:///var/www/scripts/foo.js`,
mimeType: 'text/javascript',
content: 'console.log(\'foo.js!\');',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
{
url: urlString`file:///var/www/styles/bar.css`,
mimeType: 'text/css',
content: '* { box-sizing: border-box }',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
null,
),
},
{
url: urlString`file:///var/www/scripts/baz.js`,
mimeType: 'text/javascript',
content: bazContent,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
new Date('December 4, 1989'),
null,
),
},
{
url: urlString`file:///var/www/images/image.png`,
mimeType: 'image/png',
content: '0123456789',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
10,
),
},
{
url: urlString`file:///var/www/modules/elements/module.json`,
mimeType: 'application/json',
content: 'module descriptor 1',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
{
url: urlString`file:///var/www/modules/sources/module.json`,
mimeType: 'application/json',
content: 'module descriptor 2',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
];
const fileSystemUISourceCodes = fileSystemResources.map(resource => {
return createFileSystemUISourceCode({
url: resource.url,
mimeType: resource.mimeType,
content: resource.content,
fileSystemPath: 'file:///var/www',
metadata: resource.metadata,
autoMapping: true,
universe: backend.universe,
})
.uiSourceCode;
});
const fileSystemIndexHtml = fileSystemUISourceCodes[0];
const fileSystemFooJs = fileSystemUISourceCodes[1];
const fileSystemBarCss = fileSystemUISourceCodes[2];
const fileSystemElementsJson = fileSystemUISourceCodes[5];
const fileSystemSourcesJson = fileSystemUISourceCodes[6];
await clock.tickAsync(200);
assert.lengthOf(bindings, 5);
const indexHtmlBinding = persistence.binding(networkIndexHtml);
assert.exists(indexHtmlBinding);
assert.strictEqual(indexHtmlBinding?.fileSystem, fileSystemIndexHtml);
const fooBinding = persistence.binding(networkFooJs);
assert.exists(fooBinding);
assert.strictEqual(fooBinding?.fileSystem, fileSystemFooJs);
const barBinding = persistence.binding(networkBarCss);
assert.exists(barBinding);
assert.strictEqual(barBinding?.fileSystem, fileSystemBarCss);
const elementsBinding = persistence.binding(networkElementsJson);
assert.exists(elementsBinding);
assert.strictEqual(elementsBinding?.fileSystem, fileSystemElementsJson);
const sourcesBinding = persistence.binding(networkSourcesJson);
assert.exists(sourcesBinding);
assert.strictEqual(sourcesBinding?.fileSystem, fileSystemSourcesJson);
assert.isNull(persistence.binding(networkBazJs));
assert.isNull(persistence.binding(networkImagePng));
} finally {
clock.restore();
}
});
it('correctly maps and demaps resources as they come and go', async () => {
const url = urlString`http://example.com/path/foo.js`;
const fileURL = urlString`file:///var/www/scripts/foo.js`;
const content = 'console.log(\'foo.js!\');';
const time = new Date('December 1, 1989');
const persistence = backend.universe.persistence;
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const removedBindings: Persistence.Persistence.PersistenceBinding[] = [];
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
},
);
persistence.addEventListener(
Persistence.Persistence.Events.BindingRemoved,
event => {
removedBindings.push(event.data);
},
);
// 1. Add a network resource.
const {project: networkProject, uiSourceCode: networkSourceCode} = createContentProviderUISourceCode({
url,
mimeType: 'text/javascript',
content,
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
time,
content.length,
),
universe: backend.universe,
});
// Wait a bit to ensure no binding is created (since the file system is missing).
await new Promise(resolve => setTimeout(resolve, 50));
assert.lengthOf(bindings, 0);
// 2. Add a file system resource.
const {uiSourceCode: fileSystemSourceCode, project: fileSystemProject} = createFileSystemUISourceCode({
url: fileURL,
content,
fileSystemPath: 'file:///var/www',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
time,
content.length,
),
autoMapping: true,
universe: backend.universe,
});
const waitForBinding = (
bindingList: Persistence.Persistence.PersistenceBinding[],
length: number,
) => {
return new Promise<void>(resolve => {
const check = () => {
if (bindingList.length === length) {
resolve();
} else {
setTimeout(check, 10);
}
};
check();
});
};
// Wait for binding to be created.
await waitForBinding(bindings, 1);
assert.strictEqual(bindings[0].network, networkSourceCode);
assert.strictEqual(bindings[0].fileSystem, fileSystemSourceCode);
// 3. Remove the network resource.
networkProject.removeUISourceCode(url);
// Wait for binding to be removed.
await waitForBinding(removedBindings, 1);
assert.strictEqual(removedBindings[0].network, networkSourceCode);
assert.strictEqual(removedBindings[0].fileSystem, fileSystemSourceCode);
// 4. Re-add the network resource.
const newNetworkSourceCode = networkProject.createUISourceCode(
url,
Common.ResourceType.resourceTypes.Script,
);
const contentProvider = TextUtils.StaticContentProvider.StaticContentProvider.fromString(
url,
Common.ResourceType.resourceTypes.Script,
content,
);
const metadata = new Workspace.UISourceCode.UISourceCodeMetadata(
time,
content.length,
);
networkProject.addUISourceCodeWithProvider(
newNetworkSourceCode,
contentProvider,
metadata,
'text/javascript',
);
// Wait for binding to be created again.
await waitForBinding(bindings, 2);
assert.strictEqual(bindings[1].network, newNetworkSourceCode);
assert.strictEqual(bindings[1].fileSystem, fileSystemSourceCode);
// 5. Remove the file system.
fileSystemProject.dispose();
// Wait for binding to be removed again.
await waitForBinding(removedBindings, 2);
assert.strictEqual(removedBindings[1].network, newNetworkSourceCode);
assert.strictEqual(removedBindings[1].fileSystem, fileSystemSourceCode);
});
it('correctly maps file:// URLs with special characters (URL-encoded paths)', async () => {
const content = 'console.log(\'foo.js!\');';
// Create network resources.
const {uiSourceCodes: networkScriptCodes} = createContentProviderUISourceCodes({
items: [
{
url: urlString`file:///usr/local/node/script%201.js`,
mimeType: 'text/javascript',
content,
},
{
url: urlString`file:///usr/local/node/script%25201.js`,
mimeType: 'text/javascript',
content,
},
],
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
universe: backend.universe,
});
const networkScript1 = networkScriptCodes[0];
const networkScript2 = networkScriptCodes[1];
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const persistence = backend.universe.persistence;
const bindingCreatedPromise = new Promise<void>(resolve => {
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
if (bindings.length === 2) {
resolve();
}
},
);
});
// Create the first file system resource with a URL-encoded path.
const {uiSourceCode: fileSystemScript1} = createFileSystemUISourceCode({
url: urlString`file:///usr/local/node/script%201.js`,
content,
fileSystemPath: 'file:///usr/local/node',
mimeType: 'text/javascript',
autoMapping: true,
universe: backend.universe,
});
// Create the second file system resource with a double URL-encoded path.
const {uiSourceCode: fileSystemScript2} = createFileSystemUISourceCode({
url: urlString`file:///usr/local/node/script%25201.js`,
content,
fileSystemPath: 'file:///usr/local/node',
mimeType: 'text/javascript',
autoMapping: true,
universe: backend.universe,
});
await bindingCreatedPromise;
assert.lengthOf(bindings, 2);
const binding1 = bindings.find(b => b.network === networkScript1);
assert.exists(binding1);
assert.strictEqual(binding1?.fileSystem, fileSystemScript1);
const binding2 = bindings.find(b => b.network === networkScript2);
assert.exists(binding2);
assert.strictEqual(binding2?.fileSystem, fileSystemScript2);
});
it('is capable of mapping file:// urls', async () => {
const url = urlString`file:///usr/local/node/app.js`;
const content = 'console.log(\'foo.js!\');';
// 1. Create network UISourceCode
const {uiSourceCode: networkScript} = createContentProviderUISourceCode({
url,
mimeType: 'text/javascript',
content,
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-script-project',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
universe: backend.universe,
});
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const persistence = backend.universe.persistence;
const bindingCreatedPromise = new Promise<void>(resolve => {
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
resolve();
},
);
});
// 2. Create filesystem UISourceCode
const {uiSourceCode: fileSystemScript} = createFileSystemUISourceCode({
url,
content,
fileSystemPath: 'file:///usr/local/node',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
autoMapping: true,
universe: backend.universe,
});
await bindingCreatedPromise;
assert.lengthOf(bindings, 1);
const scriptBinding = bindings[0];
assert.strictEqual(scriptBinding.network, networkScript);
assert.strictEqual(scriptBinding.fileSystem, fileSystemScript);
});
// Replaces web test: http/tests/devtools/persistence/automapping-bind-dirty-network-sourcecode.js
it('binds dirty network uiSourceCodes to filesystem', async () => {
const url = urlString`http://127.0.0.1:8000/devtools/persistence/resources/foo.js`;
const fileURL = urlString`file:///var/www/devtools/persistence/resources/foo.js`;
const content = 'window.foo = ()=>\'foo\';';
const modifiedContent = 'window.bar = ()=>\'bar\';';
const persistence = backend.universe.persistence;
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
},
);
// 1. Add a network resource.
const {uiSourceCode: networkSourceCode} = createContentProviderUISourceCode(
{
url,
mimeType: 'text/javascript',
content,
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
content.length,
),
universe: backend.universe,
},
);
// Make the network resource dirty.
networkSourceCode.setWorkingCopy(modifiedContent);
// 2. Add a file system resource.
const {uiSourceCode: fileSystemSourceCode} = createFileSystemUISourceCode({
url: fileURL,
content,
fileSystemPath: 'file:///var/www',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
content.length,
),
autoMapping: true,
universe: backend.universe,
});
const waitForBinding = (
bindingList: Persistence.Persistence.PersistenceBinding[],
length: number,
) => {
return new Promise<void>(resolve => {
const check = () => {
if (bindingList.length === length) {
resolve();
} else {
setTimeout(check, 10);
}
};
check();
});
};
// Wait for binding to be created.
await waitForBinding(bindings, 1);
assert.strictEqual(bindings[0].network, networkSourceCode);
assert.strictEqual(bindings[0].fileSystem, fileSystemSourceCode);
// Verify file system is dirty and its working copy is updated.
assert.isTrue(bindings[0].fileSystem.isDirty());
assert.strictEqual(bindings[0].fileSystem.workingCopy(), modifiedContent);
});
it('verify that dirty fileSystem uiSourceCodes are bound to network', async () => {
const clock = sinon.useFakeTimers({toFake: ['setTimeout']});
try {
const url = urlString`http://127.0.0.1:8000/devtools/persistence/resources/foo.js`;
const fileURL = urlString`file:///var/www/devtools/persistence/resources/foo.js`;
const content = '\n\nwindow.foo = ()=>\'foo\';\n';
const dirtyContent = '\n\nwindow.foo = ()=>\'bar\';\n';
const persistence = backend.universe.persistence;
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
},
);
// 1. Create the filesystem UISourceCode.
const {uiSourceCode: fileSystemSourceCode} = createFileSystemUISourceCode(
{
url: fileURL,
content,
fileSystemPath: 'file:///var/www',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
content.length,
),
autoMapping: true,
universe: backend.universe,
},
);
// 2. Make the filesystem UISourceCode dirty.
fileSystemSourceCode.setWorkingCopy(dirtyContent);
assert.isTrue(fileSystemSourceCode.isDirty());
// 3. Create the network UISourceCode.
const {uiSourceCode: networkSourceCode} = createContentProviderUISourceCode({
url,
mimeType: 'text/javascript',
content,
projectType: Workspace.Workspace.projectTypes.Network,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
content.length,
),
universe: backend.universe,
});
await clock.tickAsync(200);
assert.lengthOf(bindings, 1);
assert.strictEqual(bindings[0].network, networkSourceCode);
assert.strictEqual(bindings[0].fileSystem, fileSystemSourceCode);
} finally {
clock.restore();
}
});
it('correctly maps and demaps resources as they are renamed', async () => {
const url = urlString`http://example.com/path/foo.js`;
const fileURL = urlString`file:///var/www/scripts/foo.js`;
const content = 'console.log(\'foo.js!\');';
const time = new Date('December 1, 1989');
const persistence = backend.universe.persistence;
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const removedBindings: Persistence.Persistence.PersistenceBinding[] = [];
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
},
);
persistence.addEventListener(
Persistence.Persistence.Events.BindingRemoved,
event => {
removedBindings.push(event.data);
},
);
// 1. Add a network resource.
const {uiSourceCode: networkSourceCode} = createContentProviderUISourceCode(
{
url,
mimeType: 'text/javascript',
content,
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
time,
content.length,
),
universe: backend.universe,
},
);
// 2. Add a file system resource.
const {uiSourceCode: fileSystemSourceCode, project: fileSystemProject} = createFileSystemUISourceCode({
url: fileURL,
content,
fileSystemPath: 'file:///var/www',
mimeType: 'text/javascript',
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
time,
content.length,
),
autoMapping: true,
universe: backend.universe,
});
const waitForBinding = (
bindingList: Persistence.Persistence.PersistenceBinding[],
length: number,
) => {
return new Promise<void>(resolve => {
const check = () => {
if (bindingList.length === length) {
resolve();
} else {
setTimeout(check, 10);
}
};
check();
});
};
// Wait for binding to be created.
await waitForBinding(bindings, 1);
assert.strictEqual(bindings[0].network, networkSourceCode);
assert.strictEqual(bindings[0].fileSystem, fileSystemSourceCode);
// Stub renameFile on the platform file system to succeed.
const platformFileSystem = fileSystemProject.fileSystem();
const renameStub = sinon.stub(platformFileSystem, 'renameFile').callsFake((path, newName, callback) => {
callback(true, newName);
});
const contentTypeStub =
sinon.stub(platformFileSystem, 'contentType').returns(Common.ResourceType.resourceTypes.Script);
// 3. Rename the file system resource to bar.js.
const newName = 'bar.js' as Platform.DevToolsPath.RawPathString;
const renameSuccess = await fileSystemSourceCode.rename(newName);
assert.isTrue(renameSuccess);
// Wait for binding to be removed.
await waitForBinding(removedBindings, 1);
assert.strictEqual(removedBindings[0].network, networkSourceCode);
assert.strictEqual(removedBindings[0].fileSystem, fileSystemSourceCode);
assert.strictEqual(
fileSystemSourceCode.url(),
urlString`file:///var/www/scripts/bar.js`,
);
// 4. Rename it back to foo.js.
const oldName = 'foo.js' as Platform.DevToolsPath.RawPathString;
const renameSuccess2 = await fileSystemSourceCode.rename(oldName);
assert.isTrue(renameSuccess2);
// Wait for binding to be created again.
await waitForBinding(bindings, 2);
assert.strictEqual(bindings[1].network, networkSourceCode);
assert.strictEqual(bindings[1].fileSystem, fileSystemSourceCode);
assert.strictEqual(fileSystemSourceCode.url(), fileURL);
renameStub.restore();
contentTypeStub.restore();
});
it('correctly maps sourcemap sources with non-exact match', async () => {
const cssContent = 'body { color: red; }';
const scssContent = '$color: red; body { color: $color; }';
class MultiFileTestPlatformFileSystem extends Persistence.PlatformFileSystem.PlatformFileSystem {
readonly #autoMapping: boolean;
readonly #files = new Set<Platform.DevToolsPath.UrlString>();
constructor(path: Platform.DevToolsPath.UrlString, autoMapping: boolean) {
super(
path,
Persistence.PlatformFileSystem.PlatformFileSystemType.WORKSPACE_PROJECT,
false,
);
this.#autoMapping = autoMapping;
}
addFile(url: Platform.DevToolsPath.UrlString): void {
this.#files.add(url);
}
override supportsAutomapping(): boolean {
return this.#autoMapping;
}
override mimeFromPath(path: Platform.DevToolsPath.UrlString): string {
const ext = Common.ParsedURL.ParsedURL.extractExtension(path);
return Common.ResourceType.mimeTypeByExtension.get(ext) || 'text/plain';
}
override searchInPath(
_query: string,
_progress: Common.Progress.Progress,
): Promise<string[]> {
return Promise.resolve([...this.#files]);
}
}
class MultiFileTestFileSystem extends Persistence.FileSystemWorkspaceBinding.FileSystem {
readonly #files = new Map<string, {content: string, metadata: Workspace.UISourceCode.UISourceCodeMetadata}>();
addMockFile(
url: string,
content: string,
metadata: Workspace.UISourceCode.UISourceCodeMetadata,
): void {
this.#files.set(url, {content, metadata});
}
override requestFileContent(
uiSourceCode: Workspace.UISourceCode.UISourceCode,
): Promise<TextUtils.ContentData.ContentDataOrError> {
const file = this.#files.get(uiSourceCode.url());
if (!file) {
return Promise.resolve({error: 'File not found'});
}
return Promise.resolve(
new TextUtils.ContentData.ContentData(
file.content,
/* isBase64 */ false,
'text/plain',
),
);
}
override requestMetadata(
uiSourceCode: Workspace.UISourceCode.UISourceCode,
): Promise<Workspace.UISourceCode.UISourceCodeMetadata|null> {
const file = this.#files.get(uiSourceCode.url());
return Promise.resolve(file ? file.metadata : null);
}
}
// 1. Create network project with both CSS and SCSS files
const {
uiSourceCodes: [networkCSS, networkSCSS],
} = createContentProviderUISourceCodes({
items: [
{
url: urlString`http://example.com/assets/s.css`,
mimeType: 'text/css',
content: cssContent,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(
null,
cssContent.length,
),
},
{
url: urlString`http://example.com/assets/s.scss`,
mimeType: 'text/x-scss',
content: scssContent,
resourceType: Common.ResourceType.resourceTypes.SourceMapStyleSheet,
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, null),
},
],
projectType: Workspace.Workspace.projectTypes.Network,
projectId: 'network-project',
universe: backend.universe,
});
const bindings: Persistence.Persistence.PersistenceBinding[] = [];
const persistence = backend.universe.persistence;
const bindingCreatedPromise = new Promise<void>(resolve => {
persistence.addEventListener(
Persistence.Persistence.Events.BindingCreated,
event => {
bindings.push(event.data);
if (bindings.length === 2) {
resolve();
}
},
);
});
const timestamp = new Date('December 1, 1989');
// 2. Create filesystem project with both CSS and SCSS files
const workspace = backend.universe.workspace;
const isolatedFileSystemManager = backend.universe.isolatedFileSystemManager;
const fileSystemWorkspaceBinding = new Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding(
isolatedFileSystemManager,
workspace,
);
const fileSystemPath = urlString`file:///var/www`;
const platformFileSystem = new MultiFileTestPlatformFileSystem(
fileSystemPath,
true,
);
const project = new MultiFileTestFileSystem(
fileSystemWorkspaceBinding,
platformFileSystem,
workspace,
);
// Add dist/s.css
const cssUrl = urlString`file:///var/www/dist/s.css`;
platformFileSystem.addFile(cssUrl);
const cssMetadata = new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
cssContent.length,
);
project.addMockFile(cssUrl, cssContent, cssMetadata);
const fileSystemCSS = project.createUISourceCode(
cssUrl,
Common.ResourceType.ResourceType.fromMimeType('text/css'),
);
project.addUISourceCode(fileSystemCSS);
// Add src/s.scss
const scssUrl = urlString`file:///var/www/src/s.scss`;
platformFileSystem.addFile(scssUrl);
const scssMetadata = new Workspace.UISourceCode.UISourceCodeMetadata(
timestamp,
scssContent.length,
);
project.addMockFile(scssUrl, scssContent, scssMetadata);
const fileSystemSCSS = project.createUISourceCode(
scssUrl,
Common.ResourceType.ResourceType.fromMimeType('text/x-scss'),
);
project.addUISourceCode(fileSystemSCSS);
await bindingCreatedPromise;
assert.lengthOf(bindings, 2);
const cssBinding = bindings.find(b => b.network === networkCSS);
assert.exists(cssBinding);
assert.strictEqual(cssBinding?.fileSystem, fileSystemCSS);
const scssBinding = bindings.find(b => b.network === networkSCSS);
assert.exists(scssBinding);
assert.strictEqual(scssBinding?.fileSystem, fileSystemSCSS);
});
});