blob: 83ef03cd174008257c91a2b5000274f297526621 [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 {afterEach, beforeEach, describe, it} from 'mocha';
import sinon from 'sinon';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.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 {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js';
import {setupSettingsHooks} from '../../testing/SettingsHelpers.js';
import {TestUniverse} from '../../testing/TestUniverse.js';
import {createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js';
import * as Workspace from '../workspace/workspace.js';
import * as Persistence from './persistence.js';
const {urlString} = Platform.DevToolsPath;
describe('FileSystemWorkspaceBinding', () => {
setupLocaleHooks();
setupSettingsHooks();
setupRuntimeHooks();
let universe: TestUniverse;
beforeEach(() => {
universe = new TestUniverse();
});
afterEach(() => {
universe.dispose();
});
it('does not conflict when file system paths share a prefix', async () => {
const fsPath1 = 'file:///var/www';
const fsPath2 = 'file:///var/www_suffix';
const {project: project1, uiSourceCode: fooSourceCode} = createFileSystemUISourceCode({
url: urlString`file:///var/www/foo.js`,
content: 'foo',
fileSystemPath: fsPath1,
mimeType: 'text/javascript',
universe,
});
const {project: project2, uiSourceCode: barSourceCode} = createFileSystemUISourceCode({
url: urlString`file:///var/www_suffix/bar.js`,
content: 'bar',
fileSystemPath: fsPath2,
mimeType: 'text/javascript',
universe,
});
const workspace = universe.workspace;
// Ensure the UISourceCodes are added to the workspace
assert.strictEqual(
workspace.uiSourceCodeForURL(urlString`file:///var/www/foo.js`),
fooSourceCode,
);
assert.strictEqual(
workspace.uiSourceCodeForURL(urlString`file:///var/www_suffix/bar.js`),
barSourceCode,
);
assert.strictEqual(
fooSourceCode.project(),
project1,
'foo.js should be in the first filesystem project',
);
assert.strictEqual(
barSourceCode.project(),
project2,
'bar.js should be in the second filesystem project',
);
// Make sure path isolation works and it does not incorrectly truncate paths.
assert.strictEqual(project1.fileSystemPath(), fsPath1);
assert.strictEqual(project2.fileSystemPath(), fsPath2);
// Update content and ensure it reflects correctly
barSourceCode.setWorkingCopy('Why!?');
const fooContent = await fooSourceCode.requestContentData();
assert.isNotOk(TextUtils.ContentData.ContentData.isError(fooContent));
assert.strictEqual(
(fooContent as TextUtils.ContentData.ContentData).text,
'foo',
);
assert.strictEqual(fooSourceCode.workingCopy(), 'foo');
assert.strictEqual(barSourceCode.workingCopy(), 'Why!?');
});
it('creates file atomically with content', async () => {
const fsPath = 'file:///var/www';
const {project} = createFileSystemUISourceCode({
url: urlString`file:///var/www/existing.js`,
content: 'existing content',
fileSystemPath: fsPath,
mimeType: 'text/javascript',
universe,
});
const platformFileSystem = project.fileSystem();
// Stub createFile to simulate successful file creation on disk.
const createFileStub = sinon.stub(platformFileSystem, 'createFile');
createFileStub.callsFake(
async (
path: Platform.DevToolsPath.EncodedPathString,
name: Platform.DevToolsPath.RawPathString|null,
) => {
return ((path ? path + '/' : '') + name) as Platform.DevToolsPath.EncodedPathString;
},
);
// Stub contentType to return the correct resource type.
const contentTypeStub = sinon.stub(platformFileSystem, 'contentType');
contentTypeStub.returns(Common.ResourceType.resourceTypes.Script);
const workspace = universe.workspace;
const addedPromise = new Promise<Workspace.UISourceCode.UISourceCode>(
resolve => {
const listener = (
event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>,
) => {
const uiSourceCode = event.data;
if (uiSourceCode.url() === 'file:///var/www/new_file.js') {
workspace.removeEventListener(
Workspace.Workspace.Events.UISourceCodeAdded,
listener,
);
resolve(uiSourceCode);
}
};
workspace.addEventListener(
Workspace.Workspace.Events.UISourceCodeAdded,
listener,
);
},
);
const newFileContent = 'new file content';
const uiSourceCode = await project.createFile(
'' as Platform.DevToolsPath.EncodedPathString,
'new_file.js' as Platform.DevToolsPath.RawPathString,
newFileContent,
);
assert.exists(uiSourceCode);
const addedUISourceCode = await addedPromise;
assert.strictEqual(addedUISourceCode, uiSourceCode);
const contentData = await addedUISourceCode.requestContentData();
assert.isFalse(TextUtils.ContentData.ContentData.isError(contentData));
assert.strictEqual(
(contentData as TextUtils.ContentData.ContentData).text,
newFileContent,
);
});
describe('FileSystem', () => {
it('deletes file from PlatformFileSystem and removes it from Project', async () => {
const fileSystemPath = urlString`file:///var/www`;
const fileUrl = urlString`${fileSystemPath}/script.js`;
const {uiSourceCode, project} = createFileSystemUISourceCode({
url: fileUrl,
mimeType: 'text/javascript',
content: 'testme',
fileSystemPath,
universe,
});
const platformFileSystem = project.fileSystem();
const deleteFileStub = sinon.stub(platformFileSystem, 'deleteFile').resolves(true);
assert.lengthOf([...project.uiSourceCodes()], 1);
assert.strictEqual([...project.uiSourceCodes()][0], uiSourceCode);
const workspace = universe.workspace;
const uiSourceCodeRemovedPromise = new Promise<void>(resolve => {
const listener = (
event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>,
) => {
if (event.data === uiSourceCode) {
workspace.removeEventListener(
Workspace.Workspace.Events.UISourceCodeRemoved,
listener,
);
resolve();
}
};
workspace.addEventListener(
Workspace.Workspace.Events.UISourceCodeRemoved,
listener,
);
});
project.deleteFile(uiSourceCode);
await uiSourceCodeRemovedPromise;
sinon.assert.callCount(deleteFileStub, 1);
assert.strictEqual(
deleteFileStub.firstCall.args[0],
'/script.js' as Platform.DevToolsPath.EncodedPathString,
);
assert.lengthOf([...project.uiSourceCodes()], 0);
});
it('does not remove file from Project if PlatformFileSystem deletion fails', async () => {
const fileSystemPath = urlString`file:///var/www`;
const fileUrl = urlString`${fileSystemPath}/script.js`;
const {uiSourceCode, project} = createFileSystemUISourceCode({
url: fileUrl,
mimeType: 'text/javascript',
content: 'testme',
fileSystemPath,
universe,
});
const platformFileSystem = project.fileSystem();
const deleteFileStub = sinon.stub(platformFileSystem, 'deleteFile').resolves(false);
assert.lengthOf([...project.uiSourceCodes()], 1);
project.deleteFile(uiSourceCode);
await deleteFileStub.firstCall.returnValue;
sinon.assert.callCount(deleteFileStub, 1);
assert.lengthOf([...project.uiSourceCodes()], 1);
assert.strictEqual([...project.uiSourceCodes()][0], uiSourceCode);
});
});
describe('with a file system root that has a trailing slash', () => {
const embedderPath = '/tmp/wstest/' as Platform.DevToolsPath.RawPathString;
const changedEmbedderPath = '/tmp/wstest/app.js' as Platform.DevToolsPath.RawPathString;
const fileSystemURL = urlString`file:///tmp/wstest`;
const fileURL = urlString`file:///tmp/wstest/app.js`;
function createFakeDOMFileSystem():
ReturnType<Host.InspectorFrontendHostAPI.InspectorFrontendHostAPI['isolatedFileSystem']> {
const fileEntry = {isFile: true, isDirectory: false, name: 'app.js', fullPath: '/app.js'};
const rootEntry = {
isFile: false,
isDirectory: true,
name: '',
fullPath: '/',
createReader() {
let done = false;
return {
readEntries(successCallback: (entries: unknown[]) => void) {
const entries = done ? [] : [fileEntry];
done = true;
successCallback(entries);
},
};
},
getDirectory(_path: string, _options: unknown, successCallback: (entry: unknown) => void) {
successCallback(rootEntry);
},
};
return {name: 'wstest', root: rootEntry} as unknown as
ReturnType<Host.InspectorFrontendHostAPI.InspectorFrontendHostAPI['isolatedFileSystem']>;
}
async function addFileSystem(manager: Persistence.IsolatedFileSystemManager.IsolatedFileSystemManager):
Promise<Persistence.PlatformFileSystem.PlatformFileSystem> {
const hostInstance = Host.InspectorFrontendHost.InspectorFrontendHostInstance;
sinon.stub(hostInstance, 'isolatedFileSystem').returns(createFakeDOMFileSystem());
const fileSystemAdded = manager.once(Persistence.IsolatedFileSystemManager.Events.FileSystemAdded);
hostInstance.events.dispatchEventToListeners(Host.InspectorFrontendHostAPI.Events.FileSystemAdded, {
fileSystem: {
type: '',
fileSystemName: 'wstest',
rootURL: 'filesystem:devtools://devtools/isolated/wstest',
fileSystemPath: embedderPath,
},
});
return await fileSystemAdded;
}
function removeFileSystem(): void {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.dispatchEventToListeners(
Host.InspectorFrontendHostAPI.Events.FileSystemRemoved, embedderPath);
}
function changeFile(): void {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.dispatchEventToListeners(
Host.InspectorFrontendHostAPI.Events.FileSystemFilesChangedAddedRemoved,
{changed: [changedEmbedderPath], added: [], removed: []});
}
it('does not create a duplicate UISourceCode when an existing file changes', async () => {
const workspace = universe.workspace;
const manager = universe.isolatedFileSystemManager;
const binding = universe.fileSystemWorkspaceBinding;
try {
await addFileSystem(manager);
const [project] = workspace.projectsForType(Workspace.Workspace.projectTypes.FileSystem);
assert.exists(project);
const [uiSourceCode] = project.uiSourceCodes();
assert.exists(uiSourceCode);
changeFile();
assert.deepEqual([...project.uiSourceCodes()], [uiSourceCode],
'the change must be reported to the existing UISourceCode instead of creating a new one');
assert.strictEqual(workspace.uiSourceCodeForURL(fileURL), uiSourceCode);
} finally {
removeFileSystem();
binding.dispose();
manager.dispose();
}
});
it('uses canonical URLs, but keeps the embedder path verbatim', async () => {
const workspace = universe.workspace;
const manager = universe.isolatedFileSystemManager;
const binding = universe.fileSystemWorkspaceBinding;
try {
const fileSystem = await addFileSystem(manager);
assert.strictEqual(fileSystem.path(), fileSystemURL);
assert.strictEqual(fileSystem.embedderPath(), embedderPath);
assert.strictEqual(manager.fileSystem(fileSystemURL), fileSystem);
const project = workspace.project(fileSystemURL) as Persistence.FileSystemWorkspaceBinding.FileSystem;
assert.exists(project);
assert.strictEqual(project.fileSystemPath(), fileSystemURL);
assert.strictEqual(project.fileSystemBaseURL, urlString`file:///tmp/wstest/`);
assert.strictEqual(project.displayName(), 'wstest');
const uiSourceCode = workspace.uiSourceCodeForURL(fileURL);
assert.exists(uiSourceCode);
assert.strictEqual(uiSourceCode.project(), project);
assert.strictEqual(project.fullDisplayName(uiSourceCode), 'wstest/app.js');
assert.deepEqual(Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(uiSourceCode),
['app.js' as Platform.DevToolsPath.EncodedPathString]);
} finally {
removeFileSystem();
binding.dispose();
manager.dispose();
}
});
it('removes the file system when the embedder reports its removal', async () => {
const workspace = universe.workspace;
const manager = universe.isolatedFileSystemManager;
const binding = universe.fileSystemWorkspaceBinding;
try {
await addFileSystem(manager);
assert.lengthOf(manager.fileSystems(), 1);
assert.lengthOf(workspace.projectsForType(Workspace.Workspace.projectTypes.FileSystem), 1);
removeFileSystem();
assert.lengthOf(manager.fileSystems(), 0);
assert.lengthOf(workspace.projectsForType(Workspace.Workspace.projectTypes.FileSystem), 0);
assert.isNull(workspace.uiSourceCodeForURL(fileURL));
} finally {
binding.dispose();
manager.dispose();
}
});
});
});