blob: 9be609d6dd8ddf1b579294cf19c2d5198d3cb42f [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 {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js';
import * as Workspace from '../workspace/workspace.js';
const {urlString} = Platform.DevToolsPath;
describeWithEnvironment('FileSystemWorkspaceBinding', () => {
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',
});
const {project: project2, uiSourceCode: barSourceCode} = createFileSystemUISourceCode({
url: urlString`file:///var/www_suffix/bar.js`,
content: 'bar',
fileSystemPath: fsPath2,
mimeType: 'text/javascript',
});
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
// 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',
});
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 = Workspace.Workspace.WorkspaceImpl.instance();
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,
});
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 = Workspace.Workspace.WorkspaceImpl.instance();
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,
});
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);
});
});
});